1、本篇前言
Spring Boot为我们提供了简化企业级开发绝大多数场景的
starter pom【比如springb-boot-starter-web,springb-boot-starter-jdbc等】, 使用应用场景所需要的starter pom,只需要引入对应的starter,即可以得到Spring Boot为我们提供的自动配置的Bean。
然而,可能在很多情况下,我们需要自定义stater,这样可以方便公司内部系统调用共同的配置模块的时候可以自动进行装载配置。比如公司的很多内部系统都有认证授权模块、以及基于AOP实现的日志切面等,这些技术在不同的项目中逻辑基本相同,而这些功能可以通过starter自动配置的形式进行配置,即可达到可复用的效果。
(1)Starter的概念
SpringBoot之所以大大地简化了我们的开发,用到的一个很重要的技术就是Starter机制!
Starter机制抛弃了以前xml中繁杂的配置,将各种配置统一集成进了Starter中,开发人员只需要在maven中引入Starter依赖,SpringBoot就能自动扫描出要加载的配置信息并按相应的默认配置来启动项目。
所以Starter可以理解为一个可拔插式的插件,提供了一系列便利的依赖描述符,使得我们可以获得所需的所有Spring和相关技术的一站式服务。应用程序只需要在maven中引入Starter依赖,SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置,我们可以把Starter看做是Springboot的场景启动器。
(2)Starter的优点
- Starter可以让我们摆脱开发过程中对各种依赖库的冲突处理。
- 可以简化原有xml中各种负载的配置信息。
- SpringBoot提供了针对一般研发中各种场景的spring-boot-starter依赖,所有这些依赖模块都遵循着约定成俗的默认配置(”约定大于配置“),并允许我们调整这些配置。Starter的出现极大的帮助开发者们从繁琐的框架配置中解放出来,从而更专注于业务代码。
(3)自定义Starter的场景
在我们的日常开发工作中,经常会有一些独立于业务之外的配置模块,我们经常将其放到一个特定的包下,然后如果另一个工程需要复用这块功能的时候,只需要将其在pom中引用依赖即可,利用SpringBoot为我们完成自动装配即可。
常见的自定义Starter场景比如:
- 动态数据源
- 登录模块
- 基于AOP技术实现日志切面
- …
(4)自定义Starter命名规范
SpringBoot官方建议其官方推出的starter以spring-boot-starter-xxx的格式来命名,而第三方开发者自定义的starter则以xxxx-spring-boot-starter的规则来命名,比如 mybatis-spring-boot-starter。
(5)自定义starter中几个重要注解
- @Configuration: 表明此类是一个配置类,将变为一个bean被Spring进行管理。
- @EnableConfigurationProperties: 启用属性配置,将读取指定类里面的属性。
- @ConditionalOnClass: 当类路径下面有指定的类时,进行自动配置。
- @ConditionalOnProperty: 判断指定的属性是否具备指定的值。
- @ConditionalOnMissingBean:当容器中没有指定bean是,创建此bean。
- @Import: 引入其他的配置类
2、自定义Starter记录日志案例
(1)项目结构
两个工程如下:log-spring-boot-starter[starter模块],spring-boot-demo-starter-test[starter使用演示工程]。
(2)log-spring-boot-starter项目pom文件添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
(3)编写请求日志注解
/**
* 功能描述: 请求日志注解
* @author TuYong
* @date 2022/9/7 20:48
*/
(ElementType.METHOD)
(RetentionPolicy.RUNTIME)
public @interface RequestLog {
//接口方法上的描述信息
String desc() default "";
}
(4)编写配置类
(prefix = "request.log")
public class LogProperties {
private Boolean enabled = Boolean.FALSE;
}
(5)编写日志拦截器
/**
* 功能描述: 日志拦截器
* @author TuYong
* @date 2022/9/7 20:49
*/
public class LogInterceptor implements HandlerInterceptor {
private static final ThreadLocal<Long> THREAD_LOCAL = new ThreadLocal<>();
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequestLog methodAnnotation = handlerMethod.getMethodAnnotation(RequestLog.class);
if(methodAnnotation != null){
long start = System.currentTimeMillis();
THREAD_LOCAL.set(start);
}
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequestLog methodAnnotation = handlerMethod.getMethodAnnotation(RequestLog.class);
if(methodAnnotation != null){
Method method = handlerMethod.getMethod();
String requestUri = request.getRequestURI();
String methodName = method.getDeclaringClass().getName()+":"+method.getName();
String desc = methodAnnotation.desc();
long end = System.currentTimeMillis();
long start = THREAD_LOCAL.get();
long l = end - start;
THREAD_LOCAL.remove();
log.info("请求路径:{},请求方法:{},描述信息:{},总计耗时:{}",requestUri,methodName,desc,l);
}
}
}
(6)编写自动配置类
/**
* 功能描述: 自动配置
* @author TuYong
* @date 2022/9/7 20:55
*/
({LogProperties.class})
(
prefix = "request.log",
name = {"enabled"},
havingValue = "true"
)
public class LogAutoConfiguration implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()).addPathPatterns("/api/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
(7)编写spring.factories
在resources/META-INF/下建立spring.factories文件,配置自动装配类路径
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.javaxxw.springboot.LogAutoConfiguration
(8)创建starter演示工程引入starter依赖
<dependencies>
<dependency>
<groupId>cn.javaxxw</groupId>
<artifactId>log-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(9)编写测试接口并使用starter中定义的注解
("api")
public class ApiController {
("test")
(desc = "测试接口")
public Object test(){
return "test api!";
}
}
(10)配置文件
server:
port: 8080
# 开启starter组件
request:
log:
enabled: true
(11)功能测试
通过请求测试接口,我们可以看到starter中的拦截器已经生效
(12)演示工程代码
gitee: https://gitee.com/trazen/springboot-demo.git
3、原理解析
第二章节中的开发流程,有两个地方需要我们了解一下:
(1)starter的自动识别加载:spring.factories里的EnableAutoConfiguration原理。
(2)实现自动加载的智能化、可配置化:@Configuration配置类里注解。
(1)配置类自动加载机制
在SpringBoot的启动类都会加上@SpringBootApplication注解。这个注解会引入@EnableAutoConfiguration注解。然后@EnableAutoConfiguration会@Import(AutoConfigurationImportSelector.class)。
AutoConfigurationImportSelector.class的selectImports方法最终会通过SpringFactoriesLoader.loadFactoryNames,加载META-INF/spring.factories里的EnableAutoConfiguration配置值,也就是我们上文中设置的资源文件。
(2)自动加载的智能化可配置化
实际项目中,我们并不总是希望使用默认配置。比如有时候我想自己配置相关功能,或者只有某些bean没有被加载时才会加载starter配置类。这些常见的场景Starter都可以实现,并提供了如下的解决方案:
@Conditional*注解
springboot starter提供了一系列的@Conditional*注解,代表什么时候启用对应的配置,具体的可以去查看springboot的官方文档。常用注解如下:
- @ConditionalOnBean:当容器里有指定的Bean的条件下。
- @ConditionalOnClass: 当类路径下面有指定的类时,进行自动配置。
- @ConditionalOnProperty: 判断指定的属性是否具备指定的值。
- @ConditionalOnMissingBean:当容器中没有指定bean是,创建此bean。
比如我们案例中的 @ConditionalOnProperty(prefix = "request.log", name = {"enabled"},havingValue = "true") ,只有在应用配置 request.log.enabled = true 时该请求日志拦截功能才会生效。
@ConfigurationProperties注解
这个注解主要是为了解决如下场景:我想要使用starter的默认配置类,但是又想对配置中的某些参数进行自定义配置。@ConfigurationProperties类就是做这个工作的。例如上述例子中,我们在配置中使用enabled参数来决定是否启动该组件的日志拦截功能。