为什么要引入注解?
使用Annotation之前,XML被广泛的应用于描述元数据。不知何时开始开发人员发现XML的维护越来越糟糕了。他们希望使用一些和代码紧耦合的描述,而不是像XML那样和代码是松耦合的描述(XML配置其实就是为了分离代码和配置而引入的)。如果在Google中搜索“XML vs. annotations”,会看到许多关于这个问题的辩论,两者观点似乎构成了一种循环,各有利弊。
- 假如你想为应用设置很多的常量或参数,这种情况下,XML是一个很好的选择,因为它不会同特定的代码相连。
- 如果你想把某个方法声明为服务,那么使用Annotation会更好一些,因为这种情况下需要注解和方法紧密耦合起来,开发人员也必须认识到这点。
另一个很重要的因素是Annotation定义了一种标准的描述元数据的方式。在这之前,开发人员通常使用他自己的方式定义元数据。例如,使用标记interfaces,注释,transient关键字等等,而不像Annotation这种标准的方式。
总之:XML追求松耦合,注解相反追求高内聚。许多框架将XML和Annotation两种方式结合使用,平衡两者之间的利弊。
Annotation是如何工作的?
1、概念:
注解是JDK1.5及以后版本引入的一个新特性,能够添加到 Java 源代码的语法元数据。类、方法、变量、参数、包都可以被注解,可用来将信息元数据与程序元素进行关联。
注解的本质就是一个继承了 Annotation 接口的接口。可以去反编译任意一个注解类,查看其代码。一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。而解析一个注解往往有两种形式:
- 一种是编译期直接扫描:编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。
- 一种是运行期反射:在运行时通过反射获取到类、方法、参数等等的注解,然后进行相关处理;
- jsr269:插件化注解处理器;
2、作用:
- 标记,用于告诉编译器一些信息
- 编译时动态处理,如动态生成代码
- 运行时动态处理,如利用反射得到注解信息
3、分类:
1)标准 Annotation:
标准 Annotation 是指jdk中的自带的一下5个:
- @Override:告诉编译器这个方法要覆盖父类的方法,编译器会去检查父类有没有这个方法
- @Deprecated:标记那些废弃的类、方法等;
- @SuppressWarnings:用于告诉编译器这个代码是安全的,不必警告
- @FunctionalInterface():1.8新增的,用于通知编译器,这个类型是 function 接口
- @SafeVarargs : 空注解,(varargs 可变参数)用于标记构造函数或者方法,通知编译器,这里的可变参数相关的操作保证安全
2)元 Annotation:
元 Annotation 是指用来定义 Annotation 的 Annotation,例如:@Retention, @Target, @Inherited, @Documented,后面详细介绍。
3)自定义 Annotation:
自定义 Annotation 表示自己根据需要定义的 Annotation,定义时需要用到上面的元 Annotation。
注意:这里只是一种分类而已,也可以根据作用域分为
- 源码时Annotation;
- 编译时Annotation;
- 运行时 Annotation;
元Annotation
1)@Target:
用于描述Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
取值(ElementType)有:
- CONSTRUCTOR:用于描述构造器
- FIELD:用于描述域
- LOCAL_VARIABLE:用于描述局部变量
- METHOD:用于描述方法
- PACKAGE:用于描述包
- PARAMETER:用于描述参数
- TYPE:用于描述类、接口(包括注解类型) 或enum声明
2)@Retention:
定义了该Annotation的生命周期:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。
取值(RetentionPoicy)有:
- SOURCE:在源文件中有效(即源文件保留)
- CLASS:在class文件中有效(即class保留)
- RUNTIME:在运行时有效(即运行时保留
3)@Documented:
用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
4)@Inherited:
@Inherited 元注解是一个标记注解,表示某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
自定义的Annotations
上面我们可知,定义好注解后还需要编写注解处理器,最常见的是通过反射来处理,当然也可以通过jsr269实现插入式注解处理器(见下一篇文章)。
1、使用@interface自定义注解:
语法:public @interface 注解名 {定义体}
使用@interface自定义注解会自动继承java.lang.annotation.Annotation接口(由编译程序自动完成其他细节)。在注解定义体中的每一个方法实际上是声明了一个配置参数,方法的名称就是参数的名称,返回值类型就是参数的类型,还可以通过default来声明参数的默认值。
注意:定义注解时,不能继承其他的注解或接口。
- 只能用public或默认(default)这两个访问权修饰参数;
- 参数成员只能用基本类型、 String、Enum、Class、annotations数据类型,以及这一些类型的数组;
- 如果只有一个参数成员,最好把参数名称设为”value”,后加小括号
2、示例:
1)自定义注解1:
package cn.nuc.edu.LogTest.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorAnnotation {
public String name();// 名字
int age() default 19;// 年龄
String gender() default "男";// 性别
}
2)自定义注解2:
package cn.nuc.edu.LogTest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface BookAnnotation {
String bookName();// 书名
String briefOfBook();// 书的简介
int sales() default 10000;// 书的销量
}
3)使用注解的类:
package cn.nuc.edu.LogTest.annotation;
@BookAnnotation(bookName="泡沫之夏", briefOfBook = "泡沫之夏...",sales = 1110000)
public class LoveStoryBook {
@AuthorAnnotation(name = "明晓溪",age=30,gender="女")
private String user;
@BookAnnotation(bookName="微微一笑很倾城", briefOfBook = "微微一笑很倾城...",sales = 800000)
public void getBookInfo(){
}
}
4)注解解析器:
package cn.nuc.edu.LogTest.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ParseAnnotation {
//类解析注解器
public static void parseTypeAnnotation() throws ClassNotFoundException{
Class<?> clazz = Class.forName("cn.nuc.edu.LogTest.annotation.LoveStoryBook");
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
BookAnnotation bookAnnotation = (BookAnnotation) annotation;
System.out.println("书名:" + bookAnnotation.bookName() + "\n" +
"书的简介:" + bookAnnotation.briefOfBook() + "\n"+
"书的销量:" + bookAnnotation .sales() + "\n");
}
}
//方法解析注解器
public static void parseMethodAnnotation() throws ClassNotFoundException {
Method[] methods = cn.nuc.edu.LogTest.annotation.LoveStoryBook.class.getDeclaredMethods();
for (Method method : methods) {
//判断方法中是否有指定注解类型的注解
boolean has = method.isAnnotationPresent(BookAnnotation.class);
if (has) {
BookAnnotation annotation = method.getAnnotation(BookAnnotation.class);
System.out.println("书名:" + annotation.bookName() + "\n" +
"书的简介:" + annotation.briefOfBook() + "\n"+
"书的销量:" + annotation .sales() + "\n");
}
}
}
//变量解析注解器
public static void parseFieldAnnotation() throws ClassNotFoundException {
Field[] fields = cn.nuc.edu.LogTest.annotation.LoveStoryBook.class.getDeclaredFields();
for (Field field : fields) {
//判断变量上是否有指定注解类型的注解
boolean has = field.isAnnotationPresent(AuthorAnnotation.class);
if (has) {
AuthorAnnotation annotation = field.getAnnotation(AuthorAnnotation.class);
System.out.println("作者:" +annotation.name() + "\n" +
"年龄:" + annotation.age() + "\n" +
"性别:" + annotation.gender() + "\n");
}
}
}
}
5)测试类:
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
// 解析域的注解
ParseAnnotation.parseFieldAnnotation();
// 解析方法的注解
ParseAnnotation.parseMethodAnnotation();
// 解析类的注解
ParseAnnotation.parseTypeAnnotation();
}
}
3、示例:扫描包下的注解,执行注解解析器
1)扫描包路径:
public class ClasspathPackageScanner {
private String basePackage;
private ClassLoader cl;
//Construct an instance with base package and class loader.
public ClasspathPackageScanner(String basePackage) {
this.basePackage = basePackage;
this.cl = getClass().getClassLoader();
}
public ClasspathPackageScanner(String basePackage, ClassLoader cl) {
this.basePackage = basePackage;
this.cl = cl;
}
//Get all fully qualified names located in the specified package and its sub-package.
public List<String> getFullyQualifiedClassNameList() throws IOException {
//logger.info("开始扫描包{}下的所有类", basePackage);
return doScan(basePackage, new ArrayList<String>());
}
//Actually perform the scanning procedure.
private List<String> doScan(String basePackage, List<String> nameList) throws IOException {
String splashPath = StringUtil.dotToSplash(basePackage);// replace dots with splashes
URL url = cl.getResource(splashPath);// get file path
String filePath = StringUtil.getRootPath(url);
List<String> names = null; // contains the name of the class file. e.g., Apple.class will be stored as "Apple"
if (isJarFile(filePath)) {// jar file
//logger.debug("{} 是一个JAR包", filePath);
names = readFromJarFile(filePath, splashPath);
} else {// directory
//logger.debug("{} 是一个目录", filePath);
names = readFromDirectory(filePath);
}
for (String name : names) {
if (isClassFile(name)) {
nameList.add(toFullyQualifiedName(name, basePackage));
} else {//this is a directory,check this directory for more classes,do recursive invocation
doScan(basePackage + "." + name, nameList);
}
}
return nameList;
}
//Convert short class name to fully qualified name.e.g., String -> java.lang.String
private String toFullyQualifiedName(String shortName, String basePackage) {
StringBuilder sb = new StringBuilder(basePackage);
sb.append('.');
sb.append(StringUtil.trimExtension(shortName));
return sb.toString();
}
private List<String> readFromJarFile(String jarPath, String splashedPackageName) {
List<String> nameList = new ArrayList<>();
try (JarInputStream jarIn = new JarInputStream(new FileInputStream(jarPath))) {
JarEntry entry = jarIn.getNextJarEntry();
while (null != entry) {
String name = entry.getName();
if (name.startsWith(splashedPackageName) && isClassFile(name)) {
nameList.add(name);
}
entry = jarIn.getNextJarEntry();
}
} catch (Exception e) {
e.printStackTrace();
}
return nameList;
}
private List<String> readFromDirectory(String path) {
File file = new File(path);
String[] names = file.list();
if (null == names) {
return null;
}
return Arrays.asList(names);
}
private boolean isClassFile(String name) {
return name.endsWith(".class");
}
private boolean isJarFile(String name) {
return name.endsWith(".jar");
}
}
2)Utils工具:
public class StringUtil {
private StringUtil() {}
/**
* "file:/home/whf/cn/fh" -> "/home/whf/cn/fh"
* "jar:file:/home/whf/foo.jar!cn/fh" -> "/home/whf/foo.jar"
*/
public static String getRootPath(URL url) {
String fileUrl = url.getFile();
int pos = fileUrl.indexOf('!');
if (-1 == pos) {
return fileUrl;
}
return fileUrl.substring(5, pos);
}
/**
* "cn.fh.lightning" -> "cn/fh/lightning"
*/
public static String dotToSplash(String name) {
return name.replaceAll("\\.", "/");
}
/**
* "Apple.class" -> "Apple"
*/
public static String trimExtension(String name) {
int pos = name.indexOf('.');
if (-1 != pos) {
return name.substring(0, pos);
}
return name;
}
/**
* /application/home -> /home
*/
public static String trimURI(String uri) {
String trimmed = uri.substring(1);
int splashIndex = trimmed.indexOf('/');
return trimmed.substring(splashIndex);
}
}
3)测试:
public static void main(String[] args) throws Exception {
ClasspathPackageScanner scan = new ClasspathPackageScanner("cn.nuc.edu");
List<String> list = scan.getFullyQualifiedClassNameList();
//系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
for (String name : list) {
System.out.println(name);
Class<?> clazz = systemClassLoader.loadClass(name);
// Class<?> clazz = Class.forName(name,false,systemClassLoader);
boolean has = clazz.isAnnotationPresent(BookAnnotation.class);
if (has) {
BookAnnotation annotation = clazz.getAnnotation(BookAnnotation.class);
System.out.println("书名:" + annotation.bookName() + "==" +
"书的简介:" + annotation.briefOfBook() + "=="+
"书的销量:" + annotation .sales() + "");
}
}
}
4、示例3——source自定义注解:
SOURCE这个policy表示注解保留在源代码中,但是编译的时候会被编译器所丢弃。 由于在编译的过程中这个注解还被保留着,所以在编译过程中可以针对这个policy进行一些操作。比如在自动生成java代码的场景下使用。最常见的就是lombok的使用了,可以自动生成field的get和set方法以及toString方法,构造器等;消除了冗长的java代码。
SOURCE这个policy可以使用jdk中的javax.annotation.processing.*包中的processor处理器进行注解的处理过程(JSR269)。
看一个个编译过程中会打印类中的方法的例子来说明SOUCRE这个policy的作用。
1)定义一个Print注解:
package cn.nuc.edu.LogTest.annotation.source;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Printer {
}
2)注解处理器:
package cn.nuc.edu.LogTest.annotation.source;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
@SupportedAnnotationTypes({"cn.nuc.edu.LogTest.annotation.source.Printer"})
public class PrintProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,RoundEnvironment roundEnv) {
Messager messager = processingEnv.getMessager();
messager.printMessage(Diagnostic.Kind.NOTE, "start to use PrintProcessor ..");
Set<? extends Element> rootElements = roundEnv.getRootElements();
messager.printMessage(Diagnostic.Kind.NOTE, "root classes: ");
for(Element root : rootElements) {
messager.printMessage(Diagnostic.Kind.NOTE, ">> " + root.toString());
}
messager.printMessage(Diagnostic.Kind.NOTE, "annotation: ");
for(TypeElement te : annotations) {
messager.printMessage(Diagnostic.Kind.NOTE, ">>> " + te.toString());
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(te);
for(Element ele : elements) {
messager.printMessage(Diagnostic.Kind.NOTE, ">>>> " + ele.toString());
}
}
return true;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
3)使用注解的类:
package cn.nuc.edu.LogTest.annotation.source;
public class SimpleObject {
@Printer
public void methodA() {
}
public void methodB() {
}
}
4)先使用javac编译Printer和PrintProcessor:
$ pwd
/e/workspace_suike/LogTest
$ javac -d target/classes/ src/main/java/cn/nuc/edu/LogTest/annotation/source/Printer.java src/main/java/cn/nuc/edu/LogTest/annotation/source/PrintProcessor.java
5)使用javac中的processor参数处理注解:
$ javac -encoding utf-8 -cp target/classes/ -processor cn.nuc.edu.LogTest.annotation.source.PrintProcessor -d target/classes/ src/main/java/cn/nuc/edu/LogTest/annotation/source/SimpleObject.java
注: start to use PrintProcessor ..
注: root classes:
注: >> cn.nuc.edu.LogTest.annotation.source.SimpleObject
注: annotation:
注: >>> cn.nuc.edu.LogTest.annotation.source.Printer
注: >>>> methodA()
注: start to use PrintProcessor ..
注: root classes:
注: annotation:
我们可以反编译查看SimpleObject,可以发现确实注解已经不见了。
5、示例4——class自定义注解:
CLASS和RUNTIME的唯一区别是RUNTIME在运行时期间注解是存在的,而CLASS则不存在。我们通过asm(http://asm.ow2.org/)来获取class文件里的annotation。
1)定义注解:
package cn.nuc.edu.LogTest.annotation.clazz;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Header {
int code();
}
class类型的注解:
package cn.nuc.edu.LogTest.annotation.clazz;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Meta {
String name();
}
2)使用注解的类:
package cn.nuc.edu.LogTest.annotation.clazz;
@Meta(name = "obj")
@Header(code = 200)
public class AnnotationObject {
private String val;
public String getVal() {
return val;
}
public void setVal(String val) {
this.val = val;
}
}
编译这3个java文件得到字节码文件AnnotationObject.class:
3)使用asm获取AnnotationObject.class中信息:
package cn.nuc.edu.LogTest.annotation.clazz;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.tree.AnnotationNode;
import jdk.internal.org.objectweb.asm.tree.ClassNode;
public class asmTest {
public static void main(String...strings) throws FileNotFoundException, IOException {
String classFile = "";
ClassReader cr = new ClassReader(new FileInputStream(classFile));
ClassNode classNode = new ClassNode();
cr.accept(classNode, 0);
System.out.println("Class Name: " + classNode.name + "Source File: " + classNode.sourceFile);
System.out.println("invisible: ");
AnnotationNode anNode = null;
for (Object annotation : classNode.invisibleAnnotations) {
anNode = (AnnotationNode) annotation;
System.out.println("Annotation Descriptor : " + anNode.desc);
System.out.println("Annotation attribute pairs : " + anNode.values);
}
System.out.println("visible: ");
for (Object annotation : classNode.visibleAnnotations) {
anNode = (AnnotationNode) annotation;
System.out.println("Annotation Descriptor : " + anNode.desc);
System.out.println("Annotation attribute pairs : " + anNode.values);
}
}
}
输出:
Class Name: cn/nuc/edu/LogTest/annotation/clazz/AnnotationObject
Source File: AnnotationObject.java
invisible:
Annotation Descriptor : Lcn/nuc/edu/LogTest/annotation/clazz/Meta;
Annotation attribute pairs : [name, obj]
visible:
Annotation Descriptor : Lcn/nuc/edu/LogTest/annotation/clazz/Header;
Annotation attribute pairs : [code, 200]
可以看出:其中policy为CLASS的注解编译完后不可见,而policy为RUNTIME的注解编译后可见。
同样,我们可以使用javap查看编译后的信息:
javap -v cn.nuc.edu.LogTest.annotation.clazz.AnnotationObject
输出:
参考:
https://juejin.cn/post/6844903636733001741
https://fangjian0423.github.io/2016/11/04/java-annotation-retentionpolicy/