浏览代码

Merge branch 'i18n-support'

zhou-hao 3 年之前
父节点
当前提交
cff805d04a
共有 48 个文件被更改,包括 1277 次插入173 次删除
  1. 1 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java
  2. 10 2
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java
  3. 6 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java
  4. 2 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java
  5. 1 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java
  6. 7 7
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenState.java
  7. 1 1
      hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java
  8. 16 0
      hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en_US.properties
  9. 16 0
      hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh_CN.properties
  10. 1 1
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java
  11. 3 4
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java
  12. 3 4
      hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java
  13. 1 1
      hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java
  14. 2 2
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java
  15. 34 15
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java
  16. 77 40
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java
  17. 15 5
      hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java
  18. 5 0
      hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en_US.properties
  19. 5 0
      hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties
  20. 5 1
      hsweb-core/pom.xml
  21. 41 33
      hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java
  22. 8 8
      hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java
  23. 40 0
      hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java
  24. 3 3
      hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java
  25. 40 16
      hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java
  26. 13 0
      hsweb-core/src/main/java/org/hswebframework/web/i18n/ContextLocaleResolver.java
  27. 182 0
      hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java
  28. 12 0
      hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java
  29. 31 0
      hsweb-core/src/main/java/org/hswebframework/web/i18n/UnsupportedMessageSource.java
  30. 34 0
      hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java
  31. 14 7
      hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java
  32. 4 0
      hsweb-core/src/main/resources/i18n/core/messages_en_US.properties
  33. 5 0
      hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties
  34. 42 0
      hsweb-core/src/test/java/org/hswebframework/web/validator/ValidatorUtilsTest.java
  35. 71 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java
  36. 56 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java
  37. 1 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java
  38. 27 13
      hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2JsonDecoder.java
  39. 340 0
      hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java
  40. 2 1
      hsweb-starter/src/main/resources/META-INF/spring.factories
  41. 88 0
      hsweb-starter/src/test/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoderTest.java
  42. 2 0
      hsweb-starter/src/test/resources/i18n/messages_en_US.properties
  43. 2 0
      hsweb-starter/src/test/resources/i18n/messages_zh_CN.properties
  44. 1 1
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/PermissionProperties.java
  45. 1 1
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionUserService.java
  46. 2 3
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java
  47. 2 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en_US.properties
  48. 2 0
      hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh_CN.properties

+ 1 - 1
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java

@@ -55,5 +55,5 @@ public @interface TwoFactor {
      * @return 错误提示
      * @since 3.0.6
      */
-    String message() default "需要进行双因子验证";
+    String message() default "validation.verify_code_error";
 }

+ 10 - 2
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java

@@ -1,9 +1,12 @@
 package org.hswebframework.web.authorization.exception;
 
 import lombok.Getter;
+import org.hswebframework.web.exception.I18nSupportException;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.ResponseStatus;
 
+import java.util.Set;
+
 /**
  * 权限验证异常
  *
@@ -11,7 +14,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
  * @since 3.0
  */
 @ResponseStatus(HttpStatus.FORBIDDEN)
