1. 程式人生 > >使用java語言中的註解生成器生成程式碼

使用java語言中的註解生成器生成程式碼

Code Generation using Annotation Processors in the Java language – part 1: Annotation Types 註解型別

這篇帖子我會開始關於使用java語言中註解處理器來程式碼生成系列文章,它是多麼強大,並在最後展示如何在編譯時使用它們生成程式碼。

在這個系列中會:

  • 介紹java中什麼是註解
  • 瞭解註解的公共使用和它的範圍
  • 瞭解什麼是註解處理器以及它的定義角色
  • 學習如何建立註解處理器
  • 學習如何通過命令列,Eclipse,Maven執行註解處理器
  • 學習如何使用註解處理器生成程式碼
  • 學習如何利用一個外部的模板引擎Apache Velocity使用註解處理器生成程式碼

java的註解是在java語言規範第三版中介紹到的,並且在java 5中第一次實現。

通過使用註解,我們能夠新增metadata元資料到我們的原始碼中,如構建或釋出資訊,配置屬性,編譯行為或者質量檢測。

與javadocs不同,註解是強型別,任何一個註解在classpath中都有一個對應的定義好的註解型別。此外,註解可以被定義為執行時可用,但是javadocs絕對不行。

註解語法

註解通常會出現在被註解程式碼段的前面,通常在自己的行,縮排到同一位置。

註解可用於包,型別(classes、interfaces、enums、annotation types),變數(class、instance and local變數–包括定義在for或者while迴圈中的變數),建構函式,方法,引數。

沒有任何元素的註解

    @Override()
    public void theMethod() {…}

在這種情況,括號可以省略

    @Override
    public void theMethod() {…}

註解也可以包含元素,只是name-value對,被逗號隔開。支援基本型別,字串,列舉和陣列

    @Author(name = "Albert",
            created = "17/09/2010",
            revision = 3,
            reviewers = {"George", "Fred"
}) public class SimpleAnnotationsTest {…}

當註解只有一個元素並且它的name是value的話,name可以省略

    @WorkProduct("WP00000182")
    @Complexity(ComplexityLevel.VERY_SIMPLE)
    public class SimpleAnnotationsTest {…}

註釋可以為某些或全部元素定義預設值。具有預設值的元素可以從註釋宣告中省略。

例如,假設註解型別的作者定義revision的預設值是1和reviewers的預設值是空的字串陣列,下面兩個註解申明是等價的。

    @Author(name = "Albert",
            created = "17/09/2010",
            revision = 1,
            reviewers = {})
    public class SimpleAnnotationsTest() {…}

    @Author(name = "Albert",        // defaults are revision 1
            created = "17/09/2010") // and no reviewers
    public class SimpleAnnotationsTest() {…}

註解的典型用途

在java語言規範(JLS)中定義了三個註解型別,它們被java編譯器使用:

  • @Deprecated: 表明它標記的元素不應該被使用。當該標記的元素被使用的任何時候編譯器會生成一個警告⚠️。它應該被javadoc沿用 @deprecated,保留在javadoc來解釋
    deprecated的動機。

  • @Override: 表明該元素是override了一個在父類中生命的元素。當編譯器發現標記的元素並沒有override任何元素,會生成一個警告⚠️。雖然它不是必須的,但是可以檢查錯誤。

  • @SuppressWarnings 表明編譯器應該Suppress一些指定的警告⚠️。

以下都可以看到很棒的例子:

  • Java Enterprise Edition and its main components – Enterprise JavaBeans, Java Persistence API or Web Services API’s
  • Spring Framework 被充分用於配置,依賴注入,以及控制反轉在該框架的核心以及spring的其他專案中
  • Seam, Weld, Guice
  • Apache Struts 2

註解型別

註解型別是用來定義自定義註解的特定介面

一個註解型別定義是使用@interface來替換interface

    public @interface Author {
        String name();
        String created();
        int revision() default 1;
        String[] reviewers() default {};
    }

    public @interface Complexity {
        ComplexityLevel value() default ComplexityLevel.MEDIUM;
    }

    public enum ComplexityLevel {
        VERY_SIMPLE, SIMPLE, MEDIUM, COMPLEX, VERY_COMPLEX;
    }

註解型別與常規的介面有一些不同:

  • 只允許是基本資料型別、字串、列舉、class類和陣列。注意Object一般是不允許的,陣列的陣列是不允許的(因為每一個數組是個Object)。
  • 註釋元素的定義非常像一個method,但是記住修飾符和引數是不允許的。
  • 定義預設值時使用default關鍵字跟在value後面。

