SpringBoot使用AOP简单示例

声明:参考链接https://blog.csdn.net/qq_21033663/article/details/73137207


环境说明:Windows10、IntelliJ IDEA、SpringBoot

准备工作:在pom.xml中引入依赖

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

注:下面的实例代码中还涉及到阿里的fastjson依赖,也需要引入;这里不再给出。

现有Controller:


编写AOP

方式一:通过表达式(模糊)匹配来指定切点。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;
import java.util.Map;


/**
 * AOP切面
 *
 * 以下几个增强的执行顺序是:
 *     1.aroundAdvice(.proceed();语句之前的代码)
 *     2.beforeAdvice
 *     3.被增强方法
 *     4.aroundAdvice(.proceed();语句之后的代码)
 *     5.afterAdvice
 *     6.afterReturningAdvice
 *     7.afterThrowingAdvice(有异常时才会走,无异常不会走此方法)
 *
 *  注: 当被增强方法 或 afterAdvice 或 afterReturningAdvice抛出异常时,会被afterThrowingAdvice
 *      捕获到异常,进而短路走 afterThrowingAdvice方法
 *
 * @author JustryDeng
 * @date 2018/12/17 20:25
 */
@Aspect
@Configuration
public class AopConfig {



    /**
     * 将表达式【* com.aspire.controller.AopController.*(..))】所匹配的所有方法标记为切点,
     * 切点名为 executeAdvice()
     *
     * 注:execution里的表达式所涉及到的类名(除了基本类以外),其它的要用全类名;干脆不管是不
     *    是基础类,都推荐使用全类名
     * 注:如果有多个表达式进行交集或者并集的话,可以使用&&、||、or,示例:
     * @Pointcut(
     *  "execution(* com.szlzcl.laodeduo.common.CommonResponse com.szlzcl.laodeduo.*.controller..*(..)) "
     *      + " || "
     *          + "execution(* com.szlzcl.laodeduo.common.CommonResponse com.szlzcl.laodeduo.config.common..*(..))"
     *  )
     *
     * @author JustryDeng
     * @date 2018/12/18 13:43
     */
     @Pointcut("execution(* com.aspire.controller.AopController.*(..))")
     /**
     * 使用注解来定位AOP作为节点的方法们
     */
    // @Pointcut("@annotation(com.aspire.annotation.AdviceOne)")
    public void executeAdvice() {
    }

    /**
     * 切点executeAdvice()的前置增强方法
     *
     * @author JustryDeng
     * @date 2018/12/18 13:47
     */
    @Before(value = "executeAdvice()")
    public void beforeAdvice(JoinPoint joinPoint) {
        Object[] paramArray = joinPoint.getArgs();
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " -> beforeAdvice获取到了被增强方法的参数了,为:"
                + Arrays.toString(paramArray));
    }

    /**
     * 切点executeAdvice()的后增强方法
     *
     * @author JustryDeng
     * @date 2018/12/18 13:47
     */
    @After("executeAdvice()")
    public void afterAdvice() {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " -> 后置增强afterAdvice执行了");
    }

    /**
     * 切点executeAdvice()的后增强方法
     *
     * 注:当被增强方法 或  afterAdvice正常执行时,才会走此方法
     * 注: returning指明获取到的(环绕增强返回的)返回值
     *
     * @author JustryDeng
     * @date 2018/12/18 13:47
     */
    @AfterReturning(value = "executeAdvice()", returning = "map")
    public void afterReturningAdvice(Map<String, Object> map) {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " -> afterReturningAdvice获得了返回结果 map -> " + map);
    }


    /**
     *  当被增强方法 或 afterAdvice 或 afterReturningAdvice抛出异常时,会被afterThrowingAdvice
     *  捕获到异常,进而短路走 afterThrowingAdvice方法
     *
     * @author JustryDeng
     * @date 2018/12/18 13:57
     */
    @AfterThrowing(value = "executeAdvice()", throwing ="ex")
    public void afterThrowingAdvice(Exception ex) {
        System.out.println("AfterThrowing捕获到了 --->" + ex);
    }


    /**
     *  环绕增强 会在 被增强方法执行完毕后  第一个执行,
     *  所以在绝大多数时候,我们都直接返回thisJoinPoint.proceed();的返回值;
     *  如果此方法返回null,那么@AfterReturning方法获取到的返回值 就会是null
     *
     * @throws Throwable 当目标方法抛出异常时
     *
     * @author JustryDeng
     * @date 2018/12/18 13:57
     */
    @Around("executeAdvice()")
    public Object aroundAdvice(ProceedingJoinPoint thisJoinPoint) throws Throwable {
        String threadName = Thread.currentThread().getName();
        System.err.println(threadName + " -> 环绕增强aroundAdvice --> before proceed()执行了");
            // 执行被增强方法,并获取到返回值
            // 类似于 过滤器的chain.doFilter(req,resp)方法
        Object obj = thisJoinPoint.proceed();
        System.err.println(threadName + " -> 环绕增强aroundAdvice --> after proceed()执行了");
        return obj;
    }

}

