java 注解原理以及示例

为什么要引入注解?

使用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来声明参数的默认值。

注意:定义注解时,不能继承其他的注解或接口。

  1. 只能用public或默认(default)这两个访问权修饰参数;
  2. 参数成员只能用基本类型、 String、Enum、Class、annotations数据类型,以及这一些类型的数组;
  3. 如果只有一个参数成员,最好把参数名称设为”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

输出:

参考:

http://www.akathink.com/2016/08/11/%E5%BD%BB%E5%BA%95%E6%90%9E%E6%87%82Java%E6%B3%A8%E8%A7%A3Annotation/

https://juejin.cn/post/6844903636733001741

https://fangjian0423.github.io/2016/11/04/java-annotation-retentionpolicy/

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赶路人儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值