fix(log/core): 修复访问日志json数组打印

对于请求参数的 json 数组打印和处理的问题修复
适配 json 数组打印处理,json 模块增加 JSONUtil 和
JsonBuilder
This commit is contained in:
吴泽威
2025-04-01 16:44:50 +08:00
parent ca2c88651f
commit 199a83fbea
5 changed files with 529 additions and 22 deletions

View File

@@ -0,0 +1,239 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.json.jackson.util;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* json 工具
*
* @author echo
* @since 2025/03/31
*/
public class JSONUtil {
/**
* 私有构造函数,防止实例化。
*/
private JSONUtil() {
}
/**
* Jackson 对象映射器,用于 JSON 解析与序列化。
*/
private static final ObjectMapper OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class);
/**
* 获取 Jackson 对象映射器。
*
* @return {@link ObjectMapper} Jackson 对象映射器
*/
public static ObjectMapper getObjectMapper() {
return OBJECT_MAPPER;
}
/**
* 对象转为 json 字符串
*
* @param object 对象
* @return {@link String }
*/
public static String toJsonStr(Object object) {
if (ObjectUtil.isNull(object)) {
return null;
}
try {
return OBJECT_MAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 将对象转换为 JsonNode。
*
* @param obj 需要转换的对象
* @return 转换后的 {@link JsonNode},如果 obj 为空,则返回 null
*/
public static JsonNode toJson(Object obj) {
if (obj == null) {
return null;
}
try {
return OBJECT_MAPPER.valueToTree(obj);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e);
}
}
/**
* 将 List 转换为 JsonNode。
*
* @param list 输入的 List
* @return 转换后的 {@link JsonNode}
*/
public static JsonNode listToJson(List<?> list) {
return toJson(list);
}
/**
* 将 Map 转换为 JsonNode。
*
* @param map 输入的 Map
* @return 转换后的 {@link JsonNode}
*/
public static JsonNode mapToJson(Map<?, ?> map) {
return toJson(map);
}
/**
* 将 JsonNode 转换为 List<String>,用于环境变量格式解析。
*
* @param jsonNode 需要转换的 JsonNode
* @return 转换后的 List<String>
*/
public static List<String> jsonToEnvList(JsonNode jsonNode) {
if (jsonNode == null || jsonNode.isNull()) {
return new ArrayList<>();
}
List<String> envList = new ArrayList<>();
jsonNode.fields().forEachRemaining(field -> {
String key = field.getKey();
JsonNode valueNode = field.getValue();
String value = valueNode.isValueNode() ? valueNode.asText() : valueNode.toString();
envList.add(key + "=" + value);
});
return envList;
}
/**
* 将 JsonNode 转换为 List<String>。
*
* @param jsonNode 需要转换的 JsonNode
* @return 转换后的 List<String>
*/
public static List<String> jsonToStringList(JsonNode jsonNode) {
if (jsonNode == null || jsonNode.isNull()) {
return new ArrayList<>();
}
try {
return OBJECT_MAPPER.convertValue(jsonNode, new TypeReference<>() {
});
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e);
}
}
/**
* 将 JsonNode 转换为指定类型的 Java 对象。
*
* @param jsonNode JSON 数据
* @param clazz 目标 Java 类
* @return 解析后的 Java 对象
*/
public static <T> T fromJson(JsonNode jsonNode, Class<T> clazz) {
try {
return OBJECT_MAPPER.treeToValue(jsonNode, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 解析 JSON 字符串为 Java 对象。
*
* @param str JSON 字符串
* @param clazz 目标 Java 类
* @return 解析后的 Java 对象
*/
public static <T> T parseObject(String str, Class<T> clazz) {
if (StrUtil.isEmpty(str)) {
return null;
}
try {
return OBJECT_MAPPER.readValue(str, clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 字符串 解析为 list<T>
*
* @param str 字符串
* @param clazz 目标 Java 类
* @return 解析后的 List<T>
*/
public static <T> List<T> parseArray(String str, Class<T> clazz) {
if (StrUtil.isEmpty(str)) {
return new ArrayList<>();
}
try {
return OBJECT_MAPPER.readValue(str, OBJECT_MAPPER.getTypeFactory()
.constructCollectionType(List.class, clazz));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 判断字符串是否为 JSON 格式。
*
* @param str 字符串
* @return 是否为 JSON 格式
*/
public static boolean isTypeJSON(String str) {
if (StrUtil.isEmpty(str)) {
return false;
}
try {
OBJECT_MAPPER.readTree(str);
return true;
} catch (IOException e) {
return false;
}
}
/**
* 将 JSON 字符串转换为指定类型的 Java 对象。
*
* @param str 字符串
* @param clazz 目标对象的 Class 类型
* @return 解析后的 Java 对象
*/
public static <T> T toBean(String str, Class<T> clazz) {
if (StrUtil.isEmpty(str)) {
return null;
}
try {
return OBJECT_MAPPER.readValue(str, clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.json.jackson.util;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* json 构建工具
*
* @author echo
* @since 2025/03/31
*/
public class JsonBuilder {
private static final ObjectMapper OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class);
private final ObjectNode rootNode;
private JsonBuilder() {
this.rootNode = OBJECT_MAPPER.createObjectNode();
}
/**
* 开始构建
*
* @return {@link JsonBuilder }
*/
public static JsonBuilder builder() {
return new JsonBuilder();
}
/**
* 添加 字符串
*
* @param key key 值
* @param value 值
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, String value) {
Objects.requireNonNull(key, "键不能为 null");
if (value != null) {
rootNode.put(key, value);
}
return this;
}
/**
* 添加 int
*
* @param key key 值
* @param value 值
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, int value) {
Objects.requireNonNull(key, "键不能为 null");
rootNode.put(key, value);
return this;
}
/**
* 添加 long
*
* @param key key 值
* @param value 值
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, long value) {
Objects.requireNonNull(key, "键不能为 null");
rootNode.put(key, value);
return this;
}
/**
* 添加 布尔
*
* @param key key 值
* @param value 值
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, boolean value) {
Objects.requireNonNull(key, "键不能为 null");
rootNode.put(key, value);
return this;
}
/**
* 添加 浮点
*
* @param key key 值
* @param value 值
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, double value) {
Objects.requireNonNull(key, "键不能为 null");
rootNode.put(key, value);
return this;
}
/**
* 添加 json
*
* @param key key 值
* @param value 值
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, JsonNode value) {
Objects.requireNonNull(key, "键不能为 null");
if (value != null) {
rootNode.set(key, value);
}
return this;
}
/**
* 添加 Object
*
* @param key key 值
* @param value 值
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, Object value) {
Objects.requireNonNull(key, "键不能为 null");
if (value != null) {
rootNode.set(key, OBJECT_MAPPER.valueToTree(value));
}
return this;
}
/**
* 添加 List 到 JSON
*
* @param key key 值
* @param list list 参数
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, List<?> list) {
Objects.requireNonNull(key, "键不能为 null");
if (list != null) {
ArrayNode arrayNode = OBJECT_MAPPER.createArrayNode();
for (Object item : list) {
arrayNode.add(OBJECT_MAPPER.valueToTree(item));
}
rootNode.set(key, arrayNode);
}
return this;
}
/**
* 添加 Map 到 JSON
*
* @param key key 值
* @param map map 参数
* @return {@link JsonBuilder }
*/
public JsonBuilder add(String key, Map<?, ?> map) {
Objects.requireNonNull(key, "键不能为 null");
if (map != null) {
ObjectNode objectNode = OBJECT_MAPPER.valueToTree(map);
rootNode.set(key, objectNode);
}
return this;
}
/**
* 构建
*
* @return {@link JsonNode }
*/
public JsonNode build() {
return rootNode;
}
/**
* 构建 json 字符串
*
* @return {@link String }
*/
public String buildString() {
try {
return rootNode.toString();
} catch (Exception e) {
throw new RuntimeException("构建 JSON 字符串失败", e);
}
}
}

View File

@@ -166,14 +166,14 @@ public abstract class AbstractLogHandler implements LogHandler {
AccessLogProperties properties = accessLogContext.getProperties().getAccessLog();
// 是否需要打印 规则: 是否打印开关 或 放行路径
if (!properties.isEnabled() || AccessLogUtils.exclusionPath(accessLogContext.getProperties(), ServletUtils
.getReqPath())) {
.getReqPath())) {
return;
}
// 构建上下文
logContextThread.set(accessLogContext);
String param = AccessLogUtils.getParam(properties);
log.info(param != null ? "[Start] [{}] {} param: {}" : "[Start] [{}] {}", ServletUtils
.getReqMethod(), ServletUtils.getReqPath(), param);
log.info(param != null ? "[Start] [{}] {} param: {}" : "[Start] [{}] {}",
ServletUtils.getReqMethod(), ServletUtils.getReqPath(), param);
}
@Override
@@ -185,7 +185,7 @@ public abstract class AbstractLogHandler implements LogHandler {
try {
Duration timeTaken = Duration.between(logContext.getStartTime(), accessLogContext.getEndTime());
log.info("[End] [{}] {} {} {}ms", ServletUtils.getReqMethod(), ServletUtils.getReqPath(), ServletUtils
.getRespStatus(), timeTaken.toMillis());
.getRespStatus(), timeTaken.toMillis());
} finally {
logContextThread.remove();
}

View File

@@ -17,7 +17,7 @@
package top.continew.starter.log.util;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import top.continew.starter.json.jackson.util.JSONUtil;
import top.continew.starter.log.model.AccessLogProperties;
import top.continew.starter.log.model.LogProperties;
import top.continew.starter.web.util.ServletUtils;
@@ -26,6 +26,7 @@ import top.continew.starter.web.util.SpringWebUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 访问日志工具类
@@ -55,25 +56,24 @@ public class AccessLogUtils {
}
// 参数为空返回空
Map<String, Object> params;
Object params;
try {
params = ServletUtils.getReqParam();
params = ServletUtils.getAccessLogReqParam();
} catch (Exception e) {
return null;
}
if (ObjectUtil.isEmpty(params) || params.isEmpty()) {
if (ObjectUtil.isEmpty(params)) {
return null;
}
// 是否需要对特定入参脱敏
if (properties.isParamSensitive()) {
params = filterSensitiveParams(params, properties.getSensitiveParams());
params = processSensitiveParams(params, properties.getSensitiveParams());
}
// 是否自动截断超长参数值
if (properties.isLongParamTruncate()) {
params = truncateLongParams(params, properties.getLongParamThreshold(), properties
params = processTruncateLongParams(params, properties.getLongParamThreshold(), properties
.getLongParamMaxLength(), properties.getLongParamSuffix());
}
return JSONUtil.toJsonStr(params);
@@ -92,6 +92,25 @@ public class AccessLogUtils {
.anyMatch(resourcePath -> SpringWebUtils.isMatch(path, resourcePath));
}
/**
* 处理敏感参数,支持 Map 和 List<Map<String, Object>> 类型
*
* @param params 参数
* @param sensitiveParams 敏感参数列表
* @return 处理后的参数
*/
private static Object processSensitiveParams(Object params, List<String> sensitiveParams) {
if (params instanceof Map) {
return filterSensitiveParams((Map<String, Object>)params, sensitiveParams);
} else if (params instanceof List) {
return ((List<?>)params).stream()
.filter(item -> item instanceof Map)
.map(item -> filterSensitiveParams((Map<String, Object>)item, sensitiveParams))
.collect(Collectors.toList());
}
return params;
}
/**
* 过滤敏感参数
*
@@ -106,11 +125,34 @@ public class AccessLogUtils {
Map<String, Object> filteredParams = new HashMap<>(params);
for (String sensitiveKey : sensitiveParams) {
filteredParams.computeIfPresent(sensitiveKey, (key, value) -> "***");
if (filteredParams.containsKey(sensitiveKey)) {
filteredParams.put(sensitiveKey, "***");
}
}
return filteredParams;
}
/**
* 处理超长参数,支持 Map 和 List<Map<String, Object>> 类型
*
* @param params 参数
* @param threshold 截断阈值(值长度超过该值才截断)
* @param maxLength 最大长度
* @param suffix 后缀(如 "..."
* @return 处理后的参数
*/
private static Object processTruncateLongParams(Object params, int threshold, int maxLength, String suffix) {
if (params instanceof Map) {
return truncateLongParams((Map<String, Object>)params, threshold, maxLength, suffix);
} else if (params instanceof List) {
return ((List<?>)params).stream()
.filter(item -> item instanceof Map)
.map(item -> truncateLongParams((Map<String, Object>)item, threshold, maxLength, suffix))
.collect(Collectors.toList());
}
return params;
}
/**
* 截断超长参数
*

View File

@@ -21,7 +21,7 @@ import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@@ -30,14 +30,12 @@ import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.UriUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.json.jackson.util.JSONUtil;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.*;
/**
* Servlet 工具类
@@ -58,7 +56,7 @@ public class ServletUtils extends JakartaServletUtil {
public static ServletRequestAttributes getRequestAttributes() {
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes)attributes;
return (ServletRequestAttributes) attributes;
} catch (Exception e) {
return null;
}
@@ -233,8 +231,30 @@ public class ServletUtils extends JakartaServletUtil {
public static Map<String, Object> getReqParam() {
String body = getReqBody();
return CharSequenceUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)
? JSONUtil.toBean(body, Map.class)
: Collections.unmodifiableMap(JakartaServletUtil.getParamMap(Objects.requireNonNull(getRequest())));
? JSONUtil.toBean(body, Map.class)
: Collections.unmodifiableMap(JakartaServletUtil.getParamMap(Objects.requireNonNull(getRequest())));
}
/**
* 获取访问日志请求参数
*
* @return {@link Object }
*/
public static Object getAccessLogReqParam() {
String body = getReqBody();
if (CharSequenceUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)) {
try {
JsonNode jsonNode = JSONUtil.getObjectMapper().readTree(body);
if (jsonNode.isArray()) {
return JSONUtil.toBean(body, List.class);
} else {
return JSONUtil.toBean(body, Map.class);
}
} catch (Exception e) {
return null;
}
}
return Collections.unmodifiableMap(JakartaServletUtil.getParamMap(Objects.requireNonNull(getRequest())));
}
/**
@@ -309,7 +329,7 @@ public class ServletUtils extends JakartaServletUtil {
return new StringBuilder();
}
return new StringBuilder().append(request.getRequestURL())
.append(StringConstants.QUESTION_MARK)
.append(queryString);
.append(StringConstants.QUESTION_MARK)
.append(queryString);
}
}