Springboot - 6.AOP

概念

当谈论AOP(面向切面编程)时,我们在软件设计中引入了一种编程范式,用于解决关注点分离的问题。关注点分离是将一个应用程序的不同关注点(例如日志记录、事务管理、安全性等)从业务逻辑中分离出来,以便提高代码的模块化和可维护性。

以下是AOP的主要概念和特性:

1. 切面(Aspect):

切面是一个包含横切关注点逻辑的模块。它定义了在何处(切点)以及何时(通知)执行关注点的逻辑。在实际场景中,我们可以创建一个日志记录切面来记录方法的调用和执行时间。

@Aspect
@Component
public class LoggingAspect {

    @Around("execution(* com.example.myapp.service.*.*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Method " + methodName + " executed in " + (endTime - startTime) + "ms");
        return result;
    }
}

在这个示例中,@Aspect注解标记了该类为切面,@Around通知定义了在哪个切点上执行日志记录逻辑。切点表达式execution(* com.example.myapp.service.*.*(..))匹配了com.example.myapp.service包下的所有方法。

2. 切点(Pointcut):

  • 切点定义了哪些方法将会被影响,即在哪些方法上应用切面的通知。在实际应用中,我们可以定义一个切点,用于匹配所有UserService类的方法。

  • 在这个示例中,我们定义了三个不同的切点并应用了不同类型的通知。serviceMethods()切点匹配com.example.myapp.service包中的所有方法。afterRepositoryMethods()切点匹配com.example.myapp.repository包中的所有方法。loggableMethods()切点匹配带有@Loggable注解的方法。

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.example.myapp.service.*.*(..))")
    public void serviceMethods() {}

    @Before("serviceMethods()")
    public void beforeAdvice() {
        System.out.println("Before method execution");
    }

    @After("execution(* com.example.myapp.repository.*.*(..))")
    public void afterRepositoryMethods() {
        System.out.println("After repository method execution");
    }

    @Around("@annotation(com.example.myapp.annotation.Loggable)")
    public Object loggableMethods(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Before method execution");
        Object result = joinPoint.proceed();
        System.out.println("After method execution");
        return result;
    }
}

在这个示例中,@Pointcut注解定义了一个切点,其切点表达式指定了匹配UserService类的所有方法。你可以在通知中引用这个切点来应用切面逻辑。

3. 通知(Advice):

通知是切面在特定切点处执行的代码。在实际场景中,我们可以创建一个记录日志的切面,其中包括在目标方法执行前后记录日志的通知。

@Aspect
@Component
public class LoggingAspect {

    @Before("userServiceMethods()")
    public void beforeAdvice() {
        System.out.println("Before method execution");
    }

    @After("userServiceMethods()")
    public void afterAdvice() {
        System.out.println("After method execution");
    }
}

在这个示例中,@Before@After注解定义了通知,它们分别在切点匹配的方法执行前和执行后执行。userServiceMethods()是之前定义的切点。

4. 引入(Introduction):

引入是一种增强方式,允许在现有的类中添加新的方法和属性。在实际应用中,我们可以为现有的类引入一个新的接口,以添加新的功能,而无需修改现有代码。

@Aspect
@Component
public class IntroductionAspect {

    @DeclareParents(value = "com.example.myapp.service.*+", defaultImpl = AuditableServiceImpl.class)
    private AuditableService auditableService;
}

在这个示例中,我们通过@DeclareParents注解将AuditableService接口引入到com.example.myapp.service包下的所有类中。这使得这些类都具备了AuditableService接口的功能。

AOP的引入(Introduction)和依赖注入

依赖注入(Dependency Injection):

  • 依赖注入是一种设计模式,旨在通过将依赖关系从一个类转移到另一个类,以实现解耦和可测试性。
  • 在依赖注入中,我们将某个类所需的依赖通过构造函数、方法参数或属性注入到该类中,而不是在类内部直接创建它们。
  • 依赖注入的目的是将组件的依赖关系集中管理,以提高模块的灵活性和可替换性。

AOP的引入(Introduction):

  • AOP的引入是一种通过在现有类中添加新接口、方法或属性的方式来增加类的功能,而无需修改现有代码。
  • 引入功能使我们能够在不破坏现有类的情况下,向类中引入新的行为或功能。
  • AOP的引入与依赖注入的目的不同,主要是为了在不修改现有代码的情况下添加新的功能,从而实现关注点的分离。

