Annotation-注解实现原理探究与自定义注解示例展示

一、什么是注解?

        Annontation是Java5开始引入的新特征,中文名称叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。

        Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation 包中。

        「注解的本质就是一个继承了 Annotation 接口的接口」

二、注解的用处

  1. 生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等
  2. 跟踪代码依赖性,实现替代配置文件功能。比如Dagger 2 依赖注入,未来java 开发,将大量注解配置,具有很大用处;
  3. 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。

三、元注解

java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解):

@Documented – 注解是否将包含在JavaDoc中
@Retention – 什么时候使用该注解
@Target – 注解用于什么地方
@Inherited – 是否允许子类继承该注解

1、@Retention – 定义该注解的生命周期

public enum RetentionPolicy {
    /**
     * 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。
     * @Override, @SuppressWarnings都属于这类注解。
     */
    SOURCE,

    /** 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式 */
    CLASS,

    /**
     * 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。
     * 我们自定义的注解通常使用这种方式。
     */
    RUNTIME
}

2、Target – 表示该注解用于什么地方。默认值为任何元素,表示该注解用于什么地方。可用的ElementType 参数包括:

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

    /** 成员变量、对象、属性(包括enum实例) */
    FIELD,

    /** 用于描述方法 */
    METHOD,

    /** 用于描述参数 */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** 用于描述局部变量 */
    LOCAL_VARIABLE,

    /** 注释类型声明 */
    ANNOTATION_TYPE,

    /** 用于描述包 */
    PACKAGE,

    /** 表示该注解能使用在自定义类型参数 jdk1.8引入 */
    TYPE_PARAMETER,

    /** 表示该注解能使用在使用类型的任意语句中 
     *  包括示例创建时、类型强转时、方法形参中、泛型中使用 
     *  jdk1.8引入 
     */
    TYPE_USE
}

3、@Documented –  一个简单的Annotations 标记注解,表示是否将注解信息添加在java 文档中。

4、@Inherited – 定义该注解和子类的关系。

        @Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class,则这个annotation 将被用于该class 的子类。

四、JDK三个内置注解

1、@Override   限定重写父类方法, 实现接口方法。该注解只能用于方法
2、@Deprecated  用于表示所修饰的元素(类, 方法,构造器等等)已过时。
3、@SuppressWarnings 抑制编译器警告。它不是一个标记类型注解。
它有一个类型为String[] 的成员,这个成员的值为被禁止的警告名。
如:@SuppressWarnings("unchecked")

五、注解的原理

        注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

六、注解原理探究

        上述内容我们简单介绍了注解的原理,也提到,「注解的本质就是一个继承了 Annotation 接口的接口」,现在我们就来从虚拟机的层面看看,注解的本质到底是什么。

首先,我们自定义一个注解类型:

/**
 * 注解原理解析-测试用
 * @author xiongxy
 * @Date 2021/9/22 20:41
 */
@Target(value = {ElementType.METHOD,ElementType.FIELD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Hello {
    String value();
}

        这里我们指定了 Hello 这个注解只能修饰字段和方法,并且该注解永久存活,以便我们反射获取。虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,就可以被写进字节码文件。属性表有以下几种:

  • RuntimeVisibleAnnotations:运行时可见的注解
  • RuntimeInVisibleAnnotations:运行时不可见的注解
  • RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
  • RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
  • AnnotationDefault:注解类元素的默认值

        给大家看虚拟机的这几个注解相关的属性表的目的在于,让大家从整体上构建一个基本的印象,注解在字节码文件中是如何存储的。所以,对于一个类或者接口来说,Class 类中提供了以下一些方法用于反射注解。

  • getAnnotation:返回指定的注解
  • isAnnotationPresent:判定当前元素是否被指定注解修饰
  • getAnnotations:返回所有的注解
  • getDeclaredAnnotation:返回本元素的指定注解
  • getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的

下面看一个完整的例子。

首先,设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

然后main函数

/**
 * 注解原理解析
 * @author xiongxy
 * @Date 2021/9/22 20:40
 *
 * 注意:需要设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。
 * -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
 */
public class Test {

    @Hello("hello")
    public static void main(String[] args) throws NoSuchMethodException{
        Class cls = Test.class;
        Method method = cls.getMethod("main", String[].class);
        Hello annotation = method.getAnnotation(Hello.class);
    }
}

当我们通过反射,也就是我们这里的 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。

我们运行程序后,会看到输出目录里有这么一个代理类,反编译之后是这样的:

代理类实现接口 Hello 并重写其所有方法,包括 value 方法以及接口 Hello 从 Annotation 接口继承而来的方法。

而这个关键的 InvocationHandler 实例是谁?我们点进去看看

上图中第三个,AnnotationInvocationHandler 是 JAVA 中专门用于处理注解的 Handler, 这个类的设计也非常有意思。

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;

    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

这里有一个 memberValues,它是一个 Map 键值对,键是我们注解属性名称,值就是该属性当初被赋上的值。

public Object invoke(Object var1, Method var2, Object[] var3) {
    String var4 = var2.getName();
    Class[] var5 = var2.getParameterTypes();
    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
        return this.equalsImpl(var3[0]);
    } else if (var5.length != 0) {
        throw new AssertionError("Too many parameters for an annotation method");
    } else {
        byte var7 = -1;
        switch(var4.hashCode()) {
        case -1776922004:
            if (var4.equals("toString")) {
                var7 = 0;
            }
            break;
        case 147696667:
            if (var4.equals("hashCode")) {
                var7 = 1;
            }
            break;
        case 1444986633:
            if (var4.equals("annotationType")) {
                var7 = 2;
            }
        }

        switch(var7) {
        case 0:
            return this.toStringImpl();
        case 1:
            return this.hashCodeImpl();
        case 2:
            return this.type;
        default:
            Object var6 = this.memberValues.get(var4);
            if (var6 == null) {
                throw new IncompleteAnnotationException(this.type, var4);
            } else if (var6 instanceof ExceptionProxy) {
                throw ((ExceptionProxy)var6).generateException();
            } else {
                if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                    var6 = this.cloneArray(var6);
                }

                return var6;
            }
        }
    }
}

