在实际开发中,仅靠SpringBoot的自动配置是远远不够的,比如要访问多个数据源,自动配置就完全无能为力了。

自动配置的本质

本质就是在容器中预配置要整合的框架所需的基础Bean。

以MyBatis为例,spring整合MyBatis无非就是完成以下事情:

  1. 配置SqlSessionFactory Bean,当然,该Bean需要注入一个DataSource
  2. 配置SqlSessionTemplate Bean,将上面的SqlSessionFactory 注入该Bean
  3. 注册Mapper组件的自动扫描,相当于添加<mybatis:scan.../>元素

自动配置非常简单,无非就是有框架提供一个@Configuration修饰的配置类(相当于传统的xml配置文件),在该配置类中用@Bean预先配置默认的SqlSessionFactory、SqlSessionTemplate,并注册Mapper组件的自动扫描即可。

比如MybatisAutoConfiguration源代码:

@Configuration // 被修饰的类变成配置类
// 当SqlSessionFactory、SqlSessionFactoryBean类存在时,才会生效。
// 条件注解之一
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// 当DataSource Bean存在时,才会生效
// 条件注解之一
@ConditionalOnSingleCandidate(DataSource.class)
// 启用Mybatis的属性处理类
// 启动属性处理类
@EnableConfigurationProperties({MybatisProperties.class})
//指定该配置类在DataSourceAutoConfiguration和MybatisLanguageDriverAutoConfiguration之后加载
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
// 实现 InitializingBean 接口,该接口中的 afterPropertiesSet 方法会在该Bean初始化完成后被自动调用
public class MybatisAutoConfiguration implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.class);
// Mybatis的配置属性
private final MybatisProperties properties;
// Mybatis的拦截器、类型处理器、语言驱动等
private final Interceptor[] interceptors;
private final TypeHandler[] typeHandlers;
private final LanguageDriver[] languageDrivers;
private final ResourceLoader resourceLoader;
private final DatabaseIdProvider databaseIdProvider;
private final List<ConfigurationCustomizer> configurationCustomizers;
private final List<SqlSessionFactoryBeanCustomizer> sqlSessionFactoryBeanCustomizers; public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider, ObjectProvider<List<SqlSessionFactoryBeanCustomizer>> sqlSessionFactoryBeanCustomizers) {
this.properties = properties;
this.interceptors = (Interceptor[])interceptorsProvider.getIfAvailable();
this.typeHandlers = (TypeHandler[])typeHandlersProvider.getIfAvailable();
this.languageDrivers = (LanguageDriver[])languageDriversProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = (DatabaseIdProvider)databaseIdProvider.getIfAvailable();
this.configurationCustomizers = (List)configurationCustomizersProvider.getIfAvailable();
this.sqlSessionFactoryBeanCustomizers = (List)sqlSessionFactoryBeanCustomizers.getIfAvailable();
} // 在Bean初始化完成后调用该方法
public void afterPropertiesSet() {
this.checkConfigFileExists();
} // 检查Mybatis配置文件是否存在
private void checkConfigFileExists() {
if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
// 获取配置文件的资源
Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
// 如果resource.exists()方法返回false,则抛出异常
Assert.state(resource.exists(), "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
} } // 创建SqlSessionFactory Bean
@Bean
// 当没有SqlSessionFactory Bean时才会创建
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// 创建SqlSessionFactoryBean实例
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
// 注入数据源
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
// 如果配置文件路径不为空,则设置配置文件位置
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
} this.applyConfiguration(factory);
// 如果配置属性不为空,则设置配置属性
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
// 应用所有的拦截器
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
// 应用所有databaseIdProvider
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
// 根据包名应用TypeAliases
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
// 根据父类型应用TypeAliases
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
// 根据包名应用TypeHandler
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
// 应用所有TypeHandler
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
// 设置mapper的加载位置
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
} Set<String> factoryPropertyNames = (Set) Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
factory.setScriptingLanguageDrivers(this.languageDrivers);
if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
defaultLanguageDriver = this.languageDrivers[0].getClass();
}
} if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
} this.applySqlSessionFactoryBeanCustomizers(factory);
// 返回SqlSessionFactory对象
return factory.getObject();
} private void applyConfiguration(SqlSessionFactoryBean factory) {
org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new org.apache.ibatis.session.Configuration();
} if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
Iterator var3 = this.configurationCustomizers.iterator(); while(var3.hasNext()) {
ConfigurationCustomizer customizer = (ConfigurationCustomizer)var3.next();
customizer.customize(configuration);
}
} factory.setConfiguration(configuration);
} private void applySqlSessionFactoryBeanCustomizers(SqlSessionFactoryBean factory) {
if (!CollectionUtils.isEmpty(this.sqlSessionFactoryBeanCustomizers)) {
Iterator var2 = this.sqlSessionFactoryBeanCustomizers.iterator(); while(var2.hasNext()) {
SqlSessionFactoryBeanCustomizer customizer = (SqlSessionFactoryBeanCustomizer)var2.next();
customizer.customize(factory);
}
} } // 创建SqlSessionTemplate Bean
@Bean
// 当没有SqlSessionTemplate Bean时才会创建
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
// 如果executorType不为null,则创建SqlSessionTemplate时使用该executorType
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
} @Configuration
// 导入MapperScannerRegistrarNotFoundConfiguration注册类
@Import({org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
// 当MapperFactoryBean和MapperScannerConfigurer都不存在时,才会生效
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
// 实现 InitializingBean 接口,该接口中的 afterPropertiesSet 方法会在该Bean初始化完成后被自动调用
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
public MapperScannerRegistrarNotFoundConfiguration() {
} // 重写afterPropertiesSet方法
public void afterPropertiesSet() {
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.debug("Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
}
} // 注册Mapper扫描器的自动配置类
// 实现 BeanFactoryAware接口可访问spring容器、
// 实现ImportBeanDefinitionRegistrar 接口可配置额外的bean
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
// BeanFactory对象,用于保存Spring容器
private BeanFactory beanFactory;
private Environment environment; public AutoConfiguredMapperScannerRegistrar() {
} public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!AutoConfigurationPackages.has(this.beanFactory)) {
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
} else {
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
// 获取自动配置要处理的包
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.isDebugEnabled()) {
packages.forEach((pkg) -> {
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
});
}
// 创建BeanDefinitionBuilder对象
// 它帮助开发者以反射的方式创建任意类的实例
// 此处就是帮助创建MapperScannerConfigurer类的实例
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
// 为要创建的对象设置属性
builder.addPropertyValue("processPropertyPlaceHolders", true);
builder.addPropertyValue("annotationClass", Mapper.class);
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
Set<String> propertyNames = (Set)Stream.of(beanWrapper.getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
if (propertyNames.contains("lazyInitialization")) {
builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
} if (propertyNames.contains("defaultScope")) {
builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");
} boolean injectSqlSession = (Boolean)this.environment.getProperty("mybatis.inject-sql-session-on-mapper-scan", Boolean.class, Boolean.TRUE);
if (injectSqlSession && this.beanFactory instanceof ListableBeanFactory) {
ListableBeanFactory listableBeanFactory = (ListableBeanFactory)this.beanFactory;
Optional<String> sqlSessionTemplateBeanName = Optional.ofNullable(this.getBeanNameForType(SqlSessionTemplate.class, listableBeanFactory));
Optional<String> sqlSessionFactoryBeanName = Optional.ofNullable(this.getBeanNameForType(SqlSessionFactory.class, listableBeanFactory));
if (!sqlSessionTemplateBeanName.isPresent() && sqlSessionFactoryBeanName.isPresent()) {
builder.addPropertyValue("sqlSessionFactoryBeanName", sqlSessionFactoryBeanName.get());
} else {
builder.addPropertyValue("sqlSessionTemplateBeanName", sqlSessionTemplateBeanName.orElse("sqlSessionTemplate"));
}
} builder.setRole(2);
// 在容器中注册BeanDefinitionBuilder创建的MapperScannerConfigurer对象
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}
} // 获取spring容器和环境对象
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
} public void setEnvironment(Environment environment) {
this.environment = environment;
} private String getBeanNameForType(Class<?> type, ListableBeanFactory factory) {
String[] beanNames = factory.getBeanNamesForType(type);
return beanNames.length > 0 ? beanNames[0] : null;
}
}
}

