Spring Boot 中的 Starter
- 1.常用 Starter
- 2.为什么要用 Starter
- 3.Starter 有哪些要素
我们都知道,Spring 的功能非常强大,但也有些弊端。比如:我们需要手动去配置大量的参数,没有默认值,需要我们管理大量的 jar
包和它们的依赖。为了提升 Spring 项目的开发效率,简化一些配置,Spring 官方引入了 SpringBoot。当然,引入 SpringBoot 还有其他原因,在这里就不过多描述了。
而 Spring Boot 为了简化配置,提供了非常多的 Starter。它先打包好与常用模块相关的所有 JAR 包,并完成自动配置,然后组装成 Starter(如把 Web 相关的 Spring MVC、容器等打包好后组装成 spring-boot-starter-web
)。这使得在开发业务代码时不需要过多关注框架的配置,只需关注业务逻辑即可。
1.常用 Starter
Spring Boot 提供了很多开箱即用的 Starter,大概有近 50 种,常见的如下表所示。
|
|
---|---|
spring-boot-starter-web | 用于构建 Web。包含 RESTful 风格框架、SpringMVC 和默认的嵌入式容器 Tomcat |
spring-boot-starter-test | 用于测试 |
spring-boot-starter-data-jpa | 带有 Hibernate 的 Spring Data JPA |
spring-boot-starter-jdbc | 传统的 JDBC。轻量级应用可以使用,学习成本低,但最好使用 JPA 或 MyBatis |
spring-boot-starter-thymeleaf | 支持 Thymeleaf 模板 |
spring-boot-starter-mail | 支持 Java Mail、Spring Email 发送邮件 |
spring-boot-starter-integration | Spring 框架创建的一个 API,面向企业应用集成(EAI) |
spring-boot-starter-mobile | Spring MVC 的扩展,用来简化手机上的 Web 应用程序开发 |
spring-boot-starter-data-redis | 通过 Spring Data Redis、Redis Client 使用 Redis |
spring-boot-starter-validation | Bean Validation 是一个数据验证的规范,Hibernate Validator 是一个数据验证框架 |
spring-boot-starter-websocket | 相对于非持久的协议 HTTP,Websocket 是一个持久化的协议 |
spring-boot-starter-web-services | SOAP Web Services |
spring-boot-starter-hateoas | 为服务添加 HATEOAS 功能 |
spring-boot-starter-security | 用 Spring Security 进行身份验证和授权 |
spring-boot-starter-data-rest | 用 Spring Data REST 公布简单的 REST 服务 |
如果想使用 Spring 的 JPA 操作数据库,则需要在项目中添加 spring-boot-starter-data-jpa
依赖,即在 pom.xml
文件中的 <dependencies>
和元素之间加入依赖,具体代如下:
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
...
</dependencies>
如果依赖项没有版本号,则 Spring Boot 会根据自己的版本号自动关联。如果需要特定的版本则需要加上 version
元素。
2.为什么要用 Starter
在 SpringBoot 还没有出来之前,我们使用 Spring 开发项目。如果程序需要连接数据库,我们一般会使用 Hibernate 或 Mybatis 等 ORM 框架,这里我以 Mybatis 为例,具体的操作步骤如下:
- 到 Maven 仓库去找需要引入的
mybatis
的jar
包,选取合适的版本。 - 到 Maven 仓库去找
mybatis-spring
整合的jar
包,选取合适的版本。 - 在 Spring 的
applicationContext.xml
文件中配置dataSource
和mybatis
相关信息。
当然有些朋友可能会指正,不是还需要引入数据库驱动包吗?
确实需要引入,但数据库驱动有很多,比如:mysql
、oracle
、sqlserver
,这不属于 mybatis
的范畴,使用者可以根据项目的实际情况单独引入。
如果程序只是需要连接数据库这一个功能还好,按上面的步骤做基本可以满足需求。但是,连接数据库可能只是庞大的项目体系中一个环节,实际项目中往往更复杂,需要引入更多的功能,比如:连接 redis
、连接 mongodb
、使用 rocketmq
、使用 excel
功能等等。
引入这些功能的话,需要再把上面的步骤再重复一次,工作量无形当中增加了不少,而且有很多重复的工作。
另外,还是有个问题,每次到要到 Maven 中找合适的版本,如果哪次找的 mybatis.jar
包和 mybatis-spring.jar
包版本不兼容,程序不是会出现问题?
SpringBoot 为了解决以上两个问题引入了 Starter 机制。
3.Starter 有哪些要素
我们首先一起看看 mybatis-spring-boot-starter.jar
是如何定义的。
可以看到它的 META-INF
目录下只包含了:
pom.protperties
:配置 Maven 所需的项目version
、groupId
和artifactId
。pom.xml
:配置所依赖的jar
包。MANIFEST.MF
:这个文件描述了该jar
文件的很多信息。spring.provides
:配置所依赖的artifactId
,给 IDE 使用的,没有其他的作用。
注意一下,没有一行代码。
我们重点看一下 pom.xml
,因为这个 jar
包里面除了这个没有啥重要的信息。
<?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>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>
从上面可以看出,pom.xml
文件中会引入一些 jar
包,其中除了引入 spring-boot-starter
,之外重点看一下:mybatis-spring-boot-autoconfigure
。
我们找到 mybatis-spring-boot-autoconfigure.jar
文件,打开这个文件。
里面包含如下文件:
pom.properties
:配置 Maven 所需的项目version
、groupId
和artifactId
pom.xml
:配置所依赖的jar
包additional-spring-configuration-metadata.json
:手动添加 IDE 提示功能MANIFEST.MF
:这个文件描述了该jar
文件的很多信息spring.factories
:SPI(Service Provider Interface)会读取的文件spring-configuration-metadata.json
:系统自动生成的 IDE 提示功能ConfigurationCustomizer
:自定义 Configuration 回调接口MybatisAutoConfiguration
:mybatis
配置类MybatisProperties
:mybatis
属性类SpringBootVFS
:扫描嵌套的jar
包中的类
spring-configuration-metadata.json
和 additional-spring-configuration-metadata.json
的功能差不多,我们在 applicationContext.properties
文件中输入 spring
时,会自动出现下面的配置信息可供选择,就是这个功能了。
这两个文件有什么区别?
如果 pom.xml
中引入了 spring-boot-configuration-processor
包,则会自动生成 spring-configuration-metadata.json
。
如果需要手动修改里面的元数据,则可以在 additional-spring-configuration-metadata.json
中编辑,最终两个文件中的元数据会合并到一起。
MybatisProperties
类是 属性实体类:
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
private String configLocation;
private String[] mapperLocations;
private String typeAliasesPackage;
private String typeHandlersPackage;
private boolean checkConfigLocation = false;
private ExecutorType executorType;
private Properties configurationProperties;
@NestedConfigurationProperty
private Configuration configuration;
public String getConfigLocation() {
return this.configLocation;
}
public void setConfigLocation(String configLocation) {
this.configLocation = configLocation;
}
@Deprecated
public String getConfig() {
return this.configLocation;
}
@Deprecated
public void setConfig(String config) {
this.configLocation = config;
}
public String[] getMapperLocations() {
return this.mapperLocations;
}
public void setMapperLocations(String[] mapperLocations) {
this.mapperLocations = mapperLocations;
}
public String getTypeHandlersPackage() {
return this.typeHandlersPackage;
}
public void setTypeHandlersPackage(String typeHandlersPackage) {
this.typeHandlersPackage = typeHandlersPackage;
}
public String getTypeAliasesPackage() {
return this.typeAliasesPackage;
}
public void setTypeAliasesPackage(String typeAliasesPackage) {
this.typeAliasesPackage = typeAliasesPackage;
}
public boolean isCheckConfigLocation() {
return this.checkConfigLocation;
}
public void setCheckConfigLocation(boolean checkConfigLocation) {
this.checkConfigLocation = checkConfigLocation;
}
public ExecutorType getExecutorType() {
return this.executorType;
}
public void setExecutorType(ExecutorType executorType) {
this.executorType = executorType;
}
public Properties getConfigurationProperties() {
return configurationProperties;
}
public void setConfigurationProperties(Properties configurationProperties) {
this.configurationProperties = configurationProperties;
}
public Configuration getConfiguration() {
return configuration;
}
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
}
public Resource[] resolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<Resource> resources = new ArrayList<Resource>();
if (this.mapperLocations != null) {
for (String mapperLocation : this.mapperLocations) {
try {
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
// ignore
}
}
}
return resources.toArray(new Resource[resources.size()]);
}
}
可以看到 Mybatis 初始化所需要的很多属性都在这里,相当于一个 JavaBean。
下面重点看一下 MybatisAutoConfiguration
的代码:
@org.springframework.context.annotation.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 = interceptorsProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = databaseIdProvider.getIfAvailable();
this.configurationCustomizers = 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()));
}
Configuration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new Configuration();
}
if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
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();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
public static class AutoConfiguredMapperScannerRegistrar
implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
try {
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
for (String pkg : packages) {
logger.debug("Using auto-configuration base package '{}'", pkg);
}
}
scanner.setAnnotationClass(Mapper.class);
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(packages));
} catch (IllegalStateException ex) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex);
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
@org.springframework.context.annotation.Configuration
@Import({ AutoConfiguredMapperScannerRegistrar.class })
@ConditionalOnMissingBean(MapperFactoryBean.class)
public static class MapperScannerRegistrarNotFoundConfiguration {
@PostConstruct
public void afterPropertiesSet() {
logger.debug("No {} found.", MapperFactoryBean.class.getName());
}
}
}
这个类就是一个 Configuration(配置类),它里面定义很多 Bean,其中最重要的就是 SqlSessionFactory
的 Bean 实例,该实例是 Mybatis 的核心功能,用它创建 SqlSession,对数据库进行 CRUD 操作。
除此之外,MybatisAutoConfiguration
类还包含了:
- @ConditionalOnClass 配置了只有包含
SqlSessionFactory.class
和SqlSessionFactoryBean.class
,该配置类才生效。 - @ConditionalOnBean 配置了只有包含
dataSource
实例时,该配置类才生效。 - @EnableConfigurationProperties 该注解会自动填充
MybatisProperties
实例中的属性。 - @AutoConfigureAfter 配置了该配置类在
DataSourceAutoConfiguration
类之后自动配置。
这些注解都是一些辅助功能,决定 Configuration 是否生效,当然这些注解不是必须的。
接下来,重点看看 spring.factories
文件有啥内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
里面只有一行配置,即 Key 为 EnableAutoConfiguration
,Value 为 MybatisAutoConfiguration
。
好了,介绍了这么多东西,现在我们来总结一下,
Starter 几个要素如下图所示:
那么,编写 Starter 需要哪些步骤?
- 需要定义一个名称为
xxx-spring-boot-starter
的空项目,里面不包含任何代码,可以有pom.xml
和pom.properties
文件。 pom.xml
文件中包含了名称为xxx-spring-boot-autoconfigure
的项目。xxx-spring-boot-autoconfigure
项目中包含了名称为xxxAutoConfiguration
的类,该类可以定义一些 Bean 实例。当然,Configuration 类上可以打一些如:ConditionalOnClass
、ConditionalOnBean
、EnableConfigurationProperties
等注解。- 需要在
spring.factories
文件中增加 Key 为EnableAutoConfiguration
,Value 为xxxAutoConfiguration
。