什么是 Starter?
举个例子,在 Spring Boot 项目中引入 spring-boot-starter-data-redis 的依赖,就获得了一个全局可用的 StringRedisTemplate 类型的 bean 对象。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>从命名上就能看出来,spring-boot-starter-data-redis 就是一个 starter。
当然,其实 spring-boot-starter-data-redis 并不是只有 StringRedisTemplate 这么一个 bean。
在 starter 中可以声明多个 bean,项目在启动的时候会加载需要的 bean,这个就是 bean 的自动装配。
在我的理解中,starter 就是一个 jar 包,只需要遵循 Spring 的约定大于配置的哲学,根据 Spring 的规则制作 jar 包。在其他项目中引入这个 jar 包就可以直接获得 bean 对象了,不需要二次配置。
如何制作自己的 starter?
官方自己制作的 starter 的命名规范是:spring-boot-starter-xxx。
- spring-boot-starter-web
- spring-boot-starter-data-redis
- spring-boot-starter-mail
第三方制作 starter 的命名建议是:xxx-spring-boot-starter,跟官方命名的方向相反。
- mybatis-spring-boot-starter
如果是自己制作的话,不用管那么多规范,因为蒙多想去哪就去哪。
第一步:创建新项目,引入依赖
<groupId>org.example</groupId> <artifactId>a</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>3.0.0</version> </dependency> </dependencies>第二步:从配置文件获取数据
还是以 spring-boot-starter-data-redis 为例,我们在引入 jar 包后,需要在配置文件中配置 redis 的相关信息。
同样的,我们制作 starter 的时候也可以从配置文件中获取数据。prefix = "a",配置文件需要以 a 为前缀。
@ConfigurationProperties(prefix = "a") public class AProperties { private String A1; private String A2; public AProperties() { } public AProperties(String a1, String a2) { A1 = a1; A2 = a2; } public String getA1() { return A1; } public void setA1(String a1) { A1 = a1; } public String getA2() { return A2; } public void setA2(String a2) { A2 = a2; } }第三步:编写业务类
public class AService { private AProperties aProperties; public AService(AProperties aProperties) { this.aProperties = aProperties; } public void work() { System.out.println("用户名:" + aProperties.getA1()); System.out.println("密码:" + aProperties.getA2()); System.out.println("starter业务执行"); } }这里简单提供了一个 work 方法,打印 A1、A2 对应的取值,用来模拟从配置文件中获得地址、用户名、密码、数据库等信息。
第四步:编写自动配置类
@Configuration @EnableConfigurationProperties(AProperties.class) public class AAutoConfig { @Bean public AService aService(AProperties aProperties) { return new AService(aProperties); } }在这个自动配置类中通过方法返回一个 bean 对象。
第五步:服从 Spring 的命令
在 resources 目录下创建 META-INF 目录。
在 META-INF 目录下创建 spring 目录。
在 spring 目录下创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。
在这个什么 imports 文件中写入自动配置类的全类名。
org.example.a.AAutoConfig这个是 Spring Boot 3 的写法,还有一种写法是 Spring Boot 2.x 的,但是我都没用过 2.x 的版本所以我也不知道怎么写。
但是写法本身不重要,也没必要研究茴香豆的茴字有多少种写法,要用的时候查一下就行了。
第六步:打 jar 包并保存到本地仓库
通过 maven 执行 install 命令,将项目打成 jar 包并保存到本地仓库中。
如果是用 IDEA 写 Java 的话,IDEA 本身就集成了 maven,就算自己安装了 maven 也会默认用 IDEA 自带的 maven,仓库目录是:C:\Users\admin\.m2\repository。
验证 starter 是否可正常使用
创建新项目,引入 starter。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>a</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>server: port: 8080 a: A1: 100 A2: 200@RestController public class BController { @Autowired private AService aService; @GetMapping("/test") public int test() { aService.work(); return 200; } }接口正常返回数据,业务方法正常执行。现在我们成功制作了一个自己的 starter,之后还需要用 AService 的功能,直接引入使用即可,可以说是开袋即食。
但是,我们这么大费周章,结果只是给 Spring 添加了一个 bean。我们不是可以用 @Component 注解声明 bean 吗?
那就试试吧。
@Component 和配置类
我在 starter 中添加了一个 bean。
@Component public class AComponent { public void work() { System.out.println("一个小组件"); } }同时把 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 的目录和文件都删除掉了,确保 bean 的加载跟上文不同。
重新打包,这次把版本号改成 2.0-SNAPSHOT。
在测试项目中注入这个新的 bean。
@RestController public class BController { @Autowired private AService aService; @Autowired private AComponent aComponent; @GetMapping("/test") public int test() { aService.work(); aComponent.work(); return 200; } }现在重新启动测试项目,结果服务启动失败,提示缺少 bean 对象。
Description: Field aComponent in org.example.b.BController required a bean of type 'org.example.a.AComponent' that could not be found. The injection point has the following annotations: - @org.springframework.beans.factory.annotation.Autowired(required=true) Action: Consider defining a bean of type 'org.example.a.AComponent' in your configuration.为什么会出现这种情况呢?
因为 Spring 默认只会扫描启动类所在包和子包下面的类。我们的测试项目的包是 org.example.b,而 starter 项目的包是 org.example.a,在测试项目中根本就没有扫描到 org.example.a 下面的类,自然不会把 AComponent 注册为 bean。
解决方法很简单,在启动类中添加 @ComponentScan("org.example.a"),指定要扫描的路径即可。
@SpringBootApplication @ComponentScan("org.example.a") public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } }现在服务启动成功了,但是接口报错了,响应码是 404。
这个问题就出在了 @ComponentScan("org.example.a") 这里,因为我们指定了要扫描路径是 org.example.a,所以它不会去扫描 org.example.b,我写的 Controller 都没有被扫描到,Controller 里面的 test 接口自然也不会开启。
浏览器向一个不存在的接口发送请求,当然是 404 资源不存在。
所以,我们需要继续修改要扫描的路径。
@SpringBootApplication @ComponentScan({"org.example.a", "org.example.b"}) public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } }现在再启动项目试一下,这次就顺利启动并且接口正常响应了。
Spring Boot 中 bean 的自动装配
在我看来,bean 的创建有两种方式:
- @Component 注解声明的 bean
- @Configuration 配置类中 @Bean 方法返回的 bean
一个项目中 bean 的来源有两个途径:
- 项目本身声明的 bean
- 项目需要的依赖中引入的 bean
现在两两组合一下,得到了四种取值:
- 项目自己通过 @Component 注解,被 @ComponentScan 扫描注册为 bean
- 项目自己通过 @Configuration 配置类中 @Bean 注解,也是被 @ComponentScan 扫描注册为 bean
- 依赖中的 @Component 注解,也需要被 @ComponentScan 扫描注册为 bean
- 依赖中的配置类中的 @Bean 注解
- 要么被 @ComponentScan 扫描注册为 bean
- 要么遵循 Spring 的规则,创建 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件并写上配置类的全类名
如果是通过 @ComponentScan 扫描配置类的话,不需要这个 imports 文件。
但是,如果不用 @ComponentScan 的话,就必须用 imports 文件声明配置类的位置。
在 Spring Boot 的启动类中,有一个 @SpringBootApplication 注解,Ctrl + B 进入这个类,发现这个类上面还有其他注解。
- @SpringBootApplication
- @SpringBootConfiguration:这是一个配置类
- @ComponentScan:扫描的路径,默认是扫描启动类所在包及其下面的子包
- @EnableAutoConfiguration:从依赖中的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中获取自动配置类的路径,根据 @Conditional 注解这些配置类中的 @Bean 修饰的方法的返回值注册成 bean
那现在问题出现了,如果我们要用其他第三方的 bean,通过 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 这个文件只能获得配置类中 @Bean 方法返回的 bean,不能获得 @Component 声明的 bean。
如果要获得 @Component 声明的 bean,需要配置 @ComponentScan 的路径,但是我们一配置这个路径,就能直接获得 @Component 和 @Bean 声明的 bean 了,也不需要这个 imports 文件了啊。
其实直接通过 @ComponentScan 去指定路径的话是比较麻烦的。用极限法来假设一下,你要用 1000 亿个依赖,每个依赖都有自己的包名,那不就要在 @ComponentScan 中手动填写 1000 亿次?这多麻烦啊。
用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 声明配置类的位置,这个工作不是我们自己干的,是制作这个依赖的人去完成的,我们只需要引入依赖就可以使用了,这就很方便。