Java基础复习-注解篇
注解 (也被称为元数据) 为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据. —— 《Java编程思想》
说到注解可能有人会一脸懵逼,我知道注释,注解又是个什么东西呢?先举个例子,相信下面出现的符号大部分Java程序员都会知道:
- @Override //用于标明此方法覆盖了父类的方法
- @Deprecated //用于标明已经过时的方法或类
- @SuppressWarnings //关闭不当的编译器警告
- @Controller @Service @Repository //Spring 常用的注解
- @Test //Junit 单元测试
- @Bind //ButterKnife 绑定控件
注解的应用很广泛,可以使我们能够使用编译器来测试和验证格式,存储有关程序的额外信息。可以用来生成描述符文件,甚至或是新的类定义,并且有助于减轻编写“样板”代码的负担。通过使用注解,我们可以将这些元数据保存在Java源代码中,并利用annotation API为自己的注解构造处理工具。
基本语法
- 自定义注解
定义一个注解
新建一个 HelloAnnotation.java 文件,然后输入下面的代码,恭喜你一个注解就定义完成了。
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface HelloAnnotation {
}
注解的定义与接口差不多,关键字是@interface
记住要加一个@
,@Documented
,@Retention
,@Target
是元注解,下面会详细解释什么是元注解。
- 注解元素
上面的注解像一个空的接口,里面没有内容,我们一般称这样的注解为标记注解。我们也可以像普通类添加成员变量一样,给注解添加注解元素。 例如,我们要给上面定义的HelloAnnotation添加一个int类型的id元素,和一个String类型的description元素。代码如下:
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface HelloAnnotation {
int id();
String description() default "hello world";
}
id和description类似方法的定义,但它们不是方法。元素后面可以加一个default
关键字,给这个元素一个默认值。上面例子中,如果在使用注解时没有给description赋值,那么description的值就是“hello world”。
关于default有两个限制需要注意:
- 如果一个元素没有默认值,那么在使用的时候必须给这个元素赋值
- 对于非基本类型的元素,无论在生命时还是使用时,它的值都不能为null
元素类型必须是以下几种:
- 所有基本类型(int, float, boolean等)
- String
- Class
- enum
- Annotation
- 以上几种类型的数组
有一个特殊的元素叫 value, 不论value的类型是什么,当一个注解里面只有这个叫value的元素需要赋值时(只有一个value元素,或者有其他元素,但是其他的元素都有默认值),可以使用快捷方式。具体怎么使用下面会介绍。
- 注解的使用
我们在上面定义了一个HelloAnnotation,那么我们如何使用呢?
//第一种使用方式,给每个元素都赋值
@HelloAnnotation(id=11,description="如何使用注解")
private void test1(){
}
//第二种使用方式,给没有Default值的元素赋值
@HelloAnnotation(id=11)
private void test1(){
}
上面的代码展示了注解的使用方式。我们上面提到的特殊的元素value又是什么呢?我们来重新定义一个注解 ValueAnnotation,这注解有两个元素value 和 id,
@Retention(RetentionPolicy.RUNTIME)
public @interface ValueAnnotation {
int id() default 1;
int value();
}
看起来跟HelloAnnotation没有什么区别,但是使用的时候会有一点点的骚操作…
//与上面说的使用方式没有不同
@ValueAnnotation(value = 1)
private void test1(){
}
@ValueAnnotation(value = 1, id = 2)
private void test1(){
}
//可以省略value字段的写法,直接把值放进去就好,不需要指定给value赋值
@ValueAnnotation(1)
private void test1(){
}
标准注解&元注解
Java提供了三种标准注解:
1. @Override,表示当前方法是超类的重写,如果方法签名与超类的不一致,编译器会报错,避免拼写错误等失误。
2. @Deprecated,表示当前方法被弃用了,不建议使用。部分IDE会在方法上加中划线,表示弃用, add()。
3. @SuppressWarnings,表示关闭某些警告信息,比如List list = new ArrayList(),没有加泛型,会报一个unchecked警告,加上@SuppressWarnings(“unchecked”)之后,就不会再提示。
同时,Java内置了几种元注解。元注解是负责注解其他的注解的注解(怎么断句…..):
关键字 | 说明 |
---|---|
@Target |
表示该注解用于什么地方,可能的ElementType值: TYPE: 用于类,接口,注解,enum的声明 FIELD: 用于域声明,包括enum实例 METHOD: 用于修饰方法 PARAMETER: 用于修饰参数 CONSTRUCTOR: 用于修饰构造函数 LOCAL_VARIABLE: 用于修饰局部变量 ANNOTATION_TYPE: 用于修饰注解类型 PACKAGE: 用于修饰包 如果不指定Target,可以用到任何位置 |
@Retention |
表示在什么级别保存该注解,可能的RetentionPolicy值: SOURCE: 源代码级别,也就是编译时注解被丢弃。java文件编译成class文件后,注解就拿不到了。 CLASS: class级别,也就是在class文件中可用,但是会被VM丢弃。class文件里面保留注解信息,运行时拿不到 RUNTIME: 运行时级别,也就是在代码运行时也不会被丢弃,因此可以通过反射机制读取注解的信息。 如果不指定Retention,默认是class级别 |
@Documented |
使用javadoc生成文档时,默认是不包含注解文件中的doc内容的。 加上这个注解之后,javadoc生成时会包含这个注解里面的javadoc内容 |
@Inherited |
允许子类继承父类的注解。两个类Parent 和 Child,Child继承自Parent, Parent被一个有@Inherited注解的注解A声明,那么Child类也会有A这个注解 |
如何获取注解
上面我们说了怎么定义一个注解,也自定义了两个注解。那么我们如何获取注解和里面的内容呢?
我们先来一种简单的方式:
首先我们修改一下HelloAnnotation
,把它的Target变成TYPE
,让他可以用在类上面。
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface HelloAnnotation {
int id();
String description() default "hello world";
}
接下啦我们编写一个测试类 TestAnnotation
:
@HelloAnnotation
public class TestAnnotation {
@ValueAnnotation(12)
private void test() {
}
public static void main(String[] args) {
//判断TestAnnotation类有没有HelloAnnotation这个注解
boolean hasValueAnnotation = TestAnnotation.class.isAnnotationPresent(HelloAnnotation.class);
System.out.println(hasValueAnnotation);
try {
//获取test方法中的注解
Method testMethod = TestAnnotation.class.getDeclaredMethod("test");
System.out.println(testMethod.isAnnotationPresent(ValueAnnotation.class));
ValueAnnotation valueAnnotation = testMethod.getAnnotation(ValueAnnotation.class);
System.out.println("id = " + valueAnnotation.id() + ", value = " + valueAnnotation.value());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
==================输出结果=====================
false
true
id = 1, value = 12
因为HelloAnnotation定义的Retention是SOURCE,所有我们在运行时这个注解已经被弃用了,TestAnnotation.class.isAnnotationPresent(HelloAnnotation.class)
返回的是false。
ValueAnnotation定义的Retention是RUNTIME,所以test
的注解和里面的id,value值我们都可以拿到。
APT处理注解
APT(Annotation Process Tool) 注解处理工具,这是Sun为了帮助注解的处理而提供的工具,包含在javac中,可以在代码编译期解析注解,并且生成新的 Java 文件。
我们不需要关心它是如何工作的,我们只需要编写一个注解处理器,然后编译的时候APT会调用我们的注解处理器,达到我们想要的效果。
如何编写一个注解处理器呢?首先我们要了解一个类叫做AbstractProcessor
,很明显这个类是一个Abstract的类,它实现了一个叫Processor
的接口,里面有很多方法,我们不做详细的介绍,感兴趣的读者可以找一些资料深入了解一下。我们只需要关心其中几个方法就行。下面我们来自定义一个注解处理器,并重写我们关心的方法:
public class HelloProcessor extends AbstractProcessor {
//一个很重要的参数ProcessingEnvironment,我们可以拿到一些环境相关的值,和一些有用的工具类
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
//真正的处理过程
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
//表示这个注解处理器能处理哪些注解
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(ValueAnnotation.class.getCanonicalName());
}
//支持的版本,建议写成下面的代码形式
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
如何让这个注解处理器生效,有多种方式,请大家自行百度。这里限于文章篇幅以Android工程为例,搭建一个简单的demo。 首先,新建一个普通的gradle Android工程,然后新建一个Java Library: annotation。打开annotation这个module的build.gradle
文件,添加依赖compile 'com.google.auto.service:auto-service:1.0-rc2'
:
//============ annotation/build.gradle =============
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
//添加auto-service是为了使用@AutoService注解,把注解处理器添加到jvm
compile 'com.google.auto.service:auto-service:1.0-rc2'
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
然后把我们上面写的ValueAnnotation
和HelloProcessor
放到annotation工程里面,最后的目录结构如下:
annotation工程的配置,暂时就完成了。接着我们回到app工程里面,打开build.gradle
,在dependencies里面添加下面两行:
//========app/build.gradle=======
compile project(":annotation")
//表示我们的工程里面有注解处理器,需要apt处理
annotationProcessor project(":annotation")
打开MainActivity
, 添加一个方法test()
,并给这个方法一个注解 :
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@ValueAnnotation(12)
private void test(){
}
到这里简单的demo环境就打完成了。接着我们介绍一下怎么用注解处理器生成代码。主要逻辑是在HelloProcessor
里面:
//@AutoService(Processor.class) 这个注解告诉jvm这是一个注解处理器
@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor {
Messager messager ;
Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//打印log的工具类
messager = processingEnv.getMessager();
//最重要的写生成文件的工具
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
//获取所有的使用ValueAnnotation注释的Element
Set<? extends Element> genElements = roundEnv.getElementsAnnotatedWith(ValueAnnotation.class);
for (Element e : genElements) {
//获取Annotation的值
ValueAnnotation valueAnnotation = e.getAnnotation(ValueAnnotation.class);
int id = valueAnnotation.id();
int value = valueAnnotation.value();
//e.getSimpleName() 获得当前作用对象的名称,这里就是test方法
//e.getEnclosingElement() 获取当前作用对象的上一级对象,也就是MainActivity
String className = e.getEnclosingElement().getSimpleName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "class = " + className + ", method = " + e.getSimpleName());
//我们生产一个叫MainActivity$$Value的源文件,里面有个方法叫printValue,作用是打印注解的值。
JavaFileObject jfo = filer.createSourceFile("com.jiang." + className + "$$Value", e);
Writer writer = jfo.openWriter();
writer.flush();
writer.append("package com.jiang;\n" +
"\n" +
"\n" +
"public class " + className + "$$Value {\n" +
" public void printValue(){\n" +
" System.out.println(\"id= " + id + " , value = " + value + "\");\n" +
" };\n" +
"}\n");
writer.flush();
writer.close();
}
} catch (Exception e) {
messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(ValueAnnotation.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
上面的一切代码都完成了,Make project,编译一下整个工程,如果一切正常会在gradle的Console里面输出我们打印的log
这时在app工程的build目录下会生成我们想要的java文件
文件的代码就是我们在注解处理器里面添加的:
package com.jiang;
public class MainActivity$$Value {
public void printValue(){
System.out.println("id= 1 , value = 12");
};
}
最后我们可以在MainActivity
里面调用这个类:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MainActivity$$Value().printValue();
}
@ValueAnnotation(12)
private void test(){
}
}
运行项目,logcat里面会输出我们的打印结果:
Android开发中经常用到的ButterKnife就是使用了上面的实现方式,根据ResourceId生成java源文件,帮你做findViewById、setOnClickListener这些繁琐的事情。
另一个很火的网络框架Retrofit使用的是注解来定义HttpMethod,它是通过在Builder里面直接调用getAnnotations()这种方式来获取注解信息。
上一篇: php怎么将svg转换png格式
下一篇: 用正则表达式筛选email邮箱/邮件地址