性能文章>日志打印组件踩了Java反射的坑,真是一步一个脚印>

日志打印组件踩了Java反射的坑,真是一步一个脚印原创

11月前
263955
  • 报错了:Internal Server Error

  • 复现

  • 解决

  • 小结

  • 知识点总结


报错了:

有同学反馈,他啥也没做,之前的一个老接口突然报错了,让帮忙看一下

"error": "Internal Server Error",

code:

@RestController
public class ReadyController {

    @GetMapping("/ready")
    String ready() {
        return "success";
    }

}

瞅了一眼代码,没有业务逻辑,用来做心跳检测用的,接口本身没有错。应该别有原因

本地复现

稳定复现:

Method method = clazz.getMethod(joinPoint.getSignature().getName(), parameterTypes);

看了下上面的code,发现没有使用public修饰符
原因已经搞清了:
clazz.getMethod获取的方法没有使用public修饰符时,则会引发NoSuchMethodException异常。


解决办法:
方法1:ready方法增加public修饰符。这样clazz.getMethod就可以找到了
@RestController
public class ReadyController {

    @GetMapping("/ready")
    public String ready() {
        return "success";
    }

}

方法2:
使用能获取默认访问级别的api,譬如clazz.getDeclaredMethod() 

    @Around("pointCut()")
    public Object logParameter(ProceedingJoinPoint joinPoint) throws Throwable {
        Class<?> clazz = joinPoint.getSignature().getDeclaringType();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
        Method method = clazz.getDeclaredMethod(joinPoint.getSignature().getName(), parameterTypes);
        if (!method.isAnnotationPresent(LogBizReqParameters.class)) {
            return joinPoint.proceed();
        }
        。。。
     }

方法3:
使用LogBizReqParameters.class作为切入点的筛选条件。

本次使用方法3。主要考虑到日志打印组件的高性能、作用域的合适性、可靠性。

小结:
出现异常的原因是:
新增了日志打印组件,没有考虑到SpringMVC实际上是支持非public的方法提供api接口,直接使用了clazz.getMethod() 来获取类引了NoSuchMethodException异常。
直接使用LogBizReqParameters.class作为切入点的筛选条件,可以减少不必要的筛选操作,提升性能。
更改后的请求参数打印效果:
com.tree.thrive.adapter.openapi.ReadyController#ready, Header THE_BIZ_FLAG:null, url : http://localhost/ready cost:0ms

code:

package com.tree.thrive.adapter.openapi.config.log;

import com.tree.thrive.adapter.openapi.config.log.annoation.LogBizReqParameters;
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.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.Errors;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Objects;

@Aspect
@Slf4j
@Configuration
public class LogParameterAspectV2 {

    @Value(("${custom.header.name:THE_BIZ_FLAG}"))
    private String customHeaderName;

    /**
     * 定义一个切入点:
     * 满足下面条件的,会走到特定逻辑:
     */
    @Around("@annotation(logBizReqParameters)")
    public Object logParameter(ProceedingJoinPoint joinPoint, LogBizReqParameters logBizReqParameters) throws Throwable {
        StringBuilder logResult = new StringBuilder();
        appendMethodParams(joinPoint, logResult);
        appendRequestParams(logResult);
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            logResult.append(" cost:").append(System.currentTimeMillis() - start).append("ms");
            log.info("{}", logResult);
        }
    }

    private void appendMethodParams(ProceedingJoinPoint joinPoint, StringBuilder logResult) {
        /**
         * 获取参数值
         */
        Object[] args = joinPoint.getArgs();
        /**
         * 获取到方法签名
         */
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        logResult.append(methodSignature.getDeclaringType().getName()).append("#").append(methodSignature.getName());
        /**
         * 获取方法参数名称
         */
        String[] parameterNames = methodSignature.getParameterNames();
        if (Objects.nonNull(parameterNames) && Objects.nonNull(args)
                && parameterNames.length > 0 && args.length > 0
                && parameterNames.length == args.length) {
            for (int i = 0; i < parameterNames.length; i++) {
                Object arg = args[i];
                if (Objects.nonNull(arg) &&
                        (arg instanceof ServletRequest ||
                                arg instanceof ServletResponse ||
                                arg instanceof HttpSession ||
                                arg instanceof Errors ||
                                arg instanceof WebRequest ||
                                arg instanceof MultipartFile
                        )) {
                    continue;
                }
                logResult.append(",").append("parameterNames[i]").append(":").append(arg);
            }
        }
    }

    private void appendRequestParams(StringBuilder logResult) {
        try {
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            if (Objects.nonNull(requestAttributes)) {
                HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
                String theBizFlagVal = request.getHeader(customHeaderName);
                logResult.append(", Header ").append(customHeaderName).append(":").append(theBizFlagVal);
                logResult.append(", url : ").append(request.getRequestURL());
            }
        } catch (Exception ignored) {
        }
    }


}

https://gitee.com/baidumap/top-tree/blob/master/src/main/java/com/tree/thrive/adapter/openapi/config/log/LogParameterAspectV2.java

补充:

clazz.getMethod() 和 clazz.getDeclaredMethod()的区别:

Java的访问修饰符:

在Java中,有四种访问修饰符可以应用于类的成员(字段、方法和内部类):public、protected、private和默认/包私有(package-private)。

public:被声明为public的成员可以从任何地方都能够访问,无论是同一个类、同一个包还是不同的包。

protected:被声明为protected的成员可以在同一个类、同一个包以及继承该类的子类中访问。对于不在同一个包中的其他类,只能通过继承该类来访问protected成员。

private:被声明为private的成员只能在同一个类中访问,无法被其他类或子类直接访问。

默认/包私有(package-private):如果没有明确指定访问修饰符,则成员具有默认的访问级别,也称为包私有。默认访问级别允许在同一个包中访问,但不能在包外的其他类中访问。

 

点赞收藏
bit01
请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
5
5