草庐IT

Spring AOP全面详解(超级详细)

mikechen的互联网架构 2023-04-16 原文

如果说 IOC 是 Spring 的核心,那么面向切面编程AOP就是 Spring 另外一个最为重要的核心@mikechen

AOP的定义

AOP (Aspect Orient Programming),直译过来就是 面向切面编程,AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。

面向切面编程,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术,如下图所示:

AOP可以拦截指定的方法并且对方法增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离,比如Spring的事务,通过事务的注解配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略。

 

AOP的作用

AOP 采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。

主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。

简单的说,AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能。

 

AOP的应用场景

比如典型的AOP的应用场景:

  • 日志记录
  • 事务管理
  • 权限验证
  • 性能监测

AOP可以拦截指定的方法,并且对方法增强,比如:事务、日志、权限、性能监测等增强,而且无需侵入到业务代码中,使业务与非业务处理逻辑分离。

 

Spring AOP的术语

在深入学习SpringAOP 之前,让我们先对AOP的几个基本术语有个大致的概念。

AOP核心概念

Spring AOP 通知分类

Spring AOP 织入时期

 

Spring AOP三种使用方式

AOP编程其实是很简单的事情,纵观AOP编程,程序员只需要参与三个部分:

1、定义普通业务组件

2、定义切入点,一个切入点可能横切多个业务组件

3、定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作

所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。

方式1:使用Spring自带的AOP

public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice,MethodInterceptor {
    @Override
    public void before(Method method, Object[] objects, Object target) throws Throwable {
        //前置通知
    }

    @Override
    public void afterReturning(Object result, Method method, Object[] objects, Object target) throws Throwable {
        //后置通知
    }
     @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        //环绕通知
        //目标方法之前执行
        methodInvocation.proceed();   //目标方法

        //目标方法之后执行
        return resultVal;
    }
}

 

配置通知时需实现org.springframework.aop包下的一些接口

  • 前置通知:MethodBeforeAdvice
  • 后置通知:AfterReturningAdvice
  • 环绕通知:MethodInterceptor
  • 异常通知:ThrowsAdvice

创建被代理对象

<bean id="orderServiceBean" class="com.apesource.service.impl.OrderServiceImpl"/>
<bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/>

 

通知(Advice)

<bean id="logAdviceBean" class="com.apesource.log.LogAdvice"/>
<bean id="performanceAdviceBean" class="com.apesource.log.PerformanceAdvice"/>

 

切入点(Pointcut):通过正则表达式描述指定切入点(某些 指定方法)

<bean id="createMethodPointcutBean"         class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <!--注入正则表达式:描述那些方法为切入点-->
    <property name="pattern" value=".*creat.*"/>
</bean>

 

Advisor(高级通知) = Advice(通知) + Pointcut(切入点)

<bean id="performanceAdvisorBean" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <!--注入切入点-->
        <property name="pointcut" ref="createMethodPointcutBean"/>

        <!--注入通知-->
        <property name="advice" ref="performanceAdviceBean"/>
    </bean>

 

创建自动代理

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <!--Bean名称规则(数组):指定那些bean创建自动代理-->
        <property name="beanNames">
            <list>
                <value>*ServiceBean</value>
                <value>*TaskBean</value>
            </list>
        </property>

        <!--通知列表:需要执行那些通知-->
        <property name="interceptorNames">
            <list>
                <value>logAdviceBean</value>
                <value>performanceAdvisorBean</value>
            </list>
        </property>
</bean>

 

方式2:使用Aspectj实现切面(普通POJO的实现方式)

导入Aspectj相关依赖

<!--aop依赖1:aspectjrt -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.5</version>
</dependency>

<!--aop依赖2: aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

 

通知方法名随便起,没有限制

public class LogAspectj {
    //前置通知
    public void beforeAdvice(JoinPoint joinPoint){
        System.out.println("========== 【Aspectj前置通知】 ==========");
    }

    //后置通知:方法正常执行后,有返回值,执行该后置通知:如果该方法执行出现异常,则不执行该后置通知 
    public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){
        System.out.println("========== 【Aspectj后置通知】 ==========");
    }
    public void afterAdvice(JoinPoint joinPoint){
        System.out.println("========== 【Aspectj后置通知】 ==========");
    }

    //环绕通知
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("##########【环绕通知中的前置通知】##########");
        Object returnVale = joinPoint.proceed();
        System.out.println("##########【环绕通知中的后置通知】##########");
        return returnVale;
    }

    /**
     * 异常通知:方法出现异常时,执行该通知
     */
    public void throwAdvice(JoinPoint joinPoint, Exception ex){
        System.out.println("出现异常:" + ex.getMessage());
    }

}

 

使用Aspectj实现切面,使用Spring AOP进行配置

