Przeglądaj źródła

bump: 发布1.7.2

* chore: 更新 MSSQL Oracle DB 驱动库名及版本

* feat: 新增 Optional 支持

* fix: 无法覆盖全局异常处理

* chore: 升级基础版本和依赖版本

* pref: 优化自定义主键ID生成器SpringBean注入过程

* wip: 主键ID生成器优化

* wip: i18n支持枚举

* refactor: 调整web全局异常处理以支持国际化
feat: 完善国际化工具类API及其内置使用

* feat: 国际化支持开关;增加最终兜底异常枚举

* fix: 返回国际化翻译值失败

* fix: 未自动注册web logging切面

* fix: 因transient关键词导致RPC时缺失对应枚举值信息

* pref: 使用JDK自带的Locale替代spring-context的Locale

* bump: 发布1.7.2
Uncarbon 3 lat temu
rodzic
commit
75c38ab63d
23 zmienionych plików z 415 dodań i 121 usunięć
  1. 9 2
      helio-core/src/main/java/cc/uncarbon/framework/core/context/TenantContextHolder.java
  2. 9 2
      helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContextHolder.java
  3. 2 1
      helio-core/src/main/java/cc/uncarbon/framework/core/enums/HelioBaseEnum.java
  4. 8 1
      helio-core/src/main/java/cc/uncarbon/framework/core/enums/IdGeneratorStrategyEnum.java
  5. 29 4
      helio-core/src/main/java/cc/uncarbon/framework/core/exception/BusinessException.java
  6. 14 2
      helio-core/src/main/java/cc/uncarbon/framework/core/props/HelioProperties.java
  7. 5 3
      helio-starter-crud/pom.xml
  8. 16 4
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/HelioMybatisPlusAutoConfiguration.java
  9. 21 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioSequenceIdGenerateHandler.java
  10. 17 17
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioSnowflakeIdGenerateHandler.java
  11. 26 0
      helio-starter-i18n/pom.xml
  12. 114 0
      helio-starter-i18n/src/main/java/cc/uncarbon/framework/i18n/util/I18nUtil.java
  13. 0 0
      helio-starter-i18n/src/main/resources/PLACEHOLDER
  14. 5 0
      helio-starter-web/pom.xml
  15. 10 9
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/aspect/WebLoggingAspect.java
  16. 68 22
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/GlobalWebExceptionHandlerAutoConfiguration.java
  17. 0 19
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/enums/ErrorResponseEnum.java
  18. 35 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/enums/GlobalWebExceptionI18nMessageEnum.java
  19. 1 2
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/EnumConverterFactory.java
  20. 5 4
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/EnumModule.java
  21. 3 4
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/SQLFilter.java
  22. 2 1
      helio-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  23. 16 24
      pom.xml

+ 9 - 2
helio-core/src/main/java/cc/uncarbon/framework/core/context/TenantContextHolder.java

@@ -1,6 +1,7 @@
 package cc.uncarbon.framework.core.context;
 
 import com.alibaba.ttl.TransmittableThreadLocal;
+import java.util.Optional;
 import lombok.experimental.UtilityClass;
 
 /**
@@ -17,13 +18,19 @@ public class TenantContextHolder {
     /**
      * 获取当前租户上下文
      *
-     * @throws NullPointerException 链式调用时需要注意可能存在 NPE 问题
      * @return null or 当前租户上下文
      */
