[譯]使用注解處理器生成代碼-3 生成源代碼

?本博文原文地址摸我
?本篇博文是關于使用注解處理器生成java代碼系列的第三篇也是最后一篇文章。在第一篇(在這里)中,我們介紹了注解和其一般用法。在第二篇(在這里)中,我們介紹了注解處理器,如何構造并且使用它。?在本篇博文中,我們將想你展示如何使用注解處理器來生成源代碼。

簡介

?生成源代碼很簡單。生成正確的源代碼卻很難。優雅高效的去生成正確的代碼是很麻煩的任務。
?幸運的是,Model-Driver Engineering(1)為我們提供了基于已經證明有效的過程和工具的成熟的方法理論。

MDE 的 Model 和 Meta-model

?在討論如何使用注解處理器生成源代碼之前,有幾個相關的概念我們要實現講明,那就是modelsmeta-model
?MDE的理論基礎之一為抽象的構造(construction of abstractions)。我們將軟件系統在不同的層次和細節上使用不同的方法進行建模。當軟件在一個抽象層次上被建模完成之后,我們就開始對下一個抽象層次進行建模,知道建立一個完備的,可部署的產品。
?在這種理論環境下,一個model 就是我們用來在某一抽象層級上表示軟件系統的抽象。
?meta-model就是我們用來寫model的規則,你可以理解為model的綱要或者語法。

使用注解處理器生成源代碼

?由上述描述可見,注解是定義model和meta-model的好方法,注解類型(Annotation Type)充當meta-model的角色,標注在一段代碼上的注解來提供model。
?我們可以使用這個model來生成配置文件或者從現有代碼中生成新代碼。比如,通過注解bean來生成遠程代理或者數據訪問對象。
?這個方法的核心就是使用注解處理器。注解處理器可以讀取在源代碼中發現的注解,并且對注解做任何想做的事情-比如,打開文件,寫文件,等等。

Filter

?我們在第二篇博文中曾經說過,每個處理器都可以通過處理環境(processing environment)對象獲得一些有用的工具,Filter就是其中之一。
?javax.annotation.processing.Filer接口定義了一些關于創建源文件,類文件和一般資源的方法。通過使用Filter我們可以使用正確的文件目錄,并且確保不會丟失文件系統中的生成的文件或者資源。
?下面這個例子可以顯示如何在注解處理器中生成代碼。生成的類名就是被注解的類名加上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

不要這樣生成代碼

?上邊這個例子十分簡單,有趣但是很混亂。
?我們把從注解中讀取信息的邏輯和寫生成的源文件的邏輯混一起拉。
?按照上述那種方式很難寫出簡潔的代碼,如果當我們遇到一些更加復雜的邏輯時,就更難啦。
?我們需要一個更加優雅的實現方式:- 將不同邏輯分離- 使用模版來讓代碼生成更加簡單
?讓我們看看使用Apache的Velocity構造代碼生成器的例子吧。

Velocity簡介

?Velocity 是通過混合模版和java類的數據來生成各類文本文件的模版引擎。?Velocity可以在MVC框架中渲染視圖或者在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)代表對對象集合的迭代;${method.simpleName}則是打印數據信息。

Velocity代碼生成器

?既然我們決定使用Veloctiy來增強我們的代碼生成器,那么我們就要重新進行設計:

  • 設計用來生成代碼的模版
  • 注解處理器會從round environment中讀取被注解元素,并且將其保存到對象中,比如保存成員變量,方法,或者類,包的表.
  • 注解處理器需要初始化Velocity相關上下文- 注解處理器需要加載Velocity模版- 注解處理器會創建源文件(使用Filer)并且傳遞一個WriterVelocity模版
  • Veloctiy引擎生成源代碼
    ?使用這個方案,我們會發現處理器和生成器相關的代碼是清晰,良好組織并且易于理解和維護。
    ?讓我們一步一步的來實現這個方案吧。

步驟一:實現一個模版

?為了簡單,我們并不會展示BeanInfo生成器的全部代碼,而是只展示我們注解處理器需要使用的一部分成員變量和方法。
?我們先創建一個名為beaninfovm的文件,并且放置在Maven的src/main/resource下。文件內容如下:

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:生成類名

  • field:生成類中的成員變量集合;每個成員變量我們需要以下信息:

    • simpleName:成員變量名 - type:成員變量的類型(在本例中并未使用)
    • description:自我解釋型信息(在本例中并未使用)
    • ...
  • method:生成類中函數的集合;每個函數我們需要一下信息:

    • simpleName:函數名
    • arguments:函數的參數(在本例中并未使用)
    • returnType: 函數返回值類型(在本例中并未使用)
    • description:自我解釋性信息(在本例中并未使用)
    • ...

?所有的這些信息都會從源文件中的注解中獲得,并保存到JavaBean中,再傳遞給Velocity

步驟二:注解處理器讀取信息

?讓我們來實現一個注解處理器,并且注解它支持處理BeanInfo注解類型,相關原理請查看第二篇博文。

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

?注解處理器需要從注解和源文件中提取。你可以使用JavaBean來保存你需要的信息。但是在這個例子中,我們將使用javax.lang.model.element,因為我們不計劃傳遞給Velocity過多信息:

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

步驟三:初始化Velocity并且加載模版

?下邊的代碼片段展示了如何初始化Velocity并且加載模版

  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的日志和尋找模版的類路徑。

步驟四:創建新的文件并且生成代碼

?最后,我們建立新的代碼文件并以這個文件為目標運行模版。下邊的代碼片段展示了如何如何去做上述操作:

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

步驟五:打包并運行

?最終,注冊注解處理器(可以回想一下在第二篇博文中的服務配置相關內容),打包處理器并且在命令行,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"); 
  }
}

?當我們使用命令行執行編譯任務時,我們在終端上看到被注解標注的元素被找到并且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中的注解處理器框架生成源代碼:

  • 我們學習了注解和注解類型的概念和他們的基本用法
  • 我們學習了注解處理器的概念,還有如何編寫,以及從不同工具運行它。
  • 我們大致討論了一下Model-Drive Engineer和代碼生成。
  • 我們展示了如何使用注解處理器生成代碼
  • 我們學習了如何使用Velocity來創建優雅的,強大的,可維護的基于注解處理器的代碼生成器。
    ?現在是時候實現你自己的項目去啦。

(1) 如何你想詳細了解MDE,請查看這篇文件
(2) Filter的API文檔可以在這里進行查看

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容