测试一下:

以debug模式启动项目,使用postman访问测试;依次放行断点,控制台输出:

注:之所以要以debug方式运行访问,来查看控制台输出;是因为如果直接运行访问的话,可能控制台打印出的就不是真实的
         运行顺序,这是因为CPU会对无关的语句进行重排序。


方式二:通过注解来指定切点。

第一步:自定义一个注解。

注:这是一个开关性的注解,因此不需要任何属性。

第二步:在要被增强的方法上使用该注解。

第三步:定义切点时,使用注解来定义。

注:此时即为:被@annotation(com.aspire.annotation.AdviceOne注解标注的方法,作为切点excudeAdvice()。

注:相比起使用execution来匹配方法,将匹配上的方法作为节点的方式;使用注解@annotation来标记方法的方式更为灵活。

第四步:编写切面具体逻辑。

注:此处示例的切面的逻辑与上面给出的文字版逻辑一样,这里就不再赘述了。

第五步:使用测试(测试方式同上),控制台输出:


由此可见,这几个增强器的执行顺序依次为:


AOP注意事项

  • 确保被AOP的方法所在的Bean已进行了IoC

  • 调用方法的实例,必须是通过Spring注入的,不能是自己new的

  • 被AOP的方法的修饰符最好是public的。因为如果修饰符是protected或默认的,那么只有在同包或者子类中,才能调用到; 而在实际调用时,调用者可能与AOP方法所在的类处于不同的包。即:被protected修饰符(或默认修饰符)修饰的方法,可能导致AOP失效。被private修饰符修饰的方法AOP会失效
    注:如果是自己写的方法调用,那么即便是private修饰符修饰的方法,也可以通过设置Method#setAccessible(true)后,进行调用。

  • A方法直接调用同类下的B方法,不会走B方法的AOP;如果非要内部调用且要求走AOP的话,可以通过AopContext获取到当前代理,然后使用该代理调用B方法


拓展 - 灵活使用且(&&)、或(||)、非(!)定位切点(示例):

现有以下两种注解

  • (定位切点的)注解RecordParameters:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 记录 入参、出参
 *
 * @author JustryDeng
 * @date 2019/12/4 13:53
 */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.METHOD})
public @interface RecordParameters {

    /** 日志级别 */
    LogLevel logLevel() default LogLevel.INFO;

    /** 日志策略 */
    Strategy strategy() default Strategy.INPUT_OUTPUT;

    /**
     * 日志级别枚举
     *
     * P.S. 对于入参、出参日志来讲,debug、info、warn这三种级别就够了
     */
    enum LogLevel {

        /** debug */
        DEBUG,

        /** info */
        INFO,

        /** warn */
        WARN
    }

    /**
     * 日志记录策略枚举
     */
    enum Strategy {

        /** 记录入参 */
        INPUT,

        /** 记录出参 */
        OUTPUT,

        /** 既记录入参,也记录出参 */
        INPUT_OUTPUT
    }
}
  • (忽略切点的)注解IgnoreRecordParameters:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 开关注解 - 忽略RecordParameters注解的功能
 *
 * @author JustryDeng
 * @date 2019/12/4 13:53
 */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface IgnoreRecordParameters {
}

案例一实现当(定位切点的)注解加在类上时,该类下的所有方法都走aop

@Pointcut(
        "@within(com.szlaozicl.actuator.annotation.RecordParameters)"
)
public void executeAdvice() {
}

案例二实现当(定位切点的)注解加在方法上时,该方法走aop

@Pointcut(
        "@annotation(com.szlaozicl.actuator.annotation.RecordParameters)"
)
public void executeAdvice() {
}

