1、背景
当一个项目需要支持国际化时,如何在不改变后端代码的基础上增加语言?
当业务处理流程复制的时候,每种异常的抛出是否都需要try-catch?
各种请求参数的校验是否都需要一个一个去写?
其实我们会发现这些问题都可以在框架层很轻松的解决掉,减少大量冗余代码的同时,增强了系统的可扩展性和鲁棒性。
2、多语言配置
配置messageSource,指定多语言文件的路径
@Configuration
public class MessageSourceConfig implements WebMvcConfigurer {
@Value("${i18n.meessages.path:}")
private String messagesPath;
@Bean(name = "messageSource")
@ConditionalOnMissingBean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource rbms = new ReloadableResourceBundleMessageSource();
if(!StringUtils.isEmpty(messagesPath)){
//设置path
rbms.addBasenames(messagesPath.split(","));
}
rbms.setDefaultEncoding("UTF-8");
rbms.setUseCodeAsDefaultMessage(true);
rbms.setFallbackToSystemLocale(false);
return rbms;
}
/**
* 使用session存储语言信息
*
* @return
*/
// @Bean
// public LocaleResolver localeResolver() {
// SessionLocaleResolver slr = new SessionLocaleResolver();
// slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
// return slr;
// }
/**
* 使用cookie存储语言信息,默认语言-简体中文
*
* @return
*/
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver slr = new CookieLocaleResolver();
slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return slr;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("language");
return lci;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
多语言的配置方式有很多种,需要根据项目需要选择,CookieLocaleResolver是将语言信息缓存在cookie中,请求时设置参数language即可实现语言的切换。还可换为SessionLocaleResolver使用Session缓存。LocaleChangeInterceptor只是将language取出来缓存到指定的地方。
获取当前的多语言有静态方法可获取:
LocaleContextHolder.getLocale()
实际项目可能语言信息是存在header中的,除messageSource的配置外,其他的都可根据实际情况配置,修改后只需将I18nUtils中的getLocale改为实际可获取多语言的方法即可。
获取多语言的静态工具
public class I18nUtils {
private static final Logger logger = LoggerFactory.getLogger(I18nUtils.class);
/**
* 获得多语言内容,默认根据当前用户的Local获取
* @param key 多语言key
* @return 翻译后的值,如获取不到则返回key
*/
public static String getMessage(String key){
return getMessage(key,new Object[]{});
}
/**
* 获取多语言内容--带默认值,默认根据当前用户的Local获取
* @param key 多语言key
* @param defaultMsg 默认返回值
* @return 翻译后的值,获取不到返回默认值
*/
public static String getMessage(String key,String defaultMsg){
return getMessage(key,defaultMsg,new Object[]{});
}
/**
* 获得多语言内容--带参数,默认根据当前用户的Local获取
* @param key 多语言key
* @param params 参数
* @return 翻译后的值,如获取不到则返回key
*/
public static String getMessage(String key,Object[] params){
Locale locale = getLocale();
if(params == null){
params = new Object[]{};
}
return getMessage(key,params,locale);
}
/**
* 获得多语言内容--带参数--带默认值,默认根据当前用户的Local获取
* @param key 多语言key
* @param defaultMsg 默认返回值
* @param params 参数
* @return 翻译后的值,获取不到返回默认值
*/
public static String getMessage(String key,String defaultMsg,Object[] params){
Locale locale = getLocale();
return getMessage(key,params,defaultMsg,locale);
}
/**
* 不带参数
* @param key 多语言key
* @param locale 语言信息
* @return 翻译后的值
*/
public static String getMessage(String key,Locale locale){
return getMessage(key,new Object[]{},locale);
}
/**
* 获取多语言数据
* @param key 多语言key
* @param params 参数
* @param locale 语言信息
* @return 翻译后的值
*/
public static String getMessage(String key,Object[] params,Locale locale){
try{
return AppContext.getContext().getMessage(key,params,locale);
}catch (Exception e){
logger.error("get locale msg error" ,e);
}
return key;
}
/**
* 获取多语言数据
* @param key 多语言key
* @param params 参数
* @param defaultMsg 默认返回值
* @param locale 语言信息
* @return 翻译后的值
*/
public static String getMessage(String key, Object[] params, String defaultMsg, Locale locale) {
return AppContext.getContext().getMessage(key,params,defaultMsg,locale);
}
/**
* 获取当前语言
*
* @return
*/
private static Locale getLocale(){
return LocaleContextHolder.getLocale();
}
}
application.properties中配置多语言文件路径
#多语言文件路径
i18n.meessages.path=classpath:messages/message
3、参数校验框架配置
配置快速失败模式
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
/**设置validator模式为快速失败返回*/
postProcessor.setValidator(validator());
return postProcessor;
}
}
原生的hibernate-validator功能已非常丰富,无语额外配置。
4、全局异常处理配置
@RestControllerAdvice
public class CustomExceptionHandler {
private Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);
/**
* get请求的参数校验失败
*
* @param exception
* @return
*/
@ExceptionHandler
public BaseResponse handle(ConstraintViolationException exception) {
logger.error("param validation error", exception);
Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
if (!CollectionUtils.isEmpty(violations)) {
String message = violations.iterator().next().getMessage();
logger.error("validate error,message={0}", message);
return BaseResponseBuilder.createResponse(DefaultErrorCode.VALIDATE_ERROR, message);
}
return BaseResponseBuilder.createResponse(DefaultErrorCode.ERROR);
}
/**
* post请求校验失败
*
* @param exception
* @return
*/
@ExceptionHandler
public BaseResponse handle(MethodArgumentNotValidException exception) {
logger.error("param validation error", exception);
List<ObjectError> errors = exception.getBindingResult().getAllErrors();
String code = DefaultErrorCode.VALIDATE_ERROR.getCode();
String msg = null;
if (!CollectionUtils.isEmpty(errors)) {
int first = 0;
for (int i = 0; i < errors.size(); i++) {
ObjectError error = errors.get(i);
String message = error.getDefaultMessage();
String field = null;
if (error instanceof FieldError) {
field = ((FieldError) error).getField();
}
message = I18nUtils.getMessage(message);
if (i == first) {
//取一个错误信息传给前端
msg = message;
}
logger.error("validate error, field={0}, message={1}", field, message);
}
}
return new BaseResponse(code, msg);
}
@ExceptionHandler
public BaseResponse handle(CustomBusinessException e) {
//errorCode处理
String code = e.getCode();
logger.error("business error", e);
return BaseResponseBuilder.createResponse(code, e.getMessage(), e.getParams());
}
/**
* 其他没有处理的错误
*
* @param e
* @return
*/
@ExceptionHandler
public BaseResponse<Object> handle(Exception e) {
logger.error("unkown error", e);
return BaseResponseBuilder.createResponse(DefaultErrorCode.ERROR);
}
}
5、测试
5.1 测试业务异常的抛出
直接在接口中抛出业务异常,业务异常上配置多语言信息
@GetMapping("/test3")
public BaseResponse test3(){
throw new CustomBusinessException(BusinessErrorCode.BUSINESS_ERROR_1);
}
public enum BusinessErrorCode implements IErrorCode{
BUSINESS_ERROR_1("20001", "business.error.test"),
;
......
配置多语言中的中文翻译
success=成功
unkown.error=未知系统错误
validate.error=参数校验错误
param1.empty.error=param1不能为空
business.error.test=业务异常
5.2 测试请求参数校验
@PostMapping("/test2")
public BaseResponse test2(@RequestBody @Valid RequestBo requestBo){
return BaseResponseBuilder.createResponse();
}
@Data
public class RequestBo {
@NotEmpty(message = "param1.empty.error")
private String param1;
private Integer param2;
}
5.3 多语言切换