一文带你认识Spring AOP

Posted 风在哪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文带你认识Spring AOP相关的知识,希望对你有一定的参考价值。

Spring AOP

简介

AOP(Aspect-Oriented Programming:面向切面编程)是对OOP(Object-Oriented Programming:面向对象编程)的补充和完善。

OOP引入封装、继承和多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。封装就要求将功能分散到不同的对象中去,这在软件设置中往往称之为职责分配,实际上也就是说,让不同的类设计不同的方法,这样代码就分散到一个个类中去了。这样的设计降低了代码的复杂度,使类可重用。

但是在分散代码的同时,也增加了代码的重复性,当我们需要为分散的对象引入公共的行为时,例如增加日志功能,假如要在整个系统所有的接口中增加日志功能,我们是不是需要在所有的接口中增加日志的逻辑,这些代码可能是完全重复的,但是面向对象的设计使得这些代码无法很好的统一起来。也就是说使用OOP我们可以轻松的解决从上到下的关系,但是并不适合定义从左到右的关系。

例如我们在开发系统时,可以很轻松的把业务相关的接口开发出来,例如下面列举的服务:

image-20210509203444515

但是,如果我们要引入与业务核心功能无关的代码,例如日志服务,那么我们可能要在原有接口的基础之上,添加新的日志相关的代码。我们也许可以这样实现:将日志功能的代码抽象出来,编写在一个独立的类里面,这样在所有要加日志的接口里面引用该代码即可。但是这样的话就增加了两个类之间的耦合性,那么有什么办法可以降低耦合还能实现功能呢?

此时,使用AOP就可以解决我们的问题,AOP就是在运行时,动态地将代码切入到类的指定方法、指定位置上的一种编程方法,亦或是编程思想。

这样来看,AOP就是OOP的补充,OOP负责的是在横向区分出一个个不同的功能和方法,就像上图中不同的服务一样,而AOP则是在纵向上向对象加入特定的代码,有了AOP,OOP变得立体了。

image-20210509204523641

OOP的空间结合AOP的空间就可以构建一个完美的系统。

AOP是一种编程的思想,要实现AOP有多种方式,AOP也需要某种语言实现相应的概念实体,这些概念实体队中都需要某种方式集成到系统实现语言所实现的OOP实体组件中。将AOP实体组件集成到OOP组件的过程就称为织入(weave)过程。

Spring AOP主要是通过动态代理的方式实现的,其织入过程是在系统运行开始之后进行的,而不是预先编译到系统类中。其底层就是采用了Java的动态代理技术以及动态字节码增强技术,其中动态代理就使用了jdk的动态代理,要求被代理的类必须实现某个接口;而动态字节码增强技术就使用了CGLIB库的相关功能来实现。

Spring AOP相关概念和术语

Joinpoint

在系统运行之前,AOP的功能模块都需要织入到OOP的功能模块中,所以要进行这种织入过程,我们需要知道在系统的哪些执行点上进行织入操作,这些将要在其之上进行织入操作的系统执行点就称为Joinpoint。

Jointpoint包含如下常见的类型:

  • 方法调用(Method):方法被调用时候所处的程序执行点,进入方法前,调用对象上的执行点
  • 方法执行(Method Call execution):相当于方法内部执行的开始时间点,进入方法但方法运行前,方法逻辑执行的时间点,方法调用先于方法执行
  • 构造方法调用(constructor call):程序执行过程中对某个对象调用其构造方法进行初始化的时点
  • 字段设置(field set):对象的某个属性通过setter方法被设置的时点
  • 字段获取(field get):对象属性被访问的时点,getter方法
  • 异常处理执行(exception handler execution):当方法执行时,抛出某些类型的异常后,对应的异常处理逻辑执行的时点
  • 类初始化(Class initialization):类的某些静态类型或者静态块的初始化时点

基本上程序执行过程中你认为必要的点都可以是Joinpoint。

