feat: error message support i18n (#271)

* feat: error message support i18n

* fix: ut failed
This commit is contained in:
vran
2023-08-27 19:09:34 +08:00
committed by GitHub
parent afc7b18330
commit 421ebc8005
25 changed files with 358 additions and 211 deletions

View File

@@ -5,8 +5,10 @@ import com.databasir.common.JsonData;
import com.databasir.common.SystemException;
import com.databasir.common.exception.Forbidden;
import com.databasir.common.exception.InvalidTokenException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -30,15 +32,19 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@RestControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
private final MessageSource messageSource;
@ExceptionHandler({ConstraintViolationException.class})
public ResponseEntity<Object> handleConstraintViolationException(
ConstraintViolationException constraintViolationException, WebRequest request) {
ConstraintViolationException constraintViolationException, WebRequest request) {
String errorMsg = "";
String path = getPath(request);
@@ -46,17 +52,20 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
for (ConstraintViolation<?> item : violations) {
errorMsg = item.getMessage();
log.warn("ConstraintViolationException, request: {}, exception: {}, invalid value: {}",
path, errorMsg, item.getInvalidValue());
path, errorMsg, item.getInvalidValue());
break;
}
return handleNon200Response(errorMsg, HttpStatus.BAD_REQUEST, path);
}
@ExceptionHandler({InvalidTokenException.class})
protected ResponseEntity<Object> handleInvalidTokenException(InvalidTokenException ex, WebRequest request) {
protected ResponseEntity<Object> handleInvalidTokenException(InvalidTokenException ex,
WebRequest request,
Locale locale) {
String path = getPath(request);
log.warn("handle InvalidTokenException " + path + ", " + ex);
JsonData<Object> data = JsonData.error(ex.getErrCode(), ex.getErrMessage());
String msg = messageSource.getMessage(ex.getErrCode(), ex.getArgs(), locale);
JsonData<Object> data = JsonData.error(ex.getErrCode(), msg);
return handleNon200Response(ex.getMessage(), HttpStatus.UNAUTHORIZED, path, data);
}
@@ -82,17 +91,21 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
}
@ExceptionHandler(value = DatabasirException.class)
public ResponseEntity<Object> handleBusinessException(
DatabasirException databasirException, WebRequest request) {
public ResponseEntity<Object> handleBusinessException(DatabasirException databasirException,
WebRequest request,
Locale locale) {
String path = getPath(request);
JsonData<Void> body = JsonData.error(databasirException.getErrCode(), databasirException.getErrMessage());
String msg = messageSource.getMessage(databasirException.getErrCode(), databasirException.getArgs(), locale);
JsonData<Void> body = JsonData.error(databasirException.getErrCode(), msg);
if (databasirException.getCause() == null) {
log.warn("BusinessException, request: {}, exception: {}", path, databasirException.getErrMessage());
log.warn("BusinessException, request: {}, exception: {}", path, msg);
} else {
log.warn("BusinessException, request: " + path, databasirException);
}
return ResponseEntity.ok().body(body);
return ResponseEntity.ok()
.header("X-Error-Code", databasirException.getErrCode())
.body(body);
}
@ExceptionHandler({SystemException.class})
@@ -101,7 +114,7 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
String path = getPath(request);
if (systemException.getCause() != null) {
log.error("SystemException, request: " + path
+ ", exception: " + systemException.getMessage() + ", caused by:", systemException.getCause());
+ ", exception: " + systemException.getMessage() + ", caused by:", systemException.getCause());
} else {
log.error("SystemException, request: " + path + ", exception: " + systemException.getMessage());
}
@@ -119,7 +132,7 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
@Override
public ResponseEntity<Object> handleBindException(
BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String errorMsg = buildMessages(ex.getBindingResult());
log.warn("BindException, request: {}, exception: {}", getPath(request), errorMsg);
@@ -128,7 +141,7 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String errorMsg = buildMessages(ex.getBindingResult());
log.warn("MethodArgumentNotValidException, request: {}, exception: {}", getPath(request), errorMsg);
@@ -137,7 +150,7 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
@Override
public ResponseEntity<Object> handleTypeMismatch(
TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("TypeMismatchException, request: {}, exception: {}", getPath(request), ex.getMessage());
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
@@ -145,16 +158,16 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
@Override
public ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("MissingServletRequestParameterException, request: {}, exception: {}",
getPath(request), ex.getMessage());
getPath(request), ex.getMessage());
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
}
@Override
public ResponseEntity<Object> handleMissingServletRequestPart(
MissingServletRequestPartException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
MissingServletRequestPartException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("MissingServletRequestPartException, request: {}, exception: {}", getPath(request), ex.getMessage());
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
@@ -162,7 +175,7 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
@Override
public ResponseEntity<Object> handleHttpMessageNotReadable(
HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String errorMsg = ex.getMostSpecificCause().getMessage();
@@ -172,7 +185,7 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
@Override
public ResponseEntity<Object> handleServletRequestBindingException(
ServletRequestBindingException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ServletRequestBindingException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("ServletRequestBindingException, request: {}, exception: {}", getPath(request), ex.getMessage());
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
@@ -180,7 +193,7 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
@Override
public ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String errorMsg = ex.getMessage();
log.warn("HttpRequestMethodNotSupportedException, request: {}, exception: {}", getPath(request), errorMsg);
@@ -222,7 +235,7 @@ public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
}
private ResponseEntity<Object> handleOverriddenException(
Exception ex, HttpHeaders headers, HttpStatus status, WebRequest request, String errorMsg) {
Exception ex, HttpHeaders headers, HttpStatus status, WebRequest request, String errorMsg) {
return handleExceptionInternal(ex, null, headers, status, request);
}

View File

@@ -0,0 +1,29 @@
package com.databasir.api.config.i18n;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import java.util.Locale;
@Configuration
public class I18nConfig {
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(Locale.CHINA);
return localeResolver;
}
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setDefaultEncoding("UTF-8");
messageSource.addBasenames("i18n.messages");
return messageSource;
}
}

