Spring 10: AspectJ框架 + @Before前置通知

2年前 (2022) 程序员胖胖胖虎阿
290 0 0

AspectJ框架

概述

  • AspectJ是一个优秀的面向切面编程的框架,他扩展了java语言,提供了强大的切面实现
  • 本身是java语言开发的,可以对java语言面向切面编程进行无缝扩展

AOP常见术语分析

  • 切面:那些重复的,公共的,通用的功能被称为切面,例如,日志,事务,权限等功能

  • 连接点:实际就是目标方法,因为在目标方法中要实现业务功能和切面功能的整合

  • 切入点(Pointcut):用来指定切入的位置,切入点可以是一个目标方法,可以是一个类中的所有方法,还可以是某个包下的所有类中的方法等

  • 目标对象:操作谁,谁就是目标对象,往往是业务接口的实现类对象

  • 通知(Advice):来指定切入的时机是在目标方法执行前,执行后,出错时,还是环绕目标方法来切入切面功能

AspectJ常见通知类型

  • 前置通知:@Before
  • 后置通知:@AfterReturning
  • 环绕通知:@Around
  • 最终通知:@After
  • 定义切入点:@Pointcut(了解)

AspectJ的切入点表达式

公式

  • 关键字:切入点表达式由execution关键字引出,后面括号内跟需要切入切面功能的业务方法的定位信息

  • 规范的公式:execution( 访问权限 方法返回值 方法声明(参数) 异常类型 )

  • 简化后的公式:execution( 方法返回值 方法声明(参数) )

示例

  • 切入点表达式及其定位的需要切入切面功能的业务方法
1. execution(public * *(..) ):任意的公共方法
2. execution(*    set*(..)    ):任何以set开始的方法
3. execution(*    com.xyz.service.impl.*.*(..)):com.xyz.service.impl包下的任意类的任意方法
4. execution(*    com.xyz.service..*.*(..)):com.xyz.service包及其子包下的任意类的任意方法
5. execution(*    *..service.*.*(..)):service包下的任意类的任意方法,注意service包前可以有任意包的子包
6. execution(*    *.service.*.*(..)):service包下的任意类的任意方法,注意:service包前只能有一个任意的包

@Before通知

图解

  • 重点想表达的是:前置通知最多获取到目标业务方法的方法签名等前置信息,获取不到目标方法的返回值,因为前置切面在目标方法前执行
  • 关于前置切面方法可以获取到的目标业务方法的信息,本文后面讨论JoinPoint类型的参数时会讨论

Spring 10: AspectJ框架 + @Before前置通知

maven项目的pom.xml

  • 重点是添加spring-context和spring-aspects依赖
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>ch08-spring-aspectj</artifactId>
  <version>1.0.0</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <!-- junit测试依赖 -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>

    <!-- 添加spring-context依赖 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.3.22</version>
    </dependency>

    <!-- 添加spring-aspects -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.3.22</version>
    </dependency>
  </dependencies>

  <build>
    <!-- 添加资源文件的指定-->
    <resources>
      <resource>
        <!-- 目标目录1 -->
        <directory>src/main/java</directory>
        <includes>
          <!-- 被包括的文件类型 -->
          <include>**/*.xml</include>
          <include>**/*.properties</include>
        </includes>
        <filtering>false</filtering>
      </resource>
      <resource>
        <!-- 目标目录2 -->
        <directory>src/main/resources</directory>
        <includes>
          <!-- 被包括的文件类型 -->
          <include>**/*.xml</include>
          <include>**/*.properties</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>
  </build>

</project>

业务接口

  • 业务接口:SomeService
package com.example.s01;

/**
 * 定义业务接口
 */
public interface SomeService {
    //定义业务功能
    default String doSome(int orderNums){return null;}
}

业务实现类

  • 业务实现类:SomeServiceImpl
package com.example.s01;

/**
 * 业务功能实现类
 */
public class SomeServiceImpl implements SomeService{
    @Override
    public String doSome(int orderNums) {
        System.out.println("---- 业务功能 ----");
        System.out.println("预定图书: " + orderNums + " 册");
        return "预定成功!";
    }
}

切面类

  • 切面类:SomeServiceAspect
package com.example.s01;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

/**
 * 切面类
 */