Pointcut

pointcut概念代表的是Joinpoint的表述方式,将横切逻辑织入当前系统的过程中,需要参照规定的joinpoint信息,才可以知道应该往系统的哪些joinpoint上织入横切逻辑。

pointcut包括表述方式和pointcut运算,pointcut的表述方式就是指定joinpoint所在的方法名称,或者特定的pointcut表述语言;而运算方式就是多个pointcut之间进行的逻辑运算。

Advice

Advice是单一横切关注点逻辑的载体,他代表将会织入到joinpoint的横切逻辑,也就是我们对于joinpoint做的操作会在advice中定义。

Advice具有多种形式:

  • before advice:在joinpoint指定位置之前执行的advice类型,它通常不会中断程序的中断流程,如果有必要,可以通过在before advice中抛出异常的方式来中断当前程序流程。可以使用before advice做一些系统的初始化工作,比如设置系统初值等。
  • after advice:就是在joinpoint之后执行的advice类型,具体可以细分为以下三类:
    • after returning advice:连接点出执行流程正常完成后,才会执行该类型的advice,就像try里面的逻辑
    • after throwing advice:连接点执行过程中抛出异常下才会执行该类型的advice,就像catch里面的逻辑
    • after advice:不管连接点执行流程正常完成还是抛出异常都回执行的advice,就像finall块里面的逻辑
  • around advice:对joinpoint进行包裹,可以在joinpoint之前和之后都指定相应的逻辑,甚至于中断或者忽略joinpoint原来的程序流程的执行
  • introduction:可以为原有对象添加新的特性或者行为,AspectJ采用静态织入的方式实现,在对象使用的时候,introduction逻辑已经编译织入完成

Aspect

Aspect是对系统中的横切关注点逻辑进行模块化封装的AOP概念实体,相当于类对象,上述的pointcut和advice都封装到aspect里面,spring在2.0之后,集成了aspectj,可以通过使用@AspectJ注解并结合普通的pojo来声明aspect

织入和织入器

织入的过程就是飞架在aop和oop之间的那座桥,只有经过织入过程之后,aspect模块化的横切关注点才会集成到oop的现存系统中。

spring aop使用一组专门的类完成最终的织入操作,ProxyFactory类则是spring aop最通用的织入器。

而aspectj有专门的编译器来完成织入操作,即ajc,所以ajc就是aspectj完成织入的织入器。

目标对象

符合pointcut所指定的条件,将在织入过程中被织入横切逻辑的对象就称为目标对象。

小结

spring是通过纯java实现的,不需要特殊的编译过程。spring aop不需要控制类加载的层次,因此它非常合适在servlet容器和应用服务器上使用。

Spring AOP目前只支持方法执行连接点(建议Spring bean上方法的执行)。它不支持属性字段的拦截,如果想建议属性字段的访问并且更新连接点,可以考虑使用AspectJ。

spring aop的目的不是提供最全面的aop实现,其目的是提供AOP实现和Spring IoC之间的紧密集成,以帮助解决企业应用程序中的常见问题。因此,Spring框架的AOP功能通常与Spring IoC容器一起使用。

Aspect通过使用普通的bean定义语法配置,使用Spring AOP不能轻松或有效地完成一些事情,比如通知非常细粒度的对象(通常是域对象)。这类问题最好选择Aspect。我们的经验是,Spring AOP为企业级Java应用程序中的大多数问题提供了一种优秀的解决方案,而这些问题都是符合AOP的。

Spring AOP从未努力与AspectJ竞争以提供全面的AOP解决方案。我们相信基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是竞争的。Spring与AspectJ无缝地集成了Spring AOP和IoC,以便在一致的基于Spring的应用程序体系结构中启用AOP的所有使用。

