前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >日志打印组件踩了Java反射的坑,真是一步一个脚印呢

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

作者头像
烟雨平生
发布2023-09-05 11:29:44
2440
发布2023-09-05 11:29:44
举报
文章被收录于专栏:数字化之路

报错了:

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

"error": "Internal Server Error",

code:

代码语言:javascript
复制
@RestController
public class ReadyController {

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

}

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

本地复现

稳定复现:

代码语言:javascript
复制
Method method = clazz.getMethod(joinPoint.getSignature().getName(), parameterTypes);

看了下上面的code,发现没有使用public修饰符

原因已经搞清了: clazz.getMethod获取的方法没有使用public修饰符时,则会引发NoSuchMethodException异常。 解决办法: 方法1:ready方法增加public修饰符。这样clazz.getMethod就可以找到了

代码语言:javascript
复制
@RestController
public class ReadyController {

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

}

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

代码语言:javascript
复制
    @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作为切入点的筛选条件,可以减少不必要的筛选操作,提升性能。

更改后的请求参数打印效果:

代码语言:javascript
复制
com.tree.thrive.adapter.openapi.ReadyController#ready, Header THE_BIZ_FLAG:null, url : http://localhost/ready cost:0ms

code:

代码语言:javascript
复制
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):如果没有明确指定访问修饰符,则成员具有默认的访问级别,也称为包私有。默认访问级别允许在同一个包中访问,但不能在包外的其他类中访问。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-07-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 的数字化之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档