-    public TenantContext getTenantContext() throws NullPointerException {
+    public TenantContext getTenantContext() {
         return THREAD_LOCAL_CONTEXT.get();
     }
 
+    /**
+     * 获取当前租户上下文
+     */
+    public Optional<TenantContext> getTenantContextOptional() {
+        return Optional.ofNullable(THREAD_LOCAL_CONTEXT.get());
+    }
+
     /**
      * 设置当前租户上下文
      *

+ 9 - 2
helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContextHolder.java

@@ -1,6 +1,7 @@
 package cc.uncarbon.framework.core.context;
 
 import com.alibaba.ttl.TransmittableThreadLocal;
+import java.util.Optional;
 import lombok.experimental.UtilityClass;
 
 /**
@@ -17,13 +18,19 @@ public class UserContextHolder {
     /**
      * 获取当前用户上下文
      *
-     * @throws NullPointerException 链式调用时需要注意可能存在 NPE 问题
      * @return null or 当前用户上下文
      */
-    public UserContext getUserContext() throws NullPointerException {
+    public UserContext getUserContext() {
         return THREAD_LOCAL_CONTEXT.get();
     }
 
+    /**
+     * 获取当前用户上下文
+     */
+    public Optional<UserContext> getUserContextOptional() {
+        return Optional.ofNullable(THREAD_LOCAL_CONTEXT.get());
+    }
+
     /**
      * 设置当前用户上下文
      *

+ 2 - 1
helio-core/src/main/java/cc/uncarbon/framework/core/enums/HelioBaseEnum.java

@@ -2,6 +2,7 @@ package cc.uncarbon.framework.core.enums;
 
 import cc.uncarbon.framework.core.exception.BusinessException;
 import cn.hutool.core.collection.IterUtil;
+import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
@@ -143,7 +144,7 @@ public interface HelioBaseEnum<T> extends Serializable {
     }
 
     default String formatLabel(Object... templateParams) {
-        if (templateParams == null || templateParams.length == 0) {
+        if (ArrayUtil.isEmpty(templateParams)) {
             return this.getLabel();
         }
 

+ 8 - 1
helio-core/src/main/java/cc/uncarbon/framework/core/enums/IdGeneratorStrategyEnum.java

@@ -15,7 +15,14 @@ public enum IdGeneratorStrategyEnum implements HelioBaseEnum<Integer> {
     /**
      * Twitter雪花算法
      */
-    SNOWFLAKE(1, "SNOWFLAKE");
+    SNOWFLAKE(1, "Twitter Snowflake"),
+
+    /**
+     * Mybatis-Plus 提供的 Sequence 算法(类雪花)
+     */
+    SEQUENCE(2, "Mybatis-Plus Sequence"),
+
+    ;
 
     private final Integer value;
     private final String label;

+ 29 - 4
helio-core/src/main/java/cc/uncarbon/framework/core/exception/BusinessException.java

@@ -1,11 +1,12 @@
 package cc.uncarbon.framework.core.exception;
 
-import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
-
 import cc.uncarbon.framework.core.enums.HelioBaseEnum;
 import cn.hutool.core.util.StrUtil;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
+import lombok.NonNull;
+
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
 
 /**
  * 业务异常类
@@ -18,6 +19,12 @@ public class BusinessException extends RuntimeException {
 
     private Integer code = null;
 
+    /*
+    若使用枚举类参数的构造方法创建的本异常,则记录对应枚举类及模板参数
+     */
+    private Enum<?> customEnumField;
+    private Object[] templateParams;
+
     /**
      * 仅错误信息
      * @param msg 错误信息
@@ -52,9 +59,17 @@ public class BusinessException extends RuntimeException {
      * 从枚举类生成异常
      * @param customEnum 枚举类对象
      */
-    public BusinessException(HelioBaseEnum<Integer> customEnum) {
+    public BusinessException(@NonNull HelioBaseEnum<Integer> customEnum) {
         super(customEnum.getLabel());
         this.code = customEnum.getValue();
+
+        /*
+        @since 1.7.2 国际化支持
+        实际应用在GlobalWebExceptionHandlerAutoConfiguration.handleBusinessException
+         */
+        if (customEnum.getClass().isEnum()) {
+            this.customEnumField = (Enum<?>) customEnum;
+        }
     }
 
     /**
@@ -62,9 +77,18 @@ public class BusinessException extends RuntimeException {
      * @param customEnum 枚举类对象
      * @param templateParams label 中如果有占位符的话,向里面填充的模板参数
      */
-    public BusinessException(HelioBaseEnum<Integer> customEnum, Object... templateParams) {
+    public BusinessException(@NonNull HelioBaseEnum<Integer> customEnum, Object... templateParams) {
         super(customEnum.formatLabel(templateParams));
         this.code = customEnum.getValue();
+
+        /*
+        @since 1.7.2 国际化支持
+        实际应用在GlobalWebExceptionHandlerAutoConfiguration.handleBusinessException
+         */
+        if (customEnum.getClass().isEnum()) {
+            this.customEnumField = (Enum<?>) customEnum;
+        }
+        this.templateParams = templateParams;
     }
 
     /**
@@ -74,4 +98,5 @@ public class BusinessException extends RuntimeException {
     public synchronized Throwable fillInStackTrace() {
         return this;
     }
+
 }

+ 14 - 2
helio-core/src/main/java/cc/uncarbon/framework/core/props/HelioProperties.java

@@ -3,12 +3,13 @@ package cc.uncarbon.framework.core.props;
 import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.enums.IdGeneratorStrategyEnum;
 import cc.uncarbon.framework.core.enums.TenantIsolateLevelEnum;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashSet;
 import java.util.List;
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
 
 /**
  * 配置项会被反序列化到本 Spring Bean 中,可自行依赖注入后使用
@@ -24,6 +25,7 @@ public class HelioProperties {
     private final Knife4j knife4j = new Knife4j();
     private final Tenant tenant = new Tenant();
     private final Web web = new Web();
+    private final I18n i18n = new I18n();
 
 
     @Data
@@ -147,4 +149,14 @@ public class HelioProperties {
         }
 
     }
+
+    @Data
+    public static class I18n {
+
+        /**
+         * 是否启用国际化 默认为 false
+         */
+        private Boolean enabled = Boolean.FALSE;
+
+    }
 }

+ 5 - 3
helio-starter-crud/pom.xml

@@ -37,13 +37,15 @@
         </dependency>
 
         <dependency>
-            <groupId>com.oracle</groupId>
-            <artifactId>ojdbc6</artifactId>
+            <groupId>com.oracle.database.jdbc</groupId>
+            <artifactId>ojdbc8</artifactId>
+            <optional>true</optional>
         </dependency>
 
         <dependency>
             <groupId>com.microsoft.sqlserver</groupId>
-            <artifactId>sqljdbc4</artifactId>
+            <artifactId>mssql-jdbc</artifactId>
+            <optional>true</optional>
         </dependency>
 
         <dependency>

+ 16 - 4
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/HelioMybatisPlusAutoConfiguration.java

@@ -1,7 +1,9 @@
 package cc.uncarbon.framework.crud.config;
 
+import cc.uncarbon.framework.core.enums.IdGeneratorStrategyEnum;
 import cc.uncarbon.framework.core.props.HelioProperties;
-import cc.uncarbon.framework.crud.handler.HelioIdentifierGeneratorHandler;
+import cc.uncarbon.framework.crud.handler.HelioSequenceIdGenerateHandler;
+import cc.uncarbon.framework.crud.handler.HelioSnowflakeIdGenerateHandler;
 import cc.uncarbon.framework.crud.handler.MybatisPlusAutoFillColumnHandler;
 import cc.uncarbon.framework.crud.support.TenantSupport;
 import cc.uncarbon.framework.crud.support.impl.DefaultTenantSupport;
@@ -77,12 +79,22 @@ public class HelioMybatisPlusAutoConfiguration {
     }
 
     /**
-     * 自定义ID生成器
+     * 自定义ID生成器 - 雪花ID
      */
     @Bean
     @ConditionalOnMissingBean
-    public IdentifierGenerator helioSnowflakeIdentifierGeneratorHandler() {
-        return new HelioIdentifierGeneratorHandler(helioProperties);
+    public IdentifierGenerator helioIdentifierGenerator() {
+        IdGeneratorStrategyEnum strategy = helioProperties.getCrud().getIdGenerator().getStrategy();
+
+        if (strategy == IdGeneratorStrategyEnum.SNOWFLAKE) {
+            return new HelioSnowflakeIdGenerateHandler(helioProperties);
+        }
+
+        if (strategy == IdGeneratorStrategyEnum.SEQUENCE) {
+            return new HelioSequenceIdGenerateHandler();
+        }
+
+        throw new IllegalArgumentException("Value of 'helio.crud.idGenerator.strategy' is illegal");
     }
 
     /**

+ 21 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioSequenceIdGenerateHandler.java

@@ -0,0 +1,21 @@
+package cc.uncarbon.framework.crud.handler;
+
+import cc.uncarbon.framework.core.enums.IdGeneratorStrategyEnum;
+import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 自定义ID生成器 - Mybatis-Plus Sequence
+ *
+ * @author Uncarbon
+ */
+@Slf4j
+public class HelioSequenceIdGenerateHandler extends DefaultIdentifierGenerator {
+
+    public HelioSequenceIdGenerateHandler() {
+        super();
+        log.info("[主键ID生成器] >> strategy=[{}]",
+                IdGeneratorStrategyEnum.SEQUENCE
+        );
+    }
+}

+ 17 - 17
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioIdentifierGeneratorHandler.java → helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioSnowflakeIdGenerateHandler.java

@@ -13,16 +13,17 @@ import lombok.extern.slf4j.Slf4j;
 import java.util.Date;
 
 /**
- * 自定义雪花ID生成器
+ * 自定义ID生成器 - 雪花ID
+ *
  * @author Uncarbon
  */
 @Slf4j
-public class HelioIdentifierGeneratorHandler implements IdentifierGenerator {
+public class HelioSnowflakeIdGenerateHandler implements IdentifierGenerator {
 
-    private Snowflake snowflakeInstance;
+    private final Snowflake snowflakeInstance;
 
 
-    public HelioIdentifierGeneratorHandler(HelioProperties helioProperties) {
+    public HelioSnowflakeIdGenerateHandler(HelioProperties helioProperties) {
         long workerId;
 
         try {
@@ -32,21 +33,20 @@ public class HelioIdentifierGeneratorHandler implements IdentifierGenerator {
             workerId = NetUtil.getLocalhost().hashCode();
         }
 
-        if (IdGeneratorStrategyEnum.SNOWFLAKE.equals(helioProperties.getCrud().getIdGenerator().getStrategy())) {
-            // Hutool的雪花生成器仅支持0-31
-            workerId = workerId % 32;
-            Long datacenterId = helioProperties.getCrud().getIdGenerator().getDatacenterId();
-            Date epochDate = DateUtil.parseDate(helioProperties.getCrud().getIdGenerator().getEpochDate());
+        // Hutool的雪花生成器仅支持0-31
+        workerId = workerId % 32;
+        Long datacenterId = helioProperties.getCrud().getIdGenerator().getDatacenterId();
+        String epochDateStr = helioProperties.getCrud().getIdGenerator().getEpochDate();
+        Date epochDate = DateUtil.parseDate(epochDateStr);
 
-            log.info("[主键ID生成器] >> strategy=[{}], workerId=[{}], datacenterId=[{}], epochDate=[{}]",
-                    helioProperties.getCrud().getIdGenerator().getStrategy().getLabel(),
-                    workerId,
-                    datacenterId,
-                    epochDate
-            );
+        log.info("[主键ID生成器] >> strategy=[{}], workerId=[{}], datacenterId=[{}], epochDate=[{}]",
+                IdGeneratorStrategyEnum.SNOWFLAKE,
+                workerId,
+                datacenterId,
+                epochDate
+        );
 
-            snowflakeInstance = getSnowflake(workerId, datacenterId, epochDate);
-        }
+        snowflakeInstance = getSnowflake(workerId, datacenterId, epochDate);
     }
 
     @Override

+ 26 - 0
helio-starter-i18n/pom.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>${revision}</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-i18n</artifactId>
+
+    <properties>
+
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 114 - 0
helio-starter-i18n/src/main/java/cc/uncarbon/framework/i18n/util/I18nUtil.java

@@ -0,0 +1,114 @@
+package cc.uncarbon.framework.i18n.util;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import cn.hutool.core.text.StrPool;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.MessageSource;
+import org.springframework.context.NoSuchMessageException;
+
+import java.util.Locale;
+
+/**
+ * 获取 i18n 资源
+ *
+ * @author Lion Li
+ * @author Uncarbon
+ */
+@UtilityClass
+@Slf4j
+public class I18nUtil {
+
+    private final MessageSource MESSAGE_SOURCE = SpringUtil.getBean(MessageSource.class);
+    private static final String SLF4J_STYLE_PLACEHOLDER = StrPool.DELIM_START + StrPool.DELIM_END;
+
+
+    public MessageSource getMessageSource() {
+        return MESSAGE_SOURCE;
+    }
+
+    /**
+     * 根据消息键和参数,获取国际化翻译值
+     * 支持模板填充,如: Nickname '{}' is already exist, do you like '{}'?
+     *
+     * @param code 消息代码
+     * @param templateParams 模板填充参数
+     * @return null or 国际化翻译值
+     */
+    public String messageOf(String code, Object... templateParams) {
+        if (code == null) {
+            return null;
+        }
+
+        try {
+            String msg = getMessageSource().getMessage(code, null, Locale.getDefault());
+            if (StrUtil.isEmpty(msg)) {
+                return msg;
+            }
+
+            if (StrUtil.contains(msg, SLF4J_STYLE_PLACEHOLDER)) {
+                // 使用 hutool 的模板填充
+                return StrUtil.format(msg, templateParams);
+            }
+
+            return msg;
+        } catch (NoSuchMessageException nsme) {
+            // 未找到对应国际化翻译值
+        }
+        return null;
+    }
+
+    /**
+     * 根据消息键和参数,获取国际化翻译值
+     * 支持模板填充,如: Nickname '{}' is already exist, do you like '{}'?
+     *
+     * @param code 消息代码
+     * @param defaultValue 未找到对应国际化翻译值的情况下,默认返回值
+     * @param templateParams 模板填充参数
+     * @return null or 国际化翻译值
+     */
+    public String messageOf(String code, String defaultValue, Object... templateParams) {
+        String msg = messageOf(code, templateParams);
+        return msg == null ? defaultValue : msg;
+    }
+
+    /**
+     * 根据枚举值和参数,获取国际化翻译值
+     * 支持模板填充,如: Nickname '{}' has been existing, do you like '{}'?
+     *
+     * @param enumField 枚举值
+     * @param templateParams 模板填充参数
+     * @return null or 国际化翻译值
+     */
+    public String messageOf(Enum<?> enumField, Object... templateParams) {
+        if (enumField == null) {
+            return null;
+        }
+
+        String i18nCode;
+
+        // 尝试以较长的 枚举类短名.枚举值name 为 code 尝试获取翻译值
+        i18nCode = String.format("%s.%s", enumField.getDeclaringClass().getSimpleName(), enumField.name());
+        String i18nMessage = messageOf(i18nCode, templateParams);
+        if (StrUtil.isNotEmpty(i18nMessage)) {
+            return i18nMessage;
+        }
+
+        // 尝试以 枚举值name 为 code 尝试获取翻译值
+        i18nCode = enumField.name();
+        i18nMessage = messageOf(i18nCode, templateParams);
+        if (StrUtil.isNotEmpty(i18nMessage)) {
+            return i18nMessage;
+        }
+
+        // 以上都没找到,兜底:如果是 HelioBaseEnum 的实现,直接返回 label 值
+        if (HelioBaseEnum.class.isAssignableFrom(enumField.getDeclaringClass())) {
+            return ((HelioBaseEnum<?>) enumField).getLabel();
+        }
+
+        // 最终兜底:返回枚举值 name
+        return enumField.name();
+    }
+}

+ 0 - 0
helio-starter-i18n/src/main/resources/PLACEHOLDER


+ 5 - 0
helio-starter-web/pom.xml

@@ -27,6 +27,11 @@
             <artifactId>helio-starter-aop</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-i18n</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>

+ 10 - 9
helio-starter-web/src/main/java/cc/uncarbon/framework/web/aspect/WebLoggingAspect.java

@@ -1,22 +1,23 @@
 package cc.uncarbon.framework.web.aspect;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-import javax.servlet.http.HttpServletRequest;
 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.annotation.Pointcut;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Component;
 import org.springframework.web.context.request.RequestAttributes;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
 
 /**
  * Web 请求及响应日志切面
@@ -25,8 +26,8 @@ import org.springframework.web.context.request.ServletRequestAttributes;
  */
 @Aspect
 @Slf4j
-@Component
-@ConditionalOnProperty(value = "helio.web.logging.enabled", havingValue = "true")
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "helio.web.logging", value = "enabled", havingValue = "true")
 public class WebLoggingAspect {
 
     /**

+ 68 - 22
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/AdviceExceptionAutoConfiguration.java → helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/GlobalWebExceptionHandlerAutoConfiguration.java

@@ -2,18 +2,20 @@ package cc.uncarbon.framework.web.config;
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.i18n.util.I18nUtil;
+import cc.uncarbon.framework.web.enums.GlobalWebExceptionI18nMessageEnum;
 import cc.uncarbon.framework.web.model.response.ApiResult;
 import cc.uncarbon.framework.web.util.InvalidFieldUtil;
 import cn.dev33.satoken.exception.NotLoginException;
 import cn.dev33.satoken.exception.NotPermissionException;
 import cn.dev33.satoken.exception.NotRoleException;
 import com.fasterxml.jackson.core.JsonParseException;
-import java.nio.charset.StandardCharsets;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.core.annotation.Order;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
@@ -28,6 +30,9 @@ import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
 import org.springframework.web.servlet.NoHandlerFoundException;
 
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.StandardCharsets;
+
 /**
  * Web 全局异常处理自动配置类
  *
@@ -37,12 +42,13 @@ import org.springframework.web.servlet.NoHandlerFoundException;
 @RestController
 @ControllerAdvice
 @AutoConfiguration
-// 主动标注为最低优先级,便于覆盖
-@Order
-public class AdviceExceptionAutoConfiguration {
+@ConditionalOnMissingBean(value = GlobalWebExceptionHandlerAutoConfiguration.class)
+@RequiredArgsConstructor
+public class GlobalWebExceptionHandlerAutoConfiguration {
 
     protected static final MediaType MEDIA_TYPE = new MediaType("application", "json", StandardCharsets.UTF_8);
     protected static final String DUBBO_PACKAGE_PREFIX = "org.apache.dubbo";
+    private final HelioProperties helioProperties;
 
 
     /**
@@ -50,9 +56,22 @@ public class AdviceExceptionAutoConfiguration {
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler({BusinessException.class})
-    public ResponseEntity<ApiResult<?>> handleBusinessException(BusinessException e, HttpServletRequest request, HttpServletResponse response) {
+    public ResponseEntity<ApiResult<?>> handleBusinessException(BusinessException e, HttpServletRequest request) {
         this.logError(e, request);
-        ApiResult<?> ret = ApiResult.fail(e.getCode(), e.getMessage());
+
+        /*
+        @since 1.7.2,国际化支持;详见文档:进阶使用-国际化
+         */
+        String msg;
+        if (helioProperties.getI18n().getEnabled() && e.getCustomEnumField() != null) {
+            // 如果启用了国际化,并制定了枚举值,则按国际化翻译值显示
+            msg = I18nUtil.messageOf(e.getCustomEnumField(), e.getTemplateParams());
+        } else {
+            // 按已有值显示
+            msg = e.getMessage();
+        }
+
+        ApiResult<?> ret = ApiResult.fail(e.getCode(), msg);
         return createResponseEntity(HttpStatus.BAD_REQUEST, ret);
     }
 
@@ -63,7 +82,9 @@ public class AdviceExceptionAutoConfiguration {
     @ExceptionHandler({NotLoginException.class})
     public ResponseEntity<ApiResult<?>> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
         this.logError(e, request);
-        ApiResult<?> ret = ApiResult.fail(HttpStatus.UNAUTHORIZED.value(), "请您先登录");
+
+        GlobalWebExceptionI18nMessageEnum msgEnum = GlobalWebExceptionI18nMessageEnum.GLOBAL__NO_LOGIN;
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.UNAUTHORIZED.value(), this.getI18nMessage(msgEnum));
         return createResponseEntity(HttpStatus.UNAUTHORIZED, ret);
     }
 
@@ -74,18 +95,22 @@ public class AdviceExceptionAutoConfiguration {
     @ExceptionHandler({NotPermissionException.class})
     public ResponseEntity<ApiResult<?>> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
         this.logError(e, request);
-        ApiResult<?> ret = ApiResult.fail(HttpStatus.FORBIDDEN.value(), "您权限不足");
+
+        GlobalWebExceptionI18nMessageEnum msgEnum = GlobalWebExceptionI18nMessageEnum.GLOBAL__PERMISSION_NOT_MATCH;
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.FORBIDDEN.value(), this.getI18nMessage(msgEnum));
         return createResponseEntity(HttpStatus.FORBIDDEN, ret);
     }
 
     /**
-     * 用户身份异常
+     * 用户角色异常
      */
     @ResponseStatus(HttpStatus.FORBIDDEN)
     @ExceptionHandler({NotRoleException.class})
     public ResponseEntity<ApiResult<?>> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
         this.logError(e, request);
-        ApiResult<?> ret = ApiResult.fail(HttpStatus.FORBIDDEN.value(), "您与要求角色不符");
+
+        GlobalWebExceptionI18nMessageEnum msgEnum = GlobalWebExceptionI18nMessageEnum.GLOBAL__ROLE_NOT_MATCH;
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.FORBIDDEN.value(), this.getI18nMessage(msgEnum));
         return createResponseEntity(HttpStatus.FORBIDDEN, ret);
     }
 
@@ -96,7 +121,9 @@ public class AdviceExceptionAutoConfiguration {
     @ExceptionHandler({NoHandlerFoundException.class})
     public ResponseEntity<ApiResult<?>> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
         this.logError(e, request);
-        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_FOUND.value(), "你迷路啦");
+
+        GlobalWebExceptionI18nMessageEnum msgEnum = GlobalWebExceptionI18nMessageEnum.GLOBAL__NOT_FOUND;
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_FOUND.value(), this.getI18nMessage(msgEnum));
         return createResponseEntity(HttpStatus.NOT_FOUND, ret);
     }
 
