[Spring]Spring5 AOP

3.1 AOP 概念

AOP 的全称是“Aspect Oriented Programming”,译为“面向切面编程”,和 OOP(面向对象编程)类似,它也是一种编程思想

面向切面编程,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

通俗描述:不通过修改源代码方式,在主干功能里面添加新功能

3.1.1 面向切面编程

与 OOP 中纵向的父子继承关系不同,AOP 是通过横向的抽取机制实现的。它将应用中的一些非业务的通用功能抽取出来单独维护,并通过声明的方式(例如配置文件、注解等)定义这些功能要以何种方式作用在那个应用中,而不是在业务模块的代码中直接调用。

这虽然设计公共函数有几分类似,但传统的公共函数除了在代码直接硬调用之外并没有其他手段。AOP 则为这一问题提供了一套灵活多样的实现方法(例如 Proxy 代理、拦截器、字节码翻译技术等),可以在无须修改任何业务代码的基础上完成对这些通用功能的调用和修改。

AOP 编程和 OOP 编程的目标是一致的,都是为了减少程序中的重复性代码,让开发人员有更多的精力专注于业务逻辑的开发,只不过两者的实现方式大不相同。

OOP 就像是一根“绣花针”,是一种婉约派的选择,它使用继承和组合方式,仔细地为所有涉及通用功能的模块编制成一套类和对象的体系,以达到减少重复性代码的目标。而 AOP 则更像是一把“砍柴刀”,是一种豪放派的选择,大刀阔斧的规定,凡是某包某类下的某方法都一并进行处理。

AOP 不是用来替换 OOP 的,而是 OOP 的一种延伸,用来解决 OOP 编程中遇到的问题。

3.1.2 AOP 框架

目前最流行的 AOP 实现(框架)主要有两个,分别为 Spring AOP 和 AspectJ

AOP 框架说明
Spring AOP是一款基于 AOP 编程的框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。 Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。Spring AOP 支持 2 种代理方式,分别是基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。
AspectJ是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。 AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。

3.1.3 AOP 术语

名称说明
Joinpoint(连接点)AOP 的核心概念,指的是程序执行期间明确定义的一个点,例如方法的调用、类初始化、对象实例化等。 在 Spring 中,连接点则指可以被动态代理拦截目标类的方法。
Pointcut(切入点)又称切点,指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。
Advice(通知)指拦截到 Joinpoint 之后要执行的代码,即对切入点增强的内容。
Target(目标)指代理的目标对象,通常也被称为被通知(advised)对象。
Weaving(织入)指把增强代码应用到目标对象上,生成代理对象的过程。
Proxy(代理)指生成的代理对象。
Aspect(切面)切面是切入点(Pointcut)和通知(Advice)的结合。

Advice 直译为通知,也有人将其翻译为“增强处理”,共有 5 种类型,如下表所示。

通知说明
before(前置通知)通知方法在目标方法调用之前执行
after(最终通知)通知方法在目标方法返回或异常后调用
after-returning(后置通知)通知方法会在目标方法返回后调用
after-throwing(异常通知)通知方法会在目标方法抛出异常后调用
around(环绕通知)通知方法会将目标方法封装起来

3.1.4 AOP 类型

AOP 可以被分为以下 2 个不同的类型。

3.1.4.1 动态 AOP

动态 AOP 的织入过程是在运行时动态执行的。其中最具代表性的动态 AOP 实现就是 Spring AOP,它会为所有被通知的对象创建代理对象,并通过代理对象对被原对象进行增强。

相较于静态 AOP 而言,动态 AOP 的性能通常较差,但随着技术的不断发展,它的性能也在不断的稳步提升。

动态 AOP 的优点是它可以轻松地对应用程序的所有切面进行修改,而无须对主程序代码进行重新编译。

3.1.4.2 静态 AOP

静态 AOP 是通过修改应用程序的实际 Java 字节码,根据需要修改和扩展程序代码来实现织入过程的。最具代表性的静态 AOP 实现是 AspectJ。

