springboot自定义starter,实现自动装配

1、SpringBoot Starter介绍

       随着Spring的日渐臃肿,为了简化配置、开箱即用、快速集成,Spring Boot 横空出世。 目前已经成为 Java 目前最火热的框架了。平常我们用Spring Boot开发web应用。Spring mvc 默认使用tomcat servlet容器, 因为Spring mvc组件集成了spring-boot-starter-tomcat 。但是现在undertow servlet容器的性能非常好。我们可以通过以下方式先排除tomcat:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<version>${springboot.version}</version>
	<!-- 排除Tomcat依赖 -->
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
		</exclusion>
	</exclusions>
</dependency>

然后直接替换为undertow:

<!--添加 Undertow依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

        代码无需更改。这就是组件化后的好处:1.可插拔。2.可定制。3.按需集成。为什么能够做到快速适配?Spring Boot 有一个“约定大于配置”的规则,让程序组件之间来减少配置,降低复杂性。因此在开发中自定义一个Spring Boot Starter的时候,也最好考虑你的starter如何达到以上的便利性。

2、Spring Boot的一些约定

2.1、starter命名规范

        Spring官方提供的starter命名规范:spring-boot-starter-xxx.jar

        第三方的starter命名规范:xxx-spring-boot-starter.jar,比如mybatis-spring-boot-starter

3、starter自动配置原理

       starter会将具备某种功能的坐标打包在一起,可以简化导入依赖的过程,例如,当导入spring-boot-starter-web这个Starter时,会将和Web开发的相关jar包一起导入到项目中。

自动配置设计了几个关键的步骤:

  1. 基于Java代码的Bean配置
  2. 自动配置条件依赖
  3. Bean参数获取
  4. Bean的发现
  5. Bean的装载

接下来,就mybatis-spring-boot-starter这个starter的引入,基于上面5个步骤来讲解Starter的自动配置原理

3.1、基于Java代码的Bean配置

        pom文件中添加mybatis-spring-boot-starter的依赖坐标后,将会导入相关的jar包。

<dependency>
     <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.2</version>
</dependency>

        其中一个重要的的jar包为mybatis.spring.boot.autoconfigure,根据命名可知,该jar包涉及了mybatis的自动装配信息。

 

        在该jar中,有一个非常关键的类 MybatisAutoConfiguration,关键代码如下:

  @Configuration 与 @Bean 注解的使用,可以创建一个基于java的配置类,相当于原生的xml配置文件。根据MybatisAutoConfiguration,自动创建了一个SqlSessionFactory session工厂和一个操作Sql的SqlSessionTemplate的template工具类,交给Spring容器自动管理。

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)  
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean{

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
      //忽略业务代码
    }
    
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
      //忽略业务代码
     }
}

3.2、自动配置条件依赖

        根据MybatisAutoConfiguration 上标注的其他注解可知,完成Mybatis配置类的装配是有多个依赖条件的:
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)

完成自动装配首先类路径上必须存在SqlSessionFactory和SqlSessionFactoryBean两个类,同时DataSource的单例Bean也必须先行加载到Spring容器中。
同时也可根据 生成SqlSessionFactory 和SqlSessionTemplate 的方法参数可知必须依赖SqlSessionFactory、DataSource。

注解功能说明
@ConditionalOnBean仅在当前上下文中存在某个bean时,才会实例化这个Bean
@ConditionalOnClass

某个class位于类路径上,才会实例化这个Bean。

使用场景:

当需要确保某些配置仅在引入特定第三方库的情况下才被激活。

@ConditionalOnExpression当表达式为true的时候,才会实例化这个Bean
@ConditionalOnMissingBean仅在当前上下文中不存在某个bean时,才会实例化这个Bean
@ConditionalOnMissingClass某个class在类路径上不存在的时候,才会实例化这个Bean
@ConditionalOnNotWebApplication不是web应用时才会实例化这个Bean
@AutoConfigureAfter在某个bean完成自动配置后实例化这个bean
@AutoConfigureBefore在某个bean完成自动配置前实例化这个bean

3.3、Bean参数获取

        要完成mybatis的自动配置,需要在配置文件中提供数据源相关的参数,例如数据库驱动

URL,数据库用户名及密码等。根据@AutoConfigureAfter({DataSourceAutoConfiguration.

class, MybatisLanguageDriverAutoConfiguration.class})可知,数据源信息是通过DataSou

rceAutoConfiguration配置类先行配置的,关键代码如下:

