From e664d680233d5da9dfb56d1d8e38e7dbaac0f2a5 Mon Sep 17 00:00:00 2001 From: zhou-hao Date: Mon, 21 Jun 2021 18:21:20 +0800 Subject: [PATCH 1/7] =?UTF-8?q?i18n=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authorization/annotation/TwoFactor.java | 2 +- .../exception/AccessDenyException.java | 12 +++- .../exception/AuthenticationException.java | 7 +- .../exception/UnAuthorizedException.java | 3 +- .../token/DefaultUserTokenManager.java | 2 +- .../web/authorization/token/TokenState.java | 14 ++-- .../token/redis/RedisUserTokenManager.java | 2 +- .../resources/i18n/messages_zh_CN.properties | 14 ++++ .../DefaultBasicAuthorizeDefinition.java | 2 +- .../TwoFactorHandlerInterceptorAdapter.java | 6 +- .../basic/web/AuthorizationController.java | 6 +- .../crud/web/CommonErrorControllerAdvice.java | 18 +++-- .../crud/web/CommonWebFluxConfiguration.java | 32 +++++++-- .../web/exception/BusinessException.java | 18 ++--- .../web/exception/I18nSupportException.java | 27 ++++++++ .../web/exception/NotFoundException.java | 6 +- .../hswebframework/web/i18n/LocaleUtils.java | 67 +++++++++++++++++++ .../web/i18n/WebFluxLocaleFilter.java | 18 +++++ .../resources/i18n/messages_zh_CN.properties | 2 + .../configuration/PermissionProperties.java | 2 +- .../service/DefaultDimensionUserService.java | 2 +- .../resources/i18n/messages_zh_CN.properties | 1 + 22 files changed, 218 insertions(+), 45 deletions(-) create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties create mode 100644 hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java create mode 100644 hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java create mode 100644 hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java create mode 100644 hsweb-core/src/main/resources/i18n/messages_zh_CN.properties create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/messages_zh_CN.properties diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java index ae03f13ad..1d3d6b98f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java +++ b/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 "assert.verify_code_error"; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java index 240ed822e..e9f118475 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AccessDenyException.java +++ b/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 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); } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java index dce94a7bf..767166d5e 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/AuthenticationException.java +++ b/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; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java index 025d72c72..75dd34fd6 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/exception/UnAuthorizedException.java +++ b/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; diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java index b7ba54b1c..05e61807f 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/DefaultUserTokenManager.java +++ b/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(); }) diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenState.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenState.java index a35973dbc..6c9ac63af 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenState.java +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/TokenState.java @@ -13,30 +13,30 @@ public enum TokenState implements EnumDict { /** * 正常,有效 */ - 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; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java index 4002379a1..a3def4cb4 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/token/redis/RedisUserTokenManager.java +++ b/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; }); diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 000000000..412d3de60 --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,14 @@ +error.access_denied=权限不足,拒绝访问! +error.permission_denied=当前用户无权限[{0}]:{1} +error.logged_in_elsewhere=该用户已在其他地方登陆 +# +message.token_state_normal=正常 +message.token_state_deny=已被禁止访问 +message.token_state_expired=用户未登录 +message.token_state_offline=用户已在其他地方登录 +message.token_state_lock=登录状态已被锁定 +# +assert.need_two_factor_verify=需要双因子验证 +assert.username_must_not_be_empty=用户名不能为空 +assert.password_must_not_be_empty=密码不能为空 +assert.verify_code_error=验证码错误 \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java index ca42b8184..d5c556493 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/define/DefaultBasicAuthorizeDefinition.java +++ b/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; diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java index d1eea30c6..5aa9efbcb 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java @@ -22,7 +22,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 +45,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("assert.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); diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java index 4347c46f0..659fe9879 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java +++ b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java @@ -85,8 +85,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_, "assert.username_must_not_be_empty"); + Assert.hasLength(password_, "assert.password_must_not_be_empty"); Function parameterGetter = parameters::get; return Mono.defer(() -> { @@ -101,7 +101,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); diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java index 673442ebf..9f9b44b98 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java +++ b/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; @@ -40,11 +39,20 @@ 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> 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 diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java index af48922dd..755d8a915 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java @@ -1,14 +1,22 @@ 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.context.i18n.LocaleContext; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; @Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @@ -16,17 +24,31 @@ 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 MessageSource messageSource() { + ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); + messageSource.setBasenames("i18n/messages"); + messageSource.setDefaultEncoding("UTF-8"); + return messageSource; + } + + @Bean + public WebFilter localeWebFilter() { + return new WebFluxLocaleFilter(); + } + } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java index 4dea9edb3..b46301e58 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java @@ -26,7 +26,7 @@ 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 @@ -39,20 +39,20 @@ public class BusinessException extends RuntimeException { this(message, 500); } - public BusinessException(String message, String code) { - this(message, code, 500); + public BusinessException(String message, int status, Object... args) { + this(message, null, status, args); } - public BusinessException(String message, String code, int status) { - super(message); + public BusinessException(String message, String code, Object... args) { + this(message, code, 500, args); + } + + 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); diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java new file mode 100644 index 000000000..12a180ebe --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java @@ -0,0 +1,27 @@ +package org.hswebframework.web.exception; + + +import lombok.Getter; + +@Getter +public class I18nSupportException extends RuntimeException { + + private final Object[] args; + + public I18nSupportException(String code, Object... args) { + super(code); + this.args = args; + } + + public I18nSupportException(String code, Throwable cause, Object... args) { + super(code, cause); + this.args = args; + } + + + @Override + public String getLocalizedMessage() { + // TODO: 2021/6/21 + return super.getLocalizedMessage(); + } +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java index 819481e79..b005303e4 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/NotFoundException.java +++ b/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"); } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java new file mode 100644 index 000000000..4a6137f66 --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java @@ -0,0 +1,67 @@ +package org.hswebframework.web.i18n; + +import org.hswebframework.web.exception.I18nSupportException; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.SimpleLocaleContext; +import reactor.core.publisher.Mono; + +import java.util.Locale; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class LocaleUtils { + + public static final LocaleContext DEFAULT_CONTEXT = new SimpleLocaleContext(Locale.getDefault()); + + public static Mono reactive() { + return Mono + .subscriberContext() + .map(ctx -> ctx + .getOrEmpty(LocaleContext.class) + .orElse(DEFAULT_CONTEXT)); + } + + + public static Mono resolveThrowable(MessageSource messageSource, + S source, + BiFunction mapper) { + return doWithReactive(messageSource, source, Throwable::getMessage, mapper, source.getArgs()); + } + + public static Mono resolveThrowable(MessageSource messageSource, + S source, + BiFunction mapper, + Object... args) { + return doWithReactive(messageSource, source, Throwable::getMessage, mapper, args); + } + + public static Mono doWithReactive(MessageSource messageSource, + S source, + Function message, + BiFunction mapper, + Object... args) { + return reactive() + .map(ctx -> { + String msg = message.apply(source); + String newMsg = resolveMessage(messageSource, msg, ctx.getLocale(), msg, args); + return mapper.apply(source, newMsg); + }); + } + + public static Mono reactiveMessage(MessageSource messageSource, + String code, + Object... args) { + return reactive() + .map(ctx -> resolveMessage(messageSource, code, ctx.getLocale(), code, args)); + } + + public static String resolveMessage(MessageSource messageSource, + String code, + Locale locale, + String defaultMessage, + Object... args) { + return messageSource.getMessage(code, args, defaultMessage, locale); + } + +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java new file mode 100644 index 000000000..8b4347a6a --- /dev/null +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java @@ -0,0 +1,18 @@ +package org.hswebframework.web.i18n; + +import org.springframework.context.i18n.LocaleContext; +import org.springframework.lang.NonNull; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +public class WebFluxLocaleFilter implements WebFilter { + @Override + @NonNull + public Mono filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) { + return chain + .filter(exchange) + .subscriberContext(ctx -> ctx.put(LocaleContext.class, exchange.getLocaleContext())); + } +} diff --git a/hsweb-core/src/main/resources/i18n/messages_zh_CN.properties b/hsweb-core/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 000000000..a60689754 --- /dev/null +++ b/hsweb-core/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,2 @@ +error.not_found=数据不存在 +error.cant_create_instance=无法创建实例:{0} \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/PermissionProperties.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/PermissionProperties.java index 7f37d667f..e4765108d 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/configuration/PermissionProperties.java +++ b/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); } }; diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionUserService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionUserService.java index 20b0263f0..f3c8d7e7c 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultDimensionUserService.java +++ b/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 new BusinessException("重复的绑定请求")); + .onErrorMap(DuplicateKeyException.class, (err) -> new BusinessException("error.duplicate_key")); } @Override diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/messages_zh_CN.properties b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 000000000..0fade07c1 --- /dev/null +++ b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1 @@ +error.duplicate_key=重复的请求 \ No newline at end of file From 8b211de5e47a739d3149efd287636a5352fddecd Mon Sep 17 00:00:00 2001 From: zhou-hao Date: Fri, 25 Jun 2021 10:54:58 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BC=98=E5=8C=96i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authorization/annotation/TwoFactor.java | 2 +- .../AuthorizationI18nConfiguration.java | 20 ++++ .../main/resources/META-INF/spring.factories | 3 +- .../authentication/messages_en_US.properties | 16 ++++ .../messages_zh_CN.properties | 10 +- .../TwoFactorHandlerInterceptorAdapter.java | 3 +- .../basic/web/AuthorizationController.java | 5 +- .../web/oauth2/OAuth2Exception.java | 2 +- .../Commons18nConfiguration.java | 20 ++++ .../crud/web/CommonErrorControllerAdvice.java | 91 ++++++++++++------- .../crud/web/CommonWebFluxConfiguration.java | 9 +- .../main/resources/META-INF/spring.factories | 3 +- .../i18n/commons/messages_en_US.properties | 5 + .../i18n/commons/messages_zh_CN.properties | 5 + .../org/hswebframework/web/dict/EnumDict.java | 49 +++++----- .../web/exception/BusinessException.java | 4 - .../web/exception/ValidationException.java | 4 +- .../hswebframework/web/i18n/LocaleUtils.java | 6 +- .../web/i18n/WebFluxLocaleFilter.java | 17 +++- .../i18n/core/messages_en_US.properties | 3 + .../i18n/core/messages_zh_CN.properties | 4 + .../resources/i18n/messages_zh_CN.properties | 2 - .../starter/i18n/CompositeMessageSource.java | 70 ++++++++++++++ .../web/starter/i18n/I18nConfiguration.java | 38 ++++++++ .../main/resources/META-INF/spring.factories | 3 +- 25 files changed, 306 insertions(+), 88 deletions(-) create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java create mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en_US.properties rename hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/{ => authentication}/messages_zh_CN.properties (56%) create mode 100644 hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java create mode 100644 hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en_US.properties create mode 100644 hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties create mode 100644 hsweb-core/src/main/resources/i18n/core/messages_en_US.properties create mode 100644 hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties delete mode 100644 hsweb-core/src/main/resources/i18n/messages_zh_CN.properties create mode 100644 hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java create mode 100644 hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java index 1d3d6b98f..7805bb966 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/annotation/TwoFactor.java +++ b/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 "assert.verify_code_error"; + String message() default "validation.verify_code_error"; } diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java new file mode 100644 index 000000000..13430e3fe --- /dev/null +++ b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java @@ -0,0 +1,20 @@ +package org.hswebframework.web.authorization.configuration; + +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; + +@Configuration +public class AuthorizationI18nConfiguration { + + @Bean + public MessageSource authorizationMessageSource(){ + ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource(); + messageSource.setDefaultEncoding("UTF-8"); + messageSource.setBundleClassLoader(AuthorizationI18nConfiguration.class.getClassLoader()); + messageSource.setBasenames("i18n/authentication/messages"); + return messageSource; + } + +} diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories b/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories index cb2dcecb0..374161647 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories +++ b/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories @@ -1,3 +1,4 @@ # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration \ No newline at end of file +org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration,\ +org.hswebframework.web.authorization.configuration.AuthorizationI18nConfiguration \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en_US.properties b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_en_US.properties new file mode 100644 index 000000000..9e1bcb7d1 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh_CN.properties similarity index 56% rename from hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties rename to hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh_CN.properties index 412d3de60..a9bd62303 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/messages_zh_CN.properties +++ b/hsweb-authorization/hsweb-authorization-api/src/main/resources/i18n/authentication/messages_zh_CN.properties @@ -1,6 +1,8 @@ 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=已被禁止访问 @@ -8,7 +10,7 @@ message.token_state_expired=用户未登录 message.token_state_offline=用户已在其他地方登录 message.token_state_lock=登录状态已被锁定 # -assert.need_two_factor_verify=需要双因子验证 -assert.username_must_not_be_empty=用户名不能为空 -assert.password_must_not_be_empty=密码不能为空 -assert.verify_code_error=验证码错误 \ No newline at end of file +validation.need_two_factor_verify=需要双因子验证 +validation.username_must_not_be_empty=用户名不能为空 +validation.password_must_not_be_empty=密码不能为空 +validation.verify_code_error=验证码错误 \ No newline at end of file diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java index 5aa9efbcb..c76076d5f 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/twofactor/TwoFactorHandlerInterceptorAdapter.java +++ b/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; @@ -45,7 +44,7 @@ public class TwoFactorHandlerInterceptorAdapter extends HandlerInterceptorAdapte code = request.getHeader(factor.parameter()); } if (StringUtils.isEmpty(code)) { - throw new NeedTwoFactorException("assert.need_two_factor_verify", factor.provider()); + throw new NeedTwoFactorException("validation.need_two_factor_verify", factor.provider()); } else if (!validator.verify(code, factor.timeout())) { throw new NeedTwoFactorException(factor.message(), factor.provider()); } diff --git a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java b/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java index 659fe9879..a8f553f04 100644 --- a/hsweb-authorization/hsweb-authorization-basic/src/main/java/org/hswebframework/web/authorization/basic/web/AuthorizationController.java +++ b/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.username_must_not_be_empty"); - Assert.hasLength(password_, "assert.password_must_not_be_empty"); + Assert.hasLength(username_, "validation.username_must_not_be_empty"); + Assert.hasLength(password_, "validation.password_must_not_be_empty"); Function parameterGetter = parameters::get; return Mono.defer(() -> { diff --git a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java b/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java index fc48ad3e1..41d8a62a5 100644 --- a/hsweb-authorization/hsweb-authorization-oauth2/src/main/java/org/hswebframework/web/oauth2/OAuth2Exception.java +++ b/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; } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java new file mode 100644 index 000000000..fe73ac510 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java @@ -0,0 +1,20 @@ +package org.hswebframework.web.crud.configuration; + +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; + +@Configuration +public class Commons18nConfiguration { + + @Bean + public MessageSource commonsMessageSource(){ + ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource(); + messageSource.setDefaultEncoding("UTF-8"); + messageSource.setBundleClassLoader(Commons18nConfiguration.class.getClassLoader()); + messageSource.setBasenames("i18n/commons/messages"); + return messageSource; + } + +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java index 9f9b44b98..27943ce59 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java @@ -22,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; @@ -39,7 +36,6 @@ import java.util.stream.Collectors; @Order public class CommonErrorControllerAdvice { - private final MessageSource messageSource; public CommonErrorControllerAdvice(MessageSource messageSource) { @@ -58,25 +54,31 @@ public class CommonErrorControllerAdvice { @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Mono> handleException(UnsupportedOperationException e) { - return Mono.just(ResponseMessage.error("unsupported", e.getMessage())); + return LocaleUtils + .resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.error(401, "unsupported", msg))); } @ExceptionHandler @ResponseStatus(HttpStatus.UNAUTHORIZED) public Mono> handleException(UnAuthorizedException e) { - return Mono.just(ResponseMessage.error(401, "unauthorized", e.getMessage()).result(e.getState())); + return LocaleUtils + .resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.error(401, "unauthorized", msg).result(e.getState()))); } @ExceptionHandler @ResponseStatus(HttpStatus.FORBIDDEN) - public Mono> handleException(AccessDenyException e) { - return Mono.just(ResponseMessage.error(403, e.getCode(), e.getMessage())); + public Mono> handleException(AccessDenyException e) { + return LocaleUtils + .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(403, e.getCode(), e.getMessage())) + ; } @ExceptionHandler @ResponseStatus(HttpStatus.NOT_FOUND) - public Mono> handleException(NotFoundException e) { - return Mono.just(ResponseMessage.error(404, "not_found", e.getMessage())); + public Mono> handleException(NotFoundException e) { + return LocaleUtils + .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(404, "not_found", msg)) + ; } @ExceptionHandler @@ -138,6 +140,7 @@ public class CommonErrorControllerAdvice { @ExceptionHandler @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT) public Mono> handleException(TimeoutException e) { + return Mono.just(ResponseMessage.error(504, "timeout", e.getMessage())) .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e))); @@ -163,51 +166,70 @@ public class CommonErrorControllerAdvice { @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public Mono> 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> 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.getMessage(), e))) + ; } @ExceptionHandler @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) - public Mono> 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> handleException(UnsupportedMediaTypeStatusException e) { + return LocaleUtils + .resolveMessage(messageSource, "error.unsupported_media_type") + .map(msg -> ResponseMessage + .error(415, "unsupported_media_type", msg) + .result(e.getSupportedMediaTypes())) + .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e))); } @ExceptionHandler @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) public Mono> 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 + .resolveMessage(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> 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 + .resolveMessage(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> handleException(R2dbcDataIntegrityViolationException exception) { - if (exception.getMessage().contains("Duplicate")) { - return Mono.just(ResponseMessage.error("存在重复的数据")); + public Mono> 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 + .resolveMessage(messageSource, code) + .map(msg -> ResponseMessage.error(400, code, msg)); } @@ -223,7 +245,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)); } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java index 755d8a915..df47c43ce 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java @@ -10,12 +10,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.i18n.LocaleContext; import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.i18n.LocaleContextResolver; import reactor.core.publisher.Mono; @Configuration @@ -38,13 +40,6 @@ public class CommonWebFluxConfiguration { return new ResponseMessageWrapper(codecConfigurer.getWriters(), resolver, registry); } - @Bean - public MessageSource messageSource() { - ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); - messageSource.setBasenames("i18n/messages"); - messageSource.setDefaultEncoding("UTF-8"); - return messageSource; - } @Bean public WebFilter localeWebFilter() { diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories index 3a51d2ae5..c17a4b2d6 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories +++ b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories @@ -3,4 +3,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.hswebframework.web.crud.configuration.EasyormConfiguration,\ org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration,\ org.hswebframework.web.crud.configuration.R2dbcSqlExecutorConfiguration,\ -org.hswebframework.web.crud.web.CommonWebFluxConfiguration \ No newline at end of file +org.hswebframework.web.crud.web.CommonWebFluxConfiguration,\ +org.hswebframework.web.crud.configuration.Commons18nConfiguration \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en_US.properties b/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_en_US.properties new file mode 100644 index 000000000..f35bfc889 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties b/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties new file mode 100644 index 000000000..e988c8dee --- /dev/null +++ b/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=数据错误 \ No newline at end of file diff --git a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java index e98330323..e1d4ce2e1 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java @@ -127,7 +127,6 @@ public interface EnumDict extends JSONSerializable { } - /** * 从指定的枚举类中查找想要的枚举,并返回一个{@link Optional},如果未找到,则返回一个{@link Optional#empty()} * @@ -150,8 +149,8 @@ public interface EnumDict extends JSONSerializable { static List findList(Class type, Predicate predicate) { if (type.isEnum()) { return Arrays.stream(type.getEnumConstants()) - .filter(predicate) - .collect(Collectors.toList()); + .filter(predicate) + .collect(Collectors.toList()); } return Collections.emptyList(); } @@ -162,7 +161,9 @@ public interface EnumDict extends JSONSerializable { * @see this#find(Class, Predicate) */ static > Optional findByValue(Class 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))); } /** @@ -203,8 +204,8 @@ public interface EnumDict extends JSONSerializable { if (all.length >= 64) { List 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); } @@ -295,7 +296,7 @@ public interface EnumDict extends JSONSerializable { @AllArgsConstructor @NoArgsConstructor class EnumDictJSONDeserializer extends JsonDeserializer implements ObjectDeserializer { - private Function mapper; + private Function mapper; @Override @SuppressWarnings("all") @@ -324,8 +325,10 @@ public interface EnumDict 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 +350,11 @@ public interface EnumDict 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 +367,20 @@ public interface EnumDict extends JSONSerializable { findPropertyType = BeanUtils.findPropertyType(currentName, currentValue.getClass()); } Supplier exceptionSupplier = () -> { - List values= Stream.of(findPropertyType.getEnumConstants()) + List 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("validation.parameter_does_not_exist_in_enums", + Arrays.asList( + new ValidationException.Detail(currentName, "选项中不存在此值", values) + ), currentName); }; if (EnumDict.class.isAssignableFrom(findPropertyType) && findPropertyType.isEnum()) { if (node.isObject()) { @@ -394,12 +398,13 @@ public interface EnumDict extends JSONSerializable { .find(findPropertyType, node.textValue()) .orElseThrow(exceptionSupplier); } - throw new ValidationException("参数[" + currentName + "]在选项中不存在", Arrays.asList( + throw new ValidationException("validation.parameter_does_not_exist_in_enums", Arrays.asList( new ValidationException.Detail(currentName, "选项中不存在此值", null) - )); + ), currentName); } 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()); diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java index b46301e58..de3110580 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java @@ -43,10 +43,6 @@ public class BusinessException extends I18nSupportException { this(message, null, status, args); } - public BusinessException(String message, String code, Object... args) { - this(message, code, 500, args); - } - public BusinessException(String message, String code, int status, Object... args) { super(message, args); this.code = code; diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java index ba0802a8b..492631ab0 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java @@ -28,8 +28,8 @@ public class ValidationException extends BusinessException { this(message, Collections.singletonList(new Detail(property, message, null))); } - public ValidationException(String message, List details) { - super(message); + public ValidationException(String message, List details,Object... args) { + super(message,400,args); this.details = details; } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java index 4a6137f66..028d0bbd1 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java @@ -49,9 +49,9 @@ public class LocaleUtils { }); } - public static Mono reactiveMessage(MessageSource messageSource, - String code, - Object... args) { + public static Mono resolveMessage(MessageSource messageSource, + String code, + Object... args) { return reactive() .map(ctx -> resolveMessage(messageSource, code, ctx.getLocale(), code, args)); } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java index 8b4347a6a..4f788800f 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java @@ -1,18 +1,33 @@ package org.hswebframework.web.i18n; import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.SimpleLocaleContext; 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; +import java.util.Optional; + public class WebFluxLocaleFilter implements WebFilter { @Override @NonNull public Mono filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) { return chain .filter(exchange) - .subscriberContext(ctx -> ctx.put(LocaleContext.class, exchange.getLocaleContext())); + .subscriberContext(ctx -> ctx.put(LocaleContext.class, getLocaleContext(exchange))); + } + + public LocaleContext getLocaleContext(ServerWebExchange exchange) { + String lang = exchange.getRequest() + .getQueryParams() + .getFirst(":lang"); + if (StringUtils.hasText(lang)) { + return new SimpleLocaleContext(Locale.forLanguageTag(lang)); + } + return exchange.getLocaleContext(); } } diff --git a/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties b/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties new file mode 100644 index 000000000..37d8fb0a5 --- /dev/null +++ b/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties @@ -0,0 +1,3 @@ +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 \ No newline at end of file diff --git a/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties b/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties new file mode 100644 index 000000000..a51a0485d --- /dev/null +++ b/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties @@ -0,0 +1,4 @@ +error.not_found=数据不存在 +error.cant_create_instance=无法创建实例:{0} + +validation.parameter_does_not_exist_in_enums=参数[{0}]在选择中不存在 \ No newline at end of file diff --git a/hsweb-core/src/main/resources/i18n/messages_zh_CN.properties b/hsweb-core/src/main/resources/i18n/messages_zh_CN.properties deleted file mode 100644 index a60689754..000000000 --- a/hsweb-core/src/main/resources/i18n/messages_zh_CN.properties +++ /dev/null @@ -1,2 +0,0 @@ -error.not_found=数据不存在 -error.cant_create_instance=无法创建实例:{0} \ No newline at end of file diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java new file mode 100644 index 000000000..fcb22af11 --- /dev/null +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java @@ -0,0 +1,70 @@ +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.concurrent.CopyOnWriteArrayList; + +public class CompositeMessageSource implements MessageSource { + + private final List messageSources = new CopyOnWriteArrayList<>(); + + public void addMessageSources(Collection 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, defaultMessage, locale); + if (StringUtils.hasText(result)) { + return result; + } + } + return null; + } + + @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); + } +} diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java new file mode 100644 index 000000000..796500c47 --- /dev/null +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java @@ -0,0 +1,38 @@ +package org.hswebframework.web.starter.i18n; + +import org.hswebframework.web.exception.BusinessException; +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 java.util.stream.Collectors; + +@Configuration(proxyBeanMethods = false) +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +public class I18nConfiguration { + + + @Bean + public MessageSource coreMessageSource(){ + ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource(); + messageSource.setDefaultEncoding("UTF-8"); + messageSource.setBundleClassLoader(BusinessException.class.getClassLoader()); + messageSource.setBasenames("i18n/core/messages"); + return messageSource; + } + + @Bean + @Primary + public MessageSource compositeMessageSource(ObjectProvider objectProvider) { + CompositeMessageSource messageSource = new CompositeMessageSource(); + messageSource.addMessageSources(objectProvider.stream().collect(Collectors.toList())); + return messageSource; + } + + +} diff --git a/hsweb-starter/src/main/resources/META-INF/spring.factories b/hsweb-starter/src/main/resources/META-INF/spring.factories index 8357d086b..43f2e42db 100644 --- a/hsweb-starter/src/main/resources/META-INF/spring.factories +++ b/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 \ No newline at end of file +org.hswebframework.web.starter.CorsAutoConfiguration,\ +org.hswebframework.web.starter.i18n.I18nConfiguration \ No newline at end of file From d5a01ce922658f42e317cd7ab7f96249c89f70c2 Mon Sep 17 00:00:00 2001 From: zhou-hao Date: Thu, 1 Jul 2021 14:16:48 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/factory/MapperEntityFactory.java | 2 +- .../crud/web/CommonErrorControllerAdvice.java | 8 +- .../i18n/commons/messages_zh_CN.properties | 2 +- .../org/hswebframework/web/dict/EnumDict.java | 14 +- .../hswebframework/web/i18n/LocaleUtils.java | 114 +++++- .../web/i18n/MessageSourceInitializer.java | 12 + .../starter/i18n/CompositeMessageSource.java | 5 +- .../web/starter/i18n/I18nConfiguration.java | 2 + .../CustomCodecsAutoConfiguration.java | 1 + .../jackson/CustomJackson2jsonEncoder.java | 340 ++++++++++++++++++ .../CustomJackson2jsonEncoderTest.java | 88 +++++ .../resources/i18n/messages_en_US.properties | 2 + .../resources/i18n/messages_zh_CN.properties | 2 + 13 files changed, 570 insertions(+), 22 deletions(-) create mode 100644 hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java create mode 100644 hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java create mode 100644 hsweb-starter/src/test/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoderTest.java create mode 100644 hsweb-starter/src/test/resources/i18n/messages_en_US.properties create mode 100644 hsweb-starter/src/test/resources/i18n/messages_zh_CN.properties diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java index 39e156887..61ac7cd2b 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java @@ -172,7 +172,7 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory { return (T) new HashSet<>(); } - throw new NotFoundException("无法初始化实体类:"+beanClass); + throw new NotFoundException("can not create instance:"+beanClass); } @Override diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java index 27943ce59..fd320afe5 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java @@ -186,7 +186,7 @@ public class CommonErrorControllerAdvice { @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) public Mono> handleException(UnsupportedMediaTypeStatusException e) { return LocaleUtils - .resolveMessage(messageSource, "error.unsupported_media_type") + .resolveMessageReactive(messageSource, "error.unsupported_media_type") .map(msg -> ResponseMessage .error(415, "unsupported_media_type", msg) .result(e.getSupportedMediaTypes())) @@ -198,7 +198,7 @@ public class CommonErrorControllerAdvice { public Mono> handleException(NotAcceptableStatusException e) { return LocaleUtils - .resolveMessage(messageSource, "error.not_acceptable_media_type") + .resolveMessageReactive(messageSource, "error.not_acceptable_media_type") .map(msg -> ResponseMessage .error(406, "not_acceptable_media_type", msg) .result(e.getSupportedMediaTypes())) @@ -209,7 +209,7 @@ public class CommonErrorControllerAdvice { @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) public Mono> handleException(MethodNotAllowedException e) { return LocaleUtils - .resolveMessage(messageSource, "error.method_not_allowed") + .resolveMessageReactive(messageSource, "error.method_not_allowed") .map(msg -> ResponseMessage .error(406, "method_not_allowed", msg) .result(e.getSupportedMethods())) @@ -228,7 +228,7 @@ public class CommonErrorControllerAdvice { log.warn(e.getMessage(), e); } return LocaleUtils - .resolveMessage(messageSource, code) + .resolveMessageReactive(messageSource, code) .map(msg -> ResponseMessage.error(400, code, msg)); } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties b/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties index e988c8dee..9b9331846 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties +++ b/hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties @@ -1,5 +1,5 @@ error.unsupported_media_type=不支持的请求类型 -error.not_acceptable_media_type=不支持的响应类型 +error.not_acceptable_media_type=不支持的媒体类型 error.method_not_allowed=不支持的请求方法 error.duplicate_data=重复的数据 error.data_error=数据错误 \ No newline at end of file diff --git a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java index e1d4ce2e1..23f98caa0 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java +++ b/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; @@ -260,6 +260,14 @@ public interface EnumDict extends JSONSerializable { 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的时候,会写出此方法返回的对象 * @@ -271,7 +279,7 @@ public interface EnumDict extends JSONSerializable { if (isWriteJSONObjectEnabled()) { Map 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; @@ -281,7 +289,7 @@ public interface EnumDict 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 { diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java index 028d0bbd1..a36a68eb5 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java @@ -10,25 +10,86 @@ import java.util.Locale; import java.util.function.BiFunction; import java.util.function.Function; +/** + * 用于进行国际化消息转换 + * + * @author zhouhao + * @since 4.0.11 + */ public class LocaleUtils { public static final LocaleContext DEFAULT_CONTEXT = new SimpleLocaleContext(Locale.getDefault()); - public static Mono reactive() { + private static final ThreadLocal CONTEXT_THREAD_LOCAL = new ThreadLocal<>(); + + static MessageSource messageSource; + + /** + * 获取当前的语言地区,如果没有设置则返回系统默认语言 + * + * @return Locale + */ + public static Locale current() { + LocaleContext context = CONTEXT_THREAD_LOCAL.get(); + if (context == null || context.getLocale() == null) { + context = DEFAULT_CONTEXT; + } + return context.getLocale(); + } + + /** + * 在指定的语言环境中执行函数,只能在非响应式同步操作时使用,如:转换实体类中某些属性的国际化消息。 + *

