【项目地址】 从零开始学习springmvc
如果觉得有用可以关注一下公众号:码字不易,求赞求关注
五、Spring国际化和全局异常处理
五、Spring国际化和全局异常处理
5.1 国际化介绍
国际化的意思指对于同一个信息,可以识别不同的用户,从而展现出匹配用户当地语言信息。比如中文"提交",对于不懂中文的英国人你要使用“post”来表达一样。
对于页面来说,可以根据用户输入的Accept-Language
请求头来识别用户语言环境,从而加载已经提前准备好的语言资源包,展现出适配用户语言的环境。
5.1.1 ResourceBundle的介绍
ResourceBundle
是java.util
包下的工具类,主要用来解决国际化问题。当程序需要一个特定于语言环境的资源时(如 String),程序可以从适合当前用户语言环境的资源包(大多数情况下也就是.properties文件)中加载它。这样可以很大程度上独立于用户语言环境的程序代码,它将资源包中大部分(即便不是全部)特定于语言环境的信息隔离开来。
5.1.2 ResourceBundle的使用
-
新建语言资源包,为了适配spring国际化资源包的要求如下:
- 新建资源包目录名为
i18n
目录 - 以messages开头命名资源文件,则此目录下以messages开头的properties文件会被认为是一个资源包,资源包的
baseName
为messages。格式为messages_语言(小写)_国家(大写)
,如messages_zh_CN.properties
代表简体中文,messages_en_US.properties
代表美式英语。 messages.properties
----存放不需要国际化的消息
# messages_zh_CN.properties response.404.code=404 response.404.message.0001=请求参数不合法。 # messages_en_US.properties response.404.code=404 response.404.message.0001=Request parameters are invalid.
- 新建资源包目录名为
-
使用ResourceBundle
package org.numb.common.util; import java.util.Locale; import java.util.ResourceBundle; import org.junit.Test; public class MessagesTest { @Test public void test_messagesConsistency() { ResourceBundle zhMessages = ResourceBundle.getBundle("i18n/messages", Locale.SIMPLIFIED_CHINESE); System.out.println(zhMessages.getString("response.404.message.0001")); ResourceBundle enMessages = ResourceBundle.getBundle("i18n/messages", Locale.US); System.out.println(enMessages.getString("response.404.message.0001")); } }
-
测试
5.1.3 Spring国际化概述
5.1.3.1 Spring国际化资源文件约定
一般需要两个条件才可以确定一个特定类型的本地化信息,它们分别是“语言类型”和“国家/地区的类型”。比如中国大陆地区的中文是zh_CN,美式英语是en_US,英式英语是en_GB,国际标准规范ISO-3166规定了常用的国家和地区编码,这里给出了一些常用的国家地区编码
语言 | 简称 |
---|---|
简体中文(中国) | zh_CN |
繁体中文(中国台湾) | zh_TW |
繁体中文(中国香港) | zh_HK |
英语(中国香港) | en_HK |
英语(美国) | en_US |
英语(英国) | en_GB |
英语(全球) | en_WW |
英语(加拿大) | en_CA |
英语(澳大利亚) | en_AU |
英语(爱尔兰) | en_IE |
英语(芬兰) | en_FI |
芬兰语(芬兰) | fi_FI |
英语(丹麦) | en_DK |
丹麦语(丹麦) | da_DK |
英语(以色列) | en_IL |
希伯来语(以色列) | he_IL |
英语(南非) | en_ZA |
英语(印度) | en_IN |
英语(挪威) | en_NO |
英语(新加坡) | en_SG |
英语(新西兰) | en_NZ |
英语(印度尼西亚) | en_ID |
英语(菲律宾) | en_PH |
英语(泰国) | en_TH |
英语(马来西亚) | en_MY |
英语(阿拉伯) | en_XA |
韩文(韩国) | ko_KR |
日语(日本) | ja_JP |
荷兰语(荷兰) | nl_NL |
荷兰语(比利时) | nl_BE |
葡萄牙语(葡萄牙) | pt_PT |
葡萄牙语(巴西) | pt_BR |
法语(法国) | fr_FR |
法语(卢森堡) | fr_LU |
法语(瑞士) | fr_CH |
法语(比利时) | fr_BE |
法语(加拿大) | fr_CA |
西班牙语(拉丁美洲) | es_LA |
西班牙语(西班牙) | es_ES |
西班牙语(阿根廷) | es_AR |
西班牙语(美国) | es_US |
西班牙语(墨西哥) | es_MX |
西班牙语(哥伦比亚) | es_CO |
西班牙语(波多黎各) | es_PR |
德语(德国) | de_DE |
德语(奥地利) | de_AT |
德语(瑞士) | de_CH |
俄语(俄罗斯) | ru_RU |
意大利语(意大利) | it_IT |
希腊语(希腊) | el_GR |
挪威语(挪威) | no_NO |
匈牙利语(匈牙利) | hu_HU |
土耳其语(土耳其) | tr_TR |
捷克语(捷克共和国) | cs_CZ |
斯洛文尼亚语 | sl_SL |
波兰语(波兰) | pl_PL |
瑞典语(瑞典) | sv_SE |
西班牙语(智利) | es_CL |
i18n(来源于internationalization的首末字符i和n,18为中间的字符数)是“国际化”的简称,一般将资源文件放在resources\i18n
目录下,各资源文件以messages_
开头
resources
|----i18n
| |----messages.properties // 不需要国际化的消息
| |----messages_zh_CN.properties // 中文消息
| |----messages_en_US.properties // 英文消息
| |----..... // 其他消息
5.1.3.2 如何使用spring国际化
Spring国际化主要是依赖资源文件绑定器ResourceBundleMessageSource
。
<!--spring国际化-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n/messages"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
basename
类似于【5.1.2 ResourceBundle的使用】中ResourceBundle的basename,要配到资源名下,这里i18n是文件夹,messages是资源包名。defaultEncoding
指定编码方式
在使用之前为了方便引入日志框架slf4j+log4j2
,可以参考我的log4j2专栏。
配置如下
<?xml version="1.0" encoding="UTF-8"?>
<Configuration name="log-demo-config" status="error" monitorInterval="10">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{yyyy-MM-dd;HH:mm:ss.SSS Z}] [%-5p] [%t] [%c] %m%n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="org.numb" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
</Loggers>
</Configuration>
ResourceBundleMessageSource
使用
/**
* 抽象父类AbstractMessageSource的方法
*
* @param code 指国际化资源文件中的key
* @param args 可以将参数放入国际化信息中,如 "{0}", "{1,date}", "{2,time}" 或 null,后面具体演示如何使用
* @param locale 本地化信息
* @throws NoSuchMessageException 如果找不到国际化key指会抛此异常
* @return 国际化后的信息
*/
public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException
这里封装一个工具MessageProcessor
,通过request的getLocale()方法获取Locale,ErrorCode
封装所有的国际化key。
public class ErrorCode {
/**
* 400相关错误国际化信息
*/
public static final String INVALID_PARAMETERS_MESSAGE = "response.400.message.0001";
/**
* 500相关错误国际化信息
*/
public static final String INTERNAL_ERROR_DEFAULT_MESSAGE =
"response.500.message.0001";
}
@Component
public class MessageProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(MessageProcessor.class);
@Resource
private ResourceBundleMessageSource messageSource;
public String getMessage(HttpServletRequest request, String msgCode, Object... params) {
if (request == null || msgCode == null) {
throw new IllegalArgumentException("request or message code is null!");
}
try {
return messageSource.getMessage(msgCode, params, request.getLocale());
} catch (NoSuchMessageException exception) {
LOGGER.error("can not find the message of the message code of {}", msgCode);
return messageSource.getMessage(INTERNAL_ERROR_DEFAULT_MESSAGE, params, request.getLocale());
}
}
}
资源文件
response.400.message.0001=请求参数不合法
response.404.message.0001=资源不存在。
response.500.message.0001=系统错误,请稍后重试。
response.400.message.0001=Request parameters are invalid
response.404.message.0001=Request resource can not be found.
response.500.message.0001=System error, please try again later.
测试,UserController使用如下:
@RequestMapping(value = "/user/{user_id}", method = RequestMethod.DELETE, consumes =
MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@ResponseStatus(code = HttpStatus.OK)
public String deleteUser(HttpServletRequest request, HttpServletResponse response,
@PathVariable(value = "user_id") String userId) {
String userName = userService.getUserName(userId);
if (userName == null) {
return messageProcessor.getMessage(request, INVALID_PARAMETERS_MESSAGE);
}
// delete user
return "success";
}
ResourceBundleMessageSource
也可以在国际化信息中传入参数,可以传入一个object[]数组,传入{0}, {1}…代表数组的每个索引的参数,如下所示
国际化资源包
response.400.message.0002=请求参数 {0} 不合法。
response.400.message.0002=Request parameters {0} are invalid.
UserController
@RequestMapping(value = "/user/{user_id}", method = RequestMethod.DELETE, consumes =
MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@ResponseStatus(code = HttpStatus.OK)
public String deleteUser(HttpServletRequest request, HttpServletResponse response,
@PathVariable(value = "user_id") String userId) {
String userName = userService.getUserName(userId);
if (userName == null) {
return messageProcessor.getMessage(request, "response.400.message.0002", "userId");
}
// delete user
return "success";
}
测试:
5.1.3.3 如何确定本地信息(获取Locale信息)
上面直接根据request.getLocale()获取的Locale信息,此方法会寻找Accept-Language
请求头信息,如果没有则返回默认的Locale。上小节我们并未特意强调传入Accept-Language
请求头,所以是获取的默认Locale。这样一个问题是服务部署在不同环境上,默认Locale会不同导致国际化失效。下面将明确指出本地化的方法。
1. 前端如何传Locale信息
- 基于浏览器语言(常用方式):根据Request Headers中的
Accept-Language
来判断。 - 基于客户端传参:要求客户端第一次(或者每次)传递的自定义参数值来判断。如果在第一次传参中确定,那么locale信息要存入session或者cookie中,后面的请求语言方式则直接从两者中取,其有效时间与session和cookie设置的生命周期关联。这种方式一般用于需要覆盖请求头的Accept-Language。
- 基于默认配置:当获取语言类型时没有找到对应类型时,会使用默认的语言类型
2. 后端如何获取Locale信息
-
请求头获取
AcceptHeaderLocaleResolver
:默认根据Accept-Language
请求头判断Locale信息。// 可以继承此类,重写resolveLocale方法,获取自定义的请求头 public class AcceptHeaderLocaleResolver implements LocaleResolver { // 获取Locale信息 @Override public Locale resolveLocale(HttpServletRequest request) { Locale defaultLocale = getDefaultLocale(); if (defaultLocale != null && request.getHeader("Accept-Language") == null) { return defaultLocale; } Locale requestLocale = request.getLocale(); List<Locale> supportedLocales = getSupportedLocales(); if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) { return requestLocale; } Locale supportedLocale = findSupportedLocale(request, supportedLocales); if (supportedLocale != null) { return supportedLocale; } return (defaultLocale != null ? defaultLocale : requestLocale); } // 设置Locale信息,默认不支持设置Locale信息 @Override public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) { throw new UnsupportedOperationException( "Cannot change HTTP accept header - use a different locale resolution strategy"); } }
-
session获取
SessionLocaleResolver
:public class SessionLocaleResolver extends AbstractLocaleContextResolver { // 根据session获取Locale信息,获取不到返回默认Locale @Override public LocaleContext resolveLocaleContext(final HttpServletRequest request) { return new TimeZoneAwareLocaleContext() { @Override public Locale getLocale() { Locale locale = (Locale) WebUtils.getSessionAttribute(request, localeAttributeName); if (locale == null) { locale = determineDefaultLocale(request); } return locale; } @Override public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,@Nullable LocaleContext localeContext) { Locale locale = null; TimeZone timeZone = null; if (localeContext != null) { locale = localeContext.getLocale(); if (localeContext instanceof TimeZoneAwareLocaleContext) { timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); } } WebUtils.setSessionAttribute(request, this.localeAttributeName, locale); WebUtils.setSessionAttribute(request, this.timeZoneAttributeName, timeZone); } }
-
cookie获取
CookieLocaleResolver
:public class CookieLocaleResolver extends CookieGenerator implements LocaleContextResolver { // 根据COOKIE的.LOCALE获取Locale信息 @Override public LocaleContext resolveLocaleContext(final HttpServletRequest request) { parseLocaleCookieIfNecessary(request); return new TimeZoneAwareLocaleContext() { @Override @Nullable public Locale getLocale() { return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); } @Override @Nullable public TimeZone getTimeZone() { return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME); } }; } @Override public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,@Nullable LocaleContext localeContext) { Assert.notNull(response, "HttpServletResponse is required for" + "CookieLocaleResolver"); Locale locale = null; TimeZone timeZone = null; if (localeContext != null) { locale = localeContext.getLocale(); if (localeContext instanceof TimeZoneAwareLocaleContext) { timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); } addCookie(response, (locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? '/' + timeZone.getID() : "")); } else { removeCookie(response); } request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, (locale != null ? locale : determineDefaultLocale(request))); request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, (timeZone != null ? timeZone : determineDefaultTimeZone(request))); } }
一般正常使用传入Accept-Language
请求头即可
5.2 全局异常处理
如果在Controller请求中触发了异常,未处理的话,默认会提示500,服务器内部错误。但是有些异常又不得不去处理,如果在业务代码抛异常处处理,那就会产生很多冗余代码。因为大多数异常触发的原因是相似的。幸好Spring也提供了通用机制去处理。
5.2.1 @ControllerAdvice注解
@ControllerAdvice
注解的类可以在多个Controller类(@Controller
注解标注的类)之间共享由@ExceptionHandler
、 @InitBinder
、或者@ModelAttribute
注解的代码,且该类必须由@Component
注解。简而言之,一个被Spring托管的bean(@Component),如果加了@ControllerAdvice注解,则其内部的方法如果被@ExceptionHandler
、 @InitBinder
、或者@ModelAttribute
注解,则所有的Controller只要满足特定条件,都可以走到这个类中的方法。
5.2.2 @ExceptionHandler注解
@ExceptionHandler
注解作用是Controller类中抛出异常,会在此处进行处理,可以用于类或者方法上。@ExceptionHandler
注解的方法的参数可以是以下任意数量的参数:
- 异常参数:@ExceptionHandler中的
value()
指定得异常类; - request:
ServletRequest
或者HttpServletRequest
等; - response:
ServletResponse
或者HttpServletResponse
等; - session:
HttpSession
等,如果加了此参数将强制存在相应的会话。因此,这个参数永远不会为空。但是,会话访问可能不是线程安全的,尤其是在 Servlet 环境中:如果允许多个请求同时访问一个会话,可以将"synceOnSession"标志切换为"true"。 - WebRequest或者NativeWebRequest:允许通用请求参数访问以及请求/会话属性访问,而无需绑定到 Servlet API。
- Locale:本地化信息
- InputStream或者Reader: Servlet API 暴露的原始 InputStream/Reader。
- OutputStream或者Writer:Servlet API 暴露的原始 OutputStream/Writer。
- Model:模型作为从处理程序方法返回模型映射的替代方法。注意,提供的模型未使用常规模型属性预先填充,因此始终为空,以便为特定于异常的视图准备模型。
@ExceptionHandler
注解的方法的返回可以是:
- ModelAndView对象;
- Model对象:其视图名称通过 org.springframework.web.servlet.RequestToViewNameTranslator 隐式确定
- Map对象:暴露的视图,其视图名称通过 org.springframework.web.servlet.RequestToViewNameTranslator 隐式确定
- View对象;
- String:视图名称
- @ResponseBody
- HttpEntity<?> 或者 ResponseEntity<?> 对象:对象(仅限 Servlet)来设置响应标头和内容。响应实体正文将使用消息转换器进行转换并写入响应流
- void:参数中加response,可以手动将响应设置进response。
5.2.3 全局异常处理使用
@Component
@ControllerAdvice
public class GlobalExceptionHandler {
@Resource
private MessageProcessor messageProcessor;
/**
* 处理其他未被捕获的异常,返回服务器内部错误,响应500
*
* @param request http请求
* @param exception 参数绑定异常
* @return 异常响应国际化信息
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public String handleException(HttpServletRequest request, Exception exception) {
return messageProcessor.getMessage(request, INTERNAL_ERROR_DEFAULT_MESSAGE);
}
}
测试
@RequestMapping(value = "/user/{user_id}", method = RequestMethod.DELETE, consumes =
MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@ResponseStatus(code = HttpStatus.OK)
public String deleteUser(HttpServletRequest request, HttpServletResponse response,
@PathVariable(value = "user_id") String userId) {
String userName = userService.getUserName(userId);
if (userName == null) {
throw new NullPointerException();
// return messageProcessor.getMessage(request, "response.400.message.0002",
// "userId");
}
// delete user
return "success";
}
发现竟然乱码了,下面分析下原因
5.2.4 响应体返回中文乱码
分析下原因,是因为触发了全局异常捕获,handleException()方法上标注了@ResponseBody,即返回的message会被放入响应体中返回给前端。和Controller方法不同的是,使用@RequestMapping方法指定了consumes=MediaType.APPLICATION_JSON_VALUE
,即响应体会被json处理,但是@ExceptionHandler
方法未指定响应体格式Content-Type
,可以在postman中查看:
由SpringMVC处理流程可知
6、返回ModelAndView之后仍然是交由HandleAdapter去处理,所以重点分析下Adapter。这里的Adapter实现类为RequestMappingHandlerAdapter
,入口为handleInternal
方法,调用invokeHandlerMethod()
- handleInternal()
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
mav = invokeHandlerMethod(request, response, handlerMethod);
return mav;
}
- invokeHandlerMethod()
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// 创建ServletWebRequest对象
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
// 设置方法参数解析器
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
// 设置方法返回值处理器
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
// ModelAndView容器,将上述参数设置进去并初始化相关配置
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
// 异步请求处理AsyncWebRequest,不涉及已忽略
// 调用此方法并处理返回值
invocableMethod.invokeAndHandle(webRequest, mavContainer);
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
- invokeAndHandle()
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 调用方法获取方法返回值,如果发生异常,此时获取的是异常处理的方法的返回值
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 根据@ResponseStatus注解设置响应码
setResponseStatus(webRequest);
// 返回值为null时处理
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
// 如果在@ResponseStatus注解设置reason(),则进去此处
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
// returnValueHandlers处理返回值
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
- handleReturnValue()
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 在returnHandler列表中根据supportsReturnType()方法,获取第一个支持的Handler
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
// Handler中有messageConverters列表,根据messageConverter的canWrite()方法选择合适的messageConvert,并将message写入到response中。
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
由上分析可知,最终是根据messageConvert将返回值写入到response中。由于返回值是String,而messageConvert所以会使用StringHttpMessageConvert:
而StringHttpMessageConvert的Content-Type
默认是text/plain;charset=UTF-8
,编码方式是ISO-8859-1,所以产生中文乱码
-
解决方法一:在配置文件中指定StringHttpMessageConverter的字符集,推荐此方案。
<!--开启SpringMVC注解驱动--> <!--指定validator--> <mvc:annotation-driven validator="validator"> <!--指定StringHttpMessageConverter的字符集--> <mvc:message-converters> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/plain;charset=utf-8</value> <value>text/html;charset=UTF-8</value> </list> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>
-
解决方法二:在bean后处理器中指定StringHttpMessageConverter的字符集,所有的被spring托管的bean都会执行postProcessAfterInitialization方法,建议使用解决方法一。
@Component public class DefineCharSet implements BeanPostProcessor { //实例化之前调用 @Nullable public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } //实例化之后调用 @Nullable public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // 指定StringHttpMessageConverter的字符集 if(bean instanceof StringHttpMessageConverter){ MediaType mediaType = new MediaType("text", "html", Charset.forName("UTF-8")); List<MediaType> types = new ArrayList<MediaType>(); types.add(mediaType); ((StringHttpMessageConverter) bean).setSupportedMediaTypes(types); } return bean; } }
测试:
5.3 Spring国际化整合Hibernate参数校验
5.3.1 LocalValidatorFactoryBean配置validationMessageSource
<!--Hibernate Validator参数校验-->
<!--开启Spring的校验功能,使用Hibernate Validator校验-->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<!--参数校验配置为Hibernate Validator-->
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<!--参数校验错误信息国际化-->
<property name="validationMessageSource" ref="messageSource"/>
</bean>
5.3.2 三种常见参数校验异常
5.3.2.1 BindException
BindException,仅对于表单提交的请求体body校验有效(@Validated 或@Valid注解),校验失败会抛出此类异常,对于以json格式提交将会失效。
5.3.2.2 MethodArgumentNotValidException
MethodArgumentNotValidException是BindException的子类,提交的请求体body为json格式时有效,校验失败是会抛出身份校验异常。
5.3.2.3 ConstraintViolationException
Spring的校验能力(@Validated加MethodValidationPostProcessor,参考4.4 Spring的校验机制以及@Validated注解的使用)会抛出此种异常。对请求参数(@RequestParam)和路径参数(@PathVariable)有效。
5.3.3 国际化错误信息
前面章节4.3.2 hibernate-validator常用注解中参数校验注解message信息都由一个默认值,如@NotNull注解(validation-api包的注解)所示:
如果在校验注解上重新指定message,则可以把默认的message这些值国际化,
- messages_zh_CN.properties
response.400.message.0001=请求参数不合法。
response.400.message.0002=请求参数 {0} 不合法。
response.404.message.0001=资源不存在。
response.500.message.0001=系统错误,请稍后重试。
# 参数校验默认国际化key值
javax.validation.constraints.NotNull.message=请求参数 %s 不能为null。
javax.validation.constraints.NotBlank.message=请求参数 %s 不能为空。
javax.validation.constraints.Min.message=请求参数 %s 不能小于{value}。
javax.validation.constraints.Max.message=请求参数 %s 不能大于{value}。
javax.validation.constraints.Size.message=请求参数 %s 长度(或数量)必须在{min}和{max}之间。
javax.validation.constraints.Pattern.message=请求参数 %s 不满足正则规则{regexp}。
org.hibernate.validator.constraints.Length.message=请求参数 %s 长度必须在{min}和{max}之间。
org.hibernate.validator.constraints.Range.message=请求参数 %s 必须在{min}和{max}之间。
- message_en_US.properties
# 英文
response.400.message.0001=Request parameters are invalid.
response.400.message.0002=Request parameters {0} are invalid.
response.404.message.0001=Request resource can not be found.
response.500.message.0001=System error, please try again later.
# 参数校验默认国际化key值
javax.validation.constraints.NotNull.message=The request parameter %s cannot be null.
javax.validation.constraints.NotBlank.message=The request parameter %s cannot be blank.
javax.validation.constraints.Min.message=Request parameter %s cannot be less than {value}.
javax.validation.constraints.Max.message=Request parameter %s cannot be greater than {value}.
javax.validation.constraints.Size.message=The request parameter %s length (or quantity) must be between {min} and {max}.
javax.validation.constraints.Pattern.message=Request parameter %s does not satisfy regular rule {regexp}.
org.hibernate.validator.constraints.Length.message=The request parameter %s length must be between {min} and {max}.
org.hibernate.validator.constraints.Range.message=The request parameter %s must be between {min} and {max}
这里{value}
、{min}
和{max}
在国际化的时候会将注解内的值替换,比如@Min和@Max的value()
在国际化是会替换javax.validation.constraints.Min.message=请求参数 %s 不能小于{value}。
中的{value}
。这里也一并将参数的名称放入国际化消息中,可以使用%s,在返回错误信息之前使用String.format()
方法将参数名放入国际化后的消息。
5.3.4 全局异常处理
package org.numb.common.handler;
import static org.numb.common.i18n.ErrorCode.INTERNAL_ERROR_DEFAULT_MESSAGE;
import static org.numb.common.i18n.ErrorCode.INVALID_PARAMETERS_MESSAGE;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import org.apache.commons.lang3.StringUtils;
import org.numb.common.i18n.MessageProcessor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@Component
@ControllerAdvice
public class GlobalExceptionHandler {
@Resource
private MessageProcessor messageProcessor;
/**
* BindException和MethodArgumentNotValidException异常,响应码为400
*
* @param request http请求
* @param exception 参数绑定异常
* @return 异常响应国际化信息
*/
@ExceptionHandler(value = {BindException.class, MethodArgumentNotValidException.class})
@ResponseBody
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public String handleBindException(HttpServletRequest request, BindException exception) {
String message = "";
String fieldName = "";
// 从exception中获取参数名称fieldName
if (exception.getBindingResult().getFieldError() != null) {
String field = exception.getBindingResult().getFieldError().getField();
if (StringUtils.isNotBlank(field)) {
fieldName = field;
}
}
if (exception.getBindingResult().getAllErrors().size() > 0) {
message = exception.getBindingResult().getAllErrors().get(0).getDefaultMessage();
}
// 如果国际化消息为空,使用默认的国际化消息
if (StringUtils.isBlank(message)) {
message = messageProcessor.getMessage(request, INVALID_PARAMETERS_MESSAGE);
}
return String.format(message, fieldName);
}
/**
* 处理ConstraintViolationException异常,响应400
*
* @param request http请求
* @param exception 参数绑定异常
* @return 异常响应国际化信息
*/
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public String handleConstraintViolationException(HttpServletRequest request, ConstraintViolationException exception) {
return exception.getMessage();
}
/**
* 处理其他未被捕获的异常,返回服务器内部错误,响应500
*
* @param request http请求
* @param exception 参数绑定异常
* @return 异常响应国际化信息
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public String handleException(HttpServletRequest request, Exception exception) {
return messageProcessor.getMessage(request, INTERNAL_ERROR_DEFAULT_MESSAGE);
}
}
测试