Featured image of post Java工程师 SpringAOP实战

Java工程师 SpringAOP实战

🌏Java工程师 SpringAOP原理实战 🎯 这篇文章用于记录 对SpringAOP的使用实践 包括 1️⃣AOP介绍 2️⃣使用切面表达式实践AOP的使用 3️⃣使用注解实践AOP的使用

🎄概述

这一篇文章是Java工程师 SpringAOP原理实现的第一部分 主要内容是AOP概念的引入、编码实战和原理分析

AOP是基于IoC的基础之上实现的 如果对IoC不了解 可见下面的文章

🌏此处见 IoC实现I

🌏此处见 IoC实现II

🎄AOP的概念

要理解AOP的概念,我们先用OOP举例,比如一个业务组件BookService 它有几个业务方法:

  • createBook:添加新的Book
  • updateBook:修改Book
  • deleteBook:删除Book

对每个业务方法,例如 createBook() 除了业务逻辑 还需要1️⃣安全检查 2️⃣日志记录和3️⃣事务处理 它的代码像这样:

public class BookService {
    public void createBook(Book book) {
        // 1️⃣安全检查
        securityCheck();
        //  3️⃣事务处理
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit(); // 3️⃣事务处理
        } catch (RuntimeException e) {
            tx.rollback(); // 3️⃣事务处理
            throw e;
        }
        // 2️⃣日志记录
        log("created book: " + book);
    }
    public void updateBook(Book book) {
        // 1️⃣安全检查
        securityCheck();
        // 3️⃣事务处理
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        // 2️⃣日志记录
        log("updated book: " + book);
    }
    
    /**
     * 其他方法...
     */
}

考察业务模型可以发现,BookService关心的是自身的核心逻辑,但整个系统还要求关注安全检查、日志、事务等功能,这些功能实际上“横跨”多个业务方法,为了实现这些功能,💩不得不在每个业务方法上重复编写代码

一种可行的方式是使用Proxy模式,将某个功能,例如,权限检查,放入Proxy中 这种方式的缺点是比较麻烦,必须先抽取接口,然后,针对每个方法实现Proxy:

public class SecurityCheckBookService implements BookService{
    private final BookService target;
    public SecurityCheckBookService(BookService target) {
        this.target = target;
    }
    public void createBook(Book book) {
        securityCheck();
        target.createBook(book);
    }
    public void updateBook(Book book) {
        securityCheck();
        target.updateBook(book);
    }
    public void deleteBook(Book book) {
        securityCheck();
        target.deleteBook(book);
    }

    private void securityCheck() { ... }
}

✨进一步分析,既然SecurityCheckBookService的代码都是标准的Proxy样板代码,不如把权限检查视作一种切面(Aspect),把日志、事务也视为切面,然后,以某种自动化的方式,把切面织入到核心逻辑中,实现Proxy模式 这样就演变出AOP 面向切面编程了

🎄实战使用AOP_01

接下来 直接创建Maven项目来使用AOP 实战AOP编程

🎯创建Maven项目

项目名为AOP-In-Practice_20230916

点击创建 创建后的项目结构如下:

🎯修改pom.xml引入AOP编程需要的依赖

<?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>bbm.com</groupId>
	<artifactId>AOP-In-Practice_20230916</artifactId>
	<version>1.0-SNAPSHOT</version>

	<!-- 父项目 使用Spring Boot Starter -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.4</version>
	</parent>
	
	<properties>
		<maven.compiler.source>8</maven.compiler.source>
		<maven.compiler.target>8</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<!-- 项目依赖 -->
	<dependencies>
		<!-- Spring Boot Starter -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
			<version>2.5.4</version>
		</dependency>
		<!-- Spring Boot Starter AOP -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
        <!-- 使用lombok -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

🎯编写要被代理的类Duck(切面类要切入的类)

package bbm.com;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
@author Liu Xianmeng
@createTime 2023/9/16 16:32
@instruction 鸭子类
*/

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Duck {
    // 鸭子的name
    private String name;
    // 鸭子的游泳方法 这个方法有一个参数speed 表示鸭子游泳的速度 单位 cm/s
    public void swim(int speed) {
        // swim()方法体打印一句日志
        System.out.println("C Duck M swim() -> " + name + " is swmming! speed = " + speed + "cm/s");
    }
}

🎯编写日志切面类LoggingAspect

阅读代码的时候注意以下几点:

  • 切入表达式的写法和含义
  • @Before、@AfterReturning、@After注解的含义(代码之后还有详细的说明)
  • JoinPoint是什么 有什用
