注解
注解分类
注解分为标准注解和元注解
- 标准注解
- @Override:对覆盖超类中的方法进行标注,如果被标注的方法并没有实际覆盖超类中的方法,则编译器会发出警告
- @Deprecated:对不鼓励使用或已经过时的方法添加注解,当编程人员使用这些方法时,会在编译时显示错误信息
- @SuppressWarning:选择性的取消特定代码段中的警告
- @SafeVarargs:JDK7新增,用于声明可变长度参数的方法,其在与泛型类一起使用时不会出现类型安全问题
- 元注解
- @Target:注解所修饰的对象范围
- @Inherited:表示注解可以被继承
- @Documented:表示这个注解应该被JavaDoc工具记录
- @Retention:用来声明注解的保留策略
- @Repeatable:JDK8新增,允许一个注解在同一声明类型中多次使用
其中,@Target是一个ElementType类型的数组,有以下几种取值,对应不同的对象范围
- ElementType.TYPE:修饰类,接口和枚举类型
- ElementType.FIELD:修饰成员变量
- ElementType.METHOD:修饰方法
- ElementType.PARAMETER:修饰参数
- ElementType.CONSTRUCTOR:修饰构造方法
- ElementType.LOCAL_VARIABLE:修饰局部变量
- ElementType.ANNOTATION_TYPE:修饰注解
- ElementType.PACKAGE:修饰包
- ElementType.TYPE_PARAMETER:类型参数声明
- ElementType.TYPE_USE:使用类型
@Retention注解有三种类型,分别表示不同级别的保留策略
- RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在Java源码中,源码在编译后注解信息被丢弃,不会保留在.class文件中
- RetentionPolicy.CLASS:编译时注解。注解信息会保留在Java源码和.class文件中,运行Java程序时,JVM会丢弃该注解信息,不会保留在JVM中
- RetentionPolicy.RUNTIME:运行时注解。运行Java程序时,JVM会保留该注解信息,可通过反射获取该注解信息
定义注解
1. 基本定义
定义新的注解类型用@interface关键字,与定义一个接口很像,如下
public @interface Swordsman {
}
定义完注解后,就可以在类中使用
@Swordsman
public class test{
}
2. 定义成员变量
注解只有成员变量,没有方法。注解的成员变量在注解定义中以“无形参的方法”形式来声明,其“方法名”定义了该成员变量的名字,其返回值定义了该成员变量的类型
public @interface Swordsman {
String name();
int age();
}
这里定义了两个成员变量,这两个成员变量以方法的形式来定义,定义了成员变量后,使用该注解时就应该为该注解的成员变量指定值
public class test{
@Swordsman(name = "zwj", age = 23) public void fighting(){
}
}
也可以在定义注解的成员变量时,使用default关键字为其指定默认值,如下
public @interface Swordsman {
String name() default "zwj";
int age() default 23;
}
所以使用时可无需显示指定
public class test{
@Swordsman public void fighting(){
}
}
3. 定义运行时注解
可以用@Retention来指定注解的保留策略,上文所述的三个策略的生命周期长度为SOURCE<CLASS<RUNTIME,对于生命周期短的能起作用的地方,生命周期长的也可以起作用,一般如果要在运行时动态获取注解信息,那么只能用RetentionPolicy.RUNTIME。如果要在编译时进行一些预处理操作,比如生成一些辅助代码,就用RetentionPolicy.CLASS。如果要做一些检查性操作,比如@Override和@SuppressWarning,则可选用RetentionPolicy.SOURCE,当设定为RetentionPolicy.RUNTIME时,这个注解就是运行时注解,如下
@Retention(RetentionPolicy.RUNTIME)
public @interface Swordsman {
String name() default "zwj";
int age() default 23;
}
4. 定义编译时注解
同样,如果将@Retention的保留策略设定为RetentionPolicy.CLASS,这个注解就是编译时注解,如下
@Retention(RetentionPolicy.CLASS)
public @interface Swordsman {
String name() default "zwj";
int age() default 23;
}
注解处理器
如果没有处理注解的工具,那么注解就不会有什么大作用,对于不同的注解,有不同的注解处理器,虽然注解处理器的编写千变万化,但是也有处理标准。比如针对运行时注解会采用反射机制,对于编译时注解会采用AbstractProcessor处理
1. 运行时注解处理器
处理运行时注解需要用到反射机制,首先定义运行时注解,如下
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GET {
String value() default "";
}
定义了GET注解,应用于方法,接下来应用这个注解
public class AnnotationTest {
@GET(value = "http://baidu.com/59.102.53.24") public String getIpMsg(){
return "";
}
@GET(value = "http://baidu.com") public String getIp(){
return "";
}
}
上面代码为@GET的成员变量赋值,接下来编写注解处理器
public class AnnotationProcessor {
public static void main(String[] args) {
Method[] methods = AnnotationTest.class.getDeclaredMethods();
for(Method method : methods){
GET get = method.getAnnotation(GET.class);
System.out.println(get.value());
}
}
}
上面代码用了两个反射方法,都属于AnnotatedElement接口,Class、Method、Field等类都实现了这个接口。调用getAnnotation方法返回指定类型的注解对象,也就是GET,最后调用GET的value方法返回从GET对象中所提取的元素的值即可
2. 编译时注解处理器
处理编译时注解的步骤稍微多些,仍旧需要先定义注解。在项目中新建一个Java Library来存放注解,这个Library命名为annotations。接下来定义注解,如下
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView{
int value() default 1;
}
这段代码中定义的注解类似于ButterKnife的@BindView注解。接下来编写注解处理器,再次新建一个Java Library存放注解处理器,这个Library名为processor,先来配置其gradle
plugins {
id 'java-library'
}
apply plugin:'java'
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
dependencies {
implementation fileTree(includes: ['*.jar'], dir: 'libs')
implementation project(':annotations')
}
接下来编写注解处理器ClassProcessor,继承自AbstractProcessor,如下
public class ClassProcessor extends AbstractProcessor {
@Override public synchronized void init(ProcessingEnvironment processingEnvironment){
super.init(processingEnvironment);
}
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return true;
}
@Override public Set<String> getSupportedAnnotationTypes(){
Set<String> annotations = new LinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
@Override public SourceVersion getSupportedSourceVersion(){
return SourceVersion.latestSupported();
}
}
介绍下重写的四个方法的作用
- init:被注解处理工具调用,输入ProcessingEnvironment参数,这个参数代表的类提供了很多有用的工具类,比如Elements,Types,Filer,Messager等
- process:相当于每个处理器的主函数main,在这里编写扫描,评估和处理注解的代码,以及生成Java文件,输入参数RoundEnvironment,可以查询出包含特定注解的被注解元素
- getSupportedAnnotationTypes:这是必须指定的方法,指定这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含该处理器想要处理的注解类型的合法全称
- getSupportedSourceVersion:用于指定你使用的Java版本,通常这里返回SourceVersion.latestSupported()
接下来编写还未实现的process方法,如下
@Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Messager messager = processingEnv.getMessager();
for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
if (element.getKind() == ElementKind.FIELD){
messager.printMessage(Diagnostic.Kind.NOTE, "printMessage:" + element.toString());
}
}
return true;
}
这里用到了Messager的printMessage来打印注解修饰的成员变量的名称,这个名称会在Console窗口中打印
接着,为了能使用注解处理器,我们需要一个服务文件来注册它,采用谷歌开源的AutoService,可以帮助开发者生成META-INF.services/javax.annotation.processing.Processor文件。点击“File” -> “Project Structure”,搜索“auto-service”来查找该库并添加
implementation 'com.google.auto.service:auto-service:1.0.1'
之后在注解处理器ClassProcessor中添加注解即可
@AutoService(Processor.class) public class ClassProcessor extends AbstractProcessor {
···
}
最后就到了应用注解,我们要在主工程项目中引用注解,首先要在主工程app项目的build.gradle中引用annotations和processor两个库,如下
implementation project(':annotations')
implementation project(':processor')
随后在MainActivity中应用注解,如下
public class MainActivity extends AppCompatActivity {
@BindView(value = R.id.tv_text) TextView textView;
@Override protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
最后Make Project,可以发现在Gradle Console窗口中运行如下截图,打印出了具体的注解信息
除此之外,我们也可以使用android-apt插件,我们的主工程项目app中引用了processor 库,但是注解处理器只在编译期间需要用到,编译处理完后就没有实际作用了,而主工程项目添加了这个库,就会引入很多不必要的麻烦,为了处理这个问题我们引入插件android-apt,主要有以下两个作用
- 仅在编译期间去依赖注解处理器所在的函数库并进行工作,但不会打包到apk中
- 为注解处理器生成的代码设置好路径,以便AS能够找到它
接下来介绍如何使用,首先需要在整个项目的build.gradle中添加如下语句
buildScript {
···
dependencies {
···
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
然后,在主工程app的build.gradle中以apt方式引入注解处理器processor,如下
apply plugin: 'com.neenbedankt.android-apt'
dependencies{
apt project(':processor')
}
依赖注入原理
说完了注解,下面说说依赖注入,想要了解依赖注入,先从控制反转说起
1. 控制反转
对象之间的耦合是无法避免的,但随着工业级应用越来越庞大,对象之间的依赖关系越来越复杂,经常会出现对象之间的多重依赖关系。为了解决耦合度过高的问题,有人提出了IOC理论,即控制反转,用于实现对象之间的解耦。在软件系统引入IOC之前,如果对象A依赖于对象B,那么对象A在运行到某一固定时刻需要自行创建B对象,无论是创建还是使用,控制权都在自己手上。引入IOC后,对象A和B可以失去直接联系,当A运行至需要B时,IOC容器会创建一个对象B,注入到A所需的地方,通过引入IOC的前后对比,可以看出对象A获得B的过程由主动转为了被动,此为控制反转的由来。于是,由于依赖对象的过程被反转了,控制反转有了一个更合适的名字,即依赖注入,指的是IOC容器在运行期间,动态将某种依赖关系注入到对象中
2. 依赖注入的方式
编写代码时我们会发现有一些类是依赖于其他类的,下面举一个汽车与零件的例子,汽车Car包含了Engine等组件,如下
public class Car{
private Engine engine;
public Car(){
engine = new PetrolEngine();
}
}
这段代码本身没有错,但是Car和Engine高度耦合,因为Car需要创造Engine对象,而且还需要知道PetrolEngine的存在,如果引擎更改为DieselEngine,还需要修改Car的构造方法,以上问题都可以通过依赖注入来解决,依赖注入有三种常见实现方式,如下
- 构造方法注入。往Car的构造方法里传递Engine对象
public class Car{
private Engine engine;
public Car(Engine engine){
this.engine = engine;
}
}
- setter方法注入。通过set方法传递Engine对象
public class Car{
private Engine engine;
public void set(Engine engine){
this.engine = engine;
}
}
- 接口注入 在接口中定义需要注入的信息,并通过接口完成注入,接口代码如下
public interface ICar{
public void setEngine(Engine engine);
}
然后Car类实现接口
public class Car implements ICar{
private Engine engine;
@Override public void setEngine(Engine engine){
this.engine = engine;
}
}
这样一来,Car和Engine解耦了,Car不关心Engine的实现,即使Engine需要修改,Car也无需做任何修改