@@ -104,10 +131,10 @@ public class AdviceExceptionAutoConfiguration {
      * JsonParseException, HttpMessageNotReadableException
      * Jackson反序列化异常
      * 通常是因为JSON格式错误,或枚举输入值超出范围
-     *
+     * <p>
      * IllegalArgumentException
      * 不合法的参数异常
-     *
+     * <p>
      * MethodArgumentTypeMismatchException
      * "@PathVariable" 注解收参类型为 Long,但传的是 String
      */
@@ -115,8 +142,10 @@ public class AdviceExceptionAutoConfiguration {
     @ExceptionHandler({JsonParseException.class, HttpMessageNotReadableException.class, IllegalArgumentException.class, MethodArgumentTypeMismatchException.class})
     public ResponseEntity<ApiResult<?>> handleJsonParseException(Exception e, HttpServletRequest request) {
         this.logError(e, request);
-        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_ACCEPTABLE.value(), "错误参数格式或值");
-        return createResponseEntity(HttpStatus.BAD_REQUEST, ret);
+
+        GlobalWebExceptionI18nMessageEnum msgEnum = GlobalWebExceptionI18nMessageEnum.GLOBAL__UNACCEPTABLE_PARAMETERS;
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_ACCEPTABLE.value(), this.getI18nMessage(msgEnum));
+        return createResponseEntity(HttpStatus.NOT_ACCEPTABLE, ret);
     }
 
     /**
@@ -127,8 +156,10 @@ public class AdviceExceptionAutoConfiguration {
     @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
     public ResponseEntity<ApiResult<?>> handleBindException(BindException e, HttpServletRequest request) {
         this.logError(e, request);
-        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_ACCEPTABLE.value(), "错误参数格式或值", InvalidFieldUtil.getInvalidField(e.getBindingResult()));
-        return createResponseEntity(HttpStatus.BAD_REQUEST, ret);
+
+        GlobalWebExceptionI18nMessageEnum msgEnum = GlobalWebExceptionI18nMessageEnum.GLOBAL__UNACCEPTABLE_PARAMETERS;
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.NOT_ACCEPTABLE.value(), this.getI18nMessage(msgEnum), InvalidFieldUtil.getInvalidField(e.getBindingResult()));
+        return createResponseEntity(HttpStatus.NOT_ACCEPTABLE, ret);
     }
 
     /**
@@ -138,7 +169,9 @@ public class AdviceExceptionAutoConfiguration {
     @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
     public ResponseEntity<ApiResult<?>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
         this.logError(e, request);
-        ApiResult<?> ret = ApiResult.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), "错误的请求方式");
+
+        GlobalWebExceptionI18nMessageEnum msgEnum = GlobalWebExceptionI18nMessageEnum.GLOBAL__METHOD_NOT_ALLOWED;
+        ApiResult<?> ret = ApiResult.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), this.getI18nMessage(msgEnum));
         return createResponseEntity(HttpStatus.METHOD_NOT_ALLOWED, ret);
     }
 
@@ -160,7 +193,8 @@ public class AdviceExceptionAutoConfiguration {
             responseCode = HelioConstant.Dubbo.RPC_EXCEPTION_RESPONSE_CODE;
         }
 
-        ApiResult<?> ret = ApiResult.fail(responseCode, "请稍后再试");
+        GlobalWebExceptionI18nMessageEnum msgEnum = GlobalWebExceptionI18nMessageEnum.GLOBAL__INTERNAL_ERROR;
+        ApiResult<?> ret = ApiResult.fail(responseCode, this.getI18nMessage(msgEnum));
         return createResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, ret);
     }
 
@@ -171,4 +205,16 @@ public class AdviceExceptionAutoConfiguration {
     protected static ResponseEntity<ApiResult<?>> createResponseEntity(HttpStatus httpStatus, ApiResult<?> body) {
         return ResponseEntity.status(httpStatus.value()).contentType(MEDIA_TYPE).body(body);
     }
+
+    /**
+     * 取国际化翻译值或默认消息,取决于是否实际启用了国际化功能
+     * @param msgEnum 全局异常处理国际化消息枚举
+     * @return 消息文本
+     */
+    protected String getI18nMessage(@NonNull GlobalWebExceptionI18nMessageEnum msgEnum) {
+        if (helioProperties.getI18n().getEnabled()) {
+            return I18nUtil.messageOf(msgEnum.i18nCode(), msgEnum.getDefaultValue());
+        }
+        return msgEnum.getDefaultValue();
+    }
 }