package bbm.com;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

import java.util.Arrays;

/**
@author Liu Xianmeng
@createTime 2023/9/16 16:39
@instruction 日志切面类
*/

@Aspect // @Aspect标识这是一个切面类
@Component 
public class LoggingAspect {
    /**
     * 【"execution(* bbm.com.Duck.*(..))" 是一个切入表达式 它的具体含义如下】
     * 在切入点表达式中 第一个"*"表示返回类型 它代表任意类型的返回值
     * 在这个特定的切入点表达式中 它用于匹配bbm.com.Duck类中的所有方法 不论其返回类型是什么
     * 换句话说 无论Duck类的方法返回什么类型的值 都将触发该切入点
     * 我定义的Duck类的swim()方法的返回值是void 所以会触发该切入点
     *
     * 第二个 "*" 表示方法名的通配符
     * ".."(两个点点)表示方法参数的通配符 如果切面表达式指向的方法不含参数 可以使用空括号表示匹配无参数的方法
     *
     * 如果该切入表达式更精确地指向Duck类的swim()方法 则可以写成下面的样子:
     * "execution(void bbm.com.Duck.swim())"
     *
     *
     * 【@Before("execution(* bbm.com.Duck.*(..))")作用于beforeAdvice()方法的含义】
     * Duck类的所有方法在执行前 会先执行beforeAdvice()方法
     *
     *
     * 【@param joinPoint参数表示连接点 它是Spring AOP框架提供的一个类 用于表示该切面方法指向的目标方法swim()】
     * JoinPoint 提供了一些有用的方法和信息 可以在切面中使用 以下是一些常用的 JoinPoint 方法:
     *     getSignature():获取当前执行方法的签名 包括方法名、修饰符、返回类型等信息
     *     getArgs():获取当前执行方法的参数列表
     *     getTarget():获取当前执行方法的目标对象
     *     getSourceLocation():获取当前执行方法所在的源代码位置
     *     toShortString():将当前执行方法描述为一个简短的字符串
     * 我们可以在 beforeAdvice 方法中使用 JoinPoint 参数来访问和操作这些信息
     *
     * 例如代码体中:
     *     可以通过 joinPoint.getSignature().getName() 获取当前执行方法的方法名
     *     通过 joinPoint.getArgs() 获取当前执行方法的参数列表 并根据实际需求进行相应的处理
     */
    @Before("execution(* bbm.com.Duck.*(..))")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("C LoggingAspect M beforeAdvice() -> " + joinPoint.getSignature().getName());
        // 获取swim()方法的名字
        String methodName = joinPoint.getSignature().getName();
        // 获取swim()方法的参数
        Object[] args = joinPoint.getArgs();
        // 打印获取的信息
        System.out.println("C LoggingAspect M beforeAdvice() -> 切入的方法名:" + methodName);
        System.out.println("C LoggingAspect M beforeAdvice() -> 切入的方法的参数列表:" + Arrays.toString(args));
    }

    /**
     * @After("execution(void bbm.com.Duck.swim(int))")作用于afterAdvice()方法的含义:
     * Duck类的swim方法在执行后 会紧着着执行afterAdvice()方法
     */
    @After("execution(void bbm.com.Duck.swim(int))") // 使用更具体放入切面表达式
    public void afterAdvice(JoinPoint joinPoint) {
        System.out.println("C LoggingAspect M afterAdvice() -> " + joinPoint.getSignature().getName());
    }

    /**
     * @AfterReturning(pointcut = "execution(* bbm.com.Duck.*(..))", returning = "result")
     * 作用于afterReturningAdvice()方法的含义:
     *
     * Duck类的方法在返回后 会紧着着执行afterReturningAdvice()方法 并且返回值被result接收
     */
    @AfterReturning(pointcut = "execution(* bbm.com.Duck.*(..))", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        System.out.println("C LoggingAspect M afterReturningAdvice() -> " + joinPoint.getSignature().getName() +
            ", returned value: " + result);
    }
}

📑关于@AfterReturning和@After的进一步探讨

@AfterReturning和@After是Spring AOP中的切面注解,用于在方法执行后执行相应的逻辑。其中,@AfterReturning只有在目标方法成功完成后才执行,而@After则无论目标方法是否成功都会执行。

根据执行顺序,先执行@AfterReturning注解标注的方法,然后再执行@After注解标注的方法。也就是说,@AfterReturning注解的方法先于@After注解的方法执行。