开开发完自动配置类后,还需要使用META-INF/spring.factories文件来定义自动配置类,比如:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

自动配置类只能通过META-INF/spring.factories来加载,并确保它们处于一个特殊的包空间内,尤其不能让他们变成普通@ComponentScan的目标。此外,自动配置类不应该使用@ComponentScan来扫描其他组件,如果需要加载其他配置文件,应使用@Import来加载。

如果要为自动配置类指定加载顺序,可使用以下注解:

  1. @AutoConfigureAfter:指定被修饰的类必须在一个或多个自动配置类之后加载
  2. @AutoConfigureBefore:指定被修饰的类必须在一个或多个自动配置类之前加载

如果自动配置包中包含多个自动配置类,且以特定的顺序来加载,可使用@AutoConfigureOrder来修饰它们,@AutoConfigureOrder类似于@Order注解,只不过专门修饰自动配置类。

条件注解

条件注解用于修饰@Configuration类 或@Bean方法等,表示只有条件有效时,被修饰的 配置类或配置方法才生效。SpringBoot的条件 注解可支持如下几种条件:

  1. 类条件注解:@ConditionalOnClass(表示某些类存在时,可通过Value或 name指定所要求存在的类,value属性是 被检查类的 Class对象;name属性是被检查类的全限定类名的字符串形式)、@ConditionalOnMissingClass(某些类不存在时,只能通过value属性指定不存在的类,value属性值只能是被检查类的全限定类名的字符串形式)
  2. Bean条件注解 :@ConditionalOnMissingBean、@ConditionalOnSingleCandidate、@ConditionalOnBean、@ConditionalOnMissingFilterBean
  3. 属性条件注解:@ConditionalOnProperity
  4. 资源条件注解:@ConditionalOnResource
  5. Web应用条件注解:@ConditionalOnWebApplication、@ConditionalOnNotWebApplication、@ConditionalOnWarDeployment
  6. SpEL表达式条件注解:@ConditionalOnExpression
  7. 特殊条件注解:@ConditionalOnCloudPlatform、@ConditionalOnJava、@ConditionalOnJndi、@ConditionalOnRepositoryType

代码示例:

@Configuration(proxyBeanMethods = false)
// 仅当com.mysql.cj.jdbc.Driver类存在时该配置类生效
@ConditionalOnClass(name = "com.mysql.cj.jdbc.Driver")
public class FkConfig
{
@Bean
public MyBean myBean()
{
return new MyBean();
}
}

@ConditionalOnMissingBean、@ConditionalOnSingleCandidate、@ConditionalOnBean可指定 如下属性:

  1. Class<? extends Annotation>[] annotattion:指定 要检查的 Bean必须用该属性指定的注解修饰
  2. Class<?>[] ignored:指定要忽略哪些类型 的Bean。该属性及ignoredType 仅对@ConditionalOnMissingBean注解有效
  3. String[] ignoredType :与ignored属性的作用相同,只不过该属性用字符串形式 的全限定类名
  4. String[] name:指定要检查的Bean的ID
  5. search:指定搜索目标Bean的 搜索策略、支持CURRENT(仅在容器中搜索)、ACESTORS(仅在祖先容器中搜索)、ALL(在所有容器中搜索)三个枚举值
  6. Class<?> [] value:指定要检查的Bean的类型
  7. String[] type:与value属性作用相同,只不过该属性用字符串形式 的全限定类名

@ConditionalOnSingleCandidate注解相当于@ConditionalOnMissingBean的增强版,不仅要求被检查的Bean必须存在,而且只能有一个“候选者”--能满足byType依赖注入条件。

