Spring MVC(3)Spring MVC 高级应用
一、Spring MVC 的数据转换和格式化
前面的应用,都只是用HandlerAdapter去执行处理器。
处理器和控制器不是一个概念,处理器是在控制器功能的基础上加上了一层包装,有了这层包装,在HTTP请求达到控制器之前它就能够对HTTP的各类消息进行处理。
首先当一个请求到达 DispatcherServlet 的时候,需要找到对应的HandlerMapping,然后根据 HandlerMapping 去找到对应的 HandlerAdapter 执行处理器。处理器在要调用的控制器之前,需要先获取 HTTP (即jsp文件对应的网页)发送过来的信息,然后将其转变为控制器的各种不同类型的参数,这就是各类注解能够得到丰富类型参数的原因。
它首先用 HTTP 的消息转换器(HttpMessageConverter)对消息转换,但是这是一个比较原始的过程,它是String 类型和文件类型比较简易的转换,还需要进一步转换为 POJO 或者其他丰富的参数类型。为了拥有这样的能力,Spring 4 提供了转换器和格式化器,这样通过注解的信息和参数的类型,它就能把 HTTP 发送过来的各种消息转换为控制器所需要的各类参数了。
当处理器处理完了这些参数的转换,它就会进行验证,验证表单的方法已经提过。完成了这些内容,下一步就是调用开发者所提供的控制器了,将之前转换成功的参数传递进去,这样我们开发的控制器就能够得到丰富的 Java 类型的支持了,进而完成控制器的逻辑,控制器完成了对应的逻辑,返回结果后,处理器如果可以找到对应处理结果类型的 HttpMessageConverter 的实现类,它就会调用对应的 HttpMessageConverter 的实现类方法对控制器返回的结果进行 HTTP 转换,这一步不是必须的,可以转换的前提是能够找到对应的转换器,做完这些处理器的功能就完成了。
接下来就是关于视图解析和视图解析器的流程了,整个过程是比较复杂的,有时候要自定义一些特殊的转换规则,比如如果需要和第三方合作,合作方可能提供的不是一个良好的JSON 格式,而是一些特殊的规则,这个时候就可能需要使用自定义的消息转换规则,把消息类型转换为对应的 Java 类型,从而简化开发。
对于 Spirng MVC ,在XML上配置了<mvc:annotation-driven>,或者 Java 配置的注解上加入@EnableWebMvc 的时候,Spring IoC 容器会自定义生成一个关于转换器和格式化器的类实例----------FormattingConversionServiceFactoryBean,这样就可以从 Spring IoC容器中获取这个对象了。FormattingConversionServiceFactoryBean可以生成DefaultFormattingConversionService类对象,类对象继承了一些类,并实现了许多接口。
其中,顶层接口是ConversionService接口,它还实现了转换器的注册机(ConverterRegistry)和格式化器注册机(FormatterRegistry)两个接口。
而事实上 Spring MVC 已经注册了一些常用的转换器和格式化器。
在运行控制器之前,处理器就会使用这些转换器把 HTTP 的数据转换为对应的类型,用以填充控制器的参数,这就是为什么可以在控制器保持一定的规则下就能得到参数的原因。
当控制器返回数据模型后,再通过 Spirng MVC 后面对应的流程渲染数据,然后显示给客户端。
在Java类型转换之前,在 Spring MVC 中,为了应对 HTTP 请求,它还定义了HttpMessageConverter,它是一个总体的接口,通过它可以读入HTTP的请求内容,即在读取 HTTP 请求的参数和内容的时候会先用 HttpMessageConverter 读出,做一次简单转换为 Java 类型,主要是字符串,然后就可以使用各类转换器进行转换了,在逻辑业务处理完成后,还可以通过它把数据转换为相应给用户的内容。
对于转换器而言,在 Spring 中有两大类,一种是由 Converter 接口所定义的,另外一种是 GenericConverter,它们都可以使用注册机注册。但是,它们都是来自于 Spring Core 项目,它的作用范围是 Java 内部各种类型之间的转换。
1.HttpMessageConverter 和 JSON 消息转换器
HttpMessageConverter 是定义从 HTTP 接收请求信息和应答给用户的,用的比较多的实现类是 MappingJackson2HttpMessageConverter,这是一个关于 JSON 消息的转换类,通过它能够把控制器返回的结果在处理器内转换为 JSON 数据。
(1)注册MappingJackson2HttpMessageConverter
jsonConverter中的application/json;charset=UTF-8表示 HTTP 相应请求的类型是一个 JSON 类型,当在控制器中使用 @ResponseBody 的时候,Spring MVC 便会将应答请求转变为关于 JSON 的类型。这样的一次转换,就意味着处理器会在控制器返回结果后,遍历其项目中定义的各类HttpMessageConverter 实现类,由于 MappingJackson2HttpMessageConverter 定义为支持 JSON 数据的转换,它和@ResponseBody 所定义的相应类型一样,因此 Spirng MVC 就知道采用 MappingJackson2HttpMessageConverter 将控制器返回的结果进行处理,这样就转换为了 JSON 数据集了。
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="messageConverters"> <list> <ref bean="jsonConverter" /> </list> </property> </bean> <bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>application/json;charset=UTF-8</value> </list> </property> </bean>
(2)通过 @ResponseBody 注解可以使用上面配置的消息转换器
当通过@ResponseBody注解后,Spring MVC 就会将控制器返回的结果通过类型判断找到 MappingJackson2HttpMessageConverter 实例,进而在处理器内部转变成 JSON 类型。
@RequestMapping(value = "/getRole") // 注解,使得Spring MVC把结果转化为JSON类型响应,进而找到转换器 @ResponseBody public Role getRole(Long id) { Role role = roleService.getRole(id); return role; }
(3)测试(复习:这里getRole(Long id)是通过默认的参数一致性进行参数传递的,不需要加注解)
日志:
第一行,返回 JDBC 连接后,然后,第二行是关键:
通过之前定义过的处理器规则:将[com.ssm.chapter16.pojo.Role@39c434fa]转换成"application/json"形式,通过[MappingJackson2HttpMessageConverter@1ec15d1b] 实例
org.springframework.jdbc.datasource.DataSourceUtils: Returning JDBC Connection to DataSource org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor:
Written [com.ssm.chapter16.pojo.Role@39c434fa] as "application/json" using
[org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@1ec15d1b] org.springframework.web.servlet.DispatcherServlet: Null ModelAndView returned to DispatcherServlet with name 'dispatcher': assuming HandlerAdapter completed request handling org.springframework.web.servlet.FrameworkServlet: Successfully completed request org.springframework.beans.factory.support.AbstractBeanFactory: Returning cached instance of singleton bean 'SqlSessionFactory'
HttpMessageConverter 的主要作用在于 Java 和 HTTP 之间的消息转换。
2.一对一转换器(Converter)
Converter是一种一对一的转换器,接口定义如下,其中S代表源类型,而T代表目标类型
public interface Converter<S, T> { T convert(S source); }
Spring Core 项目已经实现了不少的转换器:
CharacterToNumber :将字符转换为数字
IntegerToEnum:将整数转换为枚举类型
ObjectToStringConverter:将对象转换为字符串
SerializingConverter:序列化转换器
DeserializingConverter:反序列化转换器
StiringToBooleanConverter:将字符串转换为布尔值
StringToCurrencyConverter:将字符串转换为金额
EnumToStringConverter:将枚举转换为字符串
前面提高过的,通过HttpMessageConverter 可以把 HTTP 的消息读出后,Spring MVC 就开始使用这些转换器来将 HTTP 的信息,转换为控制器的参数,这就是能在控制器上获得各类参数的原因。大部分情况下,Spring MVC 所提供的功能,能够满足一般的需求,但是又是受需要进行自定义转换规则,这时候,只要实现接口Converter,然后注册给对应的转换服务类就可以了。
假设需要实现:将角色对象按照{id}-{role_name}-{note}进行传递:
(1)定义一个关于字符串和角色的转换类,这个类实现了Converter接口,实现了其convert方法,这个类的作用是,将字符串类型的id-role_name-note转换为Role类型的实例role
package com.ssm.chapter16.converter; importpublic class StringToRoleConverter implements Converter<String, Role> { @Override public Role convert(String str) { //空串 if (StringUtils.isEmpty(str)) { return null; } //不包含指定字符 if (str.indexOf("-") == -1) { return null; } String[] arr = str.split("-"); //字符串长度不对 if (arr.length != 3) { return null; } Role role = new Role(); role.setId(Long.parseLong(arr[0])); role.setRoleName(arr[1]); role.setNote(arr[2]); return role; } }
(2)在之前提到过,在配置 Spring MVC 时,如果使用这届@EnableWebMvc 或者在XML配置文件中使用<mvc:annotation-driven/>,那么系统就会自动初始化FormattingConversionServiceFactoryBean实例,通过它可以生成一个ConversionService接口对象,实际为DefaultFormattingConversionService对象,通过这个对象就可以注册一个新的转换器了。
在Webconfig.java的配置中,首先定义一个转换器列表,然后将自定义的StringToRoleConverter类的实例添加到转换器列表中,最后通过FormattingConversionServiceFactoryBean类型的实例将自定义的转换器StringToRoleConverter注册成一个新的转换器。
这里要回顾一下上面的注册MappingJackson2HttpMessageConverter的流程,和这个是一样的。
@Service public class WebConfig { //自定义转换器列表 private List<Converter> myConverter = null; //依赖注入FormattingConversionServiceFactoryBean对象,它是一个自动初始化的对象 @Autowired private FormattingConversionServiceFactoryBean fcsfb = null; @Bean(name = "myConverter") public List<Converter> initMyConverter() { if (myConverter == null) { myConverter = new ArrayList<Converter>(); } //自定义的字符串和角色转换器 Converter roleConverter = new StringToRoleConverter(); myConverter.add(roleConverter); //往转换服务类注册转换器 fcsfb.getObject().addConverter(roleConverter); return myConverter; }
其实我更偏向于使用XML配置,因为比较简单直观:
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven conversion-service="conversionService"/> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.*" /> <!-- 定义视图解析器 --> <!-- 找到Web工程/WEB-INF/JSP文件夹,且文件结尾为jsp的文件作为映射 --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" /> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="com.ssm.chapter16.converter.StringToRoleConverter"/> </list> </property> </bean> </beans>
(3)增加自定义控制器方法
这里必须要提的是:@RequestParam("role")Role role中必须使用@RequestParam("role")注解声明参数的类型,否则会转换失败。(困扰了我很久的问题)
@RequestMapping(value = "/updateRole") @ResponseBody public Map<String, Object> updateRole(@RequestParam("role")Role role) { Map <String, Object> result = new HashMap<String, Object>(); //更新角色 boolean updateFlag = (roleService.updateRole(role) == 1); result.put("success", updateFlag); if (updateFlag) { result.put("msg", "更新成功"); } else { result.put("msg", "更新失败"); } return result; }
(4)进行测试
数据库结果为:
mysql> select * from t_role; +----+--------------------+---------------+ | id | role_name | note | +----+--------------------+---------------+ | 1 | update_role_name_1 | update_note_1 | | 2 | 2 | 2 | | 3 | role_name_3 | note_3 | | 4 | role_name_4 | note_4 | | 5 | role_name_5 | note_5 | +----+--------------------+---------------+ 5 rows in set (0.00 sec)
3.数组和集合转换器 GenericConverter
上述的转换器是一种一对一的转换,它存在一个弊端:只能从一种类型转换为另一种类型,不能进行一对多转换,比如把String 转换成 List<String> 或者 String[],甚至是List<Role>,一对一转换器都无法满足,为了克服这个问题,Spring Core 项目还加入了另外一个转换器结构 GenericConverter,它能够满足数组和集合转换的要求。
GenericConverter 接口能够满足数组和集合转换的要求,ConditionalGenericConverter 接口继承了 GenericConverter 接口,其实现类例如 StringToArrayConverter 可以将数组或者集合类型的源格式转换成 Java 对象格式,StringToArrayConverter 需要接收一个ConversionService 类型的参数,其包含三个方法:getConvertibleTypes 方法表示可接受的类型,matches 方法查找是否存在 Converter 支持原类型和目标类型的转换,而convert 方法用于完成对字符串和数字的转换。
仅在配置文件中,新注册StringToArrayConverter即可,
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven conversion-service="conversionService"/> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.*" /> <!-- 定义视图解析器 --> <!-- 找到Web工程/WEB-INF/JSP文件夹,且文件结尾为jsp的文件作为映射 --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" /> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="com.ssm.chapter16.converter.StringToRoleConverter"/> </list> </property> </bean> <bean id="stringToArrayConverter" class="org.springframework.core.convert.support.StringToArrayConverter"> </bean> </beans>
然后编写对应的控制器:
@RequestMapping(value = "/updateRoleList") @ResponseBody public Map<String, Object> updateRoleList(List<Role> roleList) { Map <String, Object> result = new HashMap<String, Object>(); //更新角色列表 boolean updateFlag = (roleService.updateRoleArr(roleList) > 1); result.put("success", updateFlag); if (updateFlag) { result.put("msg", "更新成功"); } else { result.put("msg", "更新失败"); } return result; }
对应的Service方法:
@Override public int updateRoleArr(List<Role> roleArr) { int count = 0; for (Role role : roleArr) { count += this.updateRole(role); } return count; }
进行测试:
数据库中:
mysql> select * from t_role; +----+-------------+--------+ | id | role_name | note | +----+-------------+--------+ | 1 | 2 | 3 | | 2 | 3 | 4 | | 3 | role_name_3 | note_3 | | 4 | role_name_4 | note_4 | | 5 | role_name_5 | note_5 | +----+-------------+--------+ 5 rows in set (0.00 sec)
可以看到,成功地更新了数据库。
由于在conversionService里定义了自已定义的StringToRoleConverter转换器,然后又定义了StringToArrayConverter转换器。
根据 StringToArrayConverter 的源码,可以看到流程大概是:
输入为roleList=1-2-3,2-3-4,由于roleList是一个List<role>类型的对象,因此:在 StringToArrayConverter 中的convert方法中
首先将String根据逗号分隔成String[],即{1-2-3,2-3-4},然后对于数组中的每个元素,根据源类型和目的类型,确定使用之前定义过的 conversionService 中的 StringToRoleConverter转换器,将1-2-3和2-3-4分别转换成两个role对象。
4.格式化器(Formmater)
有些数据需要格式化,必须金额、日期等。日期格式可以为 yyyy-MM-dd 或者 yyyy-MM-dd hh:ss:mm。金额格式可以为:1万元表示成¥10 000.00。
为了实现格式化转换,Spring Context 提供了Formatter接口,其扩展的两个接口分别为 Printer 和 Parser。通过Printer接口的print方法能将结果按照一定的格式输出字符串,通过Parser接口的parse方法,可以将字符串转换为对象。
定义含有“date1”和“amount1”两个参数的提交表单:
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>date</title> </head> <body> <form id="form" action="./convert/formatPojo.do"> <table> <tr> <td>日期</td> <td><input id="date " name="date1" type="text" value="2017-06-01" /></td> </tr> <tr> <td>日期</td> <td><input id="amount " name="amount1" type="text" value="123,000.00" /></td> </tr> <tr> <td></td> <td align="right"><input id="commit" type="submit" value="提交" /></td> </tr> </table> </form> </body> </html>
(1)第一种方式:控制器直接转换
通过@DateTimeFormat(iso = ISO.DATE)和@NumberFormat(pattern = "#,###.##")注解,控制器就能够拿到HTTP传递的参数了。
@RequestMapping("/format") public ModelAndView format( //日期格式化 @RequestParam("date1") @DateTimeFormat(iso = ISO.DATE) Date date, //金额格式化 @RequestParam("amount1") @NumberFormat(pattern = "#,###.##") Double amount) { ModelAndView mv = new ModelAndView("index"); mv.addObject("date", date); mv.addObject("amount", amount); return mv; }
(2)第二种方式:将两个参数定义一个POJO
package com.ssm.chapter16.pojo; import java.math.BigDecimal; import java.util.Date; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.NumberFormat; public class FormatPojo { @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private Date date1; @NumberFormat(pattern = "##,###.00") private BigDecimal amount1; /getter and setter/ }
然后将POJO参数传递进控制器:
@RequestMapping("/formatPojo") public ModelAndView formatPojo(FormatPojo pojo) { ModelAndView mv = new ModelAndView("index"); mv.addObject("date", pojo.getDate1()); mv.addObject("amount", pojo.getAmount1()); return mv; }
二、为控制器添加通知
与Spirng AOP 一样,Spring MVC 也能够给控制器加入通知,主要涉及四个注解:
- @ControllerAdvice,主要作用于类,用以标识全局性的控制器的拦截器,将应用于对应的控制器
- @InitBinder,是一个允许构建POJO参数的方法,允许在构造控制器参数的时候,加入一定的自定义控制
- @ExceptionHandler,通过它可以注册一个控制器异常,使用当控制器发生注册异常时,就会跳转到该方法上
- @ModelAttribute,是一种针对于数据模型的注解,它先于控制器方法运行,当标注方法返回对象时,它会保存到数据模型中。
(1)定义一个控制器通知的实例
@ControllerAdvice已经包含了@Component标注,因此Spring MVC在扫描的时候就会将其放置到Spring IoC容器中,而它的属性basePackages表示指定的拦截的控制器
package com.ssm.chapter16.controller.advice; import//标识控制器通知,并且指定对应的包 @ControllerAdvice(basePackages={"com.ssm.chapter16.controller.advice"}) public class CommonControllerAdvice { //定义HTTP对应参数处理规则 @InitBinder public void initBinder(WebDataBinder binder) { //针对日期类型的格式化,其中CustomDateEditor是客户自定义编辑器 //它的boolean参数表示是否允许为空 binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false)); } //处理数据模型,如果返回对象,则该对象会保存在 @ModelAttribute public void populateModel(Model model) { model.addAttribute("projectName", "chapter16"); } //异常处理,使得被拦截的控制器方法发生异常时,都能用相同的视图响应 @ExceptionHandler(Exception.class) public String exception() { return "exception"; } }
(2)测试控制器通知
由于这个控制器在前面定义的扫描包basePackages={"com.ssm.chapter16.controller.advice"}下,因此会被通知拦截。
在testAdvice方法中,并没有加入对日期的格式化,这是因为在通知initBinder方法里已经声明了日期的格式为"yyyy-MM-dd",因此无需重复。
而对于参数model来说,由于在进入控制器方法之前已经执行了通知方法,所以model.addAttribute("projectName", "chapter16");已经将一个键值对添加进了数据模型model中。
而exception方法则是为了发生异常的时候显示成友好的提示界面,即出现异常时,就返回exception.jsp视图给用户。
package com.ssm.chapter16.controller.advice; @Controller @RequestMapping("/advice") public class MyAdviceController { /*** * * @param date 日期,在@initBinder 绑定的方法有注册格式 * @param model 数据模型,@ModelAttribute方法会先于请求方法运行 * @return map */ @RequestMapping("/test") @ResponseBody public Map<String, Object> testAdvice(Date date, @NumberFormat(pattern = "##,###.00") BigDecimal amount, Model model) { Map<String, Object> map = new HashMap<String, Object>(); //由于@ModelAttribute注解的通知会在控制器方法前运行,所以这样也会取到数据 map.put("project_name", model.asMap().get("projectName")); map.put("date", DateUtils.format(date, "yyyy-MM-dd")); map.put("amount", amount); return map; } }
(3)测试结果
当不按照格式进行传递时,就会跳转到异常视图:
(4)事实上,@ControllerAdvice注解中使用@InitBinder、@ModelAttribute和@ExceptionHandler是对其参数basePackages指定的所有包下的通知其都有效;
同样的,@Controller注解的类中也可以使用@InitBinder、@ModelAttribute和@ExceptionHandler,只不过是只对于当前控制器有效。
这个控制器并不在上述的basePackages包中,因此,不会被公共通知拦截。它内部的注解@ModelAttribute只是针对当前控制器本身,同时,由于通知方法会在控制器方法执行前完成,因此,在控制器方法中就可以使用getRoleFromModelAttribute方法中的@ModelAttribute("role")注解直接从数据模型中取出变量role。
package com.ssm.chapter16.controller; @Controller @RequestMapping("/role") public class RoleController { // 注册角色服务类 @Autowired private RoleService roleService = null;/** * 在进入控制器方法前运行,先从数据库中查询角色,然后以键role保存角色对象到数据模型 * @param id 角色编号 * @return 角色 */ @ModelAttribute("role") public Role initRole(@RequestParam(value="id", required = false) Long id) { //判断id是否为空 if (id == null || id < 1 ) { return null; } Role role = roleService.getRole(id); return role; } /** * @ModelAttribute注解从数据模型中取出数据, * @param role 从数据模型中取出的角色对象 * @return 角色对象 */ @RequestMapping(value="getRoleFromModelAttribute") @ResponseBody public Role getRoleFromModelAttribute(@ModelAttribute("role") Role role) { return role; } }
三、处理异常
Spring MVC 提供的@ExceptionHandler可以处理异常。在默认的情况下,Spring 会将自身产生的异常转换为合适的状态码,通过这些状态码可以进一步确定异常发生的原因。
除此之外,还可以使用@ResponseStatus注解自定义一些异常以及状态码,其中cole表示异常码,而reason表示产生异常的原因。
package com.ssm.chapter16.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; //新增Spring MVC的异常映射,code代表异常映射码,而reason则代表异常原因 @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "找不到角色信息异常!!") public class RoleException extends RuntimeException { private static final long serialVersionUID = 5040949196309781680L; }
在使用的时候,可以类似于Java的try...catch...finally...语句的处理方法,例如:
notFound方法中,首先查找角色信息,如果查找不到就抛出 RoleException 异常,而此时Spring MVC 就会去找到被 @ExceptionHandler 标注的方法,如果和配置的异常匹配,那么就进入该方法。由于只是在当前控制器上加入,因此这个异常处理方法仅对当前控制器有效,即回去寻找HandleRoleException方法,然后返回exception.jsp视图展示给用户。
@RequestMapping("notFound") @ResponseBody public Role notFound(Long id) { Role role = roleService.getRole(id); //找不到角色信息抛出RoleException if (role == null) { throw new RoleException(); } return role; } //当前控制器发生RoleException异常时,进入该方法 @ExceptionHandler(RoleException.class) public String HandleRoleException(RoleException e) { //返回指定的页面,避免不友好 return "exception"; }
四、国际化
对于外资企业来说,有可能需要国际化,而国际化一般分为语言、时区和国家信息。
1.LocaleResolver 接口
Spring MVC 实现国际化:DispatcherServlet 会解析一个 LocaleResolver 接口对象,通过它来决定用户区域(User Locale),读出对应用户系统设定的语言或者用户选择的语言,以确定国际化。但是,对于DispatcherServlet 而言,只能够注册一个 LocaleResolver 接口对象。LocaleResolver 接口的主要作用是实现解析国际化,此外还会解析内容的问题,尤其是时区。
LocaleResolver 接口有很多子接口:
- LocaleContextResolver:处理一些用户区域上的问题,包括语言和时区的问题。
- CookieLocalResolver:主要是使用浏览器的Cookie实现国家化的,而 Cookie 有时候需要服务器去写入到浏览器中,所以它会集成一个产生Cookie的类CookieGenerator
- AbstractLocaleContextResolver:是一个能够提供语言和时区的抽象类,而它的语言功能则继承了 AbstractLocaleResolver,而时区功能的实现则扩展了LocaleContextResolver接口。
- AcceptHeaderLocaleResolver:是Spring 默认的区域解析器,通过HTTP请求的accept-language头部来解析区域。这个头部是由用户的Web浏览器根据底层操作系统的区域设置的。
- FixedLocaleResolver:使用固定Locale国际化,不可修改Locale
- SessionLocaleResolver:根据Session进行国际化,也就是根据用户Session的变量读取区域位置,所以是可变的。
为了可以修改国际化,Spirng MVC 还提供了一个国际化的拦截器--LocaleChangeInterceptor,通过它可以获取参数,然后既可以通过 CookieLocalResolver 使用浏览器的 Cookie来实现国际化,也可以通过 SessionLocaleResolver 通过服务器的 Session 来实现国家化。而使用 Cookie 的问题是用户可以删除或者禁用 Cookie,所以它并非那么可靠;而使用 Session 虽然可靠,但是又存在过期的问题。
2.MessageSource接口
MessageSource 接口是 Spring MVC 为了加载消息所设置的接口,可以通过它来加载对应的国际化属性文件。
MessageSource 接口的两个实现类使用较多,分别是ResourceBundleMessageSource 和 ReloadableResourceBundleMessageSource
- ResourceBundleMessageSource:使用JDK提供的ResourceBundle,它只能把文件放置在对应到的类路径下,不具备热加载的功能。
- ReloadableResourceBundleMessageSource:可以把属性文件放置在任何地方,可以在系统不重启的情况下重新加载属性文件。
也就是说,实现国际化需要三个条件:LocaleResolver + MessageSource + LocaleChangeInterceptor
(1)ResourceBundleMessageSource 和 ReloadableResourceBundleMessageSource
创建ReloadableResourceBundleMessageSource 的实例:
Bean的名称必须为messageSource,不能修改。setDefaultEncoding定义了编码格式,setBasename定义了在classpath下查找前缀为msg的属性文件,可以在任意文件夹下。setCacheSeconds设置了1小时的探测时间,如果属性文件被修改过,就可以重新加载。
@Bean(name="messageSource") public MessageSource initMessageSource() { ReloadableResourceBundleMessageSource msgSrc = new ReloadableResourceBundleMessageSource(); msgSrc.setDefaultEncoding("UTF-8"); msgSrc.setBasename("classpath:msg"); //缓存3 600秒,相当于1小时,然后重新刷新 msgSrc.setCacheSeconds(3600); //缓存3 600×1 000毫秒,相当于1小时,然后重新刷新 //msgSrc.setCacheMillis(3600*1000); return msgSrc; }
创建ResourceBundleMessageSource的实例:
Bean的名称必须为messageSource,不能修改。setBasename方法,传递一个文件名的前缀,这里是msg,而只能在classPath路径下。
@Bean(name="messageSource") public MessageSource initMessageSource() { ResourceBundleMessageSource msgSrc = new ResourceBundleMessageSource(); msgSrc.setDefaultEncoding("UTF-8"); msgSrc.setBasename("msg"); return msgSrc; }
与之对应的,还可以使用XML 方式进行配置:
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="defaultEncoding" value="UTF-8"/> <property name="basenames" value="msg"/> </bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="defaultEncoding" value="UTF-8"/> <property name="basenames" value="classpath:msg"/> <property name="CacheSeconds" value="3600"/> </bean>
(2)CookieLocalResolver 和 SessionLocaleResolver
创建CookieLocalResolver实例:
setCookieName方法设置其cookieName属性,它是一个 cookie变量的名称,而setCookieMaxAge方法设置 cookie 超时时间,单位为秒。默认使用简体中文。
@Bean(name="localeResolver") public LocaleResolver initCookieLocaleResolver() { CookieLocaleResolver clr = new CookieLocaleResolver(); //cookie名称 clr.setCookieName("lang"); //cookie超时秒数 clr.setCookieMaxAge(1800); //默认使用简体中文 clr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return clr; }
创建SessionLocaleResolver实例:
对于Cookie而言,用户可以进行删除甚至禁用,使得其安全性难以得到保证,导致大量使用默认值。为了避免这个问题,一般用的多的是Session,具有更高的可靠性。
@Bean(name="localeResolver") public LocaleResolver initSessionLocaleResolver() { SessionLocaleResolver slr = new SessionLocaleResolver(); //默认使用简体中文 slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return slr; }
与之对应的,也可以使用 XML 方式对它们进行配置:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"> <property name="cookieName" value="lang"/> <property name="cookieMaxAge" value="20"/> <property name="defaultLocale" value="zh_CN"/> </bean> <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"> <property name="defaultLocale" value="zh_CN"/> </bean>
(3)LocaleChangeInterceptor
通过请求参数去改变国际化的值时,可以使用LocaleChangeInterceptor拦截器:
当请求到来时,首先拦截器会监控有没有language请求参数,如果有则获取它,然后通过系统配置的 LocaleResolver 实现国际化。在获取不到参数或者获取到的参数的国际化并非系统能够支持的主题时,就会采用默认的国际化,也就是 LocaleResolver 所调用的 setDefaultLocale 方法指定的国际化。
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/message/*.do" /> <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> <!--监控请求参数language --> <property name="paramName" value="language" /> </bean> </mvc:interceptor> </mvc:interceptors>
(4)与前面的 MessageSource 配置的 setBasename("classpath:msg"); 相对应,需要在classpath下新建两个国际化的属性文件,且名称和内容分别为:
- msg_en_US.properties:welcome=the project name is chapter16
- msg_zh_CN.properties:welcome=\u5DE5\u7A0B\u540D\u79F0\u4E3A\uFF1Achapter16
这里需要和LocaleResolver 配置的 <property name="defaultLocale" value="zh_CN"/>配置的国际化后缀名保持一致,而前缀在之前已经定义过了为msg
(5)然后,新建一个国际化控制器
package com.ssm.chapter16.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/message") public class MessageController { @RequestMapping("/msgpage") public String page(Model model) { return "msgpage"; } }
(6)与之对应的,需要有一个跳转jsp文件msgpage.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@taglib prefix="mvc" uri="http://www.springframework.org/tags/form"%> <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head> <title>Spring MVC国际化</title> </head> <body> <h2> <!-- 找到属性文件变量名为welcome的配置 --> <spring:message code="welcome" /> </h2> Locale: ${pageContext.response.locale } </body> </html>
(7)测试结果
默认属性文件: 添加language配置为en_US:
添加language配置为zh_CN: 添加language配置为不存在的属性文件: