为什么SpringBoot什么都不需要配置就可以运行起来

2022-04-30  本文已影响0人  Java码农

写在前面

SpringBoot毋庸置疑是目前流行度最高的框架。而这原因,即是大家最清楚的0配置化,不需要配置便可以启动一个tomcat服务。

我相信,只要你用过Spring Boot,就会对这样一个现象非常的好奇:

引入一个组件依赖,加个配置,这个组件就生效了。

为了故事的发展,我们举个例子来说,比如我们常用的Redis, 在Spring Boot中就是这样使用的

引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置yml文件

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456

这就配置OK了,可以直接引用了,神奇嘛

@Autowired
private RedisTemplate redisTemplate;

就这样,两步搞定,中间没有做任何事情。这里面我们应该提出几个问题。

第一,pom文件redis是在哪里,怎么连版本号都没有,reids版本是什么

第二,RedisTemplate 是怎么交给Spring注入管理的,怎么就能用了呢。

不知道你们刚接触Boot的时候,有没有听说过约定大于配置。我们要明白一个道理,事情总要有人做,自己不做,肯定是别人帮着做了,我们带着这些疑问去深入看源码。

SPI

先不着急,讲流程源码之前,我们先来学习下SPI,这个非常重要,Boot里面大都是靠这个机制在做事情。上一篇只是简单提了下,今天发现绕不开了。

SPI ,全称为 Service Provider Interface(服务提供者接口),是一种服务发现机制。它通过在classpath路径下的META-INF/services文件夹查找文件,自动加载文件中所定义的类。

为了故事的继续发展 我们建个工程

我们用医生给我们做核酸的场景:

spi-doctor 为服务提供方,可以理解为我们的框架

spi-person 为使用方,因为我的服务提供接口叫PlayHeSuan(做核酸),所以所有实现都是人~

我们看下代码

在spi-doctor模块中定义接口PlayHeSuan

/**
 * 服务提供者,做核酸
 * @Date 2022/4/29 5:50 下午
 * @Author shushi
 */
public interface PlayHeSuan {
    //做核酸
    void playHeSuan();
}

在spi-person模块中定义两个实现类ZhangSan,Lisi

/**
 * 李四做核酸
 * @Date 2022/4/29 5:57 下午
 * @Author shushi
 */
public class Lisi implements PlayHeSuan {

    @Override
    public void playHeSuan() {
        System.out.println("李四做核酸~~~~");
    }
}
/**
 * 张三做核酸
 * @Date 2022/4/29 5:56 下午
 * @Author shushi
 */
public class ZhangSan implements PlayHeSuan {

    @Override
    public void playHeSuan() {
        System.out.println("张三做核酸 ~~~~");
    }
}

编写配置文件

新建文件夹META-INF/services

在文件夹下新建文件cn.shushi.spi.doctor.PlayHeSuan

这里要特别说明 建配置文件一定要直接写 META-INF/services,不能直接写META-INF.services,否则会无效

文件里面写实现类的全路径

测试

public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader<PlayHeSuan> load = ServiceLoader.load(PlayHeSuan.class);
        load.forEach(PlayHeSuan::playHeSuan);
    }
}

SPI就讲到这里,我们回过头来结合实例理解下。SPI的目的就是借助SPI理解自动装配

借助SPI理解自动装配

回顾一下我们做了什么,我们在resources下创建了一个文件,里面放了些实现类,然后通过ServiceLoader这个类加载器就把它们加载出来了。

假设有人已经把编写配置之类的前置步骤完成了,那么我们是不是只需要使用下面的这部分代码,就能将PlayHeSuan有关的所有实现类调度出来。

// 使用Java的ServiceLoader进行加载
ServiceLoader<PlayHeSuan> load = ServiceLoader.load(PlayHeSuan.class);
load.forEach(PlayHeSuan::playHeSuan);

再进一步讲,如果再有人把上面这部分代码也给写了,然后把这些实现类全部注入到Spring容器里,那会发生什么?

相信到这里大家看到这里心里都已经有个谱了,我们接下来进入SpringBoot开始深入理解

SpringBoot的配置文件