<!--业务组件bean-->
<bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/>

<!--日志Aspect切面-->
<bean id="logAspectjBean" class="com.apesource.log.LogAspectj"/>

<!--使用Aspectj实现切面,使用Spring AOP进行配置-->
<aop:config>
    <!--配置切面-->
    <!--注入切面bean-->
    <aop:aspect ref="logAspectjBean">
        <!--定义Pointcut:通过expression表达式,来查找 特定的方法(pointcut)-->
        <aop:pointcut id="pointcut"
                     expression="execution(* com.apesource.service.impl.*.create*(..))"/>

        <!--配置"前置通知"-->
        <!--在pointcut切入点(serviceMethodPointcut)查找到 的方法执行"前",
            来执行当前logAspectBean的doBefore-->
        <aop:before method="beforeAdvice" pointcut-ref="pointcut"/>

        <!--配置“后置通知”-->
        <!--returning属性:配置当前方法中用来接收返回值的参数名-->
        <aop:after-returning returning="returnVal" 
                             method="afterReturningAdvice" pointcut-ref="pointcut"/> 
        <aop:after method="afterAdvice" pointcut-ref="pointcut"/>
        
        <!--配置"环绕通知"-->
        <aop:around method="aroundAdvice" pointcut-ref="pointcut"/>
        
        <!--配置“异常通知”-->
        <!--throwing属性:配置当前方法中用来接收当前异常的参数名-->
        <aop:after-throwing throwing="ex" method="throwAdvice" pointcut-ref="pointcut"/>
    </aop:aspect>

</aop:config>

 

方式3:使用Aspectj实现切面(基于注解的实现方式)

//声明当前类为Aspect切面,并交给Spring容器管理
@Component
@Aspect
public class LogAnnotationAspectj {
    private final static String EXPRESSION = 
                            "execution(* com.apesource.service.impl.*.create*(..))";

    //前置通知   
    @Before(EXPRESSION)
    public void beforeAdvice(JoinPoint joinPoint){
        System.out.println("========== 【Aspectj前置通知】 ==========");
    }


    //后置通知:方法正常执行后,有返回值,执行该后置通知:如果该方法执行出现异常,则不执行该后置通知
    @AfterReturning(value = EXPRESSION,returning = "returnVal")
    public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){
        System.out.println("========== 【Aspectj后置通知】 ==========");
    }

    //后置通知
    @After(EXPRESSION)
    public void afterAdvice(JoinPoint joinPoint){
        System.out.println("========== 【Aspectj后置通知】 ==========");
    }

    //环绕通知
    @Around(EXPRESSION)
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("##########【环绕通知中的前置通知】##########");
        Object returnVale = joinPoint.proceed();
        System.out.println("##########【环绕通知中的后置通知】##########");
        return returnVale;
    }

    // 异常通知:方法出现异常时,执行该通知
    @AfterThrowing(value = EXPRESSION,throwing = "ex")
    public void throwAdvice(JoinPoint joinPoint, Exception ex){
        System.out.println("********** 【Aspectj异常通知】执行开始 **********");
        System.out.println("出现异常:" + ex.getMessage());
        System.out.println("********** 【Aspectj异常通知】执行结束 **********");
    }

}
<!-- 自动扫描器 -->
<context:component-scan base-package="com.apesource"/>

<!--配置Aspectj的自动代理-->
<aop:aspectj-autoproxy/>
 

 

Spring AOP的实现原理

Spring的AOP实现原理其实很简单,就是通过动态代理实现的。

Spring AOP 采用了两种混合的实现方式:JDK 动态代理和 CGLib 动态代理。

  • JDK动态代理:Spring AOP的首选方法。 每当目标对象实现一个接口时,就会使用JDK动态代理。目标对象必须实现接口
  • CGLIB代理:如果目标对象没有实现接口,则可以使用CGLIB代理。

JDK动态代理

Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。

JDK实现动态代理需要两个组件,首先第一个就是InvocationHandler接口。

我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。

如下源码所示:

/**
 * 动态代理
 *
 * @author mikechen
 */
public class JdkProxySubject implements InvocationHandler {

    private Subject subject;

    public JdkProxySubject(Subject subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("before 前置通知");
        Object result = null;

        try {
            result = method.invoke(subject, args);
        }catch (Exception ex) {
            System.out.println("ex: " + ex.getMessage());
            throw ex;
        }finally {
            System.out.println("after 后置通知");
        }
        return result;
    }
}

 

然后JDK动态代理需要使用的第二个组件就是Proxy这个类,我们可以通过这个类的newProxyInstance方法,返回一个代理对象。

生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke方法。

public class Main { public static void main(String[] args) { 
   //获取InvocationHandler对象 在构造方法中注入目标对象 
   InvocationHandler handler = new JdkProxySubject(new RealSubject()); 