在任何類和介面中,一個列舉型別可以巢狀在註解型別定義中

    public @interface Complexity {
        public enum Level {
            VERY_SIMPLE, SIMPLE, MEDIUM, COMPLEX, VERY_COMPLEX;
        }
    …

用來定義註解的註解

JDK自帶的一些註解用來修改註解型別的行為:

  • @Documented: 表示標記的註解型別每次找到的被註解元素通過javadoc文件化
  • @Inherited(繼承): 表示標記的註解型別會被子類也繼承。就是,如果被標記的註解並沒有出現,那它一定在父類中。該註解只適用類的繼承不適用介面的實現。

  • @Retention(保留): 表示被標記註解型別要保留到什麼時候。有以下幾種保留策略:

    • CLASS 預設——包含在class檔案中,但是允許時不能訪問
    • SOURCE 當建立class檔案時被編譯器丟棄
    • RUNTIME 執行時可用
  • @Target: 表示該註解型別可以標註哪種元素。適用以下幾種元素型別:

    • ANNOTATION_TYPE
    • CONSTRUCTOR
    • FIELD
    • LOCAL_VARIABLE
    • METHOD
    • PACKAGE
    • PARAMETER
    • TYPE

Code Generation using Annotation Processors in the Java language – part 2: Annotation Processors 註解處理器

在第一部分中我們介紹了java語言中什麼是註解以及它們的基本使用。在第二部分中我們介紹註解處理器,如何構建它們以及如何執行它們

註解真的很贊。它具有定義良好的語法以及使用不同的型別來設定任何元資料或者配置。

從目前來看,與javadoc相比有優勢,但是還不足以證明它的作用。那麼有沒有可能與註解互動並獲取更多呢?當然是可以的。

  • 在執行時,執行時保留策略的註解可以通過反射訪問。 在Class類中通過方法getAnnotation()getAnnotations()來完成。
  • 在編譯時,註解處理器(Annotation Processors),一個專業的類,可以在編譯時程式碼裡找到的各種註解

註解處理器 API

當註解在java 5中被介紹的時候,註解處理器的API並沒有成熟或者標準化。在
com.sun.mirror釋出了一個獨立工具叫做apt(Annotation Processor Tool),可以用來寫自定義處理器來處理註解以及映象API(Mirror API)

從java 6開始,註解處理器已經通過JSR 269 (2)標註化,合併到了標準庫中並且無縫整合到了javac(Java Compiler Tool)中。

雖然我們只描述java 6中新的註解處理器API,但是你可以在這裡這裡以及這篇文章的一個很好的例子,都是關於之前提到的apt以及java 5中的映象api。

一個註解處理器只不過是一個實現了javax.annotation.processing.Processor介面的類並遵守一些規定。javax.annotation.processing.AbstractProcessor提供的類抽象實現了公共方法,更方便自定義處理器。

自定義處理器可以使用三個註解來配置自己:

  • javax.annotation.processing.SupportedAnnotationTypes:該註解用來註冊處理器支援的註解。有效值是註解型別的完全限定名稱——支援萬用字元
  • javax.annotation.processing.SupportedSourceVersion:該註解用來註冊註解處理器支援的source版本
  • javax.annotation.processing.SupportedOptions:該註解用來註冊支援的自定義選項,可以通過命令列傳入

最後,我們提供自己對process()方法的實現。

寫自己的第一個註解處理器

根據第一部分的介紹,我們建立一個類來處理Complexity annotation。

    package sdc.assets.annotations.processors;

    import@SupportedAnnotationTypes("sdc.assets.annotations.Complexity")
    @SupportedSourceVersion(SourceVersion.RELEASE_6)
    public class ComplexityProcessor extends AbstractProcessor {

        public ComplexityProcessor() {
            super();
        }

        @Override
        public boolean process(Set<? extends TypeElement> annotations,
                               RoundEnvironment roundEnv) {
            return true;
        }
    }

這是個不完整的類,當呼叫時不會做任何操作,被註冊支援註解型別是sdc.assets.annotations.Complexity。因此,每次當編譯器找到一個類被該型別註解就會執行該處理器,提供的process在classpath中可以用了。

為了與註解互動,process()方法接收兩個引數:

  • 一個包含java.lang.model.TypeElement物件的set集合:註解處理是在一輪或多輪後完成。在每一輪中,處理器被呼叫並接收在本輪需要被處理的註解型別。
  • 一個javax.annotation.processing.RoundEnvironment物件:通過該物件可以訪問本輪或前一輪被處理的被註解的資源元素。

處理這兩個引數,在processingEnv實力變數中有一個ProcessingEnvironment可以使用。該物件可以訪問log以及一些工具類。後面會討論一些。

使用RoundEnvironment物件以及Element介面的反射方法,我們寫一個簡單的處理器的實現,可以列印complexity每一個註解的元素。

    for (Element elem : roundEnv.getElementsAnnotatedWith(Complexity.class)) {
        Complexity complexity = elem.getAnnotation(Complexity.class);
        String message = "annotation found in " + elem.getSimpleName()
                       + " with complexity " + complexity.value();
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message);
    }
    return true; // no further processing of this annotation type

打包並註冊註解處理器

完成註解處理器的最後一步是打包並註冊它,讓java編譯器或者其他工具可以找到它。

註冊註解處理器的最簡單的方法是利用標準java服務鏈(standard Java services mechanism):

  • 將註解處理器打入jar包中
  • 包含在jar檔案目錄META-INF/services
  • 在目錄下包含一個叫javax.annotation.processing.Processor的檔案
  • 在檔案中寫入處理器的全限定名,每行一個

java編譯器以及其他工具會在提供claspath中尋找這個檔案並使用註冊的處理器

在我們的例子中,資料夾結構如下


打包之後,我們開始使用它

通過javac執行處理器

想象一下你有一個java專案使用了一些自定義註解並且有相關的註解處理器。在java 5時,編譯和處理時兩個不同的步驟(使用兩個不同的工具),但是在java 6下所有任務都整合到java編譯器中(javac)。

如果你把註解處理器新增到javac classpath中並使用上面java服務鏈方式註冊,那它們可以被javac執行。

以下例子,命令會編譯並使用Complexity註解處理java原始檔

    >javac -cp sdc.assets.annotations-1.0-SNAPSHOT.jar;
     sdc.assets.annotations.processors-1.0-SNAPSHOT.jar
     SimpleAnnotationsTest.java

用來被測試的java類內容是:

    package sdc.startupassets.annotations.base.client;

    import ...

    @Complexity(ComplexityLevel.VERY_SIMPLE)
    public class SimpleAnnotationsTest {

        public SimpleAnnotationsTest() {
            super();
        }

        @Complexity() // this annotation type applies also to methods
                      // the default value 'ComplexityLevel.MEDIUM' is assumed
        public void theMethod() {
            System.out.println("consoleut");
        }
    }

當執行javac命令後,輸入如下:

雖然預設的javac通常表現很好,不過還有一些選項能幫助我們執行註解處理器在更多情況。

  • -Akey[=value]: 用來向處理器傳遞選項。只有通過SupportedOptions註冊的選項才有用
  • -proc:{none|only}:預設情況下,javac會執行註解處理器並編譯所有資源。
    • proc:none 沒有註解處理完成,當你構建註解處理器本身時很有用
    • proc:only 只有註解處理完成,當你執行校驗,如質量工具或標準檢查時很有用
  • -processorpath path:用來指定註解處理器在哪以及去哪裡找依賴。清楚區分專案依賴和處理器依賴(在執行時並不需要的依賴)是很有用的。
  • -s dir: 用來指定生產的程式碼放在哪裡。
  • -processor class1[,class2,class3…]: 用來指定哪些處理器被執行。如果沒有會去尋找jva服務鏈提供的。當我們執行限制執行註冊處理器的一小部分時很有用。

通過Eclipse執行處理器

Eclipse IDE以及其他主流IDE都支援註解處理器並整合進了他們的常規構建過程中。

在Eclipse IDE中,當你訪問屬性配置時,你可以發現在Java Compiler group下,有一個叫Annotation Processing的選項。

啟用Annotation Processing(預設是關閉的),就可以通過選項頁的表格出入處理器了。

此外,可以通過Factory Path選項執行選擇的處理器。

只要配置一次,每次構建行為被觸發,就會執行註解處理了

通過Maven執行處理器

註解處理器也可以整合在Apache Maven構建中執行。

這種自動化水平,可以幫助我們無縫整合所以處理型別。標準驗證或者程式碼生成在整個專案生命週期中作為一個單獨的過程。它還可以通過連續整合引擎(Continuous Integration engines)無縫整合。

