写在开头
好久没有去写博客了,最近一年发生了太多事情,感觉自己需要好好沉淀和消化这些,也更加需要去思考未来的路该如何去走,趁着这段时间比较空闲,写几篇关于spring的知识点,也顺便去熟悉下自己的知识栈。同时加强一下自己的写作“基本功”。
这篇文章主要讲什么?
这篇文章主要会围绕以下俩个点来进行探究:
- spring自动装配的俩种方式的规范以及俩种方式的区别和相似点
- spring自动装配的一些特性和注意点以及springboot自身对不同组件选用不同的装配模式的原因
可能上面的说法太过抽象,没关系,下面我会结合生动形象的例子来为大家阐述我今天所讲的内容。
spring自动装配俩种方式
在去了解spring自动装配之前,有个小的知识点需要大家掌握,springboot项目中,会优先扫描项目工程的包路径去注入到spring容器中,其次才会扫描注入大家一直所听说的“约定大于配置”中的spring自身的一些组件。
举个例子:
如果你的工程路径所在的包名为:com.xxxx.web,springboot启动类也在这个类下,那么springboot项目启动后会优先扫描当前项目classpath路径下所有com.xxxx.web包(包括当前工程以及依赖过来的所有第三方jar)的所有spring的bean(类上含有spring容器注解的class),加入到容器,然后会再去解析启动类上的spring自动装配的注解。这个理解是很重要的一点,很多注入失败或者spring特性应用失败,都跟这个有很大关系。如果具体想要了解详细的请看我之前的一篇文章springbean的加载顺序,好的,话不多说,我们赶紧进入正题。
1.starter模式
自动装配的starter模式是最经典的方式之一,为何说经典,因为他真正实现了可插拔模式,想用使用某个功能组件,就引入相应的pom坐标,之后所有的操作都有spring来帮你完成,包括bean的注入,资源的建立等等。
当然,我说了这些优点肯定不够权威,毕竟我不是spring作者,那么我们看下spring官网的描述
1.1 spring官方描述
虽然我的英语真的很烂,我就中式解读下他这个说的啥意思:
- starters是一组依赖的聚合体,你可以使用它在你的项目中
- 你可以使用它里面涉及到spring的所提供的一站式功能(one-stop),不需要再去网上或者其他地方进行ctrl+c、ctrl+v了(鄙视我们有木有)。然后他举了个例子,如果你想使用jpa,那么就引用data-jpa组件就好了
- 关于命名的解释,官方使用spring-boot-starter开头,第三方或个人建议使用xxx-spring-boot-starter开头,这个没什么好说的
怎么样,感觉翻译的还挺言简意赅吧
那么springboot官方有哪些组件呢,我们从官网上也可以看一下:
可以看到,spring提供了相当之数量的starter组件,涵盖各个领域和中间件,这里就不做过多分析,贴下官网的地址,大家可以自行去查看。SpringBoot Starters官网地址
1.2 简单案例
看了上面官网的demo,下面我来进行操作一个案例,展示如何构建一个自己的starter
-
首先创建一个maven项目工程,用来被第三方进行依赖
可以看到我建了一个父工程,名叫xxq-spring-starters,然后新建了一个子模块,叫做spring-boot-starter-test1,这个就是我创建的starter,当然名字没有按照spring建议的去命名,pom文件只需要引入一个autoconfigure依赖即可
-
创建自己的依赖bean,可以看到下图,我这个依赖,只有一个TestService,我需要将这个bean提供给第三方使用
-
编写自己的依赖配置,我这边通过javaconfig的形式去配置了我的testService的注入和相关条件,在没有testService的情况下才允许注入。这是spring提供的一个特性
-
在meta-inf中配置自动装配的spi。这一步所作的意义就是spring容器在初始化的时候,会基于spi机制扫描所有jar包下的META-INF路径下的spring.factories文件中的自动装配的配置类,然后进行解析注入
- 最后一步,进行打包部署。
上述五步做完之后,在springboot项目中引入这个jar就可以自动注入TestService这个bean了。
我这边也是新建了springboot简单项目,用于测试。
运行结果:
可以看到,打印了初始化的构造函数,意味着TestService这个bean就被自动注入进来了。我们仅在新项目中引用了一个pom,其他的什么都没做。这就是starter的神奇之处,能帮我们把一些公用的可以复用的代码和架构进行整合成一个单独项目,所有想要使用这些功能的其他项目,就直接引入就可以了。
上面通过举了一个简单例子,演示了如何创建一个starter,那么这时候有个问题来了,我上面的步骤流程是创建starter的规范吗?又或者网络上那么多讲解springstarter的,谁是规范,该听哪个,学哪个的呢,这时候我的做法就是,谁都看,谁都学,最后通过官方demo验证。下面我通过官方代码去看什么是标准
1.3 spring官方案例
- 首先我们选一个spring官方的starter去研究,这里我选择的是spring-boot-starter-data-redis
- 我们进行分析这个starter的工程模块,打开spring-boot-starter-data-redis的结构
我们看到,spring官方的好像跟我们不太一样,一眼至少能看出来俩个不一样
- 它没有其他代码,只有一个pom文件,没有我们上面的什么Configuration的配置,按道理这里应该有RedisAutoConfiguration之类的配置代码,但是没有。
- 它的pom文件也没有引入autoconfigure依赖,而是和redis比较紧密的spring-data-redis模块。(补充一个知识点,对于之前没有接触过springframework而直接使用spring-boot-framework的,对这个spring-data-redis会很陌生,其实spring-data-redis才是正统的spring接入redis中间件的jar包,它是属于spring-data中的一部分,脱离springboot也能生存,并且能直接和springframework进行紧密协作。而spring-boot-starter-data-redis仅仅是做了一些封装,通过我们看到的,它连代码都没,就是套了个壳,他为什么要套个壳呢,其实还是为了方便spring-boot的自动注入做的一个中间层)
那么问题来了,为什么spring官方的和我不一样呢,是我写的不对,还是我找的示例不对呢,带着这个问题,我们深入去分析下spring的自动装配到底是怎么做的。
首先,我们知道redis如果要进行自动装配,必然有Configuration类,为什么说必然有,因为所有spring接入的第三方组建,都会有这个配置类,那么既然redis这个配置类不在starter里面,那么肯定在某个地方存在。我们进行全局模糊搜索
发现确实存在这个配置类,但是它所属的路径很奇怪,竟然在spring-boot-autoconfigure中,这明显和我上面创建starter的步骤和流程不一样,那么这个spring-boot-autoconfigure有什么作用,他又为什么包含了redisAutoConfiguration,同时又是怎么自动装配redis中间件的呢,带着这么三个问题,我们进行深入分析。
-
首先分析第一个问题,spring-boot-autoconfigure是什么?
我们切入到这个jar包中去看项目结构,如下图所示:
我们可以看到,这个spring-boot-autoconfigure包含的不仅仅是redis相关的配置类,基本涵盖了spring提供的所有组件能力的配置类,是springboot核心能力的聚合。
它又为什么包含了redisAutoConfiguration这个配置类,它可以不包含吗?答:可以,它可以不包含,就像我们的写法一样,单独拎出来去写。这就跟我们上面所提到的疑问一样,为什么我们的写法和spring官方的starter写法不一样呢,我们可以使用spring官方的写法去写我们自己的starter嘛?
答:不可以。为什么?因为我们构建自己的starter只能是新建工程去做,如果要想使用spring官方这种做法,那么我们需要修改springboot源码,把自己的代码放到spring源码中重新编译打包,定制自己企业级的标准,这么做的成本太大,而且也没必要,因此我们不使用spring官方的做法,使用我们自己的构建方式,那么我们的写法是错的吗?或者为什么使用这种写法呢,带着这个问题我们分析第三个问题。它是如何自动装配了redis的呢
我们知道,如果要想使一个配置类基于starter模式去注入,最后都要使用spring的spi机制使得这个自动装配类被加载和解析。
我们看到配置类上有一个ConditionalOnClass注解,这个注解见面知意。当classpath中存在这个class的时候,这个配置类生效,那么我们看JedisConnection.class, RedisOperations.class, Jedis.class 这三个类都是哪个包下的
因此我们可以大致得出一个构思,springboot先是把所有内置组件的自动装配类封装到autoconfigure的jar包中,然后采用ConditionalOnClass来判断每一个自动装配类是否生效,当我们引入某一个组件的时候,例如spring-boot-starter-data-redis,其中的pom文件会自动引入spring-data-redis依赖,而这个依赖中存在着是否开启这个中间件的class类。正好和spring-boot-autoconfigure中的装配类中的条件前后呼应,达到自动注入的功能。
好的,既然我们已经分析这一层的因果关系,那么,最终的redisAutoConfiguration是在哪里被spi机制配置呢,其实大概也能猜出,这个配置类在哪,基本就是在那个jar包中进行spi配置,如下图所示:
相信这张图能够清晰的展现出spring是如何最终将redisAutoConfiguration和spi机制结合起来的,也是和我们之前的案例中的做法类似,是写在MTA-INF中,唯一不同的是,我们是在starter包下面写的,而它是在autoconfigure的jar包中写的。
以上总结:
通过我们自己的案例和分析spring官方的做法,我们知道了差异和相似点。那么我相信有很多有洁癖的开发同学,肯定会说:我就觉得你这种写法不好,我就喜欢官方的写法。那么我们是否依旧学习官方的这种做法呢。答:虽然我上面说是不可以的,但是实际上是可以的,但是会有些区别。
1.4 借鉴官方创建标准starter
首先我们分析下spring-boot-autoconfigure这个jar包,我们上面的分析发现,spring把自己的所有内置组件的Configuration配置类都放在这里。那么我们也依葫芦画瓢去做这么一个autoconfigure包
-
在创建这个包之前,我们还得分析下,需要引入哪些依赖,有同学会问,我可以什么都不引入,单纯去做自己的组件吗?或者看我上面的demo是引入了spring-boot-autoconfigure这个jar,那么我们自己模仿的可以不引入吗?答:是不行的,首先我们最需要依赖的EnableAutoConfiguration注解在这个jar包中,所以需要要引入这个jar
-
经过上面俩点论证,我们准备创建自己的autoconfigure包,在创建autoconfigure之前,我们注意一个点,类似于redis,我们需要有redis自身的类,类似于spring-data-redis这种,因此,我们需要先创建spring-test1表示是自己的一个测试包
spring-xxq-test1工程很简单,就只有一个TestService,也没有任何依赖。
-
创建autoconfigure,如下
注意,我们这里引入的spring-boot-test1只是为了实现ConditionalOnClass,因此optional一定要设置为true,表示依赖不传递,这也是为了和spring官方做法保持一致,还有一点。这里的包的命名不要和我们测试boot工程的启动类的命名路径重合,我们测试的boot工程启动类在com.xxq包路径下,因此我们这边将autoconfigure的包名设置为了com.test路径,因为如果autoconfigure也在com.xxq下,默认就会被spring启动扫描,这会对测试造成干扰。
-
创建我们的starter
可以看到,我们的starter也是没有任何spring依赖,仅仅是我们需要的spring-xxq-test1
-
进行测试
测试结果如上,当我们引入上面接入的俩个jar包后,启动该项目就会自动打印出testService初始化这行日志,也就表明我们的配置是成功的。
总结:
上面我们通过了分析spring-boot-starter-redis这个案例,然后我们也仿照写了一个类似的。通过我们自己的案例,不知道有没有小伙伴发现,我们这种写法太啰嗦了,完全没有我一开始的那个demo清晰,光是单独建立一个autoconfigure jar包就感觉毫无意义。这里确实是很罗嗦,我们的这种写法,会造成事实上的jar包维护困难,而spring之所以能这么做,是因为spring中的sprig-boot-autoconfigure已经内置在spring框架中。因此,如果我们也这么做,那么我们的xxq-spring-boot-autoconfigure就不应该仅仅是为了spring-xxq-test1模块去服务,而是为了我们自己所有的模块进行自动装配,这个一般在企业级自定义框架中使用比较广泛。如果是个人创建或者某个中间件的starter,往往情况是都只有一种功能需要去进行封装,远没有企业级的那么多的框架需求进行封装。所以我们下面会参考mybatis-spring-boot-starter的案例来进行分析出最恰当的starter应该是什么样的。
在分析mybatis-spring-boot-starter案例之前,我们先通过一张图来总结我们上面的结论
内置组件指的就是springboot项目内置依赖中的一些pom的依赖,而外置依赖就是我们开发者不断往springboot项目中加的功能(包括spring提供的以及我们自己添加的)
1.5 分析第三方标准组件
下面我们来深入分析第三方标准组件mybatis-spring-boot-starter,mybatis是一个广为使用的半orm框架,因此它和springboot的组件化,一定是能够代表大部分第三方组件的标准写法。
1.5.1 引入mybatis的pom坐标
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xxq</groupId>
<artifactId>spring-source-analysis</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>com.xxq</groupId>
<artifactId>xxq-spring-boot-autoconfigure</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xxq</groupId>
<artifactId>xxq-spring-boot-starter-test4</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<-- 引入mybatis组件 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
</dependencies>
</project>
我们使用的是2.1.2版本作为研究版本
1.5.2 分析jar包结构
上图是mybatis-spring-boot-starter的pom文件,基于我们上面的总体分析,一般来说xxx-starter依赖中是不含任何代码的,仅仅是相关依赖的引入,真正的配置在autoconfigure中,我们用自己的一张图来进行说明
可以看到mybatis-spring-boot-starter包架构整体还是遵循了spring-boot-starter标准模式,但又有其中不太一样的地方是它将autoconfigure放进了starter中,这是和官方唯一不一样的地方。
上面我们也说了,如果完全按照官方的来进行设计,那么整体包维护将变得非常困难,业务项目工程需要单独引入autoconfigure和starter俩个包,显得尤其臃肿,mybatis选择用自己的方式,是因为它无法像springboot官方一样,把autoconfigure包内置进spring-boot-autoconfigure中,以及项目中。所以我们如果想要自定义第三方starter,可以整体参考mybatis的做法。如下图所示:
1.5.3 MybatisAutoConfiguration分析
上面我们借助mybatis-spring-boot-starter分析了标准化的第三方spring-boot-starter应该怎么去定义。接下里我们顺便分析下mybatis的自动装配类是如何工作的
我们看到这个配置类上有很多注解,我们简要分析下其中重要几个注解的作用
- Configuration(spring的自动装配类注解,属于springframework的注解,在springboot项目中得到广泛应用)
- ConditionalOnClass(springboot的条件自动装配筛选器,配合自动装配一起使用,classpath路径下存在某些类,才会使得当前配置类生效,否则不生效。注意:SqlSessionFactory.class, SqlSessionFactoryBean.class这个是属于mybatis和mybatis-spring中的,这也是为什么需要optional依赖这俩个jar包的原因)
- ConditionalOnSingleCandidate需要和AutoConfigureAfter一起解释,首先这俩个都是springboot注解,说明他是为了自动装配而服务的,其次不知道大家还记得上面mybatis-spring-boot-starter结构图中为何要引入spring-boot-starter-jdbc,就是在此处有用,因为DataSource.class和DataSourceAutoConfiguration.class都是在这个jar包中,因此需要引入依赖,其次ConditionalOnSingleCandidate这个注解主要作用我简单说明一下:当前spring容器中必须要有DataSource这个bean,而且这个注解一般要结合AutoConfigureAfter这个注解一起使用,因为如果ConditionalOnSingleCandidate所依赖的bean不是当前配置类初始化,你很难保证它在当前配置类之前初始化,而AutoConfigureAfter这个配置类就是表示当前配置类在哪个配置类之后进行初始化。
我们可以总结:mybatis肯定是需要依赖DataSource初始化,毕竟它是orm框架,存在的前提是需要有数据源。mybatis使用springboot以及spring注解各种特性来完成和保证mybatis能够正确成功的初始化
1.6 分析AutoConfigureAfter注解
我们上面分析了springboot-starter官方组件以及第三方标准starter结构,对整体自动装配中的starter模式有了很清晰的认识,下面我们分析其中一个比较重要的springboot装配特性AutoConfigureAfter注解
1.6.1 AutoConfigureAfter注解
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Hint for that an {@link EnableAutoConfiguration auto-configuration} should be applied
* after other specified auto-configuration classes.
*
* @author Phillip Webb
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
public @interface AutoConfigureAfter {
/**
* The auto-configure classes that should have already been applied.
* @return the classes
*/
Class<?>[] value() default {};
/**
* The names of the auto-configure classes that should have already been applied.
* @return the class names
* @since 1.2.2
*/
String[] name() default {};
}
这个注解内容不多,含义也很明确,就是表示某个自动装配类在另外一个自动装配类之后进行应用或者初始化,注意注释中有明确说明是EnableAutoConfiguration的自动装配类,不是普通装配类。
下面我们自己来写个demo测试一下这个注解
我们建了个三个工程,工程2在工程1之后初始化,工程3在工程1之前初始化。我们把这三个jar引入到项目工程中去,看看效果。
运行结果:
可以看到,这个注解确实能够自定义注入的先后顺序。
但这里有俩个小插曲
1. 6.2 AutoConfigureAfter失效场景
需要注意下,如果不是基于starer模式的这种自动装配,是不起作用的。
我在当前项目工程中创建了一个类似的配置类,如下:
理论上来说:这个配置类会先于我引入的其他starter被扫描,因为它在启动类的classpath下,所以按道理如果不加AutoConfigureAfter,他肯定先于ConfigurationTest1被初始化,然而我现在加上AutoConfigureAfter,基于这个注解特性,按道理应该晚于ConfigurationTest1被初始化,下面我们看结果:
发现这个注解在这里没有启动作用,这个原因其实在注解的注释中写的很明确了,他只支持EnableAutoConfiguration自动装配的类,不支持我们这种普通装配。
1.6.3 AutoConfigureAfter循环依赖问题
上面的示例我们分析后,发现AutoConfigureAfter能够解决配置类的自定义顺序问题,但是如果我们使用不恰当,就会造成不可解决的循环依赖问题
假设存在一个工程1,工程2需要在工程1之后初始化,我们在工程2上加上AutoConfigureAfter这个注解,然后这时候又来一个工程3,它需要在工程1之前初始化,我们加上AutoConfigureBefore注解,这时候突然提了个需求,工程3同时也需要在工程2之后初始化,那么还需要在工程3上加上一个AutoConfigureAfter注解。如下图所示:
我们发现,这成了一个闭环,spring肯定是无法解决这个循环依赖的,大家不信的可以自己去试试
写在最后
这边留下一个问题,给大家思考,大家用过spring的应该都知道,还有另外一个方法可以控制bean的初始化顺序,这个方法就是@order注解或者Order接口,这俩个在作用上都是为了自定义初始化顺序,那么spring为什么不用这个方式而选择采用新增一个注解来进行实现呢?
问题将在下篇进行解答,也欢迎大家在评论区进行发言。
本章总结
由于篇幅有限,同时为了大家观感体验,自动装配的第二种方式,就不在本章继续进行描述,转而到下一篇文章进行继续描述。