//添加@Aspect注释,表明切面类交给AspectJ这一面向切面编程框架管理
@Aspect
public class SomeServiceAspect {
    /**
     * a. 切面功能由切面类中的切面方法负责完成
     *
     * b. 前置通知的切面方法规范
     * 	1.访问权限是public
     * 	2.方法的返回值是void
     * 	3.方法名称自定义
     * 	4.方法没有参数,如果有参数也只能是JoinPoint类型
     * 	5.必须使用注解:@Before,来声明切入的时机是前切和切入点的信息
     * 	参数:value,用来指定切入点表达式
     *
     * c.前切示例
     *	目标方法(即业务实现类中的方法):public String doSome(int orderNums)
     */
    @Before(value = "execution(public String com.example.s01.SomeServiceImpl.doSome(int))")
    public void myBefore(){
        System.out.println("前置通知: 查询图书是否有剩余");
    }
}

applicationContext.xml

  • 绑定业务功能和切面功能
<?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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 创建业务功能对象 -->
    <bean id="someServiceImpl" class="com.example.s01.SomeServiceImpl"/>
    
    <!-- 创建切面功能对象 -->
    <bean id="someServiceAspect" class="com.example.s01.SomeServiceAspect"/>
    
    <!-- 绑定业务功能和切面功能-->
    <aop:aspectj-autoproxy/>
    
</beans>

测试

package com.example.test;

import com.example.s01.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestBeforeAspect {
    //测试前置切面功能
    @Test
    public void testBeforeAspect(){
        //创建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
        
        //实际获取的是业务实现类对象的jdk动态代理对象
        SomeService agent = (SomeService) ac.getBean("someServiceImpl");
        
        //测试agent类型
        System.out.println("agent类型: " + agent.getClass());
        
        //代理对象调用业务功能(切面功能 + 被代理对象的传统业务功能)
        String res = agent.doSome(10);  //接住目标对象目标业务方法的返回值
        System.out.println("业务执行结果: " + res);
    }
}

测试输出

  • 这里特意输出了一下agent变量的类型,可见此时底层使用的是jdk动态代理来获取代理对象
  • 前置切面功能成功在业务功能前执行
agent类型: class com.sun.proxy.$Proxy10
前置通知: 查询图书是否有剩余
---- 业务功能 ----
预定图书: 10 册
业务执行结果: 预定成功!

Process finished with exit code 0

扩展测试1

  • 下面所示的切面类中的myBefore切面方法中的切入点表达式,限定的目标方法的范围太小,
execution(public String com.example.s01.SomeServiceImpl.doSome(int))	//目标方法的限定范围太小

业务接口

  • SomeService新增方法show()
    //新增一个业务功能
    default void show(){}

业务实现类

  • SomeServiceImpl对show()方法进行实现
    @Override
    public void show() {
        System.out.println("新增的show()方法被调用.....");
    }

测试

    //测试切入点表达式对新增的业务方法是否起作用
    @Test
    public void testBeforeAspect02(){
        //创建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
        //获取业务实现类的jdk动态代理对象
        SomeService agent = (SomeService) ac.getBean("someServiceImpl");
        //代理对象调用业务功能
        agent.show();
    }

测试输出

  • 可以看到由于切入点表达式划定的目标方法的范围并没有包括新增的业务方法,所以新增的show()方法并没有被成功的切入前置切面
新增的show()方法被调用.....

Process finished with exit code 0

修改切入点表达式

  • 将上述切面类(指:SomeServiceAspect)中的切面方法中的切入点表达式做如下修改,此时限定的范围为:com.example.s01包下的所有类的所有方法
execution(* com.example.s01.*.*(..))	//限定的范围不大不小,开发中常用
  • 再次做上述测试:testBeforeAspect02(),测试结果如下,此时前置切面的范围包括show()方法,前置切面成功切入
前置通知: 查询图书是否有剩余
新增的show()方法被调用.....

Process finished with exit code 0

再次修改切入点表达式

  • 上述切入点表达式还可以做如下修改,指:com.example.s01包及其子包和当前路径下的所有类中的所有方法
execution(* com.example.s01..*(..))	//不常用,了解即可
  • 或者做如下修改,指:项目中的所有方法
execution(* *(..))	//限定范围太大,不常用,了解即可

扩展测试2

