Spring Boot Restfull 国际化
一、思路
基于Restful的spring boot应用,涉及国际化的部分主要包括
1.请求参数异常信息国际化,此处参数校验采用hibernate validator,利用hibernate自身国际化机制。
2. 用户业务异常信息国际化,解决方法将业务异常进行封装,指定业务异常编码(对应国际化.prppreties的key),通过全局异常处理机制(@RestControllerAdvice),解析出当前语系,并根据异常编码,拿到对应的国际化信息,然后进行封装,返回给前端。
3. 其它信息国际化,通过AOP对返回信息拦截。返回对象封装如下:
@Getter
@NoArgsConstructor
public class ApiResult implements Serializable{
private static final long serialVersionUID = 1L;
public static final Integer STATUS_SUCCESS = 0;
public static final Integer STATUS_FAILED = -1;
/** 状态 **/
private Integer status;
/** i18n信息 **/
private String message;
/** i18n message key,对每个请求,需要解析出语系,填充message信息 **/
@NonNull
private String code;
/** 返回数据 **/
private Object rows;
public ApiResult(Integer status, @NonNull String code) {
super();
this.status = status;
this.code = code;
}
public ApiResult(Integer status, @NonNull String code, Object rows) {
super();
this.status = status;
this.code = code;
this.rows = rows;
}
public static ApiResult success() {
ApiResult apiResult = new ApiResult(STATUS_SUCCESS, I18nResourceCode.COMMON_INFO_SUCCESS.getCode());
return apiResult;
}
public static ApiResult failed() {
ApiResult apiResult = new ApiResult(STATUS_FAILED, I18nResourceCode.COMMON_INFO_FAIELD.getCode());
return apiResult;
}
public static ApiResult internalError() {
ApiResult apiResult = new ApiResult(STATUS_FAILED, I18nResourceCode.COMMON_ERROR_INTERNAL.getCode());
return apiResult;
}
public ApiResult setStatus(Integer status) {
this.status = status;
return this;
}
public ApiResult setMessage(String message) {
this.message = message;
return this;
}
public ApiResult setCode(String code) {
this.code = code;
return this;
}
public ApiResult setRows(Object rows) {
this.rows = rows;
return this;
}
@Setter
@Getter
@NoArgsConstructor
public static class Page {
private Integer pageSize;
private Integer pageNumber;
private Integer totalPage;
private Integer totalRows;
public Page(Integer pageSize, Integer pageNumber, Integer totalPage, Integer totalRows) {
super();
this.pageSize = pageSize;
this.pageNumber = pageNumber;
this.totalPage = totalPage;
this.totalRows = totalRows;
}
public Page(Integer pageSize, Integer pageNumber, Integer totalPage) {
super();
this.pageSize = pageSize;
this.pageNumber = pageNumber;
this.totalPage = totalPage;
}
}
}
二、步骤
2.1 配置ResourceBundleMessageSource
@Configuration
public class ResourceConfig {
@Bean
@Primary
ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource bundleMessageSource = new ResourceBundleMessageSource();
bundleMessageSource.setDefaultEncoding("UTF-8");
// 指定国际化资源目录,其中i18n/error为文件夹,ValidationMessages为国际化文件前缀
bundleMessageSource.setBasenames("i18n/error/ValidationMessages");
bundleMessageSource.setCacheMillis(10);
return bundleMessageSource;
}
}
ResourceBundleMessageSource 读取资源属性文件(.properties),然后根据.properties文件的名称信息(本地化信息),匹配当前系统的国别语言信息,然后获取相应的properties文件的内容。
配置文件的命名格式一般为${name}_${language}_${region},此处指定了.properties的存储路径(i18n/error),baseName为ValidationMessages,也可以通过yml配置。
i18n国际化文件目录见下图
2.2 配置国际化文件解析组件
@Component
public class I18nComponent {
private static final java.util.List<Locale> DEFAULT_ACCEPT_LOACLES = Arrays.asList(Locale.US, Locale.CHINESE);
// 请求头信息,通过此解析locale
public static final String HTTP_ACCEPT_LANGUAGE = "Accept-Language";
@Autowired
private ResourceBundleMessageSource bundleMessageSource;
public Locale getLocale() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
String locale = httpServletRequest.getHeader(HTTP_ACCEPT_LANGUAGE);
if (StringUtils.isEmpty(locale)) {
return Locale.US;
}
return Locale.lookup(Locale.LanguageRange.parse(locale), DEFAULT_ACCEPT_LOACLES);
}
public String getLoacleMessage(String propertyKey) {
Locale locale = getLocale();
String message = bundleMessageSource.getMessage(propertyKey, null, locale);
return message;
}
public String getLoacleMessageWithPlaceHolder(String propertyKey,Object ... params) {
Locale locale = getLocale();
String message = bundleMessageSource.getMessage(propertyKey, params, locale);
return message;
}
}
通过,此方法类,可以根据响应的键,拿到对应的国际化信息,其中public Locale getLocale(){}是解析当前设置语系,getLoacleMessage()和getLoacleMessageWithPlaceHolder()根据语系获取对应国际化信息。其中getLoacleMessageWithPlaceHolder()会将参数 param 替换到占位符,具体格式如下:
参考博客:https://blog.csdn.net/zhengyongcong/article/details/48666787
2.3 Hibernate validator配置
处理参数异常国际化,具体配置如下:
@Configuration
public class ValidatorConfig {
@Resource
private ResourceBundleMessageSource resourceBundleMessageSource;
@Bean
public Validator validator() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(resourceBundleMessageSource);
return validator;
}
}
对于参数校验异常信息,直接利用hibernate 国际化机制,直接获取国际化信息,详见validaterExceptionHandler处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionAdvice {
@Autowired
private I18nComponent i18nComponent;
@ExceptionHandler(BusinessException.class)
public ApiResult businessExceptionHandler(final BusinessException businessException) {
ApiResult apiResult = null;
try {
// 异常信息国际化
apiResult = new ApiResult(ApiResult.STATUS_FAILED, businessException.getI18nResourceCode().getCode());
apiResult.setMessage(i18nComponent.getLoacleMessage(businessException.getI18nResourceCode().getCode()));
} catch (Exception e) {
log.error("", e);
apiResult = getLoacleInternalErrorApiResult();
}
return apiResult;
}
@ExceptionHandler(Exception.class)
public ApiResult uncaughtExceptionHandler(Exception exception) {
log.error("", exception);
return getLoacleInternalErrorApiResult();
}
/**
* 参数校验异常
*
* @param exception
* @return
*/
@ExceptionHandler(value = { BindException.class, MethodArgumentNotValidException.class,
ConstraintViolationException.class })
public ApiResult validaterExceptionHandler(final Exception exception) {
ApiResult apiResult = null;
try {
String i18nMessage = null;
apiResult = new ApiResult(ApiResult.STATUS_FAILED,
I18nResourceCode.COMMON_ERROR_PARAMETER_VALIDATE.getCode());
if (exception instanceof MethodArgumentNotValidException) {
i18nMessage = methodArgumentNotValidExceptionHandler((MethodArgumentNotValidException) exception);
} else if (exception instanceof ConstraintViolationException) {
i18nMessage = methodConstraintViolationExceptionHandler((ConstraintViolationException) exception);
} else if (exception instanceof BindException) {
i18nMessage = methodBindExceptionHandler((BindException) exception);
}
apiResult.setMessage(i18nMessage);
} catch (Exception e) {
// TODO Auto-generated catch block
log.error("", e);
apiResult = getLoacleInternalErrorApiResult();
e.printStackTrace();
}
return apiResult;
}
private String methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException exception) {
String i18nMessage = exception.getBindingResult().getFieldError().getDefaultMessage();
return i18nMessage;
}
private String methodConstraintViolationExceptionHandler(ConstraintViolationException exception) {
String i18nMessage = null;
if (!CollectionUtils.isEmpty(exception.getConstraintViolations())) {
Iterator<ConstraintViolation<?>> ite = exception.getConstraintViolations().iterator();
i18nMessage = ite.hasNext() ? ite.next().getMessage() : exception.getLocalizedMessage();
} else {
i18nMessage = exception.getLocalizedMessage();
}
return i18nMessage;
}
private String methodBindExceptionHandler(BindException exception) {
FieldError fieldError = exception.getFieldError();
String i18nMessage = fieldError.getDefaultMessage();
return i18nMessage;
}
private ApiResult getLoacleInternalErrorApiResult() {
ApiResult apiResult = ApiResult.internalError();
apiResult.setMessage(i18nComponent.getLoacleMessage(apiResult.getCode()));
return apiResult;
}
}
2.4 业务异常信息处理
对也为异常信息进行封装,业务异常封装见下,异常处理逻辑见businessExceptionHandler()
@Getter
public class BusinessException extends RuntimeException {
/* error code of i18n */
@NonNull
private I18nResourceCode i18nResourceCode;
public BusinessException(@NonNull I18nResourceCode i18nResourceCode) {
super();
this.i18nResourceCode = i18nResourceCode;
}
public BusinessException(@NonNull I18nResourceCode i18nResourceCode, Throwable cause) {
super(i18nResourceCode.getCode(), cause);
}
}
其中I18nResourceCode为统一的国际化信息汇总类,定义如下:
@Getter
@AllArgsConstructor
public enum I18nResourceCode {
COMMON_INFO_SUCCESS("common.info.success"),
COMMON_INFO_FAIELD("common.info.failed"),
COMMON_ERROR_INTERNAL("common.error.internal"),
COMMON_ERROR_PARAMETER_VALIDATE("common.error.parameter.validate"),
//business exception
MODULE_USER_ERROR_EDIT_FAILED("module.user.error.edit.failed");
private String code;
//user parameter error code
public static final String USER_ERROR_ID_IS_NOT_INT_RANGE = "{user.error.id.is.not.in.range}";
public static final String USER_ERROR_NAME_MUST_NOT_BE_EMPTY = "{user.error.name.must.not.be.empty}";
}
2.5 其它业务非异常国际化信息处理
@ControllerAdvice(annotations = RestController.class)
public class I18nMessageAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private I18nComponent i18nComponent;
private static final Class[] DEFUALT_SUPPORT_ANNOTATIONS = { RequestMapping.class, GetMapping.class,
PostMapping.class, DeleteMapping.class, PutMapping.class };
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// TODO Auto-generated method stub
AnnotatedElement annotatedElement = returnType.getAnnotatedElement();
return Arrays.stream(DEFUALT_SUPPORT_ANNOTATIONS)
.anyMatch(annotaion -> annotaion.isAnnotation() && annotatedElement.isAnnotationPresent(annotaion));
}
/**
* 返回信息統一封裝為ApiResult,并进行国际化信息处理
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
// TODO Auto-generated method stub
ApiResult apiResult = null;
try {
if (body == null) {
apiResult = ApiResult.internalError();
} else if (body instanceof ApiResult) {
apiResult = (ApiResult) body;
} else {
// not support
apiResult = ApiResult.internalError();
}
apiResult = apiResult.setMessage(i18nComponent.getLoacleMessage(apiResult.getCode()));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
apiResult = ApiResult.internalError();
apiResult.setMessage(i18nComponent.getLoacleMessage(apiResult.getCode()));
}
return apiResult;
}
}
详细源码可以参见我的GitHub地址:https://github.com/dongzhi1129/spring-boot/tree/master/spring-boot-i18n