您当前的位置: 首页 >  spring

石头wang

暂无认证

  • 0浏览

    0关注

    295博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

spring AOP使用和注意事项

石头wang 发布时间:2020-02-18 21:48:26 ,浏览量:0

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. 添加依赖


    org.springframework.boot
    spring-boot-starter-aop

  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方法)
     * 

* 注意:仅方法有注解,类上没注解的,不拦截 */ @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 {
    /**
     * 
     * 【建议直接看伪代码更加简单!!!】
     * 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,并继续往上抛出异常。【结束】
     * 
*/ 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},
     * 例如:%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{RequestId}] [%thread] %-5level %logger{36} - %msg%n
     */
    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方法)
     * 

* 注意:仅方法有注解,类上没注解的,不拦截 */ @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 parameters = new LinkedHashMap(); Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); // 将自己写在Controller里的HttpServletRequest和HttpServletResponse等排除掉。常用的是这些,要排除掉以免某些类无法json化导致异常 for (int i = 0; i

关注
打赏
1663722529
查看更多评论
0.0395s