品Spring:能工巧匠们对注解的“加持”

问题的描述与方案的提出


在Spring从XML转向注解时,为了自身的开发方便,对注解含义进行了扩充(具体参考本号上一篇文章)。

这个扩充直接导致了一个问题,就是需要从注解往元注解以及元元注解(即沿着从下向上的方向)里传递数据。

为了更好的描述这个问题,请再看个示例:

@interface A {

    String a() default "";
}

@A
@interface B {

    String a() default "";

    String b() default "";
}

@B
@interface C {

    String a() default "";

    String b() default "";

    String c() default "";
}

@C(a = "a", b = "b", c = "c")
class D {

}

这里共有三个注解@A、@B、@C。@A的级别最高,@C的级别最低,数据传递是从@C向@A,所以是从下向上。

温馨提示,请务必明白注解级别的高低,什么是高级别注解,什么是低级别注解,本文会一直这样称呼。

我们最终设置的是注解@C的a、b、c三个属性。同时急切期望的是注解@C能把a、b属性传递给注解@B,注解@B能把a属性传递给注解@A。

这个期望对Spring来说非常重要,可惜了,Java注解在这方面是空白的。不过Spring早已司空见惯,不必大惊小怪,没有路就自己开一条。

首先要解决的就是一个映射的问题,总得知道哪个注解的哪个属性映射到哪个注解的哪个属性吧,其实就是需要再额外附加上一个映射信息。

Spring给出的方案很简单,就是用一个注解@AliasFor,把它标注在需要传递数据的属性上,并指明传递给谁(哪个注解的哪个属性)。

再次呼应一下上一篇文章的主题,要添加的映射信息是额外附加信息,这正是注解的功能所在呀,所以现在就是用注解去解决注解的问题。哈哈。

这样一来的话,上面的示例就变为:

@interface A {

    String a() default "";
}

@A
@interface B {

    @AliasFor(annotation = A.class, attribute = "a")
    String a() default "";

    String b() default "";
}

@B
@interface C {

    @AliasFor(annotation = B.class, attribute = "a")
    String a() default "";

    @AliasFor(annotation = B.class, attribute = "b")
    String b() default "";

    String c() default "";
}

通过简单的标注,就已经建立起了映射关系,可以说已经对上号了,至于代码如何实现则另当别论啦。

@AliasFor也有一些简洁用法:

1)如果映射的属性名称一样,则可以不指定属性名,即attribute不用设置。

2)如果映射的是同一个注解里的属性名,则可以不指定注解,即annotation不用设置。

3)这个映射是可以跳级的,可以从注解跳过元注解直接映射到元元注解。

下面请再看下@AliasFor的源码:

public @interface AliasFor {

    @AliasFor("attribute")
    String value() default "";

    @AliasFor("value")
    String attribute() default "";

    Class<? extends Annotation> annotation() default Annotation.class;
}



@AliasFor注解的三种用法


一)注解内部的显式别名

看下面这个注解:

@interface A {

    @AliasFor("y")
    String x() default "";

    @AliasFor("x")
    String y() default "";
}

在一个注解里面,把一对属性相互指定为对方的别名

注意,只能是一对,即两个,不能是三个及以上。

至于为什么会有这种用法,我给出一个猜测吧。

Java注解规定:

当只需设置注解的一个属性时,且属性名称是“value”,设置时可以省略属性名称。

看个示例:

@interface E {

    String value(default "";
}

在使用这个注解时有两种写法,@E(value = "abc") 和 @E("abc"),它俩是一回事,但后者要简洁很多。

这里有个问题,就是属性名必须是“value”才行,但value这个名字很中性,不能很好的表达意图。

所以很多时候,都会定义更有意义的属性名,比如用“name”表示名称要比value好得多,如下:

@interface E {

    String value() default "";

    String name() default "";
}

但是又想使用刚才那种简洁的用法,咋办呢?让它俩互为别名即可,如下:

@interface E {

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";
}

现在value和name已经互为对方的别名,所以@E("abc")、@E(name = "abc")都是一样的,显然前者更好看。

不知你是否注意到@AliasFor注解的源码本身就已经使用了这种方法。

二)元注解中属性显式别名

看个示例:

@E
@interface F {

    @AliasFor(annotation = E.class, attribute = "name")
    String id() default "";
}

这样就把注解@F的id属性映射为元注解@E的name属性了。

三)注解内部的隐式别名

看个示例:

@F
@interface G {

    @AliasFor(annotation = F.class, attribute = "id")
    String a() default "";

    @AliasFor(annotation = F.class, attribute = "id")
    String b() default "";

    @AliasFor(annotation = E.class, attribute = "name")
    String c() default "";
}