+ * 在函数的逻辑中可以通过{@link LocaleUtils#current()}来获取当前语言. + * + * @param data 参数 + * @param locale 语言地区 + * @param mapper 函数 + * @param 参数类型 + * @param 函数返回类型 + * @return 返回值 + */ + public static R doWith(T data, Locale locale, BiFunction mapper) { + try { + CONTEXT_THREAD_LOCAL.set(new SimpleLocaleContext(locale)); + return mapper.apply(data, locale); + } finally { + CONTEXT_THREAD_LOCAL.remove(); + } + } + + /** + * 响应式方式获取当前语言地区 + * @return 语言地区 + */ + public static Mono currentReactive() { return Mono .subscriberContext() .map(ctx -> ctx .getOrEmpty(LocaleContext.class) - .orElse(DEFAULT_CONTEXT)); + .map(LocaleContext::getLocale) + .orElseGet(Locale::getDefault) + ); } + public static Mono resolveThrowable(S source, + BiFunction mapper) { + return resolveThrowable(messageSource, source, mapper); + } + public static Mono resolveThrowable(MessageSource messageSource, S source, BiFunction mapper) { return doWithReactive(messageSource, source, Throwable::getMessage, mapper, source.getArgs()); } + public static Mono resolveThrowable(S source, + BiFunction mapper, + Object... args) { + return resolveThrowable(messageSource, source, mapper, args); + } + public static Mono resolveThrowable(MessageSource messageSource, S source, BiFunction mapper, @@ -36,32 +97,63 @@ public class LocaleUtils { return doWithReactive(messageSource, source, Throwable::getMessage, mapper, args); } + public static Mono doWithReactive(S source, + Function message, + BiFunction mapper, + Object... args) { + return doWithReactive(messageSource, source, message, mapper, args); + } + public static Mono doWithReactive(MessageSource messageSource, S source, Function message, BiFunction mapper, Object... args) { - return reactive() - .map(ctx -> { + return currentReactive() + .map(locale -> { String msg = message.apply(source); - String newMsg = resolveMessage(messageSource, msg, ctx.getLocale(), msg, args); + String newMsg = resolveMessage(messageSource, locale, msg, msg, args); return mapper.apply(source, newMsg); }); } - public static Mono resolveMessage(MessageSource messageSource, - String code, - Object... args) { - return reactive() - .map(ctx -> resolveMessage(messageSource, code, ctx.getLocale(), code, args)); + public static Mono 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, - String code, 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); + } + } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java new file mode 100644 index 000000000..a30fcaca4 --- /dev/null +++ b/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 = messageSource; + } + } +} diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java index fcb22af11..aa96385ac 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java @@ -10,6 +10,7 @@ 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 { @@ -27,12 +28,12 @@ public class CompositeMessageSource implements MessageSource { @Override public String getMessage(@Nonnull String code, Object[] args, String defaultMessage, @Nonnull Locale locale) { for (MessageSource messageSource : messageSources) { - String result = messageSource.getMessage(code, args, defaultMessage, locale); + String result = messageSource.getMessage(code, args, null, locale); if (StringUtils.hasText(result)) { return result; } } - return null; + return defaultMessage; } @Override diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java index 796500c47..b098245d6 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java @@ -1,6 +1,7 @@ package org.hswebframework.web.starter.i18n; import org.hswebframework.web.exception.BusinessException; +import org.hswebframework.web.i18n.MessageSourceInitializer; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.context.MessageSource; @@ -31,6 +32,7 @@ public class I18nConfiguration { public MessageSource compositeMessageSource(ObjectProvider objectProvider) { CompositeMessageSource messageSource = new CompositeMessageSource(); messageSource.addMessageSources(objectProvider.stream().collect(Collectors.toList())); + MessageSourceInitializer.init(messageSource); return messageSource; } diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java index 8e1f57bd1..b6605d30f 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java +++ b/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)); }; } diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java new file mode 100644 index 000000000..a999fb9f6 --- /dev/null +++ b/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 { + + private static final byte[] NEWLINE_SEPARATOR = {'\n'}; + + private static final Map STREAM_SEPARATORS; + + private static final Map 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 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. + *

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 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 encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map 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 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 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 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 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 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 getEncodableMimeTypes() { + return getMimeTypes(); + } + + @Override + public List getStreamingMediaTypes() { + return Collections.unmodifiableList(this.streamingMediaTypes); + } + + @Override + public Map getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType, + @Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) { + + return (actualType != null ? getHints(actualType) : Hints.none()); + } + + + // Jackson2CodecSupport + + @Override + protected A getAnnotation(MethodParameter parameter, Class annotType) { + return parameter.getMethodAnnotation(annotType); + } +} \ No newline at end of file diff --git a/hsweb-starter/src/test/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoderTest.java b/hsweb-starter/src/test/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoderTest.java new file mode 100644 index 000000000..8fdbce763 --- /dev/null +++ b/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 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 { + e1("enum.e1"), + e2("enum.e2"); + + private final String text; + + @Override + public String getValue() { + return name(); + } + + } +} \ No newline at end of file diff --git a/hsweb-starter/src/test/resources/i18n/messages_en_US.properties b/hsweb-starter/src/test/resources/i18n/messages_en_US.properties new file mode 100644 index 000000000..04bf44168 --- /dev/null +++ b/hsweb-starter/src/test/resources/i18n/messages_en_US.properties @@ -0,0 +1,2 @@ +enum.e1=Option1 +enum.e2=Option2 \ No newline at end of file diff --git a/hsweb-starter/src/test/resources/i18n/messages_zh_CN.properties b/hsweb-starter/src/test/resources/i18n/messages_zh_CN.properties new file mode 100644 index 000000000..2a8d80a92 --- /dev/null +++ b/hsweb-starter/src/test/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,2 @@ +enum.e1=选项1 +enum.e2=选项2 \ No newline at end of file From 05e86743d12cd1ebc706eabb17f4f7b7775b9a3a Mon Sep 17 00:00:00 2001 From: zhou-hao Date: Thu, 1 Jul 2021 15:25:35 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/hswebframework/web/dict/EnumDict.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java index 23f98caa0..22439010d 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java @@ -118,7 +118,7 @@ public interface EnumDict extends JSONSerializable { } /** - * 枚举选项的描述,对一个选项进行详细的描述有时候是必要的.默认值为{@link this#getText()} + * 枚举选项的描述,对一个选项进行详细的描述有时候是必要的.默认值为{@link EnumDict#getText()} * * @return 描述 */ @@ -158,7 +158,7 @@ public interface EnumDict extends JSONSerializable { /** * 根据枚举的{@link EnumDict#getValue()}来查找. * - * @see this#find(Class, Predicate) + * @see EnumDict#find(Class, Predicate) */ static > Optional findByValue(Class type, Object value) { return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String @@ -169,7 +169,7 @@ public interface EnumDict extends JSONSerializable { /** * 根据枚举的{@link EnumDict#getText()} 来查找. * - * @see this#find(Class, Predicate) + * @see EnumDict#find(Class, Predicate) */ static Optional findByText(Class type, String text) { return find(type, e -> e.getText().equalsIgnoreCase(text)); @@ -178,7 +178,7 @@ public interface EnumDict extends JSONSerializable { /** * 根据枚举的{@link EnumDict#getValue()},{@link EnumDict#getText()}来查找. * - * @see this#find(Class, Predicate) + * @see EnumDict#find(Class, Predicate) */ static Optional find(Class type, Object target) { return find(type, v -> v.eq(target)); @@ -254,7 +254,7 @@ public interface EnumDict extends JSONSerializable { /** * @return 是否在序列化为json的时候, 将枚举以对象方式序列化 - * @see this#DEFAULT_WRITE_JSON_OBJECT + * @see EnumDict#DEFAULT_WRITE_JSON_OBJECT */ default boolean isWriteJSONObjectEnabled() { return DEFAULT_WRITE_JSON_OBJECT; @@ -269,10 +269,10 @@ public interface EnumDict extends JSONSerializable { } /** - * 当{@link this#isWriteJSONObjectEnabled()}返回true时,在序列化为json的时候,会写出此方法返回的对象 + * 当{@link EnumDict#isWriteJSONObjectEnabled()}返回true时,在序列化为json的时候,会写出此方法返回的对象 * * @return 最终序列化的值 - * @see this#isWriteJSONObjectEnabled() + * @see EnumDict#isWriteJSONObjectEnabled() */ @JsonValue default Object getWriteJSONObject() { From f3808589dba971d539fa01cf96e89eb4714d9d50 Mon Sep 17 00:00:00 2001 From: zhou-hao Date: Thu, 1 Jul 2021 18:33:24 +0800 Subject: [PATCH 5/7] =?UTF-8?q?jsr303=E6=94=AF=E6=8C=81i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crud/events/ValidateEventListener.java | 49 +++++++++++------ .../crud/web/CommonErrorControllerAdvice.java | 16 +++--- hsweb-core/pom.xml | 6 ++- .../org/hswebframework/web/dict/EnumDict.java | 9 +--- .../web/exception/BusinessException.java | 6 ++- .../web/exception/I18nSupportException.java | 15 ++++-- .../web/exception/ValidationException.java | 51 +++++++++++------- .../web/i18n/ContextLocaleResolver.java | 13 +++++ .../hswebframework/web/i18n/LocaleUtils.java | 53 +++++++++++++------ .../web/i18n/WebFluxLocaleFilter.java | 15 +++--- .../web/validator/ValidatorUtils.java | 21 +++++--- .../i18n/core/messages_en_US.properties | 3 +- .../i18n/core/messages_zh_CN.properties | 3 +- .../web/validator/ValidatorUtilsTest.java | 42 +++++++++++++++ .../jackson/CustomJackson2JsonDecoder.java | 40 +++++++++----- .../service/DefaultReactiveUserService.java | 5 +- .../messages_en_US.properties | 2 + .../messages_zh_CN.properties | 2 + .../resources/i18n/messages_zh_CN.properties | 1 - 19 files changed, 253 insertions(+), 99 deletions(-) create mode 100644 hsweb-core/src/main/java/org/hswebframework/web/i18n/ContextLocaleResolver.java create mode 100644 hsweb-core/src/test/java/org/hswebframework/web/validator/ValidatorUtilsTest.java create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en_US.properties create mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh_CN.properties delete mode 100644 hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/messages_zh_CN.properties diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java index 88bb3fc2f..96b292136 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/events/ValidateEventListener.java +++ b/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 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)); } - } } diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java index fd320afe5..8e7c63347 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java @@ -62,7 +62,8 @@ public class CommonErrorControllerAdvice { @ResponseStatus(HttpStatus.UNAUTHORIZED) public Mono> handleException(UnAuthorizedException e) { return LocaleUtils - .resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.error(401, "unauthorized", msg).result(e.getState()))); + .resolveThrowable(messageSource, e, (err, msg) -> (ResponseMessage.error(401, "unauthorized", msg) + .result(e.getState()))); } @ExceptionHandler @@ -84,14 +85,17 @@ public class CommonErrorControllerAdvice { @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public Mono>> handleException(ValidationException e) { - return Mono.just(ResponseMessage.>error(400, "illegal_argument", e.getMessage()) - .result(e.getDetails())); + return LocaleUtils + .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage + .>error(400, "illegal_argument",msg) + .result(e.getDetails())) + ; } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public Mono>> handleException(ConstraintViolationException e) { - return handleException(new ValidationException(e.getMessage(), e.getConstraintViolations())); + return handleException(new ValidationException(e.getConstraintViolations())); } @ExceptionHandler @@ -178,7 +182,7 @@ public class CommonErrorControllerAdvice { public Mono> handleException(AuthenticationException e) { return LocaleUtils .resolveThrowable(messageSource, e, (err, msg) -> ResponseMessage.error(400, e.getCode(), msg)) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e))) + .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))) ; } @@ -190,7 +194,7 @@ public class CommonErrorControllerAdvice { .map(msg -> ResponseMessage .error(415, "unsupported_media_type", msg) .result(e.getSupportedMediaTypes())) - .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getMessage(), e))); + .doOnEach(ReactiveLogger.onNext(r -> log.error(e.getLocalizedMessage(), e))); } @ExceptionHandler diff --git a/hsweb-core/pom.xml b/hsweb-core/pom.xml index 25a891231..3e3287c15 100644 --- a/hsweb-core/pom.xml +++ b/hsweb-core/pom.xml @@ -35,7 +35,6 @@ org.springframework spring-web - @@ -97,5 +96,10 @@ 3.0.0 + + org.hibernate.validator + hibernate-validator + + \ No newline at end of file diff --git a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java index 22439010d..ac5494c0b 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java @@ -385,10 +385,7 @@ public interface EnumDict extends JSONSerializable { return e.name(); }).collect(Collectors.toList()); - return new ValidationException("validation.parameter_does_not_exist_in_enums", - Arrays.asList( - new ValidationException.Detail(currentName, "选项中不存在此值", values) - ), currentName); + return new ValidationException(currentName,"validation.parameter_does_not_exist_in_enums", currentName); }; if (EnumDict.class.isAssignableFrom(findPropertyType) && findPropertyType.isEnum()) { if (node.isObject()) { @@ -406,9 +403,7 @@ public interface EnumDict extends JSONSerializable { .find(findPropertyType, node.textValue()) .orElseThrow(exceptionSupplier); } - throw new ValidationException("validation.parameter_does_not_exist_in_enums", Arrays.asList( - new ValidationException.Detail(currentName, "选项中不存在此值", null) - ), currentName); + return exceptionSupplier.get(); } if (findPropertyType.isEnum()) { return Stream diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java index de3110580..16a09d7a7 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/BusinessException.java @@ -31,7 +31,6 @@ public class BusinessException extends I18nSupportException { @Getter private int status = 500; - @Getter private String code; @@ -43,6 +42,11 @@ public class BusinessException extends I18nSupportException { this(message, null, status, args); } + public BusinessException(String message, String code) { + this(message, code, 500); + } + + public BusinessException(String message, String code, int status, Object... args) { super(message, args); this.code = code; diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java index 12a180ebe..ce7e3a6d0 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java @@ -1,27 +1,36 @@ 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; - private final 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 getLocalizedMessage() { - // TODO: 2021/6/21 - return super.getLocalizedMessage(); + return LocaleUtils.resolveMessage(code, args); } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java index 492631ab0..c3dfaa005 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java @@ -4,19 +4,17 @@ 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 List details; @@ -24,25 +22,36 @@ 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 details,Object... args) { - super(message,400,args); + public ValidationException(String message, List details, Object... args) { + super(message, 400, args); this.details = details; - } - - public ValidationException(String message, Set> 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)); - } + for (Detail detail : this.details) { + detail.translateI18n(args); } } + public ValidationException(Set> violations) { + ConstraintViolation first = violations.iterator().next(); + if (Objects.equals(first.getMessageTemplate(), first.getMessage())) { + //模版和消息相同,说明是自定义的message,而不是已经通过i18n获取的. + setCode(first.getMessage()); + } else { + setCode("validation.property_validate_failed"); + } + //{0} 属性 ,{1} 验证消息 + setArgs(new Object[]{first.getPropertyPath().toString(), 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 +64,11 @@ public class ValidationException extends BusinessException { @Schema(description = "详情") Object detail; + + public void translateI18n(Object... args) { + if (message.contains(".")) { + message = LocaleUtils.resolveMessage(message, message, args); + } + } } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/ContextLocaleResolver.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/ContextLocaleResolver.java new file mode 100644 index 000000000..cfc7b986f --- /dev/null +++ b/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(); + } +} diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java index a36a68eb5..c4c18817d 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java @@ -2,12 +2,15 @@ package org.hswebframework.web.i18n; import org.hswebframework.web.exception.I18nSupportException; import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContext; -import org.springframework.context.i18n.SimpleLocaleContext; 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; /** @@ -18,9 +21,9 @@ import java.util.function.Function; */ public class LocaleUtils { - public static final LocaleContext DEFAULT_CONTEXT = new SimpleLocaleContext(Locale.getDefault()); + public static final Locale DEFAULT_LOCALE = Locale.getDefault(); - private static final ThreadLocal CONTEXT_THREAD_LOCAL = new ThreadLocal<>(); + private static final ThreadLocal CONTEXT_THREAD_LOCAL = new ThreadLocal<>(); static MessageSource messageSource; @@ -30,11 +33,11 @@ public class LocaleUtils { * @return Locale */ public static Locale current() { - LocaleContext context = CONTEXT_THREAD_LOCAL.get(); - if (context == null || context.getLocale() == null) { - context = DEFAULT_CONTEXT; + Locale locale = CONTEXT_THREAD_LOCAL.get(); + if (locale == null) { + locale = DEFAULT_LOCALE; } - return context.getLocale(); + return locale; } /** @@ -51,27 +54,47 @@ public class LocaleUtils { */ public static R doWith(T data, Locale locale, BiFunction mapper) { try { - CONTEXT_THREAD_LOCAL.set(new SimpleLocaleContext(locale)); + CONTEXT_THREAD_LOCAL.set(locale); return mapper.apply(data, locale); } finally { CONTEXT_THREAD_LOCAL.remove(); } } + public static Function useLocale(Locale locale) { + return ctx -> ctx.put(Locale.class, locale); + } + + public static void doWith(Locale locale, Consumer consumer) { + try { + CONTEXT_THREAD_LOCAL.set(locale); + consumer.accept(locale); + } finally { + CONTEXT_THREAD_LOCAL.remove(); + } + } + /** * 响应式方式获取当前语言地区 + * * @return 语言地区 */ + @SuppressWarnings("all") public static Mono currentReactive() { return Mono .subscriberContext() - .map(ctx -> ctx - .getOrEmpty(LocaleContext.class) - .map(LocaleContext::getLocale) - .orElseGet(Locale::getDefault) - ); + .map(ctx -> ctx.getOrDefault(Locale.class, DEFAULT_LOCALE)); } + public static void onNext(Signal signal, BiConsumer 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 Mono resolveThrowable(S source, BiFunction mapper) { @@ -81,7 +104,7 @@ public class LocaleUtils { public static Mono resolveThrowable(MessageSource messageSource, S source, BiFunction mapper) { - return doWithReactive(messageSource, source, Throwable::getMessage, mapper, source.getArgs()); + return doWithReactive(messageSource, source, I18nSupportException::getCode, mapper, source.getArgs()); } public static Mono resolveThrowable(S source, diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java index 4f788800f..748d7faee 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/WebFluxLocaleFilter.java @@ -1,7 +1,5 @@ package org.hswebframework.web.i18n; -import org.springframework.context.i18n.LocaleContext; -import org.springframework.context.i18n.SimpleLocaleContext; import org.springframework.lang.NonNull; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; @@ -10,7 +8,6 @@ import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import java.util.Locale; -import java.util.Optional; public class WebFluxLocaleFilter implements WebFilter { @Override @@ -18,16 +15,20 @@ public class WebFluxLocaleFilter implements WebFilter { public Mono filter(@NonNull ServerWebExchange exchange, WebFilterChain chain) { return chain .filter(exchange) - .subscriberContext(ctx -> ctx.put(LocaleContext.class, getLocaleContext(exchange))); + .subscriberContext(LocaleUtils.useLocale(getLocaleContext(exchange))); } - public LocaleContext getLocaleContext(ServerWebExchange exchange) { + public Locale getLocaleContext(ServerWebExchange exchange) { String lang = exchange.getRequest() .getQueryParams() .getFirst(":lang"); if (StringUtils.hasText(lang)) { - return new SimpleLocaleContext(Locale.forLanguageTag(lang)); + return Locale.forLanguageTag(lang); } - return exchange.getLocaleContext(); + Locale locale = exchange.getLocaleContext().getLocale(); + if (locale == null) { + return Locale.getDefault(); + } + return locale; } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java index 13b9a16a0..e578f1b4d 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/validator/ValidatorUtils.java +++ b/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 tryValidate(T bean, Class... group) { Set> violations = getValidator().validate(bean, group); if (!violations.isEmpty()) { - throw new ValidationException(violations.iterator().next().getMessage(), violations); + throw new ValidationException(violations); } return bean; diff --git a/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties b/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties index 37d8fb0a5..b39f8b8dc 100644 --- a/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties +++ b/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties @@ -1,3 +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 \ No newline at end of file +validation.parameter_does_not_exist_in_enums=Parameter {0} does not exist in option +validation.property_validate_failed=Parameter '{0}' {1} \ No newline at end of file diff --git a/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties b/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties index a51a0485d..b222feca2 100644 --- a/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties +++ b/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties @@ -1,4 +1,5 @@ error.not_found=数据不存在 error.cant_create_instance=无法创建实例:{0} -validation.parameter_does_not_exist_in_enums=参数[{0}]在选择中不存在 \ No newline at end of file +validation.parameter_does_not_exist_in_enums=参数[{0}]在选择中不存在 +validation.property_validate_failed=参数'{0}'{1} \ No newline at end of file diff --git a/hsweb-core/src/test/java/org/hswebframework/web/validator/ValidatorUtilsTest.java b/hsweb-core/src/test/java/org/hswebframework/web/validator/ValidatorUtilsTest.java new file mode 100644 index 000000000..07ac1d5b9 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2JsonDecoder.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2JsonDecoder.java index 0c7d3bc56..576817be3 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2JsonDecoder.java +++ b/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 decodeToMono(@NonNull Publisher input, @NonNull ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map 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 diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java index 5fadaae38..43bfe03d8 100644 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/java/org/hswebframework/web/system/authorization/defaults/service/DefaultReactiveUserService.java +++ b/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 { - 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)) diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en_US.properties b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_en_US.properties new file mode 100644 index 000000000..280214ac6 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh_CN.properties b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/authentication-default/messages_zh_CN.properties new file mode 100644 index 000000000..16da067f5 --- /dev/null +++ b/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=用户已存在 \ No newline at end of file diff --git a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/messages_zh_CN.properties b/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/messages_zh_CN.properties deleted file mode 100644 index 0fade07c1..000000000 --- a/hsweb-system/hsweb-system-authorization/hsweb-system-authorization-default/src/main/resources/i18n/messages_zh_CN.properties +++ /dev/null @@ -1 +0,0 @@ -error.duplicate_key=重复的请求 \ No newline at end of file From 8a140bc8535a667b9638c9a30fcdefa538ddf843 Mon Sep 17 00:00:00 2001 From: zhou-hao Date: Fri, 2 Jul 2021 10:58:18 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=89=AB=E6=8F=8Fclasspa?= =?UTF-8?q?th*:i18n/**=E4=B8=8B=E7=9A=84messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthorizationI18nConfiguration.java | 20 ------------ .../Commons18nConfiguration.java | 20 ------------ .../entity/factory/MapperEntityFactory.java | 4 +-- .../crud/web/CommonWebFluxConfiguration.java | 7 ----- .../web/exception/I18nSupportException.java | 4 +++ .../web/exception/ValidationException.java | 11 ++++++- .../hswebframework/web/i18n/LocaleUtils.java | 2 +- .../web/i18n/MessageSourceInitializer.java | 2 +- .../web/i18n/UnsupportedMessageSource.java | 31 +++++++++++++++++++ .../i18n/core/messages_en_US.properties | 2 +- .../i18n/core/messages_zh_CN.properties | 2 +- .../web/starter/i18n/I18nConfiguration.java | 28 +++++++++++++---- 12 files changed, 73 insertions(+), 60 deletions(-) delete mode 100644 hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java delete mode 100644 hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java create mode 100644 hsweb-core/src/main/java/org/hswebframework/web/i18n/UnsupportedMessageSource.java diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java b/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java deleted file mode 100644 index 13430e3fe..000000000 --- a/hsweb-authorization/hsweb-authorization-api/src/main/java/org/hswebframework/web/authorization/configuration/AuthorizationI18nConfiguration.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.hswebframework.web.authorization.configuration; - -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.ResourceBundleMessageSource; - -@Configuration -public class AuthorizationI18nConfiguration { - - @Bean - public MessageSource authorizationMessageSource(){ - ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource(); - messageSource.setDefaultEncoding("UTF-8"); - messageSource.setBundleClassLoader(AuthorizationI18nConfiguration.class.getClassLoader()); - messageSource.setBasenames("i18n/authentication/messages"); - return messageSource; - } - -} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java deleted file mode 100644 index fe73ac510..000000000 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/configuration/Commons18nConfiguration.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.hswebframework.web.crud.configuration; - -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.ResourceBundleMessageSource; - -@Configuration -public class Commons18nConfiguration { - - @Bean - public MessageSource commonsMessageSource(){ - ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource(); - messageSource.setDefaultEncoding("UTF-8"); - messageSource.setBundleClassLoader(Commons18nConfiguration.class.getClassLoader()); - messageSource.setBasenames("i18n/commons/messages"); - return messageSource; - } - -} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java index 61ac7cd2b..1d2667584 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java +++ b/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("can not create instance:"+beanClass); + throw new NotFoundException("error.cant_create_instance", beanClass); } @Override diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java index df47c43ce..1c814012f 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonWebFluxConfiguration.java @@ -8,17 +8,10 @@ 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.context.i18n.LocaleContext; -import org.springframework.context.support.ReloadableResourceBundleMessageSource; -import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; -import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; -import org.springframework.web.server.i18n.LocaleContextResolver; -import reactor.core.publisher.Mono; @Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java index ce7e3a6d0..5ebf1925b 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java @@ -28,6 +28,10 @@ public class I18nSupportException extends RuntimeException { this.code = code; } + @Override + public String getMessage() { + return code; + } @Override public String getLocalizedMessage() { diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java index c3dfaa005..be5936ff0 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/ValidationException.java @@ -16,6 +16,8 @@ import java.util.*; @ResponseStatus(HttpStatus.BAD_REQUEST) public class ValidationException extends I18nSupportException { + private static final boolean propertyI18nEnabled = Boolean.getBoolean("i18n.validation.property.enabled"); + private List details; public ValidationException(String message) { @@ -42,8 +44,15 @@ public class ValidationException extends I18nSupportException { } else { setCode("validation.property_validate_failed"); } + String property = first.getPropertyPath().toString(); + //{0} 属性 ,{1} 验证消息 - setArgs(new Object[]{first.getPropertyPath().toString(), first.getMessage()}); + //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) { diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java index c4c18817d..2a64e1d12 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java @@ -25,7 +25,7 @@ public class LocaleUtils { private static final ThreadLocal CONTEXT_THREAD_LOCAL = new ThreadLocal<>(); - static MessageSource messageSource; + static MessageSource messageSource = UnsupportedMessageSource.instance(); /** * 获取当前的语言地区,如果没有设置则返回系统默认语言 diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java index a30fcaca4..59072c03d 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java @@ -5,7 +5,7 @@ import org.springframework.context.MessageSource; public class MessageSourceInitializer { public static void init(MessageSource messageSource) { - if (LocaleUtils.messageSource == null) { + if (LocaleUtils.messageSource == null || LocaleUtils.messageSource instanceof UnsupportedMessageSource) { LocaleUtils.messageSource = messageSource; } } diff --git a/hsweb-core/src/main/java/org/hswebframework/web/i18n/UnsupportedMessageSource.java b/hsweb-core/src/main/java/org/hswebframework/web/i18n/UnsupportedMessageSource.java new file mode 100644 index 000000000..8c040606d --- /dev/null +++ b/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(); + } +} diff --git a/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties b/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties index b39f8b8dc..27ba5f25b 100644 --- a/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties +++ b/hsweb-core/src/main/resources/i18n/core/messages_en_US.properties @@ -1,4 +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=Parameter '{0}' {1} \ No newline at end of file +validation.property_validate_failed={0} {1} \ No newline at end of file diff --git a/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties b/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties index b222feca2..181700341 100644 --- a/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties +++ b/hsweb-core/src/main/resources/i18n/core/messages_zh_CN.properties @@ -2,4 +2,4 @@ error.not_found=数据不存在 error.cant_create_instance=无法创建实例:{0} validation.parameter_does_not_exist_in_enums=参数[{0}]在选择中不存在 -validation.property_validate_failed=参数'{0}'{1} \ No newline at end of file +validation.property_validate_failed={0}{1} \ No newline at end of file diff --git a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java index b098245d6..c7b38a83e 100644 --- a/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java +++ b/hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java @@ -1,6 +1,7 @@ package org.hswebframework.web.starter.i18n; -import org.hswebframework.web.exception.BusinessException; +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; @@ -10,20 +11,35 @@ 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 - public MessageSource coreMessageSource(){ - ResourceBundleMessageSource messageSource=new ResourceBundleMessageSource(); + @SneakyThrows + public MessageSource autoResolveI18nMessageSource() { + + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setDefaultEncoding("UTF-8"); - messageSource.setBundleClassLoader(BusinessException.class.getClassLoader()); - messageSource.setBasenames("i18n/core/messages"); + 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; } From b1eba306e409a020ddaf122cd33d0216c07db905 Mon Sep 17 00:00:00 2001 From: zhou-hao Date: Fri, 2 Jul 2021 11:20:24 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8Di18n=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/META-INF/spring.factories | 3 +-- .../src/main/resources/META-INF/spring.factories | 3 +-- .../org/hswebframework/web/exception/I18nSupportException.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories b/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories index 374161647..cb2dcecb0 100644 --- a/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories +++ b/hsweb-authorization/hsweb-authorization-api/src/main/resources/META-INF/spring.factories @@ -1,4 +1,3 @@ # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration,\ -org.hswebframework.web.authorization.configuration.AuthorizationI18nConfiguration \ No newline at end of file +org.hswebframework.web.authorization.simple.DefaultAuthorizationAutoConfiguration \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories index c17a4b2d6..3a51d2ae5 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories +++ b/hsweb-commons/hsweb-commons-crud/src/main/resources/META-INF/spring.factories @@ -3,5 +3,4 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.hswebframework.web.crud.configuration.EasyormConfiguration,\ org.hswebframework.web.crud.configuration.JdbcSqlExecutorConfiguration,\ org.hswebframework.web.crud.configuration.R2dbcSqlExecutorConfiguration,\ -org.hswebframework.web.crud.web.CommonWebFluxConfiguration,\ -org.hswebframework.web.crud.configuration.Commons18nConfiguration \ No newline at end of file +org.hswebframework.web.crud.web.CommonWebFluxConfiguration \ No newline at end of file diff --git a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java index 5ebf1925b..ae2df4abe 100644 --- a/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java +++ b/hsweb-core/src/main/java/org/hswebframework/web/exception/I18nSupportException.java @@ -30,7 +30,7 @@ public class I18nSupportException extends RuntimeException { @Override public String getMessage() { - return code; + return super.getMessage() != null ? super.getMessage() : getLocalizedMessage(); } @Override