相较于动态 AOP 来说,性能较好。但它也有一个明显的缺点,那就是对切面的任何修改都需要重新编译整个应用程序。

3.1.5 AOP 优势

AOP 是 Spring 的核心之一,在 Spring 中经常会使用 AOP 来简化编程。

在 Spring 框架中使用 AOP 主要有以下优势。

  • 提供声明式企业服务,特别是作为 EJB 声明式服务的替代品,最重要的是,这种服务是声明式事务管理。
  • 允许用户实现自定义切面。在某些不适合用 OOP 编程的场景中,采用 AOP 来补充。
  • 可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时也提高了开发效率。

3.2 AOP 编程

Spring AOP 是 Spring 框架的核心模块之一,它使用纯 Java 实现,因此不需要专门的编译过程和类加载器,可以在程序运行期通过代理方式向目标类织入增强代码。

3.2.1 Spring AOP 代理机制

Spring 在运行期会为目标对象生成一个动态代理对象,并在代理对象中实现对目标对象的增强。

Spring AOP 的底层是通过以下 2 种动态代理机制,为目标对象(Target Bean)执行横向织入的。

代理技术描述
JDK 动态代理Spring AOP 默认的动态代理方式,若目标对象实现了若干接口,Spring 使用 JDK 的 java.lang.reflect.Proxy 类进行代理。
CGLIB 动态代理若目标对象没有实现任何接口,Spring 则使用 CGLIB 库生成目标对象的子类,以实现对目标对象的代理。

注意:由于被标记为 final 的方法是无法进行覆盖的,因此这类方法不管是通过 JDK 动态代理机制还是 CGLIB 动态代理机制都是无法完成代理的

3.2.1.1 JDK 动态代理
package top.shiyiri.spring5;

public interface UserDao {
    int add(int a, int b);
    String update(String id);
}
package top.shiyiri.spring5;

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 id) {
        return id;
    }
}
package top.shiyiri.spring5;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * @author Aunean
 * @date 2022/2/17 18:12
 */
public class JDKProxy {

    public static void main(String[] args) {

        UserDao userDao = new UserDaoImpl();
        Class[] interfaces = {UserDao.class};
        //创建接口实现类代理对象
        UserDao dao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(),
                interfaces, new UserDaoProxy(userDao));

        int result = dao.add(1, 34);
        System.out.println(result);
    }
}


//创建代理对象代码
class UserDaoProxy implements InvocationHandler {

    //把创建是谁的代理对象,把谁传递过来
    private Object object;
    public UserDaoProxy(Object object) {
        this.object = object;
    }

    //增加的逻辑
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        //方法之前
        System.out.println("方法之前执行..." + method.getName() + ":传递的参数..." + Arrays.toString(args));
        //被增强的方法执行
        Object invoke = method.invoke(object, args);
        //方法之后
        System.out.println("方法之后执行..." + object);
        return invoke;
    }
}

/*输出结果:
方法之前执行...add:传递的参数...[1, 34]
add方法执行
方法之后执行...top.shiyiri.spring5.UserDaoImpl@2f0e140b
35
*/

3.2.2 Spring AOP 连接点

Spring AOP 并没有像其他 AOP 框架(例如 AspectJ)一样提供了完成的 AOP 功能,它是 Spring 提供的一种简化版的 AOP 组件。其中最明显的简化就是,Spring AOP 只支持一种连接点类型:方法调用。您可能会认为这是一个严重的限制,但实际上 Spring AOP 这样设计的原因是为了让 Spring 更易于访问

方法调用连接点是迄今为止最有用的连接点,通过它可以实现日常编程中绝大多数与 AOP 相关的有用的功能。如果需要使用其他类型的连接点(例如成员变量连接点),我们可以将 Spring AOP 与其他的 AOP 实现一起使用,最常见的组合就是 Spring AOP + ApectJ

3.2.3 Spring AOP 通知类型

