一、动态代理
先定义代理类的生成逻辑,在运行时动态生成代理类。
1.特点
在class运行期间,字节码随用随创建,随用随加载。
2.作用
不修改源码的基础上对方法增强。
3.分类
1)基于接口的动态代理(被代理类必须要实现接口)
①提供者: JDK官方
②涉及的类: Proxy
③创建代理对象: 使用Proxy类中的newProxyInstance方法,要求被代理类最少实现一个接口,如果没有则不能使用。
④具体实现:
final Producer producer = new Producer();
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*TODO:提供增强的代码
1.拿到要增强的方法
2.实现增强的逻辑
3.method.invoke(args)
*/
//例子:
Object returnVlue = null;
//1.获取方法执行参数
//通过下标方式获取
Float money = (Float)args[0];
//2.判断当前方法是不是需要的方法
if("saleProduct".equals(method.getName())){
//找到要增强的方法,写入增强逻辑,调用增强后的方法
returnVlue= method.invoke(producer,money*0.8f);
}
return returnVlue;
}
});
proxyProducer.saleProduct(10000f);
⑤解释:
newProxyInstance方法的参数有
-
ClassLoader: 类加载器,它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。(固定写法,代理的是谁就是谁的类加载器)
-
Class[]: 字节码数组,用于让代理对象和被代理对象有相同的方法。(固定写法,代理谁就写谁的接口就行)
-
InvocationHandler: 用于提供增强的代码,让我们写如何代理,一般都是写一个该接口的实现类,通常都是匿名内部类。在InvocationHandler中要重写invoke方法。
执行被代理对象的任何接口方法都会经过invoke方法 ,通过使用invoke的形参可以对被代理类中的方法进行改造。
invoke方法参数 及 返回值如下
- Object proxy: 代理对象的引用。
- Method method: 代理对象接口当前执行的方法。
- Object[ ] args: 当前执行方法所需的参数,可以通过数组下标获取。与原方法参数位置要一致。
- 返回值: 和被代理对象方法有相同的返回值。
2)基于子类的动态代理
① 提供者: 第三方cglib库(需要导入依赖)
② 涉及的类: Enhancer
③ 创建代理对象: Enhancer类中的create方法,要求被代理类不能是最终类(最终类:加final关键字,不能被继承的)。
④ 具体实现:
与jdk提供的差不多。
public static void main(String[] args) {
final Producer producer = new Producer();
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
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(producer, money * 0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(122);
}
⑤ 解释:
create方法的参数有:
- Class: 字节码,用于指定被代理对象的字节码。要想代理谁就写谁的字节码。
- Callback: 用于提供增强的代码,我们一般使用该接口的子接口实现类,MethodInterceptor。在该实现类中重写intercept方法。
与proxy类似,执行被代理对象的任何方法都会经过intercept方法。
intercept方法参数 及 返回值如下
- Object o: 代理对象的引用。
- Method method: 代理对象接口当前执行的方法。
- Object[ ] objects: 当前执行方法所需的参数,可以通过数组下标获取。与原方法参数位置要一致。
- MethodProxy methodProxy: 当前执行方法的代理对象。
- 返回值: 和被代理对象方法有相同的返回值。
二、AOP的概念
1.什么是AOP
面向切面编程,通过预编译方法和运行期动态代理实现程序功能统一维护的技术。
就是把程序重复的代码提取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对已有方法进行增强。
2.优势
减少了重复代码、提高开发效率、维护方便。
3.实现
动态代理技术,基于接口,基于子类都可以。
三、Spring中AOP的相关术语
Spring可以基于配置的方式,实现上述功能。
- Joinpoint(连接点)
指被拦截到的点,在Spring中指的是方法,因为,Spring只支持方法类型的连接点。也就是被代理类中的所有方法。 - Pointcut(切入点)
指要对哪些连接点进行拦截的定义。就是那些被代理类中的被增强的方法。 - Advice(通知/增强)
指拦截到连接点后要做的事情。也就是动态代理中增强的逻辑。分为前置通知,后置通知,异常通知,最终通知,环绕通知。以invoke()方法为节点分类各种通知。 - Target(目标对象)
指被代理对象。 - Waving(织入)
指把增强应用到目标对象创建新对象的过程。返回代理对象的过程。 - Proxy(代理)
一个类AOP织入增强后,产生一个结果代理类。返回的代理对象 - Aspect(切面)
是切入点和通知的结合。
四、Spring中基于XML和注解的AOP配置
1.基于xml的AOP配置
1)步骤
- step1:在xml配置文件中加入aop的约束。
- step2:把通知的bean交给spring管理。
- step3:使用aop:config标签表明开始AOP的配置。
- step4:使用aop:aspect标签表明开始配置切面。id属性给切面提供一个唯一标志,ref属性指定通知类bean的id。
- step5:在aop:aspect标签的内部使用对应的标签来配置通知的类型,例如:前置通知使用aop:before标签,method属性用于指定通知类中哪个方法是对应通知。
- step6:使用pointcut属性用于指定切入点表达式,该表达式的含义是指对业务层哪些方法增强 。
- 切入点表达式写法:
execution(表达式)
,
表达式:访问修饰符 返回值 包名.类名.方法名(参数列表)
例如:pointcut = execution(public void com.Yu.Service.saveService())
2)切入点表达式
需要导入aspectj依赖,帮助我们解析切入点表达式。
①标准写法:访问修饰符 返回值 包名.类名.方法名(参数列表)
例如:public void com.Yu.Service.saveService()
②全通配写法:* *..*.*(..)
(慎用)
③规则:
- 访问修饰符是可以省略的。
- 返回值可以使用通配符表示任意返回值。
- 包名可以使用通配符表示任意包,但有几级包,就要写几个
*.
。包名也可以使用..
表示当前包及其子包。 - 类名和方法名都可以使用
*
来实现通配。 - 参数列表
如果是基本数据类型可以直接写直接写数据类型,引用类型写包名.类
的方法;
也可以使用通配符表示任意类型,但必须有参数;
可以使用..
表示有无参数均可,有参数可以是任意类型。
实际开发中要切到业务层实现类下的所有方法。
④通用切入点表达式
在<aop:aspect>
标签内可以配置切入点表达式,其中的expression属性用于指定表达式内容,配置完成后,当通知要使用表达式时,只需要给pointcut-ref
属性传入配好的id即可。
例子:
<aop:aspect id="???" ref="???">
<aop:before method="..." pointcut-ref="p1"/>
<aop:point id="p1" expression=execution(* com.Yu.service.*.*(..))/>
<aop:aspect id="???" ref="???">
注意:如果aop:aspect
写在aop:aspect标签外部表示所有切面可用(但必须出现在aop:aspect之前),内部表示当前切面可用。
3)常用的通知类型
①前置通知: 在目标类(切入点)方法执行前执行。
<aop:before method="???" pointcut="excution(??)"></aop:before>
②后置通知: 在目标类方法执行之后执行。
<aop:after-returing method="???" pointcut="excution(??)"></aop:before>
注意:它和异常通知永远只能执行一个,不能都执行。
③异常通知: 当目标类方法抛出异常时执行,它不会捕获原方法抛出的异常。
<aop:throwing method="" pointcut="excution(??)"></aop:before>
注意: 如果在原方法中对异常使用了try{}catch(){},就不会触发异常通知
④最终通知: 在目标类的方法执行之后,如果程序出现了异常,最终通知也会执行。
<aop:after method="" pointcut="excution(??)"></aop:before>
⑤环绕通知: spring框架提供的一种可以在代码中手动控制增强方法何时执行的方法。
在通知类的环绕通知中,传入参数ProceedingJoinPoint 类型的参数,就可以通过该参数拿到方法执行所需的参数等,再通过proceed方法明确调用业务层方法(切入点方法)。
例子:
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方法开始记录日志了。。。最终");
}
}
2.基于注解的AOP配置(※※)
** 先在配置文件中写上Spring开启注解AOP的支持,如果想用纯注解,只需要在通知类上加入
<context:aspectj-autoproxy/>
0)@EnableAspectJAutpProxy
作用: 用纯注解实现AOP时,添加在通知类上。
1)@Aspect
作用: 表示当前类是一个切面类
使用对象: 类
2)@Pointcut("切入点表达式")
使用:表示方法是一个切入点
3)@Before()
作用:方法是前置通知
4)@AfterReturning()
作用:方法是后置通知
5)@AfterThrowing()
作用:方法是异常通知
6)@After()
作用:方法是最终通知
7)@Around()
作用:方法是环绕通知
使用例子:
@Aspect
public class AnnotationAudienceAround{
//使用@Pointcut注解声明切入点表达式
@Pointcut("execution(* com.qin.util.*.*(..))")
public void pt1(){}
@Before("pt1()")
public void beforePrintLog(){sout("前置通知Logger类beforePrintLog方法开始了")}
@AfterRetruning("pt1()")
public void afterRetruningLog(){sout("后置通知Logger类afterRetruningLog方法开始了")}
@AfterThrowing("pt1()")
public void afterThrowing(){sout("异常通知Logger类afterThrowing方法开始了")}
@After()("pt1()")
public void afterPrintLog(){sout("最终通知Logger类afterPrintLog方法开始了")}
//※使用注解方法应用AOP时记得放在环绕通知里,因为SPring的bug导致通知执行的过程是乱序的。
@Around("pt1()")
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方法开始记录日志了。。。最终");
}
}
}