Spring5-AOP详解

1. 什么是AOP

在软件业,AOP为 Aspect Oriented Programming 的缩写,意为面向切面编程,是通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

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

这里,我们通过一个登录的例子来说明 AOP:

下图是一个传统的登录流程。
在这里插入图片描述
因为不同的用户具有不同的角色,如管理员具有更高的权限,普通用户具有较少的权限。所以,现在我们想要在登录功能的基础上,添加一个权限判断的功能。

如果我们使用了原始方式,我们需要修改源代码添加逻辑功能。而使用 AOP 编程,只需要单独写一个权限判断模块,然后配置到登录功能中去。这样就可以不通过修改源代码的方式,完成添加新的功能。
在这里插入图片描述

2. AOP基本原理

AOP 的底层使用到了动态代理,通过动态代理的方式增强类中的某个方法的功能。

动态代理分为两种:
(1)有接口的动态代理,需要使用JDK动态代理
主要思路就是:创建接口实现类的代理对象,增强类的方法。

关于有接口的JDK 的动态代理细节,可以阅读一下这篇文章:【Java-代理模式】

(2)没有接口的动态代理,需要使用CGLIB的动态代理
主要思路:因为被代理类没有实现的接口,因此通过CGLIB 动态创建被代理类的子类,将这个子类作为代理类,然后重写被代理类的核心功能,做相关增强功能。

3. AOP常用术语

(1) 连接点
类中能够被增强的方法,称为连接点

(2) 切入点
类中实际被增强的方法,称为切入点

(3) 通知[增强]
切入点实际被增强的逻辑部分,被称为通知。例如,在登录功能中增加一个权限判断,那么这个权限判断就是一个通知。
通知分为多种类型:
前置通知:在切入点执行之前执行的通知
后置通知【返回通知】:在切入点执行完毕执行的通知
环绕通知:在切入点执行之前、执行完毕后都执行的通知
异常通知:在切入点执行期间出现了异常才会执行的通知
最终通知:类似于 finally,切入点不管有没有异常,最终都会执行的通知

(4) 切面
切面是一个动作,是将通知应用到切入点的过程。

4. 切入点表达式

Spring框架一般基于 AspectJ 实现针对 AOP 的相关操作

AspectJ 不是Spring 的组成部分,是一个独立的 AOP框架。但是一般编程时把 AspectJ 和 Spring框架一起使用,进行 AOP的操作。

4.1 切入点表达式的作用

通过切入点表达式确定类中哪个方法需要进行增强。

4.2 切入点表达式的语法结构

语法结构execute([访问修饰符] [返回值类型] [全类名].[方法名](参数列表))

举例1:对 com.zju.dao.BookDao类里面的 add方法进行增强
execute(* com.zju.dao.BookDao.add(..))
解释:
[访问修饰符]使用* 表示任意访问修饰符
[返回值类型]可以省略不写
[参数列表]使用.. 表示全部参数.

举例2:对 com.zju.dao.BookDao类里面的所有方法进行增强
execute(* com.zju.dao.BookDao.*(..))

举例3:对 com.zju.dao 包里面的所有类、类里面的所有方法进行增强
execute(* com.zju.dao.*.*(..))

5. Spring基于注解方式实现AOP【最常用】

5.1 创建被代理类

package com.zju.spring5.aopanno;

// 被代理类
public class User {
    public void add() {
        System.out.println("add...");
    }
}

5.2 创建代理类

在代理类中,创建方法,让不同的方法代表不同的通知类型,这里我们创建一个 UserProxy:before 方法,想让其在 User:add方法之前执行,也就是前置通知。

package com.zju.spring5.aopanno;

// 代理类
public class UserProxy {

    // 前置通知
    public void before() {
        System.out.println("before...");
    }
}

5.3 进行通知的配置

(1) 在Spring 的配置文件中,开启注解的扫描【或者写个配置类,效果也是一样的】
这里我们除了引用了 xmlns:context 命名空间,还引入了一个全新的 xmlns:aop 命名空间。

<?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.zju.spring5.aopanno"></context:component-scan>
</beans>

(2) 使用注解创建 User 和 UserProxy 对象

package com.zju.spring5.aopanno;

import org.springframework.stereotype.Component;

// 被代理类
@Component
public class User {
    public void add() {
        System.out.println("add...");
    }
}
package com.zju.spring5.aopanno;

import org.springframework.stereotype.Component;

// 代理类
@Component
public class UserProxy {

    // 前置通知
    public void before() {
        System.out.println("before...");
    }
}

(3) 在代理类上面添加注解 @Aspect

package com.zju.spring5.aopanno;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

// 代理类
@Component
@Aspect // 生成代理对象
public class UserProxy {

    // 前置通知
    public void before() {
        System.out.println("before...");
    }
}

5.4 在配置文件中开启生成代理对象

