转载

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

古时的风筝第 71  篇原创文章 

前面已经粗略的讲了 Spring 的 IoC 过程 「 看完就懂的 Spring IoC 实现过程 ,之所以又从 Spring Boot 的角度来说,是因为现在大多数开发都采用 Spring Boot 了,而且 Spring 官方也推荐使用 Spring Boot,而且Spring Boot 的启动入口比较明显,从入口往下推比较容易。但是,其实这个 IoC 过程中 Spring Boot 并没有做什么,核心的东西还是 Spring 自己的,因为 Spring 除了 XML 配置之外,本身就是支持完全注解的方式的。

什么是 IoC

IoC,全称 Inversion of Control - 控制反转,还有一种叫法叫做 DI( Dependency Injection)-依赖注入。也可以说控制反转是最终目的,依赖注入是实现这个目的的具体方法。

什么叫控制反转

为什么叫做控制反转呢。

在传统的模式下,我想要使用另外一个非静态对象的时候会怎么做呢,答案就是 new 一个实例出来。

举个例子,假设有一个 Logger 类,用来输出日志的。定义如下:

public class Logger {
   public void log(String text){
       System.out.println("log:" + text);
  }
}

那现在我要调用这个 log 方法,会怎么做呢。

Logger logger = new Logger();
logger.log("日志内容");

对不对,以上就是一个传统的调用模式。何时 new 这个对象实例是由调用方来控制,或者说由我们开发者自己控制,什么时候用就什么时候 new 一个出来。

而当我们用了 IoC 之后,事情就变得不一样了。简单来看,结果就是开发者不需要关心 new 对象的操作了。还是那个 Logger 类,我们在引入 IoC 之后会如何使用它呢?

public class UserController {
   @Autowired
   private Logger logger;
   public void log(){
       logger.log("please write a log");
  }
}

开发者不创建对象,但是要保证对象被正常使用,不可能没有 new 这个动作,这说不通。既然如此,肯定是谁帮我们做了这个操作,那就是 Spring 框架做了,准确的说是 Spring IoC Container 帮我们做了。这样一来,控制权由开发者转变成了第三方框架,这就叫做控制反转。

什么叫依赖注入

依赖注入的主谓宾补充完整,就是将调用者所依赖的类实例对象注入到调用者类。拿前面的那个例子来说,UserController 类就是调用者,它想要调用 Logger 实例化对象出来的 log 方法,logger 作为一个实例化(也就是 new 出来的)对象,就是 UserController 的依赖对象,我们在代码中没有主动使用 new 关键字,那是因为 Spring IoC Container 帮我们做了,这个对于开发者来说透明的操作就叫做注入。

注入的方式有三种:构造方法的注入、setter 的注入和注解注入,前两种方式基本上现在很少有人用了,开发中更多的是采用注解方式,尤其是 Spring Boot 越来越普遍的今天。我们在使用 Spring 框架开发时,一般都用 @Autowired ,当然有时也可以用  @Resource

@Autowired
private IUserService userService;
@Autowired
private Logger logger;

Spring IoC Container

前面说了注入的动作其实是 Spring IoC Container 帮我们做的,那么 Spring IoC Container 究竟是什么呢?

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

本次要讨论的就是上图中的 Core Container 部分,包括 Beans、Core、Context、SpEL 四个部分。

Container 负责实例化,配置和组装Bean,并将其注入到依赖调用者类中。Container 是管理 Spring 项目中 Bean 整个生命周期的管理者,包括 Bean 的创建、注册、存储、获取、销毁等等。

Spring Boot 的 IoC

前面一直都是在以 Spring 的角度介绍 IoC,那 Spring Boot IoC 的核心还是 Spring IoC,只是具体细节上有些不一样而已。

首先 Spring Boot 其实就是对 Spring 的更进一步包装,省去了 XML 的配置,取而代之的以注解的形式,另外 Spring Boot 采用自动装配机制,帮我们省去了很多的配置,但是纠其内在,还是 Spring。

