metadata = nacosInstance.getMetadata();
String targetVersion = MapUtil.getStr(metadata, CommonConstants.VERSION);
return reqVersion.equalsIgnoreCase(targetVersion);
- }).toList();
+ }).toList();
// 存在 随机返回
if (CollUtil.isNotEmpty(serviceInstanceList)) {
diff --git a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/IdempotentAutoConfiguration.java b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/IdempotentAutoConfiguration.java
index b8ab16d3e..7ab8e7a4d 100644
--- a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/IdempotentAutoConfiguration.java
+++ b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/IdempotentAutoConfiguration.java
@@ -4,11 +4,17 @@ import com.pig4cloud.pigx.common.idempotent.aspect.IdempotentAspect;
import com.pig4cloud.pigx.common.idempotent.expression.ExpressionResolver;
import com.pig4cloud.pigx.common.idempotent.expression.KeyResolver;
import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.SingleServerConfig;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
+import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.util.StringUtils;
/**
* @author lengleng
@@ -17,7 +23,8 @@ import org.springframework.context.annotation.Configuration;
* 幂等插件初始化
*/
@Configuration(proxyBeanMethods = false)
-@AutoConfigureAfter(RedisAutoConfiguration.class)
+@AutoConfigureAfter(DataRedisAutoConfiguration.class)
+@EnableConfigurationProperties(DataRedisProperties.class)
public class IdempotentAutoConfiguration {
@@ -40,7 +47,31 @@ public class IdempotentAutoConfiguration {
* @return {@link IdempotentAspect }
*/
@Bean
- public IdempotentAspect idempotentAspect(Redisson redisson, KeyResolver keyResolver) {
+ @ConditionalOnMissingBean(RedissonClient.class)
+ public RedissonClient redissonClient(DataRedisProperties redisProperties) {
+ Config config = new Config();
+ SingleServerConfig singleServerConfig = config.useSingleServer()
+ .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort())
+ .setDatabase(redisProperties.getDatabase());
+
+ if (StringUtils.hasText(redisProperties.getUsername())) {
+ singleServerConfig.setUsername(redisProperties.getUsername());
+ }
+ if (StringUtils.hasText(redisProperties.getPassword())) {
+ singleServerConfig.setPassword(redisProperties.getPassword());
+ }
+ if (redisProperties.getConnectTimeout() != null) {
+ singleServerConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
+ }
+ if (redisProperties.getTimeout() != null) {
+ singleServerConfig.setTimeout((int) redisProperties.getTimeout().toMillis());
+ }
+
+ return Redisson.create(config);
+ }
+
+ @Bean
+ public IdempotentAspect idempotentAspect(RedissonClient redisson, KeyResolver keyResolver) {
return new IdempotentAspect(redisson, keyResolver);
}
diff --git a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/aspect/IdempotentAspect.java b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/aspect/IdempotentAspect.java
index 037553c83..dc02646de 100644
--- a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/aspect/IdempotentAspect.java
+++ b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/aspect/IdempotentAspect.java
@@ -11,8 +11,8 @@ import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
-import org.redisson.Redisson;
import org.redisson.api.RMapCache;
+import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
@@ -60,7 +60,7 @@ public class IdempotentAspect {
/** 是否自动删除 Key 标识 */
private static final String DELKEY = "delKey";
- private final Redisson redisson;
+ private final RedissonClient redisson;
private final KeyResolver keyResolver;
diff --git a/pigx-common/pigx-common-log/src/main/java/com/pig4cloud/pigx/common/log/init/ApplicationLoggerInitializer.java b/pigx-common/pigx-common-log/src/main/java/com/pig4cloud/pigx/common/log/init/ApplicationLoggerInitializer.java
index 3c7c59572..d037a6324 100644
--- a/pigx-common/pigx-common-log/src/main/java/com/pig4cloud/pigx/common/log/init/ApplicationLoggerInitializer.java
+++ b/pigx-common/pigx-common-log/src/main/java/com/pig4cloud/pigx/common/log/init/ApplicationLoggerInitializer.java
@@ -1,7 +1,7 @@
package com.pig4cloud.pigx.common.log.init;
+import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
-import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
@@ -22,8 +22,6 @@ public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, O
System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName));
// 避免 sentinel 1.8.4+ 心跳日志过大
System.setProperty("csp.sentinel.log.level", "OFF");
- // 避免 sentinel 健康检查 server
- System.setProperty("management.health.sentinel.enabled", "false");
// 避免各种依赖的地方组件造成 BeanPostProcessorChecker 警告
System.setProperty("logging.level.org.springframework.context.support.PostProcessorRegistrationDelegate",
"ERROR");
diff --git a/pigx-common/pigx-common-log/src/main/java/com/pig4cloud/pigx/common/log/util/JacksonSensitiveFieldUtil.java b/pigx-common/pigx-common-log/src/main/java/com/pig4cloud/pigx/common/log/util/JacksonSensitiveFieldUtil.java
index 5d7e253c9..ad7acb598 100644
--- a/pigx-common/pigx-common-log/src/main/java/com/pig4cloud/pigx/common/log/util/JacksonSensitiveFieldUtil.java
+++ b/pigx-common/pigx-common-log/src/main/java/com/pig4cloud/pigx/common/log/util/JacksonSensitiveFieldUtil.java
@@ -1,7 +1,9 @@
package com.pig4cloud.pigx.common.log.util;
import com.fasterxml.jackson.annotation.JsonFilter;
+import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@@ -10,6 +12,12 @@ import lombok.SneakyThrows;
/**
* Jackson 脱敏工具类
+ *
+ * 状态字段 {@code objectMapper} 通过 read-copy-write 模式更新,{@link #configureSensitiveFields}
+ * 与 {@link #registerCustomModule} 都需要在并发调用下保证修改不丢失,因此设为 {@code synchronized}。
+ * 读取路径({@link #readValue} 等)依赖 {@code volatile} 保证可见性,无需加锁。
+ *
+ * 注:{@link ObjectMapper#copy()} 是 Jackson 2 才支持的能力,这里有意保留 Jackson 2 依赖。
*
* @author lengleng
* @date 2024/07/18
@@ -17,20 +25,22 @@ import lombok.SneakyThrows;
public class JacksonSensitiveFieldUtil {
@Getter
- private static final ObjectMapper objectMapper = new ObjectMapper();
+ private static volatile ObjectMapper objectMapper = JsonMapper.builder()
+ .addMixIn(Object.class, PropertyFilterMixIn.class)
+ .build();
- static {
- objectMapper.addMixIn(Object.class, PropertyFilterMixIn.class);
- }
-
- public static void configureSensitiveFields(String[] ignorableFieldNames) {
+ public static synchronized void configureSensitiveFields(String[] ignorableFieldNames) {
FilterProvider filters = new SimpleFilterProvider().addFilter("filter properties by name",
SimpleBeanPropertyFilter.serializeAllExcept(ignorableFieldNames));
- objectMapper.setFilterProvider(filters);
+ ObjectMapper mapper = objectMapper.copy();
+ mapper.setFilterProvider(filters);
+ objectMapper = mapper;
}
- public static void registerCustomModule(com.fasterxml.jackson.databind.Module module) {
- objectMapper.registerModule(module);
+ public static synchronized void registerCustomModule(Module module) {
+ ObjectMapper mapper = objectMapper.copy();
+ mapper.registerModule(module);
+ objectMapper = mapper;
}
/**
diff --git a/pigx-common/pigx-common-log/src/main/resources/META-INF/spring.factories b/pigx-common/pigx-common-log/src/main/resources/META-INF/spring.factories
index 83eb42f33..f1710ea77 100644
--- a/pigx-common/pigx-common-log/src/main/resources/META-INF/spring.factories
+++ b/pigx-common/pigx-common-log/src/main/resources/META-INF/spring.factories
@@ -1,2 +1,2 @@
-org.springframework.boot.env.EnvironmentPostProcessor=\
+org.springframework.boot.EnvironmentPostProcessor=\
com.pig4cloud.pigx.common.log.init.ApplicationLoggerInitializer
diff --git a/pigx-common/pigx-common-sensitive/src/main/java/com/pig4cloud/pigx/common/sensitive/core/SensitiveSerialize.java b/pigx-common/pigx-common-sensitive/src/main/java/com/pig4cloud/pigx/common/sensitive/core/SensitiveSerialize.java
index c9e936e33..c64c49d1e 100644
--- a/pigx-common/pigx-common-sensitive/src/main/java/com/pig4cloud/pigx/common/sensitive/core/SensitiveSerialize.java
+++ b/pigx-common/pigx-common-sensitive/src/main/java/com/pig4cloud/pigx/common/sensitive/core/SensitiveSerialize.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2026, lengleng All rights reserved.
+ * Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -77,8 +77,7 @@ public class SensitiveSerialize extends JsonSerializer implements Contex
sensitive = beanProperty.getContextAnnotation(Sensitive.class);
}
if (sensitive != null) {
- this.sensitive = sensitive;
- return this;
+ return new SensitiveSerialize(sensitive);
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
diff --git a/pigx-common/pigx-common-sensitive/src/main/java/com/pig4cloud/pigx/common/sensitive/util/SensitiveWordsProperties.java b/pigx-common/pigx-common-sensitive/src/main/java/com/pig4cloud/pigx/common/sensitive/util/SensitiveWordsProperties.java
index d1f35bd4e..1e96ca541 100644
--- a/pigx-common/pigx-common-sensitive/src/main/java/com/pig4cloud/pigx/common/sensitive/util/SensitiveWordsProperties.java
+++ b/pigx-common/pigx-common-sensitive/src/main/java/com/pig4cloud/pigx/common/sensitive/util/SensitiveWordsProperties.java
@@ -19,7 +19,7 @@ public class SensitiveWordsProperties {
/**
* The sensitive words to detect.
*/
- private String mask = "no_mask";
+ private String mask = "*";
/**
* Whether the sensitive words detection is enabled.
diff --git a/pigx-common/pigx-common-sentinel/src/main/java/com/pig4cloud/pigx/common/sentinel/feign/PigxSentinelFeign.java b/pigx-common/pigx-common-sentinel/src/main/java/com/pig4cloud/pigx/common/sentinel/feign/PigxSentinelFeign.java
index dfcfa8eb0..557d54e29 100644
--- a/pigx-common/pigx-common-sentinel/src/main/java/com/pig4cloud/pigx/common/sentinel/feign/PigxSentinelFeign.java
+++ b/pigx-common/pigx-common-sentinel/src/main/java/com/pig4cloud/pigx/common/sentinel/feign/PigxSentinelFeign.java
@@ -19,6 +19,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
+
/**
* 支持自动降级注入 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelFeign}
*
@@ -90,7 +91,7 @@ public final class PigxSentinelFeign {
Object fallbackInstance = feignClientFactory.getInstance(name, fallbackType);
if (fallbackInstance == null) {
throw new IllegalStateException(String
- .format("No %s instance of type %s found for feign client %s", type, fallbackType, name));
+ .format("No %s instance of type %s found for feign client %s", type, fallbackType, name));
}
if (!targetType.isAssignableFrom(fallbackType)) {
diff --git a/pigx-common/pigx-common-sentinel/src/main/java/com/pig4cloud/pigx/common/sentinel/handle/GlobalBizExceptionHandler.java b/pigx-common/pigx-common-sentinel/src/main/java/com/pig4cloud/pigx/common/sentinel/handle/GlobalBizExceptionHandler.java
index 6631a3e10..ceabf12a4 100644
--- a/pigx-common/pigx-common-sentinel/src/main/java/com/pig4cloud/pigx/common/sentinel/handle/GlobalBizExceptionHandler.java
+++ b/pigx-common/pigx-common-sentinel/src/main/java/com/pig4cloud/pigx/common/sentinel/handle/GlobalBizExceptionHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2026, lengleng All rights reserved.
+ * Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -17,130 +17,323 @@
package com.pig4cloud.pigx.common.sentinel.handle;
+import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.Tracer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pig4cloud.pigx.common.core.util.R;
import feign.FeignException;
-import lombok.SneakyThrows;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.method.ParameterValidationResult;
+import org.springframework.web.HttpMediaTypeNotSupportedException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
+import org.springframework.web.method.annotation.HandlerMethodValidationException;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
+import java.nio.ByteBuffer;
import java.sql.SQLException;
import java.util.List;
+import java.util.stream.Collectors;
/**
+ * 全局异常处理器
+ *
+ * 适配 Spring Boot 4.0+/Spring Framework 7.0+,统一拦截 Web、参数校验、Feign、IO 等异常。
+ *
* @author lengleng
- * @date 2020-06-29
+ * @date 2026-05-18
*/
@Slf4j
@RestControllerAdvice
+@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalBizExceptionHandler {
- private final ObjectMapper objectMapper = new ObjectMapper();
+ /**
+ * JSON 序列化工具,用于解析 Feign 透传的远端 R 响应体。
+ */
+ private final ObjectMapper objectMapper = new ObjectMapper();
- /**
- * 全局异常.
- *
- * @param e the e
- * @return R
- */
- @ExceptionHandler(Exception.class)
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- public R handleGlobalException(Exception e) {
+ /**
+ * 全局兜底异常
+ * @param e 未被更具体处理器捕获的异常
+ * @return 统一失败响应,消息来自异常本地化描述
+ */
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public R handleGlobalException(Exception e) {
+ log.error("全局异常信息 ex={}", e.getMessage(), e);
+ // 业务异常交由 sentinel 记录
+ Tracer.trace(e);
+ return R.failed(e.getLocalizedMessage());
+ }
- log.error("全局异常信息 ex={}", e.getMessage(), e);
+ /**
+ * 客户端断开导致的异步响应不可用异常,无需返回任何内容
+ *
+ * Spring Boot 3.2+ 在 SSE / 长连接场景客户端主动断开时抛出,需静默处理避免污染日志
+ * @param e 异步请求不可用异常
+ */
+ @ExceptionHandler(AsyncRequestNotUsableException.class)
+ @ResponseStatus(HttpStatus.OK)
+ public void handleAsyncRequestNotUsableException(AsyncRequestNotUsableException e) {
+ log.debug("异步请求已不可用 ex={}", e.getMessage());
+ }
- // 业务异常交由 sentinel 记录
- Tracer.trace(e);
- return R.failed(e.getLocalizedMessage());
- }
+ /**
+ * 数据库异常
+ * @param exception 数据库调用异常
+ * @return 统一失败响应,不向客户端暴露底层 SQL 细节
+ */
+ @ExceptionHandler(SQLException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public R handleSQLException(SQLException exception) {
+ log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
+ return R.failed("数据库调用异常,请联系管理员处理");
+ }
- /**
- * SQLException Exception
- *
- * @param exception 数据库调用异常
- * @return R
- */
- @ExceptionHandler({SQLException.class})
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- public R handleSQLException(SQLException exception) {
- log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
- return R.failed("数据库调用异常,请联系管理员处理");
- }
+ /**
+ * Feign 远程调用异常,透传远端 R 响应体
+ * @param e Feign 调用异常
+ * @return 远端 R 响应体或本地统一失败响应
+ */
+ @ExceptionHandler(FeignException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public R handleFeignException(FeignException e) {
+ log.error("Feign 调用异常 ex={}", e.getMessage(), e);
+ Tracer.trace(e);
- @SneakyThrows
- @ExceptionHandler(FeignException.class)
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- public R handleGlobalException(FeignException e) {
- log.error("全局异常信息 ex={}", e.getMessage(), e);
+ if (e.responseBody().isPresent()) {
+ try {
+ ByteBuffer responseBody = e.responseBody().get();
+ byte[] body = new byte[responseBody.remaining()];
+ responseBody.get(body);
+ return objectMapper.readValue(body, R.class);
+ }
+ catch (Exception ex) {
+ log.warn("Feign 响应体反序列化失败 ex={}", ex.getMessage());
+ }
+ }
+ return R.failed(e.getLocalizedMessage());
+ }
- // 业务异常交由 sentinel 记录
- Tracer.trace(e);
+ /**
+ * 拒绝授权异常
+ * @param e Spring Security 访问拒绝异常
+ * @return 权限不足的统一失败响应
+ */
+ @ExceptionHandler(AccessDeniedException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public R handleAccessDeniedException(AccessDeniedException e) {
+ log.error("拒绝授权异常信息 ex={}", e.getMessage());
+ return R.failed("权限不足,不允许访问");
+ }
- if (e.responseBody().isPresent()) {
- return objectMapper.readValue(e.responseBody().get().array(), R.class);
- }
+ /**
+ * 表单参数绑定校验异常(@ModelAttribute / 表单提交)
+ * @param exception 表单参数绑定异常
+ * @return 字段校验失败的统一失败响应
+ */
+ @ExceptionHandler(BindException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleBindException(BindException exception) {
+ log.warn("参数绑定异常 ex={}", exception.getMessage());
+ return R.failed(buildFieldErrorMessage(exception.getBindingResult().getFieldErrors()));
+ }
- return R.failed(e.getLocalizedMessage());
- }
+ /**
+ * @RequestBody + @Valid 校验异常
+ * @param exception 请求体参数校验异常
+ * @return 字段校验失败的统一失败响应
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
+ log.warn("请求体参数校验异常 ex={}", exception.getMessage());
+ return R.failed(buildFieldErrorMessage(exception.getBindingResult().getFieldErrors()));
+ }
- /**
- * AccessDeniedException
- *
- * @param e the e
- * @return R
- */
- @ExceptionHandler(AccessDeniedException.class)
- @ResponseStatus(HttpStatus.FORBIDDEN)
- public R handleAccessDeniedException(AccessDeniedException e) {
- log.error("拒绝授权异常信息 ex={}", e.getMessage());
- return R.failed("权限不足,不允许访问");
- }
+ /**
+ * Spring 7.0+ 方法级参数校验异常(@Validated 标注在 Controller 上的 @RequestParam / @PathVariable 校验)
+ * @param exception 方法参数校验异常
+ * @return 方法参数校验失败的统一失败响应
+ */
+ @ExceptionHandler(HandlerMethodValidationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleHandlerMethodValidationException(HandlerMethodValidationException exception) {
+ log.warn("方法参数校验异常 ex={}", exception.getMessage());
+ String parameterMessage = exception.getParameterValidationResults()
+ .stream()
+ .map(ParameterValidationResult::getResolvableErrors)
+ .flatMap(List::stream)
+ .map(error -> error.getDefaultMessage() == null ? StrUtil.EMPTY : error.getDefaultMessage())
+ .collect(Collectors.joining(StrUtil.COMMA));
+ String crossParameterMessage = buildResolvableErrorMessage(exception.getCrossParameterValidationResults());
+ String message = joinErrorMessages(parameterMessage, crossParameterMessage);
+ return R.failed(message);
+ }
- /**
- * validation Exception
- *
- * @param exception
- * @return R
- */
- @ExceptionHandler({BindException.class})
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- public R handleBodyValidException(BindException exception) {
- List fieldErrors = exception.getBindingResult().getFieldErrors();
- // 插入log 的逻辑
- return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
- }
+ /**
+ * @Validated 在 Service 层方法或单参数校验失败
+ * @param exception 约束校验异常
+ * @return 约束校验失败的统一失败响应
+ */
+ @ExceptionHandler(ConstraintViolationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleConstraintViolationException(ConstraintViolationException exception) {
+ log.warn("约束校验异常 ex={}", exception.getMessage());
+ String message = exception.getConstraintViolations()
+ .stream()
+ .map(ConstraintViolation::getMessage)
+ .collect(Collectors.joining(StrUtil.COMMA));
+ return R.failed(message);
+ }
- /**
- * 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
- *
- * @return R
- */
- @DeleteMapping("/error")
- @ResponseStatus(HttpStatus.NOT_FOUND)
- public R noHandlerFoundException() {
- return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
- }
+ /**
+ * 请求体解析异常(JSON 格式错误 / 类型不匹配)
+ * @param exception 请求体解析异常
+ * @return 请求体格式错误的统一失败响应
+ */
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) {
+ log.warn("请求体解析异常 ex={}", exception.getMessage());
+ return R.failed("请求体格式错误,无法解析");
+ }
- /**
- * 保持和低版本请求路径不存在的行为一致
- *
- * [Spring Boot
- * 3.2.0] 404 Not Found behavior #38733
- *
- * @param exception
- * @return R
- */
- @ExceptionHandler({NoResourceFoundException.class})
- @ResponseStatus(HttpStatus.NOT_FOUND)
- public R noResourceFoundException(NoResourceFoundException exception) {
- log.debug("请求路径 404 {}", exception.getMessage());
- return R.failed(exception.getMessage());
- }
+ /**
+ * 缺少必填请求参数
+ * @param exception 缺少请求参数异常
+ * @return 标明缺失参数名的统一失败响应
+ */
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleMissingServletRequestParameterException(MissingServletRequestParameterException exception) {
+ log.warn("缺少必填参数 ex={}", exception.getMessage());
+ return R.failed(StrUtil.format("缺少必填参数: {}", exception.getParameterName()));
+ }
+
+ /**
+ * 请求参数类型转换异常
+ * @param exception 方法参数类型不匹配异常
+ * @return 标明参数名的统一失败响应
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException exception) {
+ log.warn("参数类型不匹配 ex={}", exception.getMessage());
+ return R.failed(StrUtil.format("参数 {} 类型不正确", exception.getName()));
+ }
+
+ /**
+ * 请求方法不支持
+ * @param exception 请求方法不支持异常
+ * @return 请求方法不支持的统一失败响应
+ */
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
+ public R handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) {
+ log.warn("请求方法不支持 ex={}", exception.getMessage());
+ return R.failed(exception.getMessage());
+ }
+
+ /**
+ * Content-Type 不支持
+ * @param exception 媒体类型不支持异常
+ * @return 媒体类型不支持的统一失败响应
+ */
+ @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
+ @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
+ public R handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException exception) {
+ log.warn("媒体类型不支持 ex={}", exception.getMessage());
+ return R.failed(exception.getMessage());
+ }
+
+ /**
+ * 文件上传超过大小限制
+ * @param exception 文件大小超过限制异常
+ * @return 文件大小超过限制的统一失败响应
+ */
+ @ExceptionHandler(MaxUploadSizeExceededException.class)
+ @ResponseStatus(HttpStatus.CONTENT_TOO_LARGE)
+ public R handleMaxUploadSizeExceededException(MaxUploadSizeExceededException exception) {
+ log.warn("上传文件超过大小限制 ex={}", exception.getMessage());
+ return R.failed("上传文件大小超过限制");
+ }
+
+ /**
+ * 保持和低版本请求路径不存在的行为一致
+ *
+ * [Spring Boot
+ * 3.2.0] 404 Not Found behavior #38733
+ * @param exception 静态资源或请求路径不存在异常
+ * @return 404 的统一失败响应
+ */
+ @ExceptionHandler(NoResourceFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public R handleNoResourceFoundException(NoResourceFoundException exception) {
+ log.debug("请求路径 404 {}", exception.getMessage());
+ return R.failed(exception.getMessage());
+ }
+
+ /**
+ * 构造字段校验错误信息
+ * @param fieldErrors 字段校验错误列表,空集合返回空字符串
+ * @return 拼接后的字段校验错误信息
+ */
+ private String buildFieldErrorMessage(List fieldErrors) {
+ if (fieldErrors == null || fieldErrors.isEmpty()) {
+ return StrUtil.EMPTY;
+ }
+ return fieldErrors.stream()
+ .map(error -> StrUtil.format("{} {}", error.getField(), error.getDefaultMessage()))
+ .collect(Collectors.joining(StrUtil.COMMA));
+ }
+
+ /**
+ * 构造 Spring 参数校验错误信息
+ * @param errors 参数校验错误,空集合返回空字符串
+ * @return 拼接后的错误信息
+ */
+ private String buildResolvableErrorMessage(List extends MessageSourceResolvable> errors) {
+ if (errors == null || errors.isEmpty()) {
+ return StrUtil.EMPTY;
+ }
+ return errors.stream()
+ .map(error -> error.getDefaultMessage() == null ? StrUtil.EMPTY : error.getDefaultMessage())
+ .collect(Collectors.joining(StrUtil.COMMA));
+ }
+
+ /**
+ * 拼接非空错误信息片段
+ * @param first 第一段错误信息,可为空
+ * @param second 第二段错误信息,可为空
+ * @return 拼接后的错误信息,全部为空时返回空字符串
+ */
+ private String joinErrorMessages(String first, String second) {
+ if (StrUtil.isBlank(first)) {
+ return StrUtil.blankToDefault(second, StrUtil.EMPTY);
+ }
+ if (StrUtil.isBlank(second)) {
+ return first;
+ }
+ return StrUtil.join(StrUtil.COMMA, first, second);
+ }
}
diff --git a/pigx-common/pigx-common-sequence/src/main/java/com/pig4cloud/pigx/common/sequence/SequenceAutoConfiguration.java b/pigx-common/pigx-common-sequence/src/main/java/com/pig4cloud/pigx/common/sequence/SequenceAutoConfiguration.java
index 058429865..fea0c3bb5 100644
--- a/pigx-common/pigx-common-sequence/src/main/java/com/pig4cloud/pigx/common/sequence/SequenceAutoConfiguration.java
+++ b/pigx-common/pigx-common-sequence/src/main/java/com/pig4cloud/pigx/common/sequence/SequenceAutoConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2026, lengleng All rights reserved.
+ * Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -26,8 +26,8 @@ import org.anyline.metadata.Index;
import org.anyline.metadata.Table;
import org.anyline.proxy.ServiceProxy;
import org.anyline.service.AnylineService;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
@@ -49,7 +49,7 @@ public class SequenceAutoConfiguration {
/**
* 启动后自动创建表
*/
- @EventListener({WebServerInitializedEvent.class})
+ @EventListener({ApplicationReadyEvent.class})
public void onApplicationReady() throws Exception {
// 判断是否需要创建表
AnylineService service = ServiceProxy.service();
diff --git a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/holder/SseEmitterHolder.java b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/holder/SseEmitterHolder.java
index 77faed9d2..a9246497b 100644
--- a/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/holder/SseEmitterHolder.java
+++ b/pigx-common/pigx-common-sse/src/main/java/com/pig4cloud/pigx/common/sse/holder/SseEmitterHolder.java
@@ -31,10 +31,10 @@ public final class SseEmitterHolder {
/**
* 删除一个 sseEmitter
- * @param sseEmitter SseEmitter
+ * @param sseEmitter SseEmitter
*/
- public static void removeSseEmitter(SseEmitter sseEmitter) {
- USER_SSE_EMITTER_MAP.values().remove(sseEmitter);
+ public static void removeSseEmitter(SseEmitter sseEmitter) {
+ USER_SSE_EMITTER_MAP.values().remove(sseEmitter);
}
/**
diff --git a/pigx-common/pigx-common-xss/src/main/java/com/pig4cloud/pigx/common/xss/PigxXssAutoConfiguration.java b/pigx-common/pigx-common-xss/src/main/java/com/pig4cloud/pigx/common/xss/PigxXssAutoConfiguration.java
index 1f8f0f2ba..8752b53a5 100644
--- a/pigx-common/pigx-common-xss/src/main/java/com/pig4cloud/pigx/common/xss/PigxXssAutoConfiguration.java
+++ b/pigx-common/pigx-common-xss/src/main/java/com/pig4cloud/pigx/common/xss/PigxXssAutoConfiguration.java
@@ -21,7 +21,6 @@ import com.pig4cloud.pigx.common.xss.core.*;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -31,6 +30,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
+
/**
* jackson xss 配置
*
@@ -40,40 +40,41 @@ import java.util.List;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PigxXssProperties.class)
@ConditionalOnProperty(prefix = PigxXssProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
+@SuppressWarnings("removal")
public class PigxXssAutoConfiguration implements WebMvcConfigurer {
- private final PigxXssProperties xssProperties;
+ private final PigxXssProperties xssProperties;
- @Bean
- @ConditionalOnMissingBean
- public XssCleaner xssCleaner(PigxXssProperties properties) {
- return new DefaultXssCleaner(properties);
- }
+ @Bean
+ @ConditionalOnMissingBean
+ public XssCleaner xssCleaner(PigxXssProperties properties) {
+ return new DefaultXssCleaner(properties);
+ }
- @Bean
- @ConditionalOnMissingBean
- public FormXssClean formXssClean(PigxXssProperties properties, XssCleaner xssCleaner) {
- return new FormXssClean(properties, xssCleaner);
- }
+ @Bean
+ @ConditionalOnMissingBean
+ public FormXssClean formXssClean(PigxXssProperties properties, XssCleaner xssCleaner) {
+ return new FormXssClean(properties, xssCleaner);
+ }
- @Bean
- public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(PigxXssProperties properties,
- XssCleaner xssCleaner) {
- JacksonXssClean xssClean = new JacksonXssClean(properties, xssCleaner);
- return builder -> builder.deserializerByType(String.class, xssClean);
- }
+ @Bean
+ public org.springframework.boot.jackson2.autoconfigure.Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(
+ PigxXssProperties properties, XssCleaner xssCleaner) {
+ JacksonXssClean xssClean = new JacksonXssClean(properties, xssCleaner);
+ return builder -> builder.deserializerByType(String.class, xssClean);
+ }
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- List patterns = xssProperties.getPathPatterns();
- if (patterns.isEmpty()) {
- patterns.add("/**");
- }
- XssCleanInterceptor interceptor = new XssCleanInterceptor(xssProperties);
- registry.addInterceptor(interceptor)
- .addPathPatterns(patterns)
- .excludePathPatterns(xssProperties.getPathExcludePatterns())
- .order(Ordered.LOWEST_PRECEDENCE);
- }
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ List patterns = xssProperties.getPathPatterns();
+ if (patterns.isEmpty()) {
+ patterns.add("/**");
+ }
+ XssCleanInterceptor interceptor = new XssCleanInterceptor(xssProperties);
+ registry.addInterceptor(interceptor)
+ .addPathPatterns(patterns)
+ .excludePathPatterns(xssProperties.getPathExcludePatterns())
+ .order(Ordered.LOWEST_PRECEDENCE);
+ }
}
diff --git a/pigx-common/pigx-common-xss/src/main/java/com/pig4cloud/pigx/common/xss/core/JacksonXssClean.java b/pigx-common/pigx-common-xss/src/main/java/com/pig4cloud/pigx/common/xss/core/JacksonXssClean.java
index c1aa746de..786a4b77c 100644
--- a/pigx-common/pigx-common-xss/src/main/java/com/pig4cloud/pigx/common/xss/core/JacksonXssClean.java
+++ b/pigx-common/pigx-common-xss/src/main/java/com/pig4cloud/pigx/common/xss/core/JacksonXssClean.java
@@ -18,10 +18,9 @@ package com.pig4cloud.pigx.common.xss.core;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.pig4cloud.pigx.common.xss.config.PigxXssProperties;
import com.pig4cloud.pigx.common.xss.utils.XssUtil;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@@ -32,13 +31,18 @@ import java.io.IOException;
* @author L.cm
*/
@Slf4j
-@RequiredArgsConstructor
-public class JacksonXssClean extends JsonDeserializer {
+public class JacksonXssClean extends StdDeserializer {
private final PigxXssProperties properties;
private final XssCleaner xssCleaner;
+ public JacksonXssClean(PigxXssProperties properties, XssCleaner xssCleaner) {
+ super(String.class);
+ this.properties = properties;
+ this.xssCleaner = xssCleaner;
+ }
+
@Override
public String deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
// XSS filter
diff --git a/pigx-gateway/src/main/resources/logback-spring.xml b/pigx-gateway/src/main/resources/logback-spring.xml
index 9707ae89c..4e0059ebc 100644
--- a/pigx-gateway/src/main/resources/logback-spring.xml
+++ b/pigx-gateway/src/main/resources/logback-spring.xml
@@ -1,6 +1,6 @@