• Spring系列文章:面向切面编程AOP


    一、代理模式

    1、代理模式使用场景引入

    ⽣活场景1:⽜村的⽜⼆看上了隔壁村⼩花,⽜⼆不好意思直接找⼩花,于是⽜⼆找来了媒婆王妈妈。这 ⾥⾯就有⼀个⾮常典型的代理模式。⽜⼆不能和⼩花直接对接,只能找⼀个中间⼈。其中王妈妈是代理 类,⽜⼆是⽬标类。王妈妈代替⽜⼆和⼩花先⻅个⾯。(现实⽣活中的婚介所)【在程序中,对象A和对 象B⽆法直接交互时。】

    ⽣活场景2:你刚到北京,要租房⼦,可以⾃⼰找,也可以找链家帮你找。其中链家是代理类,你是⽬标 类。你们两个都有共同的⾏为:找房⼦。不过链家除了满⾜你找房⼦,另外会收取⼀些费⽤的。(现实⽣ 活中的房产中介)【在程序中,功能需要增强时。】

    ⻄游记场景:⼋戒和⾼⼩姐的故事。⼋戒要强抢⺠⼥⾼翠兰。悟空得知此事之后怎么做的?悟空幻化成 ⾼⼩姐的模样。代替⾼⼩姐与⼋戒会⾯。其中⼋戒是客户端程序。悟空是代理类。⾼⼩姐是⽬标类。那 天夜⾥,在⼋戒眼⾥,眼前的就是⾼⼩姐,对于⼋戒来说,他是不知道眼前的⾼⼩姐是悟空幻化的,在 他内⼼⾥这就是⾼⼩姐。所以悟空代替⾼⼩姐和⼋戒亲了嘴⼉。这是⾮常典型的代理模式实现的保护机 制。代理模式中有⼀个⾮常重要的特点:对于客户端程序来说,使⽤代理对象时就像在使⽤⽬标对象⼀ 样。【在程序中,⽬标需要被保护时】

    业务场景:系统中有A、B、C三个模块,使⽤这些模块的前提是需要⽤户登录,也就是说在A模块中要编 写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没 有得到复⽤,可以为A、B、C三个模块提供⼀个代理,在代理当中写⼀次登录判断即可。代理的逻辑 是:请求来了之后,判断⽤户是否登录了,如果已经登录了,则执⾏对应的⽬标,如果没有登录则跳转 到登录⻚⾯。【在程序中,⽬标不但受到保护,并且代码也得到了复⽤。】

    代理模式是GoF23种设计模式之⼀。属于结构型设计模式。

    代理模式的作⽤是:为其他对象提供⼀种代理以控制对这个对象的访问。在某些情况下,⼀个客户不想 或者不能直接引⽤⼀个对象,此时可以通过⼀个称之为“代理”的第三者来实现间接引⽤。代理对象可以 在客户端和⽬标对象之间起到中介的作⽤,并且可以通过代理对象去掉客户不应该看到的内容和服务或 者添加客户需要的额外服务。 通过引⼊⼀个新的对象来实现对真实对象的操作或者将新的对象作为真实 对象的⼀个替身,这种实现机制即为代理模式,通过引⼊代理对象来间接访问⼀个对象,这就是代理模 式的模式动机。

    代理模式中的⻆⾊:

    • 代理类(代理主题)
    • ⽬标类(真实主题)

    代理类和⽬标类的公共接⼝(抽象主题):客户端在使⽤代理类时就像在使⽤⽬标类,不被客户端 所察觉,所以代理类和⽬标类要有共同的⾏为,也就是实现共同的接⼝。

    代理模式的类图:

    1. public interface OrderService {
    2. /**
    3. * ⽣成订单
    4. */
    5. void generate();
    6. /**
    7. * 查看订单详情
    8. */
    9. void detail();
    10. /**
    11. * 修改订单
    12. */
    13. void modify();
    14. }

    代理模式在代码实现上,包括两种形式: 静态代理 动态代理 

    2、静态代理

    现在有这样⼀个接⼝和实现类

    1. public interface OrderService {
    2. /**
    3. * ⽣成订单
    4. */
    5. void generate();
    6. /**
    7. * 查看订单详情
    8. */
    9. void detail();
    10. /**
    11. * 修改订单
    12. */
    13. void modify();
    14. }

     实现类如下

    1. public class OrderServiceImpl implements OrderService {
    2. @Override
    3. public void generate() {
    4. try {
    5. Thread.sleep(1234);
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. System.out.println("订单已⽣成");
    10. }
    11. @Override
    12. public void detail() {
    13. try {
    14. Thread.sleep(2541);
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. System.out.println("订单信息如下:******");
    19. }
    20. @Override
    21. public void modify() {
    22. try {
    23. Thread.sleep(1010);
    24. } catch (InterruptedException e) {
    25. e.printStackTrace();
    26. }
    27. System.out.println("订单已修改");
    28. }
    29. }

     其中Thread.sleep()⽅法的调⽤是为了模拟操作耗时。

    项⽬已上线,并且运⾏正常,只是客户反馈系统有⼀些地⽅运⾏较慢,要求项⽬组对系统进⾏优化。于 是项⽬负责⼈就下达了这个需求。⾸先需要搞清楚是哪些业务⽅法耗时较⻓,于是让我们统计每个业务 ⽅法所耗费的时⻓。如果是你,你该怎么做呢? 第⼀种⽅案:直接修改Java源代码,在每个业务⽅法中添加统计逻辑,如下:

    1. public class OrderServiceImpl implements OrderService {
    2. @Override
    3. public void generate() {
    4. long begin = System.currentTimeMillis();
    5. try {
    6. Thread.sleep(1234);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. System.out.println("订单已⽣成");
    11. long end = System.currentTimeMillis();
    12. System.out.println("耗费时⻓" + (end - begin) + "毫秒");
    13. }
    14. @Override
    15. public void detail() {
    16. long begin = System.currentTimeMillis();
    17. try {
    18. Thread.sleep(2541);
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. System.out.println("订单信息如下:******");
    23. long end = System.currentTimeMillis();
    24. System.out.println("耗费时⻓" + (end - begin) + "毫秒");
    25. }
    26. @Override
    27. public void modify() {
    28. long begin = System.currentTimeMillis();
    29. try {
    30. Thread.sleep(1010);
    31. } catch (InterruptedException e) {
    32. e.printStackTrace();
    33. }
    34. System.out.println("订单已修改");
    35. long end = System.currentTimeMillis();
    36. System.out.println("耗费时⻓" + (end - begin) + "毫秒");
    37. }
    38. }

     需求可以满⾜,但显然是违背了OCP开闭原则。这种⽅案不可取。

    第⼆种⽅案:使⽤代理模式(这⾥采⽤静态代理) 可以为OrderService接⼝提供⼀个代理类。

    1. public class OrderServiceProxy implements OrderService { // 代理对象
    2. // ⽬标对象
    3. private OrderService orderService;
    4. // 通过构造⽅法将⽬标对象传递给代理对象
    5. public OrderServiceProxy(OrderService orderService) {
    6. this.orderService = orderService;
    7. }
    8. @Override
    9. public void generate() {
    10. long begin = System.currentTimeMillis();
    11. // 执⾏⽬标对象的⽬标⽅法
    12. orderService.generate();
    13. long end = System.currentTimeMillis();
    14. System.out.println("耗时" + (end - begin) + "毫秒");
    15. }
    16. @Override
    17. public void detail() {
    18. long begin = System.currentTimeMillis();
    19. // 执⾏⽬标对象的⽬标⽅法
    20. orderService.detail();
    21. long end = System.currentTimeMillis();
    22. System.out.println("耗时" + (end - begin) + "毫秒");
    23. }
    24. @Override
    25. public void modify() {
    26. long begin = System.currentTimeMillis();
    27. // 执⾏⽬标对象的⽬标⽅法
    28. orderService.modify();
    29. long end = System.currentTimeMillis();
    30. System.out.println("耗时" + (end - begin) + "毫秒");
    31. }
    32. }

    这种⽅式的优点:符合OCP开闭原则,同时采⽤的是关联关系,所以程序的耦合度较低。所以这种⽅案 是被推荐的。

    主程序

    1. public class Client {
    2. public static void main(String[] args) {
    3. // 创建⽬标对象
    4. OrderService target = new OrderServiceImpl();
    5. // 创建代理对象
    6. OrderService proxy = new OrderServiceProxy(target);
    7. // 调⽤代理对象的代理⽅法
    8. proxy.generate();
    9. proxy.modify();
    10. proxy.detail();
    11. }
    12. }

    以上就是代理模式中的静态代理,其中OrderService接⼝是代理类和⽬标类的共同接⼝。 OrderServiceImpl是⽬标类。OrderServiceProxy是代理类。 ⼤家思考⼀下:如果系统中业务接⼝很多,⼀个接⼝对应⼀个代理类,显然也是不合理的,会导致类爆 炸。怎么解决这个问题?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们⽣成代理类 的字节码。代理类不需要我们写了。类爆炸解决了,⽽且代码只需要写⼀次,代码也会得到复⽤。 

    3、动态代理

    在程序运⾏阶段,在内存中动态⽣成代理类,被称为动态代理,⽬的是为了减少代理类的数量。解决代 码复⽤的问题。

    在内存当中动态⽣成类的技术常⻅的包括:

    • JDK动态代理技术:只能代理接⼝。
    • CGLIB动态代理技术:CGLIB(Code Generation Library)是⼀个开源项⽬。是⼀个强⼤的,⾼性 能,⾼质量的Code⽣成类库,它可以在运⾏期扩展Java类与实现Java接⼝。它既可以代理接⼝,⼜ 可以代理类,底层是通过继承的⽅式实现的。性能⽐JDK动态代理要好。(底层有⼀个⼩⽽快的字 节码处理框架ASM。)
    • Javassist动态代理技术:Javassist是⼀个开源的分析、编辑和创建Java字节码的类库。是由东京⼯ 业⼤学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加⼊了开放源代码 JBoss 应⽤服务器项⽬,通过使⽤Javassist对字节码操作为JBoss实现动态"AOP"框架。

    3.1、jdk动态代理

    我们还是使⽤静态代理中的例⼦:⼀个接⼝和⼀个实现类。

    我们在静态代理的时候,除了以上⼀个接⼝和⼀个实现类之外,还要写⼀个代理类 UserServiceProxy,在动态代理中UserServiceProxy代理类是可以动态⽣成的。这个类不需要写。我 们直接写客户端程序即可:

    1. public class Client {
    2. public static void main(String[] args) {
    3. // 第⼀步:创建⽬标对象
    4. OrderService target = new OrderServiceImpl();
    5. // 第⼆步:创建代理对象
    6. OrderService orderServiceProxy = Proxy.newProxyInstance(
    7. target.getClass().getClassLoader(),
    8. target.getClass().getInterfaces(), 调⽤处理器对象);
    9. // 第三步:调⽤代理对象的代理⽅法
    10. orderServiceProxy.detail();
    11. orderServiceProxy.modify();
    12. orderServiceProxy.generate();
    13. }
    14. }

    以上第⼆步创建代理对象是需要⼤家理解的: OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调⽤处理器对象);

    newProxyInstance这⾏代码做了两件事:

    第⼀件事:在内存中动态的生成了一个代理类的字节码class

    第⼆件事:new对象了,通过内存中生成的代理类这个代码,实例化代理对象

    java.lang.reflect.Proxy。这是JDK提供的⼀个类(所以称为JDK动态代理)。主要是通过 这个类在内存中⽣成代理类的字节码。

    其中newProxyInstance()⽅法有三个参数:

    • 第⼀个参数:类加载器。在内存中⽣成了字节码,要想执⾏这个字节码,也是需要先把这个字节码 加载到内存当中的。所以要指定使⽤哪个类加载器加载。并且jdk要求,目标类的加载器必须和代理类的类加载器使用同一个
    • 第⼆个参数:接⼝类型。代理类和⽬标类实现相同的接⼝,所以要通过这个参数告诉JDK动态代理 ⽣成的类要实现哪些接⼝。
    • 第三个参数:调⽤处理器。这是⼀个JDK动态代理规定的接⼝,接⼝全名: java.lang.reflect.InvocationHandler。显然这是⼀个回调接⼝,也就是说调⽤这个接⼝中⽅法的程 序已经写好了,就差这个接⼝的实现类了。

    所以接下来我们要写⼀下java.lang.reflect.InvocationHandler接⼝的实现类,并且实现接⼝中的⽅法, 代码如下:

    1. public class TimerInvocationHandler implements InvocationHandler {
    2. @Override
    3. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    4. return null;
    5. }
    6. }

    InvocationHandler接⼝中有⼀个⽅法invoke,这个invoke⽅法上有三个参数:

    • 第⼀个参数:Object proxy。代理对象。设计这个参数只是为了后期的⽅便,如果想在invoke⽅法中 使⽤代理对象的话,尽管通过这个参数来使⽤。
    • 第⼆个参数:Method method。⽬标⽅法。
    • 第三个参数:Object[] args。⽬标⽅法调⽤时要传的参数。

    我们将来肯定是要调⽤“⽬标⽅法”的,但要调⽤⽬标⽅法的话,需要“⽬标对象”的存在,“⽬标对象”从 哪⼉来呢?我们可以给TimerInvocationHandler提供⼀个构造⽅法,可以通过这个构造⽅法传过来“⽬标 对象”,代码如下:

    1. public class TimerInvocationHandler implements InvocationHandler {
    2. // ⽬标对象
    3. private Object target;
    4. // 通过构造⽅法来传⽬标对象
    5. public TimerInvocationHandler(Object target) {
    6. this.target = target;
    7. }
    8. //当代理对象调用代理方法的时候 这个invoke会被jdk调用
    9. @Override
    10. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    11. return null;
    12. }
    13. }

    有了⽬标对象我们就可以在invoke()⽅法中调⽤⽬标⽅法了。代码如下:

    1. public class TimerInvocationHandler implements InvocationHandler {
    2. // ⽬标对象
    3. private Object target;
    4. // 通过构造⽅法来传⽬标对象
    5. public TimerInvocationHandler(Object target) {
    6. this.target = target;
    7. }
    8. @Override
    9. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    10. // ⽬标执⾏之前增强。
    11. long begin = System.currentTimeMillis();
    12. // 调⽤⽬标对象的⽬标⽅法
    13. Object retValue = method.invoke(target, args);
    14. // ⽬标执⾏之后增强。
    15. long end = System.currentTimeMillis();
    16. System.out.println("耗时"+(end - begin)+"毫秒");
    17. // ⼀定要记得返回。
    18. return retValue;
    19. }
    20. }

    到此为⽌,调⽤处理器就完成了。接下来,应该继续完善Client程序

    1. public class Client {
    2. public static void main(String[] args) {
    3. // 创建⽬标对象
    4. OrderService target = new OrderServiceImpl();
    5. // 创建代理对象
    6. OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
    7. target.getClass().getInterfaces(),
    8. new TimerInvocationHandler(target));
    9. // 调⽤代理对象的代理⽅法
    10. orderServiceProxy.detail();
    11. orderServiceProxy.modify();
    12. orderServiceProxy.generate();
    13. }
    14. }

    ⼤家可能会⽐较好奇:那个InvocationHandler接⼝中的invoke()⽅法没看⻅在哪⾥调⽤呀? 注意:当你调⽤代理对象的代理⽅法的时候,注册在InvocationHandler接⼝中的invoke()⽅法会被调 ⽤。

    orderServiceProxy.detail();
    orderServiceProxy.modify();
    orderServiceProxy.generate();

    这三⾏代码中任意⼀⾏代码执⾏,注册在InvocationHandler接⼝中 的invoke()⽅法都会被调⽤。

    学到这⾥可能会感觉有点懵,折腾半天,到最后这不是还得写⼀个接⼝的实现类吗?没省劲⼉呀? 你要这样想就错了!!!! 我们可以看到,不管你有多少个Service接⼝,多少个业务类,这个TimerInvocationHandler接⼝是不是 只需要写⼀次就⾏了,代码是不是得到复⽤了!!!! ⽽且最重要的是,以后程序员只需要关注核⼼业务的编写了,像这种统计时间的代码根本不需要关注。 因为这种统计时间的代码只需要在调⽤处理器中编写⼀次即可。

    到这⾥,JDK动态代理的原理就结束了。

    不过我们看以下这个代码确实有点繁琐,对于客户端来说,⽤起来不⽅便:

    1. public class ProxyUtil {
    2. public static Object newProxyInstance(Object target) {
    3. return Proxy.newProxyInstance(target.getClass().getClassLoader(),
    4. target.getClass().getInterfaces(),
    5. new TimerInvocationHandler(target));
    6. }
    7. }

    这样客户端代码就不需要写那么繁琐了:

    1. public class Client {
    2. public static void main(String[] args) {
    3. // 创建⽬标对象
    4. OrderService target = new OrderServiceImpl();
    5. // 创建代理对象
    6. OrderService orderServiceProxy = (OrderService) ProxyUtil.newProxyInstance(target);
    7. // 调⽤代理对象的代理⽅法
    8. orderServiceProxy.detail();
    9. orderServiceProxy.modify();
    10. orderServiceProxy.generate();
    11. }
    12. }

    3.2、CGLIB动态代理

    jdk只能代理接口,而CGLIB既可以代理接⼝,⼜可以代理类。底层采⽤继承的⽅式实现。所以被代理的⽬标类不能使⽤final 修饰。 使⽤CGLIB,需要引⼊它的依赖:

    1. cglib
    2. cglib
    3. 3.3.0

    一个没有实现类的接口

    1. public class UserService {
    2. public void login(){
    3. System.out.println("⽤户正在登录系统....");
    4. }
    5. public void logout(){
    6. System.out.println("⽤户正在退出系统....");
    7. }
    8. }

    使⽤CGLIB在内存中为UserService类⽣成代理类,并创建对象

    1. public class Client {
    2. public static void main(String[] args) {
    3. // 创建字节码增强器
    4. Enhancer enhancer = new Enhancer();
    5. // 告诉cglib要继承哪个类
    6. enhancer.setSuperclass(UserService.class);
    7. // 设置回调接⼝
    8. enhancer.setCallback(⽅法拦截器对象);
    9. // ⽣成源码,编译class,加载到JVM,并创建代理对象
    10. UserService userServiceProxy = (UserService)enhancer.create();
    11. userServiceProxy.login();
    12. userServiceProxy.logout();
    13. }
    14. }

    和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,⽽是: net.sf.cglib.proxy.MethodInterceptor 编写MethodInterceptor接⼝实现类:

    1. public class TimerMethodInterceptor implements MethodInterceptor {
    2. @Override
    3. public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    4. return null;
    5. }
    6. }

    MethodInterceptor接⼝中有⼀个⽅法intercept(),该⽅法有4个参数:

    第⼀个参数:⽬标对象

    第⼆个参数:⽬标⽅法

    第三个参数:⽬标⽅法调⽤时的实参

    第四个参数:代理⽅法

    在MethodInterceptor的intercept()⽅法中调⽤⽬标以及添加增强:

    1. public class TimerMethodInterceptor implements MethodInterceptor {
    2. @Override
    3. public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    4. // 前增强
    5. long begin = System.currentTimeMillis();
    6. // 调⽤⽬标
    7. Object retValue = methodProxy.invokeSuper(target, objects);
    8. // 后增强
    9. long end = System.currentTimeMillis();
    10. System.out.println("耗时" + (end - begin) + "毫秒");
    11. // ⼀定要返回
    12. return retValue;
    13. }
    14. }

    回调已经写完了,可以修改客户端程序了:

    1. public class Client {
    2. public static void main(String[] args) {
    3. // 创建字节码增强器
    4. Enhancer enhancer = new Enhancer();
    5. // 告诉cglib要继承哪个类
    6. enhancer.setSuperclass(UserService.class);
    7. // 设置回调接⼝
    8. enhancer.setCallback(new TimerMethodInterceptor());
    9. // ⽣成源码,编译class,加载到JVM,并创建代理对象
    10. UserService userServiceProxy = (UserService)enhancer.create();
    11. userServiceProxy.login();
    12. userServiceProxy.logout();
    13. }
    14. }

    对于⾼版本的JDK,如果使⽤CGLIB,需要在启动项中添加两个启动参数:

    • --add-opens java.base/java.lang=ALL-UNNAMED
    • --add-opens java.base/sun.net.util=ALL-UNNAMED 

    二、AOP

    1、aop场景介绍

    ⼀般⼀个系统当中都会有⼀些系统服务,例如:⽇志、事务管理、安全等。这些系统服务被称为:交叉业务 这些交叉业务⼏乎是通⽤的,不管你是做银⾏账户转账,还是删除⽤户数据。⽇志、事务管理、安全, 这些都是需要做的。

    如果在每⼀个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两⽅⾯问题:

    第⼀:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复⽤。并且修改 这些交叉业务代码的话,需要修改多处。

    第⼆:程序员⽆法专注核⼼业务代码的编写,在编写核⼼业务代码的同时还需要处理这些交叉业 务。 使⽤AOP可以很轻松的解决以上问题。

    下图可以帮助快速理解AOP的思想:

    ⽤⼀句话总结AOP:将与核⼼业务⽆关的代码独⽴的抽取出来,形成⼀个独⽴的组件,然后以横向交叉 的⽅式应⽤到业务流程当中的过程被称为AOP。

    AOP的优点:

    第⼀:代码复⽤性增强。

    第⼆:代码易维护。

    第三:使开发者更关注业务逻辑。

    2、AOP的七⼤术语

    • 连接点 Joinpoint
      在程序的整个执⾏流程中,可以织⼊切⾯的位置。比如⽅法的执⾏前后,异常抛出之后等位置(指的是位置)如下

      1. public class UserService {
      2. public void do1() {
      3. System.out.println("do 1");
      4. }
      5. public void do2() {
      6. System.out.println("do 2");
      7. }
      8. public void do3() {
      9. System.out.println("do 3");
      10. }
      11. public void do4() {
      12. System.out.println("do 4");
      13. }
      14. public void do5() {
      15. System.out.println("do 5");
      16. }
      17. // 核⼼业务⽅法
      18. public void service() {
      19. try {
      20. // Joinpoint 连接点
      21. do1();
      22. // Joinpoint 连接点
      23. do2();
      24. // Joinpoint 连接点
      25. do3();
      26. // Joinpoint 连接点
      27. do5();
      28. }catch (Exception e){
      29. // Joinpoint 连接点
      30. }finally {
      31. // Joinpoint 连接点
      32. }
      33. }
      34. }
    • 切点 Pointcut

            在程序执⾏流程中,真正织⼊切⾯的⽅法(指的是方法)。(⼀个切点对应多个连接点)

    1. public void service() {
    2. try {
    3. do1();//Pointcut
    4. do2();//Pointcut
    5. do3();//Pointcut
    6. do5();//Pointcut
    7. }catch (Exception e){
    8. }
    9. }
    • 通知 Advice

            通知⼜叫增强,就是具体你要织⼊的代码,通知包括:

    • 前置通知
    • 后置通知
    • 环绕通知
    • 异常通知
    • 最终通知
    1. // 核⼼业务⽅法
    2. public void service() {
    3. try {
    4. //前置通知(do1方法前执行的代码)
    5. do1();
    6. //后置通知(do1方法后执行的代码)
    7. do2();
    8. // 环绕通知(在do3前和结束都有执行的代码)
    9. do3();
    10. // 环绕通知
    11. do5();
    12. }catch (Exception e){
    13. // 异常通知
    14. }finally {
    15. // 最终通知
    16. }
    17. }
    • 切⾯ Aspect

    切点+通知就是切面

    • 织⼊ Weaving

    把通知应用到目标对象的过程

    • 代理对象 Proxy

    一个对象被织入后产生的新对象

    • ⽬标对象 Target

    被织入通知的对象

    3、切点表达式

    切点表达式用来定义通知往哪些方法切入,语法格式如下

    execution([访问控制权限修饰符] 返回值类型 [全限定类名]⽅法名(形式参数列表) [异常])

    3.1、访问控制权限修饰符:

    • 可选项。
    • 没写,就是4个权限都包括。
    • 写public就表示只包括公开的⽅法。

    3.2、返回值类型:

    • 必填项。
    • * 表示返回值类型任意。

    3.3、全限定类名:

    • 可选项。
    • 两个点“..”代表当前包以及⼦包下的所有类。
    • 省略时表示所有的类。

    3.4、⽅法名:

    • 必填项。
    • *表示所有⽅法。
    • set*表示所有的set⽅法。

    3.5、形式参数列表:

    • 必填项
    • () 表示没有参数的⽅法
    • (..) 参数类型和个数随意的⽅法
    • (*) 只有⼀个参数的⽅法
    • (*, String) 第⼀个参数类型随意,第⼆个参数是String的。

    3.6、异常:

    • 可选项。
    • 省略时表示任意异常类型。

    表达式案例

    service包下所有一delete开始的所有方法

    execution(public * com.demo.service.*.delete*(..))

    service包下所有类的所有方法

    execution(* com.demo.service..*(..))
    

    所有类的所有方法

    execution(* *(..))

    三、使⽤Spring的AOP

    IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使⽤的功能,把它转化成组件。

    AOP(Aspect Oriented Programming):⾯向切⾯编程(AOP是⼀种编程技术) AOP是对OOP的补充延伸。

    AOP底层使⽤的就是动态代理来实现的。

    Spring的AOP使⽤的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵 活切换,如果是代理接⼝,会默认使⽤JDK动态代理,如果要代理某个类,这个类没有实现接⼝,就会 切换使⽤CGLIB。当然,你也可以强制通过⼀些配置让Spring只使⽤CGLIB。

    1、简介

    Spring对AOP的实现包括以下3种⽅式:

    第⼀种⽅式:Spring框架结合AspectJ框架实现的AOP,基于注解⽅式。

    第⼆种⽅式:Spring框架结合AspectJ框架实现的AOP,基于XML⽅式。

    第三种⽅式:Spring框架⾃⼰实现的AOP,基于XML配置⽅式。

    实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第⼀种和第⼆种⽅式。

    什么是AspectJ?(Eclipse组织的⼀个⽀持AOP的框架。AspectJ框架是独⽴于Spring框架之外的⼀个框 架,Spring框架⽤了AspectJ) 。

    AspectJ项⽬起源于帕洛阿尔托(Palo Alto)研究中⼼(缩写为PARC)。该中⼼由Xerox集团资助, Gregor Kiczales领导,从1997年开始致⼒于AspectJ的开发,1998年第⼀次发布给外部⽤户,2001年发 布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3⽉正式将AspectJ项⽬移交给了 Eclipse组织,因为AspectJ的发展和受关注程度⼤⼤超出了PARC的预期,他们已经⽆⼒继续维持它的发 展。

    2、Spring结合Aspectj基于注解实现AOP

    导入依赖

    1. <dependency>
    2. <groupId>org.springframeworkgroupId>
    3. <artifactId>spring-contextartifactId>
    4. <version>6.0.2version>
    5. dependency>
    6. <dependency>
    7. <groupId>org.springframeworkgroupId>
    8. <artifactId>spring-aspectsartifactId>
    9. <version>6.0.2version>
    10. dependency>

    注意context依赖已经包含aop注解了,所以不需要再单独引入以下aop依赖

    1. org.springframework
    2. spring-aop
    3. 6.0.2

    Spring配置⽂件中添加context命名空间和aop命名空间并开启bean组件扫描

    1. "1.0" encoding="UTF-8"?>
    2. <beans xmlns="http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xmlns:context="http://www.springframework.org/schema/context"
    5. xmlns:aop="http://www.springframework.org/schema/aop"
    6. xsi:schemaLocation="http://www.springframework.org/schema/beans
    7. http://www.springframework.org/schema/beans/spring-beans.xsd
    8. http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    9. http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    10. <context:component-scan base-package="com.demo.service"/>
    11. <aop:aspectj-autoproxy proxy-target-class="true"/>
    12. beans>

    开启⾃动代理之后,凡事带有@Aspect注解的 bean都会⽣成代理对象。 

    proxy-target-class="true" 表示采⽤cglib动态代理。

    proxy-target-class="false" 表示采⽤jdk动态代理。默认值是false。即使写成false,当没有接⼝的时 候,也会⾃动选择cglib⽣成代理类。

     业务类(目标类)

    1. @Component
    2. public class OrderService {
    3. // ⽬标⽅法
    4. public void generate(){
    5. System.out.println("订单已⽣成!");
    6. }
    7. }

     编写切⾯类

    通知类型包括:

    • 前置通知:@Before ⽬标⽅法执⾏之前的通知
    • 后置通知:@AfterReturning ⽬标⽅法执⾏之后的通知
    • 环绕通知:@Around ⽬标⽅法之前添加通知,同时⽬标⽅法执⾏之后添加通知。
    • 异常通知:@AfterThrowing 发⽣异常之后执⾏的通知
    • 最终通知:@After 放在finally语句块中的通知
    1. @Component
    2. @Aspect
    3. public class MyAspect {
    4. @Around("execution(* com.demo.service.OrderService.* (..))")
    5. public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    6. System.out.println("环绕通知开始");
    7. // 执⾏⽬标⽅法。
    8. proceedingJoinPoint.proceed();
    9. System.out.println("环绕通知结束");
    10. }
    11. @Before("execution(* com.demo.service.OrderService.* (..))")
    12. public void beforeAdvice(){
    13. System.out.println("前置通知");
    14. }
    15. @AfterReturning("execution(* com.demo.service.OrderService.*(..))")
    16. public void afterReturningAdvice(){
    17. System.out.println("后置通知");
    18. }
    19. @AfterThrowing("execution(* com.demo.service.OrderService.*(..))")
    20. public void afterThrowingAdvice(){
    21. System.out.println("异常通知");
    22. }
    23. @After("execution(* com.demo.service.OrderService.*(..))"
    24. )
    25. public void afterAdvice(){
    26. System.out.println("最终通知");
    27. }
    28. }

     测试

    1. @Test
    2. public void testAOP(){
    3. ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    4. OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
    5. orderService.generate();
    6. }

    执行结果 

     通过上⾯的执⾏结果就可以判断他们的执⾏顺序了,这⾥不再赘述。 结果中没有异常通知,这是因为⽬标程序执⾏过程中没有发⽣异常。我们尝试让⽬标⽅法发⽣异常:

    1. @Component
    2. public class OrderService {
    3. // ⽬标⽅法
    4. public void generate(){
    5. System.out.println("订单已⽣成!");
    6. if (1 == 1) {
    7. throw new RuntimeException("模拟异常发⽣");
    8. }
    9. }
    10. }

    测试结果

     

     通过测试得知,当发⽣异常之后,最终通知也会执⾏,因为最终通知@After会出现在finally语句块中。 出现异常之后,后置通知和环绕通知的结束部分不会执⾏。

    切面的先后顺序

    业务流程当中不⼀定只有⼀个切⾯,可能有的切⾯控制事务,有的记录⽇志,有的进⾏安全 控制,如果多个切⾯的话,顺序如何控制:可以使⽤@Order注解来标识切⾯类,为@Order注解的value 指定⼀个整数型的数字,数字越⼩,优先级越⾼。 再定义⼀个切⾯类

    1. @Aspect
    2. @Component
    3. @Order(1) //设置优先级
    4. public class YourAspect {
    5. @Around("execution(* com.demo.service.OrderService.*(..))")
    6. public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
    7. System.out.println("YourAspect环绕通知开始");
    8. // 执⾏⽬标⽅法。
    9. proceedingJoinPoint.proceed();
    10. System.out.println("YourAspect环绕通知结束");
    11. }
    12. @Before("execution(* com.demo.service.OrderService.*(..))")
    13. public void beforeAdvice() {
    14. System.out.println("YourAspect前置通知");
    15. }
    16. @AfterReturning("execution(* com.demo.service.OrderService.*(..))")
    17. public void afterReturningAdvice() {
    18. System.out.println("YourAspect后置通知");
    19. }
    20. @AfterThrowing("execution(* com.demo.service.OrderService.*(..))")
    21. public void afterThrowingAdvice() {
    22. System.out.println("YourAspect异常通知");
    23. }
    24. @After("execution(* com.demo.service.OrderService.*(..))")
    25. public void afterAdvice() {
    26. System.out.println("YourAspect最终通知");
    27. }
    28. }
    1. @Component
    2. @Aspect
    3. @Order(2) //设置优先级
    4. public class MyAspect {
    5. @Around("execution(* com.demo.service.OrderService.*(..))")
    6. public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    7. System.out.println("环绕通知开始");
    8. // 执⾏⽬标⽅法。
    9. proceedingJoinPoint.proceed();
    10. System.out.println("环绕通知结束");
    11. }
    12. @Before("execution(* com.demo.service.OrderService.*(..))")
    13. public void beforeAdvice(){
    14. System.out.println("前置通知");
    15. }
    16. @AfterReturning("execution(* com.demo.service.OrderService.*(..))")
    17. public void afterReturningAdvice(){
    18. System.out.println("后置通知");
    19. }
    20. @AfterThrowing("execution(* com.demo.service.OrderService.*(..))")
    21. public void afterThrowingAdvice(){
    22. System.out.println("异常通知");
    23. }
    24. @After("execution(* com.demo.service.OrderService.*(..))")
    25. public void afterAdvice(){
    26. System.out.println("最终通知");
    27. }
    28. }

     测试程序

     通过修改@Order注解的整数值来切换顺序,执⾏测试程序

    优化使⽤切点表达式 

    观察上面切面类缺点是:

    第⼀:切点表达式重复写了多次,没有得到复⽤。

    第⼆:如果要修改切点表达式,需要修改多处,难维护。

    可以这样做:将切点表达式单独的定义出来,在需要的位置引⼊即可。如下:

    1. // 切⾯类
    2. @Component
    3. @Aspect
    4. @Order(2)
    5. public class MyAspect {
    6. @Pointcut("execution(* com.demo.service.OrderService.*(..))")
    7. public void pointcut() {
    8. }
    9. @Around("pointcut()")
    10. public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    11. System.out.println("环绕通知开始");
    12. // 执⾏⽬标⽅法。
    13. proceedingJoinPoint.proceed();
    14. System.out.println("环绕通知结束");
    15. }
    16. @Before("pointcut()")
    17. public void beforeAdvice() {
    18. System.out.println("前置通知");
    19. }
    20. @AfterReturning("pointcut()")
    21. public void afterReturningAdvice() {
    22. System.out.println("后置通知");
    23. }
    24. @AfterThrowing("pointcut()")
    25. public void afterThrowingAdvice() {
    26. System.out.println("异常通知");
    27. }
    28. @After("pointcut()")
    29. public void afterAdvice() {
    30. System.out.println("最终通知");
    31. }
    32. }

    使⽤@Pointcut注解来定义独⽴的切点表达式。

    注意这个@Pointcut注解标注的⽅法随意,只是起到⼀个能够让@Pointcut注解编写的位置。

    执⾏测试程序:

    全注解开发AOP

    就是编写⼀个类,在这个类上⾯使⽤⼤量注解来代替spring的配置⽂件,spring配置⽂件消失了,如下:

    1. @Configuration
    2. @ComponentScan("com.demo.service")
    3. @EnableAspectJAutoProxy(proxyTargetClass = true)
    4. public class Spring6Configuration {
    5. }

     测试程序也变化了:

    1. @Test
    2. public void testAOPWithAllAnnotation(){
    3. ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
    4. OrderService orderService = applicationContext.getBean("orderService",OrderService.class);
    5. orderService.generate();
    6. }

    3、基于XML配置⽅式的AOP(了解)

    目标类

    1. // ⽬标类
    2. public class VipService {
    3. public void add(){
    4. System.out.println("保存vip信息。");
    5. }
    6. }

     编写切⾯类,并且编写通知

    1. // 负责计时的切⾯类
    2. public class TimerAspect {
    3. public void time(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    4. long begin = System.currentTimeMillis();
    5. //执⾏⽬标
    6. proceedingJoinPoint.proceed();
    7. long end = System.currentTimeMillis();
    8. System.out.println("耗时"+(end - begin)+"毫秒");
    9. }
    10. }

     编写spring配置⽂件

    1. "1.0" encoding="UTF-8"?>
    2. "http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xmlns:context="http://www.springframework.org/schema/context"
    5. xmlns:aop="http://www.springframework.org/schema/aop"
    6. xsi:schemaLocation="http://www.springframework.org/schema/beans
    7. http://www.springframework.org/schema/beans/spring-beans.xsd
    8. http://www.springframework.org/schema/context
    9. http://www.springframework.org/schema/context/spring-context.xsd
    10. http://www.springframework.org/schema/aop
    11. http://www.springframework.org/schema/aop/spring-aop.xsd">
    12. "vipService" class="com.demo.service.VipService"/>
    13. "timerAspect" class="com.demo.service.TimerAspect"/>
    14. "p" expression="execution(* com.demo.service.VipService.*(..))"/>
    15. "timerAspect">
    16. "time" pointcut-ref="p"/>

    测试

    1. @Test
    2. public void testAOPXml(){
    3. ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop-xml.xml");
    4. VipService vipService = applicationContext.getBean("vipService", VipService.class);
    5. vipService.add();
    6. }

    4、aop实际应用-日志记录

    凡事在系统中进⾏修 改操作的,删除操作的,新增操作的,都要把操作的人记录下来。因为这⼏个操作是属于危险⾏为。例如 有业务类和业务⽅法:

    1. @Component
    2. //⽤户业务
    3. public class UserService {
    4. public void getUser() {
    5. System.out.println("获取⽤户信息");
    6. }
    7. public void saveUser() {
    8. System.out.println("保存⽤户");
    9. }
    10. public void deleteUser() {
    11. System.out.println("删除⽤户");
    12. }
    13. public void modifyUser() {
    14. System.out.println("修改⽤户");
    15. }
    16. }
    1. // 商品业务类
    2. @Component
    3. public class ProductService {
    4. public void getProduct(){
    5. System.out.println("获取商品信息");
    6. }
    7. public void saveProduct(){
    8. System.out.println("保存商品");
    9. }
    10. public void deleteProduct(){
    11. System.out.println("删除商品");
    12. }
    13. public void modifyProduct(){
    14. System.out.println("修改商品");
    15. }
    16. }

    接下来我们使⽤aop来解决上⾯的需求:编写⼀个负责安全的切⾯类

    1. @Component
    2. @Aspect
    3. public class SecurityAspect {
    4. @Pointcut("execution(* com.demo.service..save*(..))")
    5. public void savePointcut(){}
    6. @Pointcut("execution(* com.demo.service..delete*(..))")
    7. public void deletePointcut(){}
    8. @Pointcut("execution(* com.demo.service..modify*(..))")
    9. public void modifyPointcut(){}
    10. @Before("savePointcut() || deletePointcut() || modifyPointcut()")
    11. public void beforeAdivce(JoinPoint joinpoint){
    12. System.out.println("XXX操作员正在操作"+joinpoint.getSignature().getName()+"⽅法");
    13. }
    14. }

    测试

    1. @Test
    2. public void testSecurity() {
    3. ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
    4. UserService userService = applicationContext.getBean("userService", UserService.class);
    5. ProductService productService = applicationContext.getBean("productService", ProductService.class);
    6. userService.getUser();
    7. userService.saveUser();
    8. userService.deleteUser();
    9. userService.modifyUser();
    10. productService.getProduct();
    11. productService.saveProduct();
    12. productService.deleteProduct();
    13. productService.modifyProduct();
    14. }

    结果

  • 相关阅读:
    数据过多,select优化
    React 组件传 children 的各种方案
    Linux eBPF介绍(二)
    二维平面扭曲的python实现及思路
    【数据结构与算法】递归全流程详细剖析 | 详解图的深度优先遍历
    算法通关村第十七关——跳跃游戏
    Vue3中Compositions API的使用(二)
    Arch更新报错?
    Hystrix使用篇 - Hystrix的使用
    【C语言】嵌套结构体初始化 - 一个有趣的结论
  • 原文地址:https://blog.csdn.net/qq_34491508/article/details/132757386