我们一般把 ApplicationContext 叫做容器,XML 方式有具体的实现子类,注解有具体的实现子类,Spring Boot 采用完全注解方式,它里面的容器就是  AnnotationConfigServletWebServerApplicationContext

Spring Boot 的启动

Spring Boot 的 启动过程其实就是 Spring 的启动过程,只不过在 Spring 的基础上加了一些 Spring Boot 的特性,比如 Banner。Spring 有可能是采用 XML 配置的方式,而 Spring Boot 则完全采用注解的方式,完成自动装配。

Spring Boot 的启动过程分析起来其实比 Spring 要简单,因为它的入口明晰,就下面这么一行。所以顺着 run 方法进去,一路跟下去就是了。

SpringApplication.run(Application.class, args);

启动过程会根据项目中的 application.yaml 或者 application.properties 配置文件进行自动配置。

IoC 过程

整体流程概括

整个的 IoC 过程是很复杂的,涉及到的内容极多,不可能用一篇文章说清楚,本篇只介绍 IoC 过程最核心的逻辑。核心逻辑总结起来就几个步骤,我专门画了一个简单流程图来说明。

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

以上是简化的概念版的流程,实际上整个过程要繁杂的多,经过多次的源码调试之后,我画了下面的这张泳道图出来,尽量清楚的表现出 IoC 的过程。先看图吧,获取高清大图,请在公众号内回复「ioc」。

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

经典的 refresh 方法

从 run 方法进来,就开始执行经典的 refresh 方法中,可以说,Spring IoC 的秘密都藏在这里。

public void refresh() throws BeansException, IllegalStateException {
 synchronized (this.startupShutdownMonitor) {
  // Prepare this context for refreshing.
  prepareRefresh();
  // Tell the subclass to refresh the internal bean factory.
  ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  // Prepare the bean factory for use in this context.
  prepareBeanFactory(beanFactory);
  try {
   // Allows post-processing of the bean factory in context subclasses.
   postProcessBeanFactory(beanFactory);
   //找到所有的@Component 和 @ManagedBean注解的类
   //和 @Bean 注解的方法,生成对应的 BeanDefinition 注册到 BeanDefinitionMap中
   invokeBeanFactoryPostProcessors(beanFactory);
   // Register bean processors that intercept bean creation.
   registerBeanPostProcessors(beanFactory);
   // Initialize message source for this context.
   initMessageSource();
   // Initialize event multicaster for this context.
   initApplicationEventMulticaster();
   // Initialize other special beans in specific context subclasses.
   onRefresh();
   // Check for listener beans and register them.
   registerListeners();
   // 注册非懒加载的 Bean 实例到 singletonObjects 中
   finishBeanFactoryInitialization(beanFactory);
   // Last step: publish corresponding event.
   finishRefresh();
  }
  catch (BeansException ex) {
   destroyBeans();
   cancelRefresh(ex);
   throw ex;
  }
  finally {
   resetCommonCaches();
  }
}
}

IoC 过程的核心就在 invokeBeanFactoryPostProcessors(beanFactory)finishBeanFactoryInitialization(beanFactory); 这两个方法中,前者的最主要作用就是生成  BeanDefinition ,后者的主要作用就是生成 Bean 实例。

Bean 的注入过程概括起来就是两步,第一步是生成对应的 BeanDefinition ,然后存到 beanDefinitionMap 中,beanDefinitionMap 的定义如下:

private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256)

第二步,将 BeanDefinition 实例化成对应的 Bean 实例,然后存到 singletonObjects 中,singletonObjects 的定义如下:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

找到那些要被容器控制的 Bean 类

要被容器控制的 Bean 类,是用注解 @Component@ManagedBean 标识,当然我们程序中很多地方都不会直接用  @Component ,而是用它的子类注解,包括  @Service@Controller@Repository ,间接的还有  @RestController 。另外还有一类是用  @Bean 注解标识的方法体。

举个我们经常会用到的例子,下面的这两段代码,分别使用 @Service 和  @Bean 注解,来表示一个服务类和一个方法 Bean。这两个就是待注入的对象,也就是说经过 Spring 的一番运作,它们两个要存到 singletonObjects 中。

