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实现原理。