尽管AOP的引入和依赖注入都涉及在类中添加新功能,但它们的重点和用途是不同的。依赖注入更关注解耦和和可测试性,而AOP的引入更关注在不破坏现有代码的情况下添加新功能。

5. 织入(Weaving):

织入是将切面与应用程序代码结合的过程。在实际应用中,织入将切面的通知插入到切点处,从而实现关注点分离。

@Aspect
@Component
public class LoggingAspect {

    @Around("userServiceMethods()")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Before method execution");
        Object result = joinPoint.proceed();
        System.out.println("After method execution");
        return result;
    }
}

✍6. 连接点(Joinpoint):

  • 连接点是程序执行的某个位置,如方法的调用或异常的抛出。

    示例:

    @AfterReturning(pointcut = "serviceMethods()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        // ...
    }
    

    在这个示例中,logAfterReturning方法的joinPoint参数是一个连接点。

在这个示例中,@Around注解定义了织入的通知。它在目标方法执行前后执行,并将日志记录的逻辑织入到目标方法中。

✍7. 目标对象(Target Object):

  • 目标对象是被一个或多个切面通知的对象。

    示例:

    @Service
    public class ExampleService {
    
        public String doSomething(String name) {
            return "Hello, " + name;
        }
    
    }
    

    在这个示例中,ExampleService是一个目标对象,它被LoggingAspect切面通知。

✍8. AopProxyUtils:

AopProxyUtils类是Spring AOP框架的一个工具类,它提供了一些静态方法,用于处理代理对象和目标对象。

  1. getSingletonTarget(Object candidate):
    这个方法用于获取单例bean的目标对象。

    示例:

    ExampleService targetObject = (ExampleService) AopProxyUtils.getSingletonTarget(exampleService);
    

    在这个示例中,exampleServiceExampleService的代理对象。我们使用AopProxyUtils.getSingletonTarget方法获取exampleService的目标对象。

    注意:这个方法只适用于单例bean。如果bean的作用域不是单例,这个方法将返回null

  2. getTargetClass(Object candidate):
    这个方法用于获取代理对象的目标类。

    示例:

    Class<?> targetClass = AopProxyUtils.getTargetClass(exampleService);
    

    在这个示例中,exampleServiceExampleService的代理对象。我们使用AopProxyUtils.getTargetClass方法获取exampleService的目标类。

    注意:这个方法返回的是目标类,而不是目标对象。

  3. ultimateTargetClass(Object candidate):
    这个方法用于获取代理对象的最终目标类。

    示例:

    Class<?> ultimateTargetClass = AopProxyUtils.ultimateTargetClass(exampleService);
    

    在这个示例中,exampleServiceExampleService的代理对象。我们使用AopProxyUtils.ultimateTargetClass方法获取exampleService的最终目标类。

    注意:这个方法返回的是最终目标类,而不是目标对象。如果代理对象有多层代理,这个方法将返回最终的目标类。

这些是AopProxyUtils类的一些常用方法。这个类还有一些其他方法,但它们通常不需要在应用程序代码中直接使用。

注意:通常我们不需要直接访问目标对象。代理对象会将调用转发到目标对象,并在调用之前或之后执行通知。所以,通常我们应该使用代理对象,而不是目标对象。直接访问目标对象会绕过代理,这意味着切面的通知将不会被执行。

这个类还包含一些其他的方法,但是它们主要用于内部使用,通常不需要在应用程序代码中直接使用。例如,AopProxyUtils.completeProxiedInterfaces方法用于确定给定的代理配置的完整代理接口集,包括从目标类继承的接口。这个方法通常用于在创建代理对象时确定代理接口。

AOP在实际开发中的应用场景包括:

  • 日志记录:记录方法的调用、参数和返回值,以便进行调试和性能监控。
  • 事务管理:在方法调用前后控制事务的开始和提交、回滚。
  • 安全性:实现访问控制、认证和授权等安全相关的功能。
  • 性能优化:在关键方法中添加性能监控和优化逻辑。
  • 缓存管理:在方法中添加缓存逻辑,提高应用程序的响应速度。

当涉及到AOP的实际应用场景时,让我们从五个不同的方面来示例化讲解:

✍1. 日志记录:

示例:在一个Web应用中,记录每个请求的处理时间和请求参数。

@Aspect
@Component
public class LoggingAspect {

    @Around("execution(* com.example.myapp.controller.*.*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Method " + methodName + " executed in " + (endTime - startTime) + "ms");
        return result;
    }
}

✍2. 事务管理:

示例:确保在调用服务方法时,事务在适当的时候启动、提交或回滚。

