Spring AOP和Spring IoC 我真的有在学了

文章目录


学习到SpringBoot,有必要更进一步理解和学习Spring更深层的东西。这就学习和整理了这一篇文章。

参看

  1. 《深入理解JVM虚拟机》第三版
  2. https://spring.io
  3. https://mp.weixin.qq.com/s/WpRSitDqtgOuU9GnI1-HDw
  4. https://blog.csdn.net/xiaofeng10330111/article/details/105631666
  5. https://mp.weixin.qq.com/s/NXZp8a3n-ssnC6Y1Hy9lzw
  6. https://cloud.tencent.com/developer/article/1584491

中文版官方文档分享
https://www.jianshu.com/p/b3da0c8a22fe

一、Spring IOC

DI和IOC

Bean是包装的Object,无论是控制反转还是依赖注入,它们的主语都是object。Bean是Spring的主角。

Dependency Injection,依赖注入:容器动态的将某个依赖关系注入到组件之中。通过简单的配置,无需任何代码就可指定目标需要的资源,无需关心资源来自何处。**且被注入对象依赖IOC容器配置依赖对象。**可以说DI是IOC设计思想的实现。

Inversion of Control,控制反转。控制反转就是把创建和管理 bean 的过程转移给Spring IoC Container,对于 IoC 来说,最重要的就是容器。容器管理着 bean 的生命,控制着 bean 的依赖注入。

有了ioc容器,我们每次需要使用对象的时候,直接从容器中取出、使用即可,不用去关心如何创建、销毁等。

DI和IOC的关系

同一个概念的不同描述,IOC是一种设计思想,DI是一种实现。DI是IOC思想的实现,即容器管理Bean使用依赖注入技术实现。

当IOC容器实现了Bean的资源定位、载入和解析注册后,会对所管理的Bean进行依赖注入,发生依赖注入的情况:

  • 第一次调用BeanFactory的getBean()方法时,ioc容器触发依赖注入。
  • 关闭lazy-init懒加载时,容器在解析注册bean的时候会继续预实例化,触发依赖注入。

IOC容器初始化

分为四步:资源定位、载入、解析注册和依赖注入

IOC容器初始化的两个基本步骤:

  • 初始化的入口由 容器中的refresh()方法 调用完成。
  • 对Bean定义载入IOC容器使用的方法是 loadBeanDefinition()

大致过程:

  1. 通过ReasourceLoader来完成资源文件的定位,DefaultResourceLoader是默认的实现,同时上下文本身就给出了ResourceLoader的实现,可以通过类路径、文件系统、URL等方式来定位资源。
  2. 如果XmlBeanFactory作为IOC容器,那么需要为它指定Bean定义的资源,也就是说Bean定义文件是通过抽象成Resource来被IOC容器处理,容器通过BeanDefinitionReader来完成定义信息的解析和Bean信息的注册,往往使用XmlBeanDefinitionReader来解析Bean的XML定义文件—实际的处理过程是委托给BeanDefinitionParserDelegate来完成的,从而得到Bean的定义信息,这些信息在Spring中使用BeanDefinition来表示(这个名字可以让我们想到loadBeanDefinition()、registerBeanDefinition()这些相关的方法,他们都是为处理BeanDefinition服务的)。
  3. 解析得到BeanDefinition以后,需要在IOC容器中注册,这由IOC实现BeanDefinitionRegister接口来实现,注册过程就是在IOC容器内容维护一个HashMap来保存得到的BeanDefinition的过程,这个HashMap是IOC容器持有Bean信息的场所,以后Bean的操作都是围绕这个HashMap来实现。
  4. 之后我们通过BeanFactory和ApplicationContext来享受Spring IOC的服务了,在使用IOC容器的时候我们注意到,除了少量粘合代码,绝大多数以正确IOC风格编写的应用程序代码完全不关心如何到达工厂,因为容器将把这些对象与容器管理的其他对象钩在了一起,基本的策略是把工厂放到已知的地方,最好放在对预期使用的上下文有意义的地方,以及代码要实际访问工厂的地方。
  5. Spring本身提供了对声明式载入Web应用程序用法的应用程序上下文,并将其存储在ServletContext的框架实现中。

二、Spring AOP

aspect oriented programming,面向切面编程。不修改源代码的前提下,通过 预编译+运行时动态代理 实现在不同的业务组件添加某些通用功能。

它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角。在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)。


1.名词解释

JoinPoint(连接点)

程序在执行过程中,一个个的时间点,比如调用时、抛出异常时等,这些时间点可以注入切面代码(可选)。

Pointcut(切点)

