关于SpringAop中@within的使用踩坑指南

Posted u012663412

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于SpringAop中@within的使用踩坑指南相关的知识,希望对你有一定的参考价值。

最近收到一个需求,需要采集下各个业务系统的日志,实现一个统一的SDK(语言Java),别的部门引用之后,就可以自动进行采集。我采用的是Spring aop自定义注解来实现的,网上的代码抄了一份。然后思考怎么可以在类上加或者是方法上加都可以采集。实现方法上加注解采集日志花了1个小时的时间,而集成给类上加注解采集日志和实现方法加注解采集日志花费了2天的时间,由此可见,扩展一个小的功能点,如果没有现成的方法,确实比较花时间。期间感谢群里大佬@小蛮同学的指点。

代码呈上,删除了部分采集的数据信息,发送到kafka的也删除了,剩余如下:
其中切面
@Pointcut("@within(com.yto.logcollect.annotation.LogAspect) || @annotation(com.yto.logcollect.annotation.LogAspect)")
这里使用@within是可以通过aop拦截类上的注解。而@annotation是通过aop拦截方法上的注解。
附上官网对@within的定义
@within: Limits matching to join points within types that have the given annotation (the execution of methods declared in types with the given annotation when using Spring AOP).
这里有一个坑,就是用了@within之后,不能通过表达式来获取注解,如下:

@Around(value = ("logRecord() && @annotation(logAspect)"))
public Object doAroundAdvice(ProceedingJoinPoint joinPoint, LogAspect logAspect) {

这样死活都不会进入。试验了无数次,发现删除参数注解之后就可以了。
但是这样又存在问题,就是无法获取到注解中填写的值。这个就比较简单了,通过反射去获取就可以了。getClassAnnotation()可以完美拿到。问题解决。
但是在测试的时候又发现,定义的HttpServletRequest, HttpServletResponse这2个参数类型是无法被json转换的,因为里边可能会存在无限嵌套(测试框架用到了SpringSecurity)的问题。首次写的代码特别麻烦,虽然也成功过滤了,这个时候小蛮大佬又指了一条明路,通过lambda来过滤掉HttpServletRequest和HttpServletResponse来解决这个问题,代码变的整洁,观赏性也高了,更易于理解了。
如有兴趣可以共同学习探索!QQ:303579750

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.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Aspect
@Slf4j
public class LogPointCut {

    @Autowired
    private ExchangePrincipleToUserCodeService principleService;

    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    @Pointcut("@within(com.yto.logcollect.annotation.LogAspect) || @annotation(com.yto.logcollect.annotation.LogAspect)")
    public void logRecord() {
    }

    @Around(value = ("logRecord()"))
    public Object doAroundAdvice(ProceedingJoinPoint joinPoint) {
        LogAspect logAspect;
        Object proceedResult = null;
        Log logInfo = new Log();
        HttpServletRequest request;
        try {
            logAspect = getTargetAnnotation(joinPoint);
            request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            logInfo = collectDataBeforeProceed(joinPoint, logInfo, request, logAspect);
            proceedResult = joinPoint.proceed();
            logInfo = collectDataAfterProceed(proceedResult, logInfo);
            if (!ObjectUtils.isEmpty(logAspect.topic())) {
                sendDataToKafkaTopic(logAspect.topic(), logInfo);
            }
        } catch (Exception e) {
            log.error("统计日志发生异常,原因为:{}" + e.getMessage(), e);
        } catch (Throwable throwable) {
            log.error("统计日志发生异常,原因为:" + throwable.getMessage(), throwable);
        }
        return proceedResult;
    }


    /**
     * include TYPE Annotation and METHOD Annotation
     * target = TYPE or METHOD
     * 先去获取Class上的Annotation,如果Class上的Annotation为空,则去获取Method上的Annotation
     *
     * @param joinPoint
     * @return
     */
    private LogAspect getTargetAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        LogAspect annotation = getClassAnnotation(joinPoint);
        if (ObjectUtils.isEmpty(annotation)) {
            annotation = getMethodAnnotation(joinPoint);
        }
        return annotation;
    }

    private LogAspect getMethodAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        Method method = getTargetMethod(joinPoint);
        return method.getAnnotation(LogAspect.class);
    }

    private Method getTargetMethod(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        return methodSignature.getMethod();
    }

    private LogAspect getClassAnnotation(ProceedingJoinPoint joinPoint) {
        return joinPoint.getTarget().getClass().getAnnotation(LogAspect.class);
    }

    private Log collectDataBeforeProceed(ProceedingJoinPoint joinPoint, Log log,
                                         HttpServletRequest request, LogAspect logAspect) throws Throwable {
        log.setId(UUID.randomUUID().toString().replaceAll("-", ""));
        log.setRequestTime(System.currentTimeMillis());
        log.setClientUserAgent(request.getHeader("User-Agent"));
        log.setServerName(InetAddress.getLocalHost().getHostName());
        log.setProtocol(request.getProtocol());
        log.setClientPort(String.valueOf(request.getRemotePort()));
        log.setMethod(request.getMethod());
        log.setActionMethod(joinPoint.getSignature().getName());
        log.setClientAddr(getIp(request));
        log.setInstancePort(String.valueOf(request.getLocalPort()));
        log.setParams(getRequestParams(joinPoint));
        log.setUrl(request.getRequestURL().toString());
        log.setActionClass(joinPoint.getTarget().getClass().getName());
        log.setSendSize(Long.valueOf(request.getContentLength()));
        log.setServerAddr(InetAddress.getLocalHost().getHostAddress());
        log.setSessionId(request.getSession().getId());
        return log;
    }

    /**
     * 如果参数中有request和response的话,会出现递归调用的issue
     * 此方法目的是去掉request和response参数
     *
     * @param joinPoint
     * @return
     */
    private String getRequestParams(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        List<Object> argsExcludeRequestAndResponse = Arrays.stream(args).filter(a -> !(a instanceof HttpServletRequest) && !(a instanceof HttpServletResponse)).collect(Collectors.toList());
        return argsExcludeRequestAndResponse.size() == 0 ? "" : JacksonUtil.to(argsExcludeRequestAndResponse);
    }

    /**
     * 获取请求ip
     */
    protected static String getIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    private Log collectDataAfterProceed(Object proceedResult, Log logInfo) {
        logInfo.setResponseTime(System.currentTimeMillis());
        logInfo.setActionResult(JacksonUtil.to(proceedResult));
        logInfo.setReceiveSize(Long.valueOf(logInfo.getActionResult().getBytes().length));
        return logInfo;
    }
}


以上是关于关于SpringAop中@within的使用踩坑指南的主要内容,如果未能解决你的问题,请参考以下文章

Shell 脚本踩坑指北

Spring之AOP【二】

SpringAOP切入点的表达式

Flutter踩坑记: There are multiple heroes that share the same tag within a subtree.

aop中Pointcut切入点指示符

10. Spring AOP源码解析