0

0

Spring 全家桶之 Spring Framework 5.3(五)- AOP

雪夜

雪夜

发布时间:2025-07-14 09:02:36

|

503人浏览过

|

来源于php中文网

原创

一、动态代理

aop 即 aspect oriented programming面向切面编程,它是基于面向对象编程之上的新的编程思想,是指将某段代码动态的切入到指定方法的指定位置并运行。

新建一个maven项目spring-bean-aop,导入依赖

代码语言:javascript代码运行次数:0运行复制
    5.3.13            org.springframework        spring-beans        ${spring-version}                org.springframework        spring-context        ${spring-version}                org.springframework        spring-test        ${spring-version}                junit        junit        4.12        test    

在util包定义一个计算器接口类,定义四个方法加减乘除,增加一个实现类,实现加减乘除四个方法

代码语言:javascript代码运行次数:0运行复制
public interface Calculator {    int add(int x, int y);    int sub(int x, int y);    int mul(int x, int y);    int div(int x, int y);}

在util.impl包中增加实现类

代码语言:javascript代码运行次数:0运行复制
@Componentpublic class AppleCalculator implements Calculator {    @Override    public int add(int x, int y) {        int result = x + y;        return result;    }    @Override    public int sub(int x, int y) {        int result = x - y;        return result;    }    @Override    public int mul(int x, int y) {        int result = x * y;        return result;    }    @Override    public int div(int x, int y) {        int result = x / y;        return result;    }}

增加application.xml,将组件注入容器中

代码语言:javascript代码运行次数:0运行复制
        

生成Spring Test测试类AppleCalculatorTest

代码语言:javascript代码运行次数:0运行复制
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:application.xml")public class AppleCalculatorTest {    @Autowired    private AppleCalculator appleCalculator;    @Test    public void add() {    }    @Test    public void sub() {    }    @Test    public void mul() {    }    @Test    public void div() {    }}

给每个方法运行前后增加日志输出,如何在不改变代码的前提下完成?

动态代理方式解决

首先创建一个代理CalculatorProxy,包含一个方法,可以返回代理对象,给代理对象加一些方法,通过代理对象执行目标方法

创建一个代理,静态方法getProxy用户获取Calculator的代理对象,