如果@ConditionalOnMissingBean、@ConditionalOnBean注解不指定任何属性,默认根据目标Bean的类型进行检查,默认检查被修饰的方法返回的Bean类型,代码示例:

// 仅当容器中不存在名为myService的Bean时,才创建该Bean
@ConditionalOnMissingBean
@Bean
public MyService myService()
{
...
} // 当容器中不存在名为jdbcTemplate的Bean时,才创建该Bean
@ConditionalOnMissingBean(name="jdbcTemplate")
@Bean
public JdbcTemplate JdbcTemplate()
{
...
}

@ConditionalOnMissingFilterBean相当于@ConditionalOnMissingBean的特殊版本,专门检查容器中是否有指定类型的javax.servlet.Filter,因此只能通过value指定要检查的Filter的类型。

@ConditionalOnProperity注解 用于检查特定属性是否具有指定的属性值。该注解支持如下属性:

  1. String[] value:指定要检查的属性
  2. String[] name:指定value属性的别名
  3. String havingValue:被检查属性必须具有的属性值
  4. String prefix:自动为各属性名添加该属性指定的前缀
  5. boolean matchMissing:指定当属性未设置属性值时,是否通过检查

代码示例:

@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
// 只有当org.fkjava.test属性具有foo属性值时,下面配置方法才会生效
@ConditionalOnProperty(name = "test", havingValue = "foo",
prefix = "org.fkjava")
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}

启动类代码:

@SpringBootApplication
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
var ctx = SpringApplication.run(App.class, args);
System.out.println(ctx.getBean("dateFormat"));
}
}

此时直接运行程序会有异常。

在application.properties文件添加如下配置:

org.fkjava.test=foo

运行结果如下

@ConditionalOnResource的作用很简单,它要求指定的资源必须存在,修饰的配置类才会生效。使用该注解只需指定resource属性,该属性指定必须存在的资源。

@ConditionalOnWebApplication要求当前应用必须是Web应用时,修饰 的配置类才会生效。可通过type属性指定Web应用类型。该属性支持如下三个枚举值:

  1. ANY:任何Web应用
  2. REACTIVE:当应用时反应式Web应用时
  3. SERVLET:基于servlet的Web应用

代码示例:

@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
// 只有当前应用是反应式Web应用时,该配置才会生效
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}

启动类:

@SpringBootApplication
public class App
{
public static void main(String[] args)
{
var app = new SpringApplication(App.class);
// 设置Web应用的类型,如果不设置则使用默认的类型:
// 如果有Sping Web依赖,自动是基于Servlet的Web应用
// 如果有Sping WebFlux依赖,自动是反应式Web应用
app.setWebApplicationType(WebApplicationType.REACTIVE); // ①
// 创建Spring容器、运行Spring Boot应用
var ctx = app.run(args);
System.out.println(ctx.getBean("dateFormat"));
}
}

@ConditionalOnNotWebApplication要求当前应用不是Web应用时,修饰的配置类或方法才生效

@ConditionalOnWarDeployment要求当前应用以War包部署 到Web服务器或应用服务器中时(不以独立的java程序的方式运行),才生效。

@ConditionalOnNotWebApplication、@ConditionalOnWarDeployment这2个注解使用简单,不需要指定任何属性

@ConditionalOnExpression要求指定SpEL表达式的值为true,所修饰的配置类或方法才会生效。代码示例:

@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
public User user()
{
return new User("fkjava", true);
}
@Bean
// 只有当user.active表达式为true时,该方法才生效。也就是容器中User Bean的active属性为true时,该方法才生效
@ConditionalOnExpression("user.active")
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}

