理解 Spring 注解编程模型

Spring 中有一个概念叫「元注解」(Meta-Annotation),通过元注解,实现注解的「派生性」,官方的说法是「Annotation Hierarchy」。

什么是元注解

所谓元注解,即标注在注解上的注解。这种方式所形成的注解层级结构中,元注解在层级结构的上面,我叫它父注解(Super Annotation), 被注解的注解在层级结构的下面,叫它子注解(Sub Annotation)。引入元注解的目的是为了实现属性重写(Attribute Override) 的目的。

举个简单的例子:
有 一个类 Home 和 2 个注解,1 个叫 @Parent,另一个叫 @Child ,@Parent 标注在 @Child 上,@Child 标注在 Home 上,它们都只有一个属性,叫 name, 如果 @Parent.name 的默认值是 'John',而 @Child.name 的默认值是 'Jack'。
这时,从 Home 上获取 @Child.name,应该返回 'Jack',这毫无悬念。
那么,如果获取 @Parent.name,应该返回什么呢?根据 Spring 注解的「派生性」,@Child.name override @Parent.name,所以返回结果也是 'Jack'。

上述例子中的类和注解,代码大致如下

@interface Parent {
String name() default "John";
} @Parent
@interface Child {
String name() default "Jack";
} @Child
class Home { }

注解层级结构:

@Parent
@Child

相对于「属性重写」,还有另一个概念是「属性别名」(Alias),属性别名之间是互相等价的。
我们给上面的 @Child 加一个属性 value,并且使用 @AliasFor ,使 @Child.name 和 @Child.value 互相成为别名,并且默认值为空字符串:

@interface Child {
@AliasFor("name")
String value() default ""; @AliasFor("value")
String name() default "";
}

标注在 Home 上时,给 @Child.value 设置值为 "Jack":

@Child("Jack")
class Home { }

这时,无论是获取 @Child.name 还是获取 @Child.value,其结果总是相同的,都是 "Jack"。说明了属性别名之间的等价性。

属性别名 和 属性重写

属性别名 和 属性重写 其实是两个完全不同的概念,但是如果不加区分,模糊概念的话,就会对一些现象不符合预期而感到意外。 考虑以下案例,分别给出 @A.a1、@A.a2、@B.a1、@B.b、@C.c、@C.b 的值:

@interface A {
String a1() default "1";
String a2() default "1";
} @A
@interface B {
String a1() default "2"; @AliasFor(value = "a2", annotation = A.class)
String b() default "2";
} @B
@interface C {
@AliasFor(value = "a1", annotation = B.class)
String c() default "3"; String b() default "3";
}

在我没有弄清概念之前,我觉得答案应该是:@A.a1、@A.a2、@B.a1、@B.b、@C.c、@C.b 全都是 "3"。
理由如下:

  • @C.c 是 @B.a1 的别名,@B.a1 重写 @A.a1 ,所以这 3 者是一条链上的,它们的值应该相等, 是 "3"。
  • @C.b 重写 @B.b,@B.b 是 @A.a2 的别名,所以这 3 者 也是一条链上的,它们的值也应该相等,是 "3"。

而结果却是,我错了,@B.a1、@B.b、@C.c、@C.b 的值是 "3", 但 @A.a1、@A.a2 的值是 "2"。
至于为什么,我们先来认真理解一下 属性别名 和 属性重写 这 2 个概念吧。

援引官方 Wiki https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model, 其中有关于这两个概念的澄清。在 「Attribute Aliases and Overrides」 一节中,官方原文如下:

An attribute alias is an alias from one annotation attribute to another annotation attribute. Attributes within a set of aliases can be used interchangeably and are treated as equivalent. Attribute aliases can be categorized as follows.

  1. Explicit Aliases: if two attributes in one annotation are declared as aliases for each other via @AliasFor, they are explicit aliases.
  2. Implicit Aliases: if two or more attributes in one annotation are declared as explicit overrides for the same attribute in a meta-annotation via @AliasFor, they are implicit aliases.
  3. Transitive Implicit Aliases: given two or more attributes in one annotation that are declared as explicit overrides for attributes in meta-annotations via @AliasFor, if the attributes effectively override the same attribute in a meta-annotation following the law of transitivity, they are transitive implicit aliases.

An attribute override is an annotation attribute that overrides (or shadows) an annotation attribute in a meta-annotation. Attribute overrides can be categorized as follows.

  1. Implicit Overrides: given attribute A in annotation @One and attribute A in annotation @Two, if @One is meta-annotated with @Two, then attribute A in annotation @One is an implicit override for attribute A in annotation @Two based solely on a naming convention (i.e., both attributes are named A).
  2. Explicit Overrides: if attribute A is declared as an alias for attribute B in a meta-annotation via @AliasFor, then A is an explicit override for B.
  3. Transitive Explicit Overrides: if attribute A in annotation @One is an explicit override for attribute B in annotation @Two and B is an explicit override for attribute C in annotation @Three, then A is a transitive explicit override for C following the law of transitivity.

