【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 {
    @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进行处理了。

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() {
            @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实现原理。

你可能感兴趣的:(JAVA语言,java,学习,spring)