品Spring:注解终于“成功上位”

2022-10-15,,,,

历史还是抛弃了xml,当它逐渐尝到注解的甜头之后。



尤其是在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();
}


foo上标了@repeatable,所以它就是可重复注解,因此需要一个容器注解,这个容器注解也是通过@repeatable指定的,这里就是bar了。



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 {

}


定义一个注解@b,并使用@a标注,如下:



@a
public @interface b {

}


定义一个类c,并使用@b标注,如下:



@b
public class c {

}


这里的类c的class<?>对象(即c.class)就是一个被注解元素,在它上面调用刚讲到的7个api,都只能获取到注解@b,而无法获取元注解@a。



可以认为是不能越级,只能获取到离自己最近的这一级注解,除此之外的那些隔一到多级的注解统统获取不到。



那如何才能获取到注解@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 "";
}


没错,就是使用@aliasfor注解建立向上传递的映射关系,其实就是指明一个属性的值应该传递给哪个元注解的哪个属性。



这个方法是不是很简单啊,哈哈。但是实现起来可不一定简单。



也类比一下,在使用面向对象的继承和组合关系时,都涉及到数据的传递。



继承时是子类要调用父类的构造函数,子类给父类传数据,相当于从下向上传递。



组合时是产生的新类要调用参与组合的各个类的构造函数,要给它们传递数据,相当于从外向内传递。



现在应该理解的更加清晰了吧。说实话:



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系列文章列表:

品spring:帝国的基石

品spring:bean定义上梁山

品spring:实现bean定义时采用的“先进生产力”

 

作者是工作超过10年的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。下面是公众号和知识星球的二维码,欢迎关注!

 

       

《品Spring:注解终于“成功上位”.doc》

下载本文的Word格式文档,以方便收藏与打印。