每一年都奔走在自己热爱里 没有人是一座孤岛,总有谁爱着你

岁
万
月
12

Spring AOP的使用

项目地址:https://gitee.com/libeimaningg/beizhilib

一、什么是Spring AOP

AOP(Aspect Oriented Programming)即面向切面编程,是对某一类事件的集中处理。面向切面编程(AOP)是一种编程范式,旨在将横切关注点(如日志记录、事务管理、权限验证等)从业务逻辑代码中分离出来,从而提高代码的可重用性、可维护性和模块性。Spring AOP 通过代理模式在运行时动态地将横切关注点织入到目标对象中。

AOP 的组成:切面(Aspect)、切点(Pointcut)、通知(Advice)。AOP 的主要核心概念有:

  1. 切面(Aspect):
  • 定义:切面是一个模块,它封装了横切关注点,包含了定义切点和通知的代码。
  • 作用:将横切关注点模块化,便于复用和管理
  1. 连接点(Join Point):
  • 定义:程序执行过程中能够插入切面的点,如方法调用、异常抛出等。
  • 在Spring AOP中:连接点通常是方法的调用。
  1. 切点(Pointcut):
  • 定义:切点定义了一组连接点,用于确定通知应该在哪些连接点上执行。
  • 表示方式:通常使用表达式来匹配连接点,如AspectJ表达式。
  1. 通知(Advice):
  • 定义:通知是切面在某个连接点执行的动作,它包含了在连接点执行的代码。

类型:

  1. 前置通知(Before Advice)
  • 作用:在目标方法执行之前执行。可以用于执行权限检查、准备资源等操作。
  • 示例:在执行业务方法之前,检查用户是否具有执行该方法的权限。
  1. 后置通知(After Advice)
  • 作用:在目标方法执行之后执行,无论目标方法是否抛出异常。可以用于清理资源、记录日志等操作。
  • 示例:在业务方法执行后,记录方法执行的时间和结果。
  1. 返回通知(After Returning Advice)
  • 作用:在目标方法正常返回后执行。可以用于处理返回值、记录日志等操作。
  • 示例:在业务方法成功返回后,对返回值进行格式化处理。
  1. 异常通知(After Throwing Advice)
  • 作用:在目标方法抛出异常后执行。可以用于处理异常、记录错误日志等操作。
  • 示例:在业务方法抛出异常时,捕获异常并记录到错误日志中。
  1. 环绕通知(Around Advice)
  • 作用:在目标方法执行前后执行。可以在方法执行前进行前置处理,在方法执行后进行后置处理,甚至可以控制目标方法是否执行。
  • 示例:在业务方法执行前开启事务,在方法执行后提交或回滚事务

二、Spring AOP的实现

(1)添加依赖

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

启动类上需添加注解:@EnableAspectJAutoProxy

(2)自定义注解(可选)、定义切面切点

自定义注解:
@Target标识注解可以使用的范围,例如使用在方法、字段、构造方法上。
ElementType各个枚举值的作用范围如下:

①ElementType.TYPE:允许被修饰的注解作用在:类、接口、枚举上;

②ElementType.FIELD:允许被修饰的注解作用在:属性字段上;

③ElementType.METHOD:允许被修饰的注解作用在:方法上;

④ElementType.PARAMETER:允许被修饰的注解作用在:方法参数上;

⑤ElementType.CONSTRUCTOR:允许被修饰的注解作用在:构造器上;

⑥ElementType.LOCAL_VARIABLE:允许被修饰的注解作用在:本地局部变量上;

⑦ElementType.ANNOTATION_TYPE:允许被修饰的注解作用在:注解上;

⑧ElementType.PACKAGE:允许被修饰的注解作用在:包名上;

⑨ElementType.TYPE_PARAMETER:允许被修饰的注解作用在:类型参数上;

⑩ElementType.TYPE_USE:允许被修饰的注解作用在:任何语句中(声明语句、泛型、强制转化)


@Retention标识注解的生命周期
①RetentionPolicy.SOURCE:编译阶段丢弃,编译之后注解没有任何作用,不会写入字节码文件中。例如@Override、@SuppressWarnings、@Deprecated都属于这类注解;

