> 文章列表 > 【JAVA学习】注解学习

【JAVA学习】注解学习

【JAVA学习】注解学习

简介

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元注解只能应用于对类的注解。如果一个雷具有继承注解,那么它的所有子类都自动具有同样的注解。

SOURCE类型的注解处理器

从JAVA SE6开始,可以将注解处理器添加到JAVA编译器中。为了调用注解处理器,需要运行:

javac -processor processor1,processor2 sourcefiles

编译器会定位源代码中的注解,然后选择可以应用的注解处理器。每个注解处理器会依次执行。如果某个注解处理器创建了一个新的源文件,那么将重复执行这个处理过程。如果某次处理循环没有再产生任何新的源文件,那么就编译所有的源文件。划重点:SOURCE类型的注解处理器需要通过产生新的源文件来进行处理。
注解处理器通常通过扩展AbStractProcessor类来实现Processor接口,使用时需要指定你的处理器支持哪些注解。
做个试验,上代码,简单说明一下使用。

注解处理器最好单独打成一个jar,便于使用。当然,不这样也行,命令行稍微麻烦些而已。

@SupportedAnnotationTypes("注解")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class FirstProcessor extends AbstractProcessor {@Overridepublic 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进行处理了。

CLASS类型注解处理器

像前面介绍的,注解信息会保留在类文件中,但是不会被虚拟机加载,也就是反射拿不到。
用javap看下class文件:

RuntimeInvisibleAnnotations:xxx

从这个描述或者限制看,这种类型只能用于字节码处理层面的,找了相关资料,介绍是类似的:bytecode post-processing.
而字节码是在编译后生成,在虚拟机加载后运行,那可能用到该类型注解处理的地方就在于编译后改写class文件、或虚拟机加载时改写字节码。

JAVAAGENT字节码工程(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() {@Overridepublic 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; // publicInstructionList 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实现原理。