在回想下SPI机制,他们都是通过在组件下放入一个配置文件完成的,那么Spring Boot是不是也这样的呢?。

是的 没错,我们可以通过pom文件的组名称来maven看到在这个下面,我们可以看到spring.factories,翻译过来是spring工厂,是不是有那个感觉了,这个就是实例化的呗

很明显的看到最下面有个自动配置的注释,key还是个EnableAutoConfiguration,开启自动配置!找到了!

我们接着找我们最开始说的redis

进入RedisAutoConfiguration类,看看里面是些什么代码

哈哈 破案了,终于破案了,这里不就是加载进去了嘛,配置文件我们也找到了:spring.factories,也实锤了就是通过这个配置文件进行的自动配置。

接下来,我们来尝试还原一下案情经过:

那接下来,我们就看是通过什么样的方式读取spring.factories,并且注入的,看到这里是不是感觉刚要进去的感觉,坚持看,相信都能看明白

如何注入

其实读取配置,就是注入,我们一起来看下看看Spring中有哪些注入方式

类似于@Component,@Bean这些,阿鉴就不说了,大家肯定见过一种这样的注解:EnableXxxxx

比如:EnableAsync开启异步,大家好不好奇这样的注解是怎么生效的?一起来看下

大家有没有发现,框架中到处是Import注解,这里先不展开细讲,到时候讲Spring的时候,再细说,总之一句话,import就是将该类交给Spring管理,实例化该bean。

那回过头来进去 AsyncConfigurationSelector,接着进去ProxyAsyncConfiguration

真相了,EnableAsync就是通过这里(AsyncAnnotationBeanPostProcessor)生效的

好的 我们接着研究最后一个注解,也是SpringBoot的启动核心注解

SpringBootApplication注解

我们在使用SpringBoot项目时,用到的唯一的注解就是@SpringBootApplication,所以我们唯一能下手的也只有它了,打开它看看吧。

进入AutoConfigurationImportSelector核心逻辑selectImports

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
   AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
         .loadMetadata(this.beanClassLoader);
   AnnotationAttributes attributes = getAttributes(annotationMetadata);

   // 获取候选的配置类,这个其实就是开始去拿配置类去了
   List<String> configurations = getCandidateConfigurations(annotationMetadata,
         attributes);

   // 移除重复的配置     
   configurations = removeDuplicates(configurations);

   // 获取到要排除的配置
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);

   // 移除所有要排除的配置
   configurations.removeAll(exclusions);

   //过滤掉不具备注入条件的配置类,通过Conditional注解
   configurations = filter(configurations, autoConfigurationMetadata);

   //通知自动配置相关的监听器
   fireAutoConfigurationImportEvents(configurations, exclusions);

   //返回所有自动配置类
   return StringUtils.toStringArray(configurations);
}

如何从配置文件读取的(getCandidateConfigurations)

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
      AnnotationAttributes attributes) {
      //是不是有刚才那个SPI感觉了
   List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
         getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());

         //通过这个也能看出来 是去读取META-INF/spring.factorie
   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;
}

getSpringFactoriesLoaderFactoryClass

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
  return EnableAutoConfiguration.class;
}

结合上一步,就是加载配置文件,并且读取key为EnableAutoConfiguration的配置

接下来再看loadFactoryNames

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) {

  try {
    // FACTORIES_RESOURCE_LOCATION的值为:META-INF/spring.factories
    // 这步就是意味中读取classpath下的META-INF/spring.factories文件
    Enumeration<URL> urls = (classLoader != null ?
                             classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                             ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    // 接下来就是读取出文件内容,封装成map的操作了
    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);
  }
}

ok, 后面的过滤逻辑就不在这里说了,毕竟本节的重点是自动装配机制,小伙伴明白了原理就ok啦

好的 今天的自动装配原理就讲到这里,我们下期见

总结回顾

本篇介绍了关于SpringBoot的自动装配原理,我们先通过SPI机制进行了小小的热身,然后再根据SPI的机制进行推导Spring的自动装配原理,中间还带大家回顾了一下@Import注解的使用,最后成功破案~

最后给张图做下总结

上一篇 下一篇

猜你喜欢

热点阅读