案例三实现(定位切点的)注解既可加在类上,又可加在方法上

@Pointcut("@within(com.szlaozicl.actuator.annotation.RecordParameters)"
           + " || "
           + "@annotation(com.szlaozicl.actuator.annotation.RecordParameters)"
)
public void executeAdvice() {
}

案例四实现(定位切点的)注解既可加在类上,又可加在方法上;同时,若某方法上存在另一个自定义的
               标志性注解时,则该方法不会走aop

@Pointcut(
        "("
           + "@within(com.szlaozicl.actuator.annotation.RecordParameters)"
           + " || "
           + "@annotation(com.szlaozicl.actuator.annotation.RecordParameters)"
        + ")"
        + " && "
        + "!@annotation(com.szlaozicl.actuator.annotation.IgnoreRecordParameters)"
)
public void executeAdvice() {
}

案例五注解与表达式一起混合使用

/**
 * 【@within】: 当将注解加在类上时,等同于 在该类下的所有方法上加上了该注解(即:该类的所有方法都会被aop)。
 *              注意:注解必须写在类上,不能写在接口上。
 * 【@annotation】: 当将注解加在某个方法上时,该方法会被aop。
 * 【execution】: 这里:
 *                    第一个*, 匹配所有返回类型
 *                    第二个..*,匹配com.szlaozicl.demo.controller包下的,所有的类(含其子孙包下的类)
 *                    最后的*(..), 匹配任意方法任意参数。
 */
@Pointcut(
        "("
         + "@within(com.szlaozicl.demo.annotation.RecordParameters)"
         + " || "
         + "@annotation(com.szlaozicl.demo.annotation.RecordParameters)"
         + " || "
         + "execution(* com.szlaozicl.demo.controller..*.*(..))"
        + ")"
        + " && "
        + "!@annotation(com.szlaozicl.demo.annotation.IgnoreRecordParameters)"
)
public void executeAdvice() {
}

拓展 - 使用AOP记录入参/出参

提示下面的示例,涉及到的注解,在上一个拓展里有给出。

import com.alibaba.fastjson.JSON;
import com.szlaozicl.demo.annotation.RecordParameters;
import lombok.RequiredArgsConstructor;
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.context.annotation.Configuration;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 方法 入参、出参 记录
 *<p>
 * 注: 根据此AOP的逻辑, 若注解与表达式同时匹配成功,那么 注解的优先级高于表达式的优先级。
 *<p>
 * <b>特别注意:<b/>这里借助了RecordParametersAdvice的logger来记录其它地方的日志。即: 相当于其它地方将记录日志的动
 *                作委托给RecordParametersAdvice的logger来进行, 所以此logger需要能打印所有地方最下的日志级别(一般为debug)。
 *                即:需要在配置文件中配置<code>logging.level.com.szlaozicl.demo.aop.RecordParametersAdvice=debug</code>
 *                   以保证此处有“权限”记录所有用到的日志级别的日志。
 *
 * @author JustryDeng
 * @date 2019/12/4 13:57
 */
@Slf4j
@Order
@Aspect
@Configuration
@RequiredArgsConstructor
public class RecordParametersAdvice {

    /** 栈帧局部变量表参数名侦查器 */
    private static final LocalVariableTableParameterNameDiscoverer PARAMETER_NAME_DISCOVER = new LocalVariableTableParameterNameDiscoverer();

    /** 无返回值 */
    private static final String VOID_STR = void.class.getName();

    /** 判断是否是controller类的后缀 */
    private static final String CONTROLLER_STR = "Controller";

    private final AopSupport aopSupport;

    /**
     * 【@within】: 当将注解加在类上时,等同于 在该类下的所有方法上加上了该注解(即:该类的所有方法都会被aop)。
     *              注意:注解必须写在类上,不能写在接口上。
     * 【@annotation】: 当将注解加在某个方法上时,该方法会被aop。
     * 【execution】: 这里:
     *                    第一个*, 匹配所有返回类型
     *                    第二个..*,匹配com.szlaozicl.demo.controller包下的,所有的类(含其子孙包下的类)
     *                    最后的*(..), 匹配任意方法任意参数。
     */
    @Pointcut(
            "("
             + "@within(com.szlaozicl.demo.annotation.RecordParameters)"
             + " || "
             + "@annotation(com.szlaozicl.demo.annotation.RecordParameters)"
             + " || "
             + "execution(* com.szlaozicl.demo.controller..*.*(..))"
            + ")"
            + " && "
            + "!@annotation(com.szlaozicl.demo.annotation.IgnoreRecordParameters)"
    )
    public void executeAdvice() {
    }