需要注意的是,如果目标方法抛出异常或通过return语句提前返回,则@AfterReturning注解标注的方法会被跳过,不会执行。而@After注解标注的方法无论如何都会执行。

🎯在pom.xml中引入MVC需要的依赖

为了能够看到AOP的效果 我们在pom.xml中引入SpringMVC 使用Controller提供的访问路径在浏览器来调用Duck的swim()方法

<!-- 引入MVC的依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

这个依赖会引入MVC:

🎯编写配置类AppConfiguration

配置类会指定IoC容器要扫描的包 在配置类中 可以通过@Bean注解来配置要注入到IoC容器的Bean,以便在应用中使用

package bbm.com;

/**
@author Liu Xianmeng
@createTime 2023/9/16 17:32
@instruction
*/

@Configuration // 告诉Spring这是一个配置类
@ComponentScan("bbm.com") // 告诉Spring要扫描bbm.com包下的所有类 将符合条件的类注入容器进行管理
@EnableAspectJAutoProxy  // 这个注解的作用 见下文解释
public class AppConfiguration {
    @Bean // 配置一个Bean 装入IoC容器 在后文的Controller类中使用
    public Duck returnADuck(){
        return new Duck("littleBabyDuck");
    }
}

关于代码中的@EnableAspectJAutoProxy 注解(具体的源码分析可见AOP编码实现那一篇文章):

@EnableAspectJAutoProxy 是Spring框架的一个注解,用于启用AspectJ自动代理功能。AspectJ是一种面向切面编程的框架,通过在关注点代码中定义切面,可以将横切逻辑与核心业务逻辑分离,提供了更好的可维护性和可扩展性。

使用@EnableAspectJAutoProxy注解可以启用Spring对AspectJ的支持,在使用AspectJ注解定义切面时,能够自动将切面应用到相应的目标对象中。这样,我们就可以在切面中定义通知(advice)和切点(pointcut),通过AspectJ表达式进行切入操作。

启用AspectJ自动代理功能,只需在Spring配置类上添加@EnableAspectJAutoProxy注解即可。该注解可以设置proxyTargetClass属性,默认值为false,表示使用Java动态代理技术(基于接口的代理)。如果想要使用CGLIB代理技术(基于类的代理),可以将proxyTargetClass属性设置为true。此外,该注解还提供了exposeProxy属性,默认值为false,表示不暴露代理对象。如果需要在切面中获取当前代理对象,可以将exposeProxy属性设置为true。

🎯编写Controller方法

注意Duck的名字要和配置类配置的Bean的名字一致 详细可见注释

package bbm.com;

/**
@author Liu Xianmeng
@createTime 2023/9/17 9:01
@instruction
*/

@SuppressWarnings({"all"})
@Controller
public class DuckController {

    /**
     * 注意这个Duck的名字要和配置类配置的Bean的名字一致
     * 只有这样 在使用@Autowired注解注入这个Duck实例的时候才能在IoC容器中找到并注入
     * 如果是其他名字 注入的时候会因为IoC容器中找不到而返回null
     * 
     * 在注入的时候IoC容器会把自己装入的littleBabyDuck对象的引用赋给littleBabyDuck变量
     */
    @Autowired
    private Duck littleBabyDuck;

    // 访问这个路径调用duck的swim()方法
    @RequestMapping("/swim")
    public String invokeSwim() {
        littleBabyDuck.swim(20); // 鸭子的游泳速率是20cm/s
        return "ok";
    }
}

🎯添加应用配置application.yml

server:
  port: 8888 # 项目启动端口
  servlet:
    context-path: / # 项目上下文路径

🎯编写启动类并启动项目

package bbm.com;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

到此为止 项目的目录结构如下:

启动项目

🎯浏览器访问测试

🎯查看后端控制台日志

可以看到 切面类定义的三个方法在swim()方法执行的前后都得到了执行 到此为止 就成功地使用了Spring提供的AOP功能来完成简单的功能实现

🎄实战使用AOP_02

🍭概述

实战使用AOP_01中 我们使用了切面表达式的方式来标注切面方法应该在哪些类的哪些方法上执行 虽然Spring容器可以把指定的方法通过AOP规则装配到指定的Bean的指定方法前后,但是,如果自动装配时,因为不恰当的范围,容易导致意想不到的结果,即很多不需要AOP代理的Bean也被自动代理了,并且,后续新增的Bean,如果不清楚现有的AOP装配规则,容易被强迫装配

