spring AOP使用和注意事项

spring AOP使用和注意事项

本项目研究spring的AOP

官方参考文档

https://docs.spring.io/spring/docs/5.2.3.RELEASE/spring-framework-reference/core.html#aop

项目说明

(本文是一个测试项目的 README.md 文件,如果找不到代码,那就是没有提供代码,懒得上传GitHub)

例子1

  • com.wyf.test.aopspring.aophello.example01.AopTest:说明不能不启动spring容器进行测试
  • com.wyf.test.aopspring.aophello.example01.Guess:猜测了@Before、@After、@AfterReturning、@AfterThrowing、@Around 这些注解的拦截先后顺序,提供了伪代码
  • com.wyf.test.aopspring.aophello.example01.LogAspect01:例子1,说明了Aspect、Pointcut、@Before、@After、@AfterReturning、@AfterThrowing、@Around 的用法,以及这些注解拦截的先后顺序
  • com.wyf.test.aopspring.aophello.example01.LogAspect01SpringTest:是spring test的测试类,启动spring容器并测试

例子2

  • com.wyf.test.aopspring.aophello.example02.LogAspect02:演示了Pointcut的写法。

例子3

  • com.wyf.test.aopspring.aophello.example03.LogAspect03SpringTest:演示public、private、protected、default、static的方法能否得到拦截(除了private和static都可以)

例子4

  • com.wyf.test.aopspring.aophello.example04.LogAspect04SpringTest:演示如果类是final的,是否会被拦截(结果是spring容器无法启动,因为启动时需要产生子类的bean)

例子5

  • com.wyf.test.aopspring.aophello.example05.LogAspect05:演示Pointcut表达式的多种写法

例子99

  • com.wyf.test.aopspring.aophello.example99.ControllerAspect:演示了常用的拦截Controller的入参的aop的写法,详细参考附录

使用AOP步骤

  1. 添加依赖