代码语言:javascript代码运行次数:0运行复制
public class CalculatorProxy {    public static Calculator getProxy(Calculator calculator){        // 方法执行器        InvocationHandler invocationHandler = (Object proxy, Method method, Object[] args) -> {            System.out.println("动态代理执行目标方法:" + method.getName());            // 反射执行目标方法            Object result = method.invoke(calculator, args);            return result;        };        // 类接口        Class[] interfaces = calculator.getClass().getInterfaces();        // 类加载器        ClassLoader classLoader = calculator.getClass().getClassLoader();        Object proxyInstance = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);        return (Calculator) proxyInstance;    }}

该方法需要传入被代理的对象,通过newProxyInstance()创建一个代理,再强转为目标类返回,其中参数invocationHandler为目标方法执行器,通过invoke方法执行目标方法并使用Object接收保存后返回目标方法执行后的返回值, intefaces为被代理类的类接口列表,classLoader为被代理类的类加载器

在测试类中通过getProxy方法获取Calculator的代理对象,使用代理对象执行方法

代码语言:javascript代码运行次数:0运行复制
@Testpublic void add() {    Calculator calculator = CalculatorProxy.getProxy(appleCalculator);    calculator.add(2,1);}

根据控制台打印可以看出,代理类成功执行了add方法

Spring 全家桶之 Spring Framework 5.3(五)- AOP

那么要实现在方法执行前后增加日志记录,就可以修改代理类CalculatorProxy的getProxy方法,在调用invoke方法前后增加日志输出,包括对方法执行异常时的处理

代码语言:javascript代码运行次数:0运行复制
public class CalculatorProxy {    public static Calculator getProxy(Calculator calculator){        // 方法执行器        InvocationHandler invocationHandler = (Object proxy, Method method, Object[] args) -> {            Object result = null;            try {                // 反射执行目标方法                // System.out.println("动态代理执行目标方法:" + method.getName());                System.out.println(method.getName() + "方法开始执行,参数为:" + Arrays.toString(args));                result = method.invoke(calculator, args);                System.out.println(method.getName() + "方法执行结束,结果为:" + result);            } catch (Exception e){                System.out.println(method.getName() + "方法执行异常,异常信息:" + e.getCause());            } finally {                System.out.println(method.getName() + "方法最终执行结束");                return result;            }        };        // 类接口        Class[] interfaces = calculator.getClass().getInterfaces();        // 类加载器        ClassLoader classLoader = calculator.getClass().getClassLoader();        Object proxyInstance = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);        return (Calculator) proxyInstance;    }}

再次执行测试类

Spring 全家桶之 Spring Framework 5.3(五)- AOP

定义异常情况

代码语言:javascript代码运行次数:0运行复制
@Testpublic void div() {    Calculator calculator = CalculatorProxy.getProxy(appleCalculator);    calculator.div(2,0);}

执行异常情况的测试方法div,输出了异常的原因

Spring 全家桶之 Spring Framework 5.3(五)- AOP

如果目标对象没有实现任何接口,是无法创建代理对象的,而Spring AOP可以解决这个问题,Spring AOP 底层就是动态代理

二、AOP
Spring 全家桶之 Spring Framework 5.3(五)- AOP

1. 切面(Aspect)被抽取出来的公共模块,可以用来会横切多个对象多个方法。Aspect切面可以看成Pointcut切点和Advice通知的结合,一个切面可以由多个切点和通知组成。

2. 连接点(Join point)指程序运行过程中所执行的方法。在Spring AOP中,一个连接点总代表一个方法的执行。

3. 切点(Pointcut)切点用于定义要对哪些连接点或者方法的执行前后(Join point)进行拦截。

4. 通知(Advice)指要在连接点(Join Point) 上执行的动作,即增强的逻辑。比如权限校验和、日志记录,事务控制等。通知有各种类型,包括Around、Before、After、After returning、After throwing。

如何使用AOP

首先增加Spring AOP的相关依赖

代码语言:javascript代码运行次数:0运行复制
    org.springframework    spring-aop    ${spring-version}    org.springframework    spring-aspects    ${spring-version}

新增aspect包,增加一个LogAspect切面类,@Component注解表示要将切面类注入到Spring容器中,@Aspect表示这是一个切面类,针对div方法定义了异常通知,对add方法定义了前置通知,后置通知返回通知和环绕通知

代码语言:javascript代码运行次数:0运行复制
@Component@Aspectpublic class LogAspect {    @Pointcut("execution(public * com.citi.util.impl.AppleCalculator.add(int, int))")    public void addPointCut(){    }    @Pointcut("execution(public * com.citi.util.impl.AppleCalculator.div(int, int))")    public void divPointCut(){    }    @Before("addPointCut()")    public void logStart(JoinPoint joinPoint ){        System.out.println("方法调用前的输出");        System.out.println("方法名:" + joinPoint.getSignature());        System.out.println("方法参数:" + Arrays.toString(joinPoint.getArgs()));    }    @After("addPointCut()")    public void logEnd(){        System.out.println("方法调用后的输出");    }    @AfterReturning("addPointCut()")    public void logReturn(){        System.out.println("方法输出返回后输出");    }    @AfterThrowing("divPointCut()")    public void logException(){        System.out.println("方法抛出异常后输出");    }    @Around("addPointCut()")    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {        System.out.println("环绕通知:方法执行前");        Object o = joinPoint.proceed();        System.out.println("环绕通知:方法执行后输出" + o.toString());        return o;    }}

编写测试类,注意注入的是Calculator接口类,不是AppleCalculator实现类

代码语言:javascript代码运行次数:0运行复制
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:application.xml")public class AppleCalculatorTest {    @Autowired    private Calculator calculator;    @Test    public void add() {        calculator.add(2,1);    }    @Test    public void div() {        calculator.div(2,0);    }}

增加xml配置,开启AOP

代码语言:javascript代码运行次数:0运行复制

执行测试,div的异常通知可以正常输出,add方法的前置后置返回通知和环绕通知也都可以正常输出

Spring 全家桶之 Spring Framework 5.3(五)- AOP
Spring 全家桶之 Spring Framework 5.3(五)- AOP
1.实现接口的情况下使用jdk动态代理创建动态代理对象

执行测试方法

代码语言:javascript代码运行次数:0运行复制
@Testpublic void testGetBean(){    System.out.println(calculator);    System.out.println(calculator.getClass());}
Spring 全家桶之 Spring Framework 5.3(五)- AOP

从Spring容器中获取目标对象一定要自动导入接口类,不要用本类,AOP底层是动态代理,容器中保存的组件是目标对象的代理对象

如果AppleCalculator不实现接口的情况下,使用cglib创建动态代理,修改AppleCalculator

代码语言:javascript代码运行次数:0运行复制
@Componentpublic class AppleCalculator {    public int add(int x, int y) {        int result = x + y;        return result;    }      public int sub(int x, int y) {        int result = x - y;        return result;    }       public int mul(int x, int y) {        int result = x * y;        return result;    }       public int div(int x, int y) {        int result = x / y;        return result;    }}

测试类中注入

代码语言:javascript代码运行次数:0运行复制
@Autowiredprivate AppleCalculator calculator;

执行testGetBean()测试方法,可以看出是CGLIB创建的代理对象

Spring 全家桶之 Spring Framework 5.3(五)- AOP
2.切入点表达式

固定格式: execution(访问权限符 返回值类型 方法全类名(参数表))

通配符:*:1)匹配一个或者多个字符:execution(public int com.citi.util.impl.AppleCar.(int, int))2)匹配任意一个参数:第一个是int类型,第二个参数任意类型;(匹配两个参数) execution(public int com.citi.util.impl.AppleCa.(int, *))3)*只能匹配一层路径4)权限位置不能使用*,权限位置不写就行或者public(可选)

