写在前面:本文是实际工作中学习成果,记为笔记
目录
- 背景
- 什么是注解
- 注解实战:动态注解
- 注解实战:静态注解
- 注解处理器
- 调试注解器
- 注解的问题
- 总结
1. 背景
最近有些时间,突然对注解有些兴趣,很早之前也做过一些关于注解的学习,我的第一篇博客 Sqlla: 数据库操作从未如此简单里面使用到了动态注解技术,是当时学习代理和retrofit时的情况下实现的,后续又迭代了几版。最近的三方库大量使用静态注解的方式,出于学习和纳为己用的目的,打算系统的学习一下,这篇文章就是学习笔记。
2. 什么是注解
一切使用@interface声明的类就是注解
所有的注解继承于Annotation类,好比所有的类继承与Object一样。
public @interface IntentKey /*extends Annotation*/ { }
注解用来标记类,属性,方法,参数,局部变量,包。
@IntentKey
private String name;
注解有三大基本属性:
- Retention作用域
- Target标识目标
- Value常量数据值
完整的注解:可以看出Retention和Target本身也是注解。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface IntentKey { String value() default ""; String intentKey(); }
Retention的取值:源码级别,jar类级别,运行时级别
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
第一个只作用于编译期,打包之后就没有了。比如@Override
第二个作用于jar包类,打包之后还存在,如Android的@Nullable,方便编译期跨包做lint检查
第三个作用于运行期,在运行时可通过反射取得这个注解的信息,如retrofit的@GET注解
@IntentKey(value="abc", intentKey="name") private String name; @IntentKey(intentKey="name2") private String name2;
技巧:当只有一个value的时候,使用时value可以省略。如果有default值,可以不填。
坑点:
public @interface Names {
String[] names1(); Object[] names2(); }
如上,names1是合法的,names2是非法的。原因是注解的值必须是常量,支持的类型如下:
8中基本数据类型,String,Class,Annotation及子类,枚举 以及它们的数组
3. 注解实战:动态注解
动态注解,就是运行时注解。
注解声明:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicIntentKey { String value(); }
标记类:
public class MyActivity extends AppCompatActivity { // 标记 @DynamicIntentKey("key_name") private String dynamicName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my); // 注入 DynamicUtil.inject(this); // 这里使用值 Toast.make(this, "name = " + dynamicName, Toast.SHORT).show(); } }
DynamicUtil.inject(this)负责将值注入
public class DynamicUtil { public static void inject(Activity activity) { Intent intent = activity.getIntent(); // 反射 for (Field field : activity.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(DynamicIntentKey.class)) { // 获取注解 DynamicIntentKey annotation = field.getAnnotation(DynamicIntentKey.class); String intentKey = annotation.value(); // 读取实际的IntentExtra值 Serializable serializable = intent.getSerializableExtra(intentKey); if (serializable == null) { if (field.getType().isAssignableFrom(String.class)) { serializable = ""; } } try { // 插入值 boolean accessible = field.isAccessible(); field.setAccessible(true); field.set(activity, serializable); field.setAccessible(accessible); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }
可以看到解析动态注解使用的是反射的方式,人们常说,反射会有效率问题,后面会验证是或不是。
但是我们能看到优点:
对于使用的地方而言,非常简单了,省去了getXXXExtra的代码,如果这个Intent很多的时候,注解的方式优势就非常明细,添加和删除intent都不需要去改动onCreate的方法,而且inject过程可以放到super类里完成。
这个注解就特别像butterknife对view的注入,而且早期的butterknife确实是使用的同样的动态注解的方式。可是后来,静态注解出现了,如燎原之火般席卷而来。接下来是静态注解。
4. 注解实战:静态注解
静态注解,就非常好理解了,在编译期解释注解,并做一些生成操作。三种作用域都可以用来标示静态注解。
注解声明:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface StaticIntentKey { String value(); }
标记类:
public class MyActivity extends AppCompatActivity { // 标记 @StaticIntentKey("key_name") private String staticName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my); // 静态注入 StaticUtil.inject(this); // 这里使用值 Toast.make(this, "name = " + staticName, Toast.SHORT).show(); } }
上面为止和动态注入都是一模一样的。
下面是静态注入的核心实现:
public class StaticUtil { public static void inject(Activity activity) { com.alpha.staticinject.StaticMapper.bind(activity); } }
???com.alpha.staticinject.StaticMapper是个什么东西?
这个不是东西,他就是静态注解的核心:代码生成。这个类就是静态注解处理器在编译时期生成的一个辅助类。
下面两个类就是生成的类:
public final class StaticMapper { public static final void bind(Activity activity) { if (activity instanceof IntentActivity) { IntentActivity$Binder binder = new IntentActivity$Binder(); binder.bind((IntentActivity) activity); } } }
public final class IntentActivity$Binder { public static final void bind(IntentActivity activity) { Intent intent = activity.getIntent(); if (intent.hasExtra("key_name")) { activity.staticName = (String) intent.getSerializableExtra("key_name"); } } }
这就是注入值的过程。但是这两个类是怎么样生成的呢?
5. 注解处理器
Java在1.7加入了Processor类,用于处理编译时注解。通过这个类我们可以在某些目录下生成某些类,这些类可以辅助我们开发,让我们更关注业务逻辑。
public interface Processor { Set<String> getSupportedOptions(); Set<String> getSupportedAnnotationTypes(); SourceVersion getSupportedSourceVersion(); void init(ProcessingEnvironment var1); // 处理类在这里完成 boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2); Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4); }
编写一个自定义注解的四条步骤:
- 创建一个java library:intent-apt (这是我项目的名字)
- 编写一个IntentProcessor继承与Processor的抽象类AbstractProcessor。
- 将IntentProcessor作为一种服务放入标记到jar包中的META_INF文件夹中
- 在其他项目中,如android项目中使用 annotationProcessor project(':intent-apt')引入依赖
走一遍:
- 创建java library很简单,不细说
- 创建IntentProcessor
public class IntentProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) { // 在这里写你要生成的类 } }
但是这个IntentProcessor现在还没有什么核心内容,下一步我们给他添加一些东西。
要注解器能好用,我们得先要有注解:
1>. 提供注解的library
![](http://upload-images.jianshu.io/upload_images/3346029-4e3dddd2fa554e15.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/328/format/webp)
2>. 在intent-apt中引入这个library
dependencies {
...
implementation project(':intent-key')
}
3>. 上面描述的静态注解生成过程是如下这样的:
(1) 获取需要注解的类F,这里是所有有StaticIntentKey标记的类。
(2) 针对每一个类F,生成同包下的F$Binder类,用于实际的绑定
(3) 将所有类合起来生成一个StaticMapper的集线器类,用于分发绑定
完整的类如下:
public class IntentProcessor extends AbstractProcessor { private TypeName activityClassName = ClassName.get("android.app", "Activity").withoutAnnotations(); private TypeName intentClassName = ClassName.get("android.content", "Intent").withoutAnnotations(); @Override public SourceVersion getSupportedSourceVersion() { // 支持java1.7 return SourceVersion.RELEASE_7; } @Override public Set<String> getSupportedAnnotationTypes() { // 只处理 StaticIntentKey 注解 return Collections.singleton(StaticIntentKey.class.getCanonicalName()); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) { // StaticMapper的bind方法 MethodSpec.Builder method = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addParameter(activityClassName, "activity"); // 查找所有的需要注入的类描述 List<InjectDesc> injectDescs = findInjectDesc(set, re); // log一下 System.out.println(injectDescs); for (int i1 = 0; i1 < injectDescs.size(); i1++) { InjectDesc injectDesc = injectDescs.get(i1); // 创建需要注解的类的Java文件,如上面所述的 IntentActivity$Binder TypeName injectedType = createInjectClassFile(injectDesc); TypeName activityName = typeName(injectDesc.activityName); // $T导入类型 // 生成绑定分发的代码 method.addCode((i1 == 0 ? "" : " else ") + "if (activity instanceof $T) {\n", activityName); method.addCode("\t$T binder = new $T();\n", injectedType, injectedType); method.addCode("\tbinder.bind((IntentActivity) activity);\n", activityName, activityName); method.addCode("}"); } // 创建StaticMapper类 createJavaFile("com.alpha.staticinject", "StaticMapper", method.build()); return false; } /** * 创建Java文件 * @param pkg 包名 * @param classShortName 类的简短名,如java.lang.String的简短名就是String * @param method 方法列表 */ private void createJavaFile(String pkg, String classShortName, MethodSpec... method) { TypeSpec.Builder builder = TypeSpec.classBuilder(classShortName) .addModifiers(Modifier.PUBLIC, Modifier.FINAL); for (MethodSpec spec : method) { builder.addMethod(spec); } TypeSpec clazzType = builder.build(); try { JavaFile javaFile = JavaFile.builder(pkg, clazzType) .addFileComment(" This codes are generated automatically. Do not modify!") .indent(" ") .build(); // write to file javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } /** * 生成需要注解的类 如 IntentActivity 会生成 IntentActivity$Binder * * @param injectDesc 注解类的描述信息 * @return 如IntentActivity$Binder */ private TypeName createInjectClassFile(InjectDesc injectDesc) { ClassName activityName = className(injectDesc.activityName); ClassName injectedClass = ClassName.get(activityName.packageName(), activityName.simpleName() + "$Binder"); MethodSpec.Builder method = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addParameter(activityName, "activity"); // $T导入作为类,$N导入作为纯值,$S导入作为字符串 method.addStatement("$T intent = activity.getIntent()", intentClassName); for (int i = 0; i < injectDesc.fieldNames.length; i++) { TypeName fieldTypeName = typeName(injectDesc.fieldTypeNames[i]); method.addCode("if (intent.hasExtra($S)) {\n", injectDesc.intentNames[i]); method.addCode("\tactivity.$N = ($T) intent.getSerializableExtra($S);\n", injectDesc.fieldNames[i], fieldTypeName, injectDesc.intentNames[i]); method.addCode("}\n"); } // 生成最终的XXX$Binder文件 createJavaFile(injectedClass.packageName(), injectedClass.simpleName(), method.build()); return injectedClass; } // 查找所有的需要注入的类描述 private List<InjectDesc> findInjectDesc(Set<? extends TypeElement> set, RoundEnvironment re) { Map<TypeElement, List<String[]>> targetClassMap = new HashMap<>(); // 先获取所有被StaticIntentKey标示的元素 Set<? extends Element> elements = re.getElementsAnnotatedWith(StaticIntentKey.class); for (Element element : elements) { // 只关心类别是属性的元素 if (element.getKind() != ElementKind.FIELD) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field"); continue; } // 此处找到的是类的描述类型 // 因为我们的StaticIntentKey的注解描述是field,所以closingElement元素是类 TypeElement classType = (TypeElement) element.getEnclosingElement(); System.out.println(classType); // 对类做缓存,避免重复 List<String[]> nameList = targetClassMap.get(classType); if (nameList == null) { nameList = new ArrayList<>(); targetClassMap.put(classType, nameList); } // 被注解的值,如staticName String fieldName = element.getSimpleName().toString(); // 被注解的值的类型,如String,int String fieldTypeName = element.asType().toString(); // 注解本身的值,如key_name String intentName = element.getAnnotation(StaticIntentKey.class).value(); String[] names = new String[]{fieldName, fieldTypeName, intentName}; nameList.add(names); } List<InjectDesc> injectDescList = new ArrayList<>(targetClassMap.size()); for (Map.Entry<TypeElement, List<String[]>> entry : targetClassMap.entrySet()) { String className = entry.getKey().getQualifiedName().toString(); System.out.println(className); // 封装成自定义的描述符 InjectDesc injectDesc = new InjectDesc(); injectDesc.activityName = className; List<String[]> value = entry.getValue(); injectDesc.fieldNames = new String[value.size()]; injectDesc.fieldTypeNames = new String[value.size()]; injectDesc.intentNames = new String[value.size()]; for (int i = 0; i < value.size(); i++) { String[] names = value.get(i); injectDesc.fieldNames[i] = names[0]; injectDesc.fieldTypeNames[i] = names[1]; injectDesc.intentNames[i] = names[2]; } injectDescList.add(injectDesc); } return injectDescList; } /** * 快速获取一个类的TypeName结构 * * @param className 类完整名,如java.lang.String,也可能是int, boolean */ private TypeName typeName(String className) { return className(className).withoutAnnotations(); } /** * 快速获取一个类的ClassName结构,ClassName是TypeName的子类 * * @param className 类完整名,如java.lang.String,也可能是int, boolean */ private ClassName className(String className) { // 基础类型描述符 if (className.indexOf(".") <= 0) { switch (className) { case "byte": return ClassName.get("java.lang", "Byte"); case "short": return ClassName.get("java.lang", "Short"); case "int": return ClassName.get("java.lang", "Integer"); case "long": return ClassName.get("java.lang", "Long"); case "float": return ClassName.get("java.lang", "Float"); case "double": return ClassName.get("java.lang", "Double"); case "boolean": return ClassName.get("java.lang", "Boolean"); case "char": return ClassName.get("java.lang", "Character"); default: } } // 手动解析 java.lang.String,分成java.lang的包名和String的类名 String packageD = className.substring(0, className.lastIndexOf('.')); String name = className.substring(className.lastIndexOf('.') + 1); return ClassName.get(packageD, name); } /** * 需要注解的类的描述信息 如 IntentActivity */ public static class InjectDesc { // IntentActivity public String activityName; // {staticName, staticAge} public String[] fieldNames; // {java.lang.String, java.lang.Integer} public String[] fieldTypeNames; // {key_name, key_age} public String[] intentNames; @Override public String toString() { return "InjectDesc{" + "activityName='" + activityName + '\'' + ", fieldNames=" + Arrays.toString(fieldNames) + ", intentNames=" + Arrays.toString(intentNames) + '}'; } } }
StringBuilder一行一行写生成的类的代码非常无味,还容易出错,所以上面用到了square公司出品的javapoet, 方便生成类。
添加javapoet
dependencies {
...
implementation 'com.squareup:javapoet:1.9.0'
}
-
想java系统注册服务,这个服务注册应该见过,在编写自定义Gradle插件的时候也需要这么做。
注册服务
javax.annotation.processing.Processor 文件的内容如下:com.icourt.intent_apt.IntentProcessor
实际就是这个IntentProcessor的完整类名。
当然,这么写很不优雅,不一定记得住,所以google做了一个注解,方便快速的帮我们生成这个文件,只需在IntentProcessor头部注解一下
// AutoService本身就是一个静态注解,他在build/META-INF文件夹下生成了一个service指定文件
@AutoService(Processor.class)
public class IntentProcessor extends AbstractProcessor { ... }
添加这个注解需要加入依赖
dependencies {
// google auto service注解库
implementation 'com.google.auto.service:auto-service:1.0-rc2'
}
它生成的文件在build下面
![](http://upload-images.jianshu.io/upload_images/3346029-8803e499664fc630.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/533/format/webp)
这两种方法只需要一种就可以,最终会打包到jar里面。
- 在需要提供编译时注解的项目(比如app)的build.gradle文件的dependences节点下添加
dependencies {
...
annotationProcessor project(':intent-apt')
}
intent-apt项目本身完整的build.gradle
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.google.auto.service:auto-service:1.0-rc2' implementation 'com.squareup:javapoet:1.9.0' implementation project(':intent-key') } sourceCompatibility = "1.7" targetCompatibility = "1.7"
好,自此,一个完整的静态注解器就完成了,可以马上用来实验。只需要build就可以使用,他会在你的目标项目(如app)的build下生成相关的类:
![](http://upload-images.jianshu.io/upload_images/3346029-1d1066edeb7b23fc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/433/format/webp)
6. 调试注解器
有的时候,生成过程出错了,但我不确定是哪一步出错了,想要定位,有两种方法:
- 日志定位,方便快捷
// 1. sout
System.out.println(xxx)
// 2. messager
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field");
- 断点调试
有三步要配置:
第一步 配置Debug后台服务
在gradle.properties文件中加入下面两句话,然后sync一下项目(或者在控制台执行./gradlew --daemon
),会开启一个远程debug_server
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
第二步 配置Remote Debugger
在AS中创建一个RemoteDebugger
![](http://upload-images.jianshu.io/upload_images/3346029-4a303b8dfe5443d4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/294/format/webp)
![](http://upload-images.jianshu.io/upload_images/3346029-b4dc6441a9c08287.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp)
![](http://upload-images.jianshu.io/upload_images/3346029-37fe933b4216723a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp)
属性和参数和上一步的配置一样,(不修改就行)
![](http://upload-images.jianshu.io/upload_images/3346029-e17b8935e897bb8a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/253/format/webp)
切换到刚才创建的'运行配置',点击debug按钮。
第三步 执行编译过程
在需要的位置打开断点,在控制台输入
./gradlew assembleDebug
或者,带清除功能的编译
./gradlew clean assembleDebug
不出意外就会走到打开的断点,然后尽情的调试。
7. 注解的问题
(1) 性能问题:
1000次运行的效果对比
DynamicUtil.inject(this);
StaticUtil.inject(this);
(2) 代码量:
注解对于业务逻辑的代码整体减少了,但是静态注解会产生 生成代码,这些代码会占用方法数,也增加编译时间。
(3) 错误定位:
注解的错误定位会很麻烦,如果注解处理器出了bug,较难定位。
(4) 阅读性:
注解会整体影响阅读性,出现逻辑断层。
8. 总结
本文围绕两点展开:
- 使用注解(注)
- 编写注解(解)
使用注解非常有优势,让开发人员优化代码,关注业务。但是编写注解处理器本身是个技术活,好的处理器对使用者而言会事半功倍,但也不要滥用,做好错误处理。
作者:南国生红豆
链接:https://www.jianshu.com/p/aeb93a3d33d1
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。