品Spring:注解终于“成功上位”
尤其是在Spring帝国,到处充满着注解的气息。
注解从一个提供附属信息的“门客”,蜕变为颇具中流砥柱的“君侯”。
注解成功登上了帝国的舞台,定会像XML一样留下浓墨重彩的一笔。
重新认识一下注解
注解其实就是注释、批注的意思。就像看书时在旁边记笔记一样。
如果把书上印刷的内容看作是原始信息,那写上的笔记则是新添加的额外信息,且这个额外信息并不会对原始信息造成破坏。
所以注解其实是为原始数据信息添加额外附加数据信息的一种方式,且对原有数据信息没什么损害。
Java在1.5的时候引入了注解,注解依附于程序代码,但对程序代码的运行没有什么影响。且可以提供一份不同于程序代码的数据,通常称之为“元数据”。
因注解必须“寄人篱下”,所以注解一般标在类上/接口上/方法上/字段上/方法参数上/泛型类型上等。
如果把一个类型生成的对象信息看作是数据的话,那么类型本身的信息就是“元数据”。如它定义了哪些字段啊、方法啊等。
显然,这些类型的元数据信息都是通过反射的方式来获取的,由于注解也是元数据,所以也采用反射的方式获取注解。
Java注解的详细介绍
首先把能够标注解的这些程序代码统称为“元素”,那么类啊、接口啊,方法啊等这些都是元素。
站在元素的立场,注解一定是某个元素的注解,因为注解不能独立存在,所以必须依附于某个元素。
站在注解的立场,元素就是被注解标注的元素,所以也称为被标注元素或被注解元素。
我为什么非要把这个“被注解元素”的概念提溜出来讲呢,因为Java API中就有这个接口,叫做AnnotatedElement。
元素或被注解元素就是指程序代码,是主体,注解就是附属品,是提供额外信息的,是为主体服务的。
那当我们定义注解的时候,注解本身也是程序代码,也就成了主体。有意思的是这些代码上还可以再标上注解。
那么这些注解就成了定义注解的注解,或者说服务注解的注解,因此称之为“元注解”。大部分人都应该很熟悉了。
在Java官方提供的元注解中,有两个需要特别关注一下,一个是@Inherited,一个是@Repeatable,其中后者是JDK1.8才新添加的。
@Inherited是标识注解具有继承性的:
被该元注解标注的注解,在使用的时候具有继承性。相反,没有被它标注的,在使用时不具有继承性。
这种继承性的适用场景(截止到Java8)有且只有一种,就是父类(super class)上的注解可以自动传递到子类(sub class)上。
再强调一下,继承性只适用于类(即class)上的注解。除此之外的像接口,方法,字段,方法参数,泛型类型等上的注解统统不支持继承。
在这种情况下,即便使用了具有继承性的注解也不会报错,只是注解不会被继承而已。
如果子类和父类上都标注了同一个具有继承性的注解,则子类的注解会覆盖父类的。
其实可以理解为先在本类上查找注解,如果没有的话,且要查找的注解是具有继承性的,才会去父类上继续查找。
@Repeatable是表示注解是可重复标注的:
使用过注解的都会发现,在同一个元素上,一个注解只能出现一次,如果多于一次,IDE就会直接提示错误。
这说明注解是不可重复标注的。为了解决这个问题,在JDK1.8中引入了@Repeatable这个元注解。
使用该元注解定义的注解具有可重复性,也就是可以重复标注了。当标注达到两个及以上时,其实就相当于构成了注解数组了。
因为多个类型相同的东西放到一起,就是数组这种数据结构对应的场景了。因此在定义具有可重复性注解的时候,是有特殊要求的。
就是还需要定义一个与之对应的“容器”注解。这个容器注解必须定义一个value属性,且属性类型就是可重复注解的数组。
@Repeatable(Bar.class)
public @interface Foo {
}
public @interface Bar {
Foo[] value();
}
Bar既然是Foo的容器,所以就要定义Foo[]类型的value()属性。除了value属性之外,还可以定义其它属性,但是都必须要有默认值,这一点需要记住。
操作注解仅有的几个API
共7个方法,可以分为3组,且非常有特点:
1)方法名称中什么都不带的,表示关注本类上标的注解和从父类继承的注解。
2)方法名称中带Declared的,表示只关注本类上标的注解,不考虑从父类继承的。
3)方法名称中带ByType的,表示除了原来的功能外,还会对可重复性注解进行特殊处理。
看下这3组方法:
获取本类的和从父类继承的
Annotation[] getAnnotations()
<T extends Annotation> T getAnnotation(Class<T> annotationClass)
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
Annotation[] getDeclaredAnnotations()
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)
对于可重复的注解,如果没有获取到,会尝试获取它的容器注解,如果获取到则返回它的value属性,反之则不存在该可重复注解。
有一点需要说明,以上这些方法只能获取到元素上的注解,不能获取到这些注解上的元注解,看个示例吧。
定义一个元注解@A,如下:
public @interface A {
}
@A
public @interface B {
}
@B
public class C {
}
可以认为是不能越级,只能获取到离自己最近的这一级注解,除此之外的那些隔一到多级的注解统统获取不到。
那如何才能获取到注解@A呢?答案很简单,相信很多人都知道。
注解@B的Class<?>对象(即B.class)也是一个被注解元素啊,在它上面调用上面的API就可以获取到注解@A了。
因为注解@A是离B最近一级的注解呀。哈哈。
这其实是一种递归的思想,所以想要完善的操作Java注解,基本都要采用递归的方式去实现。
有几个API返回的是注解数组,注解在数组中的排序也是有规定的:
a)按照在源码中的位置,从左上到右下(其实就是出现的先后)的顺序依次出现在注解数组中。
b)如果考虑从父类继承注解的话,父类的注解会排在子类的前面(其实也是一种先后顺序)。
c)如果考虑子类重写(也称覆盖)父类注解的话,位置按父类上的来算,返回的注解则是子类上的。
Java对注解的支持也只有这么些了,至于该怎么用,就看各自发挥了。
Spring对注解含义的扩充
Spring对注解进行了3个方面的扩充:
1)注解和元注解在某些方面可以是相似的
假如有一个元注解@A,用它定义了一个注解@B,在Java中并没有规定@A和@B之间到底是什么关系,或是否要有关系。
不过Spring进行了一些规定,在某些方面@A和@B具有相似性。看一个例子。
@Component表示一个类是一个组件,它既是一个注解也是一个元注解,因为@Repository、@Service、@Controller这个三个注解在定义时用到了它。
从bean注解的角度看,这四个注解都可以让一个bean被注册,而且都是一个组件,从这方面看它们是相似的,不同之处就是后面三个注解更加侧重某一方面的功能。
类比一下,元注解和注解之间类似于面向对象中的子类和父类之间的继承关系。父类表示一般,子类表示具体。
2)多个元注解组合在一起,产生一个新注解
当标注的注解较多时,也略显麻烦,不如将它们合起来,用一个新的注解代替,这样既保留了原来的功能,也减少了注解的数量。
在Spring中这样的情况比较多,如Spring MVC中的:
@RestController = @Controller + @ResponseBody
如Spring Boot中的:
@SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
这种功能确实是一个不错的特性。不过底层代码要采用递归来实现,而且要足够通用才行。
也类比一下,这种情况类似于面向对象中类的组合关系,多个功能不同的类组合在一起,就形成了一个功能全面的综合类了。
3)打通从下往上的属性传递通道
无论是注解还是元注解都可以定义属性,而且有时必须被设置为合理的值才有意义。这样就会造成一个问题。
由于我们只能为注解设置属性,那怎么为元注解设置属性呢?Java官方并没有这方面的内容,因此无法通过注解为元注解设置属性。
但是上面的两条扩充偏偏都遇到了这个问题。看个示例或许会更明白写,如下:
@Component注解有一个String类型的value属性,可以为组件指定一个名称。用它定义出来的@Controller注解也有这样一个属性。
我们在使用@Controller时只能把属性值设置给它本身,没有办法直接设置给它的元注解@Component,但是这个属性值必须要想办法从@Controller向上传递给@Component,这样才能保证整体的扩充是意义完整的。
既然Java本身没有提及这方面的内容,那Spring又是怎么做的呢,看看源码吧,其实方法很简单:
public @interface Component {
String value() default "";
}
@Component
public @interface Controller {
@AliasFor(annotation = Component.class)
String value() default "";
}
这个方法是不是很简单啊,哈哈。但是实现起来可不一定简单。
也类比一下,在使用面向对象的继承和组合关系时,都涉及到数据的传递。
继承时是子类要调用父类的构造函数,子类给父类传数据,相当于从下向上传递。
组合时是产生的新类要调用参与组合的各个类的构造函数,要给它们传递数据,相当于从外向内传递。
现在应该理解的更加清晰了吧。说实话:
Spring对注解的扩充,就相当于让注解本身来支持继承和组合,所以必然涉及数据的传递问题。
其实就是这个样子的,只不过面向对象里的数据传递是天然支持的,而注解这里的需要Spring自己想办法搞定。
也说明了另一个问题:
继承和组合这两种法则可以用到很多事物上,而且看起来都很“完美”。
Spring获取所有注解信息的方法
注解信息是bean定义信息的一部分,Spring是采用ASM框架读取字节码文件内容来获取bean定义信息的。这是本号上一篇文章的核心内容。
当时在文章末尾也提到了这种方式对注解有一个弊端,就是只能获取到显式设置的注解属性值,注解的默认属性值是获取不到的。
因为bean的字节码文件里没有这些信息。这些信息是在注解自己的字节码文件里呢。那Spring是继续使用ASM来读注解本身的字节码文件吗?继续往下看便知。
由于Spring对注解含义进行了扩充,所以除了注解之外的那些元注解,以及元注解的元注解(还可以继续向上)对bean定义来说都非常重要。
同样也涉及到这些所有级别的元注解们的显式设置的属性值和默认的属性值。同时还涉及到属性值的向上传递问题(即对@AliasFor注解的处理)。
这不一定是一个特别复杂的问题,但一定是一个特别繁琐的问题。那Spring应该怎么处理呢?一起分析一下就会明了很多。
首先,不但要关注注解,还要关注注解的注解即元注解,以及元注解的注解即元元注解,这样一个沿着注解、元注解向上的处理方向。
理论上可以无限级,但至少会有好几级吧。可以认为这是一个纵向上的问题。
其次,注解的属性可能是另外一个注解即新注解,新注解的属性又可能是其它另外一个注解即新新注解,这样一个沿着注解、新注解向外的处理方向。
理论上可以无限级,但至少会有好几级吧。可以认为这是一个横向上的问题。
问题已经找到,那该采用什么方式来处理呢?再来分析一下吧。
纵向上的通常称为“继承”,横向上的通常称为“嵌套”。无论是继承还是嵌套,它们有一个共同的特点,都是在对自身进行着不停的重复。
对于一直在重复自身的这种情景,在数据结构里有一个名词与之契合,没错,就是递归。其实所有人都知道了。哈哈。
递归可以解决结构上重复的问题,那获取属性数据该采用什么方法呢?上一篇文章提到,总共就两种方法,反射和读字节码。
对于bean(就是类)的信息和方法(Method)的信息,Spring选择了读字节码。但本次对于注解的信息,Spring却选择了反射。
也来分析下原因,(我猜)可能是这样的:
通过ASM读取字节码,ASM使用访问者模式,本身里面就已经有递归了,所以理解起来稍显麻烦。
如果再以递归的方式去读取不同注解的字节码,就相当于把两层的递归揉到了一起,代码复杂度较高。
注解整体相对固定而且数目较少,都使用JVM来加载,并不会造成太多的消耗。
使用反射的方式获取注解信息相对简单一些,而且Java支持的还可以。
使用反射方式获取到的信息已经足够用了,没必要从字节码中读取了。
总之,对于注解信息的获取是基于反射进行的。
PS:后续会讲讲Spring对注解这块的代码实现。
源码地址:
https://github.com/coding-new-talking/java-code-demo.git
品Spring系列文章列表:
作者是工作超过10年的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。下面是公众号和知识星球的二维码,欢迎关注!
品Spring:注解终于“成功上位”的更多相关文章
- 品Spring:能工巧匠们对注解的“加持”
问题的描述与方案的提出 在Spring从XML转向注解时,为了自身的开发方便,对注解含义进行了扩充(具体参考本号上一篇文章). 这个扩充直接导致了一个问题,就是需要从注解往元注解以及元元注解(即沿着从 ...
- 品Spring:注解之王@Configuration和它的一众“小弟们”
其实对Spring的了解达到一定程度后,你就会发现,无论是使用Spring框架开发的应用,还是Spring框架本身的开发都是围绕着注解构建起来的. 空口无凭,那就说个最普通的例子吧. 在Spring中 ...
- 品Spring:对@PostConstruct和@PreDestroy注解的处理方法
在bean的实例化过程中,也会用到一系列的相关注解. 如@PostConstruct和@PreDestroy用来标记初始化和销毁方法. 平常更多的是侧重于应用,很少会有人去了解它背后发生的事情. 今天 ...
- 品Spring:对@Resource注解的处理方法
@Resource是Java的注解,表示一个资源,它具有双向的含义,一个是从外部获取一个资源,一个是向外部提供一个资源. 这其实就对应于Spring的注入和注册.当它用在字段和方法上时,表示前者.当它 ...
- 品Spring:对@Autowired和@Value注解的处理方法
在Spring中能够完成依赖注入的注解有JavaSE提供的@Resource注解,就是上一篇文章介绍的. 还有JavaEE提供的@javax.inject.Inject注解,这个用的很少,因为一般都不 ...
- 品Spring:SpringBoot和Spring到底有没有本质的不同?
现在的Spring相关开发都是基于SpringBoot的. 最后在打包时可以把所有依赖的jar包都打进去,构成一个独立的可执行的jar包.如下图13: 使用java -jar命令就可以运行这个独立的j ...
- 品Spring:负责bean定义注册的两个“排头兵”
别看Spring现在玩的这么花,其实它的“筹码”就两个,“容器”和“bean定义”. 只有先把bean定义注册到容器里,后续的一切可能才有可能成为可能. 所以在进阶的路上如果要想走的顺畅些,彻底搞清楚 ...
- 品Spring:SpringBoot轻松取胜bean定义注册的“第一阶段”
上一篇文章强调了bean定义注册占Spring应用的半壁江山.而且详细介绍了两个重量级的注册bean定义的类. 今天就以SpringBoot为例,来看看整个SpringBoot应用的bean定义是如何 ...
- 品Spring:SpringBoot发起bean定义注册的“二次攻坚战”
上一篇文章整体非常轻松,因为在容器启动前,只注册了一个bean定义,就是SpringBoot的主类. OK,今天接着从容器的启动入手,找出剩余所有的bean定义的注册过程. 具体细节肯定会颇为复杂,同 ...
随机推荐
- 【JS】308- 深入理解ESLint
点击上方"前端自习课"关注,学习起来~ 本文来自于"自然醒"投稿至[前端早读课]. 小沈是一个刚刚开始工作的前端实习生,第一次进行团队开发,难免有些紧张.在导师 ...
- K3cloud、erp系统实时滚动展示未处理数据,监控投诉处理进度
痛点:企业内部erp人工记录产品投诉销售单,是否跟踪处理完客户投诉,结果不能实时透明,当天还有多少未解决的投诉单,也不能实时查看到,除非手工去系统单据查询,很不方便,跟踪也不顺畅! 解决方案:利 ...
- java发送邮件基础方法(另附部分主流邮箱服务器地址、端口及设置方法)
java发送邮件基础方法,可通过重载简化参数 import java.io.File; import java.io.UnsupportedEncodingException; import java ...
- wxxcx_learn订单
自动写入时间戳 protected $autoWriteTimestamp = true: 事务的使用 Db::startTrans();....... Db::commit();.. Db::rol ...
- JS-变量、作用域、垃圾回收机制总结
预解析时变量和函数同名的话,保留函数
- 【Jackson】使用学习
Jackson学习 文档:http://tutorials.jenkov.com/java-json/index.html https://github.com/FasterXML/jackson/w ...
- MySQL必知必会-官方数据库表及SQL脚本导入生成
最近在复习SQL语句,看的是MySQL必知必会这本书,但是发现附录中只有表设计,没有表的具体数据.所以在学习相应的语句中体验不是很好,去网上查了数据库的内容,自己慢慢导入到了数据库中.把表放出来作为参 ...
- 人生苦短,我用Python(3)
1.对列表进行排序: (1)使用列表对象的sort()方法: 列表对象提供了sort()方法用于对原列表中的元素进行排序.排序后原列表中的元素顺序将发生改变.改变对象的sort()方法的语法格式如下: ...
- JS中原始值和引用值分析
JS中变量中两种类型的值:原始值,引用值 原始值是存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置. var x = 1; //1就是一个原始值,变量x中存放的就是原始 ...
- CentOS7 安装 Redis 并设置开机启动
1.下载 https://redis.io/download cd /usr/local/src wget -c http://download.redis.io/releases/redis-3.2 ...