View File

@@ -5,14 +5,17 @@ import com.databasir.core.domain.DomainErrors;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.ServletException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
@Service
@RequiredArgsConstructor
@@ -21,16 +24,22 @@ public class DatabasirAuthenticationEntryPoint implements AuthenticationEntryPoi
private final ObjectMapper objectMapper;
private final MessageSource messageSource;
private final LocaleResolver localeResolver;
@Override
public void commence(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.warn("验证未通过. 提示信息 - {} - {} - {}", request.getRequestURI(),
authException.getClass().getName(),
authException.getMessage());
authException.getClass().getName(),
authException.getMessage());
Locale locale = localeResolver.resolveLocale(request);
DomainErrors err = DomainErrors.INVALID_ACCESS_TOKEN;
JsonData<Void> data = JsonData.error(err.getErrCode(), err.getErrMessage());
String msg = messageSource.getMessage(err.getErrCode(), err.exception().getArgs(), locale);
JsonData<Void> data = JsonData.error(err.getErrCode(), msg);
String jsonString = objectMapper.writeValueAsString(data);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getOutputStream().write(jsonString.getBytes(StandardCharsets.UTF_8));

View File

@@ -6,6 +6,7 @@ import com.databasir.core.domain.log.service.OperationLogService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
@@ -13,12 +14,14 @@ import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
@Component
@RequiredArgsConstructor
@@ -29,6 +32,10 @@ public class DatabasirAuthenticationFailureHandler implements AuthenticationFail
private final OperationLogService operationLogService;
private final MessageSource messageSource;
private final LocaleResolver localeResolver;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
@@ -50,7 +57,9 @@ public class DatabasirAuthenticationFailureHandler implements AuthenticationFail
response.getOutputStream().write(jsonString.getBytes(StandardCharsets.UTF_8));
} else if (exception instanceof DatabasirAuthenticationException) {
DatabasirAuthenticationException bizException = (DatabasirAuthenticationException) exception;
JsonData<Void> data = JsonData.error(bizException.getErrCode(), bizException.getErrMessage());
Locale locale = localeResolver.resolveLocale(request);
String msg = messageSource.getMessage(bizException.getErrCode(), bizException.getArgs(), locale);
JsonData<Void> data = JsonData.error(bizException.getErrCode(), msg);
saveLoginFailedLog(username, data.getErrMessage());
String jsonString = objectMapper.writeValueAsString(data);
response.setStatus(HttpStatus.OK.value());

View File

@@ -27,7 +27,7 @@ public class CronExpressionValidator {
try {
new CronExpression(cron);
} catch (ParseException pe) {
throw DomainErrors.INVALID_CRON_EXPRESSION.exception("错误的 CRON 表达式:" + pe.getMessage(), pe);
throw DomainErrors.INVALID_CRON_EXPRESSION.exception(pe);
}
}
}

View File

@@ -2,23 +2,23 @@ package com.databasir.api.validator;
import org.springframework.stereotype.Component;
import static com.databasir.core.domain.DomainErrors.INVALID_DATABASE_TYPE_URL_PATTERN;
import static com.databasir.core.domain.DomainErrors.*;
@Component
public class DatabaseTypeValidator {
public void isValidUrlPattern(String urlPattern) {
if (urlPattern == null) {
throw INVALID_DATABASE_TYPE_URL_PATTERN.exception("url pattern 不能为空");
throw INVALID_DATABASE_TYPE_URL_PATTERN.exception();
}
if (!urlPattern.contains("{{jdbc.protocol}}")) {
throw INVALID_DATABASE_TYPE_URL_PATTERN.exception("必须包含变量{{jdbc.protocol}}");
throw MISS_JDBC_PROTOCOL.exception();
}
if (!urlPattern.contains("{{db.url}}")) {
throw INVALID_DATABASE_TYPE_URL_PATTERN.exception("必须包含变量{{db.url}}不能为空");
throw MISS_DB_URL.exception();
}
if (!urlPattern.contains("{{db.schema}}") && !urlPattern.contains("{{db.name}}")) {
throw INVALID_DATABASE_TYPE_URL_PATTERN.exception("{{db.schema}} 和 {{db.name}} 至少设置一个");
throw MISS_DB_SCHEMA.exception();
}
}
}

