自定义 Spring AOP 切面实战(鉴权、记录日志)

前言:

从事 Java 的小伙伴都知道 Spring AOP,也都知道 AOP 是面向切面编程,那你知道 AOP 在项目实战中怎么使用吗?本篇简单分享 Spring AOP 在项目中的实际使用。

AOP 知识储备传送门:

深入理解 Spring AOP 源码分析(附源码分析)

AOP 基础知识回顾

AOP 概念

  • 连接点(Join point):能够被拦截的地⽅、地点,Spring AOP 是基于动态代理的,所以是⽅法拦截的,每个成员⽅法都可以称之为连接点。
  • 切点(Poincut):匹配连接点的断言,在AOP中通知和一个切入点表达式关联,切点分为execution方式和annotation方式,前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
  • 通知(Advice):在切面的某个特定的连接点上执行的动作,通知分为:前置通知、后置通知、异常通知、最终通知、环绕通知(切面要完成的功能)。
  • 织⼊(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象,分为:编译时织入、类加载时织入、执行时织入
  • 引⼊/引介(Introduction):允许我们向现有的类添加新⽅法或属性,是⼀种特殊的增强。
  • 切面(Aspect):切⾯由切点和增强、通知组成,它既包括了横切逻辑的定义、也包括了连接点的定义。

通知(Advice)分类

  • 前置通知(Before Advice):在⽬标⽅法被调⽤前的通知功能。
  • 后置通知(After Advice):在⽬标⽅法被调⽤之后的通知功能。
  • 返回通知(After-returning):在⽬标⽅法成功执⾏之后的通知功能。
  • 异常通知(After-throwing):在⽬标⽅法抛出异常之后的通知功能。
  • 环绕通知(Around):把整个⽬标⽅法包裹起来,在被调⽤前和调⽤之后的通知功能。

自定义 AOP 切面常用知识储备

@Aspect 注解的作用?

@Aspect 注解的作用是定义一个切面,需要再自定义切面类上加上次注解。

@Component 注解

@Component 注解表示这个类交给 Spring 容器管理,如果不使用 @Component 注解,也要以其他方法是注册切面类,以确保 Spring 容器能够识别并管理这个切面。

Pointcut 切点中 execution 和 @annotation 定义切面的区别?

  • @annotation:切点表达式是注解的全限类名,是根据注解来匹配的,有注解的方法才会被拦截。

  • execution:切点表达式很灵活,有修饰符匹配、返回值匹配、类路径匹配、方法名匹配、参数匹配、异常类型匹配。

  • 作用于任意以 public 修饰的方法:execution(public * *(…))

  • 作用于任何一个以 set 开始的方法:execution(* set*(…))

  • 作用于 MyService 接口的任意方法:execution(* com.xxx.service.MyService.*

  • 作用于定义在 service 包和所有子包里的任意类的任意方法:execution(* com.xxx.service….(…)),第一个 * 表示匹配任意的方法返回值, …(两个点)表示零个或多个,第一个 … 表示 service 包及其子包,第二个 * 表示所有类,第三个 * 表示所有方法,第二个…表示方法的任意参数个数。

JoinPoint 的作用?

PoinPoint 代表程序中的一个点,通常把 JoinPoint 作为一个参数传递到通知方法中,通过 JoinPoint 来获取程序中这个点的相关信息,JoinPoint 只能用于 @Before、@After、@AfterReturning、@AfterThrowing 通知中,不能使用于 @Around 中,JoinPoint 常用方法如下:

  • Object[] getArgs():返回目标方法的参数。
  • Signature getSignature():返回方法的签名。
  • Object getTarget():返回被增强的目标对象,也就是被代理对象。
  • Object getThis():返回由目标对象生成的代理对象。

ProceedingJoinPoint 的作用?

ProceedingJoinPoint 是 JoinPoint 的子接口,用于环绕通知 @Around 中,它提供了一个 proceed() 方法,用于执行被拦截的方法也就是目标方法。

自定义 AOP 切面代码演示

自定义 AOP 切面实现接口请求日志记录

记录接口请求日志这种业务场景是非常常见的,如果在每个接口中去记录,显得代码臃肿且不灵活,而且有很大的侵入性,我们可以使用自定义 AOP 面来完成这种功能,代码如下:

@Slf4j
@Component
@Aspect
public class MyApiLogAspect implements Ordered {

    @Pointcut("execution(* com.my.study.controller..*(..))")
    public void logPointCut() {

    }

    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
  
        long startTime = System.currentTimeMillis();
        //获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        //记录请求日志信息
        ApiLogVO apiLogDO = new ApiLogVO();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String methodName = method.getName();
        apiLogDO.setMethodName(methodName);
        apiLogDO.setRequestMethod(request.getMethod());
        apiLogDO.setMethodParameters(getParameter(method, joinPoint.getArgs()));
        apiLogDO.setUri(request.getRequestURI());
        //执行目标方法
        Object result = joinPoint.proceed();
        apiLogDO.setResponse(result);
        long endTime = System.currentTimeMillis();
        apiLogDO.setTimeConsuming((endTime - startTime));
        log.info("请求日志信息:{}", JSON.toJSONString(apiLogDO));
        return result;
    }


    /**
     * 根据方法和传入的参数获取请求参数
     */
    private Object getParameter(Method method, Object[] args) {
        List<Object> argList = new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //将RequestBody注解修饰的参数作为请求参数
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            if (requestBody != null) {
                argList.add(args[i]);
            }
            //将RequestParam注解修饰的参数作为请求参数
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                Map<String, Object> map = new HashMap<>();
                String key = parameters[i].getName();
                if (ObjectUtil.isNotEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                map.put(key, args[i]);
                argList.add(map);
            }
        }
        if (argList.size() == 0) {
            return null;
        } else if (argList.size() == 1) {
            return argList.get(0);
        } else {
            return argList;
        }
    }


    @Override
    public int getOrder() {
        return -1;
    }
}