属性a和b都映射为注解@F的id属性,所以它俩指向一处,因此互为别名。

注解@F的id属性又映射为注解@E的name属性,这恰好又和属性c的映射一样。

因此属性a、b和c三者互相互为别名。因为它们虽然殊途但是同归。

由此也可以看出,映射是具有传递性的。X -> Y -> Z,可以推导出X -> Z。

这个映射的建立是有前提条件的:

1)属性的返回类型必须是同一个

2)属性必须要有默认值

3)属性的默认值必须是同一个

在使用时也是有限制的:

1)互为别名的属性只需设置其中任何一个即可

2)如果设置了其中多个,设置的值必须一样。

这些其实很好理解,就像不管是大名、小名、笔名或曾用名,最后指向的必须是同一个人才行。

这就是Spring给出的方案,既简单又直白,所见即所得啊,个人觉得这是上等的方案,也是鄙人的追求。

终于还是要走到该思索如何实现这一步,就像不管什么样的媳妇儿最终都要见公婆。那就勇敢面对吧。

不过可以预见的是,实现起来应该比较麻烦。因为不可能既有魔鬼身材又有天使面孔。

好事不会都让一个人占完,上帝对谁都是公平的。

长得帅的学习差,学习好的长的丑。哈哈哈哈。


从简单处入手,先捡软柿子捏


从Java语言规范中得知,注解就是一个接口(多了个@而已),注解的属性就是接口方法,而且方法必须是无参的,返回值必须不能为void。

这些方法就相当于“getter”方法,啥意思呢?只读的呗。明白了吧,就是注解的属性值在运行时是无法修改的。

因此,我们在向上传递属性值的时候,是不能像普通Java bean那样,去设置属性值的。所以只能想别的办法。

先从最简单的情况入手,看个示例:

@interface H {

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";
}

@H("编程新说")
class K {

}

来读取下注解的属性看看,如下:

H h = K.class.getAnnotation(H.class);

h.value(); //"编程新说"

h.name(); //""

value属性的值就是“编程新说”,因为我们设置的就是它,这毋庸置疑。

name属性的值是空字符串,因为我们没有显式设置它,所以是它的默认值。

这就是Java注解的表现行为,是正常合理的。

Spring想做的大家都知道了,就是标上@AliasFor注解后,name属性也能返回“编程新说”,即使站在Java注解的角度来看,name属性并没有被设置。

这叫什么呢?“异想天开、痴人说梦”?No、No、No,都不是,这叫运行时改变代码的行为。那这又是什么意思呢?

如果你现在突然想到了,说明还是很聪明的。运行时改变代码行为,这可是Spring的看家本领呀。

想想声明式事务管理,不就是在普通方法上加个注解,然后运行时就有了事务的开启和提交。这是堂堂正正的运行时改变代码行为。

背后原理再熟悉不过了,就是基于AOP(面向切面编程)实现的,在运行时拦截住它,然后加入特定行为。

这让我想到了王首富的名言,来个“截胡”。引申一下,还挺符合场景的。

AOP是采用代理实现的,代理的生成有两种方式,很熟悉的字眼吧,没错,就是JDK动态代理和CGLIB动态生成子类。

上面刚刚说过,注解其实是一个接口,正好采用JDK动态代理,在代理类内部,拦截住正在调用的方法,插入处理别名的逻辑即可。

这样当再调用h.name()时,就也会返回“编程新说”了。

看完之后是不是直拍大腿,哎呀,这么简单,我怎么就没想起来呢。哈哈。

不过没关系,机会有的是,比如这个:

如何找出在同一个注解内部的某个属性的所有别名?


稳扎稳打之后,便要节节攀升


上面通过代理的方式解决了注解内部别名的问题,可以认为是属性数据在同一个注解内部的流动。

剩下的就是要考虑,属性数据冲出注解流向别处了。不过不是随便流的,是只能单向流动,且只能从下往上,不能回头。

来一个稍微专业一点的说法,可能是这样的:

a)一个注解的属性可以去重写比它级别高的任何注解的属性。

b)一个注解的属性可以被比它级别低的任何注解的属性重写。

很容易理解吧,那就再容易一点,看个比喻吧。

假如一群超人在天上飞,每人一把枪,规定只能向上射击,且想打谁就打谁。

最终就是这样的:

1)一个超人可以打到比他高的任何一个人。

表示一个注解的属性数据可以流入比它级别高的任何一个注解里。

2)一个超人可以被比他低的任何一个人打到。

表示一个注解可以被比它级别低的任何一个注解的属性数据流入。

现在已经从宏观上清楚了属性数据的流动规则了,相当于中学物理老师常说的“定性分析”。

