Spring-AOP

发布于:2024-04-09 ⋅ 阅读:(89) ⋅ 点赞:(0)
重点看切入点、通知、基于注解的AOP。
AOP:多个方法在相同位置有相同的操作,切一刀,将切面抽象为一个对象,把相同的操作代码写进对象,对对象进行编程,底层使用动态代理机制。


0. 场景模拟:计算器加减乘除

接口Calculate:

package com.circle.aop;

public interface Calculate {
    public int add(int num1, int num2);
    public int sub(int num1, int num2);
    public int mul(int num1, int num2);
    public int div(int num1, int num2);

}

实现接口的类CalculateImpl:

package com.circle.aop;

public class CalculateImpl implements Calculate{
    @Override
    public int add(int num1, int num2) {
        System.out.println("add方法的参数为:" + num1 + "、" + num2);
        int result = num1 + num2;
        System.out.println("add方法的结果为:" + result);
        return result;
    }

    @Override
    public int sub(int num1, int num2) {
        System.out.println("sub方法的参数为:" + num1 + "、" + num2);
        int result = num1 - num2;
        System.out.println("sub方法的结果为:" + result);
        return result;
    }

    @Override
    public int mul(int num1, int num2) {
        System.out.println("mul方法的参数为:" + num1 + "、" + num2);
        int result = num1 * num2;
        System.out.println("mul方法的结果为:" + result);
        return result;
    }

    @Override
    public int div(int num1, int num2) {
        System.out.println("div方法的参数为:" + num1 + "、" + num2);
        int result = num1 / num2;
        System.out.println("div方法的结果为:" + result);
        return result;
    }
}
        可以看出,方法前后的两行输出与核心代码无关,也不好修改,代码维护性、复用性差。
        通过AOP,把这两行日志代码抽离出核心业务代码,统一处理,使核心业务代码与非业务代码解耦合。

AOP的优点:

  • 可以降低模块之间的耦合性
  • 提供代码的复用性
  • 提高代码的维护性
  • 集中管理非业务代码,便于维护
  • 业务代码不受非业务代码影响,逻辑更加清晰

一、代理模型

        二十三种设计模式中的一种,属于结构型模式。
        它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来,实现解耦。
        调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

可以想象成是明星和经纪人的关系。

代理可以在不改变源代码的基础上实现对代码的拓展
常用到的业务场景:
  1. 事务的处理
  2. 日志的打印
  3. 性能监控
  4. 异常的处理等
  • 代理:非核心内容,将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
  • 目标:核心内容,被代理“套用”了非核心逻辑代码的类、对象、方法。

1. 静态代理

        静态代理在编译时就已经确定,代理类是由程序员手动编写的。在静态代理中,代理类和目标类实现相同的接口,代理类持有目标类的引用,并委托目标类来执行实际操作。
利用接口类型对传入的对象做接收 (多态),通过带参构造传入一个接口实现类的对象。
静态代理类:

核心目标:

        静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,每个功能每个接口都需要代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
        提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现,这就需要使用动态代理技术了。 而动态代理所有接口都可以用一个代理类。

2. 动态代理

        动态代理是在运行时由JDK或第三方库(如CGLIB)生成的代理。Spring AOP通常使用JDK动态代理,也可以使用CGLIB库。

使用场景

  • 如果目标对象实现了接口,优先使用JDK动态代理,也可以CGLIB。
  • 如果目标对象没有实现接口,或者需要使用继承的方式增强方法,只能使用CGLIB。

JDK动态代理
JDK动态代理基于反射机制,可以在运行时动态创建代理对象。
它要求目标对象和代理对象实现同样的接口(兄弟拜把子)。代理类会持有一个目标对象的引用,并委托给目标对象来执行方法。

优点:

  • 代理类的生成是自动的,不需要手动编写。
  • 可以代理实现了接口的任意方法,灵活性高。

缺点:

  • 只能对实现了接口的类进行代理。
CGLIB库
CGLIB(Code Generation Library)是一个强大的代码生成库,它可以用于创建子类而不是代理。Spring AOP使用CGLIB来为没有实现接口的类创建代理。CGLIB通过继承目标类实现代理(认干爹),并覆盖父类的方法来实现增强。

优点:

  • 可以代理没有实现接口的类。
  • 生成子类的方式比反射更高效。

缺点:

  • 生成的子类会覆盖原有类的实现,可能导致一些复杂的类继承问题。

        newProxyInstance获取代理对象,需要三个参数(ClassLoader loader : 类加载器、Class<?>[] interfaces : 接口信息、InvocationHandler h : 调用处理器,其本身也是一个接口,内部声明了抽象方法invoke)
        invoke方法写具体的日志部分,需要三个参数(Object proxy代理对象、Method method方法、Object[] args方法中的参数)

JDK动态代理实现:可不看,因为Spring AOP动态代理更好

2.1 写一个生成代理对象的工厂类,这个类可以提供一个Calculate接口的代理对象,类中有一个方法返回代理对象getProxy(),将目标对象作为成员变量(为了通用使用Object类,而不是Calculate类)
target 需要被代理的目标对象
getProxy()方法:通过反射创建目标对象的代理对象

