zoukankan      html  css  js  c++  java
  • 想自己写框架?不了解Java注解机制可不行

    无论是在JDK还是框架中,注解都是很重要的一部分,我们使用过很多注解,但是你有真正去了解过他的实现原理么?你有去自己写过注解么?

    概念

    注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

    在JDK中定义了许多注解,其作用大致可以分为以下几类:

    • 编写文档:通过代码里标识的元数据生成文档【生成文档doc文档】
    • 代码分析:通过代码里标识的元数据对代码进行分析【使用反射】
    • 编译检查:通过代码里标识的元数据让编译器能够实现基本的编译检查【Override】

    注解功能的实现

    我们以spring中比较常见的Autowired来举例分析

    创建注解

    @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Autowired {

    /**
    * Declares whether the annotated dependency is required.
    * <p>Defaults to {@code true}.
    */
    boolean required() default true;

    }

    注解的创建看起来很像接口,需要用@interface来修饰,然后我们看到在Autowired注释之上还有三个注释来进行修饰。

    他们三个都叫做“元注释”,Jdk5所定义的源注释还有@Retention、@Documented、@Inherited,这些类型和它们所支持的类在java.lang.annotation包中可以找到。

    @Target

    用于描述注解的使用范围,也就是在什么时候生效,一般情况下,我们的注释可能在packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)等任何一个地方生效。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Target {
    /**
    * Returns an array of the kinds of elements an annotation type
    * can be applied to.
    * @return an array of the kinds of elements an annotation type
    * can be applied to
    */
    ElementType[] value();
    }

    注:比较有意思的是,@Target也被自己修饰,这可能有更深层的原理,在此不再深入

    通过源码我们看出@Target下有一个类型为ElementType[] 的值value(),进入ElementType[],这是一个枚举类型,他提供了我们对于这个值的选项,可供选择的值有:

    public enum ElementType {
    /** 描述类、接口(包括注解类型) 或enum声明 */
    TYPE,

    /** 描述域 */
    FIELD,

    /** 描述方法 */
    METHOD,

    /** 描述参数 */
    PARAMETER,

    /** 描述构造器*/
    CONSTRUCTOR,

    /** 描述本地值 */
    LOCAL_VARIABLE,

    /** 描述注解类型 */
    ANNOTATION_TYPE,

    /** 描述包 */
    PACKAGE,

    /**
    * 描述类型参数
    *
    * @since 1.8
    */
    TYPE_PARAMETER,

    /**
    * 一个类型的用户
    *
    * @since 1.8
    */
    TYPE_USE
    }

    在Autowired中我们给值是{ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE},也就是说,我们的这个注解可以用于构造函数、方法、参数、域,注解等等。

    @Retention

    用于描述注解的被保留的时间段,我们定义注解时有时候会希望它一直保存,而不是在编译时就被抛弃,就可以用到这个注解。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Retention {
    /**
    * Returns the retention policy.
    * @return the retention policy
    */
    RetentionPolicy value();
    }

    @Retention下有一个类型为Retention的值,可供选择的值有:

    public enum RetentionPolicy {
    /**
    * 在源文件中有效(即源文件保留)
    */
    SOURCE,

    /**
    * 在class文件中有效(即class保留)
    */
    CLASS,

    /**
    * 在运行时有效(即运行时保留)
    *
    * @see java.lang.reflect.AnnotatedElement
    */
    RUNTIME
    }

    我们再回头看@Autowired,对应Retention中的取值是RetentionPolicy.RUNTIME,也就是说我们的Autowired注解是应行时有效的,这也正是我们预期的其在spring框架工作时的状态。

    @Documented

    @Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Documented {
    }

    @Inherited

    此注解是一个标记注解,如果我们的注解被@Inherited注解,那么我们的注解被用于一个类时,就说明我们的注解应当是这个类的子类

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Inherited {
    }

    注解中的值设定

    注解中的值设定是可选择的,并不是每一个注解都需要去设定值,比如我们的@Inherited注解仅作为一个标记注解,其中是没有值的。同时,如果我们的注解中有值,那我们也可以去通过“default”关键字设置一个默认值,比如我们上面的@Autowired注解中required这个值,就有一个默认的值:true,我们在使用这个注解的时候,就可以不声明这个值,采用他的默认值

    小总结

    看到这里,我们应当明白几件事

    • 注解应当被@Target标注,用以确定我们的注解的作用范围
    • 注解应当被@Retention标注,用以确定我们的注解可以工作到什么时候
    • 注解可选择地去设置自己的值,也可以设置一个默认值,用以后来的工作

    实现注解功能

    我们想要用注解去配合被我们注解的类或其他来实现某种功能,那么首先我们得明白如何将注解与被我们注解的类联系起来,我们都知道Autowired可以对类成员变量、方法及构造函数进行标注,让 spring 完成 bean 自动装配的工作,那么Spring内部是如何进行对被Autowired注解的变量进行操作的呢?

    首先我要你明白Spring的加载机制,你需要知道,所有被@Service标注的类都在初始化时被实例化。

    我们来看一下Spring中实现@Autowired的逻辑代码,该代码在org.springframework.beans.factory.annotation的org.springframework.beans.factory.annotation包下,有兴趣的可以自己去看一下

    	private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
    LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<InjectionMetadata.InjectedElement>();
    Class<?> targetClass = clazz;//需要处理的目标类

    do {
    final LinkedList<InjectionMetadata.InjectedElement> currElements =
    new LinkedList<InjectionMetadata.InjectedElement>();
    //通过反射获取目标类的所有字段,遍历所有字段,如果有字段用@Autowired注解,那就返回Autowired的相关属性
    ReflectionUtils.doWithLocalFields(targetClass, new ReflectionUtils.FieldCallback() {
    @Override
    public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
    AnnotationAttributes ann = findAutowiredAnnotation(field);
    if (ann != null) {//判断Autowired注解是否是在static方法上
    if (Modifier.isStatic(field.getModifiers())) {
    if (logger.isWarnEnabled()) {
    logger.warn("Autowired annotation is not supported on static fields: " + field);
    }
    return;
    }
    boolean required = determineRequiredStatus(ann);//判断required
    currElements.add(new AutowiredFieldElement(field, required));
    }
    }
    });
    //和上面一样的逻辑,但是是通过反射处理类的method
    ReflectionUtils.doWithLocalMethods(targetClass, new ReflectionUtils.MethodCallback() {
    @Override
    public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
    Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
    if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
    return;
    }
    AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
    if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
    if (Modifier.isStatic(method.getModifiers())) {
    if (logger.isWarnEnabled()) {
    logger.warn("Autowired annotation is not supported on static methods: " + method);
    }
    return;
    }
    if (method.getParameterTypes().length == 0) {
    if (logger.isWarnEnabled()) {
    logger.warn("Autowired annotation should only be used on methods with parameters: " +
    method);
    }
    }
    boolean required = determineRequiredStatus(ann);
    PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
    currElements.add(new AutowiredMethodElement(method, required, pd));
    }
    }
    });
    //用@Autowired修饰的注解可能不止一个,因此都加在currElements这个容器里面,一起处理
    elements.addAll(0, currElements);
    targetClass = targetClass.getSuperclass();
    }
    while (targetClass != null && targetClass != Object.class);

    return new InjectionMetadata(clazz, elements);
    }

    加了注释是不是很容易懂了?什么?还不懂?别急,我这里有一份简化版的spring代码

    /**
    *@描述
    *@方法名 populateBean
    *@参数 [beanName, beanDefinition, beanWrapper]
    *@返回值 void
    *@创建人 Baldwin
    *@创建时间 2020/3/9
    *@修改人和其它信息
    */
    private void populateBean(String beanName, BeanDefinition beanDefinition, BeanWrapper beanWrapper) {

    Class<?> clazz = beanWrapper.getWrappedClass();

    //获得所有的成员变量
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
    //如果没有被Autowired注解的成员变量则直接跳过
    if (!field.isAnnotationPresent(YzAutowired.class)) {
    continue;
    }

    YzAutowired autowired = field.getAnnotation(YzAutowired.class);
    //拿到需要注入的类名
    String autowiredBeanName = autowired.value().trim();
    if ("".equals(autowiredBeanName)) {
    autowiredBeanName = field.getType().getName();
    }

    //强制访问该成员变量
    field.setAccessible(true);

    try {
    if (this.factoryBeanInstanceCache.get(autowiredBeanName) == null) {
    continue;
    }
    //将容器中的实例注入到成员变量中
    field.set(beanWrapper.getWrapperInstance(), this.factoryBeanInstanceCache.get(autowiredBeanName).getWrapperInstance());
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    }
    }

    上面这个是我手写的简版spring框架,虽然没有完成,但是在这里用来解释@Autowired的实现机制已经是足够的。

    我们来看上面的代码,其中YzAutowired对应着spring中的Autowired,我们来看一下他的逻辑步骤

    • 获取目标类:通过反射获取所有的目标类,具体实现过程,有需要的话我再详解
    • 获取目标类的所有成员变量进行遍历:这一步是为了得到那些被YzAutowired注解的变量
    • 判断:如果当前变量没有被YzAutowired注解,那么下一个,如果有被注解,那么开始我们实现需要的功能
    • 实现功能:首先我们注解的时候是有参数的,我们可以通过注解参数名的方式来获取这个注解的参数值,然后去使用它,然后就是我们的功能逻辑代码了

    到这里,我们的注解与被修饰这之间已经联系上,而且也实现了我们预期的功能了

    小总结

    在功能实现这一部分我们最终应该要做的事主要有两件

    • 获取被注解者
    • 获取值并且完成功能实现

     

    注解实战:创建一个自己的注解

    看完上面的内容,我相信你或许对注解已经有了一定的了解,现在可以跟着作者一起来创建一个注解并且实现一个功能。功能要求比较简单,就是为通过注解为某个变量注入一个值。

    创建注解:InjectInt

    import java.lang.annotation.*;

    @Target({ElementType.FIELD})//作用于域
    @Retention(RetentionPolicy.RUNTIME)//存在于运行时
    @Documented
    public @interface InjectInt {
    int value() default 0;//默认值为0
    }

    实现注解功能

    import java.lang.reflect.Field;

    /**
    * 类描述
    *
    * @author: 12405
    * @date: 2020/3/27-23:48
    */
    public class DoInject {
    public static void inject(){
    //反射获取对象类的class
    Class clazz = AnnotationDemo.class;
    //获得所有的成员变量
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields){

    //如果没有被InjectInt注解的成员变量则直接跳过
    if (!field.isAnnotationPresent(InjectInt.class)) {
    continue;
    }

    //拿到当前变量的注解
    InjectInt injectInt = field.getAnnotation(InjectInt.class);

    //拿到注解值
    int value = injectInt.value();

    //强制访问该成员变量
    field.setAccessible(true);

    //将值注入
    try {
    field.setInt(Integer.class,value);
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    }
    }
    }
    }
    应用注解
    /**
    * 类描述
    *
    * @author: 12405
    * @date: 2020/3/27-23:37
    */
    public class AnnotationDemo {

    //注解并设置值
    @InjectInt(value = 12) static int m;

    public static void main(String[] args) {
    //开启注解功能
    DoInject.inject();

    System.out.println(m);
    }
    }

    输出

    "C:Program FilesJavajdk1.8.0_171injava.exe" "-javaagent:E:	oolsIntelliJ IDEA 2019.3.3libidea_rt.jar=61875:E:	oolsIntelliJ IDEA 2019.3.3in" -Dfile.encoding=UTF-8 -classpath "C:Program FilesJavajdk1.8.0_171jrelibcharsets.jar;C:Program FilesJavajdk1.8.0_171jrelibdeploy.jar;C:Program FilesJavajdk1.8.0_171jrelibextaccess-bridge-64.jar;C:Program FilesJavajdk1.8.0_171jrelibextcldrdata.jar;C:Program FilesJavajdk1.8.0_171jrelibextdnsns.jar;C:Program FilesJavajdk1.8.0_171jrelibextjaccess.jar;C:Program FilesJavajdk1.8.0_171jrelibextjfxrt.jar;C:Program FilesJavajdk1.8.0_171jrelibextlocaledata.jar;C:Program FilesJavajdk1.8.0_171jrelibext
    ashorn.jar;C:Program FilesJavajdk1.8.0_171jrelibextsunec.jar;C:Program FilesJavajdk1.8.0_171jrelibextsunjce_provider.jar;C:Program FilesJavajdk1.8.0_171jrelibextsunmscapi.jar;C:Program FilesJavajdk1.8.0_171jrelibextsunpkcs11.jar;C:Program FilesJavajdk1.8.0_171jrelibextzipfs.jar;C:Program FilesJavajdk1.8.0_171jrelibjavaws.jar;C:Program FilesJavajdk1.8.0_171jrelibjce.jar;C:Program FilesJavajdk1.8.0_171jrelibjfr.jar;C:Program FilesJavajdk1.8.0_171jrelibjfxswt.jar;C:Program FilesJavajdk1.8.0_171jrelibjsse.jar;C:Program FilesJavajdk1.8.0_171jrelibmanagement-agent.jar;C:Program FilesJavajdk1.8.0_171jrelibplugin.jar;C:Program FilesJavajdk1.8.0_171jrelib
    esources.jar;C:Program FilesJavajdk1.8.0_171jrelib
    t.jar;E:WorkspacesIdeaProjectsDemoTestoutproductionDemoTest" cn.yzstu.annotation.AnnotationDemo

    Process finished with exit code 0

    这样我们就完成了一个简单的注解的实现,再次创建int值时可以使用@InjectInt来实现赋值,当然这个功能并不是实用性功能,只是抛砖引玉来给大家展示注解的实现。

    总结

    最后总结一下注解实现的三部曲:

    • 创建注解,选择合适的作用域和生存时机
    • 实现注解逻辑,这一步需要我们找到注解的位置
    • 开启注解,让注解功能实现

    实际上,我们在正常工作时需要自己创建注解的时候并不多,大多数时候只需要我们理解注解的用法即可,但是注解的应用在造轮子时是非常重要的,所以如果我们希望自己能够有朝一日向大佬们一样去自己造轮子的话,还是要多了解一些注解的知识,同时,了解注解机制的实现还可以让我们更好的了解现有框架的实现。

  • 相关阅读:
    Github 代码在线在vscode中打开
    TP5如何查询字段为空
    浏览器总是报 'https://static.hae123.cn/gc/gc3.js 错误
    Sublime 复制到word 如何保留样式?
    pdf 在浏览器中是下载,而不是打开如何实现?
    Shell脚本中的set指令,比如set -x 和 set -e【转】
    Python面向对象进阶【转】
    Redis为什么变慢了?常见延迟问题定位与分析【转】
    史上最全的 Linux Shell 文本处理工具集锦【转】
    Linux运维常用命令总结【转】
  • 原文地址:https://www.cnblogs.com/CQqfjy/p/12703052.html
Copyright © 2011-2022 走看看