【JDK源码】Java中的SPI机制

1 SPI简介

SPI全称 Service Provider Interface,是Java提供的一种服务发现机制,旨在由第三方实现或扩展的API,常用于动态加载服务。Java中的SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中,这个机制尤其的重要,其核心思想就是解耦,允许应用在运行时动态加载实现类,从而提升扩展性。
SPI机制广泛应用于JDK内部(如:JDBC、JCE、JNDI)各种框架(如:Dubbo、Spring Boot、SLF4J)
Java SPI 有四个要素:

  1. SPI接口:为服务提供者实现类约定的接口或实现类。
  2. SPI实现类:实际提供服务的实现类。
  3. SPI配置:Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于META-INF/services目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
  4. ServiceLoader:Java SPI的核心类,用于加载SPI实现类,ServiceLoader中有各种实用方法来获取特定实现、迭代他们或重新加载服务。

2 SPI使用实例

2.1 SPI接口

  • 首先,需要定义一个 SPI 接口,和普通接口并没有什么差别。
package test.spi;

public interface DataStorage {
    String say(String key);
    String say();
}

2.2 SPI实现类

  • 假设,我们需要在程序中使用两种不同的数据存储——MySQL 和 Oracle。因此,我们需要两个不同的实现类去分别完成相应工作。
    • MySQL查询相关
package test.spi;

public class MysqlStorage implements DataStorage{

    public MysqlStorage(){
        System.out.println("");
    }

    @Override
    public String say(String key) {
        return "MySQL";
    }

    @Override
    public String say() {
        return "MySQL";
    }
}
  • 实现类
    • Oracle 查询相关
package test.spi;


public class OracleStorage implements DataStorage {

    public OracleStorage(){
        System.out.println("");
    }

    @Override
    public String say(String key) {
        return "Oracle";
    }

    @Override
    public String say() {
        return "Oracle";
    }
}

2.3 SPI 配置

  • 通过 Java SPI 机制来发现服务,就需要在 SPI 配置中约定好发现服务的逻辑。
  • 配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。
  • 文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。以本示例代码为例,其文件名应该为

test.spi.DataStorage

  • 文件内容:

test.spi.MysqlStorage
test.spi.OracleStorage

2.4 ServiceLoader

  • 完成了上面的步骤,就可以通过 ServiceLoader 来加载服务。示例如下:
package test.spi;

import java.util.ServiceLoader;

public class SpiDemo {
    public static void main(String[] args) {
        ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);
        System.out.println("======Java SPI 测试======");
        serviceLoader.forEach(loader ->{
            System.out.println(loader.say());
        });
    }
}

  • 输出
    在这里插入图片描述

3 SPI原理

Java SPI机制依赖于ServiceLoader类去解析、加载服务。因此,掌握了ServiceLoader的工作流程,就掌握了SPI的原理。ServiceLoader的代码本身比较精练,接下来,让我们通过走读源码的方式,逐一的理解ServiceLoader的工作流程。

3.1 ServiceLoader的成员变量

ServiceLoader类的成员变量

	public final class ServiceLoader<S> implements Iterable<S> {
    	// SPI配置文件目录
   		private static final String PREFIX = "META-INF/services/";
    
    	// The class or interface representing the service being loaded
    	// 将要被加载的SPI服务
    	private final Class<S> service;

    	// The class loader used to locate, load, and instantiate providers
    	// 用于加载SPI服务的类加载器
    	private final ClassLoader loader;

    	// The access control context taken when the ServiceLoader is created
    	// ServiceLoader 创建时的访问控制上下文
    	private final AccessControlContext acc;

    	// Cached providers, in instantiation order
	    // SPI服务缓存,按实例化的顺序排列
    	private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    	// The current lazy-lookup iterator
    	// 懒查询迭代器
   		private LazyIterator lookupIterator;
		// ...
	}