@Aspect
@Component
public class TransactionAspect {

    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            Object result = joinPoint.proceed();
            transactionManager.commit(transactionStatus);
            return result;
        } catch (Exception ex) {
            transactionManager.rollback(transactionStatus);
            throw ex;
        }
    }
}

✍3. 安全性:

示例:在敏感操作前,检查用户是否有足够的权限执行操作。

@Aspect
@Component
public class SecurityAspect {

    @Before("@annotation(com.example.myapp.annotation.RequiresAdminRole)")
    public void checkAdminRole() {
        if (!currentUserHasAdminRole()) {
            throw new SecurityException("Admin role required");
        }
    }
}

✍4. 性能优化:

示例:在关键方法中添加性能监控和优化逻辑,如数据库查询。

@Aspect
@Component
public class PerformanceAspect {

    @Around("execution(* com.example.myapp.repository.*.*(..))")
    public Object measureQueryPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        if (endTime - startTime > 1000) {
            System.out.println("Query execution took more than 1 second");
        }
        return result;
    }
}

✍5. 缓存管理:

示例:在方法中添加缓存逻辑,提高应用程序的响应速度。

@Aspect
@Component
public class CachingAspect {

    private Map<String, Object> cache = new HashMap<>();

    @Around("execution(* com.example.myapp.service.*.*(..))")
    public Object applyCaching(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        if (cache.containsKey(methodName)) {
            return cache.get(methodName);
        } else {
            Object result = joinPoint.proceed();
            cache.put(methodName, result);
            return result;
        }
    }
}

导入依赖

在Spring Boot应用中使用AOP,你需要导入以下核心依赖以支持AOP功能:

  • spring-boot-starter-aop: 这个starter提供了Spring AOP的支持,使你可以在应用程序中使用AOP功能。

在Maven项目中,你可以将这些依赖添加到pom.xml文件中:

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-aopartifactId>
    dependency>
dependencies>

启用AOP

当涉及到在Spring Boot应用程序中启用AOP时,你可以将@EnableAspectJAutoProxy注解放置在两个位置:配置类上或入口函数所在的主类上。以下是这两种方式的合并输出:

方式1:将@EnableAspectJAutoProxy注解放置在配置类上:

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

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    // 配置类的其他内容
}

方式2:将@EnableAspectJAutoProxy注解放置在入口函数所在的主类上:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy
public class MyAppApplication {

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

无论你选择哪种方式,它们都会启用AspectJ自动代理,使AOP切面能够正常应用于Spring Boot应用程序中的Bean。根据你的偏好和项目结构,选择合适的位置来放置@EnableAspectJAutoProxy注解。

切点表达式

切点表达式(Pointcut Expression)是AOP中一个重要的概念,用于指定在哪些方法上应用通知。切点表达式使用AspectJ的语法,它允许你定义一组匹配方法的规则。这些规则可以基于方法的名称、参数、返回类型等来进行匹配。以下是切点表达式的详细说明和示例:

切点表达式的语法:

切点表达式的基本语法是使用关键字execution,后面跟着方法的返回类型、类名、方法名和参数列表。通配符可以用来匹配不同的部分。

execution([可见性] 返回类型 [类全名.]方法名(参数列表) [异常模式])

其中,方括号内的部分是可选的,具体的匹配规则如下:

  • 可见性:方法的可见性,如publicprivate等。
  • 返回类型:方法的返回类型,使用*通配符表示任意类型。
  • 类全名:类的全名,使用*通配符表示任意包名,省略则表示当前包下的所有类。
  • 方法名:方法的名称,使用*通配符表示任意方法名。
  • 参数列表:方法的参数列表,使用..表示任意数量和类型的参数。
  • 异常模式:方法可能抛出的异常。

切点表达式示例:

假设我们有以下服务类:

package com.example.myapp.service;

@Service
public class UserService {

    public void createUser(String username) {
        System.out.println("User created: " + username);
    }

    public void deleteUser(String username) {
        System.out.println("User deleted: " + username);
    }
}

我们来定义一些切点表达式示例:

✍1. 匹配所有UserService类的方法:

@Pointcut("execution(* com.example.myapp.service.UserService.*(..))")
public void userServiceMethods() {}

✍2. 匹配所有公共方法:

@Pointcut("execution(public * *(..))")
public void publicMethods() {}

✍3. 匹配任何以create开头的方法:

@Pointcut("execution(* create*(..))")
public void createMethods() {}

✍4. 匹配任何返回类型是void的方法:

@Pointcut("execution(void *(..))")
public void voidReturnMethods() {}

✍5. 匹配参数列表包含一个String类型参数的方法:

@Pointcut("execution(* *(String))")
public void stringParameterMethods() {}

✍6. 匹配参数列表包含两个参数的方法,第一个参数是int类型,第二个参数是任意类型:

@Pointcut("execution(* *(int, ..))")
public void intAndAnyParameterMethods() {}

切面注解

✌1. @Aspect:

