从零开始学习springmvc(5)——Spring国际化和全局异常处理

【项目地址】 从零开始学习springmvc
如果觉得有用可以关注一下公众号:码字不易,求赞求关注
程序猿不加班

五、Spring国际化和全局异常处理

5.1 国际化介绍

国际化的意思指对于同一个信息,可以识别不同的用户,从而展现出匹配用户当地语言信息。比如中文"提交",对于不懂中文的英国人你要使用“post”来表达一样。

对于页面来说,可以根据用户输入的Accept-Language请求头来识别用户语言环境,从而加载已经提前准备好的语言资源包,展现出适配用户语言的环境。

5.1.1 ResourceBundle的介绍

ResourceBundlejava.util包下的工具类,主要用来解决国际化问题。当程序需要一个特定于语言环境的资源时(如 String),程序可以从适合当前用户语言环境的资源包(大多数情况下也就是.properties文件)中加载它。这样可以很大程度上独立于用户语言环境的程序代码,它将资源包中大部分(即便不是全部)特定于语言环境的信息隔离开来。

5.1.2 ResourceBundle的使用

  1. 新建语言资源包,为了适配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.
    

  2. 使用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"));
           
        }
    }
    
  3. 测试

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);
    }
}

测试

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值