属性别名,有 3 种, 分别是 显式别名,隐式别名 和 传递隐式别名, 「属性别名」 只能发生在同一个注解内部。比如:

显式别名(互相@AliasFor),@A.a1 和 @A.a2,

@interface A {
@AliasFor("a2")
String a1() default ""; @AliasFor("a1")
String a2() default "";
}

隐式别名(@AliasFor到同一个属性),@B.b1 和 @B.b2

@interface A {
String a() default "";
} @A
@interface B {
@AliasFor(value = "a", annotation = A.class)
String b1() default ""; @AliasFor(value = "a", annotation = A.class)
String b2() default "";
}

传递隐式别名(最终@AliasFor到同一个属性) @C.c1 和 @C.c2

@interface A {
String a() default "";
} @A
@interface B {
@AliasFor(value = "a", annotation = A.class)
String b() default "";
} @B
@interface C {
@AliasFor(value = "a", annotation = A.class)
String c1() default ""; @AliasFor(value = "b", annotation = B.class)
String c2() default "";
}

属性重写,也有 3 种,分别是 隐式重写,显式重写 和 传递显式重写,「属性重写」只能发生在注解之间。比如:

隐式重写(同名属性), @B.a 重写 @A.a

@interface A {
String a() default "";
} @A
@interface B {
String a() default "";
}

显式重写(需要@AliasFor),@B.b 重写 @A.a

@interface A {
String a() default "";
} @A
@interface B {
@AliasFor(value = "a", annotation = A.class)
String b() default "";
}

传递显式重写(需要 @AliasFor),由于 @C.c 重写 @B.b, @B.b 重写 @A.a, 所以 @C.c 也 重写 @A.a

@interface A {
String a() default "";
} @A
@interface B {
@AliasFor(value = "a", annotation = A.class)
String b() default "";
} @B
@interface C {
@AliasFor(value = "b", annotation = B.class)
String c() default "";
}

理解清楚之后,我们回到刚才的题目,样例重贴如下:

@interface A {
String a1() default "1"; String a2() default "1";
} @A
@interface B {
String a1() default "2"; @AliasFor(value = "a2", annotation = A.class)
String b() default "2";
} @B
@interface C {
@AliasFor(value = "a1", annotation = B.class)
String c() default "3"; String b() default "3";
}

解答步骤是:

  • 对于注解 @C,@C.c = "3", @C.b = "3"
  • 对于注解 @B, @B.a1 被 @C.c 显式重写, 所以 @B.a1 = @C.c = "3"; @B.b 被 @C.b 隐式重写,所以 @B.b = @C.b = "3"
  • 对于注解 @A, @A.a1 被 @B.a1 隐式重写,所以 @A.a1 = @B.a1 = "2"; @A.a2 被 @B.b 显式重写,所以 @A.a2 = @B.b = "2"

可以看到 @A 和 @C 之间没有任何关系。这里也根本没有「属性别名」的存在,不是用了 @AliasFor 就是 「属性别名」的。
对于「显式传递重写」,像上面 "@A.a1 被 @B.a1 隐式重写, @B.a1 被 @C.c 显式重写",或者 "@A.a2 被 @B.b 显式重写, B.b 被 @C.b 隐式重写", 重写关系是不会传递的。

总结

属性别名,有 3 种, 分别是 显式别名,隐式别名 和 传递隐式别名, 「属性别名」 只能发生在同一个注解内部。 属性重写,也有 3 种,分别是 隐式重写,显式重写 和 传递显式重写,「属性重写」只能发生在注解之间。

后记

Spring 对于注解编程模型的代码实现,主要在 AnnotatedElementUtils 这个类中,做试验可以使用这个方法:AnnotatedElementUtils#getMergedAnnotationAttributes。

需要注意的是,「隐式重写」不适用于 value 属性,貌似 value 属性是一个相对特殊的属性。

以下示例, @B.value 不会 隐式重写 @A.value

@interface A {
String value() default "a";
} @A
@interface B {
String value() default "b";
}

但只要属性名不是 value,都可以 隐式重写 , @B.xxx 隐式重写 @A.xxx

@interface A {
String xxx() default "a";
} @A
@interface B {
String xxx() default "b";
}

我跟了以下源码,发现源码中确实对 value 属性做了特殊判断,代码位置在 org.springframework.core.annotation.AnnotatedElementUtils.MergedAnnotationAttributesProcessor#postProcess 方法中,代码片段如下;

// Implicit annotation attribute override based on convention
else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
overrideAttribute(element, annotation, attributes, attributeName, attributeName);
}

