一 概念

Joint Point

JointPoint是程序运行过程中可识别的点,这个点可以用来作为AOP切入点。JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。

Pointcut

pointcut 是一种程序结构和规则,它用于选取join point并收集这些point的上下文信息。
pointcut通常包含了一系列的Joint Point,我们可以通过pointcut来同时操作jointpoint。单从概念上,可以把Pointcut当做jointpoint的集合。

JointPoint和ProceedingJoinPoint区别

JointPoint
通过JpointPoint对象可以获取到下面信息

# 返回目标对象,即被代理的对象
Object getTarget();

# 返回切入点的参数内容
Object[] getArgs();

# 返回切入点的Signature
Signature getSignature();

# 返回切入的类型,比如method-call,field-get等等,感觉不重要 
 String getKind();

ProceedingJoinPoint
Proceedingjoinpoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。

环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的

暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,,这也是环绕通知和前置、后置通知方法的一个最大区别。这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。

JointPoint使用详解

这里详细介绍JointPoint的方法,这部分很重要是coding核心参考部分。开始之前我们思考一下,我们到底需要获取切入点的那些信息。我的思考如下

切入点的方法名字及其参数
切入点方法标注的注解对象(通过该对象可以获取注解信息)
切入点目标对象(可以通过反射获取对象的类名,属性和方法名)
注:有一点非常重要,Spring的AOP只能支持到方法级别的切入。换句话说,切入点只能是某个方法。

针对以上的需求JDK提供了如下API

1 获取切入点所在目标对象

Object targetObj =joinPoint.getTarget();

# 可以发挥反射的功能获取关于类的任何信息,例如获取类名如下
  String className = joinPoint.getTarget().getClass().getName();
2.获取切入点方法的名字
getSignature());是获取到这样的信息 :修饰符+ 包名+组件名(类名) +方法名

这里我只需要方法名

String methodName = joinPoint.getSignature().getName()
3. 获取方法上的注解
方法1:xxxxxx是注解名字

  Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null)
        {
            xxxxxx annoObj= method.getAnnotation(xxxxxx.class);
        }
        return null;


方法2:上面我们已经知道了方法名和类的对象,通过反射可以获取类的内部任何信息。

 // 切面所在类
        Object target = joinPoint.getTarget();
 
        String methodName = joinPoint.getSignature().getName();
 
        Method method = null;
        for (Method m : target.getClass().getMethods()) {
            if (m.getName().equals(methodName)) {
                method = m;
               //  xxxxxx annoObj= method.getAnnotation(xxxxxx.class);同上
                break;
            }
        }
4. 获取方法的参数
这里返回的是切入点方法的参数列表

Object[] args = joinPoint.getArgs();

实战

logParameter

/**
 * 参数日志记录
 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface LogParameter {
    /**
     * 是否启用
     * @return
     */
    boolean enable() default true;

    /**
     * 启用结果输出
     * @return
     */
    boolean enableResult() default true;

    /**
     * 忽略参数名
     * @return
     */
    String[] ignoreName() default {};

    /**
     * 忽略参数类型
     * @return
     */
    Class[] ignoreType() default {};

    /**
     * 输出登录用户信息
     * @return
     */
    boolean loginUserInfo() default true;

    /**
     * 记录 http 信息
     * 当前记录 uri
     */
    boolean recordHttp() default false;
}

LogParameterAspect

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.springframework.util.StopWatch;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.function.Supplier;

/**
 * 日志记录(log文件)
 */
@Aspect
public class LogParameterAspect {

    private Logger LOG_PARAM = LoggerTool.getLogParam();
    /**
     * 获取登录用户信息的函数
     */
    private Supplier loginUserGetFunc;
    /**
     * 获取Http用户信息的函数
     */
    private Supplier httpInfoGetFunc;

    private static final String SW_ID = "param";
    private static final String SW_ASPECT = "aspect";
    private static final String SW_ASPECT_AFTER = "aspectAfter";
    private static final String SW_PROCEED = "proceed";

    public LogParameterAspect() {
    }
    public LogParameterAspect(Supplier loginUserGetFunc) {
        this.loginUserGetFunc = loginUserGetFunc;
    }