package org.springframework.boot.autoconfigure.jdbc

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(type = {"io.r2dbc.spi.ConnectionFactory"})
@AutoConfigureBefore({SqlInitializationAutoConfiguration.class})
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, InitializationSpecificCredentialsDataSourceInitializationConfiguration.class, SharedCredentialsDataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
    ......
}

        Bean参数的获取离不开@EnableConfigurationProperties({DataSourceProperties.class}) 注解的使用,即引入数据源相关的配置属性,能将application.yml/application.properties配置文件中值加载到该属性配置类DataSourceProperties这个POJO对象上。【属性名和yml中配置的key保持一致】

//application.yml/application.properties 中配置数据源的前缀
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    private ClassLoader classLoader;
    private boolean generateUniqueName = true;
    private String name;
    private Class<? extends DataSource> type;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String jndiName;
    //忽略get set方法
}

  实现Bean参数获取的功能离不开两个重要的注解:

  • @ConfigurationProperties 能将application.yml/application.properties中配置的值封装映射到标注了该bean的相应属性上.
  • @EnableConfigurationProperties 启用自动装配属性,使得@ConfigurationProperties注解生效,两者搭配使用,缺一不可。

3.4、Bean的发现

        SpringBoot默认扫描:启动类所在包下的类以及子类的所有组件但并没有包括依赖包中类这里不得不思考下,依赖包中的bean是如何被发现和加载的,需要从SpringBoot项目的启动类上进行追踪,在启动类上我们会加上@SpringBootApplication注解

@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
}

 @EnableAutoConfiguration标识着开启了自动装配的功能:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration

 @Import(AutoConfigurationImportSelector.class)引入自动加载选择器,其中有一个方法装载候选Bean.

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
	List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
			getBeanClassLoader());
	Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
			+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

能将META-INF下的spring.factories文件配置的类通过类加载器装载到Spring 容器中。关于mybatis自动装配的类如下 :

# Auto Configure
org.spingframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis,spring.boot,autoconfigure.MybatisLanguageDriverAutoconfiguration,\
org.mybatis.spring.boot,autoconfigure.MybatisAutoConfiguration

3.5、Bean的加载

        在SpringBoot应用中,自动配置加载Bean是通过@Import注解这种方式,AutoConfigurationImportSelector类的selectinports方法返回一组从META-INF/spring.factories文件读取的全限定类名,通过反射并实例化这些Bean到Spring容器中。

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

	private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

4、自定义一个starter

        先创建一个项目,在该项目中定义 Starter 的内容,然后通过 Maven 将其打成 jar 包,之后在另一个项目中使用该 Starter 。

4.1、创建一个maven 项目,并引入如下依赖:

        创建一个 Maven 项目,在其 pom 文件中引入自动装配的依赖,并定义好 Starter 的名称。

   spring-boot-autoconfigure依赖是必须要引入的,spring-boot-configuration-processor的引入是为了在配置文件中使用属性时有提示。

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

4.2、新建一个 Properties 配置类,

        配置类用于保存外部化配置文件中定义的配置数据,其中配置文件包括 properties 或 yml 。

// 定义配置文件中的属性前缀
@ConfigurationProperties(prefix = "demo")
public class DemoProperties {

    private String name;

    private String date;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }
}

 4.3、新建一个功能类

       功能类  主要用来返回 DemoProperties 中的 name 和 date 属性。

public class DemoService {

    private DemoProperties demoProperties;

    public DemoService(DemoProperties demoProperties) {
        this.demoProperties = demoProperties;
    }

    public String getName() {
        return demoProperties.getName();
    }

    public String getDate() {
        return demoProperties.getDate();
    }
}

4.4、创建自动配置类

        在该配置类中完成 Starter 的功能。这里,通过构造器注入 DemoProperties 配置类对象,并初始化 DemoService 功能类。

@Configuration
@EnableConfigurationProperties(value = DemoProperties.class)
public class DemoAutoConfiguration {

    private final DemoProperties demoProperties;

    public DemoAutoConfiguration(DemoProperties demoProperties) {
        this.demoProperties = demoProperties;
    }

    @Bean
    // 当前项目是否包含 DemoService Class 
    @ConditionalOnMissingBean(DemoService.class)
    public DemoService demoService() {
        return new DemoService(demoProperties);
    }
}

        自动配置类是 SpringBoot 自动装配特性不可或缺的一环,关于 SpringBoot 自动装配底层实现,大家可以参考《SpringBoot自动装配(二)》这篇文章。

4.5、自定义初始化器和监听器

        这是 SpringBoot 提供的扩展点,主要在 SpringBoot 的不同生命周期执行相应操作.

public class DemoApplicationContextInitializer implements
        ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println(" DemoApplicationContextInitializer 初始化成功 ");
    }
}
public class DemoApplicationListener implements ApplicationListener<SpringApplicationEvent {