雖然把註解處理器整合到Maven構建中有很多方式,我們建議使用這裡描述的方式。描述基於Mojo(maven plug-in),Mojo是用來關注編譯任務。

整合Maven需要我們的註解和註解處理器作為Maven工件可用。

我們建議將註解和註解處理器分成不同的工件。因為註解處理器在其他客戶端專案不需要訪問,可以減少跨專案依賴的數量。

通過這種方法,我們設定三個不同的專案,對應每一個不同的Maven工件。

  • 註解工件(Annotation artifact): 只包含註解型別
  • 註解處理器工件(Annotation Processor artifact):包含註解處理器。他將依賴註解工件,編譯器外掛需要被設定成proc:none所以在構建該工件時註解處理器不執行。
  • 客戶端工程:包含了客戶端程式碼,它依賴上面兩個工件。

下面是註解工件的資料夾結構以及pom檔案

注意: Maven compiler plug-in的版本用來構建該工件

下面是註解處理器工件的資料夾結構以及pom檔案


注意:

  • 打包處理器服務資料夾
  • Maven compiler plug-in的版本
  • proc:none選項
  • 依賴註解工件

最後,是客戶端工程的資料夾結構和pom檔案


一旦做好,每次在Maven構建執行時,處理器也會按照預期執行。

Code Generation using Annotation Processors in the Java language – part 3: Generating Source Code 生成原始碼

第三部分我們將介紹如何誰用註解處理器生成原始碼

生成程式碼容易,生成對的程式碼就難了。並且用一個優雅又有效的方式是一件很麻煩的任務。

幸運的是,在過去幾年Model-Driven Engineering(MDE,模型驅動工程,有的引用是Model-Driven Development模型驅動開發或者Model-Driven Architecture,模型驅動架構)已經幫助發展成了一種成熟的技術。

Model-Drive Engineering(MDE)遠不止是生成程式碼,雖然我們認為它是MDE方法的自然切入點。

註解處理器就是我們可以用來生成原始碼工具之一。

在MDE中的模型和元模型

在進入如何使用註解處理器生成程式碼之前,我們想提出幾個概念:模型(model)和元模型(meta-model)

MDE的支柱之一是構造抽象(construction of abstractions)。我們給軟體系統建模(model),希望通過不同的方法來建立各個層次的細節。當一個抽象層被建模,我們進行下一層的建模,下一層再下一層,最後以一個完成的可部署的產品結束。

在這個背景下,無論我們使用多少層細節,一個模型只不過用來代表我們系統的抽象。

而meta-model則是我們用來編寫模型的規則。你可以把它理解為模型的scheme或者語義

使用註解處理器生成原始碼

如我們所見,註解是一個很棒的方式來定義一個meta-model並建立一個model。註解型別扮演meta-model的角色,一系列註解的程式碼扮演model的角色。

我們可以利用這個模型來生成配置檔案或者編寫被現有程式碼驅動的新的原始檔。比如:從一個被註解的物件建立一個遠端代理或者一個數據訪問物件。

這種方式的核心就是註解處理器。一個處理器可以讀在原始碼中找到的註解,提取這個模型,然後通過模型做任何我們想做的事(如:開啟一個檔案,傳入內容)。java編譯器會小心驗證模型。

過濾器(Filter)

在第二部分討論過,每個處理器可以訪問一些有用的工具。其中一個就是過濾器。

javax.annotation.processing.Filer介面定義了一些方法,可以建立原始檔,class檔案,或者通用資源。通過過濾器可以確保使用正確的目錄並且在我們的檔案系統中不會丟失生成的有用專案。

如果我們要寫生成器,-d-s選項在javac或者在Maven pom檔案中配置是很重要的。

下文例子展示了在註解生成器中如何建立java原始檔。生成的class名字是被註解的class名字加上“BeanInfo”。

 if (e.getKind() == ElementKind.CLASS) {
     TypeElement classElement = (TypeElement) e;
     PackageElement packageElement =
         (PackageElement) classElement.getEnclosingElement();

     JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
         classElement.getQualifiedName() + "BeanInfo");

     BufferedWriter bw = new BufferedWriter(jfo.openWriter());
     bw.append("package ");
     bw.append(packageElement.getQualifiedName());
     bw.append(";");
     bw.newLine();
     bw.newLine();
     // rest of generated class contents

不要像我兄弟一樣建立

上一個例子簡單有趣,但是很混亂。

我們將從註解獲取需要的資訊(model模組)和寫生成檔案的邏輯(view邏輯)混合在了一起。