    public LogParameterAspect(Supplier httpInfoGetFunc, Object emtpy) {
        this.httpInfoGetFunc = httpInfoGetFunc;
    }

    public LogParameterAspect(Logger logger) {
        if (logger == null) {
            return;
        }
        LOG_PARAM = logger;
    }

    /**
     * 记录日志 - 方法级别
     * 如果类上也有,以类的为准
     * @param point
     * @param logParamMethod
     * @return
     * @throws Throwable
     */
    @Around("@annotation(logParamMethod)")
    public Object recordParamOfMethod(ProceedingJoinPoint point, LogParameter logParamMethod) throws Throwable {
        return this.handleLogRecord(point, logParamMethod);
    }

    /**
     * 记录日志 - 类级别
     * @param point
     * @param logParamClass
     */
    @Around("@within(logParamClass)")
    public Object recordPram(ProceedingJoinPoint point, LogParameter logParamClass) throws Throwable {
        LogParameter logParamMethod = this.getLogAnnOnMethod(point, point.getTarget().getClass());
        // 如果防范上有此注解,优先使用方法上的
        if (logParamMethod != null) {
            return point.proceed();
        }
        return this.handleLogRecord(point, logParamClass);
    }

    /**
     * 日志记录处理
     * @param point
     * @param logParameter
     * @return
     * @throws Throwable
     */
    private Object handleLogRecord(ProceedingJoinPoint point, LogParameter logParameter) throws Throwable {
        StopWatch stopWatch = new StopWatch(SW_ID);
        stopWatch.start(SW_ASPECT);
        if (!logParameter.enable()) {
            // 关闭参数日志输出
            return point.proceed();
        }
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        List<ParameterInfoVo> paramList = ParameterInfoVo.buildParameter(methodSignature, point.getArgs());
        final String specChar = CharSeparator.COMMA;
        StringBuffer paramStr = new StringBuffer();
        try {
            paramList.parallelStream().forEach(p -> {
                if (this.ignore(p, logParameter)) {
                    return;
                }
                String value = ObjectTool.objectToJsonIgnoreBaseType(p.value);
                paramStr.append(specChar).append(p.name).append(CharSeparator.EQUALS_SIGN).append(value);
            });
            paramStr.delete(0, specChar.length());
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 将登录用户的信息拼接到末尾
        this.joinLoginUserInfoToParam(paramStr, logParameter);
        // 将httpUri信息拼接到末尾
        this.joinHttpInfoToParam(paramStr, logParameter);

        String methodName = StringUtils.join(methodSignature.getDeclaringTypeName(), CharSeparator.WELL, methodSignature.getName());
        Object result = null;
        String exMsg = null;
        stopWatch.stop();
        stopWatch.start(SW_PROCEED);
        try {
            result = point.proceed();
            return result;
        } catch (Throwable throwable) {
            exMsg = throwable.getMessage();
            throw throwable;
        } finally {
            stopWatch.stop();
            logResult(stopWatch, logParameter, paramStr, methodName, result, exMsg);
        }
    }

    /**
     * 结果输出到log日志
     * @param stopWatch 计时器
     * @param logParameter log注解对象
     * @param paramStr 入参信息
     * @param targetMethodName 目标方法
     * @param result 处理结果
     * @param exMsg 异常消息
     */
    private void logResult(StopWatch stopWatch, LogParameter logParameter, StringBuffer paramStr, String targetMethodName, Object result, String exMsg) {
        if (!stopWatch.isRunning()) {
            // 适应异步方法的计时,可获取到等待时长
            stopWatch.start(SW_ASPECT_AFTER);
        }
        String execResult = null;
        if (logParameter.enableResult() && ValidUtil.isNotBlank(result)) {
            if (result instanceof Future) {
                // 对异步调用的支持,异步获取结果
                new Thread(() -> {
                    Object callResult = null;
                    String callExMsg = null;
                    try {
                        callResult = ((Future) result).get();
                    } catch (NullPointerException e) {
                        callExMsg = e.toString();
                    } catch (Exception e) {
                        callExMsg = e.getLocalizedMessage();
                    } finally {
                        this.logResult(stopWatch, logParameter, paramStr, targetMethodName, callResult, callExMsg);
                    }
                }).start();
                return;
            }
            execResult = ObjectTool.objectToJsonString(result);
        }
        stopWatch.stop();
        String swInfo = stopWatch.toString();
        if (ValidUtil.isNotBlank(exMsg)) {
            this.LOG_PARAM.warn("{}->param=={}\t---->ex=={}\t---->costTime=={}", targetMethodName, paramStr, exMsg, swInfo);
        } else if (logParameter.enableResult()) {
            this.LOG_PARAM.info("{}->param=={}\t---->result=={}\t---->costTime=={}", targetMethodName, paramStr, execResult, swInfo);
        } else {
            this.LOG_PARAM.info("{}->param=={}\t---->costTime=={}", targetMethodName, paramStr, swInfo);
        }
    }

    /**
     * 获取方法上的日志注解
     * @param point
     * @param targetClass
     * @return
     * @throws NoSuchMethodException
     */
    private LogParameter getLogAnnOnMethod(ProceedingJoinPoint point, Class<?> targetClass) throws NoSuchMethodException {
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = targetClass.getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
        return targetMethod.getAnnotation(LogParameter.class);
    }

    /**
     * 判断该参数是否需要过滤
     * @param paramInfo
     * @param logParam
     * @return
     */
    private boolean ignore(ParameterInfoVo paramInfo, LogParameter logParam) {
        if (paramInfo == null) {
            return true;
        }
        if (logParam.ignoreName().length > 0) {
            for (String in : logParam.ignoreName()) {
                if (paramInfo.name.equals(in)) {
                    return true;
                }
            }
        }
        if (logParam.ignoreType().length > 0) {
            for (Class ic : logParam.ignoreType()) {
                if (paramInfo.type.equals(ic)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 将登录用户的信息拼接到入参信息中
     * @param param
     * @param logParam
     */
    private void joinLoginUserInfoToParam(StringBuffer param, LogParameter logParam) {
        if (loginUserGetFunc != null && logParam.loginUserInfo()) {
            Object loginUserInfo = loginUserGetFunc.get();
            if (ValidUtil.isBlank(loginUserInfo)) {
                return;
            }
            String info = ObjectTool.objectToJsonString(loginUserInfo);
            param.append(".--loginUserInfo:" + info);
        }
    }

    /**
     * 将http信息拼接到末尾
     * @param param
     * @param logParam
     */
    private void joinHttpInfoToParam(StringBuffer param, LogParameter logParam) {
        if (httpInfoGetFunc != null && logParam.recordHttp()) {
            Object obj = httpInfoGetFunc.get();
            if (obj == null) {
                return;
            }
            Map<String, Object> maps = (Map) obj;
            StringBuffer sb = new StringBuffer();

            maps.forEach((String key, Object value) -> {
                sb.append(key);
                sb.append("=");
                sb.append(value);
                sb.append(",");
            });
            param.append(".--httpInfo:[" + sb.toString() + "]");
        }
    }

    /**
     * 方法参数信息
     */
    private static class ParameterInfoVo {
        private String name;
        private Class type;
        private Object value;

        static List<ParameterInfoVo> buildParameter(MethodSignature methodSignature, Object[] values) {
            String[] names = methodSignature.getParameterNames();
            Class[] types = methodSignature.getParameterTypes();
            List<ParameterInfoVo> paramMap = new ArrayList<>(names.length);
            for (int i = 0; i < names.length; i++) {
                Class type = null;
                Object value = null;
                if (types.length > i) {
                    type = types[i];
                }
                if (values.length > i) {
                    value = values[i];
                }
                ParameterInfoVo param = new ParameterInfoVo();
                param.name = names[i];
                param.type = type;
                param.value = value;
                paramMap.add(param);
            }
            return paramMap;
        }
    }

    public LogParameterAspect setLoginUserGetFunc(Supplier loginUserGetFunc) {
        this.loginUserGetFunc = loginUserGetFunc;
        return this;
    }

    public LogParameterAspect setHttpInfoGetFunc(Supplier httpInfoGetFunc) {
        this.httpInfoGetFunc = httpInfoGetFunc;
        return this;
    }

}