使用AOP时,被装配的Bean最好自己能清清楚楚地知道自己被安排了

例如,Spring提供的@Transactional就是一个非常好的例子 如果我们希望自己写的Bean的方法在一个数据库事务中处理,就标注上@Transactional

@Component
public class UserService {
    // 有事务
    @Transactional // 作用于当前方法
    public User createUser(String name) { }
    // 无事务
    public boolean isValidName(String name) { }
    // 有事务
    @Transactional // 作用于当前方法
    public void updateUser(User user) { }
}

如果希望该Bean下的所有方法在执行的时候都能在同一个事务中(也就是说一个方法对应一个事务 方法的内部不需要进行事务的管理)就可以在类名上加上@Transactional

@Component
@Transactional // 作用于所有的方法
public class UserService { ... }

通过@Transactional,某个方法是否启用了事务就一清二楚了。因此,装配AOP的时候,使用注解是更好的方式 所以 在这一节中 我们来使用第二种方式 用🌱注解来标注切面方法的目标方法

🍭创建一个新的@ExecuteAspectBefore注解

这个注解标注哪些方法执行前 会先执行AuthorityAspect类的beforeAdvice()方法 然后再执行自己

AuthorityAspect切面类会在后面创建

package bbm.com;

/**
@author Liu Xianmeng
@createTime 2023/9/17 17:01
@instruction @ExecuteAspectBefore ->
             标注哪些方法执行前 会先执行AuthorityAspect类的beforeAdvice方法
             AuthorityAspect类会在后面创建
*/

@Target({ElementType.METHOD, ElementType.TYPE}) // 该注解可以作用于类和方法
@Retention(RetentionPolicy.RUNTIME) // 运行时可以检测到
public @interface ExecuteAspectBefore {
    // 被这个注解标识的方法 会先执行AuthorityAspect类类的beforeAdvice方法 然后再执行自己
}

🍭创建AuthorityAspect切面类

🎯@Before("@annotation(executeAspectBefore)") 使用注解 @annotation(executeAspectBefore)的方式替换原来的切面表达式 executeAspectBeforebeforeAdvice方法的形参ExecuteAspectBefore executeAspectBefore名保持一致

@ExecuteAspectBefore 注解修饰的方法执行前回先执行beforeAdvice方法

package bbm.com;

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

import java.util.Arrays;

/**
@author Liu Xianmeng
@createTime 2023/9/17 17:10
@instruction 权限验证切面类
*/

@Aspect
@Component
public class AuthorityAspect {
    @Before("@annotation(executeAspectBefore)") // 🎯使用注解的方式替换切面表达式的方式
    public void beforeAdvice(JoinPoint joinPoint, ExecuteAspectBefore executeAspectBefore) {
        System.out.println("C AuthorityAspect M beforeAdvice()执行权限验证 -> " + joinPoint.getSignature().getName());
        // 获取swim()方法的名字
        String methodName = joinPoint.getSignature().getName();
        // 获取swim()方法的参数
        Object[] args = joinPoint.getArgs();
        // 打印获取的信息
        System.out.println("C AuthorityAspect M beforeAdvice()执行权限验证 -> 切入的方法名:" + methodName);
        System.out.println("C AuthorityAspect M beforeAdvice()执行权限验证 -> 切入的方法的参数列表:" + Arrays.toString(args));
    }
}

🍭修改Duck类 添加新的方法fly(..)

public class Duck {
    // ... 前面的代码省略

    /**
     * 给鸭子加一个fly的方法
     * @ExecuteAspectBefore注解修饰这个方法
     * @param tool 鸭子飞起来所使用的的工具 比如火箭
     */
    @ExecuteAspectBefore
    public void fly(String tool) {
        System.out.println("C Duck M fly() -> " + this.name + " is flying! useTool = " + tool);
    }
}

🍭修改Controller类添加新的方法

@RequestMapping("/fly")
public String invokeFly() {
    // 鸭子使用火箭飞
    littleBabyDuck.fly("Rocket");
    // 这个字符串是视图解析的名字 目前没有视图解析器 只关心后台的日志信息
    return "cool"; // 这个字符串不重要
}

🍭启动项目执行测试

启动项目

浏览器测试

查看后台日志

🎄总结

在这一篇中 对AOP的概念做了简单的讲述 然后通过切面表达式和注解两种方式来使用AOP实战编程 在下一篇中 将会分析AOP的执行原理 并编码模拟实现AOP👻

完美✨