1、为什么用starter
我们知道,springboot框架是为了简化spring框架开发推出的,那么,在之前的spring框架开发中,如果我们需要连接数据库,引入mybatis框架,需要怎么操作呢?我们需要去maven仓库找mybatis的jar包,还要找mybatis-spring的jar包,然后在applicationContext.xml文件中配置dataSource和mybatis相关信息,还有数据库驱动包等,可见,整个步骤相当麻烦。另外,我们还要考虑版本兼容的问题。因此,springboot为了简化开发,解决上述问题,引入了starter机制。
2、什么是starter
我们还以mybatis-springboot-starter为例,看看它都有哪些东西。找到对应的jar包,展开发现,内容很少,如下
pom.properties:配置了maven所需的version、groupId和artifactId信息
pom.xml:配置了所需要的jar包,我们重点关注下mybatis-spring-boot-autoconfigure这个包
MANIFEST.MF:配置了jar包的信息,这个我们不过多关注
spring.provides:主要给ide使用,也不过多关注
我们可以看到,这个starter很简单,一行实际代码都没有,那么它是如何工作的呢,我们看下上面说到的mybatis-spring-boot-autoconfigure这个包,看到autoconfigure有没有眼前一亮,这个自动装配之前在SpringBoot自动装配原理有过讲解,接下来,我们看下这个包。
pom.properties、pom.xml和MANIFEST.MF这三个不再赘述
additional-spring-configuration-metadata.json和spring-configuration-metadata.json这两个文件配置了一些properties属性信息,都属于ide配置提示,例如我们引入这个starter后,在配置文件输入mybatis,ide自动弹出一些属性配置
那么这两个文件有什么区别呢?如果pom.xml中引入了spring-boot-configuration-processor这个包,则会自动生成spring-configuration-metadata.json文件,如果需要手动修改里面的元数据,则可以在additional-spring-configuration-metadata.json中编辑,最终两个文件中的元数据会合并到一起。
spring.factories:这个文件的内容是key-value形式,key是EnableAutoConfiguration,value就是要自动装配的内容(即对应的XXXAutoConfiguration.java文件的类路径)。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
ConfigurationCustomizer.java:一个自定义回调接口
SpringBootVFS.java:用于扫面嵌套的jar包
MybatisProperties.java:一个JavaBean,配置了mybatis初始化需要的一些属性,接下看相当重要的MybatisAutoConfiguration.java文件的内容
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.mybatis.spring.boot.autoconfigure;
import java.util.Iterator;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.ClassPathMapperScanner;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
private final MybatisProperties properties;
private final Interceptor[] interceptors;
private final ResourceLoader resourceLoader;
private final DatabaseIdProvider databaseIdProvider;
private final List<ConfigurationCustomizer> configurationCustomizers;
public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
this.properties = properties;
this.interceptors = (Interceptor[])interceptorsProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = (DatabaseIdProvider)databaseIdProvider.getIfAvailable();
this.configurationCustomizers = (List)configurationCustomizersProvider.getIfAvailable();
}
@PostConstruct
public void checkConfigFileExists() {
if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
Assert.state(resource.exists(), "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
}
}
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
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()));
}
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 var4 = this.configurationCustomizers.iterator();
while(var4.hasNext()) {
ConfigurationCustomizer customizer = (ConfigurationCustomizer)var4.next();
customizer.customize(configuration);
}
}
factory.setConfiguration(configuration);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}
@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class})
public static class MapperScannerRegistrarNotFoundConfiguration {
public MapperScannerRegistrarNotFoundConfiguration() {
}
@PostConstruct
public void afterPropertiesSet() {
MybatisAutoConfiguration.logger.debug("No {} found.", MapperFactoryBean.class.getName());
}
}
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
public AutoConfiguredMapperScannerRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
try {
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (MybatisAutoConfiguration.logger.isDebugEnabled()) {
Iterator var5 = packages.iterator();
while(var5.hasNext()) {
String pkg = (String)var5.next();
MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
}
}
scanner.setAnnotationClass(Mapper.class);
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(packages));
} catch (IllegalStateException var7) {
MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", var7);
}
}
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
}
这个文件由@Configuration
标明这是一个配置类,配置了很多bean,例如SqlSessionFactory
,SqlSessionTemplate
,最重要的是SqlSessionFactory
,它用来创建SqlSession,用来操作数据库,@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
和@ConditionalOnBean({DataSource.class})
分别注明只有SqlSessionFactory
类和SqlSessionFactoryBean
类、DataSource
实例时,该配置生效。@EnableConfigurationProperties({MybatisProperties.class})
自动配置MybatisProperties.java
中的属性,@AutoConfigureAfter({DataSourceAutoConfiguration.class})
注明这个配置类在DataSourceAutoConfiguration
之后配置。
通过上面的例子,我们可以发现,编写一个starter主要步骤如下:
- 定义一个xxx-spring-boot-starter空项目,只包含pom.properties和pom.xml,pom.xml文件中包含xxx-spring-boot-autoconfigure包
- 定义一个xxx-spring-boot-autoconfigure项目,包含xxxAutoConfiguration类,该类定义一些bean、spring.factories文件增加k-v键值对、XXXProperties类,定义配置值
接下来,尝试自定义一个starter。
3、自定义starter
首先,建一个空项目randompwd-spring-boot-starter,pom文件如下
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.zy</groupId>
<artifactId>randompwd-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>randompwd-spring-boot-starter</name>
<description>Demo for Custom Starter</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.example.zy</groupId>
<artifactId>randompwd-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
创建randompwd-spring-boot-autoconfigure项目,pom文件如下
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.zy</groupId>
<artifactId>randompwd-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>randompwd-spring-boot-autoconfigure</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
在resource下创建META-INF文件夹,创建spring.factories文件,配置k-v键值对
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.zy.RandomPwdAutoConfiguration
新建RandomPwdAutoConfiguration.java文件,(可以先新建该文件再配置k-v值,防止出错,配置成功可以从k-v值点到该文件),代码如下
package com.example.zy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author zy
* @version 1.0.0
* @ClassName RandomPwdAutoConfiguration.java
* @Description TODO
* @createTime 2022/12/14
*/
//标记配置类
@Configuration
//检测到含有RandomPwdProperties类生效
@ConditionalOnClass(RandomPwdProperties.class)
//自动加载配置类
@EnableConfigurationProperties(RandomPwdProperties.class)
public class RandomPwdAutoConfiguration {
@Autowired
private RandomPwdProperties randomPwdProperties;
@Bean
public RandomPwdService randomPwdService(){
return new RandomPwdService(randomPwdProperties.getLength());
}
}
对应的RandomPwdProperties代码如下,它会把配置文件中以randompwd开头的配置读取
package com.example.zy;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author zy
* @version 1.0.0
* @ClassName RandomPwdProperties.java
* @Description TODO
* @createTime 2022/12/14
*/
@ConfigurationProperties(prefix="randompwd")
public class RandomPwdProperties {
private int length;
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
}
RandomPwdService代码如下
package com.example.zy;
import java.util.Random;
/**
* @author zy
* @version 1.0.0
* @ClassName RandomPwdService.java
* @Description TODO
* @createTime 2022/12/14
*/
public class RandomPwdService {
private int length;
public RandomPwdService(int length) {
this.length = length;
}
public String randomPwd(){
char charr[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
char charq[] = "1234567890".toCharArray();
StringBuilder sb = new StringBuilder();
Random r = new Random();
for (int x = 0; x < length - 6; ++x) {
sb.append(charr[r.nextInt(charr.length)]);
}
for (int x = 0; x < 6; ++x) {
sb.append(charq[r.nextInt(charq.length)]);
}
return sb.toString();
}
}
将这两个打包,然后我们新建一个测试类,或者在其他项目上简单测试,引入我们的starter
<dependency>
<groupId>com.example.zy</groupId>
<artifactId>randompwd-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
配置文件敲入random,可以看到,提示了配置
然后写一个测试接口
@Autowired
private RandomPwdService randomPwdService;
@RequestMapping("/randomPwd")
public String randomPwd(){
return randomPwdService.randomPwd();
}
启动项目,测试接口,发现正常使用,至此,自定义Starter成功。