diff --git a/continew-starter-json/continew-starter-json-jackson/src/main/java/top/continew/starter/json/jackson/util/JSONUtil.java b/continew-starter-json/continew-starter-json-jackson/src/main/java/top/continew/starter/json/jackson/util/JSONUtil.java new file mode 100644 index 00000000..f1d860b6 --- /dev/null +++ b/continew-starter-json/continew-starter-json-jackson/src/main/java/top/continew/starter/json/jackson/util/JSONUtil.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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,用于环境变量格式解析。 + * + * @param jsonNode 需要转换的 JsonNode + * @return 转换后的 List + */ + public static List jsonToEnvList(JsonNode jsonNode) { + if (jsonNode == null || jsonNode.isNull()) { + return new ArrayList<>(); + } + List 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。 + * + * @param jsonNode 需要转换的 JsonNode + * @return 转换后的 List + */ + public static List 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 fromJson(JsonNode jsonNode, Class 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 parseObject(String str, Class clazz) { + if (StrUtil.isEmpty(str)) { + return null; + } + try { + return OBJECT_MAPPER.readValue(str, clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 字符串 解析为 list + * + * @param str 字符串 + * @param clazz 目标 Java 类 + * @return 解析后的 List + */ + public static List parseArray(String str, Class 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 toBean(String str, Class clazz) { + if (StrUtil.isEmpty(str)) { + return null; + } + try { + return OBJECT_MAPPER.readValue(str, clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/continew-starter-json/continew-starter-json-jackson/src/main/java/top/continew/starter/json/jackson/util/JsonBuilder.java b/continew-starter-json/continew-starter-json-jackson/src/main/java/top/continew/starter/json/jackson/util/JsonBuilder.java new file mode 100644 index 00000000..90065ac3 --- /dev/null +++ b/continew-starter-json/continew-starter-json-jackson/src/main/java/top/continew/starter/json/jackson/util/JsonBuilder.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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); + } + } +} diff --git a/continew-starter-log/continew-starter-log-core/src/main/java/top/continew/starter/log/handler/AbstractLogHandler.java b/continew-starter-log/continew-starter-log-core/src/main/java/top/continew/starter/log/handler/AbstractLogHandler.java index af8598e0..75d982a0 100644 --- a/continew-starter-log/continew-starter-log-core/src/main/java/top/continew/starter/log/handler/AbstractLogHandler.java +++ b/continew-starter-log/continew-starter-log-core/src/main/java/top/continew/starter/log/handler/AbstractLogHandler.java @@ -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(); } diff --git a/continew-starter-log/continew-starter-log-core/src/main/java/top/continew/starter/log/util/AccessLogUtils.java b/continew-starter-log/continew-starter-log-core/src/main/java/top/continew/starter/log/util/AccessLogUtils.java index 19dc366c..48872fb7 100644 --- a/continew-starter-log/continew-starter-log-core/src/main/java/top/continew/starter/log/util/AccessLogUtils.java +++ b/continew-starter-log/continew-starter-log-core/src/main/java/top/continew/starter/log/util/AccessLogUtils.java @@ -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 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> 类型 + * + * @param params 参数 + * @param sensitiveParams 敏感参数列表 + * @return 处理后的参数 + */ + private static Object processSensitiveParams(Object params, List sensitiveParams) { + if (params instanceof Map) { + return filterSensitiveParams((Map)params, sensitiveParams); + } else if (params instanceof List) { + return ((List)params).stream() + .filter(item -> item instanceof Map) + .map(item -> filterSensitiveParams((Map)item, sensitiveParams)) + .collect(Collectors.toList()); + } + return params; + } + /** * 过滤敏感参数 * @@ -106,11 +125,34 @@ public class AccessLogUtils { Map filteredParams = new HashMap<>(params); for (String sensitiveKey : sensitiveParams) { - filteredParams.computeIfPresent(sensitiveKey, (key, value) -> "***"); + if (filteredParams.containsKey(sensitiveKey)) { + filteredParams.put(sensitiveKey, "***"); + } } return filteredParams; } + /** + * 处理超长参数,支持 Map 和 List> 类型 + * + * @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)params, threshold, maxLength, suffix); + } else if (params instanceof List) { + return ((List)params).stream() + .filter(item -> item instanceof Map) + .map(item -> truncateLongParams((Map)item, threshold, maxLength, suffix)) + .collect(Collectors.toList()); + } + return params; + } + /** * 截断超长参数 * diff --git a/continew-starter-web/src/main/java/top/continew/starter/web/util/ServletUtils.java b/continew-starter-web/src/main/java/top/continew/starter/web/util/ServletUtils.java index baf6549c..e436f7a0 100644 --- a/continew-starter-web/src/main/java/top/continew/starter/web/util/ServletUtils.java +++ b/continew-starter-web/src/main/java/top/continew/starter/web/util/ServletUtils.java @@ -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 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); } }