vizcom.ai
vizcom.ai

AI草图渲染工具,快速将手绘草图渲染成精美的图像

下载

**..*1)匹配任意多个参数,任意类型参数2)匹配任意多层路径:execution(public int com.citi..AppleCa.*(..));

记住两种:最精确的:execution(public int com.citi.util.impl.AppleCalculator.add(int,int))最模糊的:execution(* .(..)),不要使用这种最模糊的切点表达式

"&&”、“||”、“!"&&:我们要切入的位置同时满足这两个表达式 execution(public int com.citi.util.impl.AppleCalculator.add(..))&&execution(* .(int,int))

||:满足任意一个表达式即可 execution(public int com.citi..AppleC*.(..))&&execution( .(int,int))

!:只要不是这个位置都切入!execution(public int com.citi..AppleC*.*(..))

3.通知方法的执行顺序

没有环绕通知的情况下,使用try/catch包裹:

代码语言:javascript代码运行次数:0运行复制
try{    @Before    // 方法执行    method.invoke(obj,args);    // 正常执行后返回    @AfterReturning}catch(){    // 异常执行    @AfterThrowing}finally{    @After}

正常执行:@Before前置通知->@AfterReturning正常返回->@After后置通知

异常执行:@Before前置通知->@After后置通知->@AfterThrowing异常通知

4.获取目标方法的详细信息

需要参数JoinPoint类,封装了目标方法的详细信息获取方法参数,joinPoint.getSignature())可以获取方法名,joinPoint.getArgs()可以获取参数列表

代码语言:javascript代码运行次数:0运行复制
@Before("addPointCut()")public void logStart(JoinPoint joinPoint ){    System.out.println(this.getClass().getName() + "切面类运行-目标方法调用前的输出-@Before前置通知");    System.out.println("目标方法名:" + joinPoint.getSignature());    System.out.println("目标方法参数:" + Arrays.toString(joinPoint.getArgs()));}

执行测试方法中的add()

获取返回值,需要定义一个变量来接收,方法在入参中,为了能接收各种类型的参数因此最好定义为Object类型