@Service
 public class UserServiceImpl implements IUserService {

    @Override
    public User getUser(Integer id) {
        User user = new User();
        user.setId(id);
        user.setName("用户"+id);
        user.setAge(18);
        return user;
    }
 }
@Bean
public Logger logger(){
   return new Logger();
}
public class Logger {
   public void log(String text){
       System.out.println("log:" + text);
  }
}

通过上面的泳道图来看,从方法 invokeBeanFactoryPostProcessors(beanFactory) 进入之后,会走到 ConfigurationClassPostProcessor 这个类的  processConfigBeanDefinitions() 方法,这个方法通过 parser.parse() 方法找到带有  @Component 注解的类,然后加入到 parser.configurationClasses 属性中,configurationClasses 属性是一个 map,其key 和 value 都是  ConfigurationClass , 里面存的是主要的类的元数据信息,比如加载器、注解等等,其中除了我们主动在项目中用注解声明的类,还有 Spring Boot 内置的几十个类,最后通过  ConfigurationClassBeanDefinitionReader 类的  loadBeanDefinitions 方法将这些  ConfigurationClass 生成对应的 BeanDefinition,然后存到到 beanDefinitionMap 中。

以下是 processConfigBeanDefinitions() 方法的主要流程,省略了很多代码。

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
 List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
 String[] candidateNames = registry.getBeanDefinitionNames();
// ...
 // Parse each @Configuration class
 ConfigurationClassParser parser = new ConfigurationClassParser(
   this.metadataReaderFactory, this.problemReporter, this.environment,
   this.resourceLoader, this.componentScanBeanNameGenerator, registry);
 Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
 Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
 do {
 // 找到带有 @Component 注解的类
  parser.parse(candidates);
  parser.validate();
  Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
  configClasses.removeAll(alreadyParsed);
  // Read the model and create bean definitions based on its content
  if (this.reader == null) {
   this.reader = new ConfigurationClassBeanDefinitionReader(
     registry, this.sourceExtractor, this.resourceLoader, this.environment,
     this.importBeanNameGenerator, parser.getImportRegistry());
  }
  this.reader.loadBeanDefinitions(configClasses);
  alreadyParsed.addAll(configClasses);
}

如何找到 @Component 注解的呢

接上面代码,进入 parser.parse() 方法,这个方法传入的参数是 Spring Boot 启动类,为什么是启动类呢,回到上面的那张泳道图,parse() 方法会调用到 ClassPathBeanDefinitionScanner 类的  doScan(String... basePackages) 方法,注意这个方法的参数是一个可变成集合,而且参数名 basePackages 我们很熟悉 ,没错,就是我们在启动类使用注解  @ComponentScan 指定的扫描包,如果不指定默认就是全部扫描。

doScan 方法中的关键调用就是下面这行, findCandidateComponents 方法遍历指定包下的所有类,查找其中符合条件的类。

Set<BeanDefinition> candidates =    findCandidateComponents(basePackage);

具体到某个方法,那就是下面这个方法,在 ClassPathScanningCandidateComponentProvider 类中。

protected boolean isCandidateComponent(MetadataReader  metadataReader) throws IOException {
		for (TypeFilter tf : this.excludeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return false;
			}
		}
		for (TypeFilter tf : this.includeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return isConditionMatch(metadataReader);
			}
		}
		return false;
}

其中 this.includeFilters 就是 @Component 和  @ManagedBean ,也就是包含这两个注解中的其中一个的。

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

如何找到 @Bean 注解的方法

别忘了还有一种声明 Bean 的方式,就是使用 @Bean 注解在某个方法上,这种 Bean 是在哪块找到的呢。

关键信息在 ConfigurationClassParser 类的  doProcessConfigurationClass 方法,其实上面说的  doScan 也在这个方法内,这个方法是一个递归调用,它会寻找当前类的继承类、接口类以及 @Import 引入的类,当然还包括了  @ComponentScan 指定的包或者类,另外,就是这里要说的  @Bean 注解标识的方法。

具体的方法段如下:

Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
  configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

最后在把这些 @Bean 注解的方法,通过  loadBeanDefinitions 方法真正转化成 BeanDefinition ,然后存到 beanDefinitionMap 中。