Spring框架的中心宗旨之一是非侵入性。 这是一个想法,不应强迫您将特定于框架的类和接口引入业务或域模型。 但是,在某些地方,Spring Framework确实为您提供了将特定于Spring Framework的依赖项引入代码库的选项。 提供此类选项的基本原理是,在某些情况下,以这种方式阅读或编码某些特定功能可能会更加容易。 但是,Spring框架(几乎)总是为您提供选择:您可以自由地就哪个选项最适合您的特定用例或场景做出明智的决定

Spring AOP实现机制

spring aop采用动态代理机制和字节码生成技术实现。

动态代理

当我们使用动态代理的时候,可以为指定的接口在系统运行期间动态地生成代理对象,从而帮助我们走出最初使用静态代理实现AOP的窘境。

动态代理机制的实现主要由一个类和一个接口组成,即java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler接口,接下来我们看看如何通过jdk的动态代理帮我们实现面向切面编程。

首先准备要被代理的接口对象及其实现:

public interface UserDao {
    int add(int a, int b);

    String update(String username);
}

public class UserDaoImpl implements UserDao {
    @Override
    public int add(int a, int b) {
        System.out.println("add方法被执行了========");
        return a+b;
    }

    @Override
    public String update(String username) {
        System.out.println("update方法被执行了========");
        return username;
    }
}

准备好接口及其实现后,我们要实现一个InvocationHandler来对我们代理对象进行增强操作:

public class MyInvoker implements InvocationHandler {

    private Object obj;

    public MyInvoker(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        System.out.println("增强的方法=========="+method.getName()+":"+objects.toString());

        Object invoke = method.invoke(obj, objects);

        System.out.println("增强的方法==========");
        System.out.println("invoke:" + invoke.toString());
        return invoke;
    }
}

当我们准备好InvocationHandler的实现类后,我们可以通过Proxy来代理我们的接口了:

public class MyProxy {
    public static void main(String[] args) {

        Class[] interfaces = { UserDao.class };
        UserDaoImpl userDao = new UserDaoImpl();
        UserDao user = (UserDao) Proxy.newProxyInstance(MyProxy.class.getClassLoader(), interfaces, new MyInvoker(userDao));
        String wyg = user.update("wyg");

        System.out.println(wyg);
    }
}

接下来我们运行这个方法,可以看到如下运行效果:

image-20210510104640569

这就完成了简单的动态代理,即使有更多的目标对象类型,只要它们织入的横切逻辑依然相同,用MyInvoker一个类并通过Proxy为它们生成相应的动态代理实例就可以满足要求。当Proxy动态生成的代理对象上相应的接口方法被调用时,对应的InvocationHandler就会拦截相应的方法调用,并进行处理。

InvocationHandler就是我们实现横切逻辑的地方,它是横切逻辑的载体,作用和Advice是一样的。所以在使用动态代理机制实现AOP的过程中,我们可以在InvocationHandler的基础上系化程序结构,并根据Advice的类型,分化出对应不同Advice类型的程序结构。

动态代理虽然好,但是不能满足所有的需求,因为动态代理机制只能对实现了相应Interface的类使用,如果某个类没有实现任何Interface,就无法使用动态代理机制为其生成相应的动态代理对象。

在默认情况下,如果spring aop发现目标对象实现了相应的interface,就采用动态代理为其生成代理对象。如果目标对象没有实现任何interface,spring aop会尝试使用cglib的开源动态字节码生成类库,为目标对象生成动态的代理对象实例。

CGLIB动态字节码增强

使用动态字节码生成技术扩展对象行为的原理是我们可以对目标对象进行继承扩展,为其生成相应的子类,而子类可以通过覆写来扩展父类的行为,只要将横切逻辑的实现放到子类中,然后系统使用扩展后的目标对象的子类,就可以达到与代理模式相同的效果了。使用继承的方式来扩展对象定义,不能像静态代理那样,为每个不同类型的目标对象都单独创建相应的扩展子类。所以要借助于cglib这样的动态字节码生成库,在系统运行期间动态地为目标对象生成相应的扩展子类。

