Java的注解功能很早就学习了,但是一直不太理解到底是如何作用的,比如为什么作用在类上,为什么在方法上或者在参数上才能生效? 这几天刚好学习了Android组件化开发,里面的Arout就是一个很好的学习例子,我在这个基础上抽象出来了一个例子方便后面忘记的时候能够回忆起来。
这里我的需求描述如下: 我需要开发一个工具类,能够将继承自某个接口或类的所有子类保存起来, 保存的规则为通过key,可以索引到该类,并把该类输出,就是一个简单的map存储。
key(String) ---------------- value (Class<?>)
"a_class" ------> A.class
"b_class" -----> B.class
分析: 如果是平时的话,我可能直接用一个单例类,在需要保存的时候直接调用方法put到map就好了,但是在组件化的场景下,涉及到多人开发,甚至很多个模块,这种方法耦合太大了,而且每次都是调用函数,每个人也不太清楚自己的调用时机,导致在get时这个方法还没有被put进去.....(扯远了)
总结:所有的技术方案如下,这里面不少技术需要一个一个的实现。
技术一 | 我需要一个单例map保存对应的继承自A的类 | Map<String, Class<? extends A> map |
技术二 | 我需要一个注解标记这个是我要保存的类 | @StoreAnnotation |
技术三 | 我需要在代码编译期间让我的注解成效完成上述功能 | 注解处理器 |
技术一:实现一个单例类,能够把对用的类型put进去,判断啥的写的也比较简单。就用最常见的方式去实现吧,虽然有很多有趣的实现方式。
package com.clazz.store;
import java.util.HashMap;
import java.util.Map;
public class ClassStoreSingleton {
private Map<String, Class<? extends ILass>> mClassMap = null;
private static volatile ClassStoreSingleton mClassStoreSingleton = null;
private ClassStoreSingleton(){
mClassMap = new HashMap<>();
}
private static ClassStoreSingleton getClassStore() {
if (mClassStoreSingleton == null) {
synchronized (ClassStoreSingleton.class) {
if (mClassStoreSingleton == null) {
mClassStoreSingleton = new ClassStoreSingleton();
}
}
}
return mClassStoreSingleton;
}
public static boolean putClassStore(String key, Class<?> clazz) {
// 只有继承自ILass的类才能被添加
if (ILass.class.isAssignableFrom(clazz) && !getClassStore().mClassMap.containsKey(key)) {
getClassStore().mClassMap.put(key,(Class<? extends ILass>) clazz);
return true;
}
return false;
}
public static boolean putObjectClass(String key, Object activity){
if (activity instanceof ILass && !getClassStore().mClassMap.containsKey(key)) {
getClassStore().mClassMap.put(key, (Class<? extends ILass>) activity.getClass());
return true;
}
return false;
}
public static void printAllClass() {
for (String key : getClassStore().mClassMap.keySet()) {
System.out.println("key = " + key + "calss = " + getClassStore().mClassMap.get(key));
}
}
}
技术二: 实现 @StoreAnnotation注解代码,就来回忆一下注解的实现方式了,首先先定义一个注解,完了?
public @interface StoreAnnotation {
}
这也仅仅是定义一个注解,关于这个注解的功能啥也没描述呢,需要为这个注解添加上功能描述,怎么添加呢?依然是用注解的方式,但是这个注解是Java定义好的,每个注解都是对应一个功能描述,这些注解有个统一的名称叫元注解。
这里用到三个常用 @Target @Retention @Repeatable
@Target
它表示注解要作用在哪里,是类上,方法上,还是方法的参数呢。
type | 作用场景 |
ElementType.TYPE | 表示注解作用类、接口... |
ElementType.METHOD | 表示注解是作用在方法上 @Override |
ElementType.FIELD | 注解作用在内部成员变量... |
ElementType.PARAMETER | 可以给一个方法内的参数进行注解 |
ElementType.PACKAGE | 注解作用在包上 |
ElementType.LOCAL_VARIABLE | 可以给局部变量进行注解 |
ElementType.CONSTRUCTOR | 可以给构造方法进行注解 |
ElementType.ANNOTATION_TYPE | 注解在注解上 |
@Retention
它解释表示这个注解的的存活时间,也就是在什么 阶段起作用,采用什么样的方式去处理,这个具体的处理需要用到后面的注解处理器去完成。一共有三种RetentionPolicy.SOURCE,RetentionPolicy.CLASS,RetentionPolicy.RUNTIME,这些区别如下
RetentionPolicy.SOURCE | 注解是在源码阶段处理 (这个就是写代码的时候处理,比如忽略一些错误信息,或这个表示标记一些方法功能,比如@Deprecated,仅仅时在写代码时标记一下这个方法或类被弃用了,使用的时候调用的时候可能不安全啥的,做个标记,开发这个心里有个数。) |
RetentionPolicy.CLASS | 表示这个注解是在编译阶段处理 (程序编译时,靠这个标记调用注解处理器完成一些处理,比如生成一些代码...) |
RetentionPolicy.RUNTIME | 注解在运行阶段处理(程序已经被加载到JVM中运行了,在运行阶段处理) |
@Repeatable
这个很简单,允许一个注解同时在某一个方法,类,....可以被使用一次或者多次,我要用这个功能,来让多个key保存同一个类,例如
"a_class" , "A_class" 这两个都key的value都是A.class
好了,根据功能描述注解应该是下面这个样子,完了?
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface StoreAnnotation {
}
当我们把注解使用在某一个类上时,指定用来存储这个类的key是什么呢?所以这个注解还需要一个能够接受key的功能,也就是要有key(),这里我写了两个,这两个都是可以用来接收key的,key()方法这个好理解,对应名字就能看出来,但是为什么还有一个value()呢?其实这个value()是一个默认的方法,它的好处是在加注解的时候不用去指定传入参数的类型,这个就是默认value方法的好处。
public @interface StoreAnnotation {
//
String[] key() default ""; // 用来接受注解参数key 默认值""
String[] value() default ""; //用来接受注解参数key 默认值""
}
至此,我们在的技术二就全部实现了。应该是这样的。
/**
* @Target
* 声明一个注解,这个注解的作用是将目标类在编译时自动加到ClassStoreSingleton的map中
* TYPE, 作用在类上,表示注解对类生效
* METHOD, 作用在方法上,表示对方法生效 类似 @Override @Deprecated
* FIELD, 作用在成员变量上
* PARAMETER, 作用在方法的参数上
*/
@Target(ElementType.TYPE)
/**
* @Retention
* 表示注解生效的策略是什么
* RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。(编译成class文件不存在)
* RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。 (编译成class文件存在,但JVM会忽略)
* RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们
*/
@Retention(RetentionPolicy.CLASS) //我们的目的是在源码编译时,将所有使用该注解的类存储到map中,所以选择在编译时生效这个策略
//通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
public @interface StoreAnnotation {
String[] value() ; //用来接受注解参数key 默认值""
}
技术三 实现注解处理器+ 反射,在编译时将所注解的代码自定添加到map中,这一步是最关键的,上面仅仅时定义,也就是相当于协议,但是最重要的实现还没有,也就是在编译时对注解的类做处理。下面说一说什么时注解处理器(APT)
APT(Annotation Processing Tool)它是Java编译期注解处理器,它可以让开发人员在编译期对注解进行处理,通过APT可以获取到注解和被注解对象的相关信息,并根据这些信息在编译期按我们的需求生成java代码模板或者配置文件(比如SPI文件或者spring.fatories)等。APT获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能
注解处理器的工作原理大概分为两步
1. 在第一次编译的时候,会在有注解的地方调用注解处理器,生成自动化的代码
此时,工程并没有被编译。
2. 在处理完成第一步后,将所有的代码包括通过注解处理器生成的代码重新打包编译。
总结:就是在编译器处理注解的一种能力,我们上面定义RetentionPolicy.SOURCE和RetentionPolicy.CLASS就是在这时候去处理的。
第一步:定义一个注解处理器AbstractProcessor (所有注解处理器都要继承的基类)。
public class StoreAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
第二步:在process方法中编写我们的处理方法,我这里要生成的类如下
代码如下:
package lei;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.io.File;
import java.io.IOException;
import java.util.*;
@AutoService(Processor.class)
public class StoreAnnotationProcessor extends AbstractProcessor {
private Filer mFiler = null; // 这个就是编译时用于生成代码的对象,所有代码资源文件都是通过它实现, 这个是编译器时生成代码的关键
//打印日志, 不能用print() 因为这个时在编译器执行的
private Messager messager = null;
private Types types = null;
private Elements elements = null;
//注解处理器可用此创建新文件(源文件、类文件、辅助资源文件)
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler(); // 在初始化是获取注解处理器的mFiler
messager = processingEnv.getMessager();
types = processingEnv.getTypeUtils();
elements =processingEnv.getElementUtils();
messager.printMessage(Diagnostic.Kind.NOTE, "init StoreAnnotationProcessor");
}
/**
* 这个要把我们自定义的注解加入到set中,可以传入不止一个注解 传入的注解就是我们要识别的注解
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationSet = new LinkedHashSet<>();
messager.printMessage(Diagnostic.Kind.NOTE, "init StoreAnnotationProcessor");
annotationSet.add(StoreAnnotation.class.getCanonicalName()); // 带有包名的完整类名 com.xxx.xxx.StoreAnnotation.class
return annotationSet;
}
/**
* 这个注解时表示注解支持的Java版本
*
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}
/**
* 这个时注解真正的处理方法,这里面时生成代码的方法
* 这个就是编译时代码生成的精髓
*
* @param annotations 这个就是通过getSupportedAnnotationTypes方法传入的注解处理器所支持的注解 @StoreAnnotation
* @param roundEnv 所有添加该注解的类,方法, 参数....返回的集合 对应的时参入的参数 @Target(ElementType.TYPE)
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 临时存储 key 和 类 value 这里的value是 string 是因为,我们要把它们写入到文件里。
// putMap("xxx", xxx.class) 这个在代码层面其实都是 string 字符
if (mFiler == null || annotations.size() == 0) return false;
Map<String, String> tmpMap = new HashMap<>(); // value 为string 因为我们要把
//1.遍历所有带有注解StoreAnnotation的元素
for (Element annotationElement : roundEnv.getElementsAnnotatedWith(StoreAnnotation.class)) {
// 2. 判断所注解的元素是否是一个类,防御性编程,虽然我们之前时声明了@Target(ElementType.TYPE),但实际上有可能传入的不是一个类
if (annotationElement.getKind() == ElementKind.CLASS) {
// 这里模拟在每个模块下创建一个类,将该模块下所有带StoreAnnotation添加到ClassStoreSingleton的map中
//String claseName = annotationElement.getClass().getCanonicalName(); //这样也能拿到全路径类名
String className = ((TypeElement) annotationElement).getQualifiedName().toString() + ".class";
String[] keys = annotationElement.getAnnotation(StoreAnnotation.class).value(); // 拿到我们的key ,可能有多个
for (String key : keys) {
tmpMap.put(key, className);
}
} else {
throw new IllegalArgumentException("StoreAnnotation can only used in a class but now it used in a "
+ annotationElement.getKind().toString()
+ "type");
}
}
if (tmpMap.size() == 0) return true; // 没有使用该注解的类直接返回
// 3. 生成一个java class 文件
// JavaFileObject classFile;
// try {
// classFile = mFiler.createClassFile("com.lei.annotation."+"ClassStore"+ System.currentTimeMillis());
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// 4.写入类文件 File中,这个是编译时生成的类 我这里用JavaPoet工具,直接用FileStream也可以
StringBuilder sb = new StringBuilder();
// 生成方法体代码
for (String key : tmpMap.keySet()) {
String clazz = tmpMap.get(key);
sb.append("putClassStore(\"").append(key).append("\", ").append(clazz).append("); \n");
}
// 3.1定义一个方法 putClass()
MethodSpec putClass = MethodSpec
.methodBuilder("putClass")
.addModifiers(Modifier.PUBLIC)
.addCode("\n" + sb) //函数体
.build();
//3.2 定义一个类
TypeSpec typeSpec = TypeSpec.classBuilder("ClassStore")
.addModifiers(Modifier.PUBLIC) //声明public
.addMethod(putClass)
.build();
// import static com.clazz.store.ClassStoreSingleton.xxx;
ClassName className = ClassName.get("com.clazz.store","ClassStoreSingleton");
// 定义一个java文件,指定package和类定义
JavaFile javaFile = JavaFile.builder("com.annotation.utils", typeSpec)
.addStaticImport(className, "putClassStore")
.build();
// 将java文件内容写入文件中
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
throw new RuntimeException(e);
}
return true;
}
}
/**
* public ClassStore {
* <p>
* public void pusStore(String key, Class<?> clazz) {
* ClassStoreSingleton.putClass(key,clazz);
* }
* <p>
* }
*/
解释一下,我们这里只是调用了注解处理器让他继承至AbstractProcessor,但这样并会使注解处理的在编译的时候工作,此时我们需要让编译器识别到我们时注解处理器。也就是新建一个target/classes/META-INF/services/javax.annotation.processing.Processor目录下的文件,将自定义的注解处理器写在里面,这里编译器会通过SPI在编译的时候调用到我们的注解处理器去解析。
@AutoService(Processor.class) 我这里用第三方库自动帮我配置完成了,这样最方便。
到这里我们所有写的关于注解处理器的代码就已经完成。接下来就是去编译了,让我们的注解处理器生效。
我新建了一个工程,里面有两个moudle, annotations moulde (注解和注解处理器)和 AnnotationTest moulde(调用注解工程)
接下来就是比较麻烦的配置了
主要是涉及到maven和idea配置
idea配置注解处理器的地方一定要关闭注解处理器。
示例代码,建议用git clone