2020-7-17
五、Spring的AOP原理及其XML配置
AOP具体定义详情查看百度百科。
AOP的原理
使用“代理”的方法来对原始方法进行“增强”
所谓“代理”,就是指使用Proxy中的newProxyInstance方法“代替”原来的方法;
所谓“增强”,就是指在不改动原来方法的基础上,对原来的方法中增加一些代码,从而实现更多的功能。
例如我写了一个transfer方法
public void transfer(String sourceName, String targetName, Float money) {
System.out.println("transfer...");
//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);
}
这是一个转账功能的实现代码,其中在执行更新转出账户这一操作之后可能会遇到报错的情况,但是此时还没更新转入账户,这就会导致在数据库中转出账户的钱少了,而转入账户的钱没有增加。为了改变这一现象,就需要引入“事物”的概念。
事物:把一组操作放进一个事物中,如果这一组操作全都成功了,再统一向数据库提交操作,如果这一组操作有任意报错,则回滚所有操作。
根据事物的概念可以对上述代码进行改造:
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.开启事务
txManager.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.提交事物
txManager.commit();
//4.返回结果 该方法为void方法,无需返回结果
} catch (Exception e) {
//5.回滚操作
txManager.rollback();
e.printStackTrace();
} finally {
//6.释放资源
txManager.release();
}
}
(其中的txManager.beginTransaction()/.commitTransaction()/.rollbackTransaction()/.releaseTransaction()等操作写在另一个类TransactionManager中,主要运用了dbutils中的ConnectionUtils.getThreadConnection().setAutoCommit()/.commit()/rollback()/close()等方法,这里不做赘述。)
由上面方法可以保证只有所有操作完成之后,才统一commit提交,从而保证数据的安全。但是除了transfer方法,该类中还有saveAccount、updateAccount、deleteAccount等等方法,如果每个方法都写一遍事物操作会导致代码比较臃肿,所以急需一个解决方法实现代码的可重用性:动态代理
动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
*
* 基于接口的动态代理:
* 涉及的类:Proxy (基于接口的代理方法)
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类至少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的。和被代理类对象使用相同的类加载器。固定写法
* Class[]:字节码数组
* 他是用于让代理对象和被代理对象有相同的方法。固定写法
* InvocationHandler:用于提供增强的代码
* 他是让我们写如何代理,我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的
* 此接口的实现类都是谁用谁写。
通过动态代理我们可以写一个新的方法:
/**
* 用于创建Service的代理对象的工厂
*/
public class BeanFactory {
private IAccountService accountService;
private TransactionManager txManager;
//通过
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
/**
* 获取Service的代理对象
* @return
*/
public IAccountService getAccountService(){
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作 该处执行的是AccountService中的transfer方法
rtValue = method.invoke(accountService,args);
//3.提交事物
txManager.commit();
//4.返回结果
return rtValue;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放资源
txManager.release();
}
}
});
}
}
这个类是一个新建的类,并没有改变原本service方法中的任何代码,只是加了一些代码,然后把原来service中的方法插入到这些代码中,等于说是给原来的service中的方法进行了“增强”,使其多了事物的功能。
Spring中基于XML的AOP配置
这里举个简单例子:
我有如下类,用println代替业务操作,模拟一个账户业务类。
注意3个方法中,两个是void类型,一个是int类型。两个void类型方法中一个带参,一个无参。这个区别在XML的配置中也有所区别。
public class AccountServiceImpl implements IAccountService {
//模拟保存账户
public void saveAccount() {
System.out.println("执行了保存");
}
//模拟更新账户
public void updateAccount(int i) {
System.out.println("执行了更新");
}
//模拟删除账户
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
现在对该类进行增强,就在每个sout前面多输出一句话:
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志,计划其在切入点方法执行之前执行(切入点方法就是业务层方法)
*/
public void printLog(){
System.out.println("Logger类中的pringtLog方法开始记录日志了。。。");
}
}
现在的目的是在saveAccount方法、updateAccount方法、deleteAccount方法println之前插入printLog方法。在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
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置spring的Ioc,把service对象配置进来-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--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类中哪个方法是前置通知
4.1、pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
访问修饰符 返回值 包名.包名.。。。类名.方法名(参数列表)
标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包就要写几个*. 。
* *.*.*.*.AccountServiceImpl.saveAccount()
包名可以用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配
* *..*.*()
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类型的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
* *..*.*(*)
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.itheima.service.impl.*.*(..)
-->
<!--配置Logger类-->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
由注释中的说明可得:
如下配置时
<aop:before method="printLog" pointcut="execution(public void com.itheima.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
输出结果
只有saveAccount方法被增强改造了
当去掉访问修饰符public时
<aop:before method="printLog" pointcut="execution(void com.itheima.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
输出结果不变
当使用通配符*来代替void时
<aop:before method="printLog" pointcut="execution( * com.itheima.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
虽然把int类型的deleteAccount方法包括进来了,但是由于在配置中指定了saveAccount()方法,所以输出结果仍然相同。
当使用通配符*代替各个包名时
<aop:before method="printLog" pointcut="execution( * *.*.*.*.AccountServiceImpl.saveAccount())"></aop:before>
虽然可以囊括所有不同名字的包,但是也指定了方法名字,由于我这个项目在各个包中只有一个AccountServiceImpl.saveAccoutn(),所以输出结果仍然不变。
当用…表示当前包及其子包时
<aop:before method="printLog" pointcut="execution(* *..AccountServiceImpl.saveAccount())"></aop:before>
同上
当使用通配符*来通配类名和方法名时
<aop:before method="printLog" pointcut="execution(* *..*.*())"></aop:before>
输出结果
可见updateAccount方法并没有被代理,原因就是因为该方法有一个int类型的参数,而配置时.*()中并没有任何参数,表示可以代理任何名字的无参函数,故而漏掉了updateAccount(int i)方法。
可以通过在()中写入*来表示任意类型的参数
或者…来表示有无参数均可
当配置文件如下时:
<aop:before method="printLog" pointcut="execution(* *..*.*(*))"></aop:before>
输出结果:
可见只有updateAccount方法被代理了。(*)虽然可以通配任意类型的参数,但是要求必须有参数,3个方法中只有该方法有参数,另外两个没有,所以只代理了该方法。
当配置文件如下时:
<aop:before method="printLog" pointcut="execution(* *..*.*(..))"></aop:before>
输出结果:
(。。)表示有无参数均可,所以三个方法都被代理了。(两个点. 不是句号)
特别注意:
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
<aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
四种常用的通知类型
分别是:
前置通知:在切入点方法执行之前执行
后置通知:在切入点方法正常执行之后执行。它和异常通知是互斥的关系
配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知是互斥的关系
最终通知:无论切入点方法是否正常执行他都会在其后面执行
示例:
首先分别创建4个通知方法对应四种不同通知:
public class Logger {
/**
* 前置通知
*/
public void beforeprintLog(){
System.out.println("前置通知Logger类中的beforeprintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
public void afterReturningprintLog(){
System.out.println("后置通知Logger类中的afterReturningprintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
public void afterThrowingprintLog(){
System.out.println("异常通知Logger类中的afterThrowingprintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
public void afterprintLog(){
System.out.println("最终通知Logger类中的afteringprintLog方法开始记录日志了。。。");
}
}
在XML文件中配置中分别用到了aop:before、aop:after-returning、aop:after-throwing、 aop-after四个关键词,配置方法如下:
<!--配置Logger类-->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知:在切入点方法执行之前执行-->
<aop:before method="beforeprintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
<!--配置后置通知:在切入点方法正常执行之后执行。它和异常通知是互斥的关系-->
<aop:after-returning method="afterReturningprintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after-returning>
<!--配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知是互斥的关系-->
<aop:after-throwing method="afterThrowingprintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after-throwing>
<!--配置最终通知:无论切入点方法是否正常执行他都会在其后面执行-->
<aop:after method="afterprintLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
</beans>
接下来我们在Junit中对Service中的saveAccount方法进行测试,当方法正常运行之后的结果如下:
可见正常运行之后,后置通知起效了,而异常通知没有出现。
当故意在saveAccount方法中插入一段异常代码:int i = 1/0;
可以预见代码会报异常,输出的结果如下:
可见异常通知起效了,而后置通知并没有出现。
通用化切入点表达式
在XML中配置切入点表达式非常麻烦,每次都要重新写表达式“pointcut="execution(* com.itheima.service.impl..(。。)”(句号代表点.)
于是可以使用一个标签来实现简化:aop:pointcut
配置切入点表达式 id属性用于指定表达式的唯一标志,expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时变成了所有切面可用
1)把该标签放入aop:aspect标签内部:
<!--配置切面-->
<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>
<!--1)把该标签放入aop:aspect标签内部,此时该标签定义的表达式只能供该切面使用-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
</aop:aspect>
2)把该标签放在aop:aspect外部,
但是要注意,一定要放在aop:aspect标签上面!
如果写在下面,则xml文件会报错。这是因为在XML文件一开始导入了约束,该约束有此规定
例子:
<!--配置AOP-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.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>
</aop:aspect>
</aop:config>
环绕通知
环绕通知XML配置方法:
标签:aop:around
示例:
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
如果只是在XML文件中配置了环绕通知,然后再Logger类中写一个aroundPrintLog方法:
//错误示范
public void aroundPrintLog(ProceedingJoinPoint pjp) {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。前置");
}
在执行的时候我们发现输出中只输出了通知类中的aroundPrintLog方法,而业务类中的saveAccount方法并没有被执行:
原因是因为我们并没有明确切入点,即并没有声明这个通知方法要插入到哪个方法,也没有声明是前置通知、后置通知还是异常通知或者是最终通知。
spring框架给我们提供了一个接口:ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于明确切入点方法。该接口可以作为环绕通知的方法参数,在程序执行的时候,spring框架会为我们提供该接口的实现类供我们使用。(意思是我们直接调用就行了,不用管怎么实现的)
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] arjs = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed();//明确调用业务层(切入点)方法
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。最终");
}
}
在proceed()方法之前执行的即为前置同时,在它之后执行的即为后置通知,在catch中执行的即为异常通知,在finally中执行的即为最终通知。输出结果:
spring中的环绕通知是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。