Preface {#preface}
本文中使用的 SpringBoot 版本为2.6.13
,对应的Spring
版本为5.3.23
。
IOC 原理 {#ioc-原理}
IOC(Inversion of Control ):控制反转。是 Spring Core 最核心的部分。
它本身并不算为一种技术,而是使我们从繁琐的对象交互中解脱出来的一种思想,使你能够专注于对象本身,更好的使用面向对象。
了解它之前必须弄清楚依赖注入(Dependency Inversion)。
DI举例:设计行李箱
首先需要设计轮子,然后根据轮子大小设计底盘,接着再根据底盘设计箱体,最后再根据箱体设计出整个行李箱。因此行李箱依赖于箱体,箱体依赖于底盘,而底盘依赖于轮子。
这样的设计看似 OK,实际上可维护性非常低,若市场需求反馈需要轮子尺寸需要改大一码,由于此时的依赖关系,轮子改大一码底盘也需重新设计,底盘重新设计了那么箱体也得改,进而整个行李箱都需要重新设计,这就是上层建筑依赖下层建筑的缺点。
上面这个场景转换为代码的形式如下:
若要满足轮子大小可以改动的话需要做如下改动:
仅仅是为了能够调整轮胎的尺寸就需要更改上层建筑的所有构造函数,这在软件工程中这样的设计几乎是不可维护的。
如果使用依赖注入的思想又如何进行设计?
首先设计出行李箱的大致样子,然后根据样子设计箱体,接着再根据箱体设计底盘,最后根据底盘设计出轮子。此时依赖关系相较于之前就完全倒置过来了。
所谓依赖注入的含义:把底层类作为参数传递给上层类,实现上层对下层的"控制"。
转换为代码的形式如下:
此时若要再实现修改轮子尺寸的功能只需更改轮子类Tire
即可:
在实际的工程中这种设计模式还有利于协同合作和单元测试。
比如开发这 4 个类分别属于 4 个开发小组,只要定义好了接口,所有小组可以同时进行开发而不相互收到受到限制。
如果要写Luggage
类的单元测试,只需传入Framework
类对象即可,而不用把Framework
、Bottom
、Tire
类全部new
一遍后再构造Luggage
对象。
IOC、DI、DL 的关系:
实现IOC
除了DI
依赖注入的方式外还有DL
依赖查找的方式,它相较于DI
是一种更为主动的方法,它通过调用框架提供的方法获取对象,获取时需要提供相关配置文件的路径,key 等信息确定获取对象的状态,由于它需要用户自己使用 API 查找资源和组装对象有侵入性,所以如今已被抛弃不再使用了。
DI
是 Spring 实现IOC
的方式,负责组件的装配,DI
也是当今主流的方式。
依赖注入的方式:
- Setter
- Interface
- Constructor
- Annotation。如 Spring 中的
autowired
注解。
依赖倒置原则、IOC、DI、IOC容器的关系:
依赖倒置原则是一种思想,大致含义是高层模块不应该依赖底层模块,两者都应该依赖于其抽象。
在依赖倒置原则的思想的指导下才有了 IOC 的思路,依赖注入DI
就是实现 IOC 的一种方式。
Spring 框架基于 IOC 才提出容器的概念,对于 IOC 来说最终要的就是容器了,容器中管理着Bean
的生命周期,控制着Bean
的依赖注入。
在上面的例子中初始化Luggage
类的地方就是 IOC容器所做的事情:
IOC 容器的优势:
- 避免在各处使用
new
来创建类,并且可以做到统一维护; - 创建实例的时候不需要了解其中的细节。
用上面的例子来说如果我们手动创建Luggage
实例就需要了解Tire
、Bottom
、Framework
类的构造函数,然后一步一步的new
直到创建Luggage
实例为止。
而使用IOC 容器时所需的步骤是反过来的,它首先会从上到下查找所需的类依赖关系,寻找完毕后再向上一步一步的new
直到创建Luggage
实例为止。
因此使用 IOC 容器可以隐藏创建Luggage
实例的具体细节。它就像个工厂,我们只需向工厂请求Luggage
实例它就会返回给我们,完全不用管Luggage
实例是怎么一步一步被创建出来的。
实际项目中有的Service
类可能是很久很久之前写的依赖几十上百个类,若我们新写的类需要实例化这个Service
总不能去弄慢慢弄清楚它所有的依赖然后一步一步创建,使用 IOC 容器就可以完美解决此问题。
Spring IOC 应用 {#spring-ioc-应用}
Spring 启动时读取容器的Bean
配置信息生成对应的Bean
定义注册表,根据这个注册表通过 Java 反射功能实例化出Bean
对象,装配好Bean
之间的依赖关系为上层提供准备就绪的运行环境。
Spring IOC 支持的功能:
-
依赖注入;
-
依赖检查;
-
自动装配;
-
支持集合;
-
指定初始化方法和销毁方法;
-
支持回调方法。
需要实现 Spring 提供的接口才行,略带有侵入性,谨慎使用。
其中最重要的就是:依赖注入 和自动装配。
SpringIOC 容器的核心接口:
- BeanFactory
- ApplicationContext
首先来看下BeanDefinition
和BeanDefinitionRegistry
。
BeanDefinition
:主要用来描述Bean
的定义。
Spring 容器在启动时会将xml
文件或注解中Bean
的定义解析成BeanDefinition
。
BeanDefinitionRegistry
:提供向 IOC容器注册BeanDefinition
对象的方法。
在 Spring5 源码中BeanDefinitionRegistry.registerBeanDefinition()
方法就是将BeanDefinition
注册到BeanFactory
接口实现类DefaultListableBeanFactory.beanDefinitionMap
属性中。
beanDefinitionMap
是ConcurrentHashMap
类型,键为beanName
,值为BeanDefinition
,同时还会将beanName
存入至beanDefinitionNames
数组中,以便后续Bean
的实例化。
BeanFactory
:Spring框架最核心的接口。
- 提供 IOC 的配置机制;
- 包含
Bean
的各种定义,便于实例化Bean
; - 建立
Bean
之间的依赖关系; Bean
生命周期的控制。
BeanFactory
体系结构:
从上图中可以看出BeanFactory
是顶级接口,前面提到的BeanDefinitionRegistry
和DefaultListableBeanFactory
都是从它扩展衍生出来的。
Spring 有个显著的特点,看接口直接的继承关系和它们的名字就能大致得出其作用和含义。
大概过下图中比较重要的接口留个眼熟,不需要记忆:
-
ListableBeanFactory
:定义了访问容器中Bean
基本信息的若干方法,如:查看Bean
的个数、获取某一类型Bean
的配置名、查看容器中是否包含某个Bean
等方法。 -
HierarchicalBeanFactory
:通过此接口可以建立出父子层级关联的容器体系,子容器可以访问父容器中的Bean
,但父容器不能访问子容器中的Bean
。Spring 通过父子容器实现出了很多功能,比如 SpringMVC 中展现层的
Bean
位于子容器中,而业务层和持久层的Bean
位于父容器中,这样展现层中的Bean
就可以引用业务层中的Bean
,但业务层则看不到展现层中的Bean
。 -
ConfigurableBeanFactory
:它增强了 IOC 容器的可定制性,定义了设置类装载器、属性遍历器、属性初始化后置处理器等方法。 -
AutowireCapableBeanFactory
:定义了将容器中的Bean
按照某种规则自动装配,如:按照名字,按照类型等。 -
SingletonBeanRegistry
:允许在运行期间向容器注册单例Bean
实例的方法。
BeanFactory
接口比较重要的方法:
-
getBean()
方法:它是 Spring 容器最重要的方法之一,功能是从 Spring 容器中获取Bean
,有 5 个重载方法,比如按名称获取Bean
,按类型获取Bean
等。 -
isSingleton()
方法:判断Bean
在 Spring IOC 中是否为单例在 Spring IOC 中默认情况下
Bean
都是单例。 -
isPrototype()
方法:判断Bean
在 Spring IOC 中是否为多例。如果返回的是
true
,在使用getBean()
方法时,Spring 容器会创建一个新的Bean
对象并返回给调用者。
由于BeanFactory
只提供一些基础的功能,所以 Spring 在其基础上又设计了更为高级的ApplicationContext
接口,我们在使用 Spring 容器时大部分使用的都是ApplicationContext
接口的实现类。
BeanFactory
与ApplicationContext
的比较:
BeanFactory
是 Spring框架的基础设施,面向 Spring;ApplicationContex
t面向使用 Spring框架的开发者。
ApplicationContext的功能(继承多个接口):
BeanFactory
:能够管理、装配Bean
;ResourcePatternResolver
:能够加载资源文件;MessageSource
:能够实现国际化等功能;ApplicationEventPublisher
:能够注册监听器,实现监听机制。
其中最底部的AnnotationConfigServletWebServerApplicationContext
类是 SpringBoot 默认的 IOC 容器类。
在 SpringBoot 源码中层层点击:
create()
方法在其两个实现类中都有,点入AnnotationConfigServletWebServerApplicationContext.create()
方法源码设置断点然后以debug
方式启动 SpringBoot。
成功进入了设置的断点,说明 SpringBoot 默认使用的就是**AnnotationConfigServletWebServerApplicationContext
**。
关于 Spring 中@Component
、@ComponentScan
、@Configuration
、@Bean
注解将Bean
注入 IOC 容器以及getBean()
方法从容器中获取Bean
等基础功能这里就不演示了。
Spring IOC 的 refresh 方法解析 {#spring-ioc-的-refresh-方法解析}
refresh()
方法作用:
- 为 IOC容器以及
Bean
的生命周期管理提供条件; - 刷新 Spring 上下文信息,定义 Spring 上下文加载流程。
下面仅对该方法的源码做简要解析。
首先找到该方法所在的为止,跟着一步一步点所在源码位置。
扩展:上图中
afterRefresh()
方法的方法体为空。因为 Spring 框架充分考虑到了扩展性,在框架中留了很多口子,使我们可以继承框架的某些模块重写其中的某些方法能够实现很多自定义的需求。
Spring IOC 的 getBean 方法解析 {#spring-ioc-的-getbean-方法解析}
getBean()
方法调用的都是doGetBean()
方法。
doGetBean()
方法的代码逻辑:
- 转换
beanName
; - 从缓存中加载实例;
- 实例化
Bean
; - 检测
parentBeanFactory
; - 初始化依赖的
Bean
; - 创建
Bean
。
Spring Bean 的作用域:
-
singleton
:Spring 的默认作用域,容器里拥有唯一的Bean
实例; -
prototype
:针对每个getBean()
请求,容器都会创建一个Bean
实例; -
request
:会为每个 Http 请求创建一个Bean
实例; -
session
:会为每个 session 创建一个Bean
实例; -
globalSession
:会为每个全局 Http Session 创建一个Bean
实例该作用域仅对 Portlet 有效。Portlet 规范提出的概念,该 Session 对构成某个 Portlet 应用的所有不同的 Portlet 所共享,因此仅在使用 PortletContext 时有用。
Spring Bean 的生命周期:
-
创建过程
-
销毁过程
- 若实现了
DisposableBean
接口,则会调用destroy()
方法; - 若配置了
destry-method
属性,则会调用其配置的销毁方法。
- 若实现了
AOP 的介绍和使用 {#aop-的介绍和使用}
软件工程中有一个基本原则关注点分离:不同的问题交给不同的部分去解决。
- 面向切面编程 AOP正 是此种技术的体现;
- 通用化功能代码的实现,对应的就是所谓的切面(Aspect);
- 业务功能代码和切面代码分开后,架构将变得高内聚低耦合;
- 确保功能的完整性:切面最终需要被合并到业务中(Weave 织入)。
AOP 的三种织入方式:
- 编译时织入:需要特殊的 Java 编译器在编译时生成完整的字节码,如 AspectJ;
- 类加载时织入:需要特殊的 Java 编译器在加载字节码时将切面的字节码融合进来,如 AspectJ 和 AspectWerkz;
- 运行时织入:Spring 采用的方式,通过动态代理的方式,实现简单。
实现一个简单的例子,在请求中通过 AOP 记录请求的来源地址:
编写 controller。
@RestController
public class HelloController {
@GetMapping("hello")
public String hello() {
return "Hello World!";
}
}
在pom.xml
文件中引入 AOP 相关依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写 AOP 类。
@Aspect
@Component
public class RequestLogAspect {
private static final Logger logger = LoggerFactory.getLogger(RequestLogAspect.class);
/**
* 定义切入点
*/
@Pointcut("execution(public * cool.ldw.framework.conttroller..*.*(..))")
public void webLog(){}
/**
* 切入的目标方法执行前执行此方法
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint){
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("IP : " + request.getRemoteAddr());
}
/**
* 切入的目标方法执行完毕后执行此方法
*/
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) {
// 处理完请求,返回内容
logger.info("RESPONSE : " + ret);
}
}
这样一个 AOP 类就写好了,它会切入 conttroller 包下所有类的所有public
方法。
@Pointcut
中execution
的用法:
execution
表达式是定义切入点时最常用的一种表达式,用于匹配特定的方法签名。其语法如下:
- execution(modifiers-pattern? return-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
具体各部分含义如下:
- modifiers-pattern : 可选部分,表示方法的访问修饰符,如
public
,protected
,private
,static
等。- return-type-pattern: 必需部分,表示方法的返回类型,可以为任意类型或特定类型。
- declaring-type-pattern: 可选部分,表示所在类的名称或者包名。
- name-pattern: 必需部分,表示方法名,可使用通配符匹配。
- param-pattern : 必需部分,表示方法参数列表,使用两个点
..
表示零个或多个任意参数,也可以指定具体的参数类型。- throws-pattern: 可选部分,表示方法可能抛出的异常。
下面是一个更详细的示例:
@Pointcut("execution(public * com.example.service.*Service.*(..))") public void serviceMethods() {} @Pointcut("execution(* com.example.repository.*Repository.*(..))") public void repositoryMethods() {} @Pointcut("executon(* com.example.controller.*Controller.*(..))") public void controllerMethods() {}
在这个示例中:
- 第一个
@Pointcut
匹配com.example.service
包中所有以 "Service" 结尾的类的公共方法。- 第二个
@Pointcut
匹配com.example.repository
包中所有以 "Repository" 结尾的类的所有方法。- 第三个
@Pointcut
匹配com.example.controller
包中所有以 "Controller" 结尾的类的所有方法。使用上述语法和示例,你可以根据具体需要编写符合要求的
execution
表达式。
AOP 的主要名词概念:
-
Aspect:通用功能的代码实现;
-
Target:被织入 Aspect 的对象;如
HelloController
; -
JoinPoint:可以作为切入点的机会,所有方法都可以作为切入点;如
hello()
方法; -
Pointcut:Aspect 实际被应用在的 JoinPoint,即定义切入点,支持正则;
-
Advice:类里的方法以及这个方法如何织入到目标方法的方式;
Advice 的种类:
- 前置通知(Before)
- 后置通知(AfterReturning)
- 异常通知(AfterThrowing)
- 最终通知(After)
- 环绕通知(Around)
-
Weaving:Aop的实现过程,将切面应用到实际对象创建一个新的代理对象的过程,对于 Spring 来说就是初始化 Context 中对象时完成织入操作。
Spring AOP 的原理 {#spring-aop-的原理}
Spring AOP 的实现:JdkProxy 和 Cglib
-
由
AopProxyFactory
根据AdvisedSupport
对象的配置来决定使用哪种实现方式; -
默认策略如果目标类是接口,则用
JDKProxy
来实现,否则用后者; -
JDKProxy 的核心:
InvocationHandler
接口和Proxy
类;实现原理是 Java 内部的反射机制。
反射机制在生成类的过程中比较高效。
通过反射接收被代理的类,并且被代理的类必须要实现
InvocationHandler
接口。 -
Cglib:以继承的方式动态生成目标类的代理。
实现原理是借助 ASM 实现。
ASM 在生成类之后的执行过程中比较高效,另外可通过缓存 ASM 生成的类可解决生成类过程中比较低效的问题。
如果目标类没有实现
InvocationHandler
接口,Spring 就会采用 Cglib 动态代理目标类。Cglib(Code Generation Library)是一个代码生成的类库,可以在运行时动态的生成某个类的子类,通过修改字节码实现代理。
Cglib 是通过继承的方式实现动态代理,所以某个类若是被
final
关键字修饰就无法使用 Cglib 实现动态代理。
代理模式:接口 + 真实实现类 + 代理类。
真实实现类和代理类都需要实现接口,在实例化时需要使用代理类,Spring AOP 需要做的就是生成一个代理类替换掉真实实现类对外提供服务。
来看一个 AOP 原理的简单例子:
支付宝用户只需使用支付功能,而无需关心钱如何从支付宝绑定的银行卡取出以及如何支付给店家的,这些都是委托给支付宝去实现。
首先编写支付接口Payment
:
public interface Payment {
void pay();
}
接着编写真实实现类RealPayment
:
public class RealPayment implements Payment {
@Override
public void pay() {
System.out.println("作为用户,我只关心支付功能。");
}
}
编写支付宝支付类AliPay
,即代理类:
public class AliPay implements Payment{
private Payment payment;
public AliPay(Payment payment) {
this.payment = payment;
}
public void beforePay() {
System.out.println("从银行取款");
}
@Override
public void pay() {
beforePay();
payment.pay();
afterPay();
}
public void afterPay() {
System.out.println("支付给商家");
}
}
最后编写测试方法:
public class ProxyDemo {
public static void main(String[] args) {
Payment proxy = new AliPay(new RealPayment());
proxy.pay();
}
/*输出:
从银行取款
作为用户,我只关心支付功能
支付给商家
*/
}
实现对pay()
方法织入了beforePay()
和afterPay()
的逻辑。
Spring 里的代理模式的实现:
- 真实实现类的逻辑包含在了
getBean()
方法里; getBean()
方法返回的实际上是Proxy
的实例;Proxy
实例是 Spring 采用JDKProxy
或CGLIB
动态生成的。
看看 Spring 中 getBean()
方法时如何生成代理类的:
getBean()
方法调用的都是doGetBean()
方法。
在doGetBean()
方法中通过一系列的方法调最终会调用到AbstractAutowireCapableBeanFactory.doCreateBean()
方法, 可以使用断点调试验证这一点:
doCreateBean()
方法也是比较复杂的,其中较为关键的地方是调用了initializeBean()
方法。
initializeBean()
方法中调用了BeanPostProcessors
中的方法,不过在这里我们重点关注调用的applyBeanPostProcessorsAfterInitialization()
方法。
在applyBeanPostProcessorsAfterInitialization()
方法中又调用了postProcessAfterInitialization()
方法。
继续点入postProcessAfterInitialization()
方法中发现已经来到接口处了。
实际上此处调用的是其实现类AbstractAutoProxyCreator
中该方法的重写。
在wrapIfNecessary()
方法中调用了createProxy()
方法。
在createProxy()
方法中最后返回的是proxyFactory.getProxy()
方法的返回值。
继续看getProxy()
方法中调用的createAopProxy()
方法的源码。
这里就是真正创建 AOP 代理的逻辑了。
它实际上调用的是其实现类DefaultAopProxyFactory.createAopProxy()
方法。
在DefaultAopProxyFactory.createAopProxy()
方法可以看到返回的不是ObjenesisCglibAopProxy
,就是JdkDynamicAopProxy
,具体根据配置情况返回。
由此证明了 Spring 中实现 AOP 的方式是JdkProxy 和 Cglib。