使用注解生成代码

Velocity生成器使用方法

现在我们决定使用Velocity来升级我们的生成器,我们需要按照下面的步骤进行重新设计:

写一个用来生成代码的模板。

注解处理器从每一轮的environment中读取被注解的元素并将它们保存到容易访问的Java对象中——包括一个保存field的map对象,一个保存method的map对象,类名和包名等等。

注解处理器实例化Velocity的context。

注解处理器加载Velocity的模板。

注解处理器创建源文件(通过使用Filer),并且连同Velocity Context将一个写入器(writer)传递给Velocity的模板。

Velocity引擎生成源代码。

通过使用这种方法,你会发现处理器/生成器的代码非常清晰,结构良好,并且易于理解和维护。

下面让我们一步一步来实现:

步骤1:写模板

为了简单起见,我们不会列出完整的BeanInfo生成器代码,只是列出部分与注解处理器一块编译时需要的field(成员变量)和method(方法)。

接下来让我们创建一个名为beaninfo.vm的(模板)文件,并把它放到包含注解处理器的Maven artifact项目的src/main/resources目录下。模板内容的示例如下:

package ${packageName};

import java.beans.MethodDescriptor;

import java.beans.ParameterDescriptor;

import java.beans.PropertyDescriptor;

import java.lang.reflect.Method;

public class ${className}BeanInfo

    extends java.beans.SimpleBeanInfo {

    /**

     * Gets the bean class object.

     *

     * @return the bean class

     */

    public static Class getBeanClass() {

        return ${packageName}.${className}.class;

    }

    /**

     * Gets the bean class name.

     *

     * @return the bean class name

     */

    public static String getBeanClassName() {

        return "${packageName}.${className}";

    }

    /**

     * Finds the right method by comparing name & number of parameters in the class

     * method list.

     *

     * @param classObject the class object

     * @param methodName the method name

     * @param parameterCount the number of parameters

     *

     * @return the method if found, <code>null</code> otherwise

     */

    public static Method findMethod(Class classObject, String methodName, int parameterCount) {

        try {

            // since this method attempts to find a method by getting all

            // methods from the class, this method should only be called if

            // getMethod cannot find the method

            Method[] methods = classObject.getMethods();

            for (Method method : methods) {

                if (method.getParameterTypes().length == parameterCount

                    && method.getName().equals(methodName)) {

                    return method;

                }

            }

        } catch (Throwable t) {

            return null;

        }

        return null;

    }

#foreach($field in $fields)

    /**

     * Returns the ${field.simpleName} property descriptor.

     *

     * @return the property descriptor

     */

    public PropertyDescriptor ${field.simpleName}PropertyDescriptor() {

        PropertyDescriptor theDescriptor = null;

        return theDescriptor;

    }

#end

#foreach($method in $methods)

    /**

     * Returns the ${method.simpleName}() method descriptor.

     *

     * @return the method descriptor

     */

    public MethodDescriptor ${method.simpleName}MethodDescriptor() {

        MethodDescriptor descriptor = null;

        Method method = null;

        try {

            // finds the method using getMethod with parameter types

            // TODO parameterize parameter types

            Class[] parameterTypes = {java.beans.PropertyChangeListener.class};

            method = getBeanClass().getMethod("${method.simpleName}", parameterTypes);

        } catch (Throwable t) {

            // alternative: use findMethod

            // TODO parameterize number of parameters

            method = findMethod(getBeanClass(), "${method.simpleName}", 1);

        }

        try {

            // creates the method descriptor with parameter descriptors

            // TODO parameterize parameter descriptors

            ParameterDescriptor parameterDescriptor1 = new ParameterDescriptor();

            parameterDescriptor1.setName("listener");

            parameterDescriptor1.setDisplayName("listener");

            ParameterDescriptor[] parameterDescriptors = {parameterDescriptor1};

            descriptor = new MethodDescriptor(method, parameterDescriptors);

        } catch (Throwable t) {

            // alternative: create a plain method descriptor

            descriptor = new MethodDescriptor(method);

        }

        // TODO parameterize descriptor properties

        descriptor.setDisplayName("${method.simpleName}(java.beans.PropertyChangeListener)");

        descriptor.setShortDescription("Adds a property change listener.");

        descriptor.setExpert(false);

        descriptor.setHidden(false);

        descriptor.setValue("preferred", false);

        return descriptor;

    }

#end

}

注意在这个模板运作之前,我们需要将以下信息传递给Velocity:

packageName:生成类的完整包名。

className:生成类的类名。

fields:源类中包含的filed的集合。我们需要从每个field中获取以下信息:

simpleName:filed的变量名。

type:filed的类型。

description:filed的自我描述(在本例中没有使用)

……

methods:源类中包含的method的集合。我们需要从每个method中获取以下信息:

simpleName:method的方法名。

arguments:method的参数(在本例中没有使用)

returnType:method的返回类型(在本例中没有使用)

description:method的自我描述(在本例中没有使用)

……

所有的这些信息(也就是模型)都需要从源类里面匹配的注解中提取,并保存到JavaBean后传递给Velocity。

步骤2:注解处理器读取Model

下面让我们创建一个注解处理器。正如本系列第二部分中所提到的,不要忘记给处理器添加注解,好让它能够处理BeanInfo注解类型:

@SupportedAnnotationTypes("example.annotations.beaninfo.BeanInfo")

@SupportedSourceVersion(SourceVersion.RELEASE_6)

public class BeanInfoProcessor

    extends AbstractProcessor {

      ...

}

