the site subtitle

我的架构之路(一)-Spring Framework原理解析

2021.08.08

Spring作为Java开发一部分,现已成为一个重量级企业级开发框架,可以说,离开Spring,基本很难实现健壮/可扩展的系统,Java开发者无一不对Spring略知一二,但很大一部分人,都只是停留在能使用,能解决一些场景问题,底层原理知道的能讲完整的只占少数,我不想成为大部分普通的Java开发者,所以,我无时无刻不在深研Spring的实现原理,Spring家族下有很多的子项目,Spring这个词范围很广也很大,今天只分析Spring最核心的一部分,也就是Spring Framework(以下简称Spring),只有掌握了基础核心的东西,更上层的知识不管是使用还是解决问题都是很有帮助的。

从宏观的角度来看Spring,先不去讨论Spring的细节,Spring到底为我们做了些什么?

对象的创建

第一次使用Spring还是在大学时期,当时的Spring还需要编写XML配置文件,将需要交给Spring管理的java对象配置到XMl中,且在执行入口(main)方法中创建一个ClassPathXmlApplicationContext对象,该对象接受参数为XML位置文件路径,然后通过调用context对象的getBean方法进行获取Spring给我们创建的Bean。基本代码如下:

public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        UserService userService = context.getBean(UserService.class);
    }

当代码写到这里,其实Spring我们就引入成功了,但是,这里的userService对象和我们自己new的UserService对象有什么区别,确实,在UserService里面没有其他Bean的情况下,他和我们自己创建的实例没有任何区别,但是,当我们在UserService中有引入其他对象的时候,这里Spring就会自动帮我们注入需要的Bean,而如果是我们自己手动创建对象,还需要创建UserService里面引入的对象,那么假如,UserService里引入的对象里面还有引入其他对象,这里就会特别的麻烦,你需要递归的去创建UserService一下所有引入的对象,而使用Spring则它会帮我们自动注入所有的引用;到这里就有了鲜明的对比,Spring确实比我们自己手动创建对象要方便。那么知道了Spring为我们做了什么事,我们可能会去想它是怎么做的,我们知道,在Java中,要想得到一个对象,通常有两种方式,一个是new,另一个是通过反射(Spring中是使用反射来进行创建),不管是new还是反射,始终会调用这个类的构造方法,在不指定构造方法的Java类,会存在一个默认的无参构造,Spring默认就会调用默认的无参构造。

依赖注入

对象创建完成后,紧接着下来就是依赖的注入,依赖注入有多种方式,例如属性注入,构造方法注入等等,这里因为对象的创建还没有完成,先讨论构造方法的注入。

构造方法注入

这里需要注意的是:
1.当类中无指定构造方法时,Spring默认调用无参构造,进行对象的创建。
2.当类中有指定构造方法,且有且只有一个构造方法时,Spring会调用这个构造方法,进行对象的创建,而构造方法中的参数,Spring会自动注入,注入规则下面细说。
3.当类中有指定的构造方法,且有多个,Spring会默认调用无参构造方法,如果类中没有指定默认无参构造,则会抛出异常。表示该类有多个构造方法,但是没有默认构造方法。那么如何解决这个问题,报错明确了是因为没有找到默认构造方法,那么我们可以为Spring指定一个默认的构造方法进行创建,只需要在构造方法上添加@Autowired注解。

通过以上规则,构造方法确定了,那么构造方法的参数上怎么被注入的,首先Spring会通过构造方法上的类型,去Spring容器里面找,当通过类型找到一个(注意,是通过类型找到一个)Bean时,将这个Bean注入到构造方法的形参上;
当通过类型找到多个Bean时,Spring会根据形参的名称去筛选。最终确定一个Bean进行注入,如果以上两种方式都没有办法确定一个Bean,则抛出异常。

属性注入

当对象创建完成,Spring就会进行属性的注入,通过扫描对象中指定@Autowired注解的属性,进行属性注入,查找方式和构造方法注入一致。

初始化前置

@PostConstruct是Java规范JSR-250引入的注解,Spring提供了实现,@PostConstruct字面意思为调用构造方法后,为什么被称为初始化前置,其实Spring的生命周期前置后置处理器都是在Bean对象实例创建完成后,Spring初始化(也就是调用InitializingBean接口子类afterPropertiesSet方法前后)之前或之后。
@PostConstruct用于初始化前执行,只需要在Java类中某个方法(非静态void方法)上,这样Spring在初始化前就会自动调用。
@PostConstruct在Spring中由CommonAnnotationBeanPostProcessor类实现,其主要逻辑在父类InitDestroyAnnotationBeanPostProcessor中,归根结底,他们都是Spring的BeanPostProcesser。

初始化