用這種方式寫一個體面的生成器是很困難的。如果我們需要一些更復雜的任務是很麻煩,容易出錯且很難維護的。

因此,我們需要一個更優雅的方式:

  • 將model和view明確分離
  • 使用模板來減輕寫生成檔案的任務

我們來看看如何使用Apache Velocity來像我們想要的方式生成。

VELOCITY歷史簡介

Velocity,來自Apache Software Foundation的一個專案,是一個java的模板引擎,可以通過混合模板和java物件的資料生成所有型別的文字檔案。

Velocity在MVC模式下提供view或者作為將資料轉換成XML的XSLT的替代。

Velocity有它自己的語言,叫做Velocity Template Language (VTL)。這是生產豐富且易於閱讀的模板的關鍵。在VTL中,我們可以通過簡單且直觀的方式定義變數,控制流和迭代以及訪問java物件中包含的資訊。

以下是一個Velocity模板的片段:

 #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;

正如你所見,VTL是很簡單且容易理解的。粗體高亮的部分#foreach($field in $fields),你可以看到兩個典型的VTL構造:迭代一個物件集合和列印集合中找到的元素的一些屬性。

VELOCITY生成器的方法

現在我們決定使用VELOCITY來加強我們的生成器,我們需要按照以下的計劃重新設計。

  • 編寫用於生成程式碼的模板
  • 註解處理器會從round environment讀取被註解的元素,並且儲存在易於訪問的java物件中。一個map用於欄位(fields),一個map用於方法(methods),類和包名等等。
  • 註解處理器將初始化Velocity上下文(context)
  • 註解處理器將載入Velocity模板
  • 註解處理器將創造原始檔(使用過濾器filter)並通過context向Velocity模板傳遞一個writer
  • Velocity引擎將生成原始碼

通過這些步驟你會發現處理器/生成器程式碼很清楚,結構良好並且易於理解和維護。

我們來一步一步操作

Step1 :寫模板

為了簡單起見,我們不打算展示全部的BeanInfo生成器,只有建立我們處理器需要的欄位和方法。

讓我們建立一個檔案,名字叫beaninfo.vm,並將它放在Maven處理器工件的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欄位 包含源class中欄位的集合
  • simpleName 欄位的名字
  • type 型別(例子中沒有用到)
  • description 自己的解釋(例子中沒有用到)
  • methods 包含源class中方法的集合

  • simpleName 方法的名字
  • arguments 方法的引數(例子中沒有用到)
  • returnType 返回型別(例子中沒有用到)
  • description 自己的解釋(例子中沒有用到)

所有這些資訊(model)會在源class中找到的註解提取出來,並存儲在javabean中傳遞給Velocity

Step 2: 讀取model的處理器

讓我們建立一個處理器而且不要忘記註解它來處理一個BeanInfo註解型別

@SupportedAnnotationTypes("example.annotations.beaninfo.BeanInfo")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class BeanInfoProcessor
    extends AbstractProcessor {

process方法需要從註解和源class本身中提取建立model所需的資訊。你可以使用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);
                }
            }

Step 3: 初始化Velocity context並載入模板

以下程式碼片段展示瞭如何初始化Volecity 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("classNameassName);
                vc.put("packageNameckageName);
                vc.put("fieldselds);
                vc.put("methodsthods);

                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設定了一個log的屬性以及一個基於classpath的資源loader來尋找模板

Step 4:建立新的source並生成內容

最後,讓我們建立一個原始檔並使用這個新檔案作為目標執行模板。以下片段展示瞭如何操作:

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();

Step 5: 打包並執行

最後,註冊處理器(像在第二部分中service配置檔案那樣),打包並從客戶端的命令列、Eclipse或者Maven構建中呼叫

假設下面是客戶端類:

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類,任務完成。

總結

通過這個系列,我們學習到了如何使用java6中的註解生成器框架來生成原始碼

  • 我們學習了什麼是註解和註解型別以及他們的基本使用
  • 我們學習了什麼是註解處理器,如何編寫他們以及使用不同的工具執行它,如java編譯器,Eclipse和Maven
  • 我們討論一點Model-Drive Engineering和程式碼生成的事
  • 我們展示瞭如何通過java編譯器,如何建立程式碼生成器並整合的
  • 我們展示瞭如何利用現有的生成器框架如(Apache Velocity)來建立優雅,強大易於維護的程式碼生成器

現在是時候運用到你的專案中了,考慮生成。

相關推薦

no