AOP 联盟为通知(Advice)定义了一个 org.aopalliance.aop.Interface.Advice 接口。

Spring AOP 按照通知(Advice)织入到目标类方法的连接点位置,为 Advice 接口提供了 6 个子接口,如下表。

通知类型接口描述
前置通知org.springframework.aop.MethodBeforeAdvice在目标方法执行前实施增强。
后置通知org.springframework.aop.AfterReturningAdvice在目标方法执行完成,并返回一个返回值后实施增强。
环绕通知org.aopalliance.intercept.MethodInterceptor在目标方法执行前后实施增强。
异常通知org.springframework.aop.ThrowsAdvice在方法抛出异常后实施增强。
引入通知org.springframework.aop.IntroductionInterceptor在目标类中添加一些新的方法和属性

3.2.4 AOP 切面编程

Spring 使用 org.springframework.aop.Advisor 接口表示切面的概念,实现对通知(Adivce)和连接点(Joinpoint)的管理。

在 Spring AOP 中,切面可以分为三类:一般切面、切点切面和引介切面

切面类型接口描述
一般切面org.springframework.aop.AdvisorSpring AOP 默认的切面类型。 由于 Advisor 接口仅包含一个 Advice(通知)类型的属性,而没有定义 PointCut(切入点),因此它表示一个不带切点的简单切面。 这样的切面会对目标对象(Target)中的所有方法进行拦截并织入增强代码。由于这个切面太过宽泛,因此我们一般不会直接使用。
切点切面org.springframework.aop.PointcutAdvisorAdvisor 的子接口,用来表示带切点的切面,该接口在 Advisor 的基础上还维护了一个 PointCut(切点)类型的属性。 使用它,我们可以通过包名、类名、方法名等信息更加灵活的定义切面中的切入点,提供更具有适用性的切面。
引介切面org.springframework.aop.IntroductionAdvisorAdvisor 的子接口,用来代表引介切面,引介切面是对应引介增强的特殊的切面,它应用于类层面上,所以引介切面适用 ClassFilter 进行定义。

3.3 Spring 集成 AspectJ

AspectJ 是一个基于 Java 语言的全功能的 AOP 框架,它并不是 Spring 组成部分,是一款独立的 AOP 框架。

但由于 AspectJ 支持通过 Spring 配置 AspectJ 切面,因此它是 Spring AOP 的完美补充,通常情况下,我们都是将 AspectJ 和 Spirng 框架一起使用,简化 AOP 操作。

3.3.1 基于 XML 的 AspectJ AOP 开发

我们可以在 Spring 项目中通过 XML 配置,对切面(Aspect 或 Advisor)、切点(PointCut)以及通知(Advice)进行定义和管理,以实现基于 AspectJ 的 AOP 开发。

Spring 提供了基于 XML 的 AOP 支持,并提供了一个名为“aop”的命名空间,该命名空间提供了一个 <aop:config> 元素。

  • 在 Spring 配置中,所有的切面信息(切面、切点、通知)都必须定义在 <aop:config> 元素中
  • 在 Spring 配置中,可以使用多个 <aop:config>
  • 每一个 <aop:config> 元素内可以包含 3 个子元素: pointcut、advisor 和 aspect ,这些子元素必须按照这个顺序进行声明
3.3.1.1 引入 AOP 命名空间
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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
">

</beans>
3.3.1.2 定义切面<aop:aspect>

在 Spring 配置文件中,使用 <aop:aspect> 元素定义切面。该元素可以将定义好的 Bean 转换为切面 Bean,所以使用 <aop:aspect> 之前需要先定义一个普通的 Spring Bean。

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

其中,id 用来定义该切面的唯一标识名称,ref 用于引用普通的 Spring Bean。

3.3.1.3 定义切入点<aop:pointcut>

<aop:pointcut> 用来定义一个切入点,用来表示对哪个类中的那个方法进行增强。它既可以在 <aop:pointcut> 元素中使用,也可以在<aop:aspect> 元素下使用。

  • <aop:pointcut>元素作为 <aop:config> 元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享;
  • <aop:pointcut> 元素作为 <aop:aspect>元素的子元素时,表示该切入点只对当前切面有效。
