TommyHu的技术小馆

微信公众号:TommyHu的技术小馆

0%

面向切面编程(AOP)概念及常见应用场景在SpringBoot中的简单实现

概念

面向切面编程(Aspect-Oriented Programming,AOP)是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,以提高代码的模块性和可维护性。横切关注点是指那些多处出现、遍布整个应用程序的功能,例如权限验证、日志记录、事务管理和缓存等。

面向对象编程中,这些横切关注点通常被分散在应用程序的各个模块中,导致它们在整个代码库中重复出现,使得修改和维护变得更加困难。AOP 的目标是通过将这些关注点从主要业务逻辑中抽离出来,使得它们能够被模块化、重用,并且不干扰主要业务逻辑的结构。

AOP 通过使用“切面(Aspect)”的结构来实现这一目标。切面是一个模块化单元,其中包含了横切关注点的代码。AOP 框架会在应用程序执行过程中动态地将这些切面织入到主要业务逻辑中,从而实现横切关注点的功能。

AOP 的一个经典例子是日志记录。传统的面向对象编程可能会在每个方法中加入日志记录的代码,但通过使用 AOP,可以将日志记录功能抽离到一个切面中,然后将这个切面应用到整个应用程序,而不需要在每个方法中手动添加日志记录代码。

SpringBoot中实现AOP

Spring 框架允许开发者以声明式定义切面,将其与主要业务逻辑关联起来。这样,横切关注点的代码与主要业务逻辑的代码保持分离,提高了代码的可读性和可维护性。

在Spring Boot中实现AOP的步骤主要包括:

  1. 引入Spring Boot AOP依赖:pom.xml中添加Spring Boot AOP的依赖。

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  2. 定义一个切面类: 创建一个Java类,该类包含横切关注点的代码。在这个类上使用@Aspect注解来标识它是一个切面类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;

    @Aspect
    @Component
    public class MyAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice() {
    // 在目标方法执行前执行的代码
    System.out.println("Before the method execution");
    }
    }

    @Before注解表示在目标方法执行前执行的代码。execution(* com.example.service.*.*(..))为切入点表达式,指定了切入点的位置。

    其含义依次为:execution():表达式主体,粒度依次为修饰符、包、类、方法;*:返回类型,星号表所有类型; com.example.service:需拦截的包名,若后面多接一个点则表包及子包;.*:类名;.*:方法名;(..)):方法参数,两个点表任何参数。

  3. 配置AOP: 在Spring Boot的主配置类(通常是@SpringBootApplication注解标记的类)上添加@EnableAspectJAutoProxy注解,启用Spring的AOP支持。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;

    @SpringBootApplication
    @EnableAspectJAutoProxy
    public class MyApplication {

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

    @EnableAspectJAutoProxy注解启用了基于AspectJ的自动代理功能。

  4. 运行应用程序: 运行Spring Boot应用程序。在目标方法被调用时,切面中定义的横切关注点的代码将会执行。

除此之外,可通过更复杂的切入点表达式和通知类型来定制AOP的行为。实际项目中可能会使用更复杂的AOP配置来处理不同的场景和需求。具体参考SpringBoot官方文档。

权限控制示例

  1. 定义权限切面类: 创建一个切面类,用于处理权限控制逻辑。该类应使用@Aspect注解标识,并包含与权限相关的通知方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;

    @Aspect
    @Component
    public class SecurityAspect {

    @Before("@annotation(com.example.annotation.RequirePermission)")
    public void checkPermission(JoinPoint joinPoint) {
    // 在目标方法执行前进行权限检查
    // 这里可以获取方法上的注解信息,判断用户是否有权限执行该方法
    // 具体的权限检查逻辑根据需求实现
    System.out.println("Checking permission before method execution...");
    }
    }
  2. 定义权限注解: 创建一个自定义注解,用于标识需要进行权限检查的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequirePermission {
    // 可以在注解中定义一些额外的信息,用于权限判断
    String value() default "";
    }
  3. 在Service层或Controller层中使用权限注解: 在需要进行权限控制的方法上使用自定义的权限注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import org.springframework.stereotype.Service;

    @Service
    public class MyService {

    @RequirePermission("admin")
    public void performAdminAction() {
    // 该方法需要进行权限控制
    System.out.println("Performing admin action...");
    }

    public void performNonAdminAction() {
    // 该方法无需权限控制
    System.out.println("Performing non-admin action...");
    }
    }
  4. 配置AOP和扫描路径: 在Spring Boot的配置类中启用AOP,并确保AOP切面所在的包被正确扫描。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;

    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan(basePackages = "com.example.aspect")
    public class AppConfig {
    // 配置类,启用AOP,并扫描切面所在的包
    }

上述配置中,@RequirePermission注解用于标识需要进行权限控制的方法,而SecurityAspect切面类包含了实际的权限检查逻辑。在目标方法执行前,AOP将调用checkPermission方法进行权限检查。

日志记录示例

可通过切面在方法执行前、执行后或抛出异常时记录相应的日志:

  1. 创建一个日志切面类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;

    import java.util.Arrays;

    @Aspect
    @Component
    public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
    // 在方法执行前记录日志
    System.out.println("Before method: " + joinPoint.getSignature().toShortString());
    System.out.println("Method arguments: " + Arrays.toString(joinPoint.getArgs()));
    }

    @After("execution(* com.example.service.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
    // 在方法执行后记录日志
    System.out.println("After method: " + joinPoint.getSignature().toShortString());
    }
    }

    @Before@After注解分别表示在目标方法执行前和执行后执行日志记录。

  2. 配置AOP和扫描路径:

    在Spring Boot的配置类中启用AOP,并确保AOP切面所在的包被正确扫描。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;

    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan(basePackages = "com.example.aspect")
    public class AppConfig {
    // 配置类,启用AOP,并扫描切面所在的包
    }

    @ComponentScan注解用于指定切面类所在的包,确保Spring容器能够扫描到它。

  3. 应用日志切面:

    在需要记录日志的服务类中,正常编写业务方法即可,无需额外的日志记录代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.springframework.stereotype.Service;

    @Service
    public class MyService {

    public void performAction(String data) {
    // 业务逻辑
    System.out.println("Performing action with data: " + data);
    }
    }

    执行performAction方法时,日志切面会在方法执行前和执行后分别记录日志。

  4. 运行应用程序:

    运行Spring Boot应用程序,观察控制台输出,可看到在方法执行前和执行后的日志记录。

