java源码级注解处理+字节码级注解处理

来源:互联网 发布:微信群如何做淘宝客 编辑:程序博客网 时间:2024/06/05 06:40

【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;      39: areturn  public boolean equals(java.lang.Object);    Code:       0: aload_0       1: aload_1       2: if_acmpne     7       5: iconst_1       6: ireturn       7: aload_1       8: ifnonnull     13      11: iconst_0      12: ireturn      13: aload_0      14: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;      17: aload_1      18: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;      21: if_acmpeq     26      24: iconst_0      25: ireturn      26: aload_1      27: checkcast     #13                 // class com/corejava/chapter10_7/Item      30: astore_2      31: aload_0      32: getfield      #2                  // Field description:Ljava/lang/String;      35: aload_2      36: getfield      #2                  // Field description:Ljava/lang/String;      39: invokestatic  #14                 // Method java/util/Objects.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z      42: ifeq          60      45: aload_0      46: getfield      #3                  // Field partNumber:I      49: aload_2      50: getfield      #3                  // Field partNumber:I      53: if_icmpne     60      56: iconst_1      57: goto          61      60: iconst_0      61: ireturn  public int hashCode();    Code:       0: iconst_2       1: anewarray     #15                 // class java/lang/Object       4: dup       5: iconst_0       6: aload_0       7: getfield      #2                  // Field description:Ljava/lang/String;      10: aastore      11: dup      12: iconst_1      13: aload_0      14: getfield      #3                  // Field partNumber:I      17: invokestatic  #16                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;      20: aastore      21: invokestatic  #17                 // Method java/util/Objects.hash:([Ljava/lang/Object;)I      24: ireturn}
6.3)SetTest.java 程序会将Item 对象插入到一个散列集中。当你用修改过的类文件来运行该程序的时候,会看到下面的日志记录信息;
official result)
Aug 18, 2004 10:57:59 AM Item hashCode
FINER: ENTRY
Aug 18, 2004 10:57:59 AM Item hashCode
FINER: ENTRY
Aug 18, 2004 10:57:59 AM Item hashCode
FINER: ENTRY
Aug 18, 2004 10:57:59 AM Item equals
FINER: ENTRY
[[description=Toaster, partNumber=1729], [description=Microwave, partNumber=4104]]
my result)
package com.corejava.chapter10_7;import java.io.*;import org.apache.bcel.*;import org.apache.bcel.classfile.*;import org.apache.bcel.generic.*;/** * Adds "entering" logs to all methods of a class that have the LogEntry annotation. * @version 1.10 2007-10-27 * @author Cay Horstmann */public class EntryLogger{   private ClassGen cg;   private ConstantPoolGen cpg;   /**    * Adds entry logging code to the given class.    * @param args the name of the class file to patch    */   public static void main(String[] args)   {      try      {         if (args.length == 0)             System.out.println("USAGE: java bytecodeAnnotations.EntryLogger classname");         else         {            JavaClass jc = Repository.lookupClass(args[0]);            ClassGen cg = new ClassGen(jc);            EntryLogger el = new EntryLogger(cg);            el.convert();            String f = Repository.lookupClassFile(cg.getClassName()).getPath();            System.out.println("Dumping " + f);            cg.getJavaClass().dump(f);         }      }      catch (Exception e)      {         e.printStackTrace();      }   }   /**    * Constructs an EntryLogger that inserts logging into annotated methods of a given class.    * @param cg the class    */   public EntryLogger(ClassGen cg)   {      this.cg = cg;      cpg = cg.getConstantPool();   }   /**    * converts the class by inserting the logging calls.    */   public void convert() throws IOException   {      for (Method m : cg.getMethods())      {         AnnotationEntry[] annotations = m.getAnnotationEntries();         for (AnnotationEntry a : annotations)         {            if (a.getAnnotationType().equals("Lcom/corejava/chapter10_7/LogEntry;"))            {               for (ElementValuePair p : a.getElementValuePairs())               {                  if (p.getNameString().equals("logger"))                  {                     String loggerName = p.getValue().stringifyValue();                     cg.replaceMethod(m, insertLogEntry(m, loggerName));                  }               }            }         }      }   }   /**    * Adds an "entering" call to the beginning of a method.    * @param m the method    * @param loggerName the name of the logger to call    */   private Method insertLogEntry(Method m, String loggerName)   {      MethodGen mg = new MethodGen(m, cg.getClassName(), cpg);      String className = cg.getClassName();      String methodName = mg.getMethod().getName();      System.out.printf("Adding logging instructions to %s.%s%n", className, methodName);      int getLoggerIndex = cpg.addMethodref("java.util.logging.Logger", "getLogger",            "(Ljava/lang/String;)Ljava/util/logging/Logger;");      int enteringIndex = cpg.addMethodref("java.util.logging.Logger", "entering",            "(Ljava/lang/String;Ljava/lang/String;)V");      InstructionList il = mg.getInstructionList();      InstructionList patch = new InstructionList();      patch.append(new PUSH(cpg, loggerName));      patch.append(new INVOKESTATIC(getLoggerIndex));      patch.append(new PUSH(cpg, className));      patch.append(new PUSH(cpg, methodName));      patch.append(new INVOKEVIRTUAL(enteringIndex));      InstructionHandle[] ihs = il.getInstructionHandles();      il.insert(ihs[0], patch);      mg.setMaxStack();      return mg.getMethod();   }}
package com.corejava.chapter10_7;import java.lang.annotation.*;/** * @version 1.00 2004-08-17 * @author Cay Horstmann */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface LogEntry{   String logger();}<strong></strong>
package com.corejava.chapter10_7;import java.util.*;/** * An item with a description and a part number. * @version 1.01 2012-01-26 * @author Cay Horstmann */public class Item{   private String description;   private int partNumber;   /**    * Constructs an item.    * @param aDescription the item's description    * @param aPartNumber the item's part number    */   public Item(String aDescription, int aPartNumber)   {      description = aDescription;      partNumber = aPartNumber;   }   /**    * Gets the description of this item.    * @return the description    */   public String getDescription()   {      return description;   }   public String toString()   {      return "[modified: description=" + description + ", partNumber=" + partNumber + "]";   }   @LogEntry(logger = "global")   public boolean equals(Object otherObject)   {      if (this == otherObject) return true;      if (otherObject == null) return false;      if (getClass() != otherObject.getClass()) return false;      Item other = (Item) otherObject;      return Objects.equals(description, other.description) && partNumber == other.partNumber;   }   @LogEntry(logger = "global")   public int hashCode()   {      return Objects.hash(description, partNumber);   }}
package com.corejava.chapter10_7;import java.util.*;import java.util.logging.*;/** * @version 1.02 2012-01-26 * @author Cay Horstmann */public class SetTest{     public static void main(String[] args)   {        Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).setLevel(Level.FINEST);      Handler handler = new ConsoleHandler();      handler.setLevel(Level.FINEST);      Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).addHandler(handler);      Set<Item> parts = new HashSet<>();      parts.add(new Item("Toaster", 1279));      parts.add(new Item("Microwave", 4104));      parts.add(new Item("Toaster", 1279));      System.out.println(parts);   }}
A2) 以上实例显示了字节码工程的强大之处: 注解可以 用来向程序中添加一些指示,而字节码编辑工具则可以提取这些知识,然后修改虚拟机指令;(干货——字节码工程的强大之处)

