写在前面
今天主要学习了Spring框架中AOP的使用。
github项目地址:https://github.com/wushenjiang/Spring03
什么是AOP?为什么要使用AOP?
现在让我们先假设一个经典的转账场景。如果转账过程中发生了异常,那钱就消失了。我们很早就学过这种情况的解决方式:使用数据库事务管理。但数据库事务管理在连接池中要使用却并不简单。我们来看一段如何实现数据库事务管理的代码:
@Override
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.开启事务
tsManager.beginTransaction();
//2.执行操作
//2.1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3.转出账户减钱
source.setMoney(source.getMoney()-money);
//2.4.转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5.更新转出账户
accountDao.updateAccount(source);
int i=1/0;
//2.6.更新转入账户
accountDao.updateAccount(target);
//3.提交事务
tsManager.commit();
}catch (Exception e){
//4.回滚操作
tsManager.rollback();
e.printStackTrace();
}finally {
//5.归还连接
tsManager.release();
}
}
其中涉及了两个工具类:TransactionManager和ConnectionUtils,分别用来封装事务管理和取连接管理.我们可以看到,这段代码里有大量的重复.而且最严重的是这段程序的耦合度十分高,如果我们的工具类出了什么问题,这个代码会直接挂掉。这里就要说一下设计模式中一个经典的代理模式:
代理模式,有些类似装饰者模式。代理模式就是在不改变原类的基础上对其进行增强(如上文提到的添加事务处理的代码),这样可以做到很好的解耦。
代理模式有两种,基于接口的代理模式和子类的代理模式。这里对于代理模式的具体操作就不再细究了,需要看的话请去上面的github地址了解一下.
那么回到我们最开始的问题,什么是AOP。AOP中文名称面向切片编程,是面向对象编程的升级版.其本质就是代理模式的具体应用。
AOP的xml使用
在了解了什么是AOP和为什么要用AOP之后,我们来了解一下如何使用AOP的XML配置。
首先要导入AOP的相关约束:
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">
然后开始对AOP的配置:
<!-- spring中基于XML的AOP配置步骤
1.把通知的bean也交给spring来管理
2.使用aop:config标签表明开始AOP的配置
3.使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标志
ref属性:指定通知类bean的id
4.在aop:aspect标签的内部使用对应的标签来配置通知的类型
我们现在示例是让printLog方法在切入点方法执行之前执行.
aop:before:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名...类型.方法名(参数列表)
标准表达式写法:
public void com.liuge.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
返回值可以使用通配符,表示任意返回值
* com.liuge.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包.但是有几级包就要写几个*.
包名可以使用..表示当前包及其子包 * *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*号来实现通配 * *..*.*()
参数列表: * *..*.*(int)
可以直接写数据类型:
基本类型直接写名称
引用类型写包名.类名的方式
可以使用通配符表示任意类型,但必须有参数* *..*.*(*)
可以使用..表示有无参数均可
全通配写法:
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.liuge.service.impl.*.*(..)
-->
<!-- 配置Logger 类-->
<bean id="logger" class="com.liuge.utils.Logger"></bean>
<!-- 配置AOP-->
<aop:config>
<!-- 配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* com.liuge.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
那么AOP中的advice有几种类型呢?我们看以下介绍:
<!-- 配置AOP-->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.liuge.service.impl.*.*(..))"/>
<!-- 配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知:在切入点方法执行之前执行
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>-->
<!-- 配置后置通知:在切入点方法正常执行之后执行
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>-->
<!-- 配置异常通知:在切入点执行产生异常后执行
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>-->
<!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>-->
<!--配置切入点表达式 id属性用于指定表达式的唯一标识,expression用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用
它还可以写在aop:aspect外面,此时就变成了所有切面可用
-->
<!--配置环绕通知 -->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
其中关于环绕通知,如下:
/**
* 环绕通知
* 问题:当我们配置了环绕通知后,切入点方法没有执行,而通知方法执行了.
* 分析:通过对比动态代理中的环绕通知代码,发现动态代理中的环绕通知有明确的切入点方法调用
* 解决:
* Spring框架为我们提供了一个接口:ProceedingJoinPoint.该接口有一个方法proceed()
* 此方法就相当于明确切入点方法
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会提供我们该接口的实现类供我们使用
* spring中的环绕通知:
* 它是spring框架中为我们提供的一种在代码中手动控制增强方法何时执行的方式
*/
注解方式的AOP
既然有xml配置,肯定也有注解方式的AOP,如下:
package com.liuge.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @ClassName: Logger
* @Description: 用于生成日志的工具类,它里面提供了公共的代码
* @author: LiuGe
* @date: 2020/5/1 21:50
*/
@Component("logger")
@Aspect//表示当前类是个切面类
public class Logger {
@Pointcut("execution(* com.liuge.service.impl.*.*(..))")
private void pt1(){}
/**
* 前置通知
*/
//@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知 beforePrintLog...");
}
/**
* 后置通知
*/
//@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知 afterPrintLog....");
}
/**
* 异常通知
*/
//@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知 afterThrowingPrintLog....");
}
/**
* 最终通知
*/
//@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知 afterPrintLog....");
}
/**
* 环绕通知
* 问题:当我们配置了环绕通知后,切入点方法没有执行,而通知方法执行了.
* 分析:通过对比动态代理中的环绕通知代码,发现动态代理中的环绕通知有明确的切入点方法调用
* 解决:
* Spring框架为我们提供了一个接口:ProceedingJoinPoint.该接口有一个方法proceed()
* 此方法就相当于明确切入点方法
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会提供我们该接口的实现类供我们使用
* spring中的环绕通知:
* 它是spring框架中为我们提供的一种在代码中手动控制增强方法何时执行的方式
*/
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
//明确调用业务层方法(切入点方法)
Object rtValue = null;
try {
//得到方法执行所需的参数
Object[] args = pjp.getArgs();
System.out.println("aroundPrintLog....前置");
rtValue = pjp.proceed(args);
System.out.println("aroundPrintLog....后置");
return rtValue;
} catch (Throwable e) {
System.out.println("aroundPrintLog....异常");
throw new RuntimeException(e);
}finally {
System.out.println("aroundPrintLog....最终");
}
}
}
我们只需要在xml中写上:
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.liuge"></context:component-scan>
<!-- 配置spring开启注解AOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
总结
其实对于AOP的使用并没有太多可说的。还是需要正确理解代理模式和面向切片编程的思想。只有理解了才能真正的用到自己的实践中去。