②RetentionPolicy.CLASS:类加载阶段丢弃,类加载进jvm后没有任何作用,在字节码文件处理中有用。注解默认使用这种方式;

③RetentionPolicy.RUNTIME:始终不会丢弃,程序运行期也保留此注解,自定义注解通常使用这种方式,因此可以通过反射获取到注解配置的属性值。

package org.yaoyao.yaoyaocommon.aop;

import java.lang.annotation.*;

/**
 * TODO 类作用描述
 *
 * @author Bryson
 * @date 2025/3/13
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcludeFromAop {
    String module()  default "";//模块
    String method()  default "";//方法
    String operateType() default "OTHER" ;//事件类型:LOGIN;LOGINOUT;ADD;DELETE;UPDATE;SELETE;UPLOAD;DOWNLOAD;OTHER
    String logType() default "0";//日志类型:0:系统日志;1:业务日志
}

自定义注解可用于方法上,如:

/**
     * pagehelper分页插件测试
     *
     * @param user
     * @return
     */
    @PostMapping("/list")
    @ExcludeFromAop(module = "用户", method = "list", operateType = "select", logType = "1")
    public RestResult<PageInfo<SysUser>> list(@RequestBody UserDto user) {
        return new RestResult<>(RestResult.SUCCESS_CODE, "查询成功", sysUserService.selectPage(user));
    }

定义切面切点

package org.yaoyao.yaoyaocommon.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.yaoyao.yaoyaocommon.core.domain.RestResult;
import org.yaoyao.yaoyaocommon.enums.BusinessEnums;
import org.yaoyao.yaoyaocommon.enums.StatusEnums;
import org.yaoyao.yaoyaocommon.exception.BusinessException;

import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;

/**
 * TODO 类作用描述
 *
 * @author Bryson
 * @date 2025/3/13
 */
@Aspect
@Component
@Slf4j
@Service
public class PermissionCheckAspect {

    private ThreadLocal<Long> startTime = new ThreadLocal<>();

    @Pointcut("execution(* org.yaoyao.yaoyaoweb.controller.system.*.*(..))")
    public void test() {
    }

//    @Around("execution(* org.yaoyao.yaoyaoweb.controller.system.*.*(..))")
    @Around("test()")
    public RestResult permissionCheck(ProceedingJoinPoint pjp) throws Throwable {
        Class<?> clazz = pjp.getTarget().getClass();
        String className = clazz.getName();// 获取类名
        String methodName = pjp.getSignature().getName();// 获取方法名
        log.info("类名:{}", className);
        log.info("方法名:{}", methodName);


        //获取传入目标方法的参数
        List<Object> args = Arrays.asList(pjp.getArgs());
        List<String> argNames = Arrays.asList(((MethodSignature) pjp.getSignature()).getParameterNames());
        List<Parameter> parameters = Arrays.asList(((MethodSignature) pjp.getSignature()).getMethod().getParameters());
        log.info("类名:{}", pjp.getSignature().getDeclaringType().getSimpleName());
        log.info("方法名:{}", pjp.getSignature().getName() );
        log.info("参数值:{}", args);
        log.info("参数名:{}", argNames);
        log.info("参数对象:{}", parameters);


        MethodSignature signature = (MethodSignature) pjp.getSignature();
        if (signature.getMethod().isAnnotationPresent(ExcludeFromAop.class)) {
            throw new BusinessException(StatusEnums.FAIL, BusinessEnums.FAIL.getType(), "没有权限");
        } else {
            return (RestResult) pjp.proceed();
        }
    }
}

之后再执行被ExcludeFromAop标记的方法会抛出无权限异常,其他正常执行。


常见的切点表达式:

// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))
 
// 匹配当前包中的指定类的所有方法
execution(* UserService.*(..))
 
// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))
 
// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法
execution(public int com.xys.service.*(..))
 
// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.xys.service.*(String name, ..))
匹配类型签名
 
// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)
 
// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)
 
// 匹配当前包中的指定类中的方法
within(UserService)
 
 
// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)
匹配 Bean 名字
 
// 匹配以指定名字结尾的 Bean 中的所有方法
bean(*Service)
切点表达式组合
 
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)
 
// 匹配名字以 Service 开头, 并且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)
posted @ 2025-03-21 15:07  果咩那塞  阅读(5)  评论(0)    收藏  举报