View File

@@ -0,0 +1,52 @@
# token
X_0001=token expired
X_0002=token invalid
X_0004=access token invalid
error.refresh-token.expired=token expired
error.refresh-token.invalid=token invalid
error.access-token.invalid=token invalid
# common
error.network.timeout=network timeout, please try later
error.parameter.required=miss required parameter,{0}
# database
error.database.metadata.get-failed=get database info failed
error.database.connect-failed=connect to database faeild
error.database.type.not-supported=not support database type, please check project configuration
error.database.type.name-duplicate=database type name duplicate
error.database.type.must-not-modify-default-type=forbidden operation
error.database.driver.download-failed=download driver failed
error.database.driver.class-not-found=driver class not found
error.database.driver.upload-failed=upload driver failed
error.database.driver.url-or-path-invalid=driver url or path invalid
error.database.driver.load-failed=load driver failed
error.database.url-pattern.invalid=invalid url pattern
error.database.url-patter.miss-db-url=must include {{db.url}} variable
error.database.url-patter.miss-protocol=must include {{jdbc.protocol}} variable
error.database.url-patter.miss-db-schema=must include {{db.schema}} or {{db.name}}
# document
error.document.version-invalid=invalid document version
error.document.version-duplicate=document version duplicate
error.document.table.not-found=table not found
# user
error.user.password.must-not-be-blank=password must not be blank
error.user.password.not-match=password not match
error.user.password.invalid=password not right
error.user.username-or-email-duplicate=username or email duplicate
error.user.role.duplicate=role duplicate
error.user.role.must-not-update-self=forbidden operation
error.user.must-not-enable-self=forbidden operation
error.user.must-not-delete-self=forbidden operation
# project
error.project.not-found=project not found
error.project.name-duplicate=duplicate project name
error.project.cron-invalid=invalid cron expression
# login-app
error.login.app.registration-id-duplicate=duplicate registration ID
error.login.app.registration-id-not-found=not found registration ID
error.login.app.miss-redirect-uri=miss parameter: redirect_uri
# mock script
error.script.mock.is-blank=mock script must not be blank
error.script.mock.dependent-column-name-required=miss required reference
error.script.mock.dependent-must-not-ref-self=should not ref to self
error.script.mock.dependent-circle-reference=circle reference
error.script.mock.expression-invalid=invalid expression

View File

@@ -0,0 +1,52 @@
# token
X_0001=token 过期
X_0002=tokena 无效
X_0004=access token 无效
error.refresh-token.expired=token expired
error.refresh-token.invalid=token invalid
error.access-token.invalid=token invalid
# common
error.network.timeout=网络不稳定,请稍后重试
error.parameter.required=缺少必要的参数:{0}
# database
error.database.metadata.get-failed=数据库信息获取失败
error.database.connect-failed=数据库连接失败
error.database.type.not-supported=不支持得数据库类型,请检查配置
error.database.type.name-duplicate=数据库类型名称重复
error.database.type.must-not-modify-default-type=禁止删除系统默认得数据库类型
error.database.driver.download-failed=驱动下载失败
error.database.driver.class-not-found=驱动类加载失败,请检查是否为合法的 JDBC 驱动
error.database.driver.upload-failed=驱动上传失败
error.database.driver.url-or-path-invalid=请手动上传或指定驱动下载 URL
error.database.driver.load-failed=驱动加载失败
error.database.url-pattern.invalid=不合法得 url 表达式
error.database.url-patter.miss-db-url=非法的 url 表达式: 缺少 {{db.url}}
error.database.url-patter.miss-protocol=非法的 url 表达式: 缺少 {{jdbc.protocol}}
error.database.url-patter.miss-db-schema=非法的 url 表达式: {{db.schema}} 和 {{db.name}} 至少指定一个
# document
error.document.version-invalid=文档版本不合法
error.document.version-duplicate=文档版本重复
error.document.table.not-found=找不到表信息
# user
error.user.password.must-not-be-blank=密码不能为空
error.user.password.not-match=密码不匹配
error.user.password.invalid=密码错误
error.user.username-or-email-duplicate=用户名或邮箱重复
error.user.role.duplicate=角色重复
error.user.role.must-not-update-self=禁止的操作
error.user.must-not-enable-self=禁止的操作
error.user.must-not-delete-self=禁止的操作
# project
error.project.not-found=项目不存在
error.project.name-duplicate=项目名称重复
error.project.cron-invalid=非法的 cron 表达式
# login-app
error.login.app.registration-id-duplicate=registration ID 重复
error.login.app.registration-id-not-found=registration ID 不存在
error.login.app.miss-redirect-uri=缺少 redirect_uri
# mock script
error.script.mock.is-blank=表达式不能为空
error.script.mock.dependent-column-name-required=请指定列
error.script.mock.dependent-must-not-ref-self=禁止引用自身
error.script.mock.dependent-circle-reference=检测到循环依赖
error.script.mock.expression-invalid=非法的表达式