朱晔和你聊Spring系列S1E9:聊聊Spring的那些注解
本文我们来梳理一下Spring的那些注解,如下图所示,大概从几方面列出了Spring的一些注解:
如果此图看不清楚也没事,请运行下面的代码输出所有的结果。
Spring目前的趋势是使用注解结合Java代码而不是配置来定义行为、属性、功能、规则和扩展点,因此梳理注解也是梳理Spring功能点的很好的方式,全面的梳理可以补足我们知识点的漏洞。
查找所有注解
首先,我们来创建一个项目,使用SPRING INITIALIZR生成一个引入Spring各种组件的项目模板,然后引入如下工具包:
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.11</version>
</dependency>
通过这个反射工具包,我们可以创建一个Spring Boot应用程序,以一行代码打印出所有Spring框架的注解:
import org.reflections.Reflections;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
@Component
public class ScanAnnotationRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
new Reflections("org.springframework")
.getSubTypesOf(Annotation.class)
.stream()
.map(clazz->clazz.getName())
.sorted()
.forEach(System.out::println);
}
}
输出结果这里就不给出了,下面我们逐一进行梳理其中的一些重要注解。
有关注解
Java的Annotation注解(类似于C#的Attribute特性),说白了就是给代码打上标签的能力。我们可以配置这个标签的保留阶段,仅源代码,源代码+字节码,源代码+字节码+运行时。通过引入注解,我们可以简单快速赋予代码生命力,大大提高代码可读性和扩展性。注解本身不具有任何能力,只是一个标签,但是我们可以定义各种标签然后实现各种标签处理器来对类、方法、属性甚至参数等进行功能扩展、功能开启、属性定义、行为定义、规则定义、关联处理、元数据定义等等。在实现各种框架的时候,我们经常会自定义标签方便框架使用者仅仅通过在合适的地方引入合适的注解来启用(或自定义)框架的一些能力并应用到我们的程序中。
不仅仅是框架的作者会大量使用注解,在之前的系列文章中我们也多次自定义注解,我们有通过定义@Metrics注解配合Spring AOP来为程序启动打点、日志、异常等功能,我们有通过定义@Sign注解配合Spring MVC的ResponseBodyAdvice进行数据签名功能,我们还经常会定义各种自定义注解配合Spring MVC的HandlerMethodArgumentResolver进行权限的校验等等功能。采用这种模式,我们的核心业务逻辑可以保持清晰干净,通过注解配合AOP赋予代码额外的能力。
你可能会说,注解还是有侵入性,我们需要耦合框架定义的那些注解,这个问题其实是无解的,100%无侵入性也代表了可读性的降低,代码的功能和能力应当聚合在一起,这也就是为什么Spring现在也不建议采用XML来做配置。Java核心类库并没有什么注解,好在Spring已经有了大量注解,而Spring也变为了Java开发的标准,所以其实我们很多时候如果希望自己的框架(RPC啥的)完全没有侵入性的话可以借用Spring的那些注解@Autowired、@Controller、@Service等注解,配合各种包的规范其实我们可以对目标元素的功能识别个八九不离十,完全有可能实现0侵入的功能增强。
有关如何实现自定义注解不赘述,这里我们简单回顾一下几个元注解(注解的注解):
- @Documented:将会在被此注解注解的元素的javadoc文档中列出注解,一般都打上这个注解没坏处
- @Target:注解能被应用的目标元素,比如类、方法、属性、参数等等,需要仔细思考
- @Retention:仅在源码保留,还是保留到编译后的字节码,还是到运行时也去加载,超过90%的应用会在运行时去解析注解进行额外的处理,所以大部分情况我们都会设置配置为RetentionPolicy.RUNTIME
- @Inherited:如果子类没有定义注解的话,能自动从父类获取定义了继承属性的注解,比如Spring的@Service是没有继承特性的,但是@Transactional是有继承特性的,在OO继承体系中使用Spring注解的时候请特别注意这点,理所当然认为注解是能被子类继承的话可能会引起不必要的Bug,需要仔细斟酌是否开启继承
- @Repeatable:Java 8 引入的特性,通过关联注解容器定义可重复注解,小小语法糖提高了代码可读性,对于元素有多个重复注解其实是很常见的事情,比如某方法可以是A角色可以访问也可以是B角色可以访问,某方法需要定时任务执行,要在A条件执行也需要在B条件执行
- @Native:是否在.h头文件中生成被标记的字段,除非原生程序需要和Java程序交互,否则很少会用到这个元注解
现在我们来从几个方面逐一温习一下Spring的那些常用的值得关注的注解。
Spring核心注解
- 首先来看一下各种stereotype:按分类定义了由Spring管理的各种组件,@Controller定义表现层组件,@Service定义业务逻辑层组件,@Repository定义数据访问层资源库组件,@Component定义其它组件(比如访问外部服务的组件),之前也说过了随着这些注解功能无区别,但是对组件进行合适的分类意义重大,不仅仅增加可读性而且方便我们通过AOP对不同类型的组件进行更多自动增强
- 再来看看IOC相关的一些注解:@Autowired自动装配不用多说了;@Required用于在setter方法标记属性值需要由Spring进行装配,对于目前版本的Spring这个注解已经废弃,现在Spring更推荐使用构造方法注入;@Qualifier用于通过给Bean定义修饰语来注入相应的Bean,和@Autowired一起使用相当于@Resource的效果,当然还有一种常见用法是嵌入其它注解用于对Bean进行区分,然后配合@Autowired一起使用,参见后面提到的Spring Cloud的@LoadBalanced注解;@Value用于注入属性配置或SpEL表达式(前者是我们常见用法,后者可以从其它对象获取值,功能更强大一点);@Lookup可以实现方法注入,如果我们的类是单例的,但是又希望Spring注入的依赖的对象是Prototype生命周期(每次new一个出来)的,这个时候可以通过此注解进行方法注入
- 然后来看一下有关事务的几个注解:@EnableTransactionManagement用于开启事务管理,使用Spring Boot如果引入Spring Data的话不需要手动开启(不过建议大家在使用事务的时候还是通过日志来验证事务管理是否生效);@Transactional大家都知道用于开启事务以及设置传播性、隔离性、回滚条件等;@TransactionalEventListener用于配置事务的回调方法,可以在事务提交前、提交后、完成后以及回滚后几个阶段接受回调事件
- @Order注解可以设置Spring管理对象的加载顺序,在之前介绍AOP的文章中我们看到有的时候我们必须通过设置合理的@Order来合理安排切面的切入顺序避免一些问题,还有在一些业务场景中,我们往往会去定义一组类似于Filter的@Component,然后会从容器获得一组Bean,这个时候业务组件的运行顺序往往会比较重要,也可以通过这个方式进行排序
- @AliasFor注解可以设置一组注解属性相互作为别名,对于有歧义的时候会使代码更清晰,此外还有一个用途是创建复合注解,Spring MVC的@GetMapping注解就是基于@RequestMapping这个注解创建的复合注解,我们可以很方便得通过这种方式来实现注解的继承
Spring上下文注解
- 首先来看一下配置相关的一些注解:@Configuration用于标注配置类,启用Java配置方式的Bean配置;@Bean用于配置一个Bean;@ComponentScan(@ComponentScans用于配置一组@ComponentScan,Java 8可以直接使用重复注解特性配置多个@ComponentScan)用于扫描包方式配置Bean;@PropertySource以及 @PropertySources用于导入配置文件;@Conditional用于设置关联的条件类,在合适的时候启用Bean的配置(Spring Boot自动配置根基);@Import用于导入其它配置类; @ImportResource用于导入非Java配置方式的XML配置;@Profile用于指定在合适的Profile下启用配置;@Lazy用于告知容器延迟到使用的时候实例化Bean(默认情况下容器启动的时候实例化Bean来检查所有的问题);@Description用于给Bean设置描述;@Scope用于设置Bean的生命周期;@Primary用于在定义了多个Bean的时候指定首选的Bean
- 其它一些注解包括:@EventListener用于设置回调方法监听Spring制定的以及自定义的各种事件;@EnableAspectJAutoProxy用于开启支持AspectJ的 @Aspect切面配置支持,使用Spring Boot引入了AOP启动器的话不需要显式开启
Spring Web注解
Spring MVC的各种注解对应了Spring MVC各方面的功能,下面我们来了解一下:
- 首先是三个定义了Bean特殊生命周期的复合注解:@RequestScope、@SessionScope和 @ApplicationScope。在Web应用中,我们可能需要Bean跟随请求、会话和应用程序的声明周期来进行创建,这个时候可以直接使用这三个快捷的复合注解
- 接下去可以看到各种 @XXXMapping的注解,分别用于配置HandlerMethod匹配到不同的Http Method,当然不使用这些快捷的注解也是可以的,直接使用@RequestMapping然后手动设置method
- @ResponseStatus可以用到方法上也可以用到异常上,前者会直接使请求得到指定的响应代码或原因(可以配合@ExceptionHandler使用),后者可以实现遇到指定异常的时候给出指定的响应代码或原因,@ResponseBody我们实现Restful接口的时候(@RestController)最常用了,把返回内容(序列化后)输出到请求体
- Spring MVC给了我们各种注解方便我们从HTTP请求各种地方获取参数,@RequestBody从请求体(处理复杂数据,比如JSON),@RequestHeader从请求头,@CookieValue从cookie中,@SessionAttribute从会话中,@RequestAttribute从请求的Attribute中(比如过滤器和拦截器手动设置的一些临时数据),@RequestParam从请求参数(处理简单数据,键值对),@PathVariable从路径片段,@MatrixAttribute矩阵变量允许我们采用特殊的规则在URL路径后加参数(分号区分不同参数,逗号为参数增加多个值)
- @ControllerAdvice是一个重要注解,允许我们在集中的地方配置控制器(有@RequestMapping的方法)相关的增强(@RestControllerAdvice也是差不多的,只是相当于为@ExceptionHandler加上了@ResponseBody)。那么可以应用哪些增强呢?首先是可以用 @ExceptionHandler进行统一的全局异常处理;第二是 @InitBinder用来设置WebDataBinder,WebDataBinder用来自动绑定前台请求参数到Model中;第三是 @ModelAttribute让全局的@RequestMapping都能获得在此处设置的键值对。当然,这里说的@InitBinder和@ExceptionHandler也可以不定义在@ControllerAdvice内部(作为全局开启),定义在Controller内部应用到某个Controller也是可以的
- 其它还有一些注解比如:@CrossOrigin可以用到Controller或Method上(需要配合@RequestMapping)设置细粒度的跨域行为
在之前的文章中我们也提到,对于Spring MVC,定义自己的注解应用到参数、方法、控制器上,配合HandlerMethodArgumentResolver、XXAdvise、以及Interceptor实现具体的功能来使用太太常见了,几乎所有的非业务横切关注点,我们都不应该在方法实现中重复任何一行代码。
Spring Boot注解
- 来看一下上下文相关的注解:@ConfigurationProperties很常用(配合 @EnableConfigurationProperties注解来设置需要启用的配置类),用来自定义配置类和配置文件进行关联;@DeprecatedConfigurationProperty用于标记废弃的配置以及设置替代配置和告知废弃原因;@ConfigurationPropertiesBinding用于指定自定义的转换器用于配置解析的时的类型转换; @NestedConfigurationProperty用于关联外部的类型作为嵌套配置类
- 再看看自动配置相关的注解,自动配置是Spring Boot最重要的特性,在之前的系列文章中我有提到一个观点,IOC是好事情,但是把组件内部的一些默认配置以及组件和组件的组装交给外部用户来配置其实是不合理的,组件应当可以自动进行自我配置实现开箱急用,只有需要自定义组件的时候才要求外部来进行个性化配置:@EnableAutoConfiguration注解可以启用自动配置,Spring Boot应用程序一般我们会直接使用复合注解@SpringBootApplication;@AutoConfigureOrder(值越小优先级越高)、@AutoConfigureAfter、@AutoConfigureBefore用于设置自动配置类加载顺序,以及精确控制加载依赖关系,有的时候我们的自动配置需要相互依赖或者会相互干扰,需要手动调节
- 最后来看一下十几种配置条件,用好这些注解是实现完善的自动配置的关键:@ConditionalOnBean用于仅当容器中已经包含指定的Bean类型或名称时才匹配条件;@ConditionalOnClass仅当classpath上存在指定类时条件匹配;@ConditionalOnCloudPlatform仅当指定的云平台处于活动状态时条件匹配;@ConditionalOnExpression依赖于SpEL表达式的值的条件元素的配置注解;@ConditionalOnJava基于应用运行的JVM版本的条件匹配;@ConditionalOnJndi基于JNDI可用和可以查找指定位置的条件匹配;@ConditionalOnMissingBean仅当容器中不包含指定的Bean类型或名称时条件匹配;@ConditionalOnMissingClass仅当classpath上不存在指定类时条件匹配;@ConditionalOnNotWebApplication 仅当不是WebApplicationContext(非Web项目)时条件匹配,对应 @ConditionalOnWebApplication;@ConditionalOnProperty是检查指定的属性是否具有指定的值;@ConditionalOnResource表示仅当 classpath 上存在指定资源时条件匹配;@ConditionalOnSingleCandidate仅当容器中包含指定的Bean类并且可以判断只有单个候选者时条件匹配。其实所有这些实现原理都是扩展SpringBootCondition抽象类(实现之前提到的Condition接口),我们完全可以实现自己的条件注解(配合 @Conditional注解关联到自己实现的SpringBootCondition)
Spring Cloud注解
在介绍本系列文章的第一篇中我们就提到了,Spring Cloud整齐划一通过各种EnableXXX注解开启某个功能,这里就不对这些注解进行说明了,使用Spring Boot组件的功能非常简单,基本就是引POM+EnableXXX+设置配置文件三部曲。
- 首先是 Netflix包下的一些注解,各种EnableXXX就不说了,参考前一篇文章,之前没介绍过 @RibbonClient,这个注解用来为负载均衡客户端做一些自定义的配置,可以进一步配置或自定义从哪里获取服务端列表、负载均衡策略、Ping也就是服务鉴活策略等等
- client包下的 @SpringCloudApplication之前文章中我们也没有使用到,这是一个复合注解就是 @SpringBootApplication+ @EnableDiscoveryClient+ @EnableCircuitBreaker,Spring Cloud那堆东西很多,还是自己亲手定义一个一个功能的注解来的踏实; @LoadBalanced注解用于和RestTemplate配合使用构成一个负载均衡的Http客户端,实现原理上其实这个注解是一个@Qualifier注解,Spring会为所有@LoadBalanced的RestTemplate加入一个LoadBalancerInterceptor(实现ClientHttpRequestInterceptor)实现负载均衡
- sleuth包下面的注解和链路跟踪相关,比较常用的是通过 @SpanName手动设置span的名称,其它注解对于业务开发并不常用
总结
好了,写了本文我发现我看到@已经Markdown的**就眼花,请点赞支持。本文我们通过代码打印出了大部分Spring相关的注解,你也可以通过这个方式熟悉其它框架的注解(毕竟注解是框架赋予我们各种便捷功能的一个重要入口,对注解了解个八九成也往往可以对框架赋予我们的丰富功能了解六七成)。然后我们梳理了一下Spring相关的各种注解,其中主要需要关注的是几方面:
- 元注解,也就是注解的注解
- Spring容器相关的一些注解,包括@Qualifier、@AliasFor、@Order等看似不重要但其实很重要的注解
- Spring Java配置相关的一些注解,包括条件注解
- Spring Boot自动配置相关的一些注解
- 很多注解可以同时应用到类型、方法、参数上,有的时候应用到不同的地方作用会略微不一样,这个需要重点关注
我们知道注解其实只是一个标识,注解如何起作用背后的实现原理还是比较多样的,你可以进一步结合本文介绍的Spring的各种注解探寻一下背后实现的原理。