<?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.zju.spring5.aopanno"></context:component-scan>

    <!--
        开启Aspect 生成代理对象
        其实就是将带有 `@Aspect` 注解的类生成代理对象
     -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

5.5 配置不同类型的通知

在代理类中,在作为通知的方法上面添加通知类型的注解,使用切入点表达式来配置内容。

package com.zju.spring5.aopanno;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

// 代理类
@Component
@Aspect // 生成代理对象
public class UserProxy {

    // 前置通知
    // 使用@Before 注解表示作为前置通知
    @Before(value = "execution(* com.zju.spring5.aopanno.User.add(..))")
    public void before() {
        System.out.println("before...");
    }
}

现在,我们已经完成了前置通知的配置,现在来编写一个测试类进行测试:

package com.zju.spring5.test;

import com.zju.spring5.aopanno.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAop {

    @Test
    public void testAopAnno() {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
        User user = context.getBean("user", User.class);
        user.add();
    }
}

运行结果为:

before...
add...

Process finished with exit code 0

现在,再将代理类中的增强方法进行扩充,演示每一种通知类型:

package com.zju.spring5.aopanno;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 代理类
@Component
@Aspect // 生成代理对象
public class UserProxy {

    // 前置通知
    // 使用@Before 注解表示作为前置通知
    @Before(value = "execution(* com.zju.spring5.aopanno.User.add(..))")
    public void before() {
        System.out.println("【前置通知】触发...");
    }

    // 最终通知,方法无论是否正常返回都执行
    // 使用@After 注解表示作为后置通知
    @After(value = "execution(* com.zju.spring5.aopanno.User.add(..))")
    public void after() {
        System.out.println("【最终通知】触发...");
    }

    // 后置通知,只有方法正常返回之后才执行
    // 使用@AfterReturning 注解表示作为后置通知
    @AfterReturning(value = "execution(* com.zju.spring5.aopanno.User.add(..))")
    public void afterReturning() {
        System.out.println("【后置通知】触发...");
    }

    // 异常通知
    // 使用@AfterThrowing 注解表示作为异常通知
    @AfterThrowing(value = "execution(* com.zju.spring5.aopanno.User.add(..))")
    public void afterThrowing() {
        System.out.println("【异常通知】触发...");
    }

    // 环绕通知
    // 使用@Around 注解表示作为环绕通知
    @Around(value = "execution(* com.zju.spring5.aopanno.User.add(..))")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("【环绕通知】触发:核心方法之前...");

        // 执行被增强的核心方法
        proceedingJoinPoint.proceed();

        System.out.println("【环绕通知】触发:核心方法之后...");
    }
}

运行结果为:

【环绕通知】触发:核心方法之前...
【前置通知】触发...
add...
【环绕通知】触发:核心方法之后...
【最终通知】触发...
【后置通知】触发...

Process finished with exit code 0

现在,为了演示异常发生情况下的通知状态,我们手动给 User:add 方法中增加除0 异常:

package com.zju.spring5.aopanno;

import org.springframework.stereotype.Component;

// 被代理类
@Component
public class User {
    public void add() {
        int i = 1 / 0;
        System.out.println("add...");
    }
}

然后再次执行测试代码,运行结果为:

【环绕通知】触发:核心方法之前...
【前置通知】触发...
【最终通知】触发...
【异常通知】触发...

java.lang.ArithmeticException: / by zero

Process finished with exit code -1

5.6 公共切入点的抽取

从上面的例子中,我们可以发现代理类中,每一个增强方法上面的 切入点表达式 是完全一样的,那么此时我们就可以对这样一个公共的切入点进行抽取。

这里,需要定义一个空方法,然后加上 @Pointcut 声明为一个切入点。以后,对于相同的切入点,直接在 value 属性中写入该空方法的方法名即可。

package com.zju.spring5.aopanno;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 代理类
@Component
@Aspect // 生成代理对象
public class UserProxy {

    // 抽取公共切入点
    @Pointcut(value = "execution(* com.zju.spring5.aopanno.User.add(..))")
    public void addPointCut() {}

    // 前置通知
    // 使用@Before 注解表示作为前置通知
    @Before(value = "addPointCut()")
    public void before() {
        System.out.println("【前置通知】触发...");
    }

    // 最终通知,方法无论是否正常返回都执行
    // 使用@After 注解表示作为后置通知
    @After(value = "addPointCut()")
    public void after() {
        System.out.println("【最终通知】触发...");
    }

    // 后置通知,只有方法正常返回之后才执行
    // 使用@AfterReturning 注解表示作为后置通知
    @AfterReturning(value = "addPointCut()")
    public void afterReturning() {
        System.out.println("【后置通知】触发...");
    }

    // 异常通知
    // 使用@AfterThrowing 注解表示作为异常通知
    @AfterThrowing(value = "addPointCut()")
    public void afterThrowing() {
        System.out.println("【异常通知】触发...");
    }