-public class AccessDenyException extends RuntimeException {
+public class AccessDenyException extends I18nSupportException {
 
     private static final long serialVersionUID = -5135300127303801430L;
 
@@ -19,16 +22,21 @@ public class AccessDenyException extends RuntimeException {
     private String code;
 
     public AccessDenyException() {
-        this("权限不足,拒绝访问!");
+        this("error.access_denied");
     }
 
     public AccessDenyException(String message) {
         super(message);
     }
 
+    public AccessDenyException(String permission, Set<String> actions) {
+        super("error.permission_denied", permission, actions);
+    }
+
     public AccessDenyException(String message, String code) {
         this(message, code, null);
     }
+
     public AccessDenyException(String message, Throwable cause) {
         this(message, "access_denied", cause);
     }

+ 6 - 1
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java

@@ -1,9 +1,10 @@
 package org.hswebframework.web.authorization.exception;
 
 import lombok.Getter;
+import org.hswebframework.web.exception.I18nSupportException;
 
 @Getter
-public class AuthenticationException extends RuntimeException {
+public class AuthenticationException extends I18nSupportException {
 
 
     public static String ILLEGAL_PASSWORD = "illegal_password";
@@ -13,6 +14,10 @@ public class AuthenticationException extends RuntimeException {
 
     private final String code;
 
+    public AuthenticationException(String code) {
+        this(code, "error." + code);
+    }
+
     public AuthenticationException(String code, String message) {
         super(message);
         this.code = code;

+ 2 - 1
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java

@@ -19,6 +19,7 @@
 package org.hswebframework.web.authorization.exception;
 
 import org.hswebframework.web.authorization.token.TokenState;
+import org.hswebframework.web.exception.I18nSupportException;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.ResponseStatus;
 
@@ -29,7 +30,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
  * @since 3.0
  */
 @ResponseStatus(HttpStatus.UNAUTHORIZED)
-public class UnAuthorizedException extends RuntimeException {
+public class UnAuthorizedException extends I18nSupportException {
     private static final long serialVersionUID = 2422918455013900645L;
 
     private final TokenState state;

+ 1 - 1
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java

@@ -253,7 +253,7 @@ public class DefaultUserTokenManager implements UserTokenManager {
                         .flatMap(this::checkTimeout)
                         .filterWhen(t -> {
                             if (t.isNormal()) {
-                                return Mono.error(new AccessDenyException("该用户已在其他地方登陆"));
+                                return Mono.error(new AccessDenyException("error.logged_in_elsewhere"));
                             }
                             return Mono.empty();
                         })

+ 7 - 7
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenState.java

@@ -13,30 +13,30 @@ public enum TokenState implements EnumDict<String> {
     /**
      * 正常,有效
      */
-    normal("normal","正常"),
+    normal("normal","message.token_state_normal"),
 
     /**
      * 已被禁止访问
      */
-    deny("deny", "已被禁止访问"),
+    deny("deny", "message.token_state_deny"),
 
     /**
      * 已过期
      */
-    expired("expired", "用户未登录"),
+    expired("expired", "message.token_state_expired"),
 
     /**
      * 已被踢下线
      * @see AllopatricLoginMode#offlineOther
      */
-    offline("offline", "用户已在其他地方登录"),
+    offline("offline", "message.token_state_offline"),
 
     /**
      * 锁定
      */
-    lock("lock", "登录状态已被锁定");
+    lock("lock", "message.token_state_lock");
 
-    private String value;
+    private final String value;
 
-    private String text;
+    private final String text;
 }

+ 1 - 1
hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java

@@ -244,7 +244,7 @@ public class RedisUserTokenManager implements UserTokenManager {
                         return userIsLoggedIn(userId)
                                 .flatMap(r -> {
                                     if (r) {
-                                        return Mono.error(new AccessDenyException("已在其他地方登录", TokenState.deny.getValue(), null));
+                                        return Mono.error(new AccessDenyException("error.logged_in_elsewhere", TokenState.deny.getValue(), null));
                                     }
                                     return doSign;
                                 });

+ 16 - 0
hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en_US.properties

@@ -0,0 +1,16 @@
+error.access_denied=Access Denied
+error.permission_denied=Permission Denied [{0}]:{1}
+error.logged_in_elsewhere=User logged in elsewhere
+error.illegal_password=Bad username or password
+error.user_disabled=User is disabled
+#
+message.token_state_normal=Normal
+message.token_state_deny=Login has denied
+message.token_state_expired=Login has expired
+message.token_state_offline=User logged in elsewhere
+message.token_state_lock=User Locked
+#
+validation.need_two_factor_verify=Two factor verification required
+validation.username_must_not_be_empty=Username must not be empty
+validation.password_must_not_be_empty=Password must not be empty
+validation.verify_code_error=Verification code error

+ 16 - 0
hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh_CN.properties

@@ -0,0 +1,16 @@
+error.access_denied=权限不足,拒绝访问!
+error.permission_denied=当前用户无权限[{0}]:{1}
+error.logged_in_elsewhere=该用户已在其他地方登陆
+error.illegal_password=用户名或密码错误
+error.user_disabled=用户已被禁用
+#
+message.token_state_normal=正常
+message.token_state_deny=已被禁止访问
+message.token_state_expired=用户未登录
+message.token_state_offline=用户已在其他地方登录
+message.token_state_lock=登录状态已被锁定
+#
+validation.need_two_factor_verify=需要双因子验证
+validation.username_must_not_be_empty=用户名不能为空
+validation.password_must_not_be_empty=密码不能为空
+validation.verify_code_error=验证码错误

+ 1 - 1
hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java

@@ -36,7 +36,7 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
     private ResourcesDefinition resources = new ResourcesDefinition();
     private DimensionsDefinition dimensions = new DimensionsDefinition();
 
-    private String message = "权限不足,拒绝访问";
+    private String message = "error.access_denied";
 
     private Phased phased;
 

+ 3 - 4
hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java

@@ -7,7 +7,6 @@ import org.hswebframework.web.authorization.annotation.TwoFactor;
 import org.hswebframework.web.authorization.exception.NeedTwoFactorException;
 import org.hswebframework.web.authorization.twofactor.TwoFactorValidator;
 import org.hswebframework.web.authorization.twofactor.TwoFactorValidatorManager;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.StringUtils;
 import org.springframework.web.method.HandlerMethod;
 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@@ -22,7 +21,7 @@ import javax.servlet.http.HttpServletResponse;
 @AllArgsConstructor
 public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapter {
 
-    private TwoFactorValidatorManager validatorManager;
+    private final TwoFactorValidatorManager validatorManager;
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
@@ -45,9 +44,9 @@ public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapte
                 code = request.getHeader(factor.parameter());
             }
             if (StringUtils.isEmpty(code)) {
-                throw new NeedTwoFactorException(factor.message(), factor.provider());
+                throw new NeedTwoFactorException("validation.need_two_factor_verify", factor.provider());
             } else if (!validator.verify(code, factor.timeout())) {
-                throw new NeedTwoFactorException("验证码错误", factor.provider());
+                throw new NeedTwoFactorException(factor.message(), factor.provider());
             }
         }
         return super.preHandle(request, response, handler);

+ 3 - 4
hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java

@@ -34,7 +34,6 @@ import org.hswebframework.web.authorization.simple.PlainTextUsernamePasswordAuth
 import org.hswebframework.web.logging.AccessLogger;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.context.event.EventListener;
 import org.springframework.http.MediaType;
 import org.springframework.util.Assert;
 import org.springframework.web.bind.annotation.*;
@@ -85,8 +84,8 @@ public class AuthorizationController {
             String username_ = (String) parameters.get("username");
             String password_ = (String) parameters.get("password");
 
-            Assert.hasLength(username_, "用户名不能为空");
-            Assert.hasLength(password_, "密码不能为空");
+            Assert.hasLength(username_, "validation.username_must_not_be_empty");
+            Assert.hasLength(password_, "validation.password_must_not_be_empty");
 
             Function<String, Object> parameterGetter = parameters::get;
             return Mono.defer(() -> {
@@ -101,7 +100,7 @@ public class AuthorizationController {
                                     .publish(eventPublisher)
                                     .then(authenticationManager
                                             .authenticate(Mono.just(new PlainTextUsernamePasswordAuthenticationRequest(username, password)))
-                                            .switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD,"密码错误")))
+                                            .switchIfEmpty(Mono.error(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD)))
                                             .flatMap(auth -> {
                                                 //触发授权成功事件
                                                 AuthorizationSuccessEvent event = new AuthorizationSuccessEvent(auth, parameterGetter);

+ 1 - 1
hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java

@@ -8,7 +8,7 @@ public class OAuth2Exception extends BusinessException {
     private final ErrorType type;
 
     public OAuth2Exception(ErrorType type) {
-        super(type.message(), type.name(), type.code());
+        super(type.message(), type.name(), type.code(), (Object[]) null);
         this.type = type;
     }
 

+ 2 - 2
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java

@@ -124,7 +124,7 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory {
         if (realType == null) {
             if (!Modifier.isInterface(beanClass.getModifiers()) && !Modifier.isAbstract(beanClass.getModifiers())) {
                 realType = beanClass;
-            }else {
+            } else {
                 mapper = defaultMapperFactory.apply(beanClass);
             }
         }
@@ -172,7 +172,7 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory {
             return (T) new HashSet<>();
         }
 
-        throw new NotFoundException("无法初始化实体类:"+beanClass);
+        throw new NotFoundException("error.cant_create_instance", beanClass);
     }
 
     @Override

+ 34 - 15
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java

@@ -5,11 +5,14 @@ import org.hswebframework.ezorm.rdb.events.EventListener;
 import org.hswebframework.ezorm.rdb.events.EventType;
 import org.hswebframework.ezorm.rdb.mapping.events.MappingContextKeys;
 import org.hswebframework.ezorm.rdb.mapping.events.MappingEventTypes;
+import org.hswebframework.ezorm.rdb.mapping.events.ReactiveResultHolder;
 import org.hswebframework.web.api.crud.entity.Entity;
+import org.hswebframework.web.i18n.LocaleUtils;
 import org.hswebframework.web.validator.CreateGroup;
 import org.hswebframework.web.validator.UpdateGroup;
 
 import java.util.List;
+import java.util.Optional;
 
 public class ValidateEventListener implements EventListener {
 
@@ -24,33 +27,49 @@ public class ValidateEventListener implements EventListener {
     }
 
     @Override
-    @SuppressWarnings("all")
+
     public void onEvent(EventType type, EventContext context) {
+        Optional<ReactiveResultHolder> resultHolder = context.get(MappingContextKeys.reactiveResultHolder);
+
+        if (resultHolder.isPresent()) {
+            resultHolder
+                    .ifPresent(holder -> holder
+                            .before(LocaleUtils
+                                            .currentReactive()
+                                            .doOnNext(locale -> LocaleUtils.doWith(locale, (l) -> tryValidate(type, context)))
+                                            .then()
+                            ));
+        } else {
+            tryValidate(type, context);
+        }
+    }
+
+    @SuppressWarnings("all")
+    public void tryValidate(EventType type, EventContext context) {
         if (type == MappingEventTypes.insert_before || type == MappingEventTypes.save_before) {
 
             boolean single = context.get(MappingContextKeys.type).map("single"::equals).orElse(false);
             if (single) {
                 context.get(MappingContextKeys.instance)
-                        .filter(Entity.class::isInstance)
-                        .map(Entity.class::cast)
-                        .ifPresent(entity -> entity.tryValidate(CreateGroup.class));
+                       .filter(Entity.class::isInstance)
+                       .map(Entity.class::cast)
+                       .ifPresent(entity -> entity.tryValidate(CreateGroup.class));
             } else {
                 context.get(MappingContextKeys.instance)
-                        .filter(List.class::isInstance)
-                        .map(List.class::cast)
-                        .ifPresent(lst -> lst.stream()
-                                .filter(Entity.class::isInstance)
-                                .map(Entity.class::cast)
-                                .forEach(e -> ((Entity) e).tryValidate(CreateGroup.class))
-                        );
+                       .filter(List.class::isInstance)
+                       .map(List.class::cast)
+                       .ifPresent(lst -> lst.stream()
+                                            .filter(Entity.class::isInstance)
+                                            .map(Entity.class::cast)
+                                            .forEach(e -> ((Entity) e).tryValidate(CreateGroup.class))
+                       );
             }
 
         } else if (type == MappingEventTypes.update_before) {
             context.get(MappingContextKeys.instance)
-                    .filter(Entity.class::isInstance)
-                    .map(Entity.class::cast)
-                    .ifPresent(entity -> entity.tryValidate(UpdateGroup.class));
+                   .filter(Entity.class::isInstance)
+                   .map(Entity.class::cast)
+                   .ifPresent(entity -> entity.tryValidate(UpdateGroup.class));
         }
-
     }
 }

+ 77 - 40
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java

@@ -1,7 +1,6 @@
 package org.hswebframework.web.crud.web;
 
 import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
-import io.r2dbc.spi.R2dbcNonTransientException;
 import lombok.extern.slf4j.Slf4j;
 import org.hswebframework.web.authorization.exception.AccessDenyException;
 import org.hswebframework.web.authorization.exception.AuthenticationException;
@@ -10,11 +9,11 @@ import org.hswebframework.web.authorization.token.TokenState;
 import org.hswebframework.web.exception.BusinessException;
 import org.hswebframework.web.exception.NotFoundException;
 import org.hswebframework.web.exception.ValidationException;
+import org.hswebframework.web.i18n.LocaleUtils;
 import org.hswebframework.web.logger.ReactiveLogger;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
-import org.springframework.core.Ordered;
+import org.springframework.context.MessageSource;
 import org.springframework.core.annotation.Order;
-import org.springframework.core.codec.DecodingException;
 import org.springframework.http.HttpStatus;
 import org.springframework.validation.BindException;
 import org.springframework.validation.FieldError;
@@ -23,10 +22,7 @@ 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.bind.support.WebExchangeBindException;
-import org.springframework.web.server.MediaTypeNotSupportedStatusException;
-import org.springframework.web.server.MethodNotAllowedException;
-import org.springframework.web.server.NotAcceptableStatusException;
-import org.springframework.web.server.ServerWebInputException;
+import org.springframework.web.server.*;
 import reactor.core.publisher.Mono;
 
 import javax.validation.ConstraintViolationException;
@@ -40,48 +36,66 @@ import java.util.stream.Collectors;
 @Order
 public class CommonErrorControllerAdvice {
 
+    private final MessageSource messageSource;
+
+    public CommonErrorControllerAdvice(MessageSource messageSource) {
+        this.messageSource = messageSource;
+    }
+
     @ExceptionHandler
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     public Mono<ResponseMessage<Object>> handleException(BusinessException e) {
-        return Mono.just(ResponseMessage.error(e.getCode(), e.getMessage()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+        return LocaleUtils
+                .resolveThrowable(messageSource,
+                                  e,
+                                  (err, msg) -> ResponseMessage.error(err.getStatus(), err.getCode(), msg));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     public Mono<ResponseMessage<?>> handleException(UnsupportedOperationException e) {
-        return Mono.just(ResponseMessage.error("unsupported", e.getMessage()));
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.<TokenState>error(401, "unsupported", msg)));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.UNAUTHORIZED)
     public Mono<ResponseMessage<TokenState>> handleException(UnAuthorizedException e) {
-        return Mono.just(ResponseMessage.<TokenState>error(401, "unauthorized", e.getMessage()).result(e.getState()));
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.<TokenState>error(401, "unauthorized", msg)
+                        .result(e.getState())));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.FORBIDDEN)
-    public Mono<ResponseMessage<?>> handleException(AccessDenyException e) {
-        return Mono.just(ResponseMessage.error(403, e.getCode(), e.getMessage()));
+    public Mono<ResponseMessage<Object>> handleException(AccessDenyException e) {
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(403, e.getCode(), e.getMessage()))
+                ;
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.NOT_FOUND)
-    public Mono<ResponseMessage<?>> handleException(NotFoundException e) {
-        return Mono.just(ResponseMessage.error(404, "not_found", e.getMessage()));
+    public Mono<ResponseMessage<Object>> handleException(NotFoundException e) {
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(404, "not_found", msg))
+                ;
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public Mono<ResponseMessage<List<ValidationException.Detail>>> handleException(ValidationException e) {
-        return Mono.just(ResponseMessage.<List<ValidationException.Detail>>error(400, "illegal_argument", e.getMessage())
-                                 .result(e.getDetails()));
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage
+                        .<List<ValidationException.Detail>>error(400, "illegal_argument",msg)
+                        .result(e.getDetails()))
+                ;
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public Mono<ResponseMessage<List<ValidationException.Detail>>> handleException(ConstraintViolationException e) {
-        return handleException(new ValidationException(e.getMessage(), e.getConstraintViolations()));
+        return handleException(new ValidationException(e.getConstraintViolations()));
     }
 
     @ExceptionHandler
@@ -130,6 +144,7 @@ public class CommonErrorControllerAdvice {
     @ExceptionHandler
     @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
     public Mono<ResponseMessage<Object>> handleException(TimeoutException e) {
+
         return Mono.just(ResponseMessage.error(504, "timeout", e.getMessage()))
                    .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
 
@@ -155,51 +170,70 @@ public class CommonErrorControllerAdvice {
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public Mono<ResponseMessage<Object>> handleException(IllegalArgumentException e) {
-        return Mono.just(ResponseMessage.error(400, "illegal_argument", e.getMessage()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(400, "illegal_argument", msg))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)))
+                ;
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public Mono<ResponseMessage<Object>> handleException(AuthenticationException e) {
-        return Mono.just(ResponseMessage.error(400, e.getCode(), e.getMessage()));
+        return LocaleUtils
+                .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(400, e.getCode(), msg))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e)))
+                ;
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
-    public Mono<ResponseMessage<Object>> handleException(MediaTypeNotSupportedStatusException e) {
-        return Mono.just(ResponseMessage
-                                 .error(415, "unsupported_media_type", "不支持的请求类型")
-                                 .result(e.getSupportedMediaTypes()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+    public Mono<ResponseMessage<Object>> handleException(UnsupportedMediaTypeStatusException e) {
+        return LocaleUtils
+                .resolveMessageReactive(messageSource, "error.unsupported_media_type")
+                .map(msg -> ResponseMessage
+                        .error(415, "unsupported_media_type", msg)
+                        .result(e.getSupportedMediaTypes()))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e)));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
     public Mono<ResponseMessage<Object>> handleException(NotAcceptableStatusException e) {
-        return Mono.just(ResponseMessage
-                                 .error(406, "not_acceptable_media_type", "不支持的响应类型")
-                                 .result(e.getSupportedMediaTypes()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+
+        return LocaleUtils
+                .resolveMessageReactive(messageSource, "error.not_acceptable_media_type")
+                .map(msg -> ResponseMessage
+                        .error(406, "not_acceptable_media_type", msg)
+                        .result(e.getSupportedMediaTypes()))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
     public Mono<ResponseMessage<Object>> handleException(MethodNotAllowedException e) {
-        return Mono.just(ResponseMessage
-                                 .error(405, "method_not_allowed", "不支持的请求方法:" + e.getHttpMethod())
-                                 .result(e.getSupportedMethods()))
-                   .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
+        return LocaleUtils
+                .resolveMessageReactive(messageSource, "error.method_not_allowed")
+                .map(msg -> ResponseMessage
+                        .error(406, "method_not_allowed", msg)
+                        .result(e.getSupportedMethods()))
+                .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e)));
     }
 
     @ExceptionHandler
     @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public Mono<ResponseMessage<Object>> handleException(R2dbcDataIntegrityViolationException exception) {
-        if (exception.getMessage().contains("Duplicate")) {
-            return Mono.just(ResponseMessage.error("存在重复的数据"));
+    public Mono<ResponseMessage<Object>> handleException(R2dbcDataIntegrityViolationException e) {
+        String code;
+
+        if (e.getMessage().contains("Duplicate")) {
+            code = "error.duplicate_data";
+        } else {
+            code = "error.data_error";
+            log.warn(e.getMessage(), e);
         }
-        log.warn(exception.getMessage(), exception);
-        return Mono.just(ResponseMessage.error("数据错误"));
+        return LocaleUtils
+                .resolveMessageReactive(messageSource, code)
+                .map(msg -> ResponseMessage.error(400, code, msg));
     }
 
 
@@ -215,7 +249,10 @@ public class CommonErrorControllerAdvice {
 
         } while (exception != null && exception != e);
 
-        return Mono.just(ResponseMessage.error(400, "illegal_argument", e.getMessage()));
+        return LocaleUtils
+                .resolveThrowable(messageSource,
+                                  exception,
+                                  (err, msg) -> ResponseMessage.error(400, "illegal_argument", msg));
     }
 
 }

+ 15 - 5
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java

@@ -1,14 +1,17 @@
 package org.hswebframework.web.crud.web;
 
+import org.hswebframework.web.i18n.WebFluxLocaleFilter;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.MessageSource;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.http.codec.ServerCodecConfigurer;
 import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
+import org.springframework.web.server.WebFilter;
 
 @Configuration
 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@@ -16,17 +19,24 @@ public class CommonWebFluxConfiguration {
 
     @Bean
     @ConditionalOnMissingBean
-    public CommonErrorControllerAdvice commonErrorControllerAdvice(){
-        return new CommonErrorControllerAdvice();
+    public CommonErrorControllerAdvice commonErrorControllerAdvice(MessageSource messageSource) {
+        return new CommonErrorControllerAdvice(messageSource);
     }
 
 
     @Bean
-    @ConditionalOnProperty(prefix = "hsweb.webflux.response-wrapper",name = "enabled",havingValue = "true",matchIfMissing = true)
+    @ConditionalOnProperty(prefix = "hsweb.webflux.response-wrapper", name = "enabled", havingValue = "true", matchIfMissing = true)
     @ConfigurationProperties(prefix = "hsweb.webflux.response-wrapper")
     public ResponseMessageWrapper responseMessageWrapper(ServerCodecConfigurer codecConfigurer,
                                                          RequestedContentTypeResolver resolver,
-                                                         ReactiveAdapterRegistry registry){
-        return new ResponseMessageWrapper(codecConfigurer.getWriters(),resolver,registry);
+                                                         ReactiveAdapterRegistry registry) {
+        return new ResponseMessageWrapper(codecConfigurer.getWriters(), resolver, registry);
     }
+
+
+    @Bean
+    public WebFilter localeWebFilter() {
+        return new WebFluxLocaleFilter();
+    }
+
 }

+ 5 - 0
hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en_US.properties

@@ -0,0 +1,5 @@
+error.unsupported_media_type=Unsupported media type
+error.not_acceptable_media_type=Not acceptable media type
+error.method_not_allowed=Method not allowed
+error.duplicate_data=Duplicate data
+error.data_error=Data error

+ 5 - 0
hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties

@@ -0,0 +1,5 @@
+error.unsupported_media_type=不支持的请求类型
+error.not_acceptable_media_type=不支持的媒体类型
+error.method_not_allowed=不支持的请求方法
+error.duplicate_data=重复的数据
+error.data_error=数据错误

+ 5 - 1
hsweb-core/pom.xml

@@ -35,7 +35,6 @@
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-web</artifactId>
-
         </dependency>
 
         <dependency>
@@ -97,5 +96,10 @@
             <version>3.0.0</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
+        </dependency>
+
     </dependencies>
 </project>

+ 41 - 33
hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java

@@ -8,7 +8,6 @@ import com.alibaba.fastjson.parser.JSONToken;
 import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
 import com.alibaba.fastjson.serializer.JSONSerializable;
 import com.alibaba.fastjson.serializer.JSONSerializer;
-import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonValue;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -21,6 +20,7 @@ import lombok.NoArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import org.hswebframework.web.exception.ValidationException;
+import org.hswebframework.web.i18n.LocaleUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.util.StringUtils;
 
@@ -118,7 +118,7 @@ public interface EnumDict<V> extends JSONSerializable {
     }
 
     /**
-     * 枚举选项的描述,对一个选项进行详细的描述有时候是必要的.默认值为{@link this#getText()}
+     * 枚举选项的描述,对一个选项进行详细的描述有时候是必要的.默认值为{@link EnumDict#getText()}
      *
      * @return 描述
      */
@@ -127,7 +127,6 @@ public interface EnumDict<V> extends JSONSerializable {
     }
 
 
-
     /**
      * 从指定的枚举类中查找想要的枚举,并返回一个{@link Optional},如果未找到,则返回一个{@link Optional#empty()}
      *
@@ -150,8 +149,8 @@ public interface EnumDict<V> extends JSONSerializable {
     static <T extends Enum & EnumDict> List<T> findList(Class<T> type, Predicate<T> predicate) {
         if (type.isEnum()) {
             return Arrays.stream(type.getEnumConstants())
-                    .filter(predicate)
-                    .collect(Collectors.toList());
+                         .filter(predicate)
+                         .collect(Collectors.toList());
         }
         return Collections.emptyList();
     }
@@ -159,16 +158,18 @@ public interface EnumDict<V> extends JSONSerializable {
     /**
      * 根据枚举的{@link EnumDict#getValue()}来查找.
      *
-     * @see this#find(Class, Predicate)
+     * @see EnumDict#find(Class, Predicate)
      */
     static <T extends Enum & EnumDict<?>> Optional<T> findByValue(Class<T> type, Object value) {
-        return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String.valueOf(e.getValue()).equalsIgnoreCase(String.valueOf(value)));
+        return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String
+                .valueOf(e.getValue())
+                .equalsIgnoreCase(String.valueOf(value)));
     }
 
     /**
      * 根据枚举的{@link EnumDict#getText()} 来查找.
      *
-     * @see this#find(Class, Predicate)
+     * @see EnumDict#find(Class, Predicate)
      */
     static <T extends Enum & EnumDict> Optional<T> findByText(Class<T> type, String text) {
         return find(type, e -> e.getText().equalsIgnoreCase(text));
@@ -177,7 +178,7 @@ public interface EnumDict<V> extends JSONSerializable {
     /**
      * 根据枚举的{@link EnumDict#getValue()},{@link EnumDict#getText()}来查找.
      *
-     * @see this#find(Class, Predicate)
+     * @see EnumDict#find(Class, Predicate)
      */
     static <T extends Enum & EnumDict> Optional<T> find(Class<T> type, Object target) {
         return find(type, v -> v.eq(target));
@@ -203,8 +204,8 @@ public interface EnumDict<V> extends JSONSerializable {
         if (all.length >= 64) {
             List<T> list = Arrays.asList(t);
             return Arrays.stream(all)
-                    .map(EnumDict.class::cast)
-                    .anyMatch(list::contains);
+                         .map(EnumDict.class::cast)
+                         .anyMatch(list::contains);
         }
         return maskIn(toMask(t), target);
     }
@@ -253,24 +254,32 @@ public interface EnumDict<V> extends JSONSerializable {
 
     /**
      * @return 是否在序列化为json的时候, 将枚举以对象方式序列化
-     * @see this#DEFAULT_WRITE_JSON_OBJECT
+     * @see EnumDict#DEFAULT_WRITE_JSON_OBJECT
      */
     default boolean isWriteJSONObjectEnabled() {
         return DEFAULT_WRITE_JSON_OBJECT;
     }
 
+    default String getI18nCode() {
+        return getText();
+    }
+
+    default String getI18nMessage(Locale locale) {
+        return LocaleUtils.resolveMessage(getI18nCode(), locale, getText());
+    }
+
     /**
-     * 当{@link this#isWriteJSONObjectEnabled()}返回true时,在序列化为json的时候,会写出此方法返回的对象
+     * 当{@link EnumDict#isWriteJSONObjectEnabled()}返回true时,在序列化为json的时候,会写出此方法返回的对象
      *
      * @return 最终序列化的值
-     * @see this#isWriteJSONObjectEnabled()
+     * @see EnumDict#isWriteJSONObjectEnabled()
      */
     @JsonValue
     default Object getWriteJSONObject() {
         if (isWriteJSONObjectEnabled()) {
             Map<String, Object> jsonObject = new HashMap<>();
             jsonObject.put("value", getValue());
-            jsonObject.put("text", getText());
+            jsonObject.put("text", getI18nMessage(LocaleUtils.current()));
             // jsonObject.put("index", index());
             // jsonObject.put("mask", getMask());
             return jsonObject;
@@ -280,7 +289,7 @@ public interface EnumDict<V> extends JSONSerializable {
     }
 
     @Override
-    default void write(JSONSerializer jsonSerializer, Object o, Type type, int i) throws IOException {
+    default void write(JSONSerializer jsonSerializer, Object o, Type type, int i) {
         if (isWriteJSONObjectEnabled()) {
             jsonSerializer.write(getWriteJSONObject());
         } else {
@@ -295,7 +304,7 @@ public interface EnumDict<V> extends JSONSerializable {
     @AllArgsConstructor
     @NoArgsConstructor
     class EnumDictJSONDeserializer extends JsonDeserializer implements ObjectDeserializer {
-        private Function<Object,Object> mapper;
+        private Function<Object, Object> mapper;
 
         @Override
         @SuppressWarnings("all")
@@ -324,8 +333,10 @@ public interface EnumDict<V> extends JSONSerializable {
                     value = parser.parse();
                     if (value instanceof Map) {
                         return (T) EnumDict.find(((Class) type), ((Map) value).get("value"))
-                                .orElseGet(() ->
-                                        EnumDict.find(((Class) type), ((Map) value).get("text")).orElse(null));
+                                           .orElseGet(() ->
+                                                              EnumDict
+                                                                      .find(((Class) type), ((Map) value).get("text"))
+                                                                      .orElse(null));
                     }
                 }
 
@@ -347,11 +358,11 @@ public interface EnumDict<V> extends JSONSerializable {
         @SneakyThrows
         public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
             JsonNode node = jp.getCodec().readTree(jp);
-            if(mapper!=null){
-                if(node.isTextual()){
+            if (mapper != null) {
+                if (node.isTextual()) {
                     return mapper.apply(node.asText());
                 }
-                if(node.isNumber()){
+                if (node.isNumber()) {
                     return mapper.apply(node.asLong());
                 }
             }
@@ -364,19 +375,17 @@ public interface EnumDict<V> extends JSONSerializable {
                 findPropertyType = BeanUtils.findPropertyType(currentName, currentValue.getClass());
             }
             Supplier<ValidationException> exceptionSupplier = () -> {
-               List<Object> values= Stream.of(findPropertyType.getEnumConstants())
+                List<Object> values = Stream
+                        .of(findPropertyType.getEnumConstants())
                         .map(Enum.class::cast)
-                        .map(e->{
-                            if(e instanceof EnumDict){
+                        .map(e -> {
+                            if (e instanceof EnumDict) {
                                 return ((EnumDict) e).getValue();
                             }
                             return e.name();
                         }).collect(Collectors.toList());
 
-                return new ValidationException("参数[" + currentName + "]在选项中不存在",
-                        Arrays.asList(
-                                new ValidationException.Detail(currentName, "选项中不存在此值", values)
-                        ));
+                return new ValidationException(currentName,"validation.parameter_does_not_exist_in_enums", currentName);
             };
             if (EnumDict.class.isAssignableFrom(findPropertyType) && findPropertyType.isEnum()) {
                 if (node.isObject()) {
@@ -394,12 +403,11 @@ public interface EnumDict<V> extends JSONSerializable {
                             .find(findPropertyType, node.textValue())
                             .orElseThrow(exceptionSupplier);
                 }
-                throw new ValidationException("参数[" + currentName + "]在选项中不存在", Arrays.asList(
-                        new ValidationException.Detail(currentName, "选项中不存在此值", null)
-                ));
+                return exceptionSupplier.get();
             }
             if (findPropertyType.isEnum()) {
-                return Stream.of(findPropertyType.getEnumConstants())
+                return Stream
+                        .of(findPropertyType.getEnumConstants())
                         .filter(o -> {
                             if (node.isTextual()) {
                                 return node.textValue().equalsIgnoreCase(((Enum) o).name());

+ 8 - 8
hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java

@@ -26,12 +26,11 @@ import lombok.Getter;
  * @author zhouhao
  * @since 2.0
  */
-public class BusinessException extends RuntimeException {
+public class BusinessException extends I18nSupportException {
     private static final long serialVersionUID = 5441923856899380112L;
 
     @Getter
     private int status = 500;
-
     @Getter
     private String code;
 
@@ -39,20 +38,21 @@ public class BusinessException extends RuntimeException {
         this(message, 500);
     }
 
+    public BusinessException(String message, int status, Object... args) {
+        this(message, null, status, args);
+    }
+
     public BusinessException(String message, String code) {
         this(message, code, 500);
     }
 
-    public BusinessException(String message, String code, int status) {
-        super(message);
+
+    public BusinessException(String message, String code, int status, Object... args) {
+        super(message, args);
         this.code = code;
         this.status = status;
     }
 
-    public BusinessException(String message, int status) {
-        super(message);
-        this.status = status;
-    }
 
     public BusinessException(String message, Throwable cause) {
         super(message, cause);

+ 40 - 0
hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java

@@ -0,0 +1,40 @@
+package org.hswebframework.web.exception;
+
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.i18n.LocaleUtils;
+
+@Getter
+@Setter(AccessLevel.PROTECTED)
+public class I18nSupportException extends RuntimeException {
+    private String code;
+    private Object[] args;
+
+    protected I18nSupportException() {
+
+    }
+
+    public I18nSupportException(String code, Object... args) {
+        super(code);
+        this.code = code;
+        this.args = args;
+    }
+
+    public I18nSupportException(String code, Throwable cause, Object... args) {
+        super(code, cause);
+        this.args = args;
+        this.code = code;
+    }
+
+    @Override
+    public String getMessage() {
+        return super.getMessage() != null ? super.getMessage() : getLocalizedMessage();
+    }
+
+    @Override
+    public String getLocalizedMessage() {
+        return LocaleUtils.resolveMessage(code, args);
+    }
+}

+ 3 - 3
hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java

@@ -24,11 +24,11 @@ import org.springframework.web.bind.annotation.ResponseStatus;
 
 @ResponseStatus(HttpStatus.NOT_FOUND)
 public class NotFoundException extends BusinessException {
-    public NotFoundException(String message) {
-        super(message, 404);
+    public NotFoundException(String message, Object... args) {
+        super(message, 404, args);
     }
 
     public NotFoundException() {
-        this("记录不存在");
+        this("error.not_found");
     }
 }

+ 40 - 16
hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java

@@ -4,19 +4,19 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Setter;
+import org.hswebframework.web.i18n.LocaleUtils;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.ResponseStatus;
 
 import javax.validation.ConstraintViolation;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 @Getter
 @Setter
 @ResponseStatus(HttpStatus.BAD_REQUEST)
-public class ValidationException extends BusinessException {
+public class ValidationException extends I18nSupportException {
+
+    private static final boolean propertyI18nEnabled = Boolean.getBoolean("i18n.validation.property.enabled");
 
     private List<Detail> details;
 
@@ -24,25 +24,43 @@ public class ValidationException extends BusinessException {
         super(message);
     }
 
-    public ValidationException(String property, String message) {
-        this(message, Collections.singletonList(new Detail(property, message, null)));
+    public ValidationException(String property, String message, Object... args) {
+        this(message, Collections.singletonList(new Detail(property, message, null)), args);
     }
 
-    public ValidationException(String message, List<Detail> details) {
-        super(message);
+    public ValidationException(String message, List<Detail> details, Object... args) {
+        super(message, 400, args);
         this.details = details;
+        for (Detail detail : this.details) {
+            detail.translateI18n(args);
+        }
     }
 
-    public ValidationException(String message, Set<? extends ConstraintViolation<?>> violations) {
-        super(message);
-        if (null != violations && !violations.isEmpty()) {
-            details = new ArrayList<>();
-            for (ConstraintViolation<?> violation : violations) {
-                details.add(new Detail(violation.getPropertyPath().toString(), violation.getMessage(), null));
-            }
+    public ValidationException(Set<? extends ConstraintViolation<?>> violations) {
+        ConstraintViolation<?> first = violations.iterator().next();
+        if (Objects.equals(first.getMessageTemplate(), first.getMessage())) {
+            //模版和消息相同,说明是自定义的message,而不是已经通过i18n获取的.
+            setCode(first.getMessage());
+        } else {
+            setCode("validation.property_validate_failed");
+        }
+        String property = first.getPropertyPath().toString();
+
+        //{0} 属性 ,{1} 验证消息
+        //property也支持国际化?
+        String resolveMessage = propertyI18nEnabled ?
+                LocaleUtils.resolveMessage(first.getRootBeanClass().getName() + "." + property, property)
+                : property;
+
+        setArgs(new Object[]{resolveMessage, first.getMessage()});
+
+        details = new ArrayList<>(violations.size());
+        for (ConstraintViolation<?> violation : violations) {
+            details.add(new Detail(violation.getPropertyPath().toString(), violation.getMessage(), null));
         }
     }
 
+
     @Getter
     @Setter
     @AllArgsConstructor
@@ -55,5 +73,11 @@ public class ValidationException extends BusinessException {
 
         @Schema(description = "详情")
         Object detail;
+
+        public void translateI18n(Object... args) {
+            if (message.contains(".")) {
+                message = LocaleUtils.resolveMessage(message, message, args);
+            }
+        }
     }
 }

+ 13 - 0
hsweb-core/src/main/java/org/hswebframework/web/i18n/ContextLocaleResolver.java

@@ -0,0 +1,13 @@
+package org.hswebframework.web.i18n;
+
+import org.hibernate.validator.spi.messageinterpolation.LocaleResolver;
+import org.hibernate.validator.spi.messageinterpolation.LocaleResolverContext;
+
+import java.util.Locale;
+
+public class ContextLocaleResolver implements LocaleResolver {
+    @Override
+    public Locale resolve(LocaleResolverContext context) {
+        return LocaleUtils.current();
+    }
+}

+ 182 - 0
hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java

@@ -0,0 +1,182 @@
+package org.hswebframework.web.i18n;
+
+import org.hswebframework.web.exception.I18nSupportException;
+import org.springframework.context.MessageSource;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.Signal;
+import reactor.core.publisher.SignalType;
+import reactor.util.context.Context;
+
+import java.util.Locale;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * 用于进行国际化消息转换
+ *
+ * @author zhouhao
+ * @since 4.0.11
+ */
+public class LocaleUtils {
+
+    public static final Locale DEFAULT_LOCALE = Locale.getDefault();
+
+    private static final ThreadLocal<Locale> CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
+
+    static MessageSource messageSource = UnsupportedMessageSource.instance();
+
+    /**
+     * 获取当前的语言地区,如果没有设置则返回系统默认语言
+     *
+     * @return Locale
+     */
+    public static Locale current() {
+        Locale locale = CONTEXT_THREAD_LOCAL.get();
+        if (locale == null) {
+            locale = DEFAULT_LOCALE;
+        }
+        return locale;
+    }
+
+    /**
+     * 在指定的语言环境中执行函数,<b>只能</b>在非响应式同步操作时使用,如:转换实体类中某些属性的国际化消息。
+     * <p>
+     * 在函数的逻辑中可以通过{@link LocaleUtils#current()}来获取当前语言.
+     *
+     * @param data   参数
+     * @param locale 语言地区
+     * @param mapper 函数
+     * @param <T>    参数类型
+     * @param <R>    函数返回类型
+     * @return 返回值
+     */
+    public static <T, R> R doWith(T data, Locale locale, BiFunction<T, Locale, R> mapper) {
+        try {
+            CONTEXT_THREAD_LOCAL.set(locale);
+            return mapper.apply(data, locale);
+        } finally {
+            CONTEXT_THREAD_LOCAL.remove();
+        }
+    }
+
+    public static Function<Context, Context> useLocale(Locale locale) {
+        return ctx -> ctx.put(Locale.class, locale);
+    }
+
+    public static void doWith(Locale locale, Consumer<Locale> consumer) {
+        try {
+            CONTEXT_THREAD_LOCAL.set(locale);
+            consumer.accept(locale);
+        } finally {
+            CONTEXT_THREAD_LOCAL.remove();
+        }
+    }
+
+    /**
+     * 响应式方式获取当前语言地区
+     *
+     * @return 语言地区
+     */
+    @SuppressWarnings("all")
+    public static Mono<Locale> currentReactive() {
+        return Mono
+                .subscriberContext()
+                .map(ctx -> ctx.getOrDefault(Locale.class, DEFAULT_LOCALE));
+    }
+
+    public static <T> void onNext(Signal<T> signal, BiConsumer<T, Locale> consumer) {
+        if (signal.getType() != SignalType.ON_NEXT) {
+            return;
+        }
+        Locale locale = signal.getContext().getOrDefault(Locale.class, DEFAULT_LOCALE);
+
+        doWith(locale, l -> consumer.accept(signal.get(), l));
+
+    }
+
+    public static <S extends I18nSupportException, R> Mono<R> resolveThrowable(S source,
+                                                                               BiFunction<S, String, R> mapper) {
+        return resolveThrowable(messageSource, source, mapper);
+    }
+
+    public static <S extends I18nSupportException, R> Mono<R> resolveThrowable(MessageSource messageSource,
+                                                                               S source,
+                                                                               BiFunction<S, String, R> mapper) {
+        return doWithReactive(messageSource, source, I18nSupportException::getCode, mapper, source.getArgs());
+    }
+
+    public static <S extends Throwable, R> Mono<R> resolveThrowable(S source,
+                                                                    BiFunction<S, String, R> mapper,
+                                                                    Object... args) {
+        return resolveThrowable(messageSource, source, mapper, args);
+    }
+
+    public static <S extends Throwable, R> Mono<R> resolveThrowable(MessageSource messageSource,
+                                                                    S source,
+                                                                    BiFunction<S, String, R> mapper,
+                                                                    Object... args) {
+        return doWithReactive(messageSource, source, Throwable::getMessage, mapper, args);
+    }
+
+    public static <S, R> Mono<R> doWithReactive(S source,
+                                                Function<S, String> message,
+                                                BiFunction<S, String, R> mapper,
+                                                Object... args) {
+        return doWithReactive(messageSource, source, message, mapper, args);
+    }
+
+    public static <S, R> Mono<R> doWithReactive(MessageSource messageSource,
+                                                S source,
+                                                Function<S, String> message,
+                                                BiFunction<S, String, R> mapper,
+                                                Object... args) {
+        return currentReactive()
+                .map(locale -> {
+                    String msg = message.apply(source);
+                    String newMsg = resolveMessage(messageSource, locale, msg, msg, args);
+                    return mapper.apply(source, newMsg);
+                });
+    }
+
+    public static Mono<String> resolveMessageReactive(MessageSource messageSource,
+                                                      String code,
+                                                      Object... args) {
+        return currentReactive()
+                .map(locale -> resolveMessage(messageSource, locale, code, code, args));
+    }
+
+    public static String resolveMessage(String code,
+                                        Locale locale,
+                                        String defaultMessage,
+                                        Object... args) {
+        return resolveMessage(messageSource, locale, code, defaultMessage, args);
+    }
+
+    public static String resolveMessage(MessageSource messageSource,
+                                        Locale locale,
+                                        String code,
+                                        String defaultMessage,
+                                        Object... args) {
+        return messageSource.getMessage(code, args, defaultMessage, locale);
+    }
+
+    public static String resolveMessage(String code, Object... args) {
+        return resolveMessage(messageSource, current(), code, code, args);
+    }
+
+    public static String resolveMessage(String code,
+                                        String defaultMessage,
+                                        Object... args) {
+        return resolveMessage(messageSource, current(), code, defaultMessage, args);
+    }
+
+    public static String resolveMessage(MessageSource messageSource,
+                                        String code,
+                                        String defaultMessage,
+                                        Object... args) {
+        return resolveMessage(messageSource, current(), code, defaultMessage, args);
+    }
+
+}

+ 12 - 0
hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java

@@ -0,0 +1,12 @@
+package org.hswebframework.web.i18n;
+
+import org.springframework.context.MessageSource;
+
+public class MessageSourceInitializer {
+
+    public static void init(MessageSource messageSource) {
+        if (LocaleUtils.messageSource == null || LocaleUtils.messageSource instanceof UnsupportedMessageSource) {
+            LocaleUtils.messageSource = messageSource;
+        }
+    }
+}

+ 31 - 0
hsweb-core/src/main/java/org/hswebframework/web/i18n/UnsupportedMessageSource.java

@@ -0,0 +1,31 @@
+package org.hswebframework.web.i18n;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.context.NoSuchMessageException;
+
+import java.util.Locale;
+
+public class UnsupportedMessageSource implements MessageSource {
+
+    private static final UnsupportedMessageSource INSTANCE = new UnsupportedMessageSource();
+
+    public static MessageSource instance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
+        return defaultMessage;
+    }
+
+    @Override
+    public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {
+        return code;
+    }
+
+    @Override
+    public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
+        return resolvable.getDefaultMessage();
+    }
+}

+ 34 - 0
hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java

@@ -0,0 +1,34 @@
+package org.hswebframework.web.i18n;
+
+import org.springframework.lang.NonNull;
+import org.springframework.util.StringUtils;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+import java.util.Locale;
+
+public class WebFluxLocaleFilter implements WebFilter {
+    @Override
+    @NonNull
+    public Mono<Void> filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) {
+        return chain
+                .filter(exchange)
+                .subscriberContext(LocaleUtils.useLocale(getLocaleContext(exchange)));
+    }
+
+    public Locale getLocaleContext(ServerWebExchange exchange) {
+        String lang = exchange.getRequest()
+                              .getQueryParams()
+                              .getFirst(":lang");
+        if (StringUtils.hasText(lang)) {
+            return Locale.forLanguageTag(lang);
+        }
+        Locale locale = exchange.getLocaleContext().getLocale();
+        if (locale == null) {
+            return Locale.getDefault();
+        }
+        return locale;
+    }
+}

+ 14 - 7
hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java

@@ -1,12 +1,10 @@
 package org.hswebframework.web.validator;
 
+import org.hibernate.validator.BaseHibernateValidatorConfiguration;
 import org.hswebframework.web.exception.ValidationException;
+import org.hswebframework.web.i18n.ContextLocaleResolver;
 
-import javax.el.ExpressionFactory;
-import javax.validation.ConstraintViolation;
-import javax.validation.Validation;
-import javax.validation.Validator;
-import javax.validation.ValidatorFactory;
+import javax.validation.*;
 import java.util.Set;
 
 public final class ValidatorUtils {
@@ -19,17 +17,26 @@ public final class ValidatorUtils {
     public static Validator getValidator() {
         if (validator == null) {
             synchronized (ValidatorUtils.class) {
-                ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+                Configuration<?> configuration = Validation
+                        .byDefaultProvider()
+                        .configure();
+                configuration.addProperty(BaseHibernateValidatorConfiguration.LOCALE_RESOLVER_CLASSNAME,
+                                          ContextLocaleResolver.class.getName());
+                configuration.messageInterpolator(configuration.getDefaultMessageInterpolator());
+
+                ValidatorFactory factory = configuration.buildValidatorFactory();
+
                 return validator = factory.getValidator();
             }
         }
         return validator;
     }
 
+    @SuppressWarnings("all")
     public static <T> T tryValidate(T bean, Class... group) {
         Set<ConstraintViolation<T>> violations = getValidator().validate(bean, group);
         if (!violations.isEmpty()) {
-            throw new ValidationException(violations.iterator().next().getMessage(), violations);
+            throw new ValidationException(violations);
         }
 
         return bean;

+ 4 - 0
hsweb-core/src/main/resources/i18n/core/messages_en_US.properties

@@ -0,0 +1,4 @@
+error.not_found=The data does not exist
+error.cant_create_instance=Unable to create instance:{0}
+validation.parameter_does_not_exist_in_enums=Parameter {0} does not exist in option
+validation.property_validate_failed={0} {1}

+ 5 - 0
hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties

@@ -0,0 +1,5 @@
+error.not_found=数据不存在
+error.cant_create_instance=无法创建实例:{0}
+
+validation.parameter_does_not_exist_in_enums=参数[{0}]在选择中不存在
+validation.property_validate_failed={0}{1}

+ 42 - 0
hsweb-core/src/test/java/org/hswebframework/web/validator/ValidatorUtilsTest.java

@@ -0,0 +1,42 @@
+package org.hswebframework.web.validator;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.exception.ValidationException;
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.junit.Test;
+
+import javax.validation.constraints.NotBlank;
+
+import java.util.Locale;
+
+import static org.junit.Assert.*;
+
+public class ValidatorUtilsTest {
+
+
+    @Test
+    public void test(){
+        test(Locale.CHINA,"不能为空");
+        test(Locale.ENGLISH,"must not be blank");
+    }
+
+    public void test(Locale locale,String msg){
+        try {
+            LocaleUtils.doWith(locale,en->{
+                ValidatorUtils.tryValidate(new TestEntity());
+            });
+            throw new IllegalStateException();
+        }catch (ValidationException e){
+            assertEquals(msg,e.getDetails().get(0).getMessage());
+        }
+    }
+
+    @Getter
+    @Setter
+    public static class TestEntity{
+
+        @NotBlank
+        private String notBlank;
+    }
+}

+ 71 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java

@@ -0,0 +1,71 @@
+package org.hswebframework.web.starter.i18n;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.context.NoSuchMessageException;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Nonnull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class CompositeMessageSource implements MessageSource {
+
+    private final List<MessageSource> messageSources = new CopyOnWriteArrayList<>();
+
+    public void addMessageSources(Collection<MessageSource> source) {
+        messageSources.addAll(source);
+    }
+
+    public void addMessageSource(MessageSource source) {
+        messageSources.add(source);
+    }
+
+    @Override
+    public String getMessage(@Nonnull String code, Object[] args, String defaultMessage, @Nonnull Locale locale) {
+        for (MessageSource messageSource : messageSources) {
+            String result = messageSource.getMessage(code, args, null, locale);
+            if (StringUtils.hasText(result)) {
+                return result;
+            }
+        }
+        return defaultMessage;
+    }
+
+    @Override
+    @Nonnull
+    public String getMessage(@Nonnull String code, Object[] args, @Nonnull Locale locale) throws NoSuchMessageException {
+        for (MessageSource messageSource : messageSources) {
+            try {
+                String result = messageSource.getMessage(code, args, locale);
+                if (StringUtils.hasText(result)) {
+                    return result;
+                }
+            } catch (NoSuchMessageException ignore) {
+
+            }
+        }
+        throw new NoSuchMessageException(code, locale);
+    }
+
+    @Override
+    @Nonnull
+    public String getMessage(@Nonnull MessageSourceResolvable resolvable, @Nonnull Locale locale) throws NoSuchMessageException {
+        for (MessageSource messageSource : messageSources) {
+            try {
+                String result = messageSource.getMessage(resolvable, locale);
+                if (StringUtils.hasText(result)) {
+                    return result;
+                }
+            } catch (NoSuchMessageException ignore) {
+
+            }
+        }
+        String[] codes = resolvable.getCodes();
+        throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
+    }
+}

+ 56 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java

@@ -0,0 +1,56 @@
+package org.hswebframework.web.starter.i18n;
+
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.i18n.MessageSourceInitializer;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.support.ResourceBundleMessageSource;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.util.StringUtils;
+
+import java.util.stream.Collectors;
+
+@Configuration(proxyBeanMethods = false)
+@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
+@Slf4j
+public class I18nConfiguration {
+
+    @Bean
+    @SneakyThrows
+    public MessageSource autoResolveI18nMessageSource() {
+
+        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
+        messageSource.setDefaultEncoding("UTF-8");
+        Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath*:i18n/**");
+
+        for (Resource resource : resources) {
+            String path = resource.getURL().getPath();
+            if (StringUtils.hasText(path) && (path.endsWith(".properties") || path.endsWith(".xml"))) {
+                String name = path.substring(path.lastIndexOf("i18n"),path.indexOf("_"));
+
+                log.info("register i18n message resource {} -> {}", path,name);
+
+                messageSource.addBasenames(name);
+            }
+        }
+        return messageSource;
+    }
+
+    @Bean
+    @Primary
+    public MessageSource compositeMessageSource(ObjectProvider<MessageSource> objectProvider) {
+        CompositeMessageSource messageSource = new CompositeMessageSource();
+        messageSource.addMessageSources(objectProvider.stream().collect(Collectors.toList()));
+        MessageSourceInitializer.init(messageSource);
+        return messageSource;
+    }
+
+
+}

+ 1 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java

@@ -51,6 +51,7 @@ public class CustomCodecsAutoConfiguration {
             return (configurer) -> {
                 CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
                 defaults.jackson2JsonDecoder(new CustomJackson2JsonDecoder(entityFactory, objectMapper));
+                defaults.jackson2JsonEncoder(new CustomJackson2jsonEncoder(objectMapper));
             };
         }
 

+ 27 - 13
hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2JsonDecoder.java

@@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectReader;
 import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
 import com.fasterxml.jackson.databind.util.TokenBuffer;
 import org.hswebframework.web.api.crud.entity.EntityFactory;
+import org.hswebframework.web.i18n.LocaleUtils;
 import org.reactivestreams.Publisher;
 import org.springframework.core.MethodParameter;
 import org.springframework.core.ResolvableType;
@@ -67,17 +68,23 @@ public class CustomJackson2JsonDecoder extends Jackson2CodecSupport implements H
 
         ObjectReader reader = getObjectReader(elementType, hints);
 
-        return tokens.handle((tokenBuffer, sink) -> {
-            try {
-                Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()));
-                logValue(value, hints);
-                if (value != null) {
-                    sink.next(value);
-                }
-            } catch (IOException ex) {
-                sink.error(processException(ex));
-            }
-        });
+        return LocaleUtils
+                .currentReactive()
+                .flatMapMany(locale -> tokens
+                        .handle((tokenBuffer, sink) -> {
+                            LocaleUtils.doWith(locale, l -> {
+                                try {
+                                    Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()));
+                                    logValue(value, hints);
+                                    if (value != null) {
+                                        sink.next(value);
+                                    }
+                                } catch (IOException ex) {
+                                    sink.error(processException(ex));
+                                }
+                            });
+
+                        }));
     }
 
     @Override
@@ -85,8 +92,15 @@ public class CustomJackson2JsonDecoder extends Jackson2CodecSupport implements H
     public Mono<Object> decodeToMono(@NonNull Publisher<DataBuffer> input, @NonNull ResolvableType elementType,
                                      @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
 
-        return DataBufferUtils.join(input)
-                              .map(dataBuffer -> decode(dataBuffer, elementType, mimeType, hints));
+        return LocaleUtils
+                .currentReactive()
+                .flatMap(locale -> DataBufferUtils
+                        .join(input)
+                        .map(dataBuffer -> LocaleUtils
+                                .doWith(dataBuffer,
+                                        locale,
+                                        (buf, l) -> decode(buf, elementType, mimeType, hints)))
+                );
     }
 
     @Override

+ 340 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java

@@ -0,0 +1,340 @@
+package org.hswebframework.web.starter.jackson;
+
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.springframework.http.codec.json.Jackson2CodecSupport;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.nio.charset.Charset;
+import java.util.*;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.util.ByteArrayBuilder;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.codec.CodecException;
+import org.springframework.core.codec.EncodingException;
+import org.springframework.core.codec.Hints;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.log.LogFormatUtils;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.HttpMessageEncoder;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.MimeType;
+
+/**
+ * Base class providing support methods for Jackson 2.9 encoding. For non-streaming use
+ * cases, {@link Flux} elements are collected into a {@link List} before serialization for
+ * performance reason.
+ *
+ * @author Sebastien Deleuze
+ * @author Arjen Poutsma
+ * @since 5.0
+ */
+public class CustomJackson2jsonEncoder extends Jackson2CodecSupport implements HttpMessageEncoder<Object> {
+
+    private static final byte[] NEWLINE_SEPARATOR = {'\n'};
+
+    private static final Map<MediaType, byte[]> STREAM_SEPARATORS;
+
+    private static final Map<String, JsonEncoding> ENCODINGS;
+
+    static {
+        STREAM_SEPARATORS = new HashMap<>(4);
+        STREAM_SEPARATORS.put(MediaType.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR);
+        STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]);
+
+        ENCODINGS = new HashMap<>(JsonEncoding.values().length + 1);
+        for (JsonEncoding encoding : JsonEncoding.values()) {
+            ENCODINGS.put(encoding.getJavaName(), encoding);
+        }
+        ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
+    }
+
+
+    private final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
+
+
+    /**
+     * Constructor with a Jackson {@link ObjectMapper} to use.
+     */
+    protected CustomJackson2jsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
+        super(mapper, mimeTypes);
+    }
+
+
+    /**
+     * Configure "streaming" media types for which flushing should be performed
+     * automatically vs at the end of the stream.
+     * <p>By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}.
+     *
+     * @param mediaTypes one or more media types to add to the list
+     * @see HttpMessageEncoder#getStreamingMediaTypes()
+     */
+    public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
+        this.streamingMediaTypes.clear();
+        this.streamingMediaTypes.addAll(mediaTypes);
+    }
+
+
+    @Override
+    public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
+        Class<?> clazz = elementType.toClass();
+        if (!supportsMimeType(mimeType)) {
+            return false;
+        }
+        if (mimeType != null && mimeType.getCharset() != null) {
+            Charset charset = mimeType.getCharset();
+            if (!ENCODINGS.containsKey(charset.name())) {
+                return false;
+            }
+        }
+        return (Object.class == clazz ||
+                (!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));
+    }
+
+    @Override
+    public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
+                                   ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
+
+        Assert.notNull(inputStream, "'inputStream' must not be null");
+        Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
+        Assert.notNull(elementType, "'elementType' must not be null");
+
+        return LocaleUtils
+                .currentReactive()
+                .flatMapMany(locale -> {
+                    if (inputStream instanceof Mono) {
+                        return Mono.from(inputStream)
+                                   .map(value -> LocaleUtils
+                                           .doWith(value, locale,
+                                                   ((val, loc) ->
+                                                           encodeValue(val, bufferFactory, elementType, mimeType, hints)
+                                                   )
+                                           ))
+                                   .flux();
+                    } else {
+                        byte[] separator = streamSeparator(mimeType);
+                        if (separator != null) { // streaming
+                            try {
+                                ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);
+                                ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer
+                                                                                            .getFactory()
+                                                                                            ._getBufferRecycler());
+                                JsonEncoding encoding = getJsonEncoding(mimeType);
+                                JsonGenerator generator = getObjectMapper()
+                                        .getFactory()
+                                        .createGenerator(byteBuilder, encoding);
+                                SequenceWriter sequenceWriter = writer.writeValues(generator);
+
+                                return Flux
+                                        .from(inputStream)
+                                        .map(value -> LocaleUtils
+                                                .doWith(value,
+                                                        locale,
+                                                        ((val, loc) -> this
+                                                                .encodeStreamingValue(val,
+                                                                                      bufferFactory,
+                                                                                      hints,
+                                                                                      sequenceWriter,
+                                                                                      byteBuilder,
+                                                                                      separator)
+                                                        )
+                                                ))
+                                        .doAfterTerminate(() -> {
+                                            try {
+                                                byteBuilder.release();
+                                                generator.close();
+                                            } catch (IOException ex) {
+                                                logger.error("Could not close Encoder resources", ex);
+                                            }
+                                        });
+                            } catch (IOException ex) {
+                                return Flux.error(ex);
+                            }
+                        } else { // non-streaming
+                            ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
+                            return Flux.from(inputStream)
+                                       .collectList()
+                                       .map(value -> LocaleUtils
+                                               .doWith(value, locale,
+                                                       ((val, loc) ->
+                                                               encodeValue(val, bufferFactory, listType, mimeType, hints)
+                                                       )
+                                               ))
+                                       .flux();
+                        }
+
+                    }
+                });
+    }
+
+    @Override
+    public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
+                                  ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
+
+        ObjectWriter writer = createObjectWriter(valueType, mimeType, hints);
+        ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
+        try {
+            JsonEncoding encoding = getJsonEncoding(mimeType);
+
+            logValue(hints, value);
+
+            try (JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding)) {
+                writer.writeValue(generator, value);
+                generator.flush();
+            } catch (InvalidDefinitionException ex) {
+                throw new CodecException("Type definition error: " + ex.getType(), ex);
+            } catch (JsonProcessingException ex) {
+                throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
+            } catch (IOException ex) {
+                throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex);
+            }
+
+            byte[] bytes = byteBuilder.toByteArray();
+            DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
+            buffer.write(bytes);
+
+            return buffer;
+        } finally {
+            byteBuilder.release();
+        }
+    }
+
+    private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFactory, @Nullable Map<String, Object> hints,
+                                            SequenceWriter sequenceWriter, ByteArrayBuilder byteArrayBuilder, byte[] separator) {
+
+        logValue(hints, value);
+
+        try {
+            sequenceWriter.write(value);
+            sequenceWriter.flush();
+        } catch (InvalidDefinitionException ex) {
+            throw new CodecException("Type definition error: " + ex.getType(), ex);
+        } catch (JsonProcessingException ex) {
+            throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
+        } catch (IOException ex) {
+            throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex);
+        }
+
+        byte[] bytes = byteArrayBuilder.toByteArray();
+        byteArrayBuilder.reset();
+
+        int offset;
+        int length;
+        if (bytes.length > 0 && bytes[0] == ' ') {
+            // SequenceWriter writes an unnecessary space in between values
+            offset = 1;
+            length = bytes.length - 1;
+        } else {
+            offset = 0;
+            length = bytes.length;
+        }
+        DataBuffer buffer = bufferFactory.allocateBuffer(length + separator.length);
+        buffer.write(bytes, offset, length);
+        buffer.write(separator);
+
+        return buffer;
+    }
+
+    private void logValue(@Nullable Map<String, Object> hints, Object value) {
+        if (!Hints.isLoggingSuppressed(hints)) {
+            LogFormatUtils.traceDebug(logger, traceOn -> {
+                String formatted = LogFormatUtils.formatValue(value, !traceOn);
+                return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]";
+            });
+        }
+    }
+
+    private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType,
+                                            @Nullable Map<String, Object> hints) {
+
+        JavaType javaType = getJavaType(valueType.getType(), null);
+        Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);
+        ObjectWriter writer = (jsonView != null ?
+                getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer());
+
+        if (javaType.isContainerType()) {
+            writer = writer.forType(javaType);
+        }
+
+        return customizeWriter(writer, mimeType, valueType, hints);
+    }
+
+    protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType,
+                                           ResolvableType elementType, @Nullable Map<String, Object> hints) {
+
+        return writer;
+    }
+
+    @Nullable
+    private byte[] streamSeparator(@Nullable MimeType mimeType) {
+        for (MediaType streamingMediaType : this.streamingMediaTypes) {
+            if (streamingMediaType.isCompatibleWith(mimeType)) {
+                return STREAM_SEPARATORS.getOrDefault(streamingMediaType, NEWLINE_SEPARATOR);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Determine the JSON encoding to use for the given mime type.
+     *
+     * @param mimeType the mime type as requested by the caller
+     * @return the JSON encoding to use (never {@code null})
+     * @since 5.0.5
+     */
+    protected JsonEncoding getJsonEncoding(@Nullable MimeType mimeType) {
+        if (mimeType != null && mimeType.getCharset() != null) {
+            Charset charset = mimeType.getCharset();
+            JsonEncoding result = ENCODINGS.get(charset.name());
+            if (result != null) {
+                return result;
+            }
+        }
+        return JsonEncoding.UTF8;
+    }
+
+
+    // HttpMessageEncoder
+
+    @Override
+    public List<MimeType> getEncodableMimeTypes() {
+        return getMimeTypes();
+    }
+
+    @Override
+    public List<MediaType> getStreamingMediaTypes() {
+        return Collections.unmodifiableList(this.streamingMediaTypes);
+    }
+
+    @Override
+    public Map<String, Object> getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType,
+                                              @Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
+
+        return (actualType != null ? getHints(actualType) : Hints.none());
+    }
+
+
+    // Jackson2CodecSupport
+
+    @Override
+    protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) {
+        return parameter.getMethodAnnotation(annotType);
+    }
+}

+ 2 - 1
hsweb-starter/src/main/resources/META-INF/spring.factories

@@ -2,4 +2,5 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration,\
 org.hswebframework.web.starter.HswebAutoConfiguration,\
-org.hswebframework.web.starter.CorsAutoConfiguration
+org.hswebframework.web.starter.CorsAutoConfiguration,\
+org.hswebframework.web.starter.i18n.I18nConfiguration

+ 88 - 0
hsweb-starter/src/test/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoderTest.java

@@ -0,0 +1,88 @@
+package org.hswebframework.web.starter.jackson;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hswebframework.web.dict.EnumDict;
+import org.hswebframework.web.i18n.MessageSourceInitializer;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.context.i18n.LocaleContext;
+import org.springframework.context.i18n.SimpleLocaleContext;
+import org.springframework.context.support.ResourceBundleMessageSource;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.MediaType;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.function.Predicate;
+
+public class CustomJackson2jsonEncoderTest {
+
+
+    @Before
+    public void init(){
+        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
+        messageSource.setDefaultEncoding("utf-8");
+        messageSource.setBasenames("i18n.messages");
+        MessageSourceInitializer.init(messageSource);
+    }
+
+    @Test
+    public void testI18n() {
+
+        doTest(new TestEntity(TestEnum.e1),Locale.forLanguageTag("en-US"),s->s.contains("Option1"));
+        doTest(new TestEntity(TestEnum.e1),Locale.forLanguageTag("zh-CN"),s->s.contains("选项1"));
+
+    }
+
+    public void doTest(TestEntity entity, Locale locale, Predicate<String> verify){
+
+        CustomJackson2jsonEncoder encoder = new CustomJackson2jsonEncoder(new ObjectMapper());
+
+        encoder.encode(Mono.just(entity),
+                       new DefaultDataBufferFactory(),
+                       ResolvableType.forType(TestEntity.class),
+                       MediaType.APPLICATION_JSON,
+                       Collections.emptyMap())
+               .as(DataBufferUtils::join)
+               .map(buf -> buf.toString(StandardCharsets.UTF_8))
+               .doOnNext(System.out::println)
+               .subscriberContext(ctx->ctx.put(LocaleContext.class,new SimpleLocaleContext(locale)))
+               .as(StepVerifier::create)
+               .expectNextMatches(verify)
+               .verifyComplete();
+    }
+
+    @Getter
+    @Setter
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class TestEntity {
+
+        private TestEnum testEnum;
+    }
+
+
+    @Getter
+    @AllArgsConstructor
+    public enum TestEnum implements EnumDict<String> {
+        e1("enum.e1"),
+        e2("enum.e2");
+
+        private final String text;
+
+        @Override
+        public String getValue() {
+            return name();
+        }
+
+    }
+}

+ 2 - 0
hsweb-starter/src/test/resources/i18n/messages_en_US.properties

@@ -0,0 +1,2 @@
+enum.e1=Option1
+enum.e2=Option2

+ 2 - 0
hsweb-starter/src/test/resources/i18n/messages_zh_CN.properties

@@ -0,0 +1,2 @@
+enum.e1=选项1
+enum.e2=选项2

+ 1 - 1
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/PermissionProperties.java

@@ -75,7 +75,7 @@ public class PermissionProperties {
                                               .map(Permission::getActions)
                                               .orElseGet(Collections::emptySet));
 
-                    throw new AccessDenyException("当前用户无权限:" + setting.getPermission() + "" +actions);
+                    throw new AccessDenyException(setting.getPermission(), actions);
                 }
             };
 

+ 1 - 1
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionUserService.java

@@ -59,7 +59,7 @@ public class DefaultDimensionUserService extends GenericReactiveCrudService<Dime
         return this
                 .publishEvent(entityPublisher, DimensionBindEvent::new)
                 .as(super::insert)
-                .onErrorMap(DuplicateKeyException.class, (err) -> new BusinessException("重复的绑定请求"));
+                .onErrorMap(DuplicateKeyException.class, (err) -> new BusinessException("error.duplicate_key"));
     }
 
     @Override

+ 2 - 3
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java

@@ -82,13 +82,12 @@ public class DefaultReactiveUserService extends GenericReactiveCrudService<UserE
                             .where(userEntity::getUsername)
                             .fetch()
                             .doOnNext(u -> {
-                                throw new org.hswebframework.web.exception.ValidationException("用户已存在");
+                                throw new org.hswebframework.web.exception.ValidationException("error.user_already_exists");
                             })
                             .then(Mono.just(userEntity))
-                            .doOnNext(e -> e.tryValidate(CreateGroup.class))
                             .as(getRepository()::insert)
                             .onErrorMap(DuplicateKeyException.class, e -> {
-                                throw new org.hswebframework.web.exception.ValidationException("用户已存在");
+                                throw new org.hswebframework.web.exception.ValidationException("error.user_already_exists");
                             })
                             .thenReturn(userEntity)
                             .flatMap(user -> new UserCreatedEvent(user).publish(eventPublisher))

+ 2 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en_US.properties

@@ -0,0 +1,2 @@
+error.duplicate_key=Duplicate Data
+error.user_already_exists=User already exists

+ 2 - 0
hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh_CN.properties

@@ -0,0 +1,2 @@
+error.duplicate_key=重复的请求
+error.user_already_exists=用户已存在