如果我们需要在Bean初始化时执行自己的逻辑,我们可以实现InitializingBean接口,该接口只有一个接口方法afterPropertiesSet,意为属性设置后,什么意思,也就是在依赖注入完成后,该接口方法中可以直接使用该Bean中的其他属性Bean。

初始化后置

当Bean对象都初始化好了,我们可以做一些需要的事情。Spring AOP就是在初始化后置处理器中实现的,如果该对象需要被代理(被切面标记),则Spring为该Bean创建一个代理对象(这个代理对象可能是JDK的动态代理也可能是CGLIB的动态代理)。那么Spring是如何判断当前Bean是否被切或者是否需要创建代理对象,可以分为已下几步:
1.扫描所有切面Bean(也就是添加了@Aspect注解的Bean对象)
2.扫描切面Bean的所有方法,找到切入点表达式
3.解析切点表达式,看是否和当前Bean中的方法匹配
4.不匹配这里不做任何处理,直接返回Bean对象,如果匹配则生成代理对象,且将Bean及切点逻辑方法保存起来,便于在执行切点方法是,执行前置及后置逻辑。
这里有几个注意点:
1.生成的代理对象中,Bean中依赖注入的属性是没有值的,但是在执行过程中,属性是有值的,这里可能会比较奇怪,其实在代理对象中还有一个原始对象的属性,属性名target,并且在代理对象上调用切点方法是,不是调用的supper,而是target属性对象的切点方法。target原始对象是一个完整的Spring Bean。

Spring事务

Spring中事务就是典型的切面编程。将添加了@Transactional注解的类或方法作为切入点,然后再方法被执行时,先执行切点的前置逻辑,前置逻辑做了如下事情:
1.创建一个数据库连接
2.将自动提交设置为false,事务的前提在于手动提交
然后执行业务逻辑,最后执行后置逻辑,后置逻辑包括,正常逻辑和异常逻辑,正常逻辑进行事务提交,异常逻辑进行事务回滚。
了解了Spring的事务大概原理后,那么Spring事务的失效就很好理解了,排查问题也很简单,最终都是因为Spring中方法之间的调用是原始Bean调用还是代理对象调用,原始Bean调用就只是普通的方法调用,而代理Bean进行调用才会执行AOP的逻辑,而事务也是在AOP的基础上实现的。
Spring中常见的事务失效:
1.事务入口没有被代理,也就是说当调用一个Service时,这个Service方法没有被代理,也就是没有@Transactional注解,在当前方法中调用了本类中的事务方法,事务会失效,因为当前方法没有指定事务,也就没有被代理,尽管调用的方法有指定事务,当前方法间的调用只是普通的方法调用,所以事务没有起到作用。
2.事务方法必须是public级别。
3.事务方法不能被final修饰,原因在于Spring的事务是通过代理类来进行的,代理类也是被代理类的子类,而final关键字的作用是,被final修饰的方法不能被子类重写,所以会导致事务失效。
4.没有被Spring管理的对象,这个很好理解,都没有被Spring管理,Spring为什么要帮你实现事务。
5.多线程调用,多个线程中的数据库连接不一样,不能进行统一的事务提交与回滚。

强大的BeanPostProcesser - IOC到AOP的桥梁

Spring的高可扩展性,BeanPostProcesser接口功不可没。上面提到的初始化前置/初始化后置处理器都是基于BeanPostProcesser来实现的。BeanPostProcesser的实现原理很简单,在Bean创建的时候,Spring通过判断当前Bean是否实现BeanPostProcesser接口来标记当前Bean是不是前置后置处理器,如果是,则将其放到列表当中,当所有Bean都扫描完毕之后,Spring就会遍历所有的BeanPostProcesser子类Bean,进行调用postProcessBeforeInitialization或者postProcessAfterInitialization方法,当前中间还有InitializingBean的afterPropertiesSet方法被调用。
需要注意的是,这里的前置/后置处理器不同于AOP的前置和后置通知,虽然说AOP和BeanPostProcesser有着很大关系,但是这种关系只是BeanPostProcesser的后置处理器为AOP生成来代理类,而AOP的前置后置通知是在代理类中实现的,中间还有原始方法的调用。

BeanFactoryPostProcessor

和BeanPostProcessor作用类似,但是BeanFactoryPostProcessor干涉的是BeanFactory的创建过程,我们可以在postProcessBeanFactory方法中对BeanFactory进行修改。

BeanDefinition

顾名思义该类记录了Bean的定义,每一个交给Spring管理的Bean都有对应的BeanDefinition,Spring通过它来创建Bean对象实例,我们可以通过注解及XML配置的方式来配置一个Bean,例如@Compontent,@Service,@Controller,@Bean,等等。该类里面包含了如下属性:

  1. beanClass:Bean类型
  2. scope:作用域
  3. isLazy:是否单例

BeanDefinitionReader