代码语言:javascript代码运行次数:0运行复制
@AfterReturning(value = "addPointCut()", returning = "result")public void logReturn(Object result){    System.out.println(this.getClass().getName() + "切面类运行-目标方法返回:" + result + ",@AfterReturning正常返回");}

异常同样

代码语言:javascript代码运行次数:0运行复制
@AfterThrowing(value = "divPointCut()", throwing = "e")public void logException(Exception e){    System.out.println(this.getClass().getName() + "切面类运行-目标方法抛出异常:" + e);}

异常也可以指定传入哪种异常,方法执行爆出的异常符合传入的异常才会打印出异常信息,否则不会,所以传入的异常类型要尽量是Exception,如果可以确切的知道方法爆出的异常,可以指定具体异常来接收, Result同样也可以指定具体类型来接收返回值

执行add方法和div方法的测试

Spring 全家桶之 Spring Framework 5.3(五)- AOP
Spring 全家桶之 Spring Framework 5.3(五)- AOP
5.切入点表达式代码语言:javascript代码运行次数:0运行复制
@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.add(int, int))")public void addPointCut(){}@Pointcut("execution(public * com.citi.util.impl.AppleCalculator.div(int, int))")public void divPointCut(){}

@Pointcut注解定一个切点表达式,方法内部不用实现任何功能

6.环绕通知

其实就是一个动态代理