+ 0 - 19
helio-starter-web/src/main/java/cc/uncarbon/framework/web/enums/ErrorResponseEnum.java

@@ -1,19 +0,0 @@
-package cc.uncarbon.framework.web.enums;
-
-
-import cc.uncarbon.framework.core.enums.HelioBaseEnum;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-@AllArgsConstructor
-@Getter
-public enum ErrorResponseEnum implements HelioBaseEnum<Integer> {
-
-    ILLEGAL_ENUM_VALUE(400, "非法枚举值"),
-    CONTAINS_ILLEGAL_CHARACTER(400, "包含非法字符"),
-
-    ;
-
-    private final Integer value;
-    private final String label;
-}

+ 35 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/enums/GlobalWebExceptionI18nMessageEnum.java

@@ -0,0 +1,35 @@
+package cc.uncarbon.framework.web.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Web 全局异常处理国际化消息枚举
+ * 枚举值name 同时视为 i18nCode
+ *
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum GlobalWebExceptionI18nMessageEnum {
+
+    GLOBAL__NO_LOGIN("请您先登录"),
+    GLOBAL__PERMISSION_NOT_MATCH("您权限不足"),
+    GLOBAL__ROLE_NOT_MATCH("您与要求角色不符"),
+    GLOBAL__NOT_FOUND("你迷路啦"),
+    GLOBAL__UNACCEPTABLE_PARAMETERS("错误参数格式或值"),
+    GLOBAL__METHOD_NOT_ALLOWED("错误的请求方式"),
+
+    /**
+     * 一般为最后兜底使用
+     */
+    GLOBAL__INTERNAL_ERROR("请稍后再试"),
+
+    ;
+
+    private final String defaultValue;
+
+    public String i18nCode() {
+        return name();
+    }
+}