3.2 ServiceLoader 的工作流程

  1. ServiceLoader.load 静态方法:应用程序加载Java SPI服务,都是先调用ServiceLoader.load静态方法
  2. ServiceLoader.load静态方法的作用
    • 指定类加载ClassLoader和访问控制上下文;
    • 重新加载SPI服务:
      清空缓存中所有已实例化的SPI服务;
      根据ClassLoader和SPI类型,创建懒加载迭代器;
  	// servuce 传入的是期望加载的 SPI 接口类型
  	// loader 是用于加载 SPI 服务的类加载器
	public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader) {
        return new ServiceLoader<>(service, loader);
	}
  
	public void reload() {
		// 清空缓存中所有已实例化的SPI服务
        providers.clear();
        // 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器
        lookupIterator = new LazyIterator(service, loader);
	}

	// 私有构造方法
	// 重新加载 SPI 服务
	private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // 指定加载 ClassLoader 和访问控制上下文
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
      	// 然后,重新加载SPI服务
        reload();
	}
  1. 应用程序通过ServiceLoader 的 iterator方法遍历SPI实例
    • ServiceLoader 的类定义,明确了ServiceLoader类实现了 Iterable接口,所以,它是可以迭代遍历的。实际上,ServiceLoader类维护了一个缓存providers(LinkedHashMap 对象),缓存providers中保存了已经被成功加载的SPI实例,这个Map的key是SPI接口实现类的全限定名,value是该实现类的一个实例对象。
    • 当应用程序调用ServiceLoader的 iterator 方法时,ServiceLoader 会先判断缓存 providers 中是否有数据;如果有,则直接返回缓存 providers 的迭代器;如果没有,则返回懒加载迭代器的迭代器。
public Iterator<S> iterator() {
        return new Iterator<S>() {
            // 缓存 SPI providers
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            // lookupIterator 是 LazyIterator 实例,用于懒加载 SPI 实例
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }
  1. 懒加载迭代器的工作流程
    在上面的源码中可以看到, lookupIterator 是 LazyIterator 实例,而LazyIterator用于懒加载SPI实例。下面通过摘取 LazyIterator关键代码,来看LazyIterator是如何工作的。
  • hasNextService方法
    • 拼接 META-INF/services/ + SPI 接口全限定名
    • 通过类加载器,尝试加载资源文件
    • 解析资源文件中的内容,获取 SPI 接口的实现类的全限定名 nextName
	private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    // 1. 拼接 META-INF/services/ + SPI 接口全限定名
                    // 2. 通过类加载器,尝试加载资源文件
                    // 3. 解析资源文件中的内容
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }
  • nextService方法
    • haseNextService() 方法解析出SPI 实现类的全限定名 nextName,通过反射,获取 SPI 实现类的类定义Class。
    • 然后,尝试通过Class 的 newInstance 方式实例化一个SPI服务对象。如果成功,则将这个对象加入到缓存 providers 中并返回该对象。
	private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

3.3 SPI 和类加载器

  1. 通过走读ServiceLoader代码,大致的了解 Java SPI 的工作原理,即通过ClassLoader 加载 SPI配置文件,解析SPI服务,然后通过反射,实例化SPI服务实例。这里需要思考一下,为什么加载SPI服务时,需要指定类加载器 ClassLoader?
  2. 学过JVM的读者,想必都了解过类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的 BootstrapClassLoader 外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
  3. 双亲委派机制约定了:一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载
  4. 双亲委派的好处: 使得Java类伴随着它的类加载器,天然具备一种带有优先级的层次关系,从而使得类加载得到统一,不会出现重新加载的问题:
    • 系统类防止内存中出现多份同样的字节码
    • 保证Java程序安全稳定运行
    • 例如:java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 classpath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 classpath 中的 Object 使用的是应用程序类加载器。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object。
  5. 双亲委派的限制: 子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。这就导致了双亲委派模型并不能解决所有的类加载的。Java SPI 就面临着这样的问题:
    • SPI 的接口是Java核心库的一部分,是由BootstrapClassLoader加载的;
    • 而SPI实现的Java类一般是由AppClassLoader来加载的。BootstrapClassLoader 是无法找到SPI的实现类的,因为它只加载Java的核心库。它不能代理给AppClassLoader,因为它是顶级的类加载器。这也就解释了,为什么加载SPI服务时,需要指定类加载器ClassLoader,因为如果不指定的话,则无法获取SPI服务。
    • 如果不做任何设置,Java应用的线程的上下文类加载器默认时AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到SPI实现类。线程上下文加载器在很多SPI的实现中都会用到。通常可以通过Thread.currentThread.getClassLoader()Thread.currentThread().getContextClassLoader() 获取线程上下文类加载器。
    • 总结: SPI用于打破双亲委派机制,比如核心库定义的标准接口,我们实现类无法被父类加载器知道,也就是儿子能用父亲的,但是父亲用不到儿子的,所以有了SPI。而SPI又是基于ServiceLoader来进行加载的,这个类是核心类库rt.jar里面的,那么它执行的时候,如果不把儿子的类加载器给他,他还是获取不到我们的实现类,继而出现了线程上下文的类加载器也就是TCCL。
	public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