实际项目中可能还需要更复杂的日志记录逻辑,例如记录方法的执行时间、异常信息等。

事务管理示例

Spring Boot中使用AOP实现事务管理通常涉及到@Transactional注解以及@Aspect注解:

  1. 引入Spring Boot事务依赖:pom.xml中添加Spring Boot事务的依赖。

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
  2. 配置数据源和JPA:application.properties文件中配置数据源和JPA相关的属性。以MySQL数据库配置为例:

    1
    2
    3
    4
    5
    6
    spring.datasource.url=jdbc:mysql://localhost:3306/your_database
    spring.datasource.username=your_username
    spring.datasource.password=your_password

    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
    spring.jpa.hibernate.ddl-auto=update
  3. 创建一个Service类: 创建一个Service类,其中的方法需要进行事务管理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    @Service
    public class MyService {

    @Autowired
    private MyRepository myRepository;

    @Transactional
    public void performTransactionalAction() {
    // 业务逻辑
    myRepository.save(new MyEntity("example data"));
    }
    }

    @Transactional注解用于标识performTransactionalAction方法需要进行事务管理。

  4. 创建一个切面类: 创建一个切面类,用于处理事务的开启、提交、回滚等操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;

    import javax.transaction.Transactional;

    @Aspect
    @Component
    public class TransactionAspect {

    @Before("@annotation(transactional)")
    public void beginTransaction(Transactional transactional) {
    System.out.println("Begin transaction...");
    }

    @After("@annotation(transactional)")
    public void commitTransaction(Transactional transactional) {
    System.out.println("Commit transaction...");
    }
    }

    这个切面类通过@Before@After注解分别处理事务的开启和提交。实际项目中需要添加回滚的逻辑以处理异常情况。

  5. 配置AOP和扫描路径: 在Spring Boot的配置类中启用AOP,并确保AOP切面所在的包被正确扫描。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;

    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan(basePackages = "com.example.aspect")
    public class AppConfig {
    // 配置类,启用AOP,并扫描切面所在的包
    }

    @ComponentScan注解用于指定切面类所在的包,确保Spring容器能够扫描到它。

  6. 运行应用程序: 运行Spring Boot应用程序,观察控制台输出,会看到事务的开启和提交日志。

实际项目中常需根据业务需要进行更细粒度的事务管理配置,例如设置事务的隔离级别、传播行为等。Spring Boot提供了丰富的事务管理配置选项,具体配置参考官方文档。

Redis缓存示例

在Spring Boot中使用AOP结合Redis实现缓存,一种常见的方式是使用@Cacheable@CacheEvict注解来定义缓存的行为,而AOP用于处理缓存逻辑。

  1. 引入Spring Boot Redis和AOP依赖:pom.xml中添加Spring Boot Redis和AOP的依赖。

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.x.x</version> <!-- 使用最新版本 -->
    </dependency>
  2. 配置Redis:application.properties文件中配置Redis的连接信息。

    1
    2
    spring.redis.host=localhost
    spring.redis.port=6379
  3. 创建一个Service类: 创建一个Service类,其中的方法需要进行Redis缓存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;

    @Service
    public class MyService {

    @Cacheable(value = "myCache", key = "#key")
    public String getCachedData(String key) {
    // 模拟从数据库或其他耗时操作获取数据
    System.out.println("Fetching data from the source for key: " + key);
    return "Data for key: " + key;
    }
    }

    @Cacheable注解用于标识getCachedData方法需要进行缓存,缓存的名称为”myCache”,并使用key参数作为缓存的键。

  4. 创建一个切面类: 创建一个切面类,用于处理缓存的清除等操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.stereotype.Component;

    @Aspect
    @Component
    public class CacheAspect {

    @Pointcut("@annotation(org.springframework.cache.annotation.CacheEvict)")
    public void cacheEvictPointcut() {}

    @After("cacheEvictPointcut()")
    public void evictCache() {
    System.out.println("Cache evicted...");
    }
    }

    这个切面类通过@CacheEvict注解定义了一个切入点,表示在使用@CacheEvict注解的方法执行后执行清除缓存的操作。

  5. 配置AOP和扫描路径: 在Spring Boot的配置类中启用AOP,并确保AOP切面所在的包被正确扫描。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;

    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan(basePackages = "com.example.aspect")
    public class AppConfig {
    // 配置类,启用AOP,并扫描切面所在的包
    }
  6. 运行应用程序: 运行Spring Boot应用程序,观察控制台输出,会看到缓存的命中和清除日志。

实际项目中可能需要更多的配置选项,例如设置缓存的过期时间、使用不同的缓存名称、配置Redis连接池等。需参考官方文档。

参考