Ccframe采用标准的spring data i18n方案。在处理多语言异常时,做了一些针对性的处理。包括以下几个方面:
多语言支持
引入LocalConfig,设置默认的语言,指定i18n properties的位置:
package org.ccframe.app;
import org.ccframe.config.GlobalEx;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import java.util.Locale;
@Configuration
public class LocaleConfig {
@Value("${app.default-ocale:zh-CN}") // 默认语言
String defaultLocale;
/**
* 默认解析器 其中locale表示默认语言
*/
@Bean
public LocaleContextResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.forLanguageTag(defaultLocale));
resolver.setCookieName(GlobalEx.CCFRAME_LOCALE);
resolver.setCookieMaxAge(Integer.MAX_VALUE);
return resolver;
}
}
我们可以通过application-dev.properties设置app.default-locale指定异常的默认多语言,默认是中文(zh-CN)
由于是微服务系统,采用session记录就不太合适了,因此,采用了cookie来针对locale进行记录,默认的cookie key是ccframeLocale
多语言切换
添加一个controller方法进行切换:
@GetMapping("locale") //多语言切换
public ResponseEntity<Void> locale(String lang, @ApiIgnore HttpServletRequest request, @ApiIgnore HttpServletResponse response) {
localeContextResolver.resolveLocaleContext(request);
localeContextResolver.setLocale(request, response, Locale.forLanguageTag(lang));
return new ResponseEntity<Void>(HttpStatus.OK);
}
这样前端可以通过该API调用实现语言切换
多语言异常
异常主要定义两种,一种是业务异常,一种是系统异常。
业务异常
BusinessException:
public BusinessException(String msgKey, Object[] args){ this.msgKey = msgKey ; this.args = args; }
主要是msgKey和args
msgKey为properties文件里的key,而args为带入的参数。为了开发方便,定义了一个原样输出的msgKey:
# -- direct output message -- message={0}
系统异常
由于整合了dubbo,当dubbo无法转换异常时,会将异常包装为一个RuntimeException输出,同样,Lobok的@SneakyThrows也会干同样的活。因此,在@RestControllerAdvice捕获输出时,实际是无法捕获到原始的异常信息的。但是有一个公共的特点,就是异常的message会包含异常类的原始信息,例如
org.springframework.orm.ObjectOptimisticLockingFailureException: oh my god; nested exception is java.lang.Exception: shit\r\norg.springframework.orm.ObjectOptimisticLockingFailureException: oh my god; nested exception is java.lang.Exception: shit\r\n\tat org.ccframe.subsys.core.service.ParamService.testException(ParamService.java:46)\r\n\tat org.ccframe.subsys.core.service.ParamService$$FastClassBySpringCGLIB
也就是ObjectOptimisticLockingFailureException实际上包含了该异常文本,因此我们不需要具体的异常类,只需根据异常的文本进行转换,就得到了多语言的properties key
通过一个正则提取了异常的前部分,并根据异常名称进行国际化处理:
/**
* 提取异常类名的正则.
*/
private static final Pattern CLASS_PATTERN = Pattern.compile("^([a-z]+(\\.[a-z_][a-z0-9_]*)*\\.[A-Za-z0-9_]*Exception): ");
具体规则实现的逻辑:
1)当异常为BusinessException时,根据BusinessException的msgKey找到国际化资源,并根据args带入占位符参数。该异常打包进API jar,因此可以被Service Interface处理。
2)当异常为Trowable时(例如包装过的RuntimeException),先尝试从国际化里查找key,如果找到了,根据国际化返回一个对应的内容。当找不到时,输出错误Throwable的message
3)错误最终输出Result对象,message为异常提示的内容,而result部分则为错误的原始信息,包括国际化的key,或者Exception的全名称(当没有找到国际化key时,使用全名)。这样前端可以根据返回不同的异常类型,分支处理对应的逻辑代码
具体实现类如下:
package org.ccframe.commons.mvc;
import com.alibaba.fastjson2.JSON;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.ccframe.commons.base.BusinessException;
import org.ccframe.commons.filter.CcRequestLoggingFilter;
import org.ccframe.config.GlobalEx;
import org.ccframe.subsys.core.dto.Result;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RestControllerAdvice
@Log4j2
public class GlobalRestControllerAdvice implements ResponseBodyAdvice<Object> {
private MessageSource messageSource; //国际化资源
private LocaleResolver localeResolver;
private static final Pattern CONTROLLER_PATTERN = Pattern.compile("^org\\.ccframe\\.(subsys|sdk)\\.[a-z0-9]+\\.controller\\. ");
private Object[] EMPTY_ARGS = new Object[0];
/**
* 提取异常类名的正则.
*/
private static final Pattern CLASS_PATTERN = Pattern.compile("^([a-z]+(\\.[a-z_][a-z0-9_]*)*\\.[A-Za-z0-9_]*Exception): ");
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return CONTROLLER_PATTERN.matcher(returnType.getContainingClass().getName()).find(); // 只有自己的cotroller类才需要进入,否则swagger都会挂了
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
System.out.println(returnType.getContainingClass());
Result<Object> result = new Result<>();
result.setResult(body);
result.setCode(HttpStatus.SC_OK);
if(returnType.getParameterType() == String.class){ //String返回要特殊处理
return JSON.toJSONString(result);
}else {
return result;
}
}
public GlobalRestControllerAdvice(MessageSource messageSource, LocaleResolver localeResolver){
this.messageSource = messageSource;
this.localeResolver = localeResolver;
}
private Result<String> createError(HttpServletRequest request, Throwable tr,int code, String msgKey, Object[] args){
Locale currentLocale = localeResolver.resolveLocale(request);
String message = "";
try {
message = messageSource.getMessage(msgKey, args, currentLocale);
}catch (NoSuchMessageException ex){
message = tr.getMessage();
}finally {
if(tr instanceof BusinessException){ //业务异常采用简短日志
log.error(message);
}else{
log.error(message, tr); //其它异常采用详细日志
}
CcRequestLoggingFilter.pendingLog(); //服务器可以记录出错时的请求啦😂
}
String messageReturn = msgKey;
if(GlobalEx.ORIGIN_MESSAGE_KEY.equals(msgKey)){ //模板输出时,msgKey为异常类名
messageReturn = tr.getClass().getSimpleName(); //JDK异常直接记录异常名称,否则记录模板key
}
return Result.error(code, message, messageReturn);
}
@ExceptionHandler(NoHandlerFoundException.class)
public Result<?> handlerNoFoundException(HttpServletRequest request, Exception e) {
return createError(request, e, HttpStatus.SC_NOT_FOUND, "error.mvc.uriNotFound", EMPTY_ARGS);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<?> httpRequestMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException e){
return createError(request,e, HttpStatus.SC_NOT_FOUND,"error.mvc.methodNotSupported",
new Object[]{e.getMethod(), StringUtils.join(e.getSupportedMethods(), GlobalEx.DEFAULT_TEXT_SPLIT_CHAR)});
}
@ExceptionHandler(BusinessException.class)
public Result<?> businessException(HttpServletRequest request, BusinessException e){
return createError(request,e, HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMsgKey(), e.getArgs());
}
@ExceptionHandler(MaxUploadSizeExceededException.class) // 文件上传超限,nginx请设置为10M
public Result<?> handleMaxUploadSizeExceededException(HttpServletRequest request, MaxUploadSizeExceededException e) {
return createError(request, e, HttpStatus.SC_INTERNAL_SERVER_ERROR, "error.mvc.fileTooLarge", EMPTY_ARGS);
}
@ExceptionHandler(Throwable.class)
public Result<?> handleException(HttpServletRequest request, Throwable tr) {
Matcher matcher = CLASS_PATTERN.matcher(tr.getMessage());
String resultStr = tr.getClass().getName(); // 默认是异常的message
if(matcher.find()){
resultStr = matcher.group(1); //如果以class形式开头,则按照class名找i18n资源
}
return createError(request,tr, HttpStatus.SC_INTERNAL_SERVER_ERROR, resultStr, EMPTY_ARGS);
}
}
对应的语言资源文件: