在很多大厂中项目中,项目中大部分都是使用自己封装的架构,比如我目前所在的平安(外包),就是对Spring在进行了二次封装,它们把它称之为ark架构。比如我们常常需要用到的一些工具类或者可以共用的类,其它组里也需要用到,那我们就可以在使用统一框架的前提下倒入我们自己的starter,来达到一个自动化配置的效果。
SpringBoot介绍
那我们就以目前流行的SpringBoot中举例,如何在项目中倒入自己自定义的一些jar包吧。首先在此之前,必须对SpringBoot有一定了解。它是基于Spring4的条件注册的一套快速开发整合包,同时又整合了Spring MVC了,所以说SpringMVC的那一套注解可以原封不动地搬来用。同时为了解决Spring框架需要进行大量的配置的问题又引入自动配置的概念,也就是说能用注解我绝不用配置文件。
在构建自己的boot项目,需要在pom或者garden中引入自己的相关包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
解释一下几个关键的包
spring-boot-starter-parent:项目可以通过继承spring-boot-starter-parent包来获得一些合理的默认配置,在在dependencies里的部分配置可以不用填写version信息,自动继承parent包的版本,当然也可以不用
spring-boot-starter:这是Spring Boot的核心启动器,包含了自动配置、日志和yaml/yml/properties
spring-boot-starter-web:构建Web,包含RESTful风格框架SpringMVC和默认的嵌入式容器Tomcat,就是这个包整合了Spring mvc,同时自带嵌入式tomcat意味着我们启动项目时就只要运行main方法就行,不用再跑eclipse上自带的tomcat了
在我们启动项目的时候,有个关键的注解@SpringBootApplication,control键然后点击该注解看源码可知它替代了@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan这三个注解的功能。接下来解释这个三个注解的作用,理解了这三个注解,自然理解了@SpringBootApplication。
@SpringBootConfiguration:该注解继承自@Configuration,一般与@Bean配合使用,使用这两个注解就可以创建一个简单的spring配置类,可以用来替代相应的xml配置文件,所以我们在一些配置文件中是如此使用的,比如我们要倒入kafka创建topic如此。
@EnableAutoConfiguration:该注解的意思就是Springboot可以根据你添加的jar包来配置你项目的默认配置,比如当你添加了mvc的jar包,它就会自动配置web项目所需的配置。
@ComponentScan:顾名思义该注解是用来扫描组件的,只要组件上有@component及其子注解@Service、@Repository、@Controller等,springboot会自动扫描到并纳入Spring 容器进行管理,有点类似xml文件中的<context:component-scan>,该注解不填属性的话就是默认扫描启动类所在的包,或者启动类所在包的下一级,所以启动类要放在最外层。比如说你想注入一个service类交给Spring容器管理,但是你这个类的路径不再项目启动类路径之下,是不会被扫描到的,自然就不会被加载到Spring容器。
yml文件引出自动配置
在我们使用yml文件中,我们可以发现只要你引入了相应的jar包,比如mybatis,当我们在用yml对mybatis属性进行配置的时候,比如只要我们输入以“mybatis”前缀开头的内容,就会出现各种提示,如下。很奇怪,究竟是怎么做到的呢?
层层揭秘:
1. 进入我们的pom文件,左键点击我们引入的mybatis包,会发现里面引入了mybatis-spring-boot-autoconfigure这个包
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
2.然后打开mybatis-spring-boot-autoconfigure 包,在META-INF文件夹中找到spring.factories和spring-configuration-metadata.json。首先我们查看spring.factories文件,查看它后发现它引入的正是下面Java文件中的一个MybatisAutoConfiguration。有一定sprig基础的都知道在我们众多Spring jar包中,并不是该jar包下搜有的Java类都会引入spring ioc容器,而是根据我们的spring.factories的配置情况来自动引入对应的class文件,这也是Springboot加载过程中的一步,根据jar包下的meta-inf文件夹下的spring.factories配置文件,来自动引入对应的类。
3.当我们在打开 spring-configuration-metadata.json文件时则又会发现其实yml/properties文件中所有的提示都是来自于此元信息,我这里只截取部分,正好对应上面mybatis的提示(其实就是MybatisProperties类中的一个属性而已,它们把所有的配置类会生成json文件,而json文件的属性正是我们配置文件的各种属性)
4.通过上面我们大概就可以猜出来,就拿mybatis来说应该是引入一个jar包之后,Springboot 在启动时候,它会扫描启动类路径下所有的注解类和jar包下带有spring.factories文件所指向的自动配置类,当然并不是指向了对应类它就一定会加载到ioc容器中,这个的根据该类上面的条件注解,如下图(截取了常用的一些注解)。
MybatisAutoConfiguration源码阅读与发现
既然我们要弄懂其究竟是如何加载进去的,我们则继续探究,我们这里就以mybatis jar包下的MybatisAutoConfiguration类作为入手,我们打开其源码发现。首先它是一个配置类,引入MybatisAutoConfiguration类到ioc容器的前提条件是容器Map<String,Class<?>>中已经加载了SqlSessionFactory和sqlSessionDactoryBean。其次是容器中必须包含DataSource的实例对象才会生效,其实说白了,你要写sql你不可能不把数据库的连接信息配置上吧。再其次是后面会去加载MybatisProperties这个类,而这个类的属性来自于yml/properties配置文件。再其次是要想让MybatisAutoConfiguration这个类生效必须是加载完DataSourceAutoConfiguration这个类才会。ok至此,我们已经知道了MybatisAutoConfiguration这个类生效的前提条件。
@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);
//省略 都是设置SqlSessionFactory的一些属性
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);
}
//省略
}
数据源的配置是通过类DataSourceAutoConfiguration.class,而在这个类生效的条件是通过@EnableConfigurationProperties(DataSourceProperties.class)这个注解就是去加载配置文件的一些配置,然后通过配置文件的属性赋予到DataSourceProperties这个类,而配置文件的一些属性啊,无非就是用户名、密码、数据库驱动、数据库连接池等等,从而达到自动配置的目的,那这些属性又是如何自动配置的。
DataSourceAutoConfiguration源码阅读与发现
打开DataSourceAutoConfiguration这个类的源码发现,它默认是引入了5个数据源的,也就是DataSource.class 的实例对象。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
//省略
}
然后我们点击进入DataSourceConfiguration类来观察其关系,其中里面也写的很清楚,引入各类,当你在配置文件中配置的属性spring.datasource.type与value值是否相同,相同则去引入对应的数据库连接池,否则为默认的数据库连接池为HikariPool,至于为什么不是Generic,相比其它连接池子有何优势,另外再去说,这里先记住。
abstract class DataSourceConfiguration {
@SuppressWarnings("unchecked")
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
/**
* Tomcat Pool DataSource configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource",
matchIfMissing = true)
static class Tomcat {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.tomcat")
org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties) {
org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(properties,
org.apache.tomcat.jdbc.pool.DataSource.class);
DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl());
String validationQuery = databaseDriver.getValidationQuery();
if (validationQuery != null) {
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery(validationQuery);
}
return dataSource;
}
}
/**
* Hikari DataSource configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
/**
* DBCP DataSource configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(org.apache.commons.dbcp2.BasicDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.commons.dbcp2.BasicDataSource",
matchIfMissing = true)
static class Dbcp2 {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.dbcp2")
org.apache.commons.dbcp2.BasicDataSource dataSource(DataSourceProperties properties) {
return createDataSource(properties, org.apache.commons.dbcp2.BasicDataSource.class);
}
}
/**
* Generic DataSource configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type")
static class Generic {
@Bean
DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
}
SpringFactoriesLoader类部分源码:主要是项目启动过程中去加载资源到ioc容器中
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
//在此路径下去加载 META-INF/spring.factories 所有的文件到ioc容器
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
终极奥义:自定义starter通用组件,供项目使用
现在我们已经知道starter实际上就是一个集成集合器,主要完成两件事:1.引入相关jar 2.自动配置,那我们如何给我们的项目中使用一个自starter组件呢?这样以后在我们的项目中也可以是配置显得更加灵活和降低耦合,这其实也就是Springcloud的一些核心概念。那我们就来构建一个超级简单的demo starter组件。
1.构建一个简单的maven项目,我这里需要一个女朋友所以就创建了一个女朋友,而这个girl正是我们在项目启动时候注入到ioc容器中去的实例
package com.dn.study.girl;
public class Girl {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Girl{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
2.构建这个女孩的properties,提供给配置文件来注入这个girl的属性,我这里以com.girl为前缀
package com.dn.study.girl;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "com.girl")
public class GirlProperties {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "GirlProperties{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
3.构建项目自启动来实例话这个bean的配置类
package com.dn.study.girl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(GirlProperties.class)
public class GirlAutoConfigure {
@Autowired
private GirlProperties girlProperties;
@Bean
@ConditionalOnMissingBean(Girl.class)
public Girl getGirl(){
Girl girl = new Girl();
girl.setAge(girlProperties.getAge());
girl.setName(girlProperties.getName());
return girl;
}
}
4.配置发现配置文件,要提供给Spring发现注册
5.编译打成jar包
mvn install:install-file -Dfile=/Users/humingming/Desktop/myStarter/target/girl-spring-boot-starter.jar -DgroupId=com.lcloud -DartifactId=girl-spring-boot-starter -Dversion=1.0-SNAPSHOT -Dpackaging=jar
6.启动我们的Springboot项目中倒入我们的starter组件,然后我们就可以在yml配置文件中输入我们刚才输入的那个girl的配置信息了
<!-- 倒入自定义的starter -->
<dependency>
<groupId>com.lcloud</groupId>
<artifactId>girl-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
7. 启动项目,并观察girl是否被注入到容器内
@RequestMapping("/test")
@RestController
public class Demo {
@Autowired
private Girl girl;
@GetMapping("test")
public void fun(){
System.out.println("注入的类" + girl);
}
}
总结:
ok,satrter组件制作完成,这对后面的Springcloud组件的学习至关重要,keep on!