@ConditionalOnCloudPlatform要求应用被部署在特定云平台,修饰的配置类或方法才生效。可通过value属性指定要求的云平台,支持如下枚举值:

  1. CLOUD_FOUNDRY
  2. HEROKU
  3. KUBERNETES
  4. SAP

@ConditionalOnJava对目标平台的java版本进行检测,既可以要求java版本是某个具体的版本,也可以要求高于或低于某个版本。可指定如下两个属性:

  1. JavaVersion value:指定要求的java版本
  2. ConditionalOnJava.Range range:该属性支持EQUAL_OR_NEWER(大于或等于某版本)和OLDER_THAN(小于某版本)两个枚举值。如果不指定该属性,则要求java版本必须是value属性所指定的版本。

代码示例:

@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
// 只有当目标平台的Java版本是11或更新的平台时,该方法才生效
@ConditionalOnJava(value = JavaVersion.ELEVEN,range = ConditionalOnJava.Range.EQUAL_OR_NEWER)
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}

@ConditionalOnJndi要求指定JNDI必须存在,通过value属性指定要检查的JNDI。

@ConditionalOnRepositoryType要求特定的Spring Data Repository被启用时,修饰的配置类或方法才会生效。

自定义条件注解

自定义条件注解的关键就是要有一个Condition实现类,该类负责条件注解的处理逻辑--它所实现的matches()方法决定了条件注解的要求是否得到满足。

代码示例:Condition实现类

public class MyCondition implements Condition
{
@Override
public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata)
{
// 获取@ConditionalCustom注解的全部属性
Map<String, Object> map = metadata.getAnnotationAttributes(
ConditionalCustom.class.getName());
// 获取注解的value属性值(String[]数组)
String[] vals = (String[]) map.get("value");
Environment env = context.getEnvironment();
// 遍历每个属性值
for (Object val : vals)
{
// 如果某个属性值对应的配置属性不存在,返回false
if (env.getProperty(val.toString()) == null)
{
return false;
}
}
return true;
}
}

此处逻辑是要求value属性所指定的所有配置属性必须存在,至于属性值是什么无所谓,这些属性是否有值也无所谓。

自定义条件注解的代码:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定Conditional的实现类
@Conditional(MyCondition.class)
public @interface ConditionalCustom
{
String[] value() default {};
}

使用自定义条件注解:

@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
// 只有当org.fkjava.test和org.crazyit.abc两个配置属性存在时该方法才生效
@ConditionalCustom({"org.fkjava.test", "org.crazyit.abc"})
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}

自定义自动配置

开发自定义的自动配置很简单,分为两步:

  1. 使用@Configuration和条件注解自定义配置类
  2. 在META-INF/spring.factories文件中注册自动配置类

为了演示,先自行开发一个funny框架,功能是用文件或数据库保存程序输出信息。

  1. 先新建一个maven项目,pom.xml如下
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <groupId>org.crazyit</groupId>
<artifactId>funny</artifactId>
<version>1.0-SNAPSHOT</version>
<name>funny</name> <properties>
<!-- 定义所使用的Java版本和源代码所用的字符集 -->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> <dependencies>
<!-- MySQL驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

开发WriterTemplate类