其中,AnnotationUtils.VALUE 是一个常量,其值为 "value"。暂时没有找到官方说明为什么要对 value 属性做特殊处理。猜测是很多注解只有一个属性, 为了编程方便,因为不需要 @A(value = "hello world) 这样使用, 只需要 @A("hello world") 即可。这种情况下,如果隐式重写,可能不是编码者想要的结果。

值得一提的是,显式重写 没有这种特殊处理,以下示例 @B.value 会显式重写 @A.value:

@interface A {

    String value() default "a";
} @A
@interface B { @AliasFor(annotation = A.class)
String value() default "b";
}

本文讨论所涉及的 Spring Boot 版本 >= 2.0.2.RELEASE。

理解 Spring 注解编程模型的更多相关文章

  1. Spring 注解(一)Spring 注解编程模型

    Spring 注解(一)Spring 注解编程模型 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring 注解系列 ...

  2. Spring 注解编程之注解属性别名与覆盖

    前两篇文章咱聊了深入了解了 Spring 注解编程一些原理,这篇文章我们关注注解属性方法,聊聊 Spring 为注解的带来的功能,属性别名与覆盖. 注解属性方法 在进入了解 Spring 注解属性功能 ...

  3. 跟Evan学Sprign编程思想 | Spring注解编程模式【译】

    Spring注解编程模式 概况 多年来,Spring Framework不断发展对注解.元注解和组合注解的支持. 本文档旨在帮助开发人员(Spring的最终用户以及Spring Framework和S ...

  4. 深入理解Spring注解机制(一):注解的搜索与处理机制

    前言 众所周知,spring 从 2.5 版本以后开始支持使用注解代替繁琐的 xml 配置,到了 springboot 更是全面拥抱了注解式配置.平时在使用的时候,点开一些常见的等注解,会发现往往在一 ...

  5. Spring 注解编程之模式注解

    Spring 框架中有很多可用的注解,其中有一类注解称模式注解(Stereotype Annotations),包括 @Component, @Service,@Controller,@Reposit ...

  6. Spring Security编程模型

    1.采用spring进行权限控制 url权限控制 method权限控制 实现:aop或者拦截器(本质就是之前之后进行控制)--------------------proxy就是 2.权限模型: 本质理 ...

  7. Spring Cloud Alibaba学习笔记(10) - Spring消息编程模型下,使用RocketMQ收发消息

    编写生产者 集成 添加依赖 <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId&g ...

  8. 深入理解spring注解之@ComponentScan注解

    今天主要从以下几个方面来介绍一下@ComponentScan注解: @ComponentScan注解是什么 @ComponentScan注解的详细使用 1,@ComponentScan注解是什么 其实 ...

  9. Spring 注解(二)注解工具类 AnnotationUtils 和 AnnotatedElementUtils

    Spring 注解(二)注解工具类 AnnotationUtils 和 AnnotatedElementUtils Spring 系列目录(https://www.cnblogs.com/binary ...

随机推荐

  1. spring-boot-plus运维部署(八)

    spring-boot-plus运维部署 线上部署 打包环境为prod mvn clean package -Pprod 打包后的目录 cd target/spring-boot-plus-1.2.0 ...

  2. net core WebApi——文件分片上传与跨域请求处理

    目录 前言 开始 测试 跨域 小结 @ 前言 在之前整理完一套简单的后台基础工程后,因为业务需要鼓捣了文件上传跟下载,整理完后就迫不及待的想分享出来,希望有用到文件相关操作的朋友可以得到些帮助. 开始 ...

  3. C#高级语法之泛型、泛型约束,类型安全、逆变和协变(思想原理)

    一.为什么使用泛型? 泛型其实就是一个不确定的类型,可以用在类和方法上,泛型在声明期间没有明确的定义类型,编译完成之后会生成一个占位符,只有在调用者调用时,传入指定的类型,才会用确切的类型将占位符替换 ...

  4. Pycharm2019.2.1永久激活

    Pycharm2019.2.1永久激活 Pycharm官网自7月24更新到pycharm2019.2版本后,在短短的一个月内与8月23又带来新版本2019.2.1,不可说更新不快,对于"喜新 ...

  5. springboot中的springSession的存储和获取

    利用redis进行springSession的存储: 存储: // 在session中保存用户信息 HttpSession session = httpRequest.getSession(true) ...

  6. SqlException (0x80131904): 超时时间已到。在操作完成之前超时时间已过或服务器未响应。

    在ms sql2005限制200M内存,framwork2.0环境下,当update更新单表数据量10k时经常出现Command超时的问题,网上查了都是说增加连接时间,尝试了还是解决不了问题,最终一个 ...

  7. python 17 内置模块

    目录 1. 序列化模块 1.1 json 模块 1.2 pickle 模块 2. os 模块 3. sys 模块 4. hashlib 加密.摘要 4.1 加密 4.2 加盐 4.3 文件一致性校验 ...

  8. 解决ionic 启动页面图片没有显示及启动页出现黑白屏

    1.ionic 正确打包完app, 并且按照正常的步骤配置config.xml文件之后 ,启动页面还是不能正常的显示出来,而是黑了一下之后,就进入首页了 原因很有可能就是你没有装cordova-plu ...

  9. unity编辑器扩展_04(使用Selection获取选择的游戏物体)

    代码: [MenuItem("Tools/GetChance", false, 1)]    static void GetChance()    {        if (Sel ...

  10. css_transition_animation(内含贝赛尔曲线详解)

    区别: transition也叫过渡动画,主要是用于让一个元素从一种状态过渡到另一种状态效果,常用于主动触发的效果.例如移动端的页面切换(很常用).button点击效果(也很常见). animatio ...