当前位置 博文首页 > 努力充实,远方可期:【springboot】5、自动配置
servlet容器要遵循servlet规范。如tomcat、netty
jdbc的接口Driver
,在用MySQL的时候,要有Driver的实现类。初始化驱动的时候,class.forName()
会加载驱动,mysql的话实现类是com.mysql.Driver
。他在工厂中 把接口作为文件名,里面写上实现类,tomcat就会读这个文件,这个是servlet规范
META-INF/services
META-INF/services/javax.servlet.ServletContainerInitializer
。DispatcherServlet是servletMETA-INF/spring.factories
SPI的全名为Service Provider Interface
。大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。
简单的总结下java SPI机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
Java SPI 规范
要使用Java SPI,需要遵循如下约定:
META-INF/services
目录下创建一个以“接口全路径名”为命名的文件,内容为实现类的全限定名;java.util.ServiceLoder
动态装载实现模块,它通过扫描META-INF/services
目录下的配置文件找到实现类的全限定名,把类加载到JVM;// 对比下java-spi的源码 // Service.load(接口.class)
public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";
在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories
文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。
com.xxx.interface=com.xxx.A,com.xxx.B // 多个实现类可以用,分隔
// 在Spring Boot的很多包中都能够找到spring.factories文件
这种自定义的SPI机制是Spring Boot Starter实现的基础。
但是SpringFactoriesLoader.java并不是springboot的内容,而是spring的内容
Spring Factories实现原理:
spring-core包里定义了SpringFactoriesLoader
类,这个类实现了检索META-INF/spring.factories
文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:
在Spring Boot中,使用的最多的就是starter。starter可以理解为一个可拔插式的插件,例如,你想使用JDBC插件,那么可以使用spring-boot-starter-jdbc;如果想使用MongoDB,可以使用spring-boot-starter-data-mongodb。
初学的同学可能会说:如果我要使用MongoDB,我直接引入驱动jar包就行了,何必要引入starter包?starter和普通jar包的区别在于,它能够实现自动配置,和Spring Boot无缝衔接,从而节省我们大量开发时间。
@EnableAutoConfiguration自动配置:从classpath中搜索所有META-INF/spring.factories配置文件,并将其中org.springframework.boot.aotoconfigure.EnableAutoConfiguration对应的配置项通过反射实例化为对应的标注了@Configuration的配置类,然后汇总为一个并加载到IOC容器。
配置文件到底能写什么?怎么写?自动配置原理;
配置文件能配置的属性参照https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#common-application-properties
1、程序从main方法开始运行
2、使用SpringApplication.run()加载主程序类
3、主程序类需要标注@SpringBootApplication
@SpringBootApplication //标注在主程序类上,表明是一个springboot应用
public class HelloWorldMainApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldMainApplication.class,args);
}
}
SpringApplication.run(HelloWorldMainApplication.class,args);
//重载为
SpringApplication.run(new Class<?>[] { HelloWorldMainApplication.class }, args);
//再重载为
new SpringApplication(HelloWorldMainApplication.class).run(args);
/* 构建 SpringApplication 并运行,创建并且刷新一个新的 ApplicationContext */
加载spring.factories文件形参一个map
先实例化ApplicationContextInitializer、ApplicationListener的实现类,然后赋值给 SpringApplication属性
// 构造器
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}//重载构造器
public SpringApplication(ResourceLoader resourceLoader,
Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断是否能够成功加载一些关键的类来确认 web 应用类型,这个类型后面会用到
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 关注下面两行
/* 获取并设置 Spring 上下文初始化器。先调用getSpringFactoriesInstances()
拿到工厂instances后赋值给SpringApplication.initializers
注意这里只是从spring.factories中拿到ApplicationContextInitializer这个接口的kv对
*/
// 设置初始化器 // set之前先get,get的时候就实例化了
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// List<ApplicationContextInitializer<?>> initializers;
// 设置容器的监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// List<ApplicationListener<?>> listeners;
// 追述到应用主类,也就是 main 方法所在的类
this.mainApplicationClass = deduceMainApplicationClass();
}
// 先简单看下set的部分,我们可以拿到就是拿到之后赋值给属性而已,而如何拿到的,我们要看getSpringFactoriesInstances()
public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) {
this.initializers = new ArrayList<>(initializers);
}
public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {
this.listeners = new ArrayList<>(listeners);
}
实例化spring.factories中的对象
我们要实例化其中的对象,首先得加载文件的内容,然后再根据文件的内容调用构造器实例化
这也是SPI思想。
要提醒一下,其实不是在这里进行自动配置的,这是我们在这里提前了解一下这个方法什么意思。
在new SpringApplication构造函数只是用到了ApplicationContextInitializer、ApplicationListener这两个kv对,而自动配置要到后面的run方法里用
// get , 从这就开始要联想java-spi的思想了 // 获取spring.factories里面的实例对象
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
//重载
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, //接口 ApplicationContextInitializer、ApplicationListener
Class<?>[] parameterTypes,
Object... args) {
ClassLoader classLoader = getClassLoader
// SpringFactoriesLoader用于加载spring.factories文件中的内容,返回指定接口的实现类全类名 // Set保证不重复
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 创建上面找到的类实例。 // 方法名字为工厂实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 构造完了 根据 @Orde r和 @Priority 进行排序
AnnotationAwareOrderComparator.sort(instances);// list【instances】.sort(new AnnotationAwareOrderComparator(););
return instances;
}
加载每个jar包路径下的META-INF/spring.factories文件,把里面的内容处理成map的kv对,注意该map是多值map,一个接口可以对应多个实现类
获取传入的类所对应的值,意思是说看看properties有没有以factoryType类名为key的键值对
SpringFactoriesLoader:
// 加载spring工厂,返回要文件中要注册bean的键值对,是一次性加载全部kv对
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
/* 加载工厂配置,根据传入的 factoryClass 获取工厂名称集合 */
return loadSpringFactories(classLoader).
getOrDefault(factoryClassName, // key // //getOrDefault()获取我们自定义的值或直接调用默认值
Collections.emptyList());// 第二个参数为默认值
}
// SpringFactoriesLoader.java
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 尝试直接从缓存中拿 // cache的key是类加载器 // 而result是多值map,key是接口,value是List
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) { return result; }
try {
// 加载资源, META-INF/spring.factories // //得到urls //因为每个jar包下都有类路径,我们有多个jar包,所以可能得到一个list的url
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : // "META-INF/spring.factories";
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
// 保存结果的map
result = new LinkedMultiValueMap<>();
//遍历url,因为有多个spring.factories文件
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
//把扫描到的文件的内容包装成成一个properties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
//遍历properties对象里的每个键值对拿到
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// value 用逗号分隔组成集合
List<String> factoryClassNames = Arrays.asList(
StringUtils.commaDelimitedListToStringArray((String) entry.getValue(