3.4 Java SPI 的不足

  • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。

4 SPI应用场景

  • SPI 在Java中应用十分的广泛,在Java 的java.util.spi.package 中就约定了很多SPI接口。例如
    • TimeZoneNameProvider :为TimeZone类提供本地化的时区名称。
    • DateFormatProvider:为指定的语言环境提供日期和时间格式。
    • NumberFormatProvider:为 NumberFormat 类提供货币,整数和百分比值。
    • Driver:从4.0版本开始,JDBC API 支持SPI模式。旧版本使用Class.forName() 方法加载驱动程序。
    • PersistenceProvider:提供JPA API的实现。
  • 下面以JDBC DriverManager 为例进行详细分析。

4.1 SPI应用案例_JDBC DriverManager

  • 在JDBC 4.0 之前,链接数据库的时候,通常会用Class.forName(XXX) 方法来加载数据库相应的驱动,然后再获取数据库链接,继而进行数据库相关的操作

Class.forName(“com.mysql.jdbc.Driver”)

  • 而JDBC4.0 之后,不再需要用Class.forName(XXX) 方法来加载数据库驱动,而是直接获取连接就可以。具体的实现方式如下:
  1. JDBC接口:首先,Java中内置了接口 java.sql.Driver。
  2. JDBC接口实现:各个数据库的驱动自行实现java.sql.Driver接口,用于管理数据库连接。
    • MySQL:在MySQL 的Java驱动包 mysql-connector-java-XXX.jar中,可以找到META-INF/services 目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver。com.mysql.cj.jdbc.Driver 正是MySQL版的java.sql.Driver实现。如下图所示:
      在这里插入图片描述
  3. 创建数据库连接
    以MySQL为例,创建数据库连接代码如下:

    final String DB_URL = String.format(“jdbc:mysql://%s:%s/%s”, DB_HOST, DB_PORT, DB_SCHEMA);
    connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);

4.1.1 DriverManager

  • 从上文中,可以看到DriverManager 是创建数据库连接的关键。它究竟是如何动作的呢?
  • 可以看到是加载实例化驱动的,接着看loadInitialDrivers方法:
private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
     
        // 通过 classloader 获取所有实现 java.sql.Driver 的驱动类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                // 利用 SPI 记载所有Driver 服务
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                // 获取迭代器
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    // 遍历迭代器
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        // 打印数据库驱动信息
        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                // 尝试实例化驱动
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

上面的代码主要步骤是:

  1. 从系统变量中获取驱动的实现类。
  2. 利用SPI来获取所有驱动的实现类。
  3. 遍历所有驱动,尝试实例化各个实现类。
  4. 根据第1步获取到的驱动列表来实例化具体的实现类。

需要关注的是下面的这行代码:

ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);

这里实际获取的是 Java.util.ServiceLoader.LazyIterator迭代器。调用其hasNext方法时,会搜索classpath下以及jar包中的META-INF/services 目录,查找java.sql.Driver文件,并找到文件中的驱动实现类的全限定名。调用其next方法时,会根据驱动类的全限定名去尝试实例化一个驱动类的对象。

4.2 SPI应用案例_Spring Boot

Spring Boot 是基于Spring 构建的框架,其设计目的在于简化Spring应用的配置、运行。在Spring Boot中,大量运用了自动装配来尽可能的减少配置。下面是一个Spring Boot示例,进行一步步的分析。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
        return String.format("Hello %s!", name);
    }
}

4.2.1 @SpringBootApplication 注解

  • 首先,Spring Boot应用的启动类上都会标记一个@SpringBootApplication 注解;
  • 除了 @Target、 @Retention、@Documented、@Inherited 这几个元注解;
  • @SpringBootApplication 注解的定义中还标记了 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 三个注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // 略
}

4.2.2 @SpringBootConfiguration 注解

  • 从@SpringBootConfiguration 注解的定义来看,@SpringBootConfiguration 注解本质上就是一个 @Configuration 注解,这意味着被@SpringBootConfiguration 注解修饰的类会被 Spring Boot 识别为一个配置类。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