2.2 完善getProxy方法:返回代理对象
//编写一个方法,用于返回代理对象 (用到反射机制)
    public Object getProxy() {
        /**
            (1) ClassLoader loader : 加载动态代理生成类的类加载器
            (2) Class<?>[] interfaces : 目标对象实现的所有接口的class类型数组
            (3) InvocationHandler h : 调用处理器,其本身也是一个接口,内部声明了抽象方法invoke,设置代理对象实现目标对象方法的过程。
         */
 
        //(1)得到类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
 
        //(2)得到被执行对象的接口信息(因为newProxyInstance方法底层是通过接口来调用的,即接口多态)
        Class<?>[] interfaces = target.getClass().getInterfaces();
 
        //(3)得到处理器对象(通过匿名内部类实现,最终返回的是一个匿名内部类对象)
        //需注意,处理器对象本身也是newProxyInstance方法的一个形参
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /*
                    proxy:代理对象、method:需要重写目标对象的方法、args:method方法的参数
                    将相同的业务逻辑代码,放在处理器对象的invoke对象中,
                    避免了代码下沉到每个实现类所造成的代码冗余。
                 */
                Object result = null;
                try{
                    System.out.println("[动态代理][日志]“+method.getName()+",参数:"+Arrays.tostring(args))");
                    result = method.invoke(target, args);  //通过反射调用实现类中的方法
                    System.out.println("[动态代理][日志]"+method.getName()+",结果:"+
result");
                } catch(Exception e) {
                    e.printStackTrace();
                    System.out.printin("[动态代理][日志]"+method.getName()+",异常:"+e.getMessage());
                } finally {
                    System.out.print1n("[动态代理][日志]"+method.getName()+“,方法执行完毕”);
                }
                return result;
            }
        };
 
        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
2.3 测试:向工厂类中传入目标对象得到一个工厂对象,调用工厂对象的getProxy方法并向下转型得到代理对象

二、AOP

1. 概念及相关术语

        AOP(Aspect Oriented Programming)是一种设计思想,面向切面编程,它是面向对象编程的一种补充和完善。它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

将那些与业务代码无关,但对多个对象产生影响的公共行为,封装成一个可重用的模块(切面)。

以下相关术语关注通知和切入点:

        切入点是用来确定“哪里”和“何时”织入横切关注点,而通知则是“如何”织入横切关注点的具体实现。切入点是通知的上下文,它决定了哪些通知应该在哪些连接点上执行。

1.1 横切关注点:相同的非核心业务

        分散在各个模块中解决同样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。
        从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
        这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

1.2 通知(增强):实现横切关注点的代码块

增强,通俗说,就是你想要增强的功能,比如 安全,事物,日志等。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
  • 前置通知:在被代理的目标方法前执行
  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
  • 异常通知:在被代理的目标方法异常结束后执行(死于非命)
  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
  • 环绕通知:使用try..catch..finaly结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置 

1.3 切面:封装通知方法的类(通知+切入点)

 

1.4 目标 :被代理、通知的对象

1.5 代理:向目标对象应用通知后创建的代理对象 

1.6 连接点:使用通知的地方,程序执行的某个特定位置

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。通俗说,就是spring允许你使用通知的地方。

1.7 切入点:筛选连接点,实际使用通知的地方

切入点是一个连接点的过滤条件,AOP 通过切点定位到特定的连接点。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL语句,即查询条件。
切点和连接点不是一对一的关系,一个切点匹配多个连接点
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

为什么要筛选连接点得到切入点?

        不是所有的连接点都是业务逻辑中重要的部分,或者都需要横切关注点的干预。通过筛选,我们可以只选择那些对横切关注点有意义的连接点,从而减少不必要的代码干预,保持业务逻辑的清晰和简洁。

        横切关注点的织入可能会对应用程序的性能产生影响。通过精确筛选切入点,我们可以避免在不必要的连接点上进行横切,从而减少性能开销。

2. 基于注解的AOP

AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入"被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了Aspectj中的注解。

 2.1 在pom.xml中引入aop、aspects依赖

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

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

2.2 准备被代理的目标资源(接口和目标实现类)

2.3 创建切面类:切入点+通知s

 2.4 配置Spring配置文件beans.xml

 2.5 测试

3. 基于xml的AOP

现多基于注解,xml可不看

3.1 3.2 与上同

3.3 切面类:去掉注解部分即可,将基于xml实现

3.4 配置文件bean.xml:实现五种通知类型

<aop:aspect ref="切面类名(首字母小写)">
<aop:pointcut id = "切入点id(任意,但后续pointcut-ref要与此保持一致)" expression="切入点表达式">
五种通知类型:<aop:before>。。。

参考:b站尚硅谷

http://t.csdnimg.cn/T80ww

http://t.csdnimg.cn/xwa6X


网站公告

今日签到

点亮在社区的每一天
去签到