注解处理器的方法需要从注解和源类本身中提取构建模型所需要的信息。你可以将全部需要的信息都保存到JavaBean里面,不过在这个示例中我们使用的是javax.lang.model.element类型,因为我们不打算传递太多细节给Velocity(当然,需要的数据还是会传递过去的,在这个例子中我们要创建的是一个完整的BeanInfo生成器):

String fqClassName = null;

String className = null;

String packageName = null;

Map<String, VariableElement> fields = new HashMap<String, VariableElement>();

Map<String, ExecutableElement> methods = new HashMap<String, ExecutableElement>();

for (Element e : roundEnv.getElementsAnnotatedWith(BeanInfo.class)) {

    if (e.getKind() == ElementKind.CLASS) {

        TypeElement classElement = (TypeElement) e;

        PackageElement packageElement = (PackageElement) classElement.getEnclosingElement();

        processingEnv.getMessager().printMessage(

            Diagnostic.Kind.NOTE,

            "annotated class: " + classElement.getQualifiedName(), e);

        fqClassName = classElement.getQualifiedName().toString();

        className = classElement.getSimpleName().toString();

        packageName = packageElement.getQualifiedName().toString();

    } else if (e.getKind() == ElementKind.FIELD) {

        VariableElement varElement = (VariableElement) e;

        processingEnv.getMessager().printMessage(

            Diagnostic.Kind.NOTE,

            "annotated field: " + varElement.getSimpleName(), e);

        fields.put(varElement.getSimpleName().toString(), varElement);

    } else if (e.getKind() == ElementKind.METHOD) {

        ExecutableElement exeElement = (ExecutableElement) e;

        processingEnv.getMessager().printMessage(

            Diagnostic.Kind.NOTE,

            "annotated method: " + exeElement.getSimpleName(), e);

        methods.put(exeElement.getSimpleName().toString(), exeElement);

    }

}

步骤3:初始化Velocity Context并加载模板

下面的代码片段展示了如何初始化Velocity Context并加载模板:

if (fqClassName != null) {

    Properties props = new Properties();

    URL url = this.getClass().getClassLoader().getResource("velocity.properties");

    props.load(url.openStream());

    VelocityEngine ve = new VelocityEngine(props);

    ve.init();

    VelocityContext vc = new VelocityContext();

    vc.put("className", className);

    vc.put("packageName", packageName);

    vc.put("fields", fields);

    vc.put("methods", methods);

    Template vt = ve.getTemplate("beaninfo.vm");

    ...

}

Velocity配置文件,在本示例中名为velocity.properties,它应该被放置到src/main/resources目录下。下面是它的内容示例:

runtime.log.logsystem.class = org.apache.velocity.runtime.log.SystemLogChute

resource.loader = classpath

classpath.resource.loader.class = org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader

这组属性配置了Velocity的日志,以及一个用于查找模板的基本资源加载路径。

步骤4:创建新的源文件并生成源代码

紧接着,让我们创建新的源文件,并将这个新文件作为模板的目标来运行模板。下面的代码片段展示了如何操作:

JavaFileObject jfo = processingEnv.getFiler().createSourceFile(

    fqClassName + "BeanInfo");

processingEnv.getMessager().printMessage(

    Diagnostic.Kind.NOTE,

    "creating source file: " + jfo.toUri());

Writer writer = jfo.openWriter();

processingEnv.getMessager().printMessage(

    Diagnostic.Kind.NOTE,

    "applying velocity template: " + vt.getName());

vt.merge(vc, writer);

writer.close();

步骤5:打包并运行

最后,注册注解处理器(记得添加本系列第二部分中提到过的service配置文件),然后打包。再通过终端命令行,Eclipse或者Maven工具在client(客户)项目中进行调用和编译。

假设client项目中的client类如下:

package example.velocity.client;

import example.annotations.beaninfo.BeanInfo;

@BeanInfo public class Article {

    @BeanInfo private String id;

    @BeanInfo private int department;

    @BeanInfo private String status;

    public Article() {

        super();

    }

    public String getId() {

        return id;

    }

    public void setId(String id) {

        this.id = id;

    }

    public int getDepartment() {

        return department;

    }

    public void setDepartment(int department) {

        this.department = department;

    }

    public String getStatus() {

        return status;

    }

    public void setStatus(String status) {

        this.status = status;

    }

    @BeanInfo public void activate() {

        setStatus("active");

    }

    @BeanInfo public void deactivate() {

        setStatus("inactive");

    }

}

当我们在终端上执行javac命令后,我们可以在控制台上看到找到的被注解元素,以及生成的BeanInfo类:

Article.java:6: Note: annotated class: example.annotations.velocity.client.Article

public class Article {

       ^

Article.java:9: Note: annotated field: id

    private String id;

                   ^

Article.java:12: Note: annotated field: department

    private int department;

                ^

Article.java:15: Note: annotated field: status

    private String status;

                   ^

Article.java:53: Note: annotated method: activate

    public void activate() {

                ^

Article.java:59: Note: annotated method: deactivate

    public void deactivate() {

                ^

Note: creating source file: file:/c:/projects/example.annotations.velocity.client/src/main/java/example/annotations/velocity/client/ArticleBeanInfo.java

Note: applying velocity template: beaninfo.vm

Note: example\annotations\velocity\client\ArticleBeanInfo.java uses unchecked or unsafe operations.

Note: Recompile with -Xlint:unchecked for details.

如果我们检查下源代码目录,我们将会找到我们生成的BeanInfo类。任务完成!