接下来演示cglib的简单用法。

首先准备一个要被代理的类,这个类不能是final修饰的类,该类可以实现接口,也可以不实现接口,这里就测试一个没有实现接口的类。

public class CglibTest {

    public void select() {
        System.out.println("select方法被执行了");
    }

    public void update() {
        System.out.println("update方法被执行了");
    }

}

接下来我们要实现一个Callback,但是大多时候我们都实现MethodInterceptor接口,它扩展了Callback。这里提供了两个方法执行的横切逻辑:

public class CglibCallback implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("增强的方法=======" + method.getName());
        System.out.println("增加相应的前置逻辑");
        methodProxy.invokeSuper(o, objects);
        System.out.println("增加相应的后置逻辑");
        return o;
    }
}

public class MyCallback implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("=====什么都不做=====");
        methodProxy.invokeSuper(o, objects);
        return null;
    }
}

此外,我们还可以通过CallbackFilter控制对哪些方法增加自己的逻辑:

public class MyCallbackFilter implements CallbackFilter {
    /**
     * 该方法返回的下标就是我们设置的callback数组对应的下标
     * @param method
     * @return
     */
    @Override
    public int accept(Method method) {
        if (method.getName().equals("select") || method.getName().equals("update")) {
            return 0;
        } else {
            return 1;
        }
    }
}

接下来就是cglib的主流程了:

public class MyEnhancer {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        // 指定我们要代理的类
        enhancer.setSuperclass(CglibTest.class);
        // 设置拦截方法的规则
        enhancer.setCallbackFilter(new MyCallbackFilter());

        // 指定增强的回调方法
        Callback[] callbacks = new Callback[2];
        callbacks[0] = new CglibCallback();
        callbacks[1] = new MyCallback();
        enhancer.setCallbacks(callbacks);

        // 获取代理对象
        CglibTest proxy = (CglibTest) enhancer.create();
        proxy.select();
        proxy.update();
    }
}

我们使用cglib进行动态字节码增强的大概流程如下:

  • 首先创建一个Enhancer对象
  • 设置我们要代理的类,该类不能是final类型的
  • 设置代理方法的拦截规则,根据方法名称或者请求参数的类型来确定对哪些方法添加横切逻辑
  • 将我们的横切逻辑也加到Enhancer中
  • 获取代理对象并运行

运行结果如下所示:

image-20210510131708443

如果我们对其调试的话,我们可以发现我们获得的代理对象的名称为:

image-20210510131852032

如果我们打印代理类的父类的话,可以发现它的父类就是被代理的那个类:

image-20210510132525316

小结

以上就是spring aop实现机制方面的内容,默认情况下,如果我们实现了接口的话,spring会通过jdk动态代理进行aop的实现,如果被代理的类没有实现任何接口,那么spring将使用cglib动态字节码增强技术实现aop

总结

本篇文章主要讲解了Spring AOP的基础概念和底层的一些实现方法,例如动态代理和动态字节码增强技术,了解这些才能更好的掌握Spring AOP,知道什么时候需要使用AOP,在开发中更加得心应手。
接下来还会讲解Spring内部的设计,深入了解Spring是如何一步步来完成AOP代理的。

本人的理解可能有所偏差,有什么问题欢迎大家评论区一起讨论!

参考

知乎-什么是面向切面编程AOP?

Spring揭秘 (豆瓣) (douban.com)

Spring官网:Core Technologies (spring.io)

以上是关于一文带你认识Spring AOP的主要内容,如果未能解决你的问题,请参考以下文章

一文带你认识Spring AOP

从源码入手,一文带你读懂Spring AOP面向切面编程

从源码入手,一文带你读懂Spring AOP面向切面编程

面试必备:从源码入手,带你一文读懂Spring AOP面向切面编程

Spring一文带你吃透AOP面向切面编程技术(上篇)

Spring一文带你吃透AOP面向切面编程技术(下篇)