@Data
public class ApiLogVO {

    @ApiModelProperty("方法名称")
    private String methodName;

    @ApiModelProperty("请求方式")
    private String requestMethod;

    @ApiModelProperty("方法路径")
    private String uri;

    @ApiModelProperty("方法耗时")
    private Long timeConsuming;

    @ApiModelProperty("方法参数")
    private Object methodParameters;

    @ApiModelProperty("方法响应")
    private Object response;

}

从代码来看,我们使用了很少的代码量完成了接口请求日志的记录,而且没有入侵业务代码,Pointcut 支持各种灵活配置,可以丰富我们记录各种场景,因为记录了接口出入参及请求时间,因此我们使用的是环绕通知。

自定义 AOP 切面实现接口鉴权

在业务项目开发中,部分接口需要进行权限校验,部分接口不需要进行权限校验,这也是一种非常常见的场景,我们如果在每个接口中去做判断同样显得代码臃肿且入侵业务代码,我们同样可以通过自定义注解 + AOP 切面的方式完成接口鉴权的功能,代码如下:

@Component
@Slf4j
@Aspect
public class MyAuthLogAspect implements Ordered {


    @Resource
    private AuthUtil authUtil;

    @Pointcut("@annotation(com.com.my.study.annotation.RequiresPermissions)")
    public void authPointCut() {

    }

    @Before("authPointCut()")
    public void before(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 注解鉴权 有注解表示需要鉴权
        RequiresPermissions requiresPermissions = signature.getMethod().getAnnotation(RequiresPermissions.class);
        if (requiresPermissions != null) {
            authUtil.checkPermission(requiresPermissions);
        }
    }

    @After("authPointCut()")
    public void after(JoinPoint joinPoint) {
        
    }

    @Override
    public int getOrder() {
        //优先级最高  值越小 越先执行
        return 1;
    }
}

通过代码可以看出通过自定义 AOP 切面完成接口权限校验也是十分简单方便,因为是进行接口权限校验,我们使用前置通知即可完成接口权限校验的功能。

自定义 AOP 切面执行顺序

当项目中有多个切面的时候,我们该如何保证切面的执行顺序?我们可以通过实现 Order 接口来自定义切面的执行顺序,order 的值越小,对应的切面的优先级就越高,也就越先执行。

自定义 AOP 切面的使用场景

  • 接口日志记录。
  • 权限校验。
  • 接口限流。
  • 。。。。。。

欢迎提出建议及对错误的地方指出纠正。

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值