为什么需要注解
在 JDK 1.5 之前,Java 还没引入注解,这个时候如果我们要在 Spring 中声明一个 Bean,我们只能通过 XML 配置的方式。
public class DemoService{
}
<bean id="demoService" class="com.chenshuyi.DemoService"/>
当有了注解,我们就可以不必写一个 XML 配置文件,可以直接在 DemoService 类上完成 Bean 的声明工作。
@Service
public class DemoService{
}
表面上看来,我们通过注解的方式减少了一个XML配置文件,减少了开发代码量。
但这真的是我们用注解而不用 XML 配置文件的原因吗?
在回答这个问题之前,我们来回顾一下上面两种配置方式的特点:
对于注解的方式。我们会发现它和代码结合得很紧密,所以注解比较适合做一些与代码相关度高的操作,例如将Bean对应的服务暴露出去。
对于XML配置方式。我们会发现它将配置和代码隔离开来了所以XML配置更适合做一些全局的、与具体代码无关的操作,例如全局的配置等。
所以引入注解的原因,基本上是两点:
1、减少代码量,方便开发
2、注解的配置可以与代码高度相关
注解是什么
注解是在Java5.0版本中被引入,其目的是用于描述数据。
用一个词就可以描述注解,那就是元数据,可以说注解是源代码的元数据。
我们可以把注解当作标签,每加上一个注解就相当于给类/方法/字段贴上来标签,而标签的作用是,通过标签可以让那个我们知道这个类/方法/字段是做什么的。
@Override
public String toString() {
return "This is String Representation of current object.";
}
比如上面代码中,我们在程序开发看到@Override,我们是很容易理解它的作用就是对实现方法的重写,而这也达到设计之初的目标,即描述数据。
java中的常用注解
@Deprecated -- @Deprecated 所标注内容,不再被建议使用。
@Override -- @Override 只能标注方法,表示该方法覆盖父类中的方法。
@Documented -- @Documented 所标注内容,可以出现在javadoc中。
@Inherited -- @Inherited只能被用来标注“Annotation类型”,它所标注的Annotation具有继承性。
@Retention -- @Retention只能被用来标注“Annotation类型”,而且它被用来指定Annotation的RetentionPolicy属性。
@Target -- @Target只能被用来标注“Annotation类型”,而且它被用来指定Annotation的ElementType属性。
@SuppressWarnings -- @SuppressWarnings 所标注内容产生的警告,编译器会对这些警告保持静默。
自定义注解的原理
1、元注解
元注解(meta-annotation)本身也是一个注解,用来标记普通注解的存留时间、使用场景、继承属性、文档生成信息。
元注解是一个特殊的注解,它是 Java 源码中就自带的注解。在Java 中只有四个元注解,它们分别是:@Target、@Retention、@Documented、@Inherited。
2、注解体
注解体是最简单的一个组成部分,只需要实例中一样有样学样即可。与接口的声明唯一的不同是在 interface 关键字前多了一个 @ 符号。
//声明了一个名为sweet的注解体
@Retention(RetentionPolicy.RUNTIME)
public @interface sweet{
}
3、四大元注解
@Target 元注解
Target 注解限定了该注解的使用场景,描述该注解是使用在哪类数据上:类、方法、属性、构造函数、接口、包等。
有下面这些取值:
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR 可以给构造方法进行注解
ElementType.FIELD 可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
比如,下面的例子表示 Autowired 注解只能在构造方法、方法、方法形参、属性、类型这 5 种场景下使用。
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface Autowired {
boolean required() default true;
}
@Retention 元注解
Retention 注解用来标记这个注解的留存时间。
public enum RetentionPolicy {
SOURCE, /* Annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该Annotation信息了 */
CLASS, /* 编译器将Annotation存储于类对应的.class文件中。默认行为 */
RUNTIME /* 编译器将Annotation存储于class文件中,并且可由JVM读入 */
}
@Documented 元注解
@ Documented 注解表示将注解信息写入到 javadoc 文档中。
在默认情况下,我们的注解信息是不会写入到 Javadoc 文档中的。但如果该注解有 @Documented 标识,那么该注解信息则会写入到 javadoc 文档中。
@Inherited 元注解
@ Inherited注解标识子类将继承父类的注解属性。
//声明一个Sweet注解,标识甜味。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Sweet {}
//桃子有甜味
@Sweet
public class Peach {}
//红色的水蜜桃
public class RedPeach extends Peach {}
我们没在 RedPeach 类上使用了 @Sweet 注解,但是我们在 Sweet 注解声明中使用了 @Inherited 注解,所以 RedPeach 继承了 Peach 的 @Sweet 注解。
注解属性
注解属性类似于类方法的声明,注解属性里有三部分信息,分别是:属性名、数据类型、默认值。
需要注意的是,注解中定义的属性,它的数据类型必须是 8 种基本数据类型(byte、short、int、long、float、double、boolean、char)或者是类、接口、注解及它们的数组。
public @interface Autowired {
boolean required() default true;
}
总结
一个注解大致可以分为三个部分:注解体、元注解、注解属性。在这三个主要组成部分中:注解体指定了注解的名字、元注解则标记了该注解的使用信息,注解属性指明注解的属性。
注解的使用
自定义注解使用也非常简单,像我们上节定义的一个 Sweet 注解。
第一种情况:如果没有任何注解属性
那么我们使用的时候就可以直接写上直接名称,不需要中括号。
public @interface Sweet {
}
public class SweetDemo {
@Sweet
public void sweetWithDoc() {
System.out.printf("sweet With Doc.");
}
}
第二种情况:注解属性有默认值,可以不进行赋值操作。
@interface AnnotationTest{
String value();
int sex() default 1;
}
// 注解中有多个属性,赋值的方式:括号内以 value="",多个属性之前用 ,隔开。
@AnnotationTest(value="3",sex=0)
public class Test {
}
第三种情况:注解内有且仅有一个名字为 value 的属性,应用这个注解时可以直接接属性值填写到括号内。
public @interface Sweet {
String value();
}
public class SweetDemo {
@Sweet("Level.03")
public void sweetWithDoc() {
System.out.printf("sweet With Doc.");
}
}
注解处理器(APT(Annotation Processing Tool))
注解本身是代码的一个标签,如果没有注解处理器的话,只能描述这个代码是什么意思,但并没有实际作用。
注解处理器(Annotation Processor)是javac的一个工具,不管是运行时注解还是编译时注解,都会通过处理器在编译时进行扫描和处理注解。
注解处理器,是一种处理注解的工具,注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。简单来说就是在编译期,通过注解生成.java文件。
示例
1、新建一个annotations库,专门存放注解。先来定义的是编译时注解,对象为类或接口等
/**
* 编译时注解
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
String value();
}
2、新建一个processors注解库,定义一个注解处理器 MyProcessor,每一个处理器都是继承于AbstractProcessor,并要求必须复写 process() 方法,通常我们使用会去复写以下4个方法:
/**
* 每一个注解处理器类都必须有一个空的构造函数,默认不写就行;
*/
public class MyProcessor extends AbstractProcessor {
/**
* init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
* ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
* @param processingEnv 提供给 processor 用来访问工具框架的环境
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
/**
* 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
* 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
* @param annotations 请求处理的注解类型
* @param roundEnv 有关当前和以前的信息环境
* @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
* 如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
*/
@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
return false;
}
/**
* 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
* @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
*/
@Override
public Set getSupportedAnnotationTypes() {
Set annotataions = new LinkedHashSet();
annotataions.add(MyAnnotation.class.getCanonicalName());
return annotataions;
}
/**
* 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
* @return 使用的Java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
注解的处理函数
@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
// roundEnv.getElementsAnnotatedWith()返回使用给定注解类型的元素
for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
System.out.println("------------------------------");
// 判断元素的类型为Class
if (element.getKind() == ElementKind.CLASS) {
// 显示转换元素类型
TypeElement typeElement = (TypeElement) element;
// 输出元素名称
System.out.println(typeElement.getSimpleName());
// 输出注解属性值
System.out.println(typeElement.getAnnotation(MyAnnotation.class).value());
}
System.out.println("------------------------------");
}
return false;
}
3、使用android-apt
大体来讲它有两个作用:
- 能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留的东西
- 能够辅助 Android Studio 在项目的对应目录中存放注解处理器在编译期间生成的文件
在整个工程的 build.gradle 中添加如下两段语句:
buildscript {
repositories {
jcenter()
mavenCentral() // add
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // add
}
}
在主项目(app)的 build.gradle 中也添加两段语句:
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // add
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile project(':annotations')
// compile project(':processors') 替换为下面
apt project(':processors')
}
android-apt帮我们省掉了这些事情:
1、在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;