AOP
1 完善Account案例
ThreadLocal对象把Connnection和当前线程绑定,从而使一个线程中只有一个能控制事务的对象。
服务器 线程池 tomcat启动创建
2 分析问题
回顾技术动态代理
动态代理:
基于接口的动态代理
/**
* 生产厂家
*/
public interface ProduceInterdace {
/**
* 销售
* @param money
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterProduct(float money);
}
public class Produce implements ProduceInterdace {
/**
* 销售
* @param money
*/
public void saleProduct(float money) {
System.out.println("销售 money" + money);
}
/**
* 售后
* @param money
*/
public void afterProduct(float money) {
System.out.println("售后服务"+money);
}
}
public static void main(final String[] args) {
final Produce produce = new Produce();
produce.saleProduct(1000);
ProduceInterdace produceInterdace = (ProduceInterdace)Proxy.newProxyInstance(produce.getClass().getClassLoader(), produce.getClass().getInterfaces(), new InvocationHandler() {
/**
* 执行被代理对象的任何接口方法都会经过该方法
* @param o 代理对象的引用
* @param method 当前执行的方法
* @param objects 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
// 增强的代码
Object returnValue = null;
// 获取方法执行参数
Float money = (Float)objects[0];
// 判断当前方法是不是销售
if ("saleProduct".equals(method.getName())) {
returnValue = method.invoke(produce, money*0.8f);
}
return returnValue;
}
});
produceInterdace.saleProduct(1000f);
}
基于子类的动态代理
增加
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
</dependencies>
public static void main(final String[] args) {
final Produce produce = new Produce();
produce.saleProduct(1000);
/**
* 动态代理
* 字节码随用随加载 随用随加载
* 不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于子类的动态代理:Enhancer 涉及的类
* 提供者 cglib
* 如何创建代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象要求:
* 被代理类不能是最终类
* create方法参数:
* Class 字节码
* 用于指定被代理对象的字节码
* Callback 用于提供增强的代码
* 如何代理。一般该接口实现类。通常是匿名实现类不是必须的
* 此接口实现类谁用谁写
* 一般是该接口的子接口的实现类 MethodInterceptor
*/
Produce produce1 = (Produce)Enhancer.create(produce.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都经过该方法
* @param o
* @param method
* @param objects
* 以上和基于接口的动态代理的invoke方法参数是一样的
* @param methodProxy 当前执行方法的代理对象
* @return
* @throws Throwable
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 增强的代码
Object returnValue = null;
// 获取方法执行参数
Float money = (Float)objects[0];
// 判断当前方法是不是销售
if ("saleProduct".equals(method.getName())) {
returnValue = method.invoke(produce, money*0.8f);
}
return returnValue;
}
});
produce1.saleProduct(20000);
}
动态代理
4 动态代理的另一种实现方式 5 解决案例问题
AOP概念
面向切面编程
程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的 基础上,对我们的已有方法进行增强。
AOP 的作用及优势
作用: 在程序运行期间,不修改源码对已有方法进行增强。
优势:
减少重复代码
提高开发效率
维护方便
AOP 的实现方式
使用动态代理技术
spring中aop相关术语
AOP 相关术语
Joinpoint(连接点): (业务层方法) 所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的 连接点。
Pointcut(切入点): (代理增强方法) 所有的切入点都是连接点 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
Advice(通知/增强):
所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
通知的类型:
前置通知 method.invoke(produce, money*0.8f) 之前执行
后置通知 method.invoke(produce, money*0.8f) 之后执行
异常通知 catch执行
最终通知 finnal 执行
环绕通知。整个invoke方法执行。有明确的切入点方法调用。
Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或Field。
Target(目标对象):
代理的目标对象。(被代理对象)
Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
Proxy(代理): (代理对象)
一个类被 AOP 织入增强后,就产生一个结果代理类。
Aspect(切面):
是切入点(增强方法)和通知(引介)的结合。
类 方法 方法的执行等等。
spring中基于XML和注解的配置
通知
<!--配置AOP-->
<aop:config>
<!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用
-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<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>-->
<!-- 配置环绕通知 详细的注释请看Logger类中-->
<aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
/**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。
* 解决:
* Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
*
* spring中的环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
*/
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
切入点表达式说明
execution:匹配方法的执行(常用) execution(表达式)
表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数)) 写法说明:
全匹配方式:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
返回值可以使用*号,表示任意返回值
- com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
包名可以使用号,表示任意包,但是有几级包,需要写几个
- ....AccountServiceImpl.saveAccount(com.itheima.domain.Account)
使用..来表示当前包,及其子包
- com..AccountServiceImpl.saveAccount(com.itheima.domain.Account)
类名可以使用*号,表示任意类
- com..*.saveAccount(com.itheima.domain.Account)
方法名可以使用*号,表示任意方法
- com...( com.itheima.domain.Account)
参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
- com...(*)
参数列表可以使用..表示有无参数均可,有参数可以是任意类型
- com...(..)
全通配方式: * ...*(..)
注: 通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。 execution(* com.itheima.service.impl..(..))
AOP注解
@Aspect//表明当前类是一个切面类
@Before
作用: 把当前方法看成是前置通知。 属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用。
@Before("execution(* com.itheima.service.impl..(..))")
@AfterReturning
作用: 把当前方法看成是后置通知。
属性:
value:用于指定切入点表达式,还可以指定切入点表达式的引用
@AfterReturning("execution(* com.itheima.service.impl..(..))")
@AfterThrowing
作用: 把当前方法看成是异常通知。
属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用
@AfterThrowing("execution(* com.itheima.service.impl..(..))")
@AfterThrowing
作用: 把当前方法看成是异常通知。
属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用
@After
作用: 把当前方法看成是最终通知。
属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用
环绕通知
@Around
作用: 把当前方法看成是环绕通知。 属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用。
切入点表达式注解
@Pointcut
作用: 指定切入点表达式 属性: value:指定表达式的内容
@Pointcut("execution(* com.itheima.service.impl..(..))") private void pt1() {}
引用方式:
/**
* 环绕通知
* @param pjp
* @return
*/
@Around("pt1()")
//注意:千万别忘了写括号
public Object transactionAround(ProceedingJoinPoint pjp) {
//定义返回值
Object rtValue = null;
try {
//获取方法执行所需的参数
Object[] args = pjp.getArgs();
//前置通知:开启事务
beginTransaction();
//执行方法
rtValue = pjp.proceed(args);
//后置通知:提交事务
commit();
}catch(Throwable e) {
//异常通知:回滚事务
rollback();
e.printStackTrace();
}finally {
//最终通知:释放资源
release();
}
return rtValue;
}
使用XML的配置方式
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
不使用XML的配置方式
@Configuration
@ComponentScan(basePackages="com.itheima") @EnableAspectJAutoProxy
public class SpringConfiguration { }