每个物理学的好的人都清楚,定性分析完之后,就该“定量计算”了。那就来吧,come on。

一旦涉及到细节,就必须改用精确、稳定、可靠的模型了。超人乱飞这种肯定是不行的,嘻嘻。

在城市里最稳定的东西就是高楼大厦了,就以楼房的不同楼层为模型,因为这和注解的情况如出一辙。

从最上层的注解到最底层的注解,之间可以有很多级,而且在查找一个注解的时候,只能从最底层开始逐级往上,且不能跳级。

楼房也有很多层,(正常情况下)我们也只能从第一层开始,然后逐层往上,无论电梯还是步梯,也都是不会跳过某层的。

下面就开始把它俩结合起来,假如注解共有10级,楼房也至少有10层。想象一下,就把所有级别的注解逐个逐个放到对应的楼层上。

假如我现在想要获取8楼的那个注解的所有属性,那我得先找到它才行啊,于是只能从1楼开始沿着步梯吭哧吭哧爬到8楼。

注解就在那里等着我,它不离不弃,我且找且珍惜,哈哈。好了,现在我已经找到它了,万里长征总算走完了第一步。

此时我就把注解的所有属性值都读了出来,但是,这是不准确的,因为1到7楼的注解都可以向它提供属性值,去“覆盖”原有的。

因为属性值“覆盖”的方向只能从下往上进行,所以越靠下的优先级越高,也就是离8楼越远的越是在最后胜出。

这就好比,长江后浪推前浪,后浪更比前浪浪,最浪的那一浪,就是最后一浪。后来者居上嘛。好理解吧。

这是一个多批次、多优先级按一定的顺序依次覆盖的过程。生活中到处是这种场景,而且非常自然。

如小弟先上场,大哥再登台,最后大BOSS闪亮登场。再如颁奖,先颁三等奖,再二等,再一等嘛。这大概就是所谓的三六九等吧。

想到了这一层,那解决方法自然就浮现出来了。我此刻在8楼,已经读完了注解的属性值,那就下到7楼吧。

找出7楼注解的属性对8楼注解的属性的覆盖关系,然后用7楼的覆盖8楼的。完事后再下到6楼。

重复同样的动作,即用6楼的覆盖8楼的。然后依次下到5楼,4楼,3楼,2楼,1楼。

最后用1楼的覆盖完之后,就全部结束了。此时的属性值就是准确无误的了。

整体来看就是,先依次进入,再原路返回,在返回时的每个节点上,执行覆盖动作。当最后退出时,处理完毕。

擦,这不就是个递归嘛,没错,就是个递归。

这里也有一个问题,可以先思考下:

在跨越不同级别注解时,如何找出一个注解有没有重写指定的注解,如果有的话如何找出属性名的映射关系?


硬核,直击映射关系的实现


先说下“传递性”,传递性非常强大,网上曾说过,一个人经过约六、七个人的传递后就能见到任何地球人。

强大的东西一般都不容易搞定,@AliasFor注解建立的映射关系就具有传递性,来看看Spring是如何实现它的。

映射关系就类似于小学生做的连线题,用一条线把相关的两个东西连起来,因此映射关系的模型抽象是非常简单的。

只需把双方都列出来,再定义一个类,把它们封装到一起即可,Spring也是这么做的:

class AliasDescriptor {

    private Class<? extends Annotation> sourceAnnotationType;

    private String sourceAttributeName;

    private Method sourceAttribute;


    private Class<? extends Annotation> aliasedAnnotationType;

    private String aliasedAttributeName;

    private Method aliasedAttribute;
}

一共六个字段,可以三个三个分成两组,前面三个是属性数据输出方(提供方),后面三个是属性数据输入方(接收方)。

每组里的三个属性分别是:注解类型(Class<?>),属性名称(String),属性方法(Method)。

注解类型和属性名称是建立映射必不可少的元素,那属性方法又是干啥用的?

前文已经说过,注解的属性就是一个方法(Method),把它也加进来有三方面作用:

1)校验,参与映射的属性返回类型和默认值必须一样,通过方法可以获得返回类型和默认值

method.getReturnType();

method.getDefaultValue();

2)比较,比较两个方法相等和比较两个注解类型和属性名称分别同时相等是一样的,但前者更简洁

method1.equals(method2);

annotation1.equals(annotation2) && name1.equals(name2);

3)构建,因为模型类就是基于方法来生成实例的,方法名称就是属性名称,方法所在的类就是注解类型

method.getName();

method.getDeclaringClass();

先看个注解内部的示例:

@interface M {

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";
}

这其实是注解内部的两组映射关系:

一个是从属性value到属性name的映射

一个是从属性name到属性value的映射