BeanDefinitionReader的作用就是BeanDefinition的读取器了,它有很多子类,比如:AnnotatedBeanDefinitionReader,XmlBeanDefinitionReader,各个Reader负责不同的BeanDefinition的配置方式。

BeanFactory

Bean的创建工厂,提供创建Bean及获取Bean的方法,这里着重讲一下它的实现类DefaultListableBeanFactory,先看一下该类的集成及构图:
image.png
可以发现DefaultListableBeanFactory的集成体系非常的复杂,实现的接口也非常的多

FactoryBean

FactoryBean本身也是一个Bean,只是它的getObject方法会帮我们创建一个Bean,FactoryBean存在的意义在于它能方便的帮我们创建复杂Bean,例如Mybatis和Spring整合时,我们配置的SqlSessionFactoryBean,因为SqlSessionFactory里面需要的属性及配置非常的多,自己配置XML或者说@Bean会非常多繁琐,所以这里Spring Mybatis整合时就继承了FactoryBean接口。
或许很多小伙伴对BeanFactory和FactoryBean比较疑惑,他们两是什么关系,什么区别,他是他们连个关系不大,区别倒是很大。

  1. BeanFactory是用于创建Bean,FactoryBean自己也是一个Bean
  2. FactoryBean的getObject方法会被注册到Spring容器中
  3. FactoryBean创建的Bean不会执行Bean初始化前及初始化的生命周期钩子,但是会执行初始化后的生命周期逻辑,所以,如果有AOP之类的,还是会生效。

ApplicationContext

这是我们最常见的对象的,在以前,我们使用的最多的就是它的子类ClassPathXmlApplicationContext,现在我们用的最多的是AnnotationConfigApplicationContext,可能有的伙伴会说,AnnotationConfigApplicationContext也没有用到, 其实是现在我们很多的项目都是使用的Spring Boot,Boot的使命是简化Spring一些繁琐的配置,很多都由Boot帮我们去完成了,其实Spring Boot使用的就是AnnotationConfigApplicationContext
ApplicationContext继承了很多接口,EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver,其目的是将其他接口的能力汇聚到一起,我们在使用的生活就不需要去多个实例调用。

TypeConverter

类型转换器,Spring中很多的类型转换相关都是用了TypeConverter,它整合了PropertyEditor和ConversionService的功能

MetadataReader、ClassMetadata、AnnotationMetadata

用于解析类的元数据信息,Spring是用ASM技术对类进行解析,为什么是用ASM,因为ASM只是解析字节码问题,而不需要将Class加载到JVM中,JVM中对类加载机制中,加载类的时机是在我们使用到某一个类的时候才加载,我们不能因为使用了Spring而打乱是JVM类加载到规则。实际上我们可以将MetadataReader看作是一个工具类,里面封装了类的解析,得到ClassMetadata对象的一个方法。

Spring Bean的生命周期及扩展点

先来看下面这张图,该图来自网络
image.png

Spring的初始化过程

这个初始化过程包含了Bean的生命周期:

  1. 加载配置文件
  2. 创建Spring容器,也就是BeanFactory的子类DefaultListableBeanFactory,AbstractRefreshableApplicationContext#createBeanFactory
  3. 将配置解析成BeanDefinition,这一步在obtainFreshBeanFactory()创建BeanFactory的时候解析,具体实现在AbstractRefreshableApplicationContext#loadBeanDefinitions方法中,这里可以添加NamespaceHandler扩展,用于解析XML文件。
  4. 注册特殊的BeanPostProcessor,例如ApplicationContextAwareProcessor,ApplicationListenerDetector,LoadTimeWeaverAwareProcessor
  5. 调用在Spring上下文注册的Bean工厂处理器,BeanDefinitionRegistryPostProcessor就是在这时被调用,BeanDefinitionRegistryPostProcessor接口继承BeanFactoryPostProcessor,这里先调用的是BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry方法,然后调用BeanFactoryPostProcessor#postProcessBeanFactory方法
  6. 注册BeanPostProcessor,这里将所有的BeanPostProcessor接口的Bean注册到容器中。
  7. 初始化应用事件传播器,注册应用监听。
  8. 实例化对象。
  9. 完成刷新所有流程,发布ContextRefreshedEvent事件。

Spring Bean的生命周期