<!-- AOP,Springboot默认未引入,需要自行引入-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 定义 aspect、pointcut、advice
  • 新建一个 aspect(切面)的java类,即用 @Aspect 注解的类;aspect 类要让 spring 容器能发现,所以要增加 @Component 注解
  • 在 aspect 类里定义 pointcut(切入点),即新增一个方法用 @Pointcut 注解,它是一个定义要拦截的规则可以定义多个@Pointcut
  • 在 aspect 类里定义 advice(通知),即Aspect类里定义怎么拦截的方法(如 @Before、@After、@Around、@AfterReturning、@AfterThrowing 所注释的方法就叫做advice方法

使用AOP中的注意实现和细节

  • @Before、@After、@AfterReturning、@AfterThrowing、@Around 这些注解的拦截先后顺序、异常情况时的细节,参考附录 com.wyf.test.aopspring.aophello.example01.Guess
  • 常见的pointcut的写法,见附录com.wyf.test.aopspring.aophello.example02.LogAspect02
  • 如果目标类是final的(Calculator),是否会被拦截? 不能。结果导致spring容器无法启动,因为启动时需要产生目标类的子类的bean用于动态代理。需要注释掉以免导致其他spring test启动不了。
  • 静态方法无法使用aop? 是的,因为需要动态代理而静态方法。
  • protected、default、private方法能否使用aop?
    protected和default可以,private无法调用,但是通过反射强制调用私有方法,表名是不行的。使用反射调用public、protected、default是可以触发aop拦截的
  • aspect可以写多个Pointcut吗? 可以
  • 我们可能会遇到将Pointcut表达式直接写在advice的注解里(即@Before…),或者让advice的注解引用@Pointcut的方法。参考附录com.wyf.test.aopspring.aophello.example05.LogAspect05

补充:AOP的概念

  • 切面(ASPECT):横切关注点被模块化的特殊对象。即,它是一个类。 ====>即 @Aspect 所注解的类
  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。 ====>即 @Aspect 所注解的类 里, @Before、@After、@Around、@AfterReturning、@AfterThrowing 所注解的方法
  • 目标(Target):被通知对象。 ===>即类似 Calculator02_00 这些类的对象
  • 代理(Proxy):向目标对象应用通知之后创建的对象。 =====> 即 Calculator02_00 这些类,会产生子类作为它的代理对象
  • 切入点(PointCut):切面通知执行的“地点”的定义。 ===>即 @Aspect 所注解的类 里, @Pointcut 所注解的方法
  • 连接点(JointPoint):与切入点匹配的执行点。

注意aop的拦截,跟javax.servlet.Filter或者spring的interceptor是不一样的,它们需要http请求才能拦截,而aop的拦截并不需要aop,只要spring容器启动起来,普通的junit测试的调用即可拦截

附录

附录1:com.wyf.test.aopspring.aophello.example02.LogAspect02
@Aspect
@Component
public class LogAspect02 {

    /**
     * 具体的某个类,的某个方法
     */
    @Pointcut("execution(* com.wyf.test.aopspring.aophello.example02.method.Calculator02_00.div2(..))")
    public void pointCutOneMethod() {
    }

    /**
     * 具体的某个类,的所有方法
     */
    @Pointcut("execution(* com.wyf.test.aopspring.aophello.example02.sub.Calculator02_01.*(..))")
    public void pointCutOneClassAllMethod() {
    }

    /**
     * 某个包下的所有类,不包括子目录,的所有方法
     */
    @Pointcut("execution(* com.wyf.test.aopspring.aophello.example02.sub2.*.*(..))")
    public void pointCutAllClassExceptSubDir() {
    }

    /**
     * 某个包下的所有类,包括子目录,的所有方法
     */
    @Pointcut("execution(* com.wyf.test.aopspring.aophello.example02.sub2..*.*(..))")
    public void pointCutPackageIncludeSubDir() {
    }

    /**
     * 任何目录下的,类上有@RestController或@Controller,方法带有@GetMapping、@PostMapping
     * 、@DeleteMapping、@PutMapping、@RequestMapping任一注解的则拦截(用于拦截Controller方法)
     * <p>
     * 注意:仅方法有注解,类上没注解的,不拦截
     */
    @Pointcut("!execution(* com.wyf.test.aopspring.aophello.example02.HealthCheckController.*(..)) &&" +
            "((@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)) " +
            "&& (@annotation(org.springframework.web.bind.annotation.GetMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)))")
    public void pointCutControllerMethod3() {
    }

    /**
     * 拦截用指定注解的方法(这个注解单独写在类上面,并不会自动实现该类所有方法被拦截)
     */
    @Pointcut("@annotation(com.wyf.test.aopspring.aophello.example02.MyAnnotaion)")
    public void pointCutSomeAnnotation() {

    }


    @Before("pointCutControllerMethod3()")
    public void before(JoinPoint joinPoint) {
        System.out.println("target:" + joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName());
    }

}
附录2:com.wyf.test.aopspring.aophello.example01.Guess
/**
 * 这是一个猜测advice的源码的类,为什么@Before、@After、@AfterReturning、@AfterThrowing、@Around 会有那样的执行顺序? 下面是猜测
 *
 * @author Stone
 * @version V1.0.0
 * @date 2020/2/14
 */
public class Guess {
    /**
     * <pre>
     * 【建议直接看伪代码更加简单!!!】
     * 1、当调用目标方法时,不是直接调用真正的实现,而是其代理方法
     * 2、代理方法最终会调用这里的 entrance 方法
     * 3、调用 @Around 所修饰的方法
     *      3.1 先执行proceed()之前的代码 `around before`
     *      3.2 执行proceed()方法
     *          3.2.1 执行 @Before (有定义@Before的话)
     *          3.2.2 执行实际的目标方法
     *          3.2.3 执行 @After(有的话),无论目标方法是否有抛出异常在finally里,所以一定会执行
     *      3.3 proceed()是否抛出异常
     *          3.3.1 否
     *              3.3.1.1 执行 proceed()之后的代码 `around after`
     *              3.3.1.2 执行 @AfterReturning(有的话)。【结束】
     *          3.3.2 是
     *              3.3.2.1 不执行proceed()之后的代码 `around after`
     *              3.3.2.1 执行 @AfterThrowing,并继续往上抛出异常。【结束】
     * </pre>
     */
    void entrance() {
        boolean isException = false;
        Throwable throwable = null;
        try {
            // 当然不是直接调用,估计是通过反射进行调用,这里为了简化写成直接调用
            around();
        } catch (Throwable t) {
            isException = true;
            throwable = t;
        }

        if (!isException) {
            // 执行 @AfterReturning(有的话)
        } else {
            // 执行 @AfterThrowing(有的话)
            throw new RuntimeException(throwable);
        }
    }

    /**
     * 这是 @Around 所修饰的方法
     */
    void around() {
        System.out.println("around before");
        proceed();
        System.out.println("around after");
    }


    void proceed() {
        // 执行 @Before(有的话)
        try {
            // 执行实际的目标方法
        } finally {
            // 执行 @After(有的话)
        }
    }

}
附录3:com.wyf.test.aopspring.aophello.example05.LogAspect05
@Aspect
@Component
public class LogAspect05 {
    /**
     * 指定具体的某个类,
     */
    @Pointcut("execution(* com.wyf.test.aopspring.aophello.example05.Calculator05_1.*(..))")
    public void pointCut() {
    }


    /**
     * 可以引用@Pointcut的方法,也可以直接将表达式写在字串里
     *
     * @param joinPoint
     */
    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("@Before,target:" + joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName());
    }

    /**
     * 可以引用@Pointcut的方法,也可以直接将表达式写在字串里
     *
     * @param joinPoint
     */
    @After("execution(* com.wyf.test.aopspring.aophello.example05.Calculator05_2.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("@Before,target:" + joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName());
    }
}
附录4:com.wyf.test.aopspring.aophello.example99.ControllerAspect
package com.wyf.test.aopspring.aophello.example99;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 拦截控制器的方法
 *
 * @author Strone
 * @version V1.0.0
 * @date 2020/2/14
 */
