序言
注解是Java程序和Android程序中常见的语法,之前虽然知道有这么个东西,但并没有深入了解注解。写EventBus源码解析和ButterKnife源码解析的时候,发现注解在其中起到很大作用,就决定专门写一篇文章介绍注解。
下面将会从这几个方面展开介绍:
- 注解的概念和语法
- 运行时注解
- 编译时注解(APT技术)
- 对比运行时和编译时注解
- 总结
注解的概念和语法
1. 注解的概念
定义:注解用于为Java提供元数据,作为元数据,注解不影响代码执行,但某些类型注解也可以用于这一目的,注解从Java5开始引入
2. 注解的语法
注解通过@interface
关键字来定义。
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface MyAnnotation {}
3. 元注解
在上面的定义中,Retention
和Target
是什么东西?它们为什么能够修饰注解。
实际上,它们是元注解:元注解是可以注解到注解上的注解,简单来说就是一种基本注解,可以作用到其他注解上。
Java中总共有5中元注解:@Retention,@Documented,@Target,@Inherited,@Repeatable
。下面分别介绍它们:
@Retention
用来说明注解的存活时间,有三种取值:
- RetentionPolicy.SOURCE:注解只在源码阶段保留,编译器开始编译时它将被丢弃忽视
- RetentionPolicy.CLASS:注解会保留到编译期,但运行时不会把它加载到JVM中
- RetentionPolicy.RUNTIME:注解可以保留到程序运行时,它会被加载到JVM中,所以程序运行过程中可以获取到它们
编译期注解和运行时注解使用得比较多,下面会有两个主题专门介绍。
@Target
指定注解可作用的目标,取值如下:
- ElementType.PACKAGE:可作用在包上
- ElementType.TYPE:可作用在类、接口、枚举上
- ElementType.ANNOTATION_TYPE:可以作用在注解上
- ElementType.FIELD:可作用在属性上
- ElementType.CONSTRUCTOR:可作用在构造方法上
- ElementType.METHOD:可作用在方法上
- ElementType.PARAMETER:可作用在方法参数上
- ElementType.LOCAL_VARIABLE:可作用在局部变量上,例如方法中定义的变量
它接收一个数组作为参数,即可以指定多个作用对象,就像上面的Demo:
@Target({ElementType.FIELD, ElementType.TYPE})
@Documented
从名字可知,这个注解跟文档相关,它的作用是能够将注解中的元素包含到Javadoc中去。
@Inherited
Inherited是继承的意思,但并不是注解本身可被继承,而是指一个父类SuperClass被该类注解修饰,那么它的子类SubClass如果没有任何注解修饰,就会继承父类的这个注解。
举个栗子:
@Inherited
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}
@Test
public class A {}
public class B extens A {}
解释:注解Test被@Inherited修饰,A被Test修饰,B继承A(B上又无其他注解),那么B就会拥有Test这个注解。
@Repeatable
这个词是可重复的意思,它是java1.8引入的,算一个新特性。
什么样的注解可以多次应用来呢,通常是注解可以取多个值,举个栗子:
public @Interface Persons {
Person[] value();
}
@Repeatable(Persons.class)
public @Interface Person {
String role() default ""
}
@Person("artist")
@Person("developer")
@Person("superman")
public class Me {}
解释:@Person被@Repeatable修饰,所以Person可以多次作用在同一个对象Me上,而Repeatable接收一个参数,这个参数是个容器注解,用来存放多个@Person。
4. 注解的属性
注解中可以定义属性,也可以叫成员变量,不能定义方法。
就如上面的例子:
- @Person中定义了一个属性role,在使用的过程中就可以传一个字符串
- 又给role设置了默认值为空字符串,以就算不传可以直接使用
- 如果有多个属性,就必须以
key=value
的形式指定属性值
注解中的属性支持8种基本类型外加字符串、类、接口、注解及以上类型的数组
5. Java预置注解
Java中提供了很多注解,如:
- @Override:表示覆写父类中的方法
- @Depracated:标记过时的类、方法、成员变量
- @FunctionalInterface:Java1.8引入的新特性,表示函数式接口(只有一个方法的普通接口),主要用于lambda表达式。
- ......
运行时注解
上面介绍过,用Retention(RetentionPolicy.RUNTIME)
修饰的就是运行时注解。使用这种注解,多数情况是为了在运行时做一些事情。至于具体做什么事?就看各位同学自己的意愿了。
这里,我通过一个例子来介绍怎么使用运行时注解。
现在,我打算通过运行时注解实现一个功能,跟ButterKnife类似,即自动注入功能,不需要我手动调用findViewById。
下面是实现的步骤:
1. 定义注解
/**
* author : user_zf
* date : 2018/11/6
* desc : 运行时通过反射自动注入View,不再需要写findViewById
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectView {
@IdRes int id() default -1;
}
这个注解中有一个属性id,表示待注入控件的id
2. 定义注解解析工具
/**
* author : user_zf
* date : 2018/11/6
* desc : 用来解析注解InjectView
*/
public class AnnotationUtil {
/**
* 解析注解InjectView
*
* @param activity 使用InjectView的目标对象
*/
public static void inject(Activity activity) {
Field[] fields = activity.getClass().getDeclaredFields();
//通过该方法设置所有的字段都可访问,否则即使是反射,也不能访问private修饰的字段
AccessibleObject.setAccessible(fields, true);
for (Field field : fields) {
boolean needInject = field.isAnnotationPresent(InjectView.class);
if (needInject) {
InjectView anno = field.getAnnotation(InjectView.class);
int id = anno.id();
if (id == -1) continue;
View view = activity.findViewById(id);
Class fieldType = field.getType();
try {
//把View转换成field声明的类型
field.set(activity, fieldType.cast(view));
} catch (Exception e) {
Log.e(InjectView.class.getSimpleName(), e.getMessage());
}
}
}
}
}
主要是通过反射,找到Activity中使用了@InjectView的字段,然后通过findViewById来初始化控件。
3. 使用注解
class MainActivity : AppCompatActivity() {
@InjectView(id = R.id.tvHello)
private var tvHello: TextView? = null
@InjectView(id = R.id.btnHello)
private var btnHello: Button? = null
@InjectView(id = R.id.rlRoot)
private var rlRoot: RelativeLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//通过注解初始化控件
AnnotationUtil.inject(this@MainActivity)
//设置控件
tvHello?.text = "Hello World!"
btnHello?.text = "Hello Button"
btnHello?.setOnClickListener {
Toast.makeText(this@MainActivity, "点击按钮了", Toast.LENGTH_SHORT).show()
}
rlRoot?.setBackgroundColor(resources.getColor(R.color.colorAccent, null))
}
}
在控件上使用注解,就不需要我们手动初始化,注解解析工具会自动帮我们初始化。大大减少重复代码。
原理:运行时注解主要通过反射进行解析,代码运行过程中,通过反射我们可以知道哪些属性、方法使用了该注解,并且可以获取注解中的参数,做一些我们想做的事情
编译时注解(APT技术)
使用Retention(RetentionPolicy.CLASS)
修饰的注解就是编译时注解。
说到编译时注解,就需要引出我们今天的主角:APT(编译时解析技术)。
APT技术主要是通过编译期解析注解,并且生成java代码的一种技术,一般会结合Javapoet技术来生成代码。
下面,我们还是通过一个栗子来介绍APT技术。
在写Bean的时候经常需要写Getter和Setter方法,我们想通过一个注解,在编译的过程中自动帮我们生成Getter和Setter方法,这里会生成一个新的类,而不是修改原来的类。
1. 编写注解
/**
* author : user_zf
* date : 2018/11/7
* desc : 编译期给bean生成getter和setter方法的注解(限java类使用)
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface GenerateGS {}
2. 编写注解解析器Processor
这里的注解解析器和运行时注解解析器不一样,这里需要继承AbstractProcessor类。
在Android Module和Android Library Module中是不能使用AbstractProcessor类的,需要新建一个Java Library Module,把注解解析器放在这个Java Module中,然后用Android Module依赖这个Java Module。
接下来,看一下我们的GenerateGSProcessor的实现:
/**
* author : user_zf
* date : 2018/11/7
* desc : generateGS编译时注解解析器
*/
//@AutoService(Processor.class)
//@SupportedAnnotationTypes("study.com.aptlib.GenerateGS")
//@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class GenerateGSProcessor extends AbstractProcessor {
private Filer mFiler;
/**
* 初始化Processor和一些工具类
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
}
/**
* 返回该Processor能够处理的注解
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
LinkedHashSet<String> types = new LinkedHashSet<>();
types.add(GenerateGS.class.getCanonicalName());
return types;
}
/**
* 返回Java的版本号
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 真正处理注解的方法
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
HashMap<String, HashSet<Element>> nameMap = new HashMap<>();
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(GenerateGS.class);
//遍历处理带注解的Element,把他们分类保存在Map中,key=包裹类 value=类中所有使用注解的Element
for (Element element : annotatedElements) {
Element parent = element.getEnclosingElement();
String parentName = parent.getSimpleName().toString();
HashSet<Element> set = nameMap.get(parentName);
if (set == null) {
set = new HashSet<>();
}
set.add(element);
nameMap.put(parentName, set);
}
generateJavaFile(nameMap);
return true;
}
/**
* 根据Map生成Java文件
*/
private void generateJavaFile(Map<String, HashSet<Element>> map) {
System.out.println("开始生成代码");
Set<Map.Entry<String, HashSet<Element>>> nameSet = map.entrySet();
for(Map.Entry<String, HashSet<Element>> entry : nameSet) {
String className = entry.getKey();
Set<Element> fields = entry.getValue();
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(className + "$Bean")
.addModifiers(Modifier.PUBLIC);
//遍历添加属性和对应的getter/setter方法
for (Element element : fields) {
//只处理field
if (element.getKind().isField()) {
//获取字段名称
String fieldName = element.getSimpleName().toString();
//字段名称首字母变成大写
char[] cs = fieldName.toCharArray();
cs[0] -= 32;
String firstUpperName = String.valueOf(cs);
//获取字段类型
TypeName type = TypeName.get(element.asType());
//生成字段
FieldSpec fieldSpec = FieldSpec.builder(type, fieldName, Modifier.PRIVATE).build();
//生成getter/setter方法
MethodSpec getterMethod = MethodSpec.methodBuilder("get" + firstUpperName)
.addModifiers(Modifier.PUBLIC)
.returns(type)
.addStatement("return " + fieldName)
.build();
MethodSpec setterMethod = MethodSpec.methodBuilder("set" + firstUpperName)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(type, fieldName)
.addStatement("this." + fieldName + " = " + fieldName)
.build();
//给$Bean添加字段及对应的getter和setter方法
typeSpecBuilder.addField(fieldSpec)
.addMethod(getterMethod)
.addMethod(setterMethod);
}
}
TypeSpec typeSpec = typeSpecBuilder.build();
JavaFile javaFile = JavaFile.builder("study.com.aptlib", typeSpec).build();
try {
javaFile.writeTo(mFiler);
System.out.println("生成" + className + "$Bean" + "类");
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("代码生成完毕");
}
}
GenerateGSProcessor中主要有四个方法:init方法,getSupportedAnnotationTypes方法,getSupportedSourceVersion方法和process方法。代码注释中给出了这四种方法的作用,其中最主要的方法就是process方法, 这个方法就是用来解析注解的。而getSupportedAnnotationTypes和getSupportedSourceVersion两个方法可以用注解来替代,分别是@SupportedAnnotationTypes
和@SupportedAnnotationTypes
,在GenerateGsProcessor的注释中可以看到他们。
有人会问,除了那两个注解之外,还有一个@AutoService
注解,这个是干嘛的呢?
别着急,下面我们会介绍的。
3. 添加SPI配置文件
我们先来简单介绍一下SPI(服务提供接口Service Provider Interface)机制,主要做作用是为接口寻找服务实现。
举个栗子,我们现在有三个模块:common、A、B,并且A和B都依赖与common。现在,common模块中有个接口Fly(有一个fly方法)而A中定义Fly的实现类Bird,B中定义Fly的实现类Butterfly。
在A和B中都添加配置文件,A的配置文件中写上Bird的带包全名,B的配置文件中写上Butterfly带包全名,接着在需要使用的地方,A和B都可以使用下面一段代码:
ServiceLoader<Fly> serviceLoader = ServiceLoader.load(Fly.class, Fly.class.getClassLoader());
Iterator<Fly> it = serviceLoader.iterator();
if (it.hasNext()) {
it.next().fly();
}
这样,在A中的效果就是Bird在飞,B中的效果是Butterfly在飞。有点类似于策略模式,可以通过配置文件动态加载。
接下来,总结一下配置方法:
1、定义接口和接口实现类
2、创建resources/META-INF/services目录
3、在该目录下创建一个文件,文件名为接口名(带包全名),内容为接口实现类的带包全名
4、在代码中通过ServiceLoader动态加载并且调用实现类的内部方法。
好,现在让我们回到APT技术来,APT技术中的Processor
就使用了SPI机制,接口是Process,实现类是GenerateGSProcessor,所以我们需要做下面几件事:
-
在main目录下创建resources/META-INF/services目录
-
在该目录下新建javax.annotation.processing.Processor文件
- 在文件中添加内容study.com.aptlib.GenerateGSProcessor
study.com.aptlib.GenerateGSProcessor
到这里SPI配置完毕。
可能大家会觉得这种配置方式比较麻烦,对,确实比较麻烦。我们可以使用Google提供的auto-service库来简化这些操作:
compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.google.auto:auto-common:0.10'
然后在GenerateGSProcessor类上添加注解:
@AutoService(Processor.class)
这就是上面提到的AutoService,用这个注解可以替代SPI的配置文件。
4. 在Android Module使用注解
首先,添加项目依赖
annotationProcessor project(':aptlib')
api project(':aptlib')
这里为什么要添加两次呢?
- annotationProcessor:指定专门的注解解析库
- api:表示添加注解依赖,因为我们的GenerateGs写在aptlib库,所以需要单独添加这个依赖,如果注解和解析器放在不同的module,就不需要这么写
接下来,在代码中使用注解:
public class Person {
@GenerateGS
private String name;
@GenerateGS
private int gender;
@GenerateGS
private String hobby;
}
通过rebuild来编译我们的项目,就会生成Person$Bean类:
package study.com.aptlib;
import java.lang.String;
public class Person$Bean {
private String hobby;
private String name;
private int gender;
public String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getGender() {
return gender;
}
public void setGender(int gender) {
this.gender = gender;
}
}
有没有很神奇。
对比运行时和编译时注解
在很多情况下,运行时注解和编译时注解可以实现相同的功能,比如依赖注入框架,我们既可以在运行时通过反射来初始化控件,也可以再编译时就生成控件初始化代码。那么,这两者有什么区别呢?
答:编译时注解性能比运行时注解好,运行时注解需要使用到反射技术,对程序的性能有一定影响,而编译时注解直接生成了源代码,运行过程中直接执行代码,没有反射这个过程。
很多框架的实现都是用到了编译时注解,如ButterKnife、EventBus、Dagger2等等。
项目中使用这些库的时候,会有一个比较让人疑惑的地方。就拿ButterKnife举例。
我们使用ButterKnife时,会添加依赖:
annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'
但我们在项目结构中怎么也找不到ButterKnifeProcessor和编译库的代码。这个是为什么呢?
经过一番研究,总算知道原因了,一些插件库、注解解析库并不会放在项目结构中,而是会放在gradle的缓存目录中:
/Users/user_zf/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-compiler/8.6.0/d3defb48a63aa0591117d0cec09f47a13fffda19
,在这个路径中,总算找到了butterknife-compiler-8.6.0.jar
。
总结
经过上面的介绍,相信大家对注解有了比较全面的认识。各位同学可以尝试在项目开发过程中去使用注解,它可以大大提升我们的开发效率,减少不必要的重复代码。