从上面步骤来看,Bean的生命周期在第8步开始,分别是:

  1. 对象创建之前会执行InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation方法,InstantiationAwareBeanPostProcessor该类继承自BeanPostProcessor,但是它的方法和BeanPostProcessor是不同的,一个是Instantiation,另一个是Initialization,分别表示实例化和初始化。改方法调用在createBean方法中被执行。
  2. 对象创建,通过反射创建对象,doCreateBean。
  3. 调用MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition方法,该类继承自BeanPostProcessor。
  4. 开始属性填充,populateBean,属性填充前会调用InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation方法以及postProcessProperties方法。
  5. 属性填充,依赖注入完毕。
  6. 开始初始化,整个初始化在initializeBean方法中,初始化前,进行Aware接口调用。
  7. 调用所有BeanPostProcessor#postProcessBeforeInitialization方法。
  8. 调用InitializingBean接口的afterPropertiesSet方法。
  9. 调用所有BeanPostProcessor#postProcessAfterInitialization方法。
  10. 在registerDisposableBeanIfNecessary方法中注册DisposableBean。

以上就是整个Bean生命周期过程。

Spring AOP实现原理

Spring中AOP分为两个阶段,第一个阶段为加载和解析配置阶段,第二个阶段为创建代理对象阶段

加载解析配置阶段

该阶段主要对AOP的配置进行提起加载解析,在IOC的解析成BeanDefinition的时候进行,其实Spring所有的配置加载都在这一阶段完成,在解析XML配置文件的时候,Spring默认只解析Bean Namespace,当Spring发现配置文件中有引入其他Spring扩展Namespace的时候,
Spring会根据配置文件的NamespaceURI进行确定使用哪一个NamespaceHandler来解析当前配置文件中的扩展配置,最后都将封装成一个BeanDefinition然后注册到IOC容器中。值得注意的是,在解析AOP配置的过程中,
Spring向容器注册了一个AspectJAwareAdvisorAutoProxyCreator,改类用于创建代理对象。

创建代理对象阶段

Spring中不管是创建对象还是依赖注入都是从getBean开始的,通过探究里面真正干活的是AbstractAutowireCapableBeanFactory中的createBean,该方法最终会调到initializeBean,该方法又会调用applyBeanPostProcessorsAfterInitialization方法,
在该方法中,会循环调用之前初始化时注册到容器中的所有BeanPostProcessorpostProcessAfterInitialization方法,而AOP在初始化的时候,注册了一个AspectJAwareAdvisorAutoProxyCreator,该类是BeanPostProcessor的子类,所以最终创建代理类的是
AspectJAwareAdvisorAutoProxyCreator的父类AbstractAutoProxyCreator的postProcessAfterInitialization方法,在该方法中调用wrapIfNecessary判断是否需要被代理(里面的判断逻辑就是切入点表达式,满足条件的表示该方法被代理),
并且应该使用哪种代理方式取决于目标对象是代理接口还是代理类,如果是代理接口则使用JDK动态代理,代理类则使用CGLIB动态代理。代码如下:

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}
}

SpringMVC实现原理

SpringMVC的运行流程大致是,配置核心入口DispatcherServlet,初始化HandlerMappingsHandlerAdaptersViewResolvers,当用户请求时,通过用户请求的URI查找处理器,封装请求参数,执行返回结果,渲染试图。
SpringMVC的实现原理可以分为三个阶段,分别是:

  • 配置阶段
  • 初始化阶段
  • 请求执行阶段

配置阶段

该阶段主要是配置SpringMVC的核心Servlet以及指定Spring的配置文件位置,配置监听等。

初始化阶段

初始化SpringMVC九大组件

this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
this.initHandlerMappings(context);
this.initHandlerAdapters(context);
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
this.initViewResolvers(context);
this.initFlashMapManager(context);

以上组件的作用分别是
MultipartResolver:文件处理器
LocaleResolver:语言处理器,用于国际化
ThemeResolver:主题处理器
HandlerMappings:SpringMVC核心请求处理器,简称处理器映射器
HandlerAdapters:处理器适配器,用于适配处理器参数,从request中获取参数,自动转型并适配形式参数
HandlerExceptionResolvers:异常处理器
RequestToViewNameTranslator:视图名称翻译器
ViewResolvers:页面渲染处理器
FlashMapManager:参数传递管理器

以上最核心的是HandlerMappingsHandlerAdaptersViewResolversHandlerMapping用于管理URI对应的处理器Method,HandlerAdapters用于适配处理器参数,ViewResolvers用于试图渲染

请求执行阶段

当SpringMVC应用启动完毕,用户像服务器发送一个请求,首先会通过用户请求的URI进行匹配HandlerMapping,得到HandlerMapping过后,进行参数封装,最后执行方法,最后通过返回的ModelAndView交给ViewResolvers渲染试图

总结

通过该篇对Spring Framework的原理有一个比较宏观的理解,其实Spring中有很多思想是很先进的。本篇并没有针对Spring中的一些实现细节进行深究,但我认为,学习要从大到小,从粗到细,如果学习是一次航行,原理就是航线及方向,实现细节就是手上的舵,使其不偏离,即使偏离也会及时修正;终将到达目的地。

参考

https://www.jb51.net/article/213253.htm
https://zhuanlan.zhihu.com/p/441476065