就以value到name的映射,看下映射模型里字段的数据值,如下:

Class<? extends Annotation> sourceAnnotationType = M.class;

String sourceAttributeName = "value";

Method sourceAttribute = M.class.getDeclaredMethod("value");


Class<? extends Annotation> aliasedAnnotationType = M.class;

String aliasedAttributeName = "name";

Method aliasedAttribute = M.class.getDeclaredMethod("name");

由于是注解内部的,所以注解类型都是M.class,源属性就是value,别名属性就是name。

再看个注解间的示例:

@M
@interface N {

    @AliasFor(annotation = M.class, attribute = "name")
    String id() default "";
}

这只有一组映射关系:

从注解@N的id属性到注解@M的name属性

看下映射模型里字段的数据值,如下:

Class<? extends Annotation> sourceAnnotationType = N.class;

String sourceAttributeName = "id";

Method sourceAttribute = M.class.getDeclaredMethod("id");


Class<? extends Annotation> aliasedAnnotationType = M.class;

String aliasedAttributeName = "name";

Method aliasedAttribute = M.class.getDeclaredMethod("name");

源注解是@N,源属性是id,别名注解是@M,别名属性是name。

这就是Spring抽象出来的映射关系的模型,同样非常简单直白。

下面就基于这个模型,把文章中预留的问题分析一下,看如何解决。

一)注解内部的显式别名

这种其实相当于自己到自己的映射,即X -> X。所以模型中两组数据值中的注解类型必然是相同的。

因此,只需做如下判断即可:

sourceAnnotationType == aliasedAnnotationType

如果这个返回true,就说明该属性是映射到了内部的另一个属性了。

所以sourceAttributeName就是属性名,aliasedAttributeName就是映射到的别名。

二)注解内部的隐式别名

隐式别名是由跨注解映射造成的,由于注解的级别可以是好几级,所以跨注解映射可以出现类似的级联映射。

先看几组“稍微复杂”点的映射示例吧:

X -> A -> B -> C -> D

Y -> E -> F -> G -> C

Z -> H -> I -> J -> G

这种映射是一级接着一级的,看起来有点像是级联。

能看出来X、Y、Z是分别互为别名吗?看不出来的话,我们来把这个映射关系补充完善一下。

X -> A -> B -> C -> D

Y -> E -> F -> G -> C -> D

Z -> H -> I -> J -> G -> C -> D

可以看到X、Y、Z通过若干次映射后都可以到达D,所以它们互为别名。

由于这种情况是隐含的,或者说是推导出来的,所以称为隐式别名。

如果此时把属性X的值设为“李大胖”,通过生成代理后,读取属性Y和Z的值也都是“李大胖”。

那么通过代码应该如何实现呢?其实上面已经把数据结构展示出来了。

每一组级联的映射,其实就是一个单向链表,从头节点开始一直逐级映射直到尾节点结束。

三组映射关系,就是三个单向链表了。

两个属性成为隐式别名的前提是它们最终能够指向同一个注解的同一个属性。

站在数据结构的角度看,就是两条单向链表在某一点进行了汇合。

就相当于两条河分别往前流着流着,在某一点交汇在了一起,自此成为一条河了。

就像一首诗描述的那样,“百川东到海,何时复西归”,怕是永无西归之时,因为是单向流动的。

它的下一句大家也都比较熟悉了,“少壮不努力,老大徒伤悲”。各位码农趁着年轻赶紧努力吧。

所以最终就演变为求两条单向链表有没有交点的问题了。这其实就是Spring采用的实现方案。

具体代码实现就很简单了,两层(for/while)循环就搞定了。

三)判断注解间是否存在重写

还以上一小节示例来说,我从8楼来到6楼,我必须要能判断出6楼的注解属性到底有没有重写8楼的。

现在这个问题就非常好解决了,只要检测下8楼的注解是否出现在以6楼注解属性为头节点的单向链表中即可。

简直so easy了,如果在的话,根据映射模型自然可以获取到被重写的属性名。

其实Spring的源码还是写的比较复杂的,而且真的不是随便看几眼就能看懂的,不看个几天根本搞不定。

我尽最大的努力,以最简单而又直击要害的方式讲述出来,这正是作者及本号的追求,即“深入浅出”。

通过本文还应该明白,无论是做模型设计还是代码实现,如果能在生活中找到映射或类比,将会变得比较容易。

最后,祝看到本文的人都有所收获。


(END)


品Spring系列文章列表:

品Spring:帝国的基石

品Spring:bean定义上梁山

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

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



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

 

       

posted on 2019-09-27 14:52  编程新说(李新杰)  阅读(698)  评论(0编辑  收藏  举报