赞
踩
JAVA开发者肯定都用过注解,但是大部分可能跟我一样,只是用到runtime的场景,这段时间了解了一下另外两种场景,简单总结一下。
开发注解最常见的有两个元注解:@Target和@Retention。
@Target用于说明该注解可以应用到哪些项上
元素类型 | 元素适用场合 |
---|---|
ANNOTATION_TYPE | 注解类型声明 |
PACKAGE | 包 |
TYPE | 类(包括enum)及接口(包括注解类型) |
METHOD | 方法 |
CONSTRUCTOR | 构造器 |
FIELD | 成员域(包括enum常量) |
PARAMETER | 方法或构造器参数 |
LOCAL_VARIABLE | 局部变量 |
一条没有@Target限制的注解可以应用于任何项上。
@Retention元注解用于指定一条注解应该保留多长时间。
保留规则 | 描述 |
---|---|
SOURCE | 不包括在类文件中的注解 |
CLASS | 包括在类文件中的注解,但是虚拟机不需要将它们载入 |
RUNTIME | 包括在类文件中的注解,并由虚拟机载入。可通过反射获取 |
@Documented元注解为像Javadoc这样的归档工具提供了一些提示,归档注解应该按照其他一些像protected或static这样用于归档目的的修饰符来处理。
@Inherited元注解只能应用于对类的注解。如果一个雷具有继承注解,那么它的所有子类都自动具有同样的注解。
从JAVA SE6开始,可以将注解处理器添加到JAVA编译器中。为了调用注解处理器,需要运行:
javac -processor processor1,processor2 sourcefiles
编译器会定位源代码中的注解,然后选择可以应用的注解处理器。每个注解处理器会依次执行。如果某个注解处理器创建了一个新的源文件,那么将重复执行这个处理过程。如果某次处理循环没有再产生任何新的源文件,那么就编译所有的源文件。划重点:SOURCE类型的注解处理器需要通过产生新的源文件来进行处理。
注解处理器通常通过扩展AbStractProcessor类来实现Processor接口,使用时需要指定你的处理器支持哪些注解。
做个试验,上代码,简单说明一下使用。
注解处理器最好单独打成一个jar,便于使用。当然,不这样也行,命令行稍微麻烦些而已。
@SupportedAnnotationTypes("注解") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class FirstProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { try { String beanClassName = null; for (TypeElement t : annotations) { Map<String[], Set<Modifier>> fields = new LinkedHashMap(); System.out.println(t.getQualifiedName()); for (Element e : roundEnv.getElementsAnnotatedWith(t)) { String name = e.getSimpleName().toString(); beanClassName = e.getEnclosingElement().toString() + "." + e.getSimpleName().toString(); System.out.println(name + " : " + e.getKind().toString()); System.out.println(name + " : " + e.getEnclosingElement().toString()); System.out.println("----------------"); for (Element x : e.getEnclosedElements()) { System.out.println(name + " : " + x.getKind().toString()); System.out.println(name + " : " + x.getSimpleName().toString()); System.out.println(name + " : " + x.asType()); x.getModifiers().stream().forEach(System.out::println); if (x.getKind().isField()) { fields.put(new String[]{x.getSimpleName().toString(), x.asType().toString()}, x.getModifiers()); } } } // 模拟生成源文件 writeBeanInfoFile(beanClassName, fields); } } catch (Exception classNotFoundException) { classNotFoundException.printStackTrace(); } return false; } private void writeBeanInfoFile(String beanClassName, Map<String[], Set<Modifier>> fields) throws IOException { JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(beanClassName + "Info"); PrintWriter out = new PrintWriter(sourceFile.openWriter()); out.println("//test"); out.close(); } }
maven工程使用注解处理器:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <generatedSourcesDirectory> ${project.build.directory}/generated-sources/ </generatedSourcesDirectory> <annotationProcessors> <annotationProcessor> xx.processor </annotationProcessor> </annotationProcessors> </configuration> </plugin>
这样在编译的时候,就可以直接触发processor进行处理了。
像前面介绍的,注解信息会保留在类文件中,但是不会被虚拟机加载,也就是反射拿不到。
用javap看下class文件:
RuntimeInvisibleAnnotations:xxx
从这个描述或者限制看,这种类型只能用于字节码处理层面的,找了相关资料,介绍是类似的:bytecode post-processing.
而字节码是在编译后生成,在虚拟机加载后运行,那可能用到该类型注解处理的地方就在于编译后改写class文件、或虚拟机加载时改写字节码。
使用javaagent机制可以在运行时改变类文件:
java -javaagent:agent.jar=参数 -cp xx.jar test
实验用的字节码工具是apache的bcel,为bean文件增加get方法。
agent:实现premain方法:
public class PropertyAgent { public static void premain(String arg, Instrumentation instr) { instr.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 修改命令行参数指定的类 if (!className.replace("/", ".").equals(arg)) { return null; } System.out.println("begin transform: " + classfileBuffer.length); try { System.out.println("begin parse"); ClassParser parser = new ClassParser(new ByteArrayInputStream(classfileBuffer), className); JavaClass jc = parser.parse(); System.out.println("begin cg"); ClassGen cg = new ClassGen(jc); PropertyProcessor processor = new PropertyProcessor(cg); System.out.println("begin convert"); processor.convert(); System.out.println("end transform"); return cg.getJavaClass().getBytes(); } catch (Exception e) { System.out.println("transform failed: " + e.getMessage()); e.printStackTrace(); return null; } } }); } } public class PropertyProcessor { private ClassGen cg; private ConstantPoolGen cpg; public PropertyProcessor(ClassGen cg) { this.cg = cg; cpg = cg.getConstantPool(); } public void convert() throws IOException { for (Field f : cg.getFields()) { cg.addMethod(insertGetMethod(f)); } } // 修改bean文件,增加get函数 private Method insertGetMethod(Field field) { String className = cg.getClassName(); String methodName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); int accessFlags = 1; // public InstructionList patch = new InstructionList(); InstructionFactory factory = new InstructionFactory(cg); MethodGen mg = new MethodGen(accessFlags, field.getType(), new Type[0], new String[0], methodName, className, patch, cpg); patch.append(InstructionConstants.ALOAD_0); patch.append(factory.createFieldAccess(className, field.getName(), field.getType(), Constants.GETFIELD)); patch.append(InstructionFactory.createReturn(field.getType())); mg.setMaxStack(); mg.setMaxLocals(); return mg.getMethod(); } }
打包时指定premainclass(手动的情况需要在MF文件中增加对应行), 同时建议依赖都打到一个包中,也可以不这样做,只是命令行会比较麻烦。
<plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestEntries> <Premain-Class>xx.PropertyAgent</Premain-Class> <Can-Redefine-Classes>false</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>
一路看下来:
1 SOURCE在编译期处理,需要生成新文件,个人感觉使用场景比较受限,毕竟如果是模板类型的,那解决方法很多;很复杂的场景,是不是直接代码实现更合适?那除非是工具型的,就是这些文件必须,但是可以减少开发者的代码开发工作量,或者隐藏无需开发者关注的细节。
2 CLASS类型的,感觉比SOURCE灵活些,毕竟可以在class文件中读取注解信息,只要在加载前改写掉,就可以达到目的。
说到这,Lombok是怎么实现的?疑问很多,使用Lombok时,编译不会出错,说明肯定在编译期而不是运行期做了工作;但是大家在使用时又没有单独指定processor,说明使用的不是processor机制。只能学习一下了,单独开一篇记录下Lombok实现原理。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。