public class WriterTemplate
{
Logger log = LoggerFactory.getLogger(this.getClass());
private final DataSource dataSource;
private Connection conn;
private final File dest;
private final Charset charset;
private RandomAccessFile raf; public WriterTemplate(DataSource dataSource) throws SQLException
{
this.dataSource = dataSource;
this.dest = null;
this.charset = null;
if (Objects.nonNull(this.dataSource))
{
log.debug("==========获取数据库连接==========");
this.conn = dataSource.getConnection();
}
} public WriterTemplate(File dest, Charset charset) throws FileNotFoundException
{
this.dest = dest;
this.charset = charset;
this.dataSource = null;
this.raf = new RandomAccessFile(this.dest, "rw");
} public void write(String message) throws IOException, SQLException
{
if (Objects.nonNull(this.conn))
{
// 查询当前数据库的funny_message表是否存在
ResultSet rs = conn.getMetaData().getTables(conn.getCatalog(), null,
"funny_message", null);
// 如果funny_message表不存在
if (!rs.next())
{
log.debug("~~~~~~创建funny_message表~~~~~~");
conn.createStatement().execute("create table funny_message " +
"(id int primary key auto_increment, message_text text)");
rs.close();
}
log.debug("~~~~~~输出到数据表~~~~~~");
// 插入要输出的字符串
conn.createStatement().executeUpdate("insert into " +
"funny_message values (null, '" + message + "')");
}
else
{
log.debug("~~~~~~输出到文件~~~~~~");
// 输出到文件
raf.seek(this.dest.length());
raf.write((message + "\n").getBytes(this.charset));
}
}
// 关闭资源
public void close() throws SQLException, IOException
{
if (this.conn != null)
{
this.conn.close();
}
if (this.raf != null)
{
this.raf.close();
}
}
}

然后使用 mvn install 命令打成jar包并安装到本地资源库。

在Starter的项目中引入上面的jar包。pom.xml如下:

<?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> <!-- 指定继承spring-boot-starter-parent POM文件 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent> <!-- 定义基本的项目信息 -->
<groupId>org.crazyit</groupId>
<artifactId>funny-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>funny-spring-boot-starter</name> <properties>
<!-- 定义所使用的Java版本和源代码所用的字符集 -->
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> <dependencies>
<!-- Spring Boot Starter依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 依赖自定义的funny框架 -->
<dependency>
<groupId>org.crazyit</groupId>
<artifactId>funny</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

然后在开发中编写自定义配置类:

@Configuration
// 当WriterTemplate类存在时配置生效
// WriterTemplate类是自己编写的工具项目中的类
@ConditionalOnClass(WriterTemplate.class)
// 启用FunnyProperties属性处理类
@EnableConfigurationProperties(FunnyProperties.class)
// 让该自动配置位于DataSourceAutoConfiguration自动配置之后处理
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class FunnyAutoConfiguration
{
// FunnyProperties类负责加载配置属性
private final FunnyProperties properties; public FunnyAutoConfiguration(FunnyProperties properties)
{
this.properties = properties;
} @Bean(destroyMethod = "close")
// 当单例的DataSource Bean存在时配置生效
@ConditionalOnSingleCandidate(DataSource.class)
// 只有当容器中没有WriterTemplate Bean时,该配置才会生效
@ConditionalOnMissingBean
// 通过@AutoConfigureOrder注解指定该配置方法
// 比下一个配置WriterTemplate的方法的优先级更高
// @AutoConfigureOrder 数值越小,优先级越高
@AutoConfigureOrder(99)
public WriterTemplate writerTemplate(DataSource dataSource) throws SQLException
{
return new WriterTemplate(dataSource);
} @Bean(destroyMethod = "close")
// 只有当前面的WriterTemplate配置没有生效时,该方法的配置才会生效
@ConditionalOnMissingBean
@AutoConfigureOrder(199)
public WriterTemplate writerTemplate2() throws FileNotFoundException
{
File f = new File(this.properties.getDest());
Charset charset = Charset.forName(this.properties.getCharset());
return new WriterTemplate(f, charset);
}
}

上面代码中的FunnyProperties类

// 定义属性处理类
@ConfigurationProperties(prefix = FunnyProperties.FUNNY_PREFIX)
public class FunnyProperties
{
public static final String FUNNY_PREFIX = "org.crazyit.funny";
private String dest;
private String charset;
// 省略getter、setter
}

接下来在META-INF/spring.factories文件中注册自动配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.crazyit.funny.autoconfigure.FunnyAutoConfiguration

然后使用 mvn install 命令打成jar包,并安装到maven本地资源库中,就会在自己的本地资源库中找到该jar包,这样就完成了自定义配置的实现。