JoinPoint表示切面代码可以被织入的地方,根据需求切入到某些点,就需要用到 Pointcut 定义规则匹配JoinPoint(Aspect Pointcut expression language 描述规则)。

Advice(增强)

在Pointcut匹配到JoinPoint后,就得通过 Advice 确定具体时机与逻辑,是切面代码真正被执行的地方,主要织入时机有:

  • Before Advice:前置增强,在目标方法执行前织入。
  • After Advice:后置增强,在目标方法执行后织入(不管是否有异常都会织入)。
  • After returning advice:返回后织入,在目标方法执行正常后织入(抛出异常则不会被织入)。
  • After throwing advice: 异常织入,目标方法执行过程中抛出异常后织入。
  • Around Advice:环绕织入,在目标方法执行前后都可织入切面代码,也可以选择是否执行原有正常的逻辑,如果不执行原有流程,它甚至可以用自己的返回值代替原有的返回值,甚至抛出异常。

在Around Advice中,环绕方法需要传入ProceedingJoinPoint对象,此对象调用proceed()就表示系统要执行的方法,在它上下添加自己想添加的代码逻辑即可实现环绕增强。

Aspect切面就是Pointcut+Advice实现的,它可以指定了哪些JoinPonit上织入哪些代码。

切点函数

execution()表达式

匹配指定的类、方法。

表达式切入点,配合 Pointcut 使用,它有一下三个符号:

  • *:0-n个字符。
  • ..:用在方法参数中,表示任意个参数。用在包命后,表示当前及其子包路径。
  • +: 用在类名后,表示当前及其子类。用在接口后,表示当前接口及其实现类。

示例(定义在 service 包里的任意类的任意方法):execution(* com.kkb.service.*.*(..))

@Pointcut(value = "execution(* com.pdh.service..*.*(..))")
private void test(){}

@annotation()

匹配被注解标注的方法。示例:@annotation(com.pdh.aop.LogAnnotation)

@Pointcut(value = "@annotation(com.pdh.aop.LogAnnotation)")
private void test(){}

@target()

匹配所有标注了指定注解的类。如 @target(org.springframework.stereotype.Service) 表示所有标注了@Service的类的所有方法。

当然,还有其他的切点函数,可以去查看官方使用手册。


2.快速使用

使用全注解配置使用Spring AOP功能。下列是在SpringBoot项目中使用Spring aop功能,SpringBoot自动整合了Spring中的依赖。

2.1 Spring AOP注解

列出一些常用注解:

  • @Aspect:切面。此注解用在切面类上,表示该类作为切面供容器读取。
  • @Pointcut:切点。此注解作用在方法上,表示该方法为一个切点,并且需要传入切点函数表达式,匹配指定的增强目标。

还有就是Advice增强的注解:@Before、@After、@AfterReturning、@AfterThrowing、@Around

2.2 使用示例

自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GlobalErrorCatch {
    // 参数
}

自定义Aspect

Pointcut+Advice(around)

@Aspect //使用@Aspect表示它是一个切面
@Component
public class TestAspect {
    // 1. 定义所有带有 GlobalErrorCatch 的注解的方法为 Pointcut
    @Pointcut("@annotation(com.example.demo.annotation.GlobalErrorCatch)")
    private void globalCatch(){}

    // 2. 将 around advice 作用于 globalCatch(){} 此 PointCut 
    @Around("globalCatch()")
    public Object handlerGlobalError(ProceedingJoinPoint point) throws Throwable {
        try {
            long beginTime = System.currentTimeMillis();
            // 执行方法
            return point.proceed();
            long time = System.currentTimeMillis() - beginTime;
            System.out.println("执行时长" + (time - beginTime) + "ms");
        } catch (Exception e) {
            System.out.println("执行错误" + e);
            return ServiceResultTO.buildFailed("系统错误");
        }
    }
}

自定义Service

public class TestService {
   @Override
   @GlobalErrorCatch
   public String test() {
        // 此处写服务里的执行逻辑
        retrun "我执行";
   }
}

通过这样的方式,所有标记着 GlobalErrorCatch 注解的方法都会统一在 handlerGlobalError 方法里执行,而不用在每一个业务代码处使用try-catch处理异常。

在体会到SpringAop的效果之后,我还想了解它的原理,你给我写!


3.Proxy代理

在学习和理解AOP的运行原理之前,先了解一下代理。

3.1 代理简介

先看一个场景,我要买一个手机,我不会直接和手机厂商对接,而是会和手机店打交道(也可以是其他渠道),手机店就是代理,手机厂商就是目标对象,我就是调用者,代理不仅实现了目标对象的行为,还可以添加自己的动作。

Spring AOP和Spring IoC 我真的有在学了

UML图:

