前言
本章节我们继续学习 AspectJ!
AspectJ是一个基于Java语言的面向切面编程(AOP)的扩展框架,它的诞生解决了很多传统面向对象编程的问题。在传统的面向对象编程中,开发者通常会将一些通用功能或者横切关注点(cross-cutting concern)手动地嵌入到业务逻辑中,导致代码难以维护和理解。而AspectJ的出现则让开发者可以通过一种简单而优雅的方式来解决这些问题,使得应用程序的代码变得更加清晰、易于维护。
在本文中,我们将介绍AspectJ的基本概念、语法和使用方法。首先,我们将简要介绍AOP的概念,并与传统的面向对象编程作对比,以突出AspectJ的优越性。其次,我们将详细阐述AspectJ的语法和组成部分,包括切入点、通知和切面等。最后,我们将提供一些实际的示例来帮助读者更好地理解如何使用AspectJ来解决常见的应用程序开发问题。
相信通过本文的介绍,读者将会深入理解AspectJ的强大之处,掌握如何使用这个框架来构建高效、易于维护的应用程序。
一、开始学习
1、新建项目,结构如下
2、添加 spring 依赖
<!-- spring 的核心依赖 -->
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.8</version>
</dependency>
</dependencies>
3、在 service 包下新建一个类 UserService 类
@Slf4j
public class UserService {
/**
* 目标方法,也就是需要被增强的方法
* 因此它就是一个连接点
* @param name
*/
public String add(String name){
log.info("添加用户..." + name);
return "success";
}
}
4、在 aspect 包下新建一个 ServiceAspect 切面类
@Slf4j
public class ServiceAspect {
/**
* 自定义前置通知,可以给一个参数
* 这个参数为连接点(JoinPoint)
* 通过这个连接点可以拦截目标方法的参数等信息
*
* @param jp
*/
public void before(JoinPoint jp) {
log.info("执行前置通知,拦截的目标方法参数:" + jp.getArgs()[0]);
}
/**
* 后置通知
*
* @param jp 连接点
* @param returnVal 目标的方法的返回值
*/
public void afterReturning(JoinPoint jp, Object returnVal) {
log.info("后置通知,目标方法返回值:" + returnVal);
}
/**
* 环绕通知
*
* @param jp 连接点,继承 JoinPoint 接口
* @return
*/
public Object around(ProceedingJoinPoint jp) throws Throwable {
log.info("环绕通知前,目标方法参数:" + jp.getArgs()[0]);
// 调用目标方法对象的方法
Object returnVal = jp.proceed();
log.info("环绕通知后,目标方法返回值:" + returnVal);
return returnVal;
}
/**
* 异常通知,当目标对象产生异常时会执行
* 后置通知将不会再生效
*
* @param jp 连接点
* @param e 目标方法产生的异常对象
*/
public void afterThrowing(JoinPoint jp, Exception e) {
log.info("异常通知,异常信息:" + e.getMessage());
}
/**
* 最终通知,不管有没有异常产生最终通知都会被执行
* @param jp 连接点
*/
public void after(JoinPoint jp){
log.info("最终通知");
}
}
这个示例中的ServiceAspect类是一个使用AspectJ编写的切面类,其中包含了几个不同类型的通知方法。
before(JoinPoint jp)
方法是一个前置通知。在目标方法执行之前被拦截执行。它接收一个JoinPoint对象作为参数,可以通过该对象获取目标方法的参数信息。在这个示例中,我们使用log.info方法记录了拦截的目标方法参数。
afterReturning(JoinPoint jp, Object returnVal)
方法是一个后置通知。在目标方法成功返回之后被拦截执行。它接收一个JoinPoint对象和目标方法的返回值作为参数。在这个示例中,我们使用log.info方法记录了目标方法的返回值。
around(ProceedingJoinPoint jp)
方法是一个环绕通知。它可以在目标方法执行的前后进行处理。在这个示例中,我们首先通过log.info方法记录了目标方法的参数信息,然后调用jp.proceed()
方法来执行目标方法,最后再通过log.info方法记录了目标方法的返回值。注意,在环绕通知中必须显式地调用jp.proceed()
方法来触发目标方法的执行。
afterThrowing(JoinPoint jp, Exception e)
方法是一个异常通知。当目标方法抛出异常时被拦截执行。它接收一个JoinPoint对象和抛出的异常对象作为参数。在这个示例中,我们使用log.info方法记录了异常信息。
after(JoinPoint jp)
方法是一个最终通知。无论目标方法是否成功执行,最终总会被执行。在这个示例中,我们使用log.info方法输出了一条简单的日志。
通过编写这些通知方法,我们可以根据不同的需求,在目标方法的不同执行阶段进行拦截和处理。例如,可以在前置通知中进行参数验证,后置通知中记录返回值,异常通知中处理异常情况等。AspectJ提供了灵活的语法和丰富的可操作性,使得开发人员可以轻松地实现横切关注点的处理。
5、在 rsources 下新建一个 beans.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 装配目标对象 -->
<bean id="userService" class="edu.nf.ch20.service.UserService"/>
<!-- 装配切面 -->
<bean id="serviceAspect" class="edu.nf.ch20.aspect.ServiceAspect"/>
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="myPointcut" expression="execution(* edu.nf.ch20.service.UserService.*(..))"/>
<!-- 配置切面,ref 引用切面的 bean 的 id -->
<aop:aspect ref="serviceAspect">
<!-- 配置各种通知,method 属性值指定通知的方法名,
pointcut-ref 属性引用切入点的 bean 的 id
-->
<!-- 前置通知 -->
<aop:before method="before" pointcut-ref="myPointcut"/>
<!-- 注意:后置通知有一个 returning 属性对应方法返回值的参数名-->
<aop:after-returning method="afterReturning" pointcut-ref="myPointcut" returning="returnVal"/>
<!-- <aop:around>标签用于配置环绕通知,即在目标方法执行前后进行一些额外的处理。
method="around"表示环绕通知的方法名是"around",这个方法需要在配置类中定义。
pointcut-ref="myPointcut"表示引用了名为"myPointcut"的切点,即指定了目标方法的执行位置。 -->
<aop:around method="around" pointcut-ref="myPointcut"/>
<!-- <aop:after>标签用于配置最终通知,即在目标方法执行之后进行一些额外的处理。
method="after"表示后置通知的方法名是"after",这个方法需要在配置类中定义。
pointcut-ref="myPointcut"表示引用了名为"myPointcut"的切点,即指定了目标方法的执行位置。 -->
<aop:after method="after" pointcut-ref="myPointcut"/>
<!-- <aop:after-throwing>标签用于配置异常通知,即在目标方法抛出异常时进行一些额外的处理。
method="afterThrowing"表示异常通知的方法名是"afterThrowing",这个方法需要在配置类中定义。
pointcut-ref="myPointcut"表示引用了名为"myPointcut"的切点,即指定了目标方法的执行位置。
throwing="e"表示在异常通知方法中传递了一个名为"e"的参数,用于接收目标方法抛出的异常。 -->
<aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut" throwing="e"/>
</aop:aspect>
</aop:config>
</beans>
XML配置代码是使用Spring AOP来实现切面编程的示例。XML配置代码是使用Spring AOP来实现切面编程的示例。
<!-- 装配目标对象 --> <bean id="userService" class="edu.nf.ch20.service.UserService"/> <!-- 装配切面 --> <bean id="serviceAspect" class="edu.nf.ch20.aspect.ServiceAspect"/>
在这个示例中,
userService
是一个具体的业务类,serviceAspect
是一个切面类,用于包含横切关注点的逻辑。继续,配置AOP:
<aop:config> <!-- 配置切入点 --> <aop:pointcut id="myPointcut" expression="execution(* edu.nf.ch20.service.UserService.*(..))"/> <!-- 配置通知 --> <aop:aspect ref="serviceAspect"> <aop:before method="before" pointcut-ref="myPointcut"/> <aop:after-returning method="afterReturning" pointcut-ref="myPointcut" returning="returnVal"/> <aop:around method="around" pointcut-ref="myPointcut"/> <aop:after method="after" pointcut-ref="myPointcut"/> <aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut" throwing="e"/> </aop:aspect> </aop:config>
在
<aop:config>
标签中,首先定义了切入点(pointcut),用于确定哪些方法将被拦截。在这个示例中,切入点被命名为myPointcut
,它使用了表达式语言来匹配edu.nf.ch20.service.UserService
类的所有方法。然后,通过
<aop:aspect>
标签配置了不同类型的通知(advice),包括<aop:before>
(前置通知)、<aop:after-returning>
(后置通知)、<aop:around>
(环绕通知)、<aop:after>
(最终通知)和<aop:after-throwing>
(异常通知)。这些通知指向了切面类中对应的方法。总结来说,这段XML配置代码实现了对
edu.nf.ch20.service.UserService
类的方法进行切面编程,并在不同的执行阶段触发对应的通知方法。这样可以将横切关注点的业务逻辑与核心业务逻辑分离,提高了代码的可维护性和可重用性。具体来说,这段代码的作用如下:
配置目标对象和切面:通过配置
<bean>
元素,将目标对象和切面类实例化并装配到Spring容器中。目标对象是具体的业务类,而切面类包含了与横切关注点相关的逻辑。配置切入点:使用
<aop:pointcut>
元素配置切入点,即确定哪些方法将被拦截和应用切面的逻辑。在这个示例中,切入点使用表达式execution(* edu.nf.ch20.service.UserService.*(..))
,表示拦截edu.nf.ch20.service.UserService
类的所有方法。配置通知:通过
<aop:before>
、<aop:after-returning>
、<aop:around>
、<aop:after>
和<aop:after-throwing>
等元素,配置了不同类型的通知(advice)。这些通知指向切面类中的相应方法,并在目标对象的方法执行前、后、返回结果时、发生异常时等特定时机触发执行。通过以上配置,可以实现以下主要功能:
前置通知(Before Advice):在目标方法执行前执行切面中的方法,可以做一些准备工作、参数验证等操作。
后置通知(After Advice):在目标方法执行后执行切面中的方法,可以进行一些清理工作、记录日志、统计时间等操作。
环绕通知(Around Advice):在目标方法执行前后,或者替代目标方法执行,都可以执行切面中的方法。可以自由控制目标方法的执行,并进行一些额外的处理。
最终通知(After-returning Advice):在目标方法正常返回结果后执行切面中的方法,可以对返回结果进行处理或记录日志。
异常通知(After-throwing Advice):在目标方法发生异常时执行切面中的方法,可以处理异常、记录日志或进行补偿操作。
通过将横切关注点与核心业务逻辑分离,AOP提供了一种更加灵活和可维护的代码组织方式。它可以帮助开发人员在不改变原有业务逻辑的情况下,统一处理与业务无关的横切关注点,提高代码的可重用性、可维护性和可测试性。
6、测试
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService bean = context.getBean(UserService.class);
bean.add("qiu");
}
}
运行结果
二、总结
上一章我们使用了MethodBeforeAdvice、AfterReturningAdvice、MethodInterceptor和ThrowsAdvice接口这四个接口去重写它们的方法来实现通知。这样有什么不好呢,每一次编写切面类的时候都需要实现这些接口,太繁重了,还有,重写异常通知时需要自己编写,而且方法名必须是afterThrowing,写错了还不行。
本章节通过使用 AspectJ ,我们自己来定义通知,自己编写。那么使用 AspectJ 的时候,可以实现接口重写他们的方法来实现通知,也可以自定义通知,它们有什么区别呢?
MethodBeforeAdvice、AfterReturningAdvice、MethodInterceptor和ThrowsAdvice接口是Spring框架中用于实现不同类型通知的接口。
MethodBeforeAdvice:这个接口用于实现前置通知,即在目标方法执行之前被触发的通知。它的
before
方法会在目标方法执行前被调用,可以在该方法中添加需要执行的逻辑。AfterReturningAdvice:这个接口用于实现后置通知,即在目标方法成功返回后被触发的通知。它的
afterReturning
方法会在目标方法成功返回后被调用,可以在该方法中对返回结果进行处理或记录日志等操作。MethodInterceptor:这个接口用于实现环绕通知,即在目标方法的执行过程中进行拦截并控制其执行。它的
invoke
方法会在目标方法执行前后被调用,通过该方法可以手动控制目标方法的执行,包括传入参数、获取返回结果等。ThrowsAdvice:这个接口用于实现异常通知,即在目标方法抛出异常时被触发的通知。它没有定义具体的方法,而是通过在实现类中定义异常处理方法来实现不同类型异常的处理。
与自定义通知相比,这些接口提供了一种约定和规范化的方式来实现通知逻辑。开发者只需实现相应的接口,并将其配置到Spring容器中,框架会在合适的时机调用相应的方法。这种方式可以使通知与被拦截的目标方法解耦,并且能够与Spring的AOP功能无缝集成。
而自定义通知则更加灵活,开发者可以根据需要编写自己的通知类,不受特定接口的限制。自定义通知可以通过注解、切面表达式或者基于XML的配置来实现,具有更高的自由度和可定制性。开发者可以自己控制通知的触发时机和执行逻辑,更好地满足特定业务需求。
总之,使用Spring的通知接口可以快速实现常见类型的通知,并与Spring的AOP功能结合使用;而自定义通知则更加灵活,可以更好地满足自定义需求,但需要更多的开发额外处理。选择采用哪种方式取决于具体场景和需求。
三、gitee 案例
案例完整地址:https://gitee.com/qiu-feng1/spring-framework.git