jdk动态代理

  • 如下是上述applicationContext.xml中绑定业务功能和切面功能的标签,使用此标签默认使用的是jdk动态代理,接住代理对象,需要用接口类型
    <!-- 绑定业务功能和切面功能-->
    <aop:aspectj-autoproxy/>
    //使用业务接口的类型去接住动态代理对象
    SomeService agent = (SomeService) ac.getBean("someServiceImpl");
  • 此时如果用实现类的类型SomeServiceImpl去接,则报错:类型转换错误。因为此时agent是jdk动态代理类型,不再是实现类的类型

Spring 10: AspectJ框架 + @Before前置通知

CGLib子类代理

  • 将applicationContext.xml中的代理标签做如下修改,此时使用的是CGLib子类代理
    <!-- 绑定业务功能和切面功能-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

测试

  • 由于底层使用的是子类来扩展业务实现类(是父类,被扩展),可以用父类型去接代理对象(子类型),因为父类指向子类型,子类型重写了父类型中的方法,调用时还是调用子类中扩展后的方法
    //测试代理对象的类型
    @Test
    public void testBeforeAspect03(){
        //创建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
        //CGLib子类代理,用实现类(父类)的类型来接
        SomeServiceImpl agent = (SomeServiceImpl) ac.getBean("someServiceImpl");
        //调用业务功能
        agent.show();
    }

测试输出

  • 成功使用父类型接住代理对象(子类),使得切面功能在业务功能前调用
前置通知: 查询图书是否有剩余
新增的show()方法被调用.....

Process finished with exit code 0

注意

  • 当使用CGLib子类代理时也可以用业务接口来接住子类代理对象,因为子类代理对象的父类(也就是被扩展的类)是接口的实现类,可以用接口指向实现类,自然也可以指向实现类的子类

小结

  • 综上所述,不管使用JDK动态代理还是CGLib子类代理,使用业务接口的类型去接住代理对象总是可以的

基于注解的@Before

业务实现类

  • 添加@Service注解
/**
 * 业务功能实现类
 */
@Service
public class SomeServiceImpl implements SomeService{
	//......
}

切面类

  • 添加@Component注解
//切面类交给Aspectj框架管理
@Aspect
@Component
public class SomeServiceAspect {
	//......
}

applicationContext.xml

  • 添加包扫描
    <!-- 添加包扫描 -->
    <context:component-scan base-package="com.example.s01"/>

测试

    //测试代理对象的类型
    @Test
    public void testBeforeAspect03(){
        //创建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
        //CGLib子类代理,用实现类(父类)的类型来接
        SomeServiceImpl agent = (SomeServiceImpl) ac.getBean("someServiceImpl");
        //调用业务功能
        agent.show();
    }

测试输出

前置通知: 查询图书是否有剩余
新增的show()方法被调用.....

Process finished with exit code 0

前置通知的JoinPoint参数

切面类

  • 为上述切面类中的切面方法传入JoinPoint参数
    @Before(value = "execution(* com.example.s01.*.*(..))")
    public void myBefore(JoinPoint joinPoint){
        System.out.println("目标方法签名: " + joinPoint.getSignature());
        System.out.println("目标方法参数: " + Arrays.toString(joinPoint.getArgs()));
        System.out.println("前置通知: 查询图书是否有剩余");
    }

测试

    //测试前置切面功能
    @Test
    public void testBeforeAspect(){
        //创建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
        //获取业务实现类的CGLib动态代理对象
        SomeService agent = (SomeService) ac.getBean("someServiceImpl");
        //测试agent类型
        System.out.println("agent类型: " + agent.getClass());
        //代理对象调用业务功能
        String res = agent.doSome(10);  //接住目标对象目标业务方法的返回值
        System.out.println("业务执行结果: " + res);
    }

测试输出

  • 此时的代理标签使用的是CGLib子类代理
  • 前置通知成功在目标业务方法前执行
  • 成功获取到目标业务方法的方法签名和参数
agent类型: class com.example.s01.SomeServiceImpl$$EnhancerBySpringCGLIB$$36b23096
目标方法签名: String com.example.s01.SomeServiceImpl.doSome(int)
目标方法参数: [10]
前置通知: 查询图书是否有剩余
---- 业务功能 ----
预定图书: 10 册
业务执行结果: 预定成功!
版权声明:程序员胖胖胖虎阿 发表于 2022年11月12日 上午12:56。
转载请注明:Spring 10: AspectJ框架 + @Before前置通知 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...