1 写作背景
最近有好几个人都问我,懂不懂springboot的starter机制,并想让我给提供个示例!!!
诶,巧了,正好我们项目里就用到了,所以我还真懂,本篇文章就简单按照我的理解来讲一下springboot的starter机制,并提供一个简单的示例!!!
2 为什么会有starter机制
其实
我觉得这个标题我起的有点大
, 这里我就从我自己的理解来简单讲一下。
2.1 springboot项目的默认扫描机制
首先请你要抛去对springboot的所有认知,假设springboot项目就是一个简简单单的spring项目
我简单建立了一个springboot的项目,其目录结构如下:
相信用过springboot的你肯定知道,由于com.yoyo路径下的Hero类,不在com.nrsc
路径下,因此当springboot项目启动后,Hero类将无法被加入到spring的IOC容器里。
原因其实很简单,就是因为启动类中的默认注解@SpringBootApplication
是一个复合注解,它包含的一个比较重要的注解:@ComponentScan
,而它默认的扫描路径就是启动类所在的路径, 对应于上图的项目就是路径com.nrsc
,也就是说只有在com.nrsc
路径或者com.nrsc
子路径的类才有可能被注入到IOC容器里。
有兴趣的可以看一下我的这篇文章,对
ComponentScan
注解做进一步的了解
《【Spring注解】@ComponentScan之 你可能没注意到的点》
当然肯定有人知道我们可以通过下面的方式覆盖掉@SpringBootApplication
默认给提供的@ComponentScan
注解,来自己指定我们要扫描哪个包:
@SpringBootApplication
@ComponentScan(basePackages = {"com.nrsc", "com.yoyo"})
@RestController
public class NrscSpringBootStarterTestApplication {
public static void main(String[] args) {
SpringApplication.run(NrscSpringBootStarterTestApplication.class, args);
}
}
但是这种方式好吗? —》 读者朋友可以先在这里思考一下!!!
2.2 starter机制的必然性
有了上面的铺垫,大家可以想这么一个问题:
假设你们公司里有个
能人
开发了一个比较通用的工具模版,就假设为RedisTemplate吧。
这时候呢,他把他写的这些代码打成了一个jar包,deploy到了你们公司的maven仓库里。然后并在公司内部进行推广使用。
而他所写的这些代码所在的包路径与他们部门的通用包路径是一致的。
那假如你们部门要想用他写的这套通用代码,你该怎么做呢??? —》相信大家应该都知道,就是
- (1)将他们提供的maven坐标引入到我们的pom.xml里
- (2)由于他们的包路径跟你们的包路径不完全一致,因此为了让他代码里的一些bean可以注入到IOC容器,你就需要在你们的项目里通过@ComponentScan注解指定要扫描的包路径也包括他们的。。。。
相信想明白了这个道理,你就知道为什么在真实的项目开发中不应该通过自己指定@ComponentScan
的方式来覆盖掉springboot默认的扫描路径的原因了:
你搞个jar,我通过@ComponentScan将你的包路径增加到spring的扫描路径下;他又搞个jar,我再增加一下;她也搞个jar,我也增加一下???
而springboot的starter机制就可以很好的解决这个问题。
3 springboot的starter机制前置知识介绍
3.1 通过@Import注解 + 实现ImportSelector 接口的方式向IOC容器里注入Bean
要明白springboot的starter机制,我觉得至少应该具备的前置知识是我在《【Spring注解】@Import》这篇文章里讲到的:
spring通过@Import注解 + 实现ImportSelector 的方式向IOC容器里注入Bean的方式,这里我将那篇文章里该部分的内容原封不到的黏贴到下面:
- 继承了ImportSelector接口的类
package com.nrsc.springstudy.c2_import.config;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class NrscImportSelector implements ImportSelector {
/**
* 该类需要实现的方法就一个
* @param importingClassMetadata
* @return
*/
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//返回要注入到spring容器的全类名
return new String[]{
"com.nrsc.springstudy.c2_import.bean.Duck",
"com.nrsc.springstudy.c2_import.bean.Elephant"};
}
}
- 配置类
@Configuration
@Import(value = {Dog.class, Cat.class,NrscImportSelector.class})
public class ImportStudyConfig2 {
}
- 运行结果
这种方式真正关键的地方在哪里呢?我觉得主要有以下几点:
- (1)它不管你是在哪个包,只要你在
selectImports
方法要返回的字符串数组里给定全额限定名
就可以了 - (2)selectImports方法要的是字符串数组
其实我觉得知道了这两点,你肯定也会想着:
诶,我能不能把要注入到IOC容器的bean对应的
全额限定名
直接写在一个配置文件里,然后这样的话,我是不是就可以在配置文件里直接指定将哪个bean注入到IOC容器了。。。 —》 其实这个事人家springboot都帮你做好了,你只需要按照人家的规范来就可以了。
3.2 springboot对@Import注解 + 实现ImportSelector接口方式向IOC容器里注入Bean方式
的封装
这里我不打算展开讲了,仅做一下简单的描述:
- (1)你需要在资源目录
resources
下建立一个 META-INF 目录,并在该目录下建立一个 spring.factories 文件 - (2)在该文件里你只需要向下图一样,指定你要被IOC容器管理的配置类的
全额限定名
就可以了
当你做完这两步后,假设你打个jar包,放到maven仓库里,别人引入你的maven坐标,则springboot项目启动时,就会管理你指定的要被IOC容器管理的配置类 — 通常这些类的命名方式都为:XXXXAutoConfiguration
—> 即自动配置类。
这里我们再看一下springboot项目中`默认的要被IOC容器管理的自动配置类`:
4 定义一个自己的starter
明白了上面的原理,其实我们就可以很容易的定义一个自己的starter了。
4.1 定义starter的一些模式 + 规范
模式和规范主要如下:
图片来自于《https://www.bilibili.com/video/BV1gW411W76m/?p=71&t=1171》
4.2 自动配置类的书写方式 —》 以WebMvcAutoConfiguration为例看看人家官方怎么写的
我觉得最好的可借鉴栗子应该就在springboot的源码里,比如说WebMvcAutoConfiguration
, 这里我简单贴一下源码,并做一些简单的备注:
@Configuration(proxyBeanMethods = false) //指定该类是一个配置类
@ConditionalOnWebApplication(type = Type.SERVLET) //指定该类在Web项目中才生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })//指定该类在括号里的三个类都存在的情况下才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //指定该类在没有WebMvcConfigurationSupport类的情况下才生效
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) //指定自动配置类的顺序
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })//指定自动配置类的顺序
public class WebMvcAutoConfiguration {
public static final String DEFAULT_PREFIX = "";
public static final String DEFAULT_SUFFIX = "";
private static final String[] SERVLET_LOCATIONS = { "/" };
@Bean
//当容器里没有HiddenHttpMethodFilter时下面的bean才被注入到IOC容器
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
//当配置文件里有spring.mvc.hiddenmethod.filter.enabled=true时,下面的bean会注入到IOC容器,
//如果没有配置、或者配置成spring.mvc.hiddenmethod.filter.enabled=false,启动就会报错
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
@Bean
@ConditionalOnMissingBean(FormContentFilter.class)
//当配置文件里有spring.mvc.formcontent.filter.enabled时,下面的bean会注入到IOC容器,如果没有配置的话,也没事
@ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
public OrderedFormContentFilter formContentFilter() {
return new OrderedFormContentFilter();
}
static String[] getResourceLocations(String[] staticLocations) {
String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length];
System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, SERVLET_LOCATIONS.length);
return locations;
}
// Defined as a nested config to ensure WebMvcConfigurer is not read when not
// on the classpath
@Configuration(proxyBeanMethods = false)//标明本内部类为一个配置类
@Import(EnableWebMvcConfiguration.class)//又是一个@Import肯定可以通过该注解往IOC容器里注入一些bean
//指定 WebMvcProperties.class, ResourceProperties.class 两个属性文件生效
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
private final ResourceProperties resourceProperties;
private final WebMvcProperties mvcProperties;
。。。。省略N行
}
对于注解
@EnableConfigurationProperties
其实我在我的这篇文章《springboot项目真实开发中如何通过yml或properties文件向项目中传值》做过比较详细的介绍,有兴趣的可以看一下。
它的作用其实就是配合
@ConfigurationProperties
注解将配置类的属性传给springboot项目中的某个bean —》我们项目里在我的坚持下其实很早就基本都改成了这种姿势!!!
4.3 正式定义自己的starter
4.3.1 XXX-starter-configure — 真正的业务逻辑 + 要自动装配到IOC的类等
XXX-starter-configure的目录结构如下:
这里粘贴一下具体的代码:
- HelloProperties
import org.springframework.boot.context.properties.ConfigurationProperties;
/***
* 将配置文件中配置的nrsc.hello.prefix 和 nrsc.hello.suffix传入到该bean
*/
@ConfigurationProperties(prefix = "nrsc.hello")
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
- HelloProperties
package com.nrsc.starter;
public class HelloService {
/***
* 将HelloProperties作为本类的一个属性
* 主要用来测试@ConfigurationProperties 和 @EnableConfigurationProperties搭配通过配置文件往bean中传入属性
*/
private HelloProperties helloProperties;
/***get、set方法*/
public HelloProperties getHelloProperties() {
return helloProperties;
}
public void setHelloProperties(HelloProperties helloProperties) {
this.helloProperties = helloProperties;
}
/***真正要提供的方法*/
public String sayHello(String name) {
return helloProperties.getPrefix() + "-" + name + "-" + helloProperties.getSuffix();
}
}
- HelloServiceAutoConfiguration
package com.nrsc.starter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnWebApplication //web应用才生效
@EnableConfigurationProperties(HelloProperties.class) //指定HelloProperties生效
public class HelloServiceAutoConfiguration {
@Autowired
HelloProperties helloProperties;
@Bean
//当配置文件里有nrsc.hello.match=true时,下面的bean会注入到IOC容器,
//如果没有配置或配置成nrsc.hello.match=false时启动就会报错
@ConditionalOnProperty(prefix = "nrsc.hello", name = "match", matchIfMissing = false)
public HelloService helloService() {
HelloService service = new HelloService();
service.setHelloProperties(helloProperties);
return service;
}
}
- spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.nrsc.starter.HelloServiceAutoConfiguration
- pom.xml — 必须要有spring-boot-starter
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.nrsc.starter</groupId>
<artifactId>nrsc-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<!--引入spring-boot-starter:所有starter的基本配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
4.3.2 XXX-starter —》仅仅用于依赖管理
其目录结构和代码如下所示:
5 简单测试
- (1)将XXX-starter-configure 打包到maven仓库
- (2)将XXX-starter打包到maven仓库
- (3)简单起一个项目并引入starter的maven坐标,以本文为例就是:
<dependency>
<groupId>com.nrsc.starter</groupId>
<artifactId>nrsc-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
就可以看到我的XXX-starter-configure和XXX-starter在新起的项目中的jar包了。
测试类中application.properties如下:
server.port=9100
nrsc.hello.prefix=hello
nrsc.hello.suffix=nice to meet you!!!
# 注意下面的配置如果没有或者配置成false时启动都会报错
nrsc.hello.match=true
访问结果如下:
end!!!