当前位置 博文首页 > 努力充实,远方可期:【springboot】5、自动配置

    努力充实,远方可期:【springboot】5、自动配置

    作者:[db:作者] 时间:2021-08-11 09:57

    servlet容器要遵循servlet规范。如tomcat、netty

    jdbc的接口Driver,在用MySQL的时候,要有Driver的实现类。初始化驱动的时候,class.forName()会加载驱动,mysql的话实现类是com.mysql.Driver。他在工厂中 把接口作为文件名,里面写上实现类,tomcat就会读这个文件,这个是servlet规范

    • java的文件路径是META-INF/services
    • tomcat 的文件路径是META-INF/services/javax.servlet.ServletContainerInitializer。DispatcherServlet是servlet
    • springboot的文件路径是META-INF/spring.factories

    一、SPI与SpringFactories

    1 java的SPI机制

    SPI的全名为Service Provider Interface。大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。

    简单的总结下java SPI机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

    java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

    Java SPI 规范

    要使用Java SPI,需要遵循如下约定:

    • 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全路径名”为命名的文件,内容为实现类的全限定名;
    • 2、接口实现类所在的jar包放在主程序的classpath中;
    • 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
    • 4、SPI的实现类必须携带一个无参构造方法
    // 对比下java-spi的源码 // Service.load(接口.class)
    public final class ServiceLoader<S> implements Iterable<S>{
    
        private static final String PREFIX = "META-INF/services/";
    

    2 Spring Boot中的SPI机制:SpringFactories

    在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

    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 */
    

    2 new SpringApplication

    加载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);
    }
    

    2.1 getSpringFactoriesInstances

    实例化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;
    }
    
    
    
    2.1.1 loadFactoryNames

    加载每个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(