【2.1】 在加载时修改字节码
1)problem+solution
1.1)problem: 把另一个工具添加到 程序的构建过程中, 会显得很笨重;
1.2)solution: 更吸引人的做法: 是将字节码工程延迟到载入时,即类加载器加载类的时候;
2)具体的solution:
2.1) 设备API 提供了一个安装字节码转换器的挂钩,不过,必须在main方法调用之前就安装这个 转换器。 (干货——引入字节码转换器)
2.2)通过定义一个代理: 即被加载用来按照某种方式监视程序的一个类库, 就可以处理这个需求了。代理代码可以在 premain 方法中执行初始化;
3)下面是构建代理所需要的steps:
s1)实现一个具有下面这个方法的类:
public static void premain(String arg , Instrumentation instr) // 当代理加载的时候, 此方法就会被调用;
s2)制作一个清单文件 EntryLoggingAgent.mf 来设置 Premain-Class 属性。 如:
 Premain-Class: bytecodeAnnotations.EntryLoggingAgent
s3)将代理代码打包, 并生成一个 JAR文件, 如:
javac -classpath .:bcel-version.jar bytecodeAnnotations.EntryLoggingAgent
jar cvfm EntryLoggingAgent.jar EntryLoggingAgent.mf bytecodeAnnotations/Entry*.class
s3.1)为了运行该代理的java程序, 使用如下命令行选项:
java -javaagent: AgentJARFile=agentArgument ....
s3.2)运行具有实体日志代理的SetTest 程序,需要调用:
javac SetTest.java
java -javaagent: EntryLoggingAgent.jar=set.Item -classpath .:bcel-version.jar set.SetTest

Attention) Item 参数是代理应该修改的类名称;
Conclusion)自此,你学习了以下知识:
C1)怎样向java程序中 添加注解;
C2)怎样设计你自己的注解接口;(干货——diy 注解接口)
C3)怎样实现可以利用注解 的工具;(干货——diy 注解工具)
C4)你看到了三种处理代码的技术: 编写脚本, 编译java程序和处理注解。 前两种技术十分简单, 而另一方面, 构建注解工具可能会很复杂,但并不是大多数程序需要解决的问题;

  
0 0
原创粉丝点击