   //获取代理类对象 
  Subject proxySubject = (Subject)Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{Subject.class}, handler); 

   //调用目标方法
   proxySubject.request(); proxySubject.response();

}

 

运行结果:

before 前置通知
执行目标对象的request方法......
after 后置通知
before 前置通知
执行目标对象的response方法......
after 后置通知

 

JDK动态代理优缺

优点

JDK动态代理是JDK原生的,不需要任何依赖即可使用;

通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;

缺点

如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;

JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。

JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;

 

CGLib代理

CGLIB组成结构

Cglib是一个强大的、高性能的代码生成包,它广泛被许多AOP框架使用,为他们提供方法的拦截,如下图所示Cglib与Spring等应用的关系:

  • 最底层的是字节码Bytecode,字节码是Java为了保证“一次编译、到处运行”而产生的一种虚拟指令格式,例如iload_0、iconst_1、if_icmpne、dup等
  • 位于字节码之上的是ASM,这是一种直接操作字节码的框架,应用ASM需要对Java字节码、Class结构比较熟悉
  • 位于ASM之上的是CGLIBGroovyBeanShell,后两种并不是Java体系中的内容而是脚本语言,它们通过ASM框架生成字节码变相执行Java代码,这说明在JVM中执行程序并不一定非要写Java代码----只要你能生成Java字节码,JVM并不关心字节码的来源,当然通过Java代码生成的JVM字节码是通过编译器直接生成的,算是最“正统”的JVM字节码
  • 位于CGLIBGroovyBeanShell之上的就是HibernateSpring AOP这些框架了,这一层大家都比较熟悉
  • 最上层的是Applications,即具体应用,一般都是一个Web项目或者本地跑一个程序

所以,Cglib的实现是在字节码的基础上的,并且使用了开源的ASM读取字节码,对类实现增强功能的。

 

以上

作者简介

陈睿|mikechen,10年+大厂架构经验,《BAT架构技术500期》系列文章作者,分享十余年BAT架构经验以及面试心得!

阅读mikechen的互联网架构更多技术文章合集

Java并发|JVM|MySQL|Spring|Redis|分布式|高并发|架构师

有关Spring AOP全面详解(超级详细)的更多相关文章

  1. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  2. 在VMware16虚拟机安装Ubuntu详细教程 - 2

    在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主

  3. 100个python算法超详细讲解:画直线 - 2

    1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva

  4. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  5. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  6. H2数据库配置及相关使用方式一站式介绍(极为详细并整理官方文档) - 2

    目录H2数据库入门以及实际开发时的使用1.H2数据库的初识1.1H2数据库介绍1.2为什么要使用嵌入式数据库?1.3嵌入式数据库对比1.3.1性能对比1.4技术选型思考2.H2数据库实战2.1H2数据库下载搭建以及部署2.1.1H2数据库的下载2.1.2数据库启动2.1.2.1windows系统可以在bin目录下执行h2.bat2.1.2.2同理可以通过cmd直接使用命令进行启动:2.1.2.3启动后控制台页面:2.1.3spring整合H2数据库2.1.3.1引入依赖文件2.1.4数据库通过file模式实际保存数据的位置2.2H2数据库操作2.2.1Mysql兼容模式2.2.2Mysql模式

  7. 华为ensp详细安装包、安装教程及所遇问题 - 2

    目录一、安装包链接二、安装详细步骤1.安装Wireshark和WinPcap2.安装OracleVMVirtualBox3.安装ensp三、安装后注册四、启动路由器出现40错误怎么解决一、安装包链接二、安装详细步骤链接:https://pan.baidu.com/s/1QbUUYMOMIV2oeIKHWP1SpA?pwd=xftx提取码:xftx1.安装Wireshark和WinPcap找到Wireshark安装包所在文件夹,双击它,按照以下步骤安装。2.安装OracleVMVirtualBox找到OracleVMVirtualBox安装包所在文件夹,双击它,按照以下步骤安装。注:可自定义安装

  8. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  9. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  10. Linux操作系统CentOS7安装Nginx[详细版] - 2

    Nginx安装1.官网下载Nginx2.使用XShell和Xftp将压缩包上传到Linux虚拟机中3.解压文件nginx-1.20.2.tar.gz4.配置nginx5.启动nginx6.拓展(修改端口和常用命令)(一)修改nginx端口(二)常用命令1.官网下载Nginxhttp://nginx.org/en/download.html这里我下载的是1.20.2版本,大家按需下载对应稳定版即可2.使用XShell和Xftp将压缩包上传到Linux虚拟机中没有XShell可以参考《Linux操作系统CentOS7连接XShell》3.解压文件nginx-1.20.2.tar.gz1)检查是否存

随机推荐