SpringBoot starter 原理及如何自定义 starter
前言
项目的开发要求是不断进化的,而随着时间以及技术的推移,在项目中除了基本的编程语言外,还需要进行大量的应用服务整合。例如,在项目中使用 MySQL 数据库进行持久化存储,同时会利用 Redis 作为缓存存储,以及使用 RocketMQ 实现异构系统整合服务等。 但在早先使用 Spring 开发的时候,如果想要进行某些服务的整合,常规的做法是引入对应服务的依赖,而后进行一些 XML 的配置和一些组件的定义。而如今在 SpringBoot 中往往各个组件都是以 starter 形式出现的,并且简化了很多配置吗,如 spring-boot-starter-data-redis、rocketmq-spring-boot-starter等。
SpringBoot starter 机制
SpringBoot 中的 starter 是一种非常重要的机制,能够抛弃以前在 Spring 中的繁杂配置,将其统一集成进 starter,应用者只需要在 maven 中引入 starter 依赖,SpringBoot 就能自动扫描到要加载的信息并启动相应的默认配置。starter 让我们摆脱了各种依赖库的处理以及需要配置各种信息的困扰。SpringBoot 会自动通过 classpath 路径下的类发现需要的 Bean,并注册进 IOC 容器。SpringBoot 提供了针对日常企业应用研发各种场景的 spring-boot-starter 依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。
自定义 starter 的作用
在我们的日常开发工作中,经常会有一些独立于业务之外的配置模块,比如对 web 请求的日志打印。我们经常将其放到一个特定的包下,然后如果另一个工程需要复用这块功能的时候,需要将代码硬拷贝到另一个工程,重新集成一遍,这样会非常麻烦。如果我们将这些可独立于业务代码之外的功配置模块封装成一个个 starter,复用的时候只需要将其在 maven pom 中引用依赖即可,让 SpringBoot 为我们完成自动装配,提高开发效率。
自定义 starter 的命名规则
SpringBoot提供的 starter 以 spring-boot-starter-xxx 的方式命名的。官方建议自定义的 starter 使用 xxx-spring-boot-starter 命名规则。以区分 SpringBoot 生态提供的 starter。如:mybatis-spring-boot-starter
如何自定义starter
步骤
- 新建两个模块,命名规范: xxx-spring-boot-starter
- xxx-spring-boot-autoconfigure:自动配置核心代码
- xxx-spring-boot-starter:管理依赖
- ps:如果不需要将自动配置代码和依赖项管理分离开来,则可以将它们组合到一个模块中。但 SpringBoot 官方建议将两个模块分开。
- 在 xxx-spring-boot-autoconfigure 项目中
- 引入 spring-boot-autoconfigure 的 maven 依赖
- 创建自定义的 XXXProperties 类: 这个类的属性根据需要是要出现在配置文件中的。
- 创建自定义的类,实现自定义的功能。
- 创建自定义的 XXXAutoConfiguration 类:这个类用于做自动配置时的一些逻辑,需将上方自定义类进行 Bean 对象创建,同时也要让 XXXProperties 类生效。
- 创建自定义的 spring.factories 文件:在 resources/META-INF 创建一个 spring.factories 文件和 spring-configuration-metadata.json,spring-configuration-metadata.json 文件是用于在填写配置文件时的智能提示,可要可不要,有的话提示起来更友好。spring.factories用于导入自动配置类,必须要有。
- 在 xxx-spring-boot-starter 项目中引入 xxx-spring-boot-autoconfigure 依赖,其他项目使用该 starter 时只需要依赖 xxx-spring-boot-starter 即可
示例工程
新建工程 jokerku-log-spring-boot-autoconfigure
命名为jokerku-log-spring-boot-autoconfigure,代码编写在这个工程中。
新建工程 jokerku-log-spring-boot-starter
命名为 jokerku-log-spring-boot-starter 工程为一个空工程,只依赖jokerku-log-spring-boot-autoconfigure
jokerku-log-spring-boot-autoconfigure:
项目结构:
1.新增注解命令为Log
/**
* @Author: guzq
* @CreateTime: 2022/07/09 18:19
* @Description: 自定义日志注解
* @Version: 1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String desc() default "";
}
2.新增LogInterceptor拦截器
/**
* @Author: guzq
* @CreateTime: 2022/07/09 18:16
* @Description: TODO
* @Version: 1.0
*/
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
private static final ThreadLocal<TraceInfo> THREAD_LOCAL = new InheritableThreadLocal();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Log log = handlerMethod.getMethodAnnotation(Log.class);
if (log != null) {
long start = System.currentTimeMillis();
TraceInfo traceInfo = new TraceInfo();
String uri = request.getRequestURI();
String method = handlerMethod.getMethod().getDeclaringClass() + "#" + handlerMethod.getMethod();
traceInfo.setStart(start);
traceInfo.setRequestMethod(method);
traceInfo.setRequestUri(uri);
String traceId = UUID.randomUUID().toString().replaceAll("-", "");
traceInfo.setTraceId(traceId);
MDC.put(TraceInfo.TRACE_ID, traceId);
THREAD_LOCAL.set(traceInfo);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Log logAnnotation = handlerMethod.getMethodAnnotation(Log.class);
if (logAnnotation != null) {
long end = System.currentTimeMillis();
TraceInfo traceInfo = THREAD_LOCAL.get();
long start = traceInfo.getStart();
log.info("requestUri:{}, requestMethod:{}, 请求耗时:{} ms", traceInfo.getRequestUri(), traceInfo.getRequestMethod(), end - start);
THREAD_LOCAL.remove();
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
3.新增config,将 LogInterceptor 加入拦截器中
@Configuration
public class LogAutoConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor());
}
}
4.核心点:在resources下新建 META-INF 文件夹,然后创建spring.factories文件
springboot自动装配会读取该配置文件,会将 LogAutoConfigure 这个类自动装配,此时strart包就被装配成功
# springboot自动装配机制 会读取该配置 进行自动装配
org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.jokerku.autoconfigure.LogAutoConfigure
spring.factories 书写规则
spring.factories 由 key = value 结构组成
- key为接口类,可以使用spring的接口,可以使用自定义的接口(自定义接口实现类必须加上 @component才能被加载)
- value为需要加载的实现类,不是必须实现interface
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
com.jokerku.autoconfigure.LogAutoConfigure
org.springframework.boot.SpringApplicationRunListener= \
com.jokerku.testListener
org.springframework.context.ApplicationContextInitializer= \
com.jokerku.testInitializer
#自定义接口
com.jokerku.service.TestService = com.jokerku.service.impl.TestServiceImpl
#如其一个接口有多个实现,如下配置:
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
其他项目引入jokerku-log-spring-boot-starter依赖
<dependency>
<groupId>com.jokerku</groupId>
<artifactId>jokeku-log-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
接口添加该注解
运行,输出拦截信息:
自定义 starter 时会可能会用到的注解
- @Conditional:按照一定的条件进行判断,满足条件给容器注册bean
- @ConditionalOnMissingBean:给定的在bean不存在时,则实例化当前Bean
- @ConditionalOnProperty:配置文件中满足定义的属性则创建bean,否则不创建
- @ConditionalOnBean:给定的在bean存在时,则实例化当前Bean
- @ConditionalOnClass: 当给定的类名在类路径上存在,则实例化当前Bean
- @ConditionalOnMissingClass :当给定的类名在类路径上不存在,则实例化当前Bean
- @ConfigurationProperties:用来把 properties 配置文件转化为bean来使用
- @EnableConfigurationProperties:使 @ConfigurationProperties 注解生效,能够在 IOC 容器中获取到转化后的 Bean
SpringBoot Starter 原理
想要了解 SpringBoot 是如何加载 starter 的(也就是 SpringBoot 的自动装配原理),首先就要从启动类上的 @SpringBootApplication 注解说起。 SpringBoot 通过 SpringApplication.run(App.class, args) 方法启动项目,在启动类上有 @SpringBootApplication 注解,研究上面的原理首先看 @SpringBootApplication 内部的组成结构,如下图: 下面对 @SpringBootConfiguration 和 @EnableAutoConfiguration 进行详解。
@SpringBootConfiguration 注解
@SpringBootConfiguration 内部结构,如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {...}
@SpringBootConfiguration 上有 @Configuation 注解,@Configuration是 Spring 的一个注解,其修饰的类会加入 Spring 容器。这就说明 SpringBoot 的启动类会加入 Spring 容器。
@EnableAutoConfiguration 注解
@EnableAutoConfiguration 内部结构,如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}
重点看 @AutoConfigurationPackage 注解和 @Import(AutoConfigurationImportSelector.class) 注解。
@AutoConfigurationPackage 注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
@AutoConfigurationPackage 上包含 @Import(AutoConfigurationPackages.Registrar.class) 注解 看其@Import进来的类AutoConfigurationPackages.Registrar类:
/**
* {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
* configuration.
*/
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
这是一个内部类,继续关注内部的 registerBeanDefinitions 方法中调用的 AutoConfigurationPackages#register 方法
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
}
else {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BasePackages.class);
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN, beanDefinition);
}
}
register 方法 apidoc 解释:以编程方式注册自动配置包名称,随后的调用将把给定的包名添加到已经注册的包名中。您可以使用此方法手动定义将用于给定BeanDefinitionRegistry的基本包。通常,建议您不要直接调用此方法,而是依赖默认约定,其中包名是从@EnableAutoConfiguration配置类中设置的。 register 方法会生成一个 BasePackages 类型的 BeanDefinition,最终会扫描 basePackages 目录及其子目录下的类,将其全部注入 Spring IOC容器中。 总结: @AutoConfigurationPackage 注解最终会扫描 packageNames 目录下的类并将其全部注入到 Spring IOC 容器中。packageNames 为当前启动类的根目录(当前的根目录为 com.jokerku)。
@Import(AutoConfigurationImportSelector.class) 注解
首先解释 @Import注解在 Spring 中的作用: @Import 通过快速导入的方式实现把实例加入spring的IOC容器中
- @Import({TestA.class}):这样就会把 TestA 注入进 IOC 容器,生成一个名字为 “com.demo.testA” 的 bean
所以 AutoConfigurationImportSelector 最终也会被 Spring 加载注入进 IOC 容器,重点关注AutoConfigurationImportSelector 中的内部类 AutoConfigurationGroup。 AutoConfigurationGroup 的 process 方法会调用 getAutoConfigurationEntry 方法,getAutoConfigurationEntry 的作用是获取自动配置项。其底层会通过 getCandidateConfigurations 方法调用SpringFactoriesLoader.loadFactoryNames 去 META-INF/spring.factories 目录下加载 org.springframework.boot.autoconfigure.EnableAutoConfiguration 自动配置项。 最终META-INF/spring.factories 目录下的 自动配置项会被 Spring IOC 容器进行加载。
流程图
SPI机制
SpringBoot 为了更好地达到 OCP 原则(即“对扩展开放,对修改封闭”的原则)通过将自动配置项写在 META-INF/spring.factories 目录下的方式进行加载。而这种方式其实就是 SPI 机制。 在 Java 中 提供了原生的 SPI 机制,Java SPI(Service Provider Interface)实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。SPI是一种为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。JDK SPI 提供了 ServiceLoader 类,入口方法是 ServiceLoader.load() 方法,ServiceLoader 会默认加载 META-INF/services/ 下的文件。 各个框架中都有类似 SPI 机制的实现,如 MyBatis 中的 Plugin、Dubbo 中的 Dubbo-SPI。
链接:https://juejin.cn/post/7193996189669261370
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。