+ 1 - 2
helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/EnumConverterFactory.java

@@ -2,7 +2,6 @@ package cc.uncarbon.framework.web.jackson;
 
 import cc.uncarbon.framework.core.enums.HelioBaseEnum;
 import cc.uncarbon.framework.core.exception.BusinessException;
-import cc.uncarbon.framework.web.enums.ErrorResponseEnum;
 import lombok.NonNull;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.convert.converter.ConverterFactory;
@@ -36,7 +35,7 @@ public class EnumConverterFactory implements ConverterFactory<String, HelioBaseE
 
         @Override
         public T convert(@NonNull Object value) {
-            return HelioBaseEnum.of(this.enumType, value).orElseThrow(() -> new BusinessException(ErrorResponseEnum.ILLEGAL_ENUM_VALUE));
+            return HelioBaseEnum.of(this.enumType, value).orElseThrow(() -> new BusinessException("Contains illegal enumeration value"));
         }
     }
 }

+ 5 - 4
helio-starter-web/src/main/java/cc/uncarbon/framework/web/jackson/EnumModule.java

@@ -54,13 +54,14 @@ public class EnumModule extends SimpleModule {
 
             @Override
             public E deserialize(JsonParser parser, DeserializationContext context) throws IOException {
-                // 前台如果传递只传value
                 if (parser.getCurrentToken().isNumeric()) {
-                    return HelioBaseEnum.of(this.enumType, parser.getIntValue()).orElseThrow(() -> new IllegalArgumentException("非法输入值"));
+                    // 前端传递value
+                    return HelioBaseEnum.of(this.enumType, parser.getIntValue()).orElseThrow(() -> new IllegalArgumentException("Unable to parse input value"));
                 } else if (StrUtil.isNotBlank(parser.getText())) {
-                    return HelioBaseEnum.of(this.enumType, parser.getText()).orElseThrow(() -> new IllegalArgumentException("非法输入值"));
+                    // 前端传递label
+                    return HelioBaseEnum.of(this.enumType, parser.getText()).orElseThrow(() -> new IllegalArgumentException("Unable to parse input value"));
                 } else {
-                    throw new IllegalArgumentException("枚举值类型错误");
+                    throw new IllegalArgumentException("Unable to parse input value 'cause wrong type");
                 }
             }
         }