    /**
     * 环绕增强
     */
    @Around("executeAdvice()")
    public Object aroundAdvice(ProceedingJoinPoint thisJoinPoint) throws Throwable {
        // 获取目标Class
        Object targetObj = thisJoinPoint.getTarget();
        Class<?> targetClazz = targetObj.getClass();
        String clazzName = targetClazz.getName();
        // 获取目标method
        MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
        Method targetMethod = methodSignature.getMethod();
        // 获取目标annotation
        RecordParameters annotation = targetMethod.getAnnotation(RecordParameters.class);
        if (annotation == null) {
            annotation = targetClazz.getAnnotation(RecordParameters.class);
            // 如果是通过execution触发的,那么annotation可能为null, 那么给其赋予默认值即可
            if (annotation == null && clazzName.endsWith(CONTROLLER_STR) ) {
                annotation = (RecordParameters) AnnotationUtils.getDefaultValue(RecordParameters.class);
            }
        }
        // 是否需要记录入参、出参
        boolean shouldRecordInputParams;
        boolean shouldRecordOutputParams;
        RecordParameters.LogLevel logLevel;
        boolean isControllerMethod;
        if (annotation != null) {
            shouldRecordInputParams = annotation.strategy() == RecordParameters.Strategy.INPUT
                                      ||
                                      annotation.strategy() == RecordParameters.Strategy.INPUT_OUTPUT;
            shouldRecordOutputParams = annotation.strategy() == RecordParameters.Strategy.OUTPUT
                                       ||
                                       annotation.strategy() == RecordParameters.Strategy.INPUT_OUTPUT;
            logLevel = annotation.logLevel();
            isControllerMethod = clazzName.endsWith(CONTROLLER_STR);
        // 此时,若annotation仍然为null, 那说明是通过execution(* com.szlaozicl.demo.controller.*.*(..)触发切面的
        } else {
            shouldRecordInputParams = shouldRecordOutputParams = true;
            logLevel = RecordParameters.LogLevel.INFO;
            isControllerMethod = true;
        }
        final String classMethodInfo = "Class#Method → " + clazzName + "#" + targetMethod.getName();

        if (shouldRecordInputParams) {
            preHandle(thisJoinPoint, logLevel, targetMethod, classMethodInfo, isControllerMethod);
        }
        Object obj = thisJoinPoint.proceed();
        if (shouldRecordOutputParams) {
            postHandle(logLevel, targetMethod, obj, classMethodInfo, isControllerMethod);

        }
        return obj;
    }

    /**
     * 前处理切面日志
     *
     * @param pjp
     *            目标方法的返回结果
     * @param logLevel
     *            日志级别
     * @param targetMethod
     *            目标方法
     * @param classMethodInfo
     *            目标类#方法
     * @param isControllerMethod
     *            是否是controller类中的方法
     * @date 2020/4/10 18:21:17
     */
    private void preHandle(ProceedingJoinPoint pjp, RecordParameters.LogLevel logLevel,
                           Method targetMethod, String classMethodInfo, boolean isControllerMethod) {
        StringBuilder sb = new StringBuilder(64);
        sb.append("\n【the way in】");
        if (isControllerMethod) {
            sb.append("request-path[").append(aopSupport.getRequestPath()).append("] ");
        }
        sb.append(classMethodInfo);
        Object[] parameterValues = pjp.getArgs();
        if (parameterValues != null && parameterValues.length > 0) {
            String[] parameterNames = PARAMETER_NAME_DISCOVER.getParameterNames(targetMethod);
            if (parameterNames == null) {
                throw new RuntimeException("parameterNames must not be null!");
            }
            sb.append(", with parameters ↓↓");
            int iterationTimes = parameterValues.length;
            for (int i = 0; i < iterationTimes; i++) {
                sb.append("\n\t").append(parameterNames[i]).append(" => ").append(aopSupport.jsonPretty(parameterValues[i]));
                if (i == iterationTimes - 1) {
                    sb.append("\n");
                }
            }
        } else {
            sb.append(", without any parameters");
        }
        aopSupport.log(logLevel, sb.toString());
    }

