1. 知识点
- 完善我们的account案例
- 分析案例中的问题
- 回顾之前讲过的技术
- 动态代理的另一种实现方式
- 解决案例中的问题
- AOP的概念
- Spring的AOP术语
- Spring中基于XML和注解AOP配置
2. 案例中出现的问题
代码冗余现象:因为账户要实现事务控制,所以使得每个方法前后都存在事务控制的代码。代码变得非常臃肿,同时假如事务管理相关的工具类transactionManager名发生改变,那么业务层的所有方法都要变化。
@Override
public List<Account> findAllAccount() {
try {
transactionManager.beginTransaction();
List<Account> accounts = accountDao.findAllAccount();
transactionManager.commit();
return accounts;
} catch (Exception e) {
transactionManager.rollback();
throw new RuntimeException(e);
} finally {
transactionManager.release();
}
}
@Override
public void deleteAccount(Integer accountId) {
try {
transactionManager.beginTransaction();
accountDao.deleteAccount(accountId);
transactionManager.commit();
} catch (Exception e) {
transactionManager.rollback();
} finally {
transactionManager.release();
}
}
@Override
public void MoneyTransfer(String sourceName, String targetName, Float money{
try {
//开启事务
transactionManager.beginTransaction();
//2. 执行操作
//2.1 根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
System.out.println(sourceName);
//2.2 根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3 转出账户减钱
source.setMoney(source.getMoney() - money);
//2.4 接收账户加钱
target.setMoney(target.getMoney() + money);
System.out.println(source.getMoney());
//2.5 更新账户
accountDao.updateAccount(source);
System.out.println(source);
int i=1/0;
accountDao.updateAccount(target);
System.out.println(target);
//3 提交事务
transactionManager.commit();
//4 返回操作
} catch (Exception e){
//回滚操作
transactionManager.rollback();
e.printStackTrace();
} finally {
//释放连接
transactionManager.release();
}
}
3. 解决冗余的思路:动态代理
动态代理:
特点:字节码随用随创建,随用随加载
作用:不修改源码的基础上对方法增强
分类:
基于接口的
基于子类的
3.1 基于子类的动态代理
- 涉及的类:Enhancer
- 提供者:第三方库
- 如何创建代理对象:
使用Enhancer类中的create方法
- 创建代理对象的要求:
被代理类不能是最终类
- create参数:
- Class:用于指定被代理对象的字节码
- Callback:用于提供增强代码。他是让我们写如何代理,我们一般都是写一个该接口的实现类,通常情况下是匿名类,不是必须
- 此接口的实现类是使谁用谁写
- 我们一般写的该接口的子接口实现类,MethodInterceptor
Producer producer = new Producer();
Producer cglibProducer=(Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/*
执行此对象的任何方法都会经过该方法
前三个参数和基于动态代理中invoke的方法参数是一样的
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Object returnValue=null;
//1 获取方法执行的参数
Float money=(Float)args[0];
//2 判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue= method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000);
}
3.2 基于接口的动态代理
- 涉及的类:Proxy
- 提供者:JDK官方
- 如何创建代理对象:
使用proxy类中的newProxyInstance方法
- 创建代理对象的要求:
被代理类最少实现一个接口,如果没有则不能使用
- newProxyInstance参数:
- Classloader:用于加载代理对象的字节码的,和被代理对象使用相同的类加载器,固定写法
- Class[]:字节码数组。他是用让代理对象和被代理对象有相同的方法,固定写法
- InvocationHandler:用于提供增强代码。他是让我们写如何代理,我们一般都是写一个该接口的实现类,通常情况下是匿名类,不是必须
- 此接口的实现类是谁用谁写
final IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(),
new InvocationHandler() {
/*
执行被代理对象的任何接口方法都会经过该方法
方法参数的含义:
proxy:代理对象的引用
args:当前执行的方法所需参数
method:当前执行的方法
return:和被代理对象方法有相同的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1 获取方法执行的参数
Float money = (Float) args[0];
//2 判断当前方法是不是销售
if ("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money * 0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(1000f);
}
3.3 使用动态代理解决代码冗余的现象
public class BeanFactory {
private IAccountService accountService;
private TransactionManager transactionManager;
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/*
获取service的代理对象
*/
public IAccountService getAccountService() {
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
/*
添加事物的支持
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//开启事务
transactionManager.beginTransaction();
//执行操作
rtValue = method.invoke(accountService, args);
//提交事务
transactionManager.commit();
//返回操作
return rtValue;
} catch (Exception e) {
//回滚操作
transactionManager.rollback();
throw new RuntimeException(e);
} finally {
//释放连接
transactionManager.release();
}
}
});
}
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
}
4. AOP
- AOP作用:在程序运行期间,不修改源码对已有方法进行增强
- 优势:
- 减少重复代码
- 提高开发效率
- 维护方便
- 实现方式:动态代理
- 相关术语:
连接点:业务层接口的所有方法,连接我们的业务和增强方法中的点,如何把增强的代码加到我们的业务中来,就是通过这些方法
切入点:不是业务层的所有方法有事务的支持(比如test方法),那么被增强的方法是切入点。所有的切入点都是连接点
通知、增强:(invoke方法是拦截),拦截之后干的事情,就是事务支持
包括前置通知
,后置通知
,异常通知
,最终通知
,环绕通知
Weaving(
织入): 是指把增强应用到目标对象来创建新的代理对象的过程。Aspect
(切面
): 是切入点和通知的结合
4.1 使用XML配置AOP
1、把通知的bean也交给spring来管理
2、使用aop:cofig标签表明AOP的配置
3、使用aop:aspect配置切面
id: 给切面提供一个唯一标识
ref:指定通知类bean的id
4、在aop:aspect标签的内部使用对应的标签来配置通知的类型
pointcut:用于指定切入点表达式,含义是指对业务层中哪些方法增强
aop:before:前置通知
aop:after-returning 后置通知
aop:after-throwing 异常通知
aop:after 最终通知
aop:around 环绕通知
4.1 切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符: 返回值 包名.包名…类名.方法名
标准写法:
<aop:pointcut expression="execution(public void com.zrf.service.impl.AccountServiceImpl.saveAccount())" id="pt1"></aop:pointcut>
访问修饰符可以省略
<aop:pointcut expression="execution(void com.zrf.service.impl.AccountServiceImpl.saveAccount())" id="pt1"></aop:pointcut>
返回值可以使用通配符,表示任意返回值
<aop:pointcut expression="execution(* com.zrf.service.impl.AccountServiceImpl.saveAccount())" id="pt1"></aop:pointcut>
包名可以使用…表示当前包极其子包,用通配符表示任意包,但是有几级包,就要写几个*
<aop:pointcut expression="execution(* *.*.*.*.AccountServiceImpl.saveAccount())" id="pt1"></aop:pointcut>
包路径可以使用*..
,表示当前包,及其子包(因为本例子中将bean.xml
放在根路径下,因此..
可以匹配项目内所有包路径)
<aop:pointcut expression="execution(* *.*..AccountServiceImpl.saveAccount())" id="pt1"></aop:pointcut>
类和方法名都可以使用通配符来适配
<aop:pointcut expression="execution(* *.*..*.*" id="pt1"></aop:pointcut>
参数列表可以直接写数据类型
- 基本类型直接写名称
- 引用类型写包名.类型的方式
- 类型可以使用*标识任意类型,但是必须有参数
- …表示有无参数都可
全通配写法:
<aop:pointcut expression="execution(* *.*..*.*(..))" id="pt1"></aop:pointcut>
实际开发中切入表达式的通常写法:
切到业务层实现类下的所有方法
<aop:pointcut expression="execution(* .com.zrf.service.impl.*.*(..))" id="pt1"></aop:pointcut>
具体的写法:
<!--配置logger类-->
<bean id="logger" class="com.zrf.utils.Logger"></bean>
<!--配置Aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型,并且建立通知方法和切入点方法类型-->
<aop:before method="printLog" pointcut="execution(* *..*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
4.2 环绕通知
- 问题:当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
- 分析: 通过对比动态代理中的环绕通知代码,发现动态代理中的环绕通知有明确的业务层方法调用,而我们的代码中没有
- 解决:
Spring框架为我们提供了一个接口,ProceedingJoinPoint,该接口有一个方法proceed(),此方法相当于明确切入点方法。该接口可以作为环绕接口的方法参数。 - spring框架为我们提供一种可以再代码中手动控制增强方法何时执行的方式
// 环绕通知方法,返回Object类型
public Object printLogAround(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs();
printLogBefore(); // 执行前置通知
rtValue = pjp.proceed(args);// 执行被拦截方法
printLogAfterReturn(); // 执行后置通知
}catch(Throwable e) {
printLogAfterThrowing(); // 执行异常通知
}finally {
printLogAfter(); // 执行最终通知
}
return rtValue;
}
5. 使用注解配置AOP
- bean.xml 中添加
<!--配置spring开启注解AOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
- 表示当前类是一个切面类,以及用于指定切入点表达式的注解
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* *..*.*(..))")
private void ptl(){}
}
- 声明通知的注解
@Before: 声明该方法为前置通知.相当于xml配置中的<aop:before>标签
@AfterReturning: 声明该方法为后置通知.相当于xml配置中的<aop:after-returning>标签
@AfterThrowing: 声明该方法为异常通知.相当于xml配置中的<aop:after-throwing>标签
@After: 声明该方法为最终通知.相当于xml配置中的<aop:after>标签
@Around: 声明该方法为环绕通知.相当于xml配置中的<aop:around>标签
@Before("ptl()")
public void beforePrintLog(){
System.out.println("前置通知类中的方法beforePrintLog开始记录日志了");
}
/*
后置通知
*/
@AfterReturning("ptl()")
public void afterReturningPrintLog(){
System.out.println(" 后置通知类中的方法afterReturningPrintLog开始记录日志了");
}
/*
异常通知
*/
@AfterThrowing("ptl()")
public void afterThrowingPrintLog(){
System.out.println("异常通知中的方法afterThrowingPrintLog开始记录日志了");
}
/*
最终通知
*/
@After("ptl()")
public void afterPrintLog(){
System.out.println("最终通知类中的方法afterPrintLog开始记录日志了");
}