4.2.3 @EnableAutoConfiguration 注解

  • @EnableAutoConfiguration 注解定义如下:
  • @EnableAutoConfiguration 注解包含了 @AutoConfigurationPackage与 @Import({AutoConfigurationImportSelector.class}) 两个注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

4.2.4 @AutoConfigurationPackage 注解

  • @AutoConfigurationPackage 会将被修饰的类作为主配置类,该类所在的 package 会被视为根路径。
  • Spring Boot 默认会自动扫描根路径下的所有 Spring Bean(被 @Component 以及继承 @Component 的各个注解所修饰的类)——这就是为什么 Spring Boot 的启动类一般要置于根路径的原因。
  • 这个功能等同于在 Spring xml 配置中通过 context:component-scan 来指定扫描路径。
  • @Import 注解的作用是向 Spring 容器中直接注入指定组件。
  • @AutoConfigurationPackage 注解中注明了@Import({Registrar.class})。Registrar 类用于保存 Spring Boot 的入口类、根路径等信息。

4.2.5 SpringFactoriesLoader.loadFactoryNames 方法

  • @Import(AutoConfigurationImportSelector.class) 表示直接注入AutoConfigurationImportSelector。
  • AutoConfigurationImportSelector 有一个核心方法getCandidateConfigurations 用于获取候选配置。该方法调用了SpringFactoriesLoader.loadFactoryNames 方法,这个方法即为 Spring Boot SPI 的关键,它负责加载所有 META-INF/spring.factories 文件,加载的过程由 SpringFactoriesLoader 负责。
  • Spring Boot 的 META-INF/spring.factories 文件本质上就是一个 properties 文件,数据内容就是一个个键值对。
  • SpringFactoriesLoader.loadFactoryNames 方法的关键源码:
// spring.factories 文件的格式为:key=value1,value2,value3
// 遍历所有 META-INF/spring.factories 文件
// 解析文件,获得 key=factoryClass 的类名称
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  String factoryTypeName = factoryType.getName();
  return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  // 尝试获取缓存,如果缓存中有数据,直接返回
  MultiValueMap<String, String> result = cache.get(classLoader);
  if (result != null) {
    return result;
  }

  try {
    // 获取资源文件路径
    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 properties = PropertiesLoaderUtils.loadProperties(resource);
      // 遍历解析出的 properties,组装数据
      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);
  }
}
  • 总结:
    • 上线的方法主要加载所有META-INF/spring.factories文件,加载过程有 SpringFactoriesLoader 负责
    • 在CLASSPATH 中搜索所有 META-INF/spring/factories配置文件
    • 然后,解析spring.factories 文件,获取指定自动配置类的全限定名

4.2.6 Spring Boot 的 AutoConfiguration 类

  • Spring Boot 有各种 starter 包,可以根据实际项目需要,按需取材。在项目开发中,只要将 starter 包引入,我们就可以用很少的配置,甚至什么都不配置,即可获取相关的能力。通过前面的 Spring Boot SPI 流程,只完成了自动装配工作的一半,剩下的工作如何处理呢 ?
  • 以 spring-boot-starter-web 的 jar 包为例,查看其 maven pom,可以看到,它依赖于 spring-boot-starter,所有 Spring Boot 官方 starter 包都会依赖于这个 jar 包。而 spring-boot-starter 又依赖于 spring-boot-autoconfigure,Spring Boot 的自动装配秘密,就在于这个 jar 包。
  • 从 spring-boot-autoconfigure 包的结构来看,它有一个 META-INF/spring.factories ,显然利用了 Spring Boot SPI,来自动装配其中的配置类。
    在这里插入图片描述
  • 下图是 spring-boot-autoconfigure 的 META-INF/spring.factories 文件的部分内容,可以看到其中注册了一长串会被自动加载的 AutoConfiguration 类。
    在这里插入图片描述
  • 以 RedisAutoConfiguration 为例,这个配置类中,会根据 @ConditionalXXX 中的条件去决定是否实例化对应的 Bean,实例化 Bean 所依赖的重要参数则通过 RedisProperties 传入。
    在这里插入图片描述
  • RedisProperties 中维护了 Redis 连接所需要的关键属性,只要在 yml 或 properties 配置文件中,指定 spring.redis 开头的属性,都会被自动装载到 RedisProperties 实例中。
    在这里插入图片描述
    通过以上分析,已经一步步解读出了SpringBoot自动装配的原理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挖土机-挖挖挖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值