@Aspect
@Component
@Slf4j
public class ControllerAspect {
    /**
     * 响应头的名字,值是请求的唯一ID;同时也是日志的全局ID,这么设置:%X{RequestId},
     * 例如:<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{RequestId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
     */
    private static final String REQUEST_ID = "RequestId";

    /**
     * 格式化JSON的工具。线程安全。
     */
    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
        // 如果json字符串中有新增的字段,但实体类中不存在的该字段,不报错。默认true
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 日期格式指定格式
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }


    /**
     * 任何目录下的,类上有@RestController或@Controller,方法带有@GetMapping、@PostMapping
     * 、@DeleteMapping、@PutMapping、@RequestMapping任一注解的则拦截,除HealthCheckController外,除HealthCheckController外(用于拦截Controller方法)
     * <p>
     * 注意:仅方法有注解,类上没注解的,不拦截
     */
    @Pointcut("!execution(* com.wyf.test.aopspring.aophello.example02.HealthCheckController.*(..)) &&" +
            "((@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)) " +
            "&& (@annotation(org.springframework.web.bind.annotation.GetMapping) " +
            "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)))")
    public void pointCutControllerMethod() {
    }

    /**
     * 拦截请求,打印请求的参数,方便DEBUG
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "pointCutControllerMethod()")
    public Object aroundRestApi(ProceedingJoinPoint joinPoint) throws Throwable {
        long beginTime = System.currentTimeMillis();

        // 防止非http请求方式进行调用导致异常
        HttpServletRequest req = null;
        HttpServletResponse resp = null;
        String requestId = null;
        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            req = attributes.getRequest();
            resp = attributes.getResponse();

            // 将该请求ID写入响应头,方便查找到该条日志
            requestId = UUID.randomUUID().toString();
            resp.setHeader(REQUEST_ID, requestId);
        } catch (Exception e) {
        }


        // 请求执行
        boolean isSuccess = true;
        try {
            // 每个请求拥有唯一的请求ID
            MDC.put(REQUEST_ID, requestId);

            // 执行实际方法
            return joinPoint.proceed();
        } catch (Throwable e) {
            isSuccess = false;
            // 需要注意以下@ControllerAdvice或@RestControllerAdvice是否能囊括这里抛出的异常并将其封装
            // (实际测试似乎不需要加@Order默认就能包揽这里抛出的异常
            throw e;
        } finally {
            MDC.clear();

            Long endTime = System.currentTimeMillis();
            log.info("IP:{}, RequestId:{}, Method:{}, ContentType:{}, Url:{}, Param:{}, Time:{}, Status:{}",
                    req == null ? null : req.getRemoteAddr(),
                    requestId,
                    req == null ? null : req.getMethod(),
                    req.getContentType(),
                    req == null ? null : req.getRequestURL(),
                    getRequestParams(joinPoint),
                    // getRequestParams(req),// 这种方式对于application/json,请求体里的参数无法获取
                    endTime - beginTime,
                    isSuccess ? "success" : "fail");

        }
    }


    /**
     * 获取请求参数
     *
     * @param joinPoint 上下文
     * @return 请求参数
     */
    private String getRequestParams(JoinPoint joinPoint) {
        try {
            Object[] args = joinPoint.getArgs();
            Map<String, Object> parameters = new LinkedHashMap<>();
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            // 将自己写在Controller里的HttpServletRequest和HttpServletResponse等排除掉。常用的是这些,要排除掉以免某些类无法json化导致异常
            for (int i = 0; i < method.getParameters().length; i++) {
                if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse) {
                    continue;
                }
                parameters.put(method.getParameters()[i].getName(), args[i]);
            }
            return objectMapper.writeValueAsString(parameters);
        } catch (Exception e) {
            log.error("OBTAIN_PARAM_ERROR", e);
            return "OBTAIN_PARAM_ERROR";
        }
    }

    /**
     * 获取请求参数。弊端:对于application/json,请求体里的参数无法获取。不予采用
     *
     * @param req
     * @return
     */
    @Deprecated
    private String getRequestParams(HttpServletRequest req) {
        try {
            if (req == null) {
                return null;
            }
            return objectMapper.writeValueAsString(req.getParameterMap());
        } catch (Exception e) {
            log.error("OBTAIN_PARAM_ERROR", e);
            return "OBTAIN_PARAM_ERROR";
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值