+ 3 - 4
helio-starter-web/src/main/java/cc/uncarbon/framework/web/xss/SQLFilter.java

@@ -1,7 +1,6 @@
 package cc.uncarbon.framework.web.xss;
 
 import cc.uncarbon.framework.core.exception.BusinessException;
-import cc.uncarbon.framework.web.enums.ErrorResponseEnum;
 import cn.hutool.core.util.StrUtil;
 import org.springframework.util.StringUtils;
 
@@ -33,9 +32,9 @@ public class SQLFilter {
         String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alter", "drop"};
 
         // 判断是否包含非法字符
-        for(String keyword : keywords){
-            if(str.contains(keyword)){
-                throw new BusinessException(ErrorResponseEnum.CONTAINS_ILLEGAL_CHARACTER);
+        for (String keyword : keywords) {
+            if (str.contains(keyword)) {
+                throw new BusinessException("Contains illegal character");
             }
         }
 

+ 2 - 1
helio-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1,6 +1,7 @@
 cc.uncarbon.framework.web.listener.LaunchEventListener
 cc.uncarbon.framework.web.config.DefaultWebMvcAutoConfiguration
-cc.uncarbon.framework.web.config.AdviceExceptionAutoConfiguration
+cc.uncarbon.framework.web.config.GlobalWebExceptionHandlerAutoConfiguration
 cc.uncarbon.framework.web.config.JacksonExtendAutoConfiguration
 cc.uncarbon.framework.web.config.MessageConverterAutoConfiguration
 cc.uncarbon.framework.web.config.XssAutoConfiguration
+cc.uncarbon.framework.web.aspect.WebLoggingAspect

+ 16 - 24
pom.xml

@@ -10,6 +10,7 @@
         <module>helio-starter-cloud</module>
         <module>helio-starter-crud</module>
         <module>helio-starter-dubbo</module>
+        <module>helio-starter-i18n</module>
         <module>helio-starter-knife4j</module>
         <module>helio-starter-redis</module>
         <module>helio-starter-rocketmq-aliyun</module>
@@ -23,7 +24,7 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.7.4</version>
+        <version>2.7.6</version>
         <relativePath/>
     </parent>
 
@@ -33,12 +34,12 @@
 
     <properties>
         <!-- core versions -->
-        <revision>1.7.1</revision>
+        <revision>1.7.2</revision>
         <helio-starters.version>${revision}</helio-starters.version>
         <!-- JDK8则改为'1.8'  JDK11则改为'11' -->
         <java.version>1.8</java.version>
         <!-- 若变更Spring Boot版本, 上面parent依赖版本也要变 -->
-        <spring-boot.version>2.7.4</spring-boot.version>
+        <spring-boot.version>2.7.6</spring-boot.version>
 
 
         <!-- Spring Cloud versions -->
@@ -48,19 +49,19 @@
 
         <!-- Spring Cloud Alibaba versions -->
         <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
-        <dubbo.version>3.1.1</dubbo.version>
+        <dubbo.version>3.1.2</dubbo.version>
         <seata.vesion>1.5.2</seata.vesion>
-        <nacos-client.vesion>2.1.1</nacos-client.vesion>
+        <nacos-client.vesion>2.1.2</nacos-client.vesion>
 
 
         <!-- 3rd-party versions -->
-        <hutool.version>5.8.7</hutool.version>
-        <sa-token.version>1.31.0</sa-token.version>
-        <transmittable-thread-local.version>2.12.1</transmittable-thread-local.version>
+        <hutool.version>5.8.10</hutool.version>
+        <sa-token.version>1.33.0</sa-token.version>
+        <transmittable-thread-local.version>2.14.2</transmittable-thread-local.version>
         <!-- 如果 mp 较新版存在问题,可以回退至 3.4.3.4 试试  -->
         <mybatis-plus.version>3.5.2</mybatis-plus.version>
         <knife4j.version>3.0.3</knife4j.version>
-        <redisson.version>3.17.6</redisson.version>
+        <redisson.version>3.18.0</redisson.version>
         <aspectj.version>1.9.6</aspectj.version>
         <ons-client.version>1.8.8.1.Final</ons-client.version>
         <guava.version>31.1-jre</guava.version>
@@ -70,9 +71,6 @@
         <!-- DB Driver versions -->
         <mysql.version>8.0.28</mysql.version>
         <postgresql.version>42.3.4</postgresql.version>
-        <!-- 以下2项版本较旧, 请根据 JDK 版本自行更新 -->
-        <mssql.version>4.0</mssql.version>
-        <oracle.version>11.2.0.3</oracle.version>
 
 
         <!-- Maven properties -->
@@ -134,6 +132,12 @@
                 <version>${helio-starters.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-i18n</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>cc.uncarbon.framework</groupId>
                 <artifactId>helio-starter-knife4j</artifactId>
@@ -292,18 +296,6 @@
                 <version>${postgresql.version}</version>
             </dependency>
 
-            <dependency>
-                <groupId>com.oracle</groupId>
-                <artifactId>ojdbc6</artifactId>
-                <version>${oracle.version}</version>
-            </dependency>
-
-            <dependency>
-                <groupId>com.microsoft.sqlserver</groupId>
-                <artifactId>sqljdbc4</artifactId>
-                <version>${mssql.version}</version>
-            </dependency>
-
             <dependency>
                 <groupId>com.baomidou</groupId>
                 <artifactId>mybatis-plus-boot-starter</artifactId>