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

news2024/10/11 12:23:18

一、代理模式

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

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

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

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

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

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

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

代理模式中的⻆⾊:

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

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

代理模式的类图:

public interface OrderService {
 /**
 * ⽣成订单
 */
 void generate();
 /**
 * 查看订单详情
 */
 void detail();
 /**
 * 修改订单
 */
 void modify();
}

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

2、静态代理

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

public interface OrderService {
 /**
 * ⽣成订单
 */
 void generate();
 /**
 * 查看订单详情
 */
 void detail();
 /**
 * 修改订单
 */
 void modify();
}

 实现类如下

public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已⽣成");
    }

    @Override
    public void detail() {
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }
}

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

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


public class OrderServiceImpl implements OrderService {

        @Override
        public void generate() {
            long begin = System.currentTimeMillis();
            try {
                Thread.sleep(1234);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("订单已⽣成");
            long end = System.currentTimeMillis();
            System.out.println("耗费时⻓" + (end - begin) + "毫秒");
        }

        @Override
        public void detail() {
            long begin = System.currentTimeMillis();
            try {
                Thread.sleep(2541);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("订单信息如下:******");
            long end = System.currentTimeMillis();
            System.out.println("耗费时⻓" + (end - begin) + "毫秒");
        }

        @Override
        public void modify() {
            long begin = System.currentTimeMillis();
            try {
                Thread.sleep(1010);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("订单已修改");
            long end = System.currentTimeMillis();
            System.out.println("耗费时⻓" + (end - begin) + "毫秒");
        }
    
}

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

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

public class OrderServiceProxy implements OrderService { // 代理对象
    // ⽬标对象
    private OrderService orderService;

    // 通过构造⽅法将⽬标对象传递给代理对象
    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        // 执⾏⽬标对象的⽬标⽅法
        orderService.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        // 执⾏⽬标对象的⽬标⽅法
        orderService.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        // 执⾏⽬标对象的⽬标⽅法
        orderService.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
    }
}

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

主程序

public class Client {
     public static void main(String[] args) {
     // 创建⽬标对象
     OrderService target = new OrderServiceImpl();
     // 创建代理对象
     OrderService proxy = new OrderServiceProxy(target);
     // 调⽤代理对象的代理⽅法
     proxy.generate();
     proxy.modify();
     proxy.detail();
 }
}

以上就是代理模式中的静态代理,其中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代理类是可以动态⽣成的。这个类不需要写。我 们直接写客户端程序即可:

public class Client {
    public static void main(String[] args) {
        // 第⼀步:创建⽬标对象
        OrderService target = new OrderServiceImpl();
        // 第⼆步:创建代理对象
        OrderService orderServiceProxy = Proxy.newProxyInstance(
                target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(), 调⽤处理器对象);
        // 第三步:调⽤代理对象的代理⽅法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

以上第⼆步创建代理对象是需要⼤家理解的: 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接⼝的实现类,并且实现接⼝中的⽅法, 代码如下:

public class TimerInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
}

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

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

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

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

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

public class TimerInvocationHandler implements InvocationHandler {
    // ⽬标对象
    private Object target;
    // 通过构造⽅法来传⽬标对象
    public TimerInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // ⽬标执⾏之前增强。
        long begin = System.currentTimeMillis();
        // 调⽤⽬标对象的⽬标⽅法
        Object retValue = method.invoke(target, args);
        // ⽬标执⾏之后增强。
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
        // ⼀定要记得返回。
        return retValue;
    }
}

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

public class Client {
    public static void main(String[] args) {
        // 创建⽬标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),

                target.getClass().getInterfaces(),

                new TimerInvocationHandler(target));
        // 调⽤代理对象的代理⽅法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

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

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

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

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

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

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

public class ProxyUtil {
    public static Object newProxyInstance(Object target) {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimerInvocationHandler(target));
    }
}

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

public class Client {
    public static void main(String[] args) {
        // 创建⽬标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService orderServiceProxy = (OrderService) ProxyUtil.newProxyInstance(target);
        // 调⽤代理对象的代理⽅法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

3.2、CGLIB动态代理

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

<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib</artifactId>
 <version>3.3.0</version>
</dependency>

一个没有实现类的接口

public class UserService {
 public void login(){
 System.out.println("⽤户正在登录系统....");
 }
 public void logout(){
 System.out.println("⽤户正在退出系统....");
 }
}

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

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

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

public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        return null;
    }
}

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

第⼀个参数:⽬标对象

第⼆个参数:⽬标⽅法

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

第四个参数:代理⽅法

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

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

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

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

对于⾼版本的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
    在程序的整个执⾏流程中,可以织⼊切⾯的位置。比如⽅法的执⾏前后,异常抛出之后等位置(指的是位置)如下

    public class UserService {
        public void do1() {
            System.out.println("do 1");
        }
    
        public void do2() {
            System.out.println("do 2");
        }
    
        public void do3() {
            System.out.println("do 3");
        }
    
        public void do4() {
            System.out.println("do 4");
        }
    
        public void do5() {
            System.out.println("do 5");
        }
    
        
        // 核⼼业务⽅法
        public void service() {
           try {
               // Joinpoint 连接点
               do1();
               // Joinpoint 连接点
               do2();
               // Joinpoint 连接点
               do3();
               // Joinpoint 连接点
               do5();
           }catch (Exception e){
               // Joinpoint 连接点
           }finally {
               // Joinpoint 连接点
           }
        }
    }
    
  • 切点 Pointcut

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

public void service() {
       try {
           do1();//Pointcut
          
           do2();//Pointcut
          
           do3();//Pointcut
           
           do5();//Pointcut
       }catch (Exception e){
           
       }
    }
  • 通知 Advice

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

  • 前置通知
  • 后置通知
  • 环绕通知
  • 异常通知
  • 最终通知
    // 核⼼业务⽅法
    public void service() {
       try {
           //前置通知(do1方法前执行的代码)
           do1();
           //后置通知(do1方法后执行的代码)
           do2();
           // 环绕通知(在do3前和结束都有执行的代码)
           do3();
           // 环绕通知
           do5();
       }catch (Exception e){
           // 异常通知
       }finally {
           // 最终通知
       }
    }
  • 切⾯ 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

导入依赖

<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-context</artifactId>
 <version>6.0.2</version>
</dependency>
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-aspects</artifactId>
 <version>6.0.2</version>
</dependency>

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

<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-aop</artifactId>
 <version>6.0.2</version>
</dependency>

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--开启组件扫描-->
    <context:component-scan base-package="com.demo.service"/>
<!--开启⾃动代理-->
 <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

<aop:aspectj-autoproxy proxy-target-class="true"/>开启⾃动代理之后,凡事带有@Aspect注解的 bean都会⽣成代理对象。 

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

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

 业务类(目标类)

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

 编写切⾯类

通知类型包括:

  • 前置通知:@Before ⽬标⽅法执⾏之前的通知
  • 后置通知:@AfterReturning ⽬标⽅法执⾏之后的通知
  • 环绕通知:@Around ⽬标⽅法之前添加通知,同时⽬标⽅法执⾏之后添加通知。
  • 异常通知:@AfterThrowing 发⽣异常之后执⾏的通知
  • 最终通知:@After 放在finally语句块中的通知
@Component
@Aspect
public class MyAspect {
    @Around("execution(* com.demo.service.OrderService.* (..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执⾏⽬标⽅法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }
    @Before("execution(* com.demo.service.OrderService.* (..))")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }
    @AfterReturning("execution(* com.demo.service.OrderService.*(..))")
    public void afterReturningAdvice(){
        System.out.println("后置通知");
    }
    @AfterThrowing("execution(* com.demo.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }
    @After("execution(* com.demo.service.OrderService.*(..))"
    )
    public void afterAdvice(){
        System.out.println("最终通知");
    }
}

 测试

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

执行结果 

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

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

测试结果

 

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

切面的先后顺序

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

@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {
    
    @Around("execution(* com.demo.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        System.out.println("YourAspect环绕通知开始");
        // 执⾏⽬标⽅法。
        proceedingJoinPoint.proceed();
        System.out.println("YourAspect环绕通知结束");
    }

    @Before("execution(* com.demo.service.OrderService.*(..))")
    public void beforeAdvice() {
        System.out.println("YourAspect前置通知");
    }

    @AfterReturning("execution(* com.demo.service.OrderService.*(..))")
    public void afterReturningAdvice() {
        System.out.println("YourAspect后置通知");
    }

    @AfterThrowing("execution(* com.demo.service.OrderService.*(..))")
    public void afterThrowingAdvice() {
        System.out.println("YourAspect异常通知");
    }

    @After("execution(* com.demo.service.OrderService.*(..))")
    public void afterAdvice() {
        System.out.println("YourAspect最终通知");
    }
}
@Component
@Aspect
@Order(2) //设置优先级
public class MyAspect {
    @Around("execution(* com.demo.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执⾏⽬标⽅法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }
    @Before("execution(* com.demo.service.OrderService.*(..))")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }
    @AfterReturning("execution(* com.demo.service.OrderService.*(..))")
    public void afterReturningAdvice(){
        System.out.println("后置通知");
    }
    @AfterThrowing("execution(* com.demo.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }
    @After("execution(* com.demo.service.OrderService.*(..))")
    public void afterAdvice(){
        System.out.println("最终通知");
    }
}

 测试程序

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

优化使⽤切点表达式 

观察上面切面类缺点是:

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

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

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

// 切⾯类
@Component
@Aspect
@Order(2)
public class MyAspect {

    @Pointcut("execution(* com.demo.service.OrderService.*(..))")
    public void pointcut() {
    }

    @Around("pointcut()")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执⾏⽬标⽅法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @Before("pointcut()")
    public void beforeAdvice() {
        System.out.println("前置通知");
    }

    @AfterReturning("pointcut()")
    public void afterReturningAdvice() {
        System.out.println("后置通知");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowingAdvice() {
        System.out.println("异常通知");
    }

    @After("pointcut()")
    public void afterAdvice() {
        System.out.println("最终通知");
    }

}

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

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

执⾏测试程序:

全注解开发AOP

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

@Configuration
@ComponentScan("com.demo.service")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Configuration {
}

 测试程序也变化了:

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

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

目标类

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

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

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

 编写spring配置⽂件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/context
 http://www.springframework.org/schema/context/spring-context.xsd
 http://www.springframework.org/schema/aop
 http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--纳⼊spring bean管理-->
    <bean id="vipService" class="com.demo.service.VipService"/>
    <bean id="timerAspect" class="com.demo.service.TimerAspect"/>
    
    <!--aop配置-->
    <aop:config>
        <!--切点表达式-->
        <aop:pointcut id="p" expression="execution(* com.demo.service.VipService.*(..))"/>
        <!--切⾯-->
        <aop:aspect ref="timerAspect">
            <!--切⾯=通知 + 切点-->
            <aop:around method="time" pointcut-ref="p"/>
        </aop:aspect>
    </aop:config>
</beans>

测试

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

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

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

@Component
//⽤户业务
public class UserService {
    public void getUser() {
        System.out.println("获取⽤户信息");
    }

    public void saveUser() {
        System.out.println("保存⽤户");
    }

    public void deleteUser() {
        System.out.println("删除⽤户");
    }

    public void modifyUser() {
        System.out.println("修改⽤户");
    }
}
// 商品业务类
@Component
public class ProductService {
    public void getProduct(){
        System.out.println("获取商品信息");
    }
    public void saveProduct(){
        System.out.println("保存商品");
    }
    public void deleteProduct(){
        System.out.println("删除商品");
    }
    public void modifyProduct(){
        System.out.println("修改商品");
    }
}

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

@Component
@Aspect
public class SecurityAspect {
    @Pointcut("execution(* com.demo.service..save*(..))")
    public void savePointcut(){}
    @Pointcut("execution(* com.demo.service..delete*(..))")
    public void deletePointcut(){}
    @Pointcut("execution(* com.demo.service..modify*(..))")
    public void modifyPointcut(){}
    @Before("savePointcut() || deletePointcut() || modifyPointcut()")
    public void beforeAdivce(JoinPoint joinpoint){
        System.out.println("XXX操作员正在操作"+joinpoint.getSignature().getName()+"⽅法");
    }
}

测试

    @Test
    public void testSecurity() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
        UserService userService = applicationContext.getBean("userService", UserService.class);
        ProductService productService = applicationContext.getBean("productService", ProductService.class);
        userService.getUser();
        userService.saveUser();
        userService.deleteUser();
        userService.modifyUser();
        productService.getProduct();
        productService.saveProduct();
        productService.deleteProduct();
        productService.modifyProduct();
    }

结果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/993309.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenCV 11(图像金字塔)

一、 图像金字塔 **图像金字塔**是图像中多尺度表达的一种&#xff0c;最主要用于图像的分割&#xff0c;是一种以多分辨率来解释图像的有效但概念简单的结构。简单来说, 图像金字塔是同一图像不同分辨率的子图集合. 图像金字塔最初用于机器视觉和图像压缩。其通过梯次向下采…

shell知识点复习

1、shell能做什么&#xff08; Shell可以做任何事(一切取决于业务需求) &#xff09; 自动化批量系统初始化程序 自动化批量软件部署程序 应用管理程序 日志分析处理程序 自动化备份恢复程序 自动化管理程序 自动化信息采集及监控程序 配合Zabbix信息采集 自动化扩容 2、获取当…

淘宝双11数据分析与预测课程案例中(林子雨)错误点总结

问题一&#xff1a;可视化代码中男女买家各个年龄段对比散点图中数值不显示以及坐标不正确问题如下图 解决方法&#xff1a; 1修改坐标 2修改数值 修改后散点图 问题二&#xff1a;各省份的总成交量对比中地图显示不出来 有时间再写

海量小文件传输对于企业选用文件传输软件的重要意义

在当前的商业环境中&#xff0c;数据具有极其重要的作用&#xff0c;是企业竞争的核心要素。随着互联网、物联网和云计算等技术的快速发展&#xff0c;数据的类型和规模变得越来越多样。在这其中&#xff0c;海量小文件作为一种普遍而重要的数据形式&#xff0c;扮演着连接信息…

新知同享 | Mobile 开发轻松跨屏,高效构建

谷歌致力于帮助开发者 更快、更轻松地打造高质量的移动体验 一起来看 2023 Google 开发者大会上 Mobile 开发值得重点关注的成果与更新 了解如何提高平台及应用质量 提升开发效率 使多设备开发体验更流畅 实现轻松跨屏&#xff0c;高效构建 精彩大会现场一览 用户对跨屏幕体验…

在k8s中创建ConfigMap的四种方式与初识helm包管理工具

非敏感数据&#xff0c;比如应用的配置信息&#xff0c;则可以用ConfigMap 创建configmap四种方式 &#xff08;1&#xff09;通过--from-literal&#xff1a; kubectl create configmap myconfigmap --from-literalconfig1xxx --from-literalconfig2yyy 每个--from-literal…

Revit SDK 介绍:Ribbon 界面

前言 Revit 通过 API 将完整的 Ribbon 做了保留&#xff0c;同时这些菜单按钮也可以和相应的命令绑定。 内容 运行效果如下所示&#xff1a; 菜单特写&#xff1a; Ribbon Sample 整体是 API 暴露出来的一个 RibbonPanel&#xff0c;对应的接口&#xff1a; namespace Au…

dll文件反编译源代码 C#反编译 dotpeek反编译dll文件后export

目录 背景下载安装dotpeek导入dll文件export导出文件参考 背景 项目合作的时候&#xff0c;使用前人的或者其他部门dll文件直接在机台运行&#xff0c;会出现很多问题&#xff0c;逻辑&#xff0c;效率等等&#xff0c;此时我们可以选择对他们的代码进行反编译和重构&#xff…

递归算法学习——被围绕的区域,太平洋大西洋流水问题

目录 ​编辑 一&#xff0c;被围绕的区域 1.题意 2.解释 3.题目接口 4.解题思路及代码 二&#xff0c;太平洋大西洋流水问题 1.题意 2.解释 3.题目接口 4.解题思路及代码 一&#xff0c;被围绕的区域 1.题意 给你一个 m x n 的矩阵 board &#xff0c;由若干字符 X 和…

对卷积的一点具象化理解

前言 卷积的公式一般被表示为下式&#xff1a; 对新手来说完全看不懂这是干什么&#xff0c;这个问题需要结合卷积的应用场景来说。 原理 卷积比较广泛的应用是在信号与系统中&#xff0c;所以有些公式的定义会按照信息流的习惯。假设存在一串信号g(x)经过一个响应h(x)时他的响…

高云USB下载器仿真器用户手册(包括在线逻辑分析仪的使用方法)

高云 USB 仿真器用户手册 一.简介 仿真器用于高云 GOWIN 公司所生产的 FPGA&#xff0c;可用于程序下载和调试。主要特点如下&#xff1a; 1.支持宽电压1.2V - 3.6V&#xff1b; 2.速度最高可达30Mb/s&#xff0c;极速完成下载和波形调试功能&#xff1b; 3.完美支持在线逻…

Java实现Modbus读写数据

背景 由于当时项目周期赶&#xff0c;引入了一个PLC4X组件&#xff0c;上手快。接下来就是使用这个组件遇到的一些问题&#xff1a; 关闭连接NioEventLoop没有释放导致oom设计思想是一个设备一个连接&#xff0c;而不是一个网关一个连接连接断开后客户端无从感知 前两个问题解…

什么牌子的电容笔比较好?开学值得买触控笔推荐

大部分学生都没有固定的收入&#xff0c;所以他们选择的商品都是偏向性价比高的。随着iPad的不断升级&#xff0c;它的各种功能也会越来越多&#xff0c;将会慢慢地走进我们的生活和工作中。随着电子设备的不断更新和软件的完善&#xff0c;电容笔的性能也在不断提高&#xff0…

783. 二叉搜索树节点最小距离

783. 二叉搜索树节点最小距离 C代码&#xff1a;二叉树 int min; int pre;int dfs(struct TreeNode* root) {if (root NULL) {return;}dfs(root->left);if (pre ! -1) {min fmin(min, root->val - pre);}pre root->val; // 中序遍历dfs(root->right); }int mi…

【深度学习】 Python 和 NumPy 系列教程(三):Python容器:1、列表List详解(初始化、索引、切片、更新、删除、常用函数、拆包、遍历)

目录 一、前言 二、实验环境 三、Python容器&#xff08;Containers&#xff09; 0、容器介绍 1、列表&#xff08;List&#xff09; 1. 初始化 a. 创建空列表 b. 使用现有元素初始化列表 c. 使用列表生成式 d. 复制列表 2. 索引和切片 a. 索引 b. 负数索引 c. 切…

MySQL触发器详解保证入土

文章目录 简介一、MySQL触发器基础触发器分类基础常用关键字1. 定义触发器2. 创建和删除触发器3. 执行时机和条件 二、MySQL触发器的使用场景1. 数据完整性约束插入触发器更新触发器删除触发器 2. 数据变更日志的记录与追踪3. 触发器与存储过程的对比与选择 三、触发器的性能和…

强大的JTAG边界扫描(5):FPGA边界扫描应用

文章目录 1. 获取芯片的BSDL文件2. 硬件连接3. 边界扫描测试4. 总结 上一篇文章&#xff0c;介绍了基于STM32F103的JTAG边界扫描应用&#xff0c;演示了TopJTAG Probe软件的应用&#xff0c;以及边界扫描的基本功能。本文介绍基于Xilinx FPGA的边界扫描应用&#xff0c;两者几乎…

巨人互动|Facebook海外户Facebook风控规则有什么

Facebook是全球最大的社交媒体平台之一&#xff0c;每天有数十亿的用户在其上发布、分享和交流各种内容。为了维护平台的安全性和用户体验&#xff0c;Facebook制定了严格的风控规则来监测和处理违规行为。下面小编讲讲Facebook风控规则。 巨人互动|Google海外户&Google Ad…

CocosCreator3.8研究笔记(十一)CocosCreator Prefab(预制件)理解

相信很多朋友都不知道 Prefab 是什么&#xff1f;为什么要使用Prefab &#xff1f; 怎么使用Prefab&#xff1f; 接下来&#xff0c;我们就一步一步来揭晓答案。 一、Prefab 是什么 &#xff1f; Prefab&#xff1a;大家习惯性地称为“预制件” 或“预制体” &#xff0c;简单说…

Java事件机制简介 内含面试题

面试题分享 云数据解决事务回滚问题 点我直达 2023最新面试合集链接 2023大厂面试题PDF 面试题PDF版本 java、python面试题 项目实战:AI文本 OCR识别最佳实践 AI Gamma一键生成PPT工具直达链接 玩转cloud Studio 在线编码神器 玩转 GPU AI绘画、AI讲话、翻译,GPU点亮…