大家注意看,我们的代理类代理了 Hello 接口中所有的方法,所以对于代理类中任何方法的调用都会被转到这里来。

var2 指向被调用的方法实例,而这里首先用变量 var4 获取该方法的简明名称,接着 switch 结构判断当前的调用方法是谁,如果是 Annotation 中的四大方法,将 var7 赋上特定的值。

如果当前调用的方法是 toString,equals,hashCode,annotationType 的话,AnnotationInvocationHandler 实例中已经预定义好了这些方法的实现,直接调用即可。

那么假如 var7 没有匹配上这四种方法,说明当前的方法调用的是自定义注解自己声明的方法,例如我们 Hello 注解的 value 方法。这种情况下,将从我们的注解 map 中获取这个注解属性对应的值。

最后,我们总结一下整个反射注解的原理:

首先,我们通过键值对的形式可以为注解属性赋值,像这样:@Hello(value = "hello")。

接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。

然后,当你进行反射的时候,虚拟机将所有生命周期在 RUNTIME 的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。

最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。

那么这样,一个注解的实例就创建出来了,它本质上就是一个代理类,我们应当去理解好 AnnotationInvocationHandler 中 invoke 方法的实现逻辑,这是核心。

一句话概括就是,通过方法名返回注解属性值

七、自定义注解示例

下面我们通过定义两个自定义注解,借助 aspectj 的环绕通知来实现对业务表中的字典编码自动映射成字典值。

首先,我们定义两个自定义注解:ReplaceField属性注解,与ReplaceMethod 方法注解

/**
 * 属性注解
 * @author xiongxy
 * @since 2021-09-11
 */
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReplaceField {
    String name() default "";
    String type() default ReplaceFieldType.FIELD;
}
/**
 * 方法注解
 * @author xiongxy
 * @since 2021-09-11
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReplaceMethod {
}
接下来,我们定义一个切面
/**
 * 处理枚举字段替换切面
 *
 * @author Xiongxy
 * @date 2021-08-25
 */
 @Slf4j
 @Aspect
 @Component
 public class ReplaceFieldAspect {
    /**
    * 处理器工厂
    */
    @Autowired
    private ReplaceFieldFactory facotry;

    /**
    * 切点,使用注解的地方
    */
    @Pointcut("@annotation(com.tuling.xxy.annotation.ReplaceMethod)")
    public void init() {
    }

    /**
    * 环绕通知,获取替换枚举的类型,替换枚举返回值
    *
    * @param point 连接点
    * @return 返回值
    */
    @Around("init()")
    public Object doAfterRunning(ProceedingJoinPoint point) {
        // 获取返回值
        Object retVal = null;
        try {
            // 获取返回值
            retVal = point.proceed();

            // 获取返回对象的类型
            String className = retVal.getClass().getName().substring(retVal.getClass().getName().lastIndexOf(".") + 1);

            // 对应handler处理
            ReplaceFieldHandler handler = facotry.get(ReplaceHandlerEnum.convert(className));
            handler.replace(retVal);

            return retVal;
        } catch (Throwable throwable) {
            log.error("替换枚举变量失败。", throwable);
        }
        return retVal;
    }
}

