【0】README
0.1)本文文字描述轉自 core java volume 2, 旨在學習 java源碼級註解處理+位元組碼級註解處理 的基礎知識;
------------------------------------------------------------------------------------------------------------------------
【1】源碼級註解處理
1)註解的用處之一: 就是自動產生包含程式額外資訊的"附檔案"。Java EE 5使用註解極大地簡化了編程模型。
2)源碼級註解是將註解處理器添加到Java編譯器中。
3)看個荔枝: 我們編寫了一個程式,可以自動產生bean資訊類。該程式使用一個註解來標記bean的屬性, 然後運行某個工具對這個源檔案進行解析,分析其註解,最後輸出 bean資訊類的源檔案;
4)提供@Property 註解: 為了避免編寫 bean 資訊類這項苦差事,我們提供了一個 @Property 註解,可以用來標記屬性的擷取器或設定器, 像下面這樣:
@PropertyString getTitle() //擷取器{ return title;}或者@Property(editor="TitlePositionEditor")public void setTitlePosition(int p) //設定器{ titlePosition = p;}
5)@Property 註解的定義
package com.corejava.chapter10_6;import java.lang.annotation.*;@Documented @Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Property{ String editor() default "";}
6)為了自動產生一個名字為 BeanClass 的bean 資訊類,我們要實現下面這些任務(tasks):
t1) 編寫一個源檔案 BeanClassBeanInfo.java,繼承自SimpleBeanInfo, 覆蓋 getPropertyDescriptors 方法;
t2)對於每個已註解的方法,通過去除掉get 或 set 首碼,然後小寫化剩餘部分,就可以恢複屬性名稱;
t3)對於每個屬性,編寫一條用於構建 PropertyDescriptor 的語句;
t4)如果該屬性具有一個編輯器,那麼編寫一個方法去調用 setPropertyEditorClass;
t5)編寫代碼返回一個包含所有屬性描述符的數組; 看個荔枝(對於下面這個在 ChartBean 類中的註解,將被轉換為:)
@Property(editor="TitlePositionEditor")public void setTitlePosition(int p){ titlePosition = p;}
將被轉換為:
public class ChartBeanBeanInfo extends java.beans.SimpleBeanInfo{ public java.beans.PropertyDescriptor[] getProperties() { java.beans.PropertyDescriptor titlePositionDescriptor = new java.beans.PropertyDescriptor("titlePosition", ChartBean.class); titlePositionDescriptor.setPropertyEditorClass(TitlePositionEditor.class) ... return new java.beans.PropertyDescriptor[] { titlePositionDescriptor, ...... } }}
6.1)如果我們可以定位所有已經用 @Property 屬性標記過的方法,那麼所有這些都很容易實現。
7)從java SE6 開始,我們可以將註解處理器添加到 java 編譯器中。
7.0)為了調用註解處理機制,需要運行 javac -processor ProcessorClassName1,ProcessorClassName2,... sourceFiles 7.1)編譯器會定位原始碼中的註解,然後選擇恰當的註解處理器。 每個註解處理器會依次執行。如果某個註解處理器建立了一個新的源檔案,那麼將重複執行這個處理過程。如果某次處理迴圈沒有再產生任何新的源檔案,那麼就編譯所有的源檔案。
7.2)下圖展示了 @Property 註解是如何處理的;
8)自訂註解處理器
8.1)註解處理器要擴充 AbstractProcessor,並重寫 process 方法:
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
8.1.1)process 方法有兩個參數: 一個是在本輪中要進行處理的註解集,另一個是包含了有關當前處理輪次的資訊的RoundEnv 引用;
8.1.2)在process 方法中,我們迭代所有註解過的方法,對於每個方法,我們剝離其名字中的get, set 和 is 首碼,並將隨後緊挨著的字母改寫為小寫,從而得到屬性名稱;
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement t : annotations) { Map<String, Property> props = new LinkedHashMap<>(); for (Element e : roundEnv.getElementsAnnotatedWith(t)) { props.put(property name, e.getAnnotation(Property.class)); } write bean info source file return ture;}
8.2)指定處理器支援的註解類型:
@SupportedAnnotationTypes("sourceAnnotations.Property")public class BeanInfoAnnotationProcessor extends AbstractProcessor
8.3)編譯註解處理器並運行
編譯註解處理器: E:\bench-cluster\cloud-data-preprocess\CoreJavaAdvanced\src>javac com/corejava/chapter10_6/BeanInfoAnnotationProcessor.java運行: E:\bench-cluster\cloud-data-preprocess\CoreJavaAdvanced\src>javac -processor com.corejava.chapter10_6.BeanInfoAnnotationProcessor com/corejava/chapter10_6/ChartBean.javaresult: 之後就可以查看自動產生的 ChartBeanBeanInfo.java 檔案;
8.4)要查看註解處理器的行為,可以在 javac 命令中添加 XprintRounds。得到下面的輸出:
Attention)for source code, please visit https://github.com/pacosonTang/core-java- volume/tree/master/coreJavaAdvanced/chapter10/10_6
package com.corejava.chapter10_6;import java.beans.*;import java.io.*;import java.util.*;import javax.annotation.processing.*;import javax.lang.model.*;import javax.lang.model.element.*;import javax.tools.*;import javax.tools.Diagnostic.*;/** * This class is the processor that analyzes Property annotations. * @version 1.11 2012-01-26 * @author Cay Horstmann */// 指定處理器支援的註解類型:@SupportedAnnotationTypes("com.corejava.chapter10_6.Property")@SupportedSourceVersion(SourceVersion.RELEASE_8)// 自訂註解處理器public class BeanInfoAnnotationProcessor extends AbstractProcessor{// process 方法有兩個參數:// annotations 一個是在本輪中要進行處理的註解集,另一個是包含了有關當前處理輪次的資訊的RoundEnv 引用; @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement t : annotations) { Map<String, Property> props = new LinkedHashMap<>(); String beanClassName = null; for (Element e : roundEnv.getElementsAnnotatedWith(t)) { String mname = e.getSimpleName().toString(); String[] prefixes = { "get", "set", "is" }; boolean found = false; for (int i = 0; !found && i < prefixes.length; i++) if (mname.startsWith(prefixes[i])) { found = true; int start = prefixes[i].length(); String name = Introspector.decapitalize(mname.substring(start)); props.put(name, e.getAnnotation(Property.class)); } if (!found) processingEnv.getMessager().printMessage(Kind.ERROR, "@Property must be applied to getXxx, setXxx, or isXxx method", e); else if (beanClassName == null) beanClassName = ((TypeElement) e.getEnclosingElement()).getQualifiedName() .toString(); } try { if (beanClassName != null) writeBeanInfoFile(beanClassName, props); } catch (IOException e) { e.printStackTrace(); } } return true; } /** * Writes the source file for the BeanInfo class. * @param beanClassName the name of the bean class * @param props a map of property names and their annotations */ private void writeBeanInfoFile(String beanClassName, Map<String, Property> props) throws IOException { // 建立輸出檔案 JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile( beanClassName + "BeanInfo"); PrintWriter out = new PrintWriter(sourceFile.openWriter()); int i = beanClassName.lastIndexOf("."); // 編寫源檔案的代碼簡單明了,僅僅是一系列的 out.println 語句 if (i > 0) { out.print("package "); out.print(beanClassName.substring(0, i)); out.println(";"); } out.print("public class "); out.print(beanClassName.substring(i + 1)); out.println("BeanInfo extends java.beans.SimpleBeanInfo"); out.println("{"); out.println(" public java.beans.PropertyDescriptor[] getPropertyDescriptors()"); out.println(" {"); out.println(" try"); out.println(" {"); for (Map.Entry<String, Property> e : props.entrySet()) { out.print(" java.beans.PropertyDescriptor "); out.print(e.getKey()); out.println("Descriptor"); out.print(" = new java.beans.PropertyDescriptor(\""); out.print(e.getKey()); out.print("\", "); out.print(beanClassName); out.println(".class);"); String ed = e.getValue().editor().toString(); if (!ed.equals("")) { out.print(" "); out.print(e.getKey()); out.print("Descriptor.setPropertyEditorClass("); out.print(ed); out.println(".class);"); } } out.println(" return new java.beans.PropertyDescriptor[]"); out.print(" {"); boolean first = true; for (String p : props.keySet()) { if (first) first = false; else out.print(","); out.println(); out.print(" "); out.print(p); out.print("Descriptor"); } out.println(); out.println(" };"); out.println(" }"); out.println(" catch (java.beans.IntrospectionException e)"); out.println(" {"); out.println(" e.printStackTrace();"); out.println(" return null;"); out.println(" }"); out.println(" }"); out.println("}"); out.close(); }}
package com.corejava.chapter10_6;import java.lang.annotation.*;@Documented@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Property{ String editor() default ""; }
【2】位元組碼工程(english version, 參見 http://www.informit.com/articles/article.aspx?p=2027052&seqNum=7)
0)for complete source code about byte code engineering , please visit : https://github.com/pacosonTang/core-java-volume/tree/master/coreJavaAdvanced/chapter10/10_7
1)處理註解的層級(levels): l1)運行期層級:具體執行個體,參見 http://blog.csdn.net/pacosonswjtu/article/details/50719233 l2)源碼層級: 具體執行個體,參見本章節【1】; l3)位元組碼層級: 具體執行個體,參見 本章節【2】; 2)對類檔案進行註解處理:類檔案格式歸檔過的, 這種格式相當複雜, 並且在沒有特殊類庫的情況下,處理類檔案具有很大的挑戰性。BCEL(Byte Code Engineering Library), 即位元組碼工程類庫, 就是這樣的特殊類庫之一;(乾貨——引入BCEL,位元組碼工程類庫) 3) 使用BCEL 向已註解方法中添加日誌資訊。 3.1)如果一個方法這樣註解過: @LogEntry(logger=loggerName) 3.2)在方法的開始部分,我們添加: Logger.getLogger(loggerName).entering(className, methodName); 4)看個荔枝: 4.1)如果對 Item 類的 hashCode 方法做了如下註解: @LogEntry(logger="global") public int hashCode(); 4.2)那麼,在任何時候調用該方法, 都會報告一條與下面列印出來的訊息相似的訊息: Aug 17, 2004, Item hashCode FINER: ENTRY 5)為了實現這個任務, 我們需要遵循下面幾點(points): p1)載入類檔案中的位元組碼; p2)定位所有的方法; p3)對於每個方法, 檢查它是不是有一個 LogEntry 註解; p4)如果有, 在方法開始部分添加下面所列指令的位元組碼:
ldc loggerNameinvokestatic java/util/logging/Logger.getLogger:(Ljava/lang/String;)Ljava/util/logging/Logger;ldc classNameldc methodNameinvokevirtual java/util/logging/Logger.entering:(Ljava/lang/String;Ljava/lang/String;)V
Attention)插入這些位元組碼看起來很複雜,不過BCEL 卻使它變得簡單;
6)看個荔枝:
6.1) 如何向Item.java 檔案添加記錄記錄指示詞
E:\bench-cluster\cloud-data-preprocess\CoreJavaAdvanced\src>javac -cp .;com/corejava/chapter10_7/bce l-6.0-SNAPSHOT.jar com/corejava/chapter10_7/EntryLogger.javaE:\bench-cluster\cloud-data-preprocess\CoreJavaAdvanced\src>java -cp .;com/corejava/chapter10_7/bcel -6.0-SNAPSHOT.jar com.corejava.chapter10_7.EntryLogger com.corejava.chapter10_7.Item Adding logging instructions to com.corejava.chapter10_7.Item.equalsAdding logging instructions to com.corejava.chapter10_7.Item.hashCodeDumping E:\bench-cluster\cloud-data-preprocess\CoreJavaAdvanced\src\com\corejava\chapter10_7\Item.class
6.2)在對 Item類檔案被修java改前和修改後分別運行下:
javap -c Item ,你就可以看到 在 hashCode, equals 以及 compareTo 方法開始部分插入的那些指令;
E:\bench-cluster\cloud-data-preprocess\CoreJavaAdvanced\src>javap -c com.corejava.chapter10_7.ItemCompiled from "Item.java"public class com.corejava.chapter10_7.Item { public com.corejava.chapter10_7.Item(java.lang.String, int); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: aload_1 6: putfield #2 // Field description:Ljava/lang/String; 9: aload_0 10: iload_2 11: putfield #3 // Field partNumber:I 14: return public java.lang.String getDescription(); Code: 0: aload_0 1: getfield #2 // Field description:Ljava/lang/String; 4: areturn public java.lang.String toString(); Code: 0: new #4 // class java/lang/StringBuilder 3: dup 4: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 7: ldc #6 // String [description= 9: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 12: aload_0 13: getfield #2 // Field description:Ljava/lang/String; 16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: ldc #8 // String , partNumber= 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: aload_0 25: getfield #3 // Field partNumber:I 28: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 31: ldc #10 // String ] 33: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;