因为假期,原因,有一段时间没有给你更新!告诉你一件事,在假期里,一个粉丝告诉我他被虐待的经历。假期前,他去了一家互联网公司面试,直接被面试官Spring AOP三连问问一脸懵逼!其实我觉得,这东西不是很简单吗?
大家在学习 AOP 以前,如果你知道代理模式,你很容易学习。接下来,我将向您介绍 AOP 这个重要的知识点!
代理模式在 Java 开发是一种常见的设计模式。设计目的是在服务和客户之间插入其他功能。插入功能对调用人员透明,起到伪装控制作用。例如,租赁的例子:租户、中介和房东。相应的代理模式是:客户和代理 、委托类(代理类)。
为某一个对象(委托类)提供一个代理(代理类),用来控制对这个对象的访问。委托类和代理类有一个共同的父类或父接口。代理类会对请求做预处理、过滤,将请求分配给指定对象。
生活中常见的代理情况: 租房中介、婚庆公司等
代理模式的两个设计原则:
- 代理类和委托类有类似的行为(共同)
- 代理增强委托行为
常用的代理模式:
- 静态代理
- 动态代理
对象提供固定的代理角色来控制对象的访问。 代理和委托有一个共同的父亲或父亲接口,以便在任何使用委托对象的地方都可以被代理对象所取代。代理负责预处理、过滤、分配给委托处理,以及委托执行后的后续处理。
- 共同行为(结婚) - 接口
- 目标角色(新人) - 实现行为
- 代理角色(婚庆公司) - 实现行为 增强目标对象的行为
- 固定目标角色
- 目标角色在应用程序执行前获得
- 代理对象会增强目标对象的行为
- 可能有多个代理,导致多个代理"类爆炸"(缺点)
/** * 定义?为 */ public interface Marry { public void toMarry(); }
/** * 静态代理 ——> ?标对象 */ public class You implements Marry { // 实现?为 @Override public void toMarry() { System.out.println("我要结婚了..."); } }
/** * 静态代理 ——> 代理对象 */ public class MarryCompanyProxy implements Marry { // ?标对象 private Marry marry; // 通过构造器将通过构造器标对象传? public MarryCompanyProxy(Marry marry) { this.marry = marry; } // 实现?为 @Override public void toMarry() { // 增强?为 before(); // 执??在标准对象中法 marry.toMarry(); // 增强?为 after(); } /** * 增强?为 */ private void after() { System.out.println("新婚快乐,早婚贵?!"); } /** * 增强?为 */ private void before() { System.out.println("场地正在布置中..."); } }
// ?标对象 You you = new You(); // 构造代理??同时传?真实?? MarryCompanyProxy marryCompanyProxy = new MarryCompanyProxy(you); // 通过代理对象的调调?在标准对象中法 marryCompanyProxy.toMarry();
静态代理对代理的作用是固定的,比如 dao 曾有20个 dao 类,如果要代理方法的访问权限,此时需要创建20个静态代理角色,导致类爆炸,不能满足生产需求,从而产生动态代理的理念。
与静态代理相比,动态代理在创建代理对象时更加灵活。在程序运行过程中,动态代理字节码是由 Java 动态产生反射机制。它将动态地为目标对象创建代理对象,而无需程序员手动编写其源代码。由于反射机制可以生成任何类型的动态代理,动态代理不仅简化了编程工作,而且提高了软件系统的可扩展性。代理行为可以代理多种方法,即满足生产需要,达到代码通用的目的。
实现动态代理的两种方式:
- JDK 动态代理
- CGLIB动态代理
- 目标对象不固定
- 在执行应用程序时动态创建目标对象
- 代理对象会增强目标对象的行为
注:JDK动态代理的目标对象必须有接口才能实现
Proxy 类:
Proxy类是专门完成代理的操作类,可以动态生成一个或多个接口,提供以下操作方法:
/* 返回?个指定接?代理类实例法调?分配到指定的调整处理程序。 (返回代理对象) loader:?个ClassLoader对象,定义哪一个ClassLoader对象来对?代理对象进入代理对象加载 interfaces:?个Interface对象的数组意味着我将为需要代理的对象提供?组什么接?,如果 我提供了?组接?给它,然后代理对象声称实现了这个连接(多态),这样我就可以调整?这组接?中的?法了 h:?个InvocationHandler接?,表示代理实例的调整实现处理程序的连接。每个代理实例都有个关联 的调?处理程序。代理实例调?对法调?进?编码并将其指派到其调整中处理程序的 invoke ?法 (传?InvocationHandler接?的?类) */ public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
public class JdkHandler implements InvocationHandler { // ?标对象 private Object target; // ?标准对象的类型不固定,创建时动态成 // 通过构造器将通过构造器标对象赋值 public JdkHandler(Object target) { this.target = target; } /** * 1、调??标对象的?法(返回Object) * 2、增强?标对象的?为 * @param proxy 调?该?法律代理实例 * @param method ?标对象的?法 * @param args ?标对象的?法形参 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 增强?为 System.out.println("==============?法前执?"); // 调??标对象的?法(返回Object) Object result = method.invoke(target,args); // 增强?为 System.out.println("?法后执?=============="); return result; } /** * 获取代理对象 * public static Object newProxyInstance(ClassLoader loader, * Class<?>[] interfaces, * InvocationHandler h) * loader:类加载器 * interfaces:接?数组 * h:InvocationHandler接? (传?InvocationHandler接?的实现类) * * * @return */ public Object getProxy() { return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterface s(),this); } }
// ?标对象 You you = new You(); // 获取代理对象 JdkHandler jdkHandler = new JdkHandler(you); Marry marry = (Marry) jdkHandler.getProxy(); // 标对象通过代理对象对象⽅法 marry.toMarry();
答:在生成的动态代理类 $Proxy0.class 中,构造方法调用了父类Proxy.class 的构造方法,给成员变量 invocationHandler 赋值,$Proxy0.class的 static 模块中创建了被代理类的方法,调用相应方法时方法体中调用了父类中的成员变量 InvocationHandler 的 invoke ()方法。
注:JDK 的动态代理依靠接口实现,如果有些类并没有接口实现,则不能使用 JDK 代理。
JDK 的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能使用 JDK 的动态代理,cglib 是针对类来实现代理的,它的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对 final 修饰的类进行代理。
在 pom.xml 文件中引入 cglib 的相关依赖
<!-- https://mvnrepository.com/artifact/cglib/cglib --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency>
实现 MethodInterceptor 接口
public class CglibInterceptor implements MethodInterceptor { // ⽬标对象 private Object target; // 通过构造器传⼊⽬标对象 public CglibInterceptor(Object target) { this.target = target; } /** * 获取代理对象 * @return */ public Object getProxy() { // 通过Enhancer对象的create()⽅法可以⽣成⼀个类,⽤于⽣成代理对象 Enhancer enhancer = new Enhancer(); // 设置⽗类 (将⽬标类作为其⽗类) enhancer.setSuperclass(target.getClass()); // 设置拦截器 回调对象为本身对象 enhancer.setCallback(this); // ⽣成⼀个代理类对象,并返回 return enhancer.create(); } /** * 拦截器 * 1、⽬标对象的⽅法调⽤ * 2、增强⾏为 * @param object 由CGLib动态⽣成的代理类实例 * @param method 实体类所调⽤的被代理的⽅法引⽤ * @param objects 参数值列表 * @param methodProxy ⽣成的代理类对⽅法的代理引⽤ * @return * @throws Throwable */ @Override public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { // 增强⾏为 System.out.println("==============⽅法前执⾏"); // 调⽤⽬标对象的⽅法(返回Object) Object result = methodProxy.invoke(target,objects); // 增强⾏为 System.out.println("⽅法后执⾏=============="); return result; } }
// ⽬标对象 You you = new You(); CglibInterceptor cglibInterceptor = new CglibInterceptor(you); Marry marry = (Marry) cglibInterceptor.getProxy(); marry.toMarry(); User user = new User(); CglibInterceptor cglibInterceptor = new CglibInterceptor(user); User u = (User) cglibInterceptor.getProxy(); u.test();
JDK代理与CGLIB代理的区别
- JDK 动态代理实现接口,Cglib 动态代理继承思想
- JDK 动态代理(目标对象存在接口时)执行效率高于 Ciglib
- 如果目标对象有接口实现,选择 JDK 代理,如果没有接口实现选择 Cglib 代理
我们有一个 Pay (接口) 然后两个实现类 DollarPay 和 RmbPay,都需要重写 pay ()方法, 这时我们需要对 pay 方法进行性能监控,日志的添加等等怎么做?
对每个字符方法均做日志代码的编写处理,如下面方式
缺点: 代码重复太多, 添加的日志代码耦合度太高(如果需要更改日志记录代码功能需求,类中方法需要全部改动,工程量浩大)
装饰器模式:动态地给一个对象添加一些额外的职责。
代理模式:以上刚讲过。于是得出以下结构:
仔细考虑过后发现虽然对原有内部代码没有进行改动, 对于每个类做日志处理,并引用目标类,但是如果待添加日志的业务类的数量很多,此时手动为每个业务类实现一个装饰器或创建对应的代理类,同时代码的耦合度也加大,需求一旦改变,改动的工程量也是可想而知的。
有没有更好的解决方案,只要写一次代码,对想要添加日志记录的地方能够实现代码的复用,达到松耦合的同时,又能够完美完成功能?
答案是肯定的,存在这样的技术,aop 已经对其提供了完美的实现!
Aspect Oriented Programing 面向切面编程,相比较 oop 面向对象编程来说,Aop 关注的不再是程序代码中某个类,某些方法,而 aop 考虑的更多的是一种面到面的切入,即层与层之间的一种切入,所以称之为切面。联想大家吃的汉堡(中间夹肉)。那么 aop 是怎么做到拦截整个面的功能呢?考虑前面学到的 servlet filter /* 的配置 ,实际上也是 aop 的实现。
AOP 主要应用于日志记录,性能统计,安全控制,事务处理等方面,实现公共功能性的重复使用。
- 降低模块与模块之间的耦合度,提高业务代码的聚合度。(高内聚低耦合)
- 提高了代码的复用性
- 提高了代码的复用性
- 可以在不影响原有的功能基础上添加新的功能
动态代理(JDK + CGLIB)
被拦截到的每个点,spring 中指被拦截到的每一个方法,spring aop 一个连接点即代表一个方法的执行。
对连接点进行拦截的定义(匹配规则定义 规定拦截哪些方法,对哪些方法进行处理),spring 有专门的表达式语言定义。
拦截到每一个连接点即(每一个方法)后所要做的操作
- 前置通知 (前置增强)— before() 执行方法前通知
- 返回通知(返回增强)— afterReturn 方法正常结束返回后的通知
- 异常抛出通知(异常抛出增强)— afetrThrow()
- 最终通知 — after 无论方法是否发生异常,均会执行该通知。
- 环绕通知 — around 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
切入点与通知的结合,决定了切面的定义,切入点定义了要拦截哪些类的哪些方法,通知则定义了拦截过方法后要做什么,切面则是横切关注点的抽象,与类相似,类是对物体特征的抽象,切面则是横切关注点抽象。
被代理的目标对象
将切面应用到目标对象并生成代理对象的这个过程即为织入
在不修改原有应用程序代码的情况下,在程序运行期为类动态添加方法或者字段的过程称为引入
<!--Spring AOP--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version> </dependency>
添加命名空间
xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
/** * 切⾯ * 切⼊点和通知的抽象 (与⾯向对象中的 类 相似) * 定义 切⼊点和通知 (切⼊点定义了要拦截哪些类的哪些⽅法,通知则定义了拦截过⽅法后要做什么) */ @Component // 将对象交给IOC容器去实例化 @Aspect // 声明当前类是⼀个切⾯ public class LogCut { /** * 切⼊点: * 匹配规则。规定什么⽅法被拦截、需要处理什么⽅法 * 定义切⼊点 * @Pointcut("匹配规则") * * Aop 切⼊点表达式简介 * 1. 执⾏任意公共⽅法: * execution(public *(..)) * 2. 执⾏任意的set⽅法 * execution(* set*(..)) * 3. 执⾏com.xxxx.service包下任意类的任意⽅法 * execution(* com.xxxx.service.*.*(..)) * 4. 执⾏com.xxxx.service 包 以及⼦包下任意类的任意⽅法 * execution(* com.xxxx.service..*.*(..)) * * 注:表达式中的第⼀个* 代表的是⽅法的修饰范围 * 可选值:private、protected、public (* 表示所有范围) */ @Pointcut("execution (* com.xxxx.service..*.*(..) )") public void cut(){} /** * 声明前置通知 并将通知应⽤到定义的切⼊点上 * ⽬标类⽅法执⾏前 执⾏该通知 * */ @Before(value = "cut()") public void before() { System.out.println("前置通知....."); } /** * 声明返回通知 并将通知应⽤到定义的切⼊点上 * ⽬标类⽅法(⽆异常)执⾏后 执⾏该通知 * */ @AfterReturning(value = "cut()") public void afterReturn() { System.out.println("返回通知....."); } /** * 声明最终通知 并将通知应⽤到定义的切⼊点上 * ⽬标类⽅法(⽆异常或有异常)执⾏后 执⾏该通知 * */ @After(value = "cut()") public void after() { System.out.println("最终通知....."); } /** * 声明异常通知 并将通知应⽤到定义的切⼊点上 * ⽬标类⽅法出现异常时 执⾏该通知 */ @AfterThrowing(value="cut()",throwing = "e") public void afterThrow(Exception e) { System.out.println("异常通知....." + " 异常原因:" + e.getCause()); } /** * 声明环绕通知 并将通知应⽤到切⼊点上 * ⽅法执⾏前后 通过环绕通知定义相应处理 * 需要通过显式调⽤对应的⽅法,否则⽆法访问指定⽅法 (pjp.proceed();) * @param pjp * @return */ @Around(value = "cut()") public Object around(ProceedingJoinPoint pjp) { System.out.println("前置通知..."); Object object = null; try { object = pjp.proceed(); System.out.println(pjp.getTarget() + "======" + pjp.getSignature()); // System.out.println("返回通知..."); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("异常通知..."); } System.out.println("最终通知..."); return object; } }
<!--配置AOP代理--> <aop:aspectj-autoproxy/>
** * 切⾯ * 切⼊点和通知的抽象 (与⾯向对象中的 类 相似) * 定义 切⼊点和通知 (切⼊点定义了要拦截哪些类的哪些⽅法,通知则定义了拦截过⽅法后要做什么) */ @Component // 将对象交给IOC容器去实例化 public class LogCut02 { public void cut(){} /** * 声明前置通知 并将通知应⽤到定义的切⼊点上 * ⽬标类⽅法执⾏前 执⾏该通知 */ public void before() { System.out.println("前置通知....."); } /** * 声明返回通知 并将通知应⽤到定义的切⼊点上 * ⽬标类⽅法(⽆异常)执⾏后 执⾏该通知 * */ public void afterReturn() { System.out.println("返回通知....."); } /** * 声明最终通知 并将通知应⽤到定义的切⼊点上 * ⽬标类⽅法(⽆异常或有异常)执⾏后 执⾏该通知 * */ public void after() { System.out.println("最终通知....."); } /** * 声明异常通知 并将通知应⽤到定义的切⼊点上 * ⽬标类⽅法出现异常时 执⾏该通知 */ public void afterThrow(Exception e) { System.out.println("异常通知....." + " 异常原因:" + e.getCause()); } /** * 声明环绕通知 并将通知应⽤到切⼊点上 * ⽅法执⾏前后 通过环绕通知定义相应处理 * 需要通过显式调⽤对应的⽅法,否则⽆法访问指定⽅法 (pjp.proceed();) * @param pjp * @return */ public Object around(ProceedingJoinPoint pjp) { System.out.println("前置通知..."); Object object = null; try { object = pjp.proceed(); System.out.println(pjp.getTarget() + "======" + pjp.getSignature()); // System.out.println("返回通知..."); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("异常通知..."); } System.out.println("最终通知..."); return object; } }
<!--aop相关配置--> <aop:config> <!--aop切⾯--> <aop:aspect ref="logCut02"> <!-- 定义aop 切⼊点 --> <aop:pointcut id="cut" expression="execution(* com.xxxx.service..*.*(..))"/> <!-- 配置前置通知 指定前置通知⽅法名 并引⽤切⼊点定义 --> <aop:before method="before" pointcut-ref="cut"/> <!-- 配置返回通知 指定返回通知⽅法名 并引⽤切⼊点定义 --> <aop:after-returning method="afterReturn" pointcut-ref="cut"/> <!-- 配置异常通知 指定异常通知⽅法名 并引⽤切⼊点定义 --> <aop:after-throwing method="afterThrow" throwing="e" pointcut-ref="cut"/> <!-- 配置最终通知 指定最终通知⽅法名 并引⽤切⼊点定义 --> <aop:after method="after" pointcut-ref="cut"/> <!-- 配置环绕通知 指定环绕通知⽅法名 并引⽤切⼊点定义 --> <aop:around method="around" pointcut-ref="cut"/> </aop:aspect> </aop:config>
- 接口定义
- 目标对象与代理对象必须实现统一接口
- 代理对象持有目标对象的引用,增强目标对象行为
- 静态代理:手动为目标对象制作代理对象,即在程序编译阶段完成代理对象的创建
- 动态代理:在程序运行期动态创建目标对象对应代理对象。
- jdk 动态代理:被代理目标对象必须实现某一或某一组接口实现方式通过回调创建代理对象。
- cglib 动态代理:被代理目标对象可以不必实现接口,继承的方式实现
- 面向切面,相比 oop 关注的是代码中的层或面
- 解耦,提高系统扩展性
- 提高代码复用
- 连接点:每一个方法
- 切入点:匹配的方法集合
- 切面:连接点与切入点的集合决定了切面,横切关注点的抽象
- 通知:集中通知
- 目标对象:被代理对象
- 植入:程序运行期将切面应用到目标对象并生成代理对象的过程
- 引入:在不修改原始代码情况下,在程序运行期为程序动态引入方法或字段的过程
感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞!
原文链接:https://segmentfault.com/a/1190000027084162
如果觉得本文对你有帮助,可以点赞关注支持一下,也可以关注我公众号,上面有更多技术干货文章以及相关资料共享,大家一起学习进步!