    /**
     * 后处理切面日志
     *
     * @param logLevel
     *            日志级别
     * @param targetMethod
     *            目标方法
     * @param obj
     *            目标方法的返回结果
     * @param classMethodInfo
     *            目标类#方法
     * @param isControllerMethod
     *            是否是controller类中的方法
     * @date 2020/4/10 18:21:17
     */
    private void postHandle(RecordParameters.LogLevel logLevel, Method targetMethod,
                            Object obj, String classMethodInfo, boolean isControllerMethod) {
        StringBuilder sb = new StringBuilder(64);
        sb.append("\n【the way out】");
        if (isControllerMethod) {
            sb.append("request-path[").append(aopSupport.getRequestPath()).append("] ");
        }
        sb.append(classMethodInfo);
        Class<?> returnClass = targetMethod.getReturnType();
        sb.append("\n\treturn type → ").append(targetMethod.getReturnType());
        if (!VOID_STR.equals(returnClass.getName())) {
            sb.append("\n\treturn result → ").append(aopSupport.jsonPretty(obj));
        }
        sb.append("\n");
        aopSupport.log(logLevel, sb.toString());
    }

    @Component
    static class AopSupport {

        private static Class<?> logClass = log.getClass();

        private static Map<String, Method> methodMap = new ConcurrentHashMap<>(8);

        @PostConstruct
        private void init() throws NoSuchMethodException {
            String debugStr = RecordParameters.LogLevel.DEBUG.name();
            String infoStr = RecordParameters.LogLevel.INFO.name();
            String warnStr = RecordParameters.LogLevel.WARN.name();
            Method debugMethod = logClass.getMethod(debugStr.toLowerCase(), String.class, Object.class);
            Method infoMethod = logClass.getMethod(infoStr.toLowerCase(), String.class, Object.class);
            Method warnMethod = logClass.getMethod(warnStr.toLowerCase(), String.class, Object.class);
            methodMap.put(debugStr, debugMethod);
            methodMap.put(infoStr, infoMethod);
            methodMap.put(warnStr, warnMethod);
        }

        /**
         * 记录日志
         *
         * @param logLevel
         *            要记录的日志的级别
         * @param markerValue
         *            formatter中占位符的值
         * @date 2020/4/11 12:57:21
         */
        private void log(RecordParameters.LogLevel logLevel, Object markerValue){
            try {
                methodMap.get(logLevel.name()).invoke(log, "{}", markerValue);
            } catch (IllegalAccessException|InvocationTargetException e) {
                throw new RuntimeException("RecordParametersAdvice$AopSupport#log occur error!", e);
            }
        }

        /**
         * json格式化输出
         *
         * @param obj
         *         需要格式化的对象
         * @return json字符串
         * @date 2019/12/5 11:03
         */
        String jsonPretty(Object obj) {
            return JSON.toJSONString(obj);
        }

        /**
         * 获取请求path
         *
         * @return  请求的path
         * @date 2020/4/10 17:13:06
         */
        String getRequestPath() {
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            if (requestAttributes == null) {
                log.warn("obtain request-path is empty");
                return "";
            }
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            return request.getRequestURI();
        }
    }

}

拓展 - 采坑提醒

对于环绕增强@Around,不建议try-catch其.proceed()考虑下面这种情况:

  1. .proceed()对应的方法出现了异常A,导致@Around方法返回的obj为null。

  2. 此时@AfterReturning方法受到的返回值就会是null。null打点调用方法,就会出现NullPointerException。

  3. 假设我们有个全局异常处理器:

            从上图可知,由于本来是抛出的异常A,但是在AOP那里发生了异常劫持,抛出了一个空指针异常,这就会导致我们全局异常处理器会走不同的逻辑,进而导致返回给前端的信息不是我们想要的信息。因此,得出以下结论:
           使用环绕增强时,需要根据自己的需要,考虑是否try-catch掉.proceed();一般情况下,不建议try-catch其.proceed()。

 

^_^ 如有不当之处,欢迎指正

^_^ 参考链接:
           
https://blog.csdn.net/qq_21033663/article/details/73137207

^_^ 测试代码托管链接:
           
https://github.com/JustryDeng...AOP...

^_^ 本文已经被收录进《程序员成长笔记(四)》,笔者JustryDeng

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值