  • 标记一个类为切面类,其中包含切点和通知。
  • 在类级别上使用。
  • 表示这个类将包含切点和通知。

示例:定义一个用于日志记录的切面类。

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

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.myapp.service.*.*(..))")
    public void beforeAdvice() {
        System.out.println("Before method execution");
    }

    @After("execution(* com.example.myapp.service.*.*(..))")
    public void afterAdvice() {
        System.out.println("After method execution");
    }
}

✌2. @Pointcut:

  • 定义切点,可以在通知中引用。
  • 在切面类中定义一个无返回值的方法,并使用@Pointcut注解。
  • 切点表达式被写入@Pointcut注解的方法体中。

示例:定义一个切点来匹配服务类中的所有方法。

@Pointcut("execution(* com.example.myapp.service.*.*(..))")
public void serviceMethods() {}

✌3. @Before:

  • 在目标方法执行之前执行通知。
  • 通知方法会在切点方法之前被调用。

示例:在调用服务方法之前打印日志。

@Before("serviceMethods()")
public void beforeAdvice() {
    System.out.println("Before method execution");
}

✌4. @After:

  • 在目标方法执行之后(不论是否发生异常)执行通知。
  • 通知方法会在切点方法之后被调用。

示例:在调用服务方法之后打印日志。

@After("serviceMethods()")
public void afterAdvice() {
    System.out.println("After method execution");
}

✌5. @AfterReturning:

  • 在目标方法成功执行之后执行通知。
  • 通知方法会在切点方法成功执行后被调用。

示例:在调用服务方法成功执行后打印日志。

@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void afterReturningAdvice(Object result) {
    System.out.println("Method returned: " + result);
}

✌6. @AfterThrowing:

  • 在目标方法抛出异常后执行通知。
  • 通知方法会在切点方法抛出异常后被调用。

示例:在调用服务方法抛出异常后打印日志。

@AfterThrowing(pointcut = "serviceMethods()", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
    System.out.println("Exception thrown: " + exception.getMessage());
}

✌7. @Around:

  • 包围通知,可以在目标方法执行前后执行自定义逻辑。
  • 通知方法需要接受一个ProceedingJoinPoint参数,通过调用其proceed()方法来执行目标方法。

示例:在调用服务方法前后记录执行时间。

@Around("serviceMethods()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    Object result = joinPoint.proceed();
    long endTime = System.currentTimeMillis();
    System.out.println("Method execution time: " + (endTime - startTime) + "ms");
    return result;
}

✌8. @Order:

  • 控制多个切面的执行顺序。
  • 值越小,优先级越高。

示例:定义切面的执行顺序。

@Aspect
@Component
@Order(1)
public class FirstAspect {
    // ...
}

@Aspect
@Component
@Order(2)
public class SecondAspect {
    // ...
}

使用注解做切点

当谈论AOP中的切面注解时,除了基于切点表达式匹配方法外,还可以使用注解作为切点。这种方式允许你在特定注解被使用时触发通知,从而实现一种基于注解的AOP。以下是涉及切面注解和监听注解的解释和示例:

示例:

假设我们有一个使用了自定义注解@Loggable的服务类:

package com.example.myapp.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Loggable
    public void createUser(String username) {
        System.out.println("User created: " + username);
    }

    @Loggable
    public void deleteUser(String username) {
        System.out.println("User deleted: " + username);
    }
}

我们可以创建一个切面来监听使用了@Loggable注解的方法,并在这些方法执行前后记录日志:

package com.example.myapp.aspect;

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

@Aspect
@Component
public class LoggingAspect {

    @Around("@annotation(com.example.myapp.annotation.Loggable)")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Before method execution");
        Object result = joinPoint.proceed();
        System.out.println("After method execution");
        return result;
    }
}

在上述示例中,我们使用了@Around注解并指定切点表达式为@annotation(com.example.myapp.annotation.Loggable),这将匹配使用了@Loggable注解的方法。在切面的logAround方法中,我们在方法执行前后记录了日志。

你可能感兴趣的:(Springboot-详解,spring,boot,python,后端)