创建自定义的Starter

一个完整的SpringBoot Starter包含一下两个组件:

  1. 自动配置模块(auto-configure):包含自动配置类和spring.factories文件
  2. Starter模块:负责管理自动配置模块和第三方依赖。简而言之,添加本Starter就能使用该自动配置。

由此看出,Starter不包含任何Class文件,只管理愿意来。如果查看官方提供的jar就会发现,它所有自动配置类的Class都由spring-boot-autoconfigure.jar提供,而各个xxx-starter.jar并未提供任何Class文件,只是在这些jar下的相同路径下提供了一个xxx-starter.pom文件,该文件指定Starter管理的自动依赖模块和第三方依赖。

SpringBoot为自动配置包和Starter包提供推荐命名

  1. 自动配置包的推荐名:xxx-spring-boot
  2. Starter包的推荐名:xxx-spring-boot-starter

对于第三方Starter不要使用spring-boot-starter-xxx这种方式,这是官方使用的。

有了自定义的Starter后,使用起来和官方的没有区别,比如

添加依赖:

<!-- 自定义的funny-spring-boot-starter依赖 -->
<dependency>
<groupId>org.crazyit</groupId>
<artifactId>funny-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

在application.properties文件添加配置

org.crazyit.funny.dest=f:/abc-98765.txt
org.crazyit.funny.charset=UTF-8
# 指定连接数据库的信息
spring.datasource.url=jdbc:mysql://localhost:3306/funny?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=32147
# 配置funny框架的日志级别为debug
logging.level.org.crazyit.funny = debug

主类的代码:

@SpringBootApplication
public class App
{
public static void main(String[] args) throws IOException, SQLException
{
// 创建Spring容器、运行Spring Boot应用
var ctx = SpringApplication.run(App.class, args);
// 获取自动配置的WriterTemplate
WriterTemplate writerTemplate = ctx.getBean(WriterTemplate.class);
writerTemplate.write("自动配置其实很简单");
}
}

SpringBoot--如何创建自己的自动配置的更多相关文章

  1. SpringBoot入门(四)——自动配置

    本文来自网易云社区 SpringBoot之所以能够快速构建项目,得益于它的2个新特性,一个是起步依赖前面已经介绍过,另外一个则是自动配置.起步依赖用于降低项目依赖的复杂度,自动配置负责减少人工配置的工 ...

  2. 接管SpringBoot对Activiti的数据源自动配置

    SpringBoot的自动配置真的让人又爱又恨,但还是爱更多一点. SpringBoot想要帮我们自动配置好一切,但是有时候配置的却并不是我们需要的,甚至有时候会默默的坑我们. 我的项目是一个多数据源 ...

  3. SpringBoot | 4.1 SpringMVC的自动配置

    目录 前言 1. SpringMVC框架的设计与流程 1.1 SpringMVC框架的示意图 1.2 SpringMVC的组件流程 2. *自动配置的源码分析 2.1 导入Web场景启动器 2.2 找 ...

  4. springboot(八) 嵌入式Servlet容器自动配置原理和容器启动原理

    1.嵌入式Servlet容器自动配置原理 1.1 在spring-boot-autoconfigure-1.5.9.RELEASE.jar => springboot自动配置依赖 jar包下,E ...

  5. SpringBoot 2.X集成 jdbc自动配置原理探究

    前言 Springboot对于数据访问层,不管是 SQL还是 NOSQL,Spring Boot 底层都是采用 Spring Data 的方式统一处理.Spring Data 是 Spring 家族中 ...

  6. 面试官:给我讲讲SpringBoot的依赖管理和自动配置?

    1.前言 从Spring转到SpringBoot的xdm应该都有这个感受,以前整合Spring + MyBatis + SpringMVC我们需要写一大堆的配置文件,堪称配置文件地狱,我们还要在pom ...

  7. SpringBoot中对SpringMVC的自动配置

    https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/htmlsingle/#boot-features-developin ...

  8. 一个由于springboot自动配置所产生的问题的解决

    由于我的项目里面需要使用到solr,我做了一下solr和springboot的整合,结果启动项目的时候,就报错了...报错的信息的第一行提示如下: org.springframework.beans. ...

  9. SpringBoot自动配置的实现原理

    之前一直在用SpringBoot框架,一直感觉SpringBoot框架自动配置的功能很强大,但是并没有明白它是怎么实现自动配置的,现在有空研究了一下,大概明白了SpringBoot框架是怎么实现自动配 ...

  10. 这样讲 SpringBoot 自动配置原理,你应该能明白了吧

    https://juejin.im/post/5ce5effb6fb9a07f0b039a14 前言 小伙伴们是否想起曾经被 SSM 整合支配的恐惧?相信很多小伙伴都是有过这样的经历的,一大堆配置问题 ...