实例化 Bean

finishBeanFactoryInitialization(beanFactory); 完成 Bean 的实例化,并把 Bean 实例放到真正该去的地方 singletonObjects 中。

有一点很重要,Bean 并不是用到的时候才实例化,而是在启动的时候就完成实例化了,除非是你主动设置成懒加载的模式,也就是加了 @Lazy 注解的。

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

上图就是 Bean 实例化的一个过程。

@Autowired 何时被查找的

我们上面声明了这两个 Bean,那肯定要有地方使用它们吧,比如这样:

@Autowired
private IUserService userService;
@Autowired
private Logger logger;

在 Bean 实例化的同时,还要做一件非常重要的事情,那就是发现那些使用了 @Autowired 注解的 Field ,给它赋值,也就是对应的 Bean 实例。

整个过程如图所示:

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

applyMergedBeanDefinitionPostProcessors 这个方式是用来做 BeanDefinition 后处理的。

protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) {
 for (BeanPostProcessor bp : getBeanPostProcessors()) {
  if (bp instanceof MergedBeanDefinitionPostProcessor) {
   MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp;
   bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
  }
 }
}

后处理器有很多个,每一个都有不同的功能,循环每个后处理器,完成其各自的功能。

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

其中就包括了 AutowiredAnnotationBeanPostProcessor ,会执行它的  postProcessMergedBeanDefinition 方法。

@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
 InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
 metadata.checkConfigMembers(beanDefinition);
}

其中 buildAutowiringMetadata 方法就是寻找  @Autowired 注解的具体逻辑。

private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
 if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
  return InjectionMetadata.EMPTY;
}
 List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
 Class<?> targetClass = clazz;
 do {
  final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
  ReflectionUtils.doWithLocalFields(targetClass, field -> {
   MergedAnnotation<?> ann = findAutowiredAnnotation(field);
   if (ann != null) {
    if (Modifier.isStatic(field.getModifiers())) {
     if (logger.isInfoEnabled()) {
      logger.info("Autowired annotation is not supported on static fields: " + field);
    }
     return;
    }
    boolean required = determineRequiredStatus(ann);
    currElements.add(new AutowiredFieldElement(field, required));
  }
  });
 //...
 return InjectionMetadata.forElements(elements, clazz);
}

通过反射找到所有 Field,然后筛选出 @Autowired 注解的 Field,其实除了  @Autowired 还会找到  @Value 标识的 Field。

最后通过 inject 方法给 Field 赋值,其实就是反射中的 invoke 方法。

public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
 Collection<InjectedElement> checkedElements = this.checkedElements;
 Collection<InjectedElement> elementsToIterate =
  (checkedElements != null ? checkedElements : this.injectedElements);
 if (!elementsToIterate.isEmpty()) {
  for (InjectedElement element : elementsToIterate) {
   if (logger.isTraceEnabled()) {
    logger.trace("Processing injected element of bean '" + beanName + "': " + element);
  }
   element.inject(target, beanName, pvs);
  }
 }
}

最后

虽然内容看上去不少,但是其中还是有很多细节没有讲清楚。

另外,读源码真是个头疼的过程,枯燥、繁琐,再把分析源码的过程写成文儿就更枯燥了,太费脑子了。虽然之前也看过,但是隔段 时间 就忘的差不多了,原理性的东西还是要多读、多看、多记录。

获取本文高清泳道图请在公众号内回复「ioc」

不容易呀,给个在看吧。

公众号:古时的风筝, 一个不只有技术的技术公众号。

我是风筝,一个主业 Java,同时也擅长 Python、React 的斜杠开发者。你可选择现在就关注我,或者看看历史文章再关注也不迟。

技术交流还可以加群或者直接加我微信。

从 Spring Boot 出发,分析 Spring IoC 过程(熬夜看源码,头疼)

原文  http://mp.weixin.qq.com/s?__biz=MzAxMjA0MDk2OA==&mid=2449469528&idx=1&sn=1856392242e54d0775a6345395c8e414
正文到此结束
Loading...