品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 {
}
温馨提示,请务必明白注解级别的高低,什么是高级别注解,什么是低级别注解,本文会一直这样称呼。
我们最终设置的是注解@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 "";
}
这里有个问题,就是属性名必须是“value”才行,但value这个名字很中性,不能很好的表达意图。
所以很多时候,都会定义更有意义的属性名,比如用“name”表示名称要比value好得多,如下:
@interface E {
String value() default "";
String name() default "";
}
@interface E {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
}
不知你是否注意到@AliasFor注解的源码本身就已经使用了这种方法。
二)元注解中属性显式别名
看个示例:
@E
@interface F {
@AliasFor(annotation = E.class, attribute = "name")
String id() default "";
}
三)注解内部的隐式别名
看个示例:
@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 "";
}
注解@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(); //""
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();
method1.equals(method2);
annotation1.equals(annotation2) && name1.equals(name2);
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
@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");
这就是Spring抽象出来的映射关系的模型,同样非常简单直白。
下面就基于这个模型,把文章中预留的问题分析一下,看如何解决。
一)注解内部的显式别名
这种其实相当于自己到自己的映射,即X -> X。所以模型中两组数据值中的注解类型必然是相同的。
因此,只需做如下判断即可:
sourceAnnotationType == aliasedAnnotationType
所以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的值也都是“李大胖”。
那么通过代码应该如何实现呢?其实上面已经把数据结构展示出来了。
每一组级联的映射,其实就是一个单向链表,从头节点开始一直逐级映射直到尾节点结束。
三组映射关系,就是三个单向链表了。
两个属性成为隐式别名的前提是它们最终能够指向同一个注解的同一个属性。
站在数据结构的角度看,就是两条单向链表在某一点进行了汇合。
就相当于两条河分别往前流着流着,在某一点交汇在了一起,自此成为一条河了。
就像一首诗描述的那样,“百川东到海,何时复西归”,怕是永无西归之时,因为是单向流动的。
它的下一句大家也都比较熟悉了,“少壮不努力,老大徒伤悲”。各位码农趁着年轻赶紧努力吧。
所以最终就演变为求两条单向链表有没有交点的问题了。这其实就是Spring采用的实现方案。
具体代码实现就很简单了,两层(for/while)循环就搞定了。
三)判断注解间是否存在重写
还以上一小节示例来说,我从8楼来到6楼,我必须要能判断出6楼的注解属性到底有没有重写8楼的。
现在这个问题就非常好解决了,只要检测下8楼的注解是否出现在以6楼注解属性为头节点的单向链表中即可。
简直so easy了,如果在的话,根据映射模型自然可以获取到被重写的属性名。
其实Spring的源码还是写的比较复杂的,而且真的不是随便看几眼就能看懂的,不看个几天根本搞不定。
我尽最大的努力,以最简单而又直击要害的方式讲述出来,这正是作者及本号的追求,即“深入浅出”。
通过本文还应该明白,无论是做模型设计还是代码实现,如果能在生活中找到映射或类比,将会变得比较容易。
最后,祝看到本文的人都有所收获。
(END)
品Spring系列文章列表:
作者是工作超过10年的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。下面是公众号和知识星球的二维码,欢迎关注!
品Spring:能工巧匠们对注解的“加持”的更多相关文章
- 品Spring:对@Resource注解的处理方法
@Resource是Java的注解,表示一个资源,它具有双向的含义,一个是从外部获取一个资源,一个是向外部提供一个资源. 这其实就对应于Spring的注入和注册.当它用在字段和方法上时,表示前者.当它 ...
- 品Spring:注解之王@Configuration和它的一众“小弟们”
其实对Spring的了解达到一定程度后,你就会发现,无论是使用Spring框架开发的应用,还是Spring框架本身的开发都是围绕着注解构建起来的. 空口无凭,那就说个最普通的例子吧. 在Spring中 ...
- 品Spring:对@PostConstruct和@PreDestroy注解的处理方法
在bean的实例化过程中,也会用到一系列的相关注解. 如@PostConstruct和@PreDestroy用来标记初始化和销毁方法. 平常更多的是侧重于应用,很少会有人去了解它背后发生的事情. 今天 ...
- 品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定义的注册过程. 具体细节肯定会颇为复杂,同 ...
- 品Spring:bean工厂后处理器的调用规则
上一篇文章介绍了对@Configuration类的处理逻辑,这些逻辑都写在ConfigurationClassPostProcessor类中. 这个类不仅是一个“bean工厂后处理器”,还是一个“be ...
随机推荐
- Win10下安装python3.x+pycharm+autopep8
一.安装Python3.X 1.Pythong官方网站:http://python.org/getit/ 下载windows的安装包.有以下几个选项: 这里选择windows x86-64 exc ...
- Jedis操作Redis--List类型
/** * List(列表) * BLPOP,BRPOP,BRPOPLPUSH,LINDEX,LINSERT,LLEN,LPOP,LPUSH,LPUSHX,LRANGE,LREM,LSET,LTRIM ...
- 洛谷P2577 [ZJOI2005]午餐 打饭时间作为容量DP
P2577 [ZJOI2005]午餐 )逼着自己做DP 题意: 有n个人打饭,每个人都有打饭时间和吃饭时间.有两个打饭窗口,问如何安排可以使得总用时最少. 思路: 1)可以发现吃饭时间最长的要先打饭. ...
- Catch That Cow POJ - 3278 [kuangbin带你飞]专题一 简单搜索
Farmer John has been informed of the location of a fugitive cow and wants to catch her immediately. ...
- 从一道看似简单的面试题重新理解JS执行机制与定时器
壹 ❀ 引 最近在看前端进阶的系列专栏,碰巧看到了几篇关于JS事件执行机制的面试文章,因为我在之前一篇 JS执行机制详解,定时器时间间隔的真正含义 博文中也有记录JS执行机制,所以正好用于作为测试自 ...
- win10 无法安装/启用 .net framework 3.5
有些程序依赖.net framework 3.5 win10可以在控制面板->程序和功能->启用或关闭windows功能 启用 但有时会报错 比如 0x800f0950 官方论坛的解决办法 ...
- Spring的事件监听机制
最近公司在重构广告系统,其中核心的打包功能由广告系统调用,即对apk打包的调用和打包完成之后的回调,需要提供相应的接口给广告系统.因此,为了将apk打包的核心流程和对接广告系统的业务解耦,利用了spr ...
- 【Offer】[24] 【反转链表】
题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点. 思路分析 利用三个指针,pre,p,pNext,将p的next ...
- Day002_LInux基础_常用命令
#空格和tab键↓↓mkdir 创建目录 ↓ls list 显示目录里面的内容详情↓cd change directory 切换目录,进入到目录↓pwd 显示当前所在路径 ,定位↓###绝对路径和相对 ...
- Vue 利用指令实现禁止反复发送请求
前端做后台管控系统,在某些接口请求时间过长的场景下,需要防止用户反复发起请求. 假设某场景下用户点击查询按钮后,后端响应需要长时间才能返回数据.那么要规避用户返回点击查询按钮无外乎是让用户无法在合理时 ...