通常搭建一个基于spring的web应用,我们需要做以下工作:
- pom文件中引入相关的jar包,包括spring、spring mvc、redis、mybatis、log4j、mysql-connector-java等相关的jar
- 配置web.xml,listenter配置、Filter配置、Servlet配置、log4j配置、error配置
- 配置数据库连接、配置spring事务
- 配置视图解析器
- 开启注解、自动扫描功能
- 配置完成后部署tomcat、启动调试
springBoot拥有很多方便使用的starter,spring提供的stater命名规范spring-boot-stater-xxx.jar,第三方提供的starter命名规范xxx-spring-boot-stater.jar。比如spring-boot-starter-log4j、mybatis-spring-boot-starter.jar等,各自代表了一个相对完整的功能模块。
以mybatis-spring-boot-starter为例,我们分析一下starter做了哪些操作
可以看到在mybatis-spring-boot-starter中,并没有任何的源码,只有一个pom文件,它的作用就是帮我们引入mybatis-spring-boot-autoconfigure这个包
@Configuration+@Bean
@Configuration
/**
要完成Mybatis的自动配置,需要在类路径中存在SqlsessionFactory.class、SqlSessionFactoryBean.class这两个类,
*/
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisAutoConfiguration {
private static Log log = LogFactory.getLog(MybatisAutoConfiguration.class);
@Autowired
private MybatisProperties properties;
@Autowired(
required = false
)
private Interceptor[] interceptors;
@Autowired
private ResourceLoader resourceLoader = new DefaultResourceLoader();
@Autowired(
required = false
)
private DatabaseIdProvider databaseIdProvider;
public MybatisAutoConfiguration() {
}
@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()));
}
factory.setConfiguration(this.properties.getConfiguration());
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 @Bean一起使用就可以创建一个基于java代码的配置类,用来代替相应的xml配置文件。MybatisAutoConfiguration自动帮我们生成了SqlSessionFactory这些Mybatis的重要实例并交给spring容器管理,从而完成bean的自动注册。
从MybatisAutoConfiguration这个类中使用的注解可以看出,要完成自动配置是有以来条件的。
1.@Conditional
这些是springboot特有的,常见的条件依赖注解有:
- @ConditionalOnBean,仅在当前上下文中存在某个bean时,才会实例化这个bean
- @ConditionalClass,某个class位于类路径上,才会实例化这个bean
- @ConditionalOnExperssion,当表达式为true时,才会实例化这个Bean
- @ConditionalOnMissingBean,仅当当前上下文不存在某个bean时,才会实例化这个Bean
- @ConditionalOnMissingClass,某个class在类路径上不存在的时候,才会实例化这个Bean
- @ConditionalOnNotWebApplication,不是web应用时才会实例化这个Bean
- @AutoConfigureAfter,在某个bean完成自动配置后实例化这个bean
- @AutoConfigureBefore,在某个bean完成自动配置前实例化这个bean
2.bean参数的获取
- @EnableConfigurationProperties注解的作用是使@ConfigurationProperties注解生效
- @ConfigurationProperties 注解的作用是把yml或者properties配置文件转换为bean
3.bean的加载
springboot默认扫描启动类所在的包下的主类与子类的所有组件,但并没有包括依赖包中的类,那么依赖包中的bean是如何被发现和加载的?
在springboot项目中,启动类通常都有@SpringBootApplication注解,打开源码我们可以看到@SpringBootApplication注解中又包含
- @SpringBootConfiguration == @Configuration,标识被注解的类将成为一个bean的配置类
- @ComponentScan 扫描并加载符合条件的组件,比如@Controller @Service等
- @EnableAutoConfiguration 这个注解借助@Import的支持,收集和注册依赖包中相关的bean定义
我们重点了解下@EnableAutoConfiguration这个注解:
虽然根据说明我们应该看EnableAutoConfigurationImportSelector。但是该类在SpringBoot 1.5.x版本已经过时了。因此我们来看下它的父类AutoConfigurationImportSelector
AutoConfigurationImportSelector首先实现了DeferredImportSelector接口,这个接口继承了ImportSelector。ImportSelector接口主要是为了导入@Configuration的配置项,而DeferredImportSelector是延期导入,当所有的@Configuration都处理后才会执行。我们主要看下AutoConfigurationImportSelector的selectImports方法:
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final String[] NO_IMPORTS = new String[0];
private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class);
private ConfigurableListableBeanFactory beanFactory;
private Environment environment;
private ClassLoader beanClassLoader;
private ResourceLoader resourceLoader;
public AutoConfigurationImportSelector() {
}
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
try {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
configurations = this.sort(configurations, autoConfigurationMetadata);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return (String[])configurations.toArray(new String[configurations.size()]);
} catch (IOException var6) {
throw new IllegalStateException(var6);
}
}
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}
/**
* 可以看到该方法会使用SpringFactoryLoader,它会读取META-INF/spring.factories
* 文件里所配置的EnableAutoConfiguration。经过exclude和filter等
* 操作,最终确定要装配的类
**/
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
ArrayList result = new ArrayList();
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName); result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
} catch (IOException var8) {
throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + "META-INF/spring.factories" + "]", var8);
}
}
经过上述的一系列步骤,我们可以总结如下:
自动配置的关键几步,以及相应的注解如下:
- @Configuration与@Bean -》基于java代码的bean配置
- @Conditional -》设置自动配置条件依赖
- @EnableConfigurationProperties 与@ConfigurationProperties 读取配置文件转换为bean
- @EnableAutoConfiguration、@AutoConfiguration Package 、@Import实现bean的发现与加载
让一个普通类交给spring 容器管理,通常有以下方法
- 使用@Configuration与@Bean注解
- 使用@Controller、@Service、@Repository
接下来,按照上述的步骤我们自己实现一个简单的starter,实现在引用此starter后,项目启动时,打印日志即可。
4.自定义starter
4.1lara-server-spring-boot-starter项目
4.1.1.引入依赖
注解中的注意事项,请务必注意,已经踩过的坑,大家就不要重复踩了~~~
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 添加这个很重要,否则打出来的jar包会被包一层BOOT-INF的文件夹,导致引用starter的项目无法找到class文件 -->
<configuration> <skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
4.1.2. LaraServerProperties
package com.lara.server.springboot.starter.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author lara
* @date 2020/2/25 10:41
*/
//读取前缀为lara.server的配置项
@ConfigurationProperties(prefix = "lara.server")
public class LaraServerProperties {
private Integer port;
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
}
4.1.3.LaraServerAutoConfiguration
package com.lara.server.springboot.starter.autoconfigure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lara
* @date 2020/2/25 10:45
*/
@Configuration
@EnableConfigurationProperties(LaraServerProperties.class)
public class LaraServerAutoConfiguration {
private Logger logger = LoggerFactory.getLogger(LaraServerAutoConfiguration.class);
@Bean
public String testBean(LaraServerProperties laraServerProperties){
logger.info("启动自己注册的starter,端口为:{}",laraServerProperties.getPort());
return laraServerProperties.getPort().toString();
}
}
4.1.4.spring.factories
在resource下创建META-INF/spring.factories,添加自动化配置类为LaraServerAutoConfiguration
至此,我们已经完成了starter项目,使用maven的install,将项目的jar上传至自己本地的仓库中,在spring-boot-config项目中引用,进行测试。
5.spring-boot-config项目
spring-boot-config项目添加依赖
<!--引入自定的starter-->
<dependency>
<groupId>com.lara</groupId>
<artifactId>lara-se
rver-spring-boot-starter</artifactId>
<version>1.0.1-SNAPSHOT</version>
</dependency>
在配置文件中添加:
lara.server.port=8081
进行测试:
控制台打印出了我们的日志,说明bean已经被自动注入了。