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

步骤

  1. 新建两个模块,命名规范: xxx-spring-boot-starter
    1. xxx-spring-boot-autoconfigure:自动配置核心代码
    2. xxx-spring-boot-starter:管理依赖
    3. ps:如果不需要将自动配置代码和依赖项管理分离开来,则可以将它们组合到一个模块中。但 SpringBoot 官方建议将两个模块分开。
  2. 在 xxx-spring-boot-autoconfigure 项目中
    1. 引入 spring-boot-autoconfigure 的 maven 依赖
    2. 创建自定义的 XXXProperties 类: 这个类的属性根据需要是要出现在配置文件中的。
    3. 创建自定义的类,实现自定义的功能。
    4. 创建自定义的 XXXAutoConfiguration 类:这个类用于做自动配置时的一些逻辑,需将上方自定义类进行 Bean 对象创建,同时也要让 XXXProperties 类生效。
    5. 创建自定义的 spring.factories 文件:在 resources/META-INF 创建一个 spring.factories 文件和 spring-configuration-metadata.json,spring-configuration-metadata.json 文件是用于在填写配置文件时的智能提示,可要可不要,有的话提示起来更友好。spring.factories用于导入自动配置类,必须要有。
  3. 在 xxx-spring-boot-starter 项目中引入 xxx-spring-boot-autoconfigure 依赖,其他项目使用该 starter 时只需要依赖 xxx-spring-boot-starter 即可

示例工程

image.png

新建工程 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 image.png

jokerku-log-spring-boot-autoconfigure:

项目结构:

image.png

1.新增注解命令为Log
 
java
复制代码
/**
* @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拦截器
 
java
复制代码
/**
* @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 加入拦截器中
 
java
复制代码
@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包就被装配成功

 
sql
复制代码
# springboot自动装配机制 会读取该配置 进行自动装配
org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.jokerku.autoconfigure.LogAutoConfigure
spring.factories 书写规则

spring.factories 由 key = value 结构组成

  • key为接口类,可以使用spring的接口,可以使用自定义的接口(自定义接口实现类必须加上 @component才能被加载)
  • value为需要加载的实现类,不是必须实现interface
 
java
复制代码
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依赖

 
java
复制代码
         <dependency>
            <groupId>com.jokerku</groupId>
            <artifactId>jokeku-log-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
接口添加该注解

image.png 运行,输出拦截信息: image.png

自定义 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 内部的组成结构,如下图: image.png 下面对 @SpringBootConfiguration 和 @EnableAutoConfiguration 进行详解。

@SpringBootConfiguration 注解

@SpringBootConfiguration 内部结构,如下:

 
java
复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {...}

@SpringBootConfiguration 上有 @Configuation 注解,@Configuration是 Spring 的一个注解,其修饰的类会加入 Spring 容器。这就说明 SpringBoot 的启动类会加入 Spring 容器。

@EnableAutoConfiguration 注解

@EnableAutoConfiguration 内部结构,如下:

 
java
复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}

重点看 @AutoConfigurationPackage 注解和 @Import(AutoConfigurationImportSelector.class) 注解。

@AutoConfigurationPackage 注解

 
java
复制代码
@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类:

 
java
复制代码
	/**
	 * {@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 方法

 
java
复制代码
	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配置类中设置的。 image.png image.png 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。 image.png image.png image.png AutoConfigurationGroup 的 process 方法会调用 getAutoConfigurationEntry 方法,getAutoConfigurationEntry 的作用是获取自动配置项。其底层会通过 getCandidateConfigurations 方法调用SpringFactoriesLoader.loadFactoryNames 去 META-INF/spring.factories 目录下加载 org.springframework.boot.autoconfigure.EnableAutoConfiguration 自动配置项。 最终META-INF/spring.factories 目录下的 自动配置项会被 Spring IOC 容器进行加载。

流程图

springboot 自动装配流程图.png

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。


作者:JokerGu
链接:https://juejin.cn/post/7193996189669261370
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

posted @ 2024-04-12 10:52  沧海一滴  阅读(1466)  评论(1编辑  收藏  举报