我们还需要定义一个工厂处理类

@Slf4j
@Component
public class ReplaceFieldFactory {

    @Autowired
    DictionaryTestService dictionaryTestService;

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * map,存放handler
     */
    private Map<String, ReplaceFieldHandler> handlerMap = new HashMap<>();

    /**
     * 注册handler
     *
     * @param key     handler key
     * @param handler handler
     */
    public void register(String key, ReplaceFieldHandler handler) {
        if (handlerMap.containsKey(key)) {
            log.error("handler is already exists!");
            return;
        }
        handlerMap.put(key, handler);
    }

    /**
     * 获取handler
     *
     * @param key key
     * @return handler
     */
    public ReplaceFieldHandler get(String key) {
        return handlerMap.getOrDefault(key, null);
    }

    /**
     * 替换数据的值
     *
     * @param obj 要替换的对象
     */
    public void replace(Object obj) {
        Class<?> objClass = obj.getClass();
        Field[] declaredFields = objClass.getDeclaredFields();
        for (Field field : declaredFields) {
            ReplaceField annotation = field.getAnnotation(ReplaceField.class);
            if (annotation != null) {
                String type = annotation.type();
                String name =
                    StringUtils.isNotEmpty(annotation.name()) ? annotation.name() : field.getName();
                if (ReplaceFieldType.FIELD.equals(type)) {
                    field.setAccessible(true);
                    try {
                        Object fieldObj = field.get(obj);
                        if (null != fieldObj) {
                            String oldValue = fieldObj.toString();
                            if (oldValue.contains(",")) {
                                String[] split = oldValue.split(",");
                                StringBuilder value = new StringBuilder();
                                for (String str : split) {
                                    String newValue = convert(name, str);
                                    value.append(newValue).append(",");
                                }

                                field.set(obj,
                                    value.toString().substring(0, value.lastIndexOf(",")));
                            } else {
                                String newValue = convert(name, fieldObj.toString());
                                field.set(obj, newValue);
                            }
                        }

                    } catch (IllegalAccessException e) {
                        log.error("替换枚举值失败。", e);
                    }
                } else {
                    try {
                        field.setAccessible(true);
                        replace(field.get(obj));
                    } catch (IllegalAccessException e) {
                        log.error("替换枚举值失败。", e);
                    }
                }
            }
        }
    }

    /**
     * 通过类型和value获取新值
     *
     * @param type dic_group
     * @param old  dic_value
     * @return 新值,没有数据返回旧值
     */
    private String convert(String type, String old) {
        String dicGroup = "";
        if (ReplaceFieldConstant.dicGroupMap.containsKey(type)) {
            dicGroup = ReplaceFieldConstant.dicGroupMap.getOrDefault(type, null);
        } else {
            dicGroup = CodeStringUtil.camel2Underline(type);
        }
        if (StringUtils.isNotBlank(dicGroup)) {
            log.info("convert -- "+dicGroup+"_"+old);
            DictionaryTest dicInfo = (DictionaryTest) redisTemplate
                .boundValueOps(dicGroup + "_" + old).get();
            if (null != dicInfo) {
                return dicInfo.getDicName();
            }

        }
        return old;
    }
}

最后我们在需要转换的属性和方法上添加注解即可:

/**
 * 实体类属性上加属性注解
 */
@ReplaceField(name = ReplaceFieldConstant.AGENCY_TYPE)
private String type;
/**service层业务方法上加方法注解*/
@Override
@ReplaceMethod
public List<AgencyTest> getAll() {
    return agencyTestMapper.selectList(new QueryWrapper<>());
}

字典表数据:

业务表数据:

最终效果:

[
    {
        "id": 1,
        "name": "代办1",
        "title": "代办1标题",
        "type": "任务代办,提醒代办,快速代办,个人代办",
        "createTm": 1631541753000,
        "updateTm": 1631541753000
    },
    {
        "id": 2,
        "name": "代办2",
        "title": "代办2标题",
        "type": "提醒代办",
        "createTm": 1631368975000,
        "updateTm": 1631368975000
    }

    ......
]

  完整项目我已上传到GitHub:

GitHub - 1989lili/interface-test: 通过自定义注解 + aspectj 的环绕通知来实现对业务表中的字典编码自动映射成字典值。

本篇参考文章:


注解Annotation实现原理与自定义注解例子 - 贾树丙 - 博客园

JAVA 注解的基本原理 - Single_Yam - 博客园

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值