    // 环绕通知
    // 使用@Around 注解表示作为环绕通知
    @Around(value = "addPointCut()")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("【环绕通知】触发:核心方法之前...");

        // 执行被增强的核心方法
        proceedingJoinPoint.proceed();

        System.out.println("【环绕通知】触发:核心方法之后...");
    }
}

5.7 设置增强类的优先级

如果现在一个被代理类,有多个代理类对其同一个方法进行增强,如何设置增强类的优先级呢?

例如,我们现在再给 User 创建一个代理类,现在UserProxyPersonProxy 都对User:add 方法添加了前置通知:

package com.zju.spring5.aopanno;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class PersonProxy {

    // 前置通知
    // 使用@Before 注解表示作为前置通知
    @Before(value = "execution(* com.zju.spring5.aopanno.User.add(..))")
    public void before() {
        System.out.println("【前置通知】Person代理类触发...");
    }
}

我们此时可以在不同被代理类的上面添加注解 @Order(数字类型值),来标记执行顺序注意:这里值越小,优先级越高!

我们将 PersonProxy 的优先级设置为最高的1

package com.zju.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)
public class PersonProxy {
....

我们将 UserProxy 的优先级设置为2

package com.zju.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 {

尝试运行,观察结果:

【前置通知】Person代理类触发...
【环绕通知】触发:核心方法之前...
【前置通知】触发...
add...
【环绕通知】触发:核心方法之后...
【最终通知】触发...
【后置通知】触发...

Process finished with exit code 0

可以发现,Person代理类的前置通知,优先于User代理类的前置通知执行

5.8 完全使用注解开发【重要】

我们在5.3 和5.4 节中依旧使用了 xml 配置文件,现在让我们看看如何创建一个配置类,完全摆脱配置文件的束缚。

创建一个配置类:
我们只要加上 @EnableAspectJAutoProxy(proxyTargetClass = true) 即可自动为所有标注 @Aspect 的类创建代理对象。

注意proxyTargetClass:这个属性,true代表使用的是CGLIB代理无接口类;false为默认值,使用的是JDK代理有接口类。我们这里设置为 true 是因为我们所采用的是无接口类代理模式。

package com.zju.spring5.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackages = {"com.zju.spring5.aopanno"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ConfigAop {
}

我们写一个测试代码进行测试:

@Test
public void testAopConfig() {
    ApplicationContext context = new AnnotationConfigApplicationContext(ConfigAop.class);
    User user = context.getBean("user", User.class);
    user.add();
}

运行结果为:

【前置通知】Person代理类触发...
【环绕通知】触发:核心方法之前...
【前置通知】触发...
add...
【环绕通知】触发:核心方法之后...
【最终通知】触发...
【后置通知】触发...

Process finished with exit code 0

6. Spring基于xml配置文件方式实现AOP【了解】

实际操作中,我们很少会通过配置文件方式去配置 AOP,这里我们主要通过一个案例演示一下,仅仅作为了解即可。

6.1 创建被代理类

package com.zju.spring5.aopxml;

// 被代理类
public class Book {

    public void buy() {
        System.out.println("buy...");
    }
}

6.2 创建代理类

package com.zju.spring5.aopxml;

// 代理类
public class BookProxy {

    // 前置通知
    public void before() {
        System.out.println("【前置通知】触发...");
    }
}

6.3 创建被代理类和代理类对象

<?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">

    <!-- 创建两个类的对象 -->
    <bean id="book" class="com.zju.spring5.aopxml.Book"></bean>
    <bean id="bookProxy" class="com.zju.spring5.aopxml.BookProxy"></bean>
</beans>

6.4 配置文件配置AOP

因为配置标签显而易见,直接结合着备注学习即可。

<?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">

    <!-- 创建两个类的对象 -->
    <bean id="book" class="com.zju.spring5.aopxml.Book"></bean>
    <bean id="bookProxy" class="com.zju.spring5.aopxml.BookProxy"></bean>

    <!-- 配置AOP增强 -->
    <aop:config>
        <!-- 配置切入点 -->
        <aop:pointcut id="addPointCut" expression="execution(* com.zju.spring5.aopxml.Book.buy(..))"/>

        <!-- 配置切面 ref指向代理类 -->
        <aop:aspect ref="bookProxy">
            <!-- 配置通知作用在哪个方法上 -->
            <aop:before method="before" pointcut-ref="addPointCut"/>
        </aop:aspect>
    </aop:config>
</beans>

现在,就完成了全部的配置。

编写测试代码:

@Test
public void testAopXml() {
    ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
    Book book = context.getBean("book", Book.class);
    book.buy();
}

运行结果为:

【前置通知】触发...
buy...

Process finished with exit code 0
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁头娃撞碎南墙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值