Spring AOP和Spring IoC 我真的有在学了

Client直接和Proxy打交道。Proxy和RealSubject都实现了Subject接口,RealSubject类就是目标类,这里假定它有request()方法,但是通过Proxy后就加上了额外的方法(preRequest()、afterRequest()、… …)。


3.2 代理分类

代理分为静态代理和动态代理,而动态代理又分为 JDK动态代理和CGLIB动态代理

我们都知道Java程序的运行机制:Java源代码 -> 字节码 -> JVM类加载 -> 内存区域执行代码。【点击我查看类加载机制】。由Java程序执行原理来看,字节码是关键,静态和动态的区别就在于字节码生成的时机。

静态代理

代理类由开发人员编写或工具自动生成源代码,在编译时将代理类(委托类)就会确定下来,在程序运行前代理类的.class文件就已经存在。字节码文件会被修改(相当于未进行静态代理的情况)。

这里可以根据上面的 UML 图,可以手写实现静态代理。

静态代理主要两大劣势

  • 代理类只能代理一个委托类(单一职责原则),由于静态代理必须在编译前确定代理类,当需要代理多个委托类时,就要编写多个代理类
  • 当需要代理类实现某种功能,比如执行时间计算的时候,在相应的方法开始前、开始后分别织入计算时间的代码,那么每个方法都会重复这一计算时间的代码

这些缺点能不能解决?能,基于动态代理动态生成代理类,就不用重复编写代码。

动态代理

Spring AOP支持 JDK动态代理 和 CGLIB动态代理

在程序运行之前是没有代理对象产生的,在程序运行后通过反射创建生成字节码再由 JVM 加载而成。动态代理避免了静态代理那样的硬编码。动态代理分为 JDK动态代理 和 CGLIB动态代理,Spring AOP使用的是CGLIB动态代理。

JDK动态代理

原理:JDK动态代理只提供接口的代理,不支持类的代理,要求委托类(目标对象)实现任意接口(目标对象中需要代理的方法必须是接口有的),代理方式调用时,还必须传入委托类实现的接口对象

JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类,当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起。

这样可避免静态代理代码重复,代码可维护性极大提高。

但由于使用JDK动态代理必须要传递委托类实现的接口对象,这点并不算好,就有了继承自委托类的 CGLIB动态代理。

CGLIB动态代理

JDK 动态代理使用 Proxy 来创建代理类,增强逻辑写在 InvocationHandler.invoke() 里,CGlib 动态代理也提供了类似的 Enhance 类,增强逻辑写在 MethodInterceptor.intercept() 中,委托类的非final方法都会被方法拦截器拦截。

简单来说,CGLIB动态代理是由拦截器拦截住委托类的要执行的方法,继承委托类,重写委托类的非final方法,在重写方法中实现代码增强。

JDK动态代理通过反射执行委托类的方法(反射效率较低),而 CGlib 采用了FastClass 的机制来实现对被拦截方法的调用FastClass 机制是对一个类的方法建立索引,通过索引来直接调用相应的方法

综合来看CGLIB动态代理具有:动态生成代理对象、不用继承接口、FastClass执行效率高于反射等优点。


4.AOP运行流程

在学习了解到上面提到的名词解释、使用示例、Proxy代理之后,这里可以梳理一下AOP的运行步骤。

  1. 解析xml配置 或 获取被@Aspect注解修饰的类信息。
  2. 实例化所有的bean。
  3. 解析aop的config信息:
    • 解析aop aspect得到aspect引用的对象(逻辑提供类);
    • 解析aop aspect里面的每一个切面。
  4. 得到该aspect对应的pointcut切点,再得到pointcut对应的pointcut的切点表达式。
  5. 使用切点表达式中用于匹配类型部分,去spring ioc容器中与所有的bean的类型进行匹配,把匹配到的对象作为spring动态代理的目标对象。
  6. 如果目标对象实现了接口,使用JDK的动态代理包装;如果目标对象没有实现接口,使用cglib包装。
  7. 得到配置的 拦截时机 + 逻辑提供类的对应方法(从method解析) + Pointcut表达式中方法的匹配模式 创建一个拦截器。
  8. 再把该拦截器使用对应的动态代理机制转换为代理对象,替换spring容器中的对应bean的实例,执行对应的方法。

这就是Spring AOP的运行流程,其中很多的点,Spring官方都采用了非常精妙的设计,推荐去阅读一下SpringAop的源码,收获颇多。

JDK动态代理图:

Spring AOP和Spring IoC 我真的有在学了

CGLIB动态代理图:
Spring AOP和Spring IoC 我真的有在学了

上一篇:Spring核心技术IOC和AOP


下一篇:java基础面试题(八)