日志打印组件踩了Java反射的坑,真是一步一个脚印原创
-
报错了: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);
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):如果没有明确指定访问修饰符,则成员具有默认的访问级别,也称为包私有。默认访问级别允许在同一个包中访问,但不能在包外的其他类中访问。