随机推荐

  1. 凯亚IOT平台在线测试MQTT接入设备

    一.概述 凯亚 (Kayak)开通了MQTT端口425,以便给感兴趣的同僚进行测试,下面将在此篇文章讲解如何平台接入设备进行MQTT通信 凯亚 (Kayak) 是什么? 凯亚(Kayak)是基于.NE ...

  2. 阿里云Ansible自动化运维平台部署

    以下是在阿里云平台上基于Ansible实现自动化运维的完整实践指南,整合所有核心操作流程和命令,适配指定的服务器规划: 一.环境规划 主机名 IP地址 角色 操作系统 manage01 192.168 ...

  3. TensorFlow 基础 (04)

    最近都面临一个问题是, 要用纯 sql 来实现所有的逻辑, 其实 union 呀, 嵌套, 子查询呀, 这些都还好, 但那带有逻辑判断的, 这就整不好整了, 就多分支的, 再分支这种... 也不知为啥 ...

  4. 彻底掌握 PCA 降维

    PCA 这类的降维算法, 我算是接触好几年了有, 从我学营销的时候, 市场研究方面就经常会用到,相关的还有 "因子分析" 比如, 商品形象认知, 客户细分等场景. 其实多年前我就能 ...

  5. Web前端入门第 51 问:移动端适配的视口元标签(meta)常见使用场景

    经常查看网页源码的同学应该都有注意到,基本上面向移动端的所有网站都有个 <meta name="viewport" xxx> 这样的HTML元素. 为什么需要 meta ...

  6. 快速修改kafka的broker配置或topic配置

    下载开源的kafka界面客户端KafkaKing:https://github.com/Bronya0/Kafka-King 连接后,双击broker配置,或者双击topic配置: 修改好后回车保存就 ...

  7. 参加 Hugging Face 组织的 Gradio & MCP 智能体主题黑客松

    欢迎参加 Gradio & MCP 智能体主题黑客松! 准备好了吗?一场以智能体(Agent)和模型上下文协议(Model Context Protocol,简称 MCP)为核心的全球在线黑客 ...

  8. Markdown中设置图片尺寸及添加图注

    设置缩放比例 使用下面的语法可以调整图片尺寸,同时保证长宽比: <img style="width:缩放比例;" src="图片资源地址"/> 在标 ...

  9. Jenkins自动化部署-----持续交付

    前言: 感谢之前带领过我的leader,让我能够知道什么是好的开发方法. 在很早之前就接触过敏捷开发.什么是敏捷开发,简单来说就是让软件可靠地,快速地发布出来的一种开发方法和技巧. 而敏捷开发中有许多 ...

  10. vivo Pulsar 万亿级消息处理实践(2)-从0到1建设 Pulsar 指标监控链路

    作者:vivo 互联网大数据团队- You Shuo 本文是<vivo Pulsar万亿级消息处理实践>系列文章第2篇,Pulsar支持上报分区粒度指标,Kafka则没有分区粒度的指标,所 ...