SpringAOP
1. 体验 OOP 的黑暗
有业务类 UserServiceImpl 代码如下:
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("新增用户");
}
public void delete() {
System.out.println("删除用户");
}
public void update() {
System.out.println("更新用户");
}
public void find() {
System.out.println("查询用户");
}
}
另有义务类 ProductServiceImpl 代码如下:
public class ProductServiceImpl implements ProductService {
public void add() {
System.out.println("新增商品");
}
public void delete() {
System.out.println("删除商品");
}
public void update() {
System.out.println("更新商品");
}
public void find() {
System.out.println("查询商品");
}
}
初级需求:
在 UserServiceImpl 的方法中添加日志功能,如:
- 在打印“新增用户”之前打印“方法执行前”
- 在打印“删除用户”之前打印“方法执行前”
- …
我们很容易写出如下代码:
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("方法执行前");
System.out.println("新增用户");
}
public void delete() {
System.out.println("方法执行前");
System.out.println("删除用户");
}
public void update() {
System.out.println("方法执行前");
System.out.println("更新用户");
}
public void find() {
System.out.println("方法执行前");
System.out.println("查询用户");
}
}
进阶需求:
在 ProductServiceImpl 的方法中也添加相同的日志功能,那么我们需要在 ProductServiceImpl 的方法中也添加日志代码
public class ProductServiceImpl implements ProductService {
public void add() {
System.out.println("方法执行前");
System.out.println("新增商品");
}
public void delete() {
System.out.println("方法执行前");
System.out.println("删除商品");
}
public void update() {
System.out.println("方法执行前");
System.out.println("更新商品");
}
public void find() {
System.out.println("方法执行前");
System.out.println("查询商品");
}
}
问题分析:
上述代码的实现思路如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g9aSbsW4-1607857993892)(without_aop.png)]
通过上述代码我们确实完成了日志功能的添加。但是,可以非常明显看出这种实现方式存在诸多问题:
-
存在大量重复代码,影响开发效率
-
在众多业务类的众多业务方法存在通用的日志记录逻辑,使得通用的逻辑和核心业务逻辑产生了耦合
-
在实现核心业务功能时还需要关心通用的日志记录
-
代码不便于维护,一旦日志记录逻辑产生变化,需要修改大量的代码
2. 迎接 AOP 的曙光
上述方式是采用传统的 OOP 的思想实现的。由于 OOP 是针对业务处理过程的实体及其属性和行为进行抽象封装,虽然有清晰的逻辑单元划分,但是难免将一些共有行为也抽象到了各个不同的对象不同方法中。为了解决上述问题,AOP 的编程思想应运而生。
2.1 AOP的概念
AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程。是一种通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的编程思想。
概念中的切面该如何理解呢?我们通过下图来阐述切面的概念:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aY3XDV7I-1607857993895)(with_aop.png)]
切面就是横切面,如上图所示,切面是由多个普遍存在的通用功能所组成的概念,比如日志记录,事务管理,权限管理等。因此 AOP 的核心思路就是将众多对象中的众多通用的业务代码抽取出来,放到某一个类中统一管理。当程序执行期间,使用动态代理的方式通过这些通用功能去增强业务对象中的核心业务方法。
其次 AOP 只是一种编程思想,Spring 针对这种编程思想作出了具体的实现而已。
2.2 SpringAOP 的相关术语
-
Target
目标对象:指的是需要被增强的对象
-
Proxy
代理对象:指的是增强后产生的对象
-
JoinPoint
连接点:目标对象中的所有方法都有机会被增强,所以都可以称之为连接点
-
PointCut
切入点:指的是真正被增强的方法(核心业务方法)
-
Advice
通知:指的是增强的方法(通用业务方法)
-
Aspect
切面:切入点和通知的结合体
-
Weaving
织入:将通知和切入点结合的过程
2.3 SpringAOP 的实现思路
-
编写核心业务对象,也就是目标对象,里面包含需要被增强的核心业务方法
-
编写通知对象,里面包含增强的方法(通用的业务逻辑)
-
通过配置的方式告知 Spring ,在指定时机,通过通知类中的指定的通知方法,去增强目标对象中的指定方法
2.4 SpringAOP 的快速入门
- 导入 aspectjweaver 的坐标
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
- 编写 LogAdvice 类,在类中定义一个 beforeLog 方法
public class LogAdvice {
public void beforeLog(){
System.out.println("方法执行前");
}
}
-
在 spring 核心配置文件中配置目标对象和通知对象
-
引入 aop 名称空间
-
完成 aop 的织入,建立通知方法和切入点方法的关系
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl"/>
<bean id="logAdvice" class="com.itheima.advice.LogAdvice"/>
<aop:config>
<aop:aspect ref="logAdvice">
<aop:before method="beforeLog" pointcut="execution(* com.itheima.service.impl.UserServiceImpl.add())"/>
</aop:aspect>
</aop:config>
</beans>
解释:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EBRPRoqb-1607857993896)(.\入门案例解释.png)]
2.5 切入点表达式的书写方式
基本语法:
execution(访问修饰符 返回值 包名.类名.方法名(参数))
特殊写法:
-
访问修饰符
- 可以省略
-
返回值
- 可以使用 * 代表任意返回值
-
包名
- 可以使用 * 代表任意名称的包名
- 可以多个 * 代表指定层级的包,如 *.*. 代表任意名称的两级包
- 可以使用 *… 代表任意名称任意层级的包
-
类名
- 可以使用 * 代表任意类
-
方法名
- 可以使用 * 代表任意方法
-
参数
- 基本数据类型可以写名称,如 int
- 引用类型可以写全限定类名,如 com.itheima.domain.User
- 可以使用 * 代表任意类型参数,多个参数使用逗号(,)分割
- 可以使用 … 代表任意类型任意个数的参数
常用写法:
通常情况下会切到业务层实现包中任意类的任意方法上,所以切入点表达式通常写法如下:
execution(* 业务层实现包.*.*(..))
execution(* com.itheima.service.impl.*.*(..))
切入点表达式的抽取:
为了避免在多个通知上书写相同的切入点表达式,可以通过 <aop:pointcut>
标签进行切入点表达式的抽取,然后在对应的通知标签上通过 pointcut-ref
标签引用对应的切入点表达式,如:
<aop:config>
<!--抽取通用的切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*())"/>
<aop:aspect ref="logAdvice">
<!--前置通知:在切入点方法执行之前执行-->
<aop:before method="beforeLog" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
2.6 通知类型
在 SpringAOP中,一共存在 5 中不同类型的通知,分别为:
- 前置通知:在切入点方法执行之前执行,通过
<aop:before>
表示 - 后置通知:在切入点方法正常执行之后执行,通过
<aop:after-returning>
表示 - 异常通知:在切入点方法产生异常之后执行,通过
<aop:after-throwing>
表示 - 最终通知:无论切入点方法是否产生异常最终都会执行,通过
<aop:after>
表示 - 环绕通知:可以替代上述所有通知,最为特殊,通过
<aop:around>
表示
前四种通知的实现方式比较类似,实现方式如下:
- 在通知类 LogAdvice 中添加通知方法
public class LogAdvice {
public void beforeLog(){
System.out.println("方法执行前");
}
public void afterReturningLog(){
System.out.println("方法正常执行后");
}
public void afterThrowingLog(){
System.out.println("方法执行产生异常后");
}
public void afterLog(){
System.out.println("方法最终执行结束");
}
}
- 在切面中配置通知方法
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*())"/>
<aop:aspect ref="logAdvice">
<!--前置通知:在切入点方法执行之前执行-->
<aop:before method="beforeLog" pointcut-ref="pt1"/>
<!--后置通知:在切入点方法正常执行之后执行-->
<aop:after-returning method="afterReturningLog" pointcut-ref="pt1" />
<!--异常通知:在切入点方法产生异常之后执行-->
<aop:after-throwing method="afterThrowingLog" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
通知方法的参数:
前四种通知方法中都可以添加一个 JoinPont 类型的参数,用于获取切入点方法的信息。该对象有如下常见方法:
- Object getTarget() 获取目标对象
- Signature getSingature() 获取切入点方法的签名对象,可以通过 Signature 的 getName 方法获取切入点方法的方法名
如现在需要对日志功能进行升级,要在控制台打印“xxxx的xxx方法执行前”,“xxxx的xxx方法正常执行后“ 等日志信息。则 LogAdvice 的代码作出如下修改即可:
public class LogAdvice {
public void beforeLog(JoinPoint joinPoint){
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println(simpleName+"的"+methodName+"方法执行前");
}
public void afterReturningLog(JoinPoint joinPoint){
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println(simpleName+"的"+methodName+"方法正常执行后");
}
public void afterThrowingLog(JoinPoint joinPoint){
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println(simpleName+"的"+methodName+"方法执行产生异常后");
}
public void afterLog(JoinPoint joinPoint){
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println(simpleName+"的"+methodName+"方法最终执行结束");
}
}
后置通知和异常通知的互斥性
由于切入点方法不可能既执行正常又产生异常,所以后置通知和异常通知不可能同时执行。所以后置通知和异常通知是互斥的。
2.7 环绕通知
由于环绕通知比较特殊,所以单独对它的概念及使用进行介绍。
环绕通知的概念:
所谓的环绕通知并不是在切入点方法执行前后调用通知方法对切入点进行增强。而是要手动在环绕通知方法中调用切入点方法。这样就可以在调用前后,产生异常后,以及最终对切入点方法进行增强,达到环绕的效果。
环绕通知方法的书写规则:
- 方法必须接受一个 ProceedingJoinPoint 类型的参数,代表正在执行的切入点方法对象
- 方法必须有一个 Object 类型的返回值,代表切入点方法执行后产生的返回值
- 必须在环绕通知方法中手动调用 ProceedingJoinPoint 的 proceed 方法执行切入点方法
代码实现:
- 编写环绕通知方法
public Object aroundLog(ProceedingJoinPoint pjp){
String simpleName = pjp.getTarget().getClass().getSimpleName();
Signature signature = pjp.getSignature();
String methodName = signature.getName();
try {
// 前置
System.out.println(simpleName+"的"+methodName+"方法执行前");
// 调用切入点方法
Object rtValue = pjp.proceed();
// 后置
System.out.println(simpleName+"的"+methodName+"方法正常执行后");
return rtValue;
} catch (Throwable throwable) {
throwable.printStackTrace();
// 异常
System.out.println(simpleName+"的"+methodName+"方法执行产生异常后");
return null;
}finally {
// 最终
System.out.println(simpleName+"的"+methodName+"方法最终执行结束");
}
}
- 配置环绕通知方法
<aop:config>
<!--抽取通用的切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*())"/>
<aop:aspect ref="logAdvice">
<!--环绕通知-->
<aop:around method="aroundLog" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>
由此可以看出,环绕通知方法可以替代前置,后置,异常,以及最终通知。而且由于切入点方法是在环绕通知方法内部被手动调用的,因此可以增强切入点方法的参数和返回值。
2.8 通知中获取切入点方法的参数
准备工作
- 添加OrderService接口和OrderServiceImpl实现类
OrderService.java
public interface OrderService {
public void add(String name, int money);
public int findCount();
public void update();
}
OrderServiceImpl.java
public class OrderServiceImpl implements OrderService {
public void add(String name, int money) {
System.out.println("添加订单..."+name+" "+money);
}
public int findCount() {
System.out.println("查询订单总数");
return 10;
}
public void update() {
System.out.println("更新订单");
int i= 1/0;
}
}
- 在配置文件中配置OrderServiceImpl
<bean id="orderService" class="com.itheima.service.impl.OrderServiceImpl"/>
- 在测试类中添加OrderService的测试
@Autowired
private OrderService orderService;
@Test
public void testOrderService()throws Exception{
// .... 调用对应方法
}
方式1:基于代码实现
在通过方法上添加JoinPoint类型的参数,通过它提供的getArgs方法进行获取
// 前置通知方法
public void beforeLog(JoinPoint joinPoint){
// 获取切入点方法的参数们
Object[] args = joinPoint.getArgs();
System.out.println(Arrays.asList(args));
// 获取目标对象的类名
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
// 获取切入点方法名
String methodName = joinPoint.getSignature().getName();
System.out.println(simpleName+"的"+methodName+"方法执行前");
}
方式2:基于配置实现(了解)
**思路:**在通知方法上添加和切入点方法相同类型相同个数的参数,通过配置让spring在调用通知方法时,将切入点方法的参数传递给通知方法。
切入点方法
public void add(String name, int money) {
System.out.println("添加订单..."+name+" "+money);
}
通知方法
public void beforeLog2(String a, int b){
System.out.println("方法执行前,参数为:"+a+","+b);
}
aop配置
<aop:config>
<aop:aspect ref="logAdvice">
<aop:before
method="beforeLog2"
pointcut="execution(* com.itheima.service.impl.*.*(..)) &&args(a,b)"
arg-names="a,b"/>
</aop:aspect>
</aop:config>
配置解释
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJtTZN6B-1607857993898)(.\获取参数.png)]
2.9 通知中获取切入点方法的返回值
**思路:**在通知方法上定义和切入点方法返回值类型一致的参数,通过配置方式让spring在调用通知方法时,将切入点方法的返回传递给通知方法。通知类型必须是后置通知<aop:after-returning/>
切入点方法
public int findCount() {
System.out.println("查询订单总数");
return 10;
}
通知方法
public void afterReturningLog2(int rtValue){
System.out.println("方法正常执行结束,返回值为:"+rtValue);
}
aop配置
<aop:after-returning
method="afterReturningLog2"
pointcut-ref="pt1"
returning="rtValue"/>
配置解释:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8aAj3JMm-1607857993900)(.\获取返回值.png)]
2.10 通知中获取切入点方法的异常信息
**思路:**在通知方法中定义一个Exception或者Throwable类型的参数,通过配置方式让spring在调用通知方法时,将切入点方法产生异常作为参数传递给通知方法。通知类型必须是异常通知<aop:after-throwing/>
,当通知方法上有两个参数时,JoinPoint必须是第一个参数。
切入点方法
public void update() {
System.out.println("更新订单");
int i= 1/0;
}
通知方法
public void afterThrowingLog(JoinPoint joinPoint,Exception e){
// 获取目标对象的类名
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
// 获取切入点方法名
String methodName = joinPoint.getSignature().getName();
System.out.println(simpleName+"的"+methodName+"方法执行产生异常后,异常原因为:"+e.getMessage());
}
aop配置
<aop:after-throwing
method="afterThrowingLog"
pointcut-ref="pt1"
throwing="e"/>
配置解释
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jvwLSjAY-1607857993901)(.\获取异常.png)]
3. 基于注解的AOP
xml 结合注解
1.开启注解AOP的支持
<context:component-scan base-package="com.itheima"/>
<!--开启注解AOP的支持-->
<aop:aspectj-autoproxy/>
2.在通知类中配置切面
@Component
@Aspect
public class LogAdvice {
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
public void pt1(){
}
@Before("pt1()")
public void beforeLog(JoinPoint joinPoint) {
// 获取切入点方法的相关信息
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println(className+"类的"+methodName+"方法执行前");
}
@AfterReturning("pt1()")
public void afterReturningLog(JoinPoint joinPoint) {
// 获取切入点方法的相关信息
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println(className+"类的"+methodName+"方法正常执行后");
}
@AfterThrowing(value="pt1()",throwing = "t")
public void afterThrowingLog(JoinPoint joinPoint, Throwable t) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println(className+"类的"+methodName+"方法执行出现异常,异常原因为:"+t.getMessage());
}
@After("pt1()")
public void afterLog(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println(className+"类的"+methodName+"方法执行结束");
}
}
注解通知的顺序问题
-
多个通知类先按照类名的字符串比较规则进行确认
ascii码值小的先执行
-
在同一个类中,多个通知方法,按照方法名的字符串比较规则进行确认
ascii码值小的先执行
纯注解AOP
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}
4.CGLIB
被代理类,UserServiceImpl2(没有实现接口)
public class UserServiceImpl2 {
public void save() {
System.out.println("水泥墙");
}
}
提供一个方法,用于生成指定对象的代理对象
class UserServiceCglibProxy{
public static UserServiceImpl2 createUserServiceCglibProxy(final UserServiceImpl2 userService){
UserServiceImpl2 userServiceProxy = (UserServiceImpl2) Enhancer.create(userService.getClass(), new MethodInterceptor() {
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
if (method.getName().equals("save")){
// 调用原始的父类中的方法
Object rtValue = method.invoke(userService, args);
System.out.println("刮大白");
return rtValue;
}
// 其它方法,原封不动的调用
return method.invoke(userService, args);
}
});
return userServiceProxy;
}
}
测试代码
public static void main(String[] args) {
UserServiceImpl2 userServiceImpl = new UserServiceImpl2();
UserServiceImpl2 userService = UserServiceCglibProxy.createUserServiceCglibProxy(userServiceImpl);
userService.save();
}
hodProxy methodProxy) throws Throwable {
if (method.getName().equals(“save”)){
// 调用原始的父类中的方法
Object rtValue = method.invoke(userService, args);
System.out.println(“刮大白”);
return rtValue;
}
// 其它方法,原封不动的调用
return method.invoke(userService, args);
}
});
return userServiceProxy;
}
}
测试代码
```java
public static void main(String[] args) {
UserServiceImpl2 userServiceImpl = new UserServiceImpl2();
UserServiceImpl2 userService = UserServiceCglibProxy.createUserServiceCglibProxy(userServiceImpl);
userService.save();
}