无论是在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 Files\Java\jdk1.8.0_171\bin\java.exe" "-javaagent:E:\tools\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=61875:E:\tools\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_171\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_171\jre\lib\rt.jar;E:\Workspaces\IdeaProjects\DemoTest\out\production\DemoTest" cn.yzstu.annotation.AnnotationDemo

Process finished with exit code 0

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

总结

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

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

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

想自己写框架?不了解Java注解机制可不行的更多相关文章

  1. Java注解 框架开发之Java注解的妙用

    原文出处: locality 注解的好处: 1.能够读懂别人写的代码,特别是框架相关的代码. 2.本来可能需要很多配置文件,需要很多逻辑才能实现的内容,就可以使用一个或者多个注解来替代,这样就使得编程 ...

  2. 组件化框架设计之Java SPI机制(三)

    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680 本篇文章将从深入理解java SPI机制来介绍组件化框架设计: ...

  3. 基础篇:深入解析JAVA注解机制

    目录 java实现注解的底层原理和概念 五种元注解详解 使用动态代理机制处理注解 spring.AOP和注解机制 (题外)@FunctionalInterface原理介绍 欢迎指正文中错误 关注公众号 ...

  4. 框架开发之Java注解的妙用

    注解的好处:1.能够读懂别人写的代码,特别是框架相关的代码.2.本来可能需要很多配置文件,需要很多逻辑才能实现的内容,就可以使用一个或者多个注解来替代,这样就使得编程更加简洁,代码更加清晰.3.(重点 ...

  5. Java 注解机制

    一.注解中的信息已经在Class中了,我们应该如何读取出来 java.lang.reflect.AnnotatedElement接口: public Annotation[] getAnnotatio ...

  6. java 注解 Annontation

    什么是注解? 对于很多初次接触的开发者来说应该都有这个疑问?Annontation是Java5开始引入的新特征,中文名称叫注解.它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metada ...

  7. 高级工程师-Java注解

    高级工程师-Java注解 前言 代码,就是我们身为程序员的名片. 简洁,优雅,统一,是我们的追求. 优秀的代码,会给浏览者一种艺术的美感.如DL大神的JUC包,感兴趣的小伙伴,可以研究一下. 那么日常 ...

  8. 【转】Android 最火框架XUtils之注解机制详解

    原文:http://blog.csdn.net/rain_butterfly/article/details/37931031 在上一篇文章Android 最火的快速开发框架XUtils中简单介绍了x ...

  9. Android 最火框架XUtils之注解机制具体解释

    在上一篇文章Android 最火的高速开发框架XUtils中简介了xUtils的基本用法,这篇文章说一下xUtils里面的注解原理. 先来看一下xUtils里面demo的代码: @ViewInject ...

随机推荐

  1. idea 2018.3.3版本激活到

        新装的,还是试用版本,下面就是进行激活操作: 先下载 链接: https://pan.baidu.com/s/1o44bsO7tx3WGuO5GgT0ytw 提取码: gbmw 第一步:将bi ...

  2. TLS是如何保障数据传输安全(中间人攻击)

    前言 前段时间和同事讨论HTTPS的工作原理,当时对这块知识原理掌握还是靠以前看了一些博客介绍,深度不够,正好我这位同事是密码学专业毕业的,结合他密码学角度对tls加解密这阐述,让我对这块原理有了更进 ...

  3. 射线与空间内三角形的相交检测算法(Möller-Trumbore)的推导与实践

    背景介绍(学习算法之前需要先了解) 射线与空间内三角形的相交检测是游戏程序设计中一个常见的问题,最典型的应用就是拾取(Picking),本文介绍一个最常见的方法,这个方法也是DirectX中采用的方法 ...

  4. 偷梁换柱:使用mock.patch辅助python单元测试

    最近在搞软工项目的后端测试,重新复习了一下python的mock.patch,并用它简化了对一些复杂逻辑的测试,在此记录 问题描述 本组的项目比较特殊,设计对教务网站的模拟登陆与信息爬取,同时不少接口 ...

  5. Java集合详解(三):HashMap原理解析

    概述 本文是基于jdk8_271版本进行分析的. HashMap是Map集合中使用最多的.底层是基于数组+链表实现的,jdk8开始底层是基于数组+链表/红黑树实现的.HashMap也会动态扩容,与Ar ...

  6. Date类常用方法总结(构造|格式化输出|String转换|Long转换|计算间隔|比较)

    java.util.Date类 它重写了toString方法,new一个Date类直接输出是按照这样的格式 // "EEE MMM dd HH:mm:ss zzz yyyy"Fri ...

  7. 最全的cURL命令使用

    cURL是什么 curl是Linux命令行工具,可以使用任何可支持的协议(如HTTP.FTP.IMAP.POP3.SCP.SFTP.SMTP.TFTP.TELNET.LDAP或FILE)在服务器之间传 ...

  8. [bug] IDEA:application context not configured for this file

    参考 https://blog.csdn.net/a772304419/article/details/79680833

  9. [DB] Hadoop免密登录原理及设置

    情景: 现有两台电脑bigdata111.bigdata112,bigdata111想免密码登录bigdata112 过程: 1.bigdata111生成公钥(用于加密,给别人)和私钥(用于解密,自己 ...

  10. [转载]libvirt(virsh命令总结)

    libvirt(virsh命令总结) virsh回车进入交互式界面: version pwd hostname 显示本节点主机名 nodeinfo  显示节点信息 list --all 显示所有云主机 ...