预备知识
对于SpringMvc,在xml配置<mvc:annotation-drivern>或是@EnableWebMvc时,Spring IoC容器会自动生成一个关于转换器和格式化器的类实例——FormattingConversionServiceFactoryBean,它是一个工厂,通过它可以获得DefaultFormattingConversionService类对象,它实现了转换器与格式化器的注册机(即ConverterRegistry与FormatterRegistry),我们可以通过它注册转换器与格式化器,SpringMvc默认已经注册了一些转换器与处理器
HttpMessageConverter
这个接口进行HTTP参数与java类型的转换,是HTTP参数与String类型、文件类型的简易转换,通过转换器或是格式化器的进一步转换,才能变为丰富的参数类型或是POJO,该接口定义如下:
public interface HttpMessageConverter<T> {
//表示转换器是否能够将对应的Http类型转换为clazz类型,mediaType为Http类型
boolean canRead(Class<?> clazz, MediaType mediaType);
//表示转换器是否可以将clazz类型转换为H对应的ttp类型
boolean canWrite(Class<?> clazz, MediaType mediaType);
//返回转换器支持转换的Http类型
List<MediaType> getSupportedMediaTypes();
//从http请求消息中读取信息,将其转换clazz类型,HttpInputMessage是Http请求消息
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
//将T类型的对象写到响应流中,同时指定相应的媒体类型为 contentType,HttpOutputMessage是Http应答消息
void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
只有canRead函数返回True,才可以执行read函数,只有canWrite函数返回True,才可以执行write函数,流程如下图片来源:
HttpMessageConverter比较重要的函数源码如下:
public interface HttpInputMessage extends HttpMessage {
/**
* Return the body of the message as an input stream.
* @return the input stream body (never {@code null})
* @throws IOException in case of I/O Errors
*/
//获取body中的数据作为输入流
InputStream getBody() throws IOException;
}
public interface HttpOutputMessage extends HttpMessage {
/**
* Return the body of the message as an output stream.
* @return the output stream body (never {@code null})
* @throws IOException in case of I/O Errors
*/
//将数据转为输出流
OutputStream getBody() throws IOException;
}
InputStream与OutputStream是servlet提供的接口,这两个流操作是与http交互的,意味着HttpMessageConverter具有读取和写入http报文的能力,而HandleAdapter就是通过HttpMessageConveter、格式化器、转换器实现http请求参数与控制器形参的相互转换的,也就是适配(个人理解),HttpMessageConverter负责读取Http请求,将http请求简单转换为java类型,除此之外,还负责将数据写入到http应答中
HttpMessageConveter只负责简单转换(http类型与String、文件类型的转换),对于一些细节上的转化,以Spring Core包提供的Conveter和GenericConveter,以及Spring Context包的Formatter实现
Converter(一对一转换)
接口代码:
public interface Converter<S,T>{
T convert(S source);
}
SpringMvc实现了许多转换器,但是有时候我们需要自定义实现转换器,此时只要实现Converter接口,并进行注册即可(通过预备知识中讲的FormattingConversionServiceFactoryBean类,实际是注册到DefaultFormattingConversionService类中),下面通过一个例子进行讲解,代码来自《java EE 互联网轻量级框架整合开发》(不推荐新手看)
- 自定义转换器
package com.ssm.StringToRoleConverter public class StringToRoleConverter implements Converter<String,Role>{ @Override public Role convert(String str){ //空串 if(StringUtild.isEmpty(str)){ return null; } //不包含指定字符 if(str.indexOf("-")==-1){ return null; } String[] arr=str.split("-"); //字符串长度不对 if(arr.length!=3){ return null; } //POJO就不敲了,看set函数就知道有哪些属性了 Role role=new role(); role.setId(Long.parseLong(arr[0])); role.setRoleName(arr[1]); role.setNote(arr[2]); return role; } }
- 注册自定义转换器(java配置)
private List<Converter> myConverter=null; @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; }
- 相比于java配置,我更喜欢xml,因为更加简洁
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="com.ssm.StringToRoleConverter"/> </list> </property> </bean>
-
http请求样例:/update?role=1-update_role_name_1-update_note_1,运用上面的转换器,即可将请求参数转换为Role对象
其实还有一个数组和集合转换器GenericConverter,它在上面的一对一转换器Converter的基础上进一步将多个数据转换为数组和集合,Spring Core项目实现的GenericConverter基本够用,如果在以后某个项目需要自定义时在回来写例子
格式化器
使用场景:有时我们需要把字符串按照一定的格式转换为日期或者是金额,为了支持这些场景,Spring Context提供了相关的Formatter
Formatter接口扩展了两个接口Printer以及Parser,这两者定义如下:
interface Printer<T>{
String print(T object,Locale locale);
}
interface Parse<T>{
T parse(String text,Locale locale);
}
通过print方法能将结果按照一定的格式输出字符串,通过parse方法能够将满足一定格式的字符串转换为对象,它的内部实际是委托给Converter机制去实现的,需要自定义的场景不多,这里介绍两个注解
@DateTimeFormat:进行日期格式的转换,转换为java对象Date
@NumberFormat:进行数字的格式转换
@Controller
@RequestMapping("/convert")
public class ConvertController{
@RequestMapping("/format")
public ModelAndView format(@DateTimeFormat(ios=ISO.DATE)Date date,@NumberFormat(pattern="#,###.##")Double amount){
..........
}
}
请求样例:http://localhost:8080/convert?date=2017-06-01&amount=123,000.00
http请求参数名与形参名字对应后,进行转换器转换
控制器通知
与SpringAOP一样,SpringMVC也可以为控制器加入通知,它主要涉及4个注解:
- @ControllerAdvice:作用于类,用以标识全局性的控制的拦截器,将应用于对应的控制器
- @InitBinder:允许在构造控制器参数的时候,加入一定的自定义控制
- @ExceptionHandler:当控制器发生异常时,就会跳转到该方法上
- @ModelAttribute:先于控制器方法执行,当标注方法返回对象时,会保存到数据模型中,并传递给拦截的控制器
实例:
//对com.ssm.advice控制器进行拦截
@ControllerAdvice(basePackages={"com.ssm.advice"})
public class Demo{
//针对日期类型的格式化,与@DataTimeFormat作用一样
@InitBinder
public void initBinder(WebDataBinder binder){
binder.registerCustomEditor(Date.class,new CustomDateEditor(new SimpleDataFormat("yyyy-MM-dd"),false));
}
//model中添加的数据可以在被拦截控制器中使用
@ModelAttribute
public void populateModel(Model model){
model.addAttribute("projectName","1234");
}
//被拦截控制器出现Exception异常时,会调用该方法,该方法返回会返回exception视图
@ExceptionHandler(Exception.class)
public String exception(){
return "exception"
}
}
也可以在控制器当中使用@InitBinder、@ExceptionHandler、@ModelAttribute,此时标注的方法只对当前控制器有效,控制器形参可以通过@ModelAttribute("参数名")获得提前保存在数据模型中的数据,拿上面的例子来说,如果一个控制器方法想获得projectName参数的值:
@RequestMapping(value="getProjectName")
public String getProjectName(@ModelAttribute("projectName") String name){
.......
}
最终name的值将为1234
其他
至此,自己对于SpringMVC的工作流程有了一定了解,HttpMessageConverter、转换器、格式化器负责进行类型转换,现在还有一个疑问,SpringMvc是如何进行请求参数绑定的,这个疑问接下来会查阅资料后总结