    @Override
    public void onApplicationEvent(SpringApplicationEvent springApplicationEvent) {
        if (springApplicationEvent instanceof ApplicationStartingEvent) {
            System.out.println(" DemoApplicationListener 监听 ApplicationStartingEvent 事件");
        }
    }
}

 关于初始化器和监听器大家可以参考《Spring Boot SpringApplication启动类(一)》的 2.2 和 2.3 小节 。

4.6、starter生效配置

 在 src/main/resources 目录下创建 META-INF 文件夹,并在文件夹中创建 spring.factories 文件,定义如下内容:

# Initializers
org.springframework.context.ApplicationContextInitializer=\
com.loong.demo.context.DemoApplicationContextInitializer

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.loong.demo.configuration.DemoAutoConfiguration

# Application Listeners
org.springframework.context.ApplicationListener=\
com.loong.demo.listener.DemoApplicationListener

说明:自动配置类放入自动配置文件中,不同版本对应的配置文件不同,按需选择。

  • springboot 2.7 之前自动配置文件为spring.factories ,配置内容的形式如下:

    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
    org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
    org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
    
  • springboot 2.7到springboot 3.0,自动配置文件可以使用spring.factories,也可以使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,推荐使用后者

  • springboot 3.0之后自动配置文件只能使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

4.7、starter主动生效和被动生效

  • 一种是主动生效,在starter组件集成入Spring Boot应用时需要你主动声明启用该starter才生效,即使你配置完全。这里会用到@Import注解,将该注解标记到你自定义的@Enable注解上, 我们将@EnableSMS 该注解标记入Spring Boot应用就可以使用短信功能了。

/**
 * 	启用demo信息配置
 */
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DemoAutoConfiguration .class)
public @interface EnableDemo {
}
  • 另一种被动生效,在starter组件集成入Spring Boot应用时就已经被应用捕捉到。这里会用到类似java的SPI机制。在autoconfigure资源包下新建META-INF/spring.factories写入DemoAutoConfiguration全限定名。 多个配置类逗号隔开,换行使用反斜杠。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.loong.demo.configuration.DemoAutoConfiguration

5、测试自定义 Starter

5.1、在另一个项目中引入该 Starter 的 Maven 依赖:

<dependency>
	<groupId>com.loong</groupId>
	<artifactId>demo-spring-boot-starter</artifactId>
	<version>1.0.0.RELEASE</version>
</dependency>

5.2、在 properties 文件中定义配置数据:

demo.name = loong
demo.date = 2020.01.01

5.3、获取自动配置类:

        在启动类中,获取 DemoService Bean ,并调用它的 getDate 和 getName 方法获取配置文件中的数据:

@SpringBootApplication
public class DiveInSpringBootApplication {
	public static void main(String[] args) {
		ConfigurableApplicationContext run = SpringApplication.run(DiveInSpringBootApplication.class, args);

		DemoService bean = run.getBean(DemoService.class);
		System.out.println(bean.getDate() + " === " + bean.getName());

	}
}

 最后,查看控制台的输出:

/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA CE.app..."
 DemoApplicationListener 监听 ApplicationStartingEvent 事件

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.3.RELEASE)

 DemoApplicationContextInitializer 初始化成功 
2024-01-01 13:14:02.023  INFO 55657 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-01-01 13:14:02.189  INFO 55657 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6b19b79: startup date [Wed Jan 01 13:13:59 CST 2020]; root of context hierarchy
2024-01-01 13:14:02.257  INFO 55657 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/hello],methods=[GET]}" onto public java.lang.String com.loong.diveinspringboot.Chapter1.controller.HelloWorldController.helloWorld(java.lang.String)
2024-01-01 13:14:02.260  INFO 55657 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2024-01-01 13:14:02.261  INFO 55657 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2024-01-01 13:14:02.296  INFO 55657 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-01-01 13:14:02.296  INFO 55657 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-01-01 13:14:02.341  WARN 55657 --- [           main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2024-01-01 13:14:02.718  INFO 55657 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
2024-01-01 13:14:02.726  INFO 55657 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2024-01-01 13:14:02.727  INFO 55657 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2024-01-01 13:14:02.728  INFO 55657 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto protected java.util.Map<java.lang.String, java.util.Map<java.lang.String, org.springframework.boot.actuate.endpoint.web.Link>> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2024-01-01 13:14:02.766  INFO 55657 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2024-01-01 13:14:02.822  INFO 55657 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2024-01-01 13:14:02.826  INFO 55657 --- [           main] c.l.d.C.DiveInSpringBootApplication      : Started DiveInSpringBootApplication in 3.607 seconds (JVM running for 3.984)
2020.01.01 === loong

可以看到,结果正确输出,且初始化器和监听器都已被加载。这里只是一个简单的演示,Starter 较为简单,大家可以根据实际情况实现一个更为复杂的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值