<aop:config>    
    <aop:pointcut id="myPointCut" expression="execution(* top.shiyiri.service.*.*(..))"/>
</aop:config>

其中,id 用于指定切入点的唯一标识名称,execution 用于指定切入点关联的切入点表达式。

execution 的语法格式格式为:

execution([权限修饰符] [返回值类型] [类的完全限定名] [方法名称]([参数列表])

其中:

  • 返回值类型、方法名、参数列表是必须配置的选项,而其它参数则为可选配置项。
  • 返回值类型:*表示可以为任何返回值。如果返回值为对象,则需指定全路径的类名。
  • 类的完全限定名:指定包名 + 类名。
  • 方法名:*代表所有方法,set* 代表以 set 开头的所有方法。
  • 参数列表:(..)代表所有参数;(*)代表只有一个参数,参数类型为任意类型;(*,String)代表有两个参数,第一个参数可以为任何值,第二个为 String 类型的值。

举例 1:对 top.shiyiri.spring5 包下 UserDao 类中的 add() 方法进行增强,配置如下。

execution(* top.shiyiri.spring5.UserDao.add(..))

举例 2:对 top.shiyiri.spring5 包下 UserDao 类中的所有方法进行增强,配置如下。

execution(* top.shiyiri.spring5.UserDao.*(..))

举例 3:对 top.shiyiri.spring5 包下所有类中的所有方法进行增强,配置如下。

execution(* top.shiyiri.spring5.*.*(..))
3.3.1.4 定义通知

AspectJ 支持 5 中类型的 advice,如下:

<aop:aspect id="myAspect" ref="aBean">
    <!-- 前置通知 -->
    <aop:before pointcut-ref="myPointCut" method="..."/>
   
    <!-- 后置通知 -->
    <aop:after-returning pointcut-ref="myPointCut" method="..."/>
    <!-- 环绕通知 -->
    <aop:around pointcut-ref="myPointCut" method="..."/>
    <!-- 异常通知 -->
    <aop:after-throwing pointcut-ref="myPointCut" method="..."/>
    <!-- 最终通知 -->
    <aop:after pointcut-ref="myPointCut" method="..."/>
    .... 
</aop:aspect>
3.3.1.5 示例
  1. 创建 Book 和 BookProxy 类

    package top.shiyiri.spring5.aopxml;
    
    public class Book {
    
        public void buy() {
            System.out.println("buy......");
        }
    }
    
    ackage top.shiyiri.spring5.aopxml;
    
    public class BookProxy {
    
        public void before(){
            System.out.println("before.....");
        }
    }
    
  2. 配置 spring5 配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           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
    ">
    
        <!--创建对象-->
        <bean id="book" class="top.shiyiri.spring5.aopxml.Book" />
        <bean id="bookProxy" class="top.shiyiri.spring5.aopxml.BookProxy" />
    
        <!--配置aop增强-->
        <aop:config>
            <!--切入点-->
            <aop:pointcut id="p" expression="execution(* top.shiyiri.spring5.aopxml.Book.buy(..))"/>
    
            <!--配置切面-->
            <aop:aspect ref="bookProxy">
                <!--增强作用在具体的方法上-->
                <aop:before method="before" pointcut-ref="p" />
            </aop:aspect>
        </aop:config>
    </beans>
    
  3. 测试代码

    @Test
    public void test2(){
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
    
        Book book = context.getBean("book", Book.class);
        book.buy();
    }
    /*输出结果:
    before.....
    buy......
    */
    

3.3.2 基于注解的 AspectJ AOP 开发

在 Spring 中,虽然我们可以使用 XML 配置文件可以实现 AOP 开发,但如果所有的配置都集中在 XML 配置文件中,就势必会造成 XML 配置文件过于臃肿,从而给维护和升级带来一定困难。

为此,AspectJ 框架为 AOP 开发提供了一套 @AspectJ 注解。它允许我们直接在 Java 类中通过注解的方式对切面(Aspect)、切入点(Pointcut)和增强(Advice)进行定义,Spring 框架可以根据这些注解生成 AOP 代理。

名称说明
@Aspect用于定义一个切面
@Pointcut用于定义一个切入点
@Before用于定义前置通知,相当于 BeforeAdvice
@AfterReturning用于定义后置通知,相当于 AfterReturningAdvice
@Around用于定义环绕通知,相当于 MethodInterceptor
@AfterThrowing用于定义抛出通知,相当于 ThrowAdvice
@After用于定义最终通知,不管是否异常,该通知都会执行
@DeclareParents用于定义引介通知,相当于 IntroductionInterceptor
3.3.2.1 示例
  1. 创建类和方法

    package top.shiyiri.spring5.aopanno;
    
    import org.springframework.stereotype.Component;
    
    //被增强的类
    @Component
    public class User {
        public void add() {
    //        int i = 1 / 0;
            System.out.println("add.......");
        }
    }
    
    package top.shiyiri.spring5.aopanno;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    
    //增强的类
    @Component
    @Aspect //生成代理对象(定义为切面)
    @Order(2)
    public class UserProxy {
    
        //相同切入点抽取
        @Pointcut(value = "execution(* top.shiyiri.spring5.aopanno.User.add(..))")
        public void pointcut() {}
    
        //前置通知
        //@Before 注解表示作为前置通知
        @Before(value = "pointcut()")
        public void before() {
            System.out.println("before.....");
        }
    
        //后置通知(返回通知)
        @AfterReturning(value = "pointcut()")
        public void afterReturning() {
            System.out.println("afterReturning.....");
        }
    
        //最终通知
        @After(value = "pointcut()")
        public void after() {
            System.out.println("after.....");
        }
    
        //异常通知
        @AfterThrowing(value = "pointcut()")
        public void afterThrowing() {
            System.out.println("afterThrowing.....");
        }
    
        //环绕通知
        @Around(value = "pointcut()")
        public void around(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("环绕之前.....");
            //被增强的方法执行
            joinPoint.proceed();
            System.out.println("环绕之后.....");
        }
    }
    
  2. 配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           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="top.shiyiri.spring5.aopanno" />
    
        <!--开启Aspect生成代理对象-->
        <aop:aspectj-autoproxy />
    </beans>
    
  3. 测试

    @Test
    public void test(){
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
    
        User user = context.getBean("user", User.class);
        user.add();
    }
    /*输出结果:
    环绕之前.....
    before.....
    add.......
    环绕之后.....
    after.....
    afterReturning.....
    */
    
  4. 有多个增强类对同一个方法进行增强,设置增强类优先级

    package top.shiyiri.spring5.aopanno;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    @Order(1)  //在增强类上面添加注解 @Order(数字类型值),数字类型值越小优先级越高
    public class PersonProxy {
    
        //前置通知
        @Before(value = "execution(* top.shiyiri.spring5.aopanno.User.add(..))")
        public void before() {
            System.out.println("Person Before...");
        }
    }
    
  5. 再次测试

    @Test
    public void test(){
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
    
        User user = context.getBean("user", User.class);
        user.add();
    }
    /*输出结果:
    Person Before...
    环绕之前.....
    before.....
    add.......
    环绕之后.....
    after.....
    afterReturning.....
    */
    
  6. 完全注解开发

    package top.shiyiri.spring5.config;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @ComponentScan(basePackages = {"top.shiyiri.spring5"})
    //开启AspectJ的自动代理
    @EnableAspectJAutoProxy(proxyTargetClass = true)  //<aop:aspectj-autoproxy />
    public class ConfigAop {
    }
    
  7. 测试

    //完全注解开发
    @Test
    public void test1(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigAop.class);
        User user = context.getBean("user", User.class);
        user.add();
    }
    

完整代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值