代码语言:javascript代码运行次数:0运行复制
@Around("addPointCut()")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {    String methodName = joinPoint.getSignature().getName();    System.out.println("环绕通知-方法执行前");    Object o = null;    try {        System.out.println("环绕通知-前置通知," + methodName + "方法开始执行");        // 反射执行目标方法        o = joinPoint.proceed();        System.out.println("环绕通知-返回通知," + methodName + "返回值:" + o.toString());    } catch (Exception e){        System.out.println("环绕通知-异常通知," + methodName + "异常信息为:" + e);    } finally {        System.out.println("环绕通知-后置通知," + methodName + "方法执行结束");    }    return o;}

将普通通知的注解注释,执行add方法

Spring 全家桶之 Spring Framework 5.3(五)- AOP

将切入点表达式改为divPointcut,执行div方法的测试

Spring 全家桶之 Spring Framework 5.3(五)- AOP

普通通知和环绕通知同时存在时的执行顺序,将环绕通知的切点表达式改为addPointcut,将普通通知方法的注解注释取消,执行add方法的测试

Spring 全家桶之 Spring Framework 5.3(五)- AOP
7.有多个切面类的情况下的运行顺序

新增切面类

代码语言:javascript代码运行次数:0运行复制
@Component@Aspectpublic class VerifyAspect {    @Pointcut("execution(public * com.citi.util.impl.AppleCalculator.add(int, int))")    public void addPointCut(){    }    @Pointcut("execution(public * com.citi.util.impl.AppleCalculator.div(int, int))")    public void divPointCut(){    }    @Before("addPointCut()")    public void verifyStart(JoinPoint joinPoint ){        System.out.println(this.getClass().getName() + "切面类执行-目标方法调用前的输出-@Before");        //System.out.println("目标方法名:" + joinPoint.getSignature());        //System.out.println("目标方法参数:" + Arrays.toString(joinPoint.getArgs()));    }    @AfterReturning(value = "addPointCut()",returning = "result")    public void verifyReturn(Object result){        System.out.println(this.getClass().getName() + "切面类执行-方法执行后输出返回值:" + result + ",@AfterReturning正常返回");    }    @After(value = "addPointCut()")    public void verifyEnd(JoinPoint joinPoint){        System.out.println(this.getClass().getName() + "切面类执行-目标方法调用结束,@After后置通知");    }    @AfterThrowing(value = "divPointCut()",throwing = "e")    public void verifyException(Exception e){        System.out.println(this.getClass().getName() + "切面类执行-方法抛出异常:" + e);    }}

LogAspect暂时注销环绕通知

代码语言:javascript代码运行次数:0运行复制
@Component@Aspectpublic class LogAspect {    @Pointcut("execution(public * com.citi.util.impl.AppleCalculator.add(int, int))")    public void addPointCut(){    }    @Pointcut("execution(public * com.citi.util.impl.AppleCalculator.div(int, int))")    public void divPointCut(){    }    @Before("addPointCut()")    public void logStart(JoinPoint joinPoint ){        System.out.println(this.getClass().getName() + "切面类运行-目标方法调用前的输出-@Before前置通知");        //System.out.println("目标方法名:" + joinPoint.getSignature());        //System.out.println("目标方法参数:" + Arrays.toString(joinPoint.getArgs()));    }    @AfterReturning(value = "addPointCut()", returning = "result")    public void logReturn(Object result){        System.out.println(this.getClass().getName() + "切面类运行-目标方法返回:" + result + ",@AfterReturning正常返回");    }    @After("addPointCut()")    public void logEnd(JoinPoint joinPoint){        System.out.println(this.getClass().getName() + "方法调用结束,@After后置通知");    }    @AfterThrowing(value = "addPointCut()", throwing = "e")    public void logException(Exception e){        System.out.println(this.getClass().getName() + "切面类运行-目标方法抛出异常:" + e);    }    //@Around("addPointCut()")    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {        String methodName = joinPoint.getSignature().getName();        System.out.println("环绕通知-方法执行前");        Object o = null;        try {            System.out.println("环绕通知-前置通知," + methodName + "方法开始执行");            // 反射执行目标方法            o = joinPoint.proceed();            System.out.println("环绕通知-返回通知," + methodName + "返回值:" + o.toString());        } catch (Exception e){            System.out.println("环绕通知-异常通知," + methodName + "异常信息为:" + e);        } finally {            System.out.println("环绕通知-后置通知," + methodName + "方法执行结束");        }        return o;    }}

运行add方法的测试方法

Spring 全家桶之 Spring Framework 5.3(五)- AOP

执行顺序,先进后出

Spring 全家桶之 Spring Framework 5.3(五)- AOP

切面类先后执行是根据切面类名字首字母排序,讲VerifyAspect改为AerifyAspect,再次执行add的测试方法

Spring 全家桶之 Spring Framework 5.3(五)- AOP

要想改变按照切面类字母顺序执行切面类,可以在类上增加@Order注解,数值越小优先级越高,给LogAspect加上@Order(1),默认是2147483647,再次执行add方法的测试方法

Spring 全家桶之 Spring Framework 5.3(五)- AOP

有环绕通知的情况下,将LogAspect的环绕通知注解注释取消,并在输出中加上类名标识

Spring 全家桶之 Spring Framework 5.3(五)- AOP

环绕通知加在哪个切面,那个切面执行环绕,环绕通知优先执行,环绕通知中执行方法

AOP使用场景:

AOP加日志保存到数据库AOP作权限验证AOP作安全检查AOP作事务控制三、XML配置实现AOP 将LogAspect和AerifiyAspect类上及方法上的注解全部注释。基于注解的AOP实现步骤:将目标类和切面类上添加@Component,交个Spring容器管理切面类添加@Aspect,表明这是一个切面类切面类方法中添加通知方法的注解xml配置中开启AOP功能,并使用component-scan扫描所有包代码语言:javascript代码运行次数:0运行复制
                                                                                                                                                                                                                                                                                                                            复制代码

环绕前置通知的顺序和普通普通前置通知的顺序于xml配置先后顺序有关,如果先配置就先打印环绕通知,如果在前就先打印普通前置通知

执行测试

Spring 全家桶之 Spring Framework 5.3(五)- AOP

至此,Spring AOP 告一段落?

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

551

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

730

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

475

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

394

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

990

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

656

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

551

2023.09.20

c++主流开发框架汇总
c++主流开发框架汇总

本专题整合了c++开发框架推荐,阅读专题下面的文章了解更多详细内容。

23

2026.01.09

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
成为PHP架构师-自制PHP框架
成为PHP架构师-自制PHP框架

共28课时 | 2.4万人学习

YMP在线手册
YMP在线手册

共64课时 | 35万人学习

PostgreSQL 教程
PostgreSQL 教程

共48课时 | 6.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号