第一个Spring AOP 应用

用一个案列讲清楚Spring AOP 如何对已有功能进行拓展,做到即插即用.

Maven 加载依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>

    <!--aspectjweaver是Spring AOP的底层依赖-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
</dependencies>

定义目标类与目标方法

定义一个 UserService 类 和 UserDao 类。

public class UserService {
    private UserDao userDao;

    public void createUser(){
        System.out.println("执行创建用户业务逻辑");
        userDao.insert();
    }

    public String generateRandomPassword(String type , Integer length){
        System.out.println("按" + type + "方式生成"+ length  + "位随机密码");
        return "Zxcquei1";
    }
    
    // getter 和 setter 方法 省略
 
}
public class UserDao {
    public void insert(){
        System.out.println("新增用户数据");
    }
}

新的需求

假设有这么一个需求,需要获取 UserService 运行过程中每一个方法执行开始时的时间戳,和方法传入的参数个数。

我们可以在UserService 所有方法内部多加几行代码,用于获取进入方法时的时间戳,和参数个数。但是这样做会非常麻烦,需要在每一个方法内部进行修改,以后不再需要这个功能时, 又需要将这些代码删掉。

Spring AOP 实现新需求

如果可以做这么一个插件,用来"卡"在目标方法上面,可以获取目标方法的关键信息那就好了。当我们不再需要这个功能时,直接将这个插件拆掉即可。

定义切面类

我们可以理解 切面类 就是插件,用来拓展功能的。

定义一个 MethodAspect 切面类,里面有一个 printExecutionTime 方法,其作用时获取 目标方法(也就是需要被拓展的方法) 执行的起始时间和参数信息(个数和名称)。

特别的是, MethodAspect 传入的是 JointPoint 对象, JoinPoint 类型 也就是切点, 可以理解为这个MethodAspect 切面类 切到哪个方法,就可以获取到哪个方法的信息。

public class MethodAspect {
    //切面方法, 用于快速扩展额外功能
    //JointPoint, 连接点, 通过连接点可以获取目标方法的信息
    public void printExecutionTime(JoinPoint joinPoint){
        SimpleDateFormat sdt= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSSS");
        String now = sdt.format(new Date());
        String className = joinPoint.getTarget().getClass().getName();//获取目标类的名称
        String methodName = joinPoint.getSignature().getName();//获取目标方法的名称
        System.out.println(now +" : "+className+"."+methodName);
        Object[] args = joinPoint.getArgs();//获取目标方法参数
        System.out.println("----> 参数个数" + args.length );
        for (Object arg :args){
            System.out.println("--->参数 : "+ arg);
        }
    }
}

配置 applicationContext.xml 文件

配置文件的抬头为 标准写法照抄即可。我们首先定义了两个 bean (userDao 和 userService)。我们还定义了一个切面类的bean。重点是下面的 <aop:config> 标签。 <aop:pointcut> 标签 作用是定义好 切面类的 pointcut 位置,其核心为 expression 属性, 也就是说清除在哪些类,哪些方法里面起作用。 <aop:aspect> 标签作用是指明Spring AOP 中用到哪个切面类,需要在目标方法运行 什么时候执行。比如下面的<aop:before> 标签说明 切面类 会在目标方法执行前去执行 切面方法。

<?xml version="1.0" encoding="UTF-8"?>
<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"
       xmlns="http://www.springframework.org/schema/beans"
       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="userDao" class="indi.chester.spring.aop.dao.UserDao"/>
 
    <bean id="userService" class="indi.chester.spring.aop.service.UserService">
        <property name="userDao" ref="userDao"/>
    </bean>

    <!-- 切面类 -->
    <bean id="methodAspect" class="indi.chester.spring.aop.aspect.MethodAspect"/>

    <!-- 配置 AOP -->
    <aop:config>
        <!-- pointcut 切点, 使用 execution 表达式描述切面的作用范围  -->

        <!-- pointcut 切点表达式: pubilc 访问修饰符, * 任意返回类型, .. 包通配符, *.* 任意类的任意方法, (..) 参数通配符 -->
        <!-- 作用于 indi.chester 包下所有public类的运行 -->
        <aop:pointcut id="pointcut" expression="execution(public * indi.chester..*.*(..))"/>

        <!-- 定义切面类 -->
        <aop:aspect ref="methodAspect">
            <!-- before 通知, 在目标方法运行前, 先去执行 切面类 的 切面方法-->
            <aop:before method="printExecutionTime" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

测试一下

public class SpringApplication {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        UserService userService = context.getBean("userService", UserService.class);
        userService.createUser();
        userService.generateRandomPassword("MD5", 16);
    }
}

运行结果:

022-02-18 21:11:38 0335 : indi.chester.spring.aop.service.UserService.createUser
----> 参数个数0
执行创建用户业务逻辑
2022-02-18 21:11:38 0357 : indi.chester.spring.aop.dao.UserDao.insert
----> 参数个数0
新增用户数据
2022-02-18 21:11:38 0366 : indi.chester.spring.aop.service.UserService.generateRandomPassword
----> 参数个数2
--->参数 : MD5
--->参数 : 16
按MD5方式生成16位随机密码

可以看出定义好的切面类在 creaeUser 和 generateRandomPassword 方法调用之前执行,打印输出了参数个数和参数.

Last updated

Was this helpful?