第4章 处理请求数据
下面是可以在请求处理方法中出现的参数类型:
- javax.servlet.ServletRequest或javax.servlet.http.HttpServletRequest;
- javax.servlet.ServletResponse或javax.servlet.http.HttpServletResponse;
- javax.servlet.http.HttpSession;
- org.springframework.web.context.request.WebRequest或org.springframework.web.context.request.NativeWebRequest;
- java.util.Locale;
- java.io.InputStream或java.io.Reader;
- java.io.OutputStream或java.io.Writer;
- java.security.Principal;
- HttpEntity<?>paramters;
- org.springframework.ui.Model;
- org.springframework.ui.ModelMap;
- org.springframework.web.servlet.mvc.support.RedirectAttributes.;
- org.springframework.validation.Errors;
- org.springframework.validation.BindingResult;
- 命令或表单对象;
- org.springframework.web.bind.support.SessionStatus;
- org.springframework.web.util.UriComponentsBuilder;
- 带@PathVariable、@MatrixVariable、@RequestParam、@RequestHeader、@RequestBody或@RequestPart注解的对象;
4.1请求处理方法签名
1) Spring MVC 通过分析处理方法的签名,HTTP请求信息绑定到处理方法的相应形参中。
2) Spring MVC 对控制器处理方法签名的限制是很宽松的,几乎可以按喜欢的任何方式对方法进行签名。
3) 必要时可以对方法及方法入参标注相应的注解( @PathVariable 、@RequestParam、@RequestHeader 等)
4) Spring MVC 框架会将 HTTP 请求的信息绑定到相应的方法入参中,并根据方法的返回值类型做出相应的后续处理。
方法的签名:方法的名字+参数列表
入参:形参
可以直接接收request的参数,以同名的方式直接接收到方法的形参中。该接收方式适用于get和post提交请求方式。
4.2 请求参数@RequestParam注解
在处理方法入参处使用 @RequestParam 可以把请求参数传递给请求方法。如果请求参数名与形参名一致, 则可以省略@RequestParam的指定。但建议写。
@RequestParam ()括号内可以写:
- value:参数名,请求原来的参数名
- required:是否必须。默认为 true,表示请求参数中必须包含对应的参数,若不存在,将抛出异常。@RequestParam 注解标注的形参必须要赋值。 必须要能从请求对象中获取到对应的请求参数。可以使用required来设置为不是必须的。 不必须的适合必须有默认值
- defaultValue:形参默认值,当没有传递参数时使用该值。使用该参数时,自动将required设为false。可以使用defaultValue来指定一个默认值取代null。用的不多
如果传入的是checkbox,java中写@RequestParam("name名") List<类型>
即可
4.2.1 实验代码
1) 增加控制器方法
/**
* @RequestParam 注解用于映射请求参数:
* value 用于映射请求参数名称
* required 用于设置请求参数是否必须的
* defaultValue 设置默认值,当没有传递参数时使用该值
*/
@RequestMapping(value="/testRequestParam")
public String testRequestParam(
@RequestParam(value="username") String username,
@RequestParam(value="age",required=false,defaultValue="0") int age){
System.out.println("testRequestParam - username="+username +",age="+age);
return "success";
}
2) 增加页面链接
<!--测试 请求参数 @RequestParam 注解使用 -->
<a href="springmvc/testRequestParam?username=atguigu&age=10">testRequestParam</a>
4.3 请求头@RequestHeader 注解
RequestParam是获取请求参数的,而RequestHeader 是获取请求头信息的。
1) 使用 @RequestHeader 绑定请求报头的属性值
2) 请求头包含了若干个属性,服务器可据此获知客户端的信息,通过 @RequestHeader 即可将请求头中的属性值绑定到处理方法的入参中
4.3.1 实验代码
//了解: 映射请求头信息 用法同 @RequestParam
@RequestMapping(value="/testRequestHeader")
public String testRequestHeader(
@RequestHeader(value="Accept-Language") String al){
System.out.println("testRequestHeader - Accept-Language:"+al);
return "success";
}
<!-- 测试 请求头@RequestHeader 注解使用 -->
<a href="springmvc/testRequestHeader">testRequestHeader</a>
4.4 Rest@PathVariable
在@RequestMapping(value="/{}/{}")
方法(@Pathvarieble)
@RequestMapping(value="/testRESTGet/{id}",method=RequestMethod.GET)//要求的请求方式
public String testRESTGet(@PathVariable(value="id") Integer id){
System.out.println("testRESTGet id="+id);
return "success";
}
4.5 @CookieValue
1) 使用 @CookieValue 绑定请求中的 Cookie 值
2) @CookieValue 可让处理方法入参绑定某个 Cookie 值
4.4.1实验代码
1) 增加控制器方法
//了解:@CookieValue: 映射一个 Cookie 值. 属性同 @RequestParam
@RequestMapping("/testCookieValue")
public String testCookieValue(@CookieValue("JSESSIONID") String sessionId) {
System.out.println("testCookieValue: sessionId: " + sessionId);
return "success";
}
2) 增加页面链接
<!--测试 请求Cookie @CookieValue 注解使用 -->
<a href="springmvc/testCookieValue">testCookieValue</a>
4.6 使用POJO作为参数
使用场景:原来是多个参数就要多个形参
1) 使用 POJO 对象绑定请求参数值
2) Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配,自动为该对象填充属性值。支持级联属性。如:dept.deptId、dept.address.tel 等
默认保证参数名称和请求中传递的参数名相同
4.5.1实验代码
1) 增加实体类
我们首先由如下的实体类
package com.atguigu.springmvc.entities;
public class Address {
private String province;
private String city;
//get/set.........
}
-----------------------
package com.atguigu.springmvc.entities;
public class User {
private Integer id ;
private String username;
private String password;
private String email;
private int age;
private Address address;// 注意这个属性是个类
//如果是个list,在jsp中就可以使用[0]的形式
//get/set
}
2) 增加控制器方法、表单页面
/**
* Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配, 自动为该对象填充属性值。
* 支持级联属性
* 如:dept.deptId、dept.address.tel 等
*/
@RequestMapping("/testPOJO")
public String testPojo(User user) {//将URL附带的参数传入user的属性中
System.out.println("testPojo: " + user);
return "success";
}
<!-- 测试 POJO 对象传参,支持级联属性 -->
<form action=" testPOJO" method="POST">
username: <input type="text" name="username"/><br>
password: <input type="password" name="password"/><br>
email: <input type="text" name="email"/><br>
age: <input type="text" name="age"/><br>
city: <input type="text" name="address.city"/><br><!--支持级联的方式-->
province: <input type="text" name="address.province"/>
<input type="submit" value="Submit"/>
</form>
3) 执行结果:
User[id=null ,username=zhansgan,password=,email=.com,age=22]
4) 如果中文有乱码,需要配置字符编码过滤器,且配置其他过滤器之前,
如(HiddenHttpMethodFilter),否则不起作用。(思考method=”get”请求的乱码问题怎么解决的)
<!-- 配置字符集 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
pojo的问题可以参数4.1节
4.7 使用Servlet原生API作为参数
1) MVC 的 Handler 方法可以接受 ServletAPI 类型的参数:
- HttpServletRequest 常用
- HttpServletResponse 常用:如果使用它,则处理方法的返回值设置为void即可。
- HttpSession:
- java.security.Principal
- Locale
- InputStream
- OutputStream
- Reader
- Writer
/**
* 可以使用 Serlvet 原生的 API 作为目标方法的参数 具体支持以下类型
*
* HttpServletRequest
* HttpServletResponse
* HttpSession
* java.security.Principal
* Locale InputStream
* OutputStream
* Reader
* Writer
* @throws IOException
*/
@RequestMapping("/testServletAPI")
public void testServletAPI(
HttpServletRequest request,
HttpServletResponse response,
Writer out) throws IOException {//只要写个形参,就会自动写到形参里
System.out.println("testServletAPI, " + request + ", " + response);
// 可以利用原生API进行请求转发或重定向
request.getRequestDispatcher("/WEV-INF/views/success.jsp").forward(request,response);
response.getWriter.println("hello");//写到页面
out.write("hello springmvc");
//return "success";
}
--------------------------
@RequestMapping("/testServletAPI")
public String testServletAPI(
HttpServletRequest request,
HttpServletResponse response,
@CookieValue(value="sessionId",required=false) String sessionId)
<!-- 测试 Servlet API 作为处理请求参数 -->
<a href="springmvc/testServletAPI">testServletAPI</a>
2) 源码参考:AnnotationMethodHandlerAdapter L866
@Override
protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
if (ServletRequest.class.isAssignableFrom(parameterType) ||
MultipartRequest.class.isAssignableFrom(parameterType)) {
Object nativeRequest = webRequest.getNativeRequest(parameterType);
if (nativeRequest == null) {
throw new IllegalStateException(
"Current request is not of type [" + parameterType.getName() + "]: " + request);
}
return nativeRequest;
}
else if (ServletResponse.class.isAssignableFrom(parameterType)) {
this.responseArgumentUsed = true;
Object nativeResponse = webRequest.getNativeResponse(parameterType);
if (nativeResponse == null) {
throw new IllegalStateException(
"Current response is not of type [" + parameterType.getName() + "]: " + response);
}
return nativeResponse;
}
else if (HttpSession.class.isAssignableFrom(parameterType)) {
return request.getSession();
}
else if (Principal.class.isAssignableFrom(parameterType)) {
return request.getUserPrincipal();
}
else if (Locale.class.equals(parameterType)) {
return RequestContextUtils.getLocale(request);
}
else if (InputStream.class.isAssignableFrom(parameterType)) {
return request.getInputStream();
}
else if (Reader.class.isAssignableFrom(parameterType)) {
return request.getReader();
}
else if (OutputStream.class.isAssignableFrom(parameterType)) {
this.responseArgumentUsed = true;
return response.getOutputStream();
}
else if (Writer.class.isAssignableFrom(parameterType)) {
this.responseArgumentUsed = true;
return response.getWriter();
}
return super.resolveStandardArgument(parameterType, webRequest);
}
@Override
protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
if (ServletRequest.class.isAssignableFrom(parameterType) ||
MultipartRequest.class.isAssignableFrom(parameterType)) {
Object nativeRequest = webRequest.getNativeRequest(parameterType);
if (nativeRequest == null) {
throw new IllegalStateException(
"Current request is not of type [" + parameterType.getName() + "]: " + request);
}
return nativeRequest;
}
else if (ServletResponse.class.isAssignableFrom(parameterType)) {
this.responseArgumentUsed = true;
Object nativeResponse = webRequest.getNativeResponse(parameterType);
if (nativeResponse == null) {
throw new IllegalStateException(
"Current response is not of type [" + parameterType.getName() + "]: " + response);
}
return nativeResponse;
}
else if (HttpSession.class.isAssignableFrom(parameterType)) {
return request.getSession();
}
else if (Principal.class.isAssignableFrom(parameterType)) {
return request.getUserPrincipal();
}
else if (Locale.class.equals(parameterType)) {
return RequestContextUtils.getLocale(request);
}
else if (InputStream.class.isAssignableFrom(parameterType)) {
return request.getInputStream();
}
else if (Reader.class.isAssignableFrom(parameterType)) {
return request.getReader();
}
else if (OutputStream.class.isAssignableFrom(parameterType)) {
this.responseArgumentUsed = true;
eturn response.getOutputStream();
}
else if (Writer.class.isAssignableFrom(parameterType)) {
this.responseArgumentUsed = true;
return response.getWriter();
}
return super.resolveStandardArgument(parameterType, webRequest);
}
@ModelAttribure接收请求参数
@ModelAttribure注解放在方法的形参上,用于将多个请求参数封装到一个实体对象,从而简化数据绑定流程,而且自动暴露为模型数据,于视图页面展示时使用。
处理方法的数据绑定
我们知道 Spring会根据请求方法签名的不同,将请求消息中的信息以一定的方式转换并绑定到请求方法的入参中。在请求消息到达真正调用处理方法的这一段时间内, Spring MvC还完成了很多工作,包括数据转换、数据格式化及数据校验等。
1 数据绑定流程剖析
Spring MVC通过反射机制对目标处理方法的签名进行分析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是 DataBinder,其运行机制描述如图所示
pring mvc主框架将 ServletRequest对象及处理方法的入参对象实例传递给DataBinnder, DataBinder调用装配在 Spring Web上下文中的ConversionService组件进行数据类型转换、数据格式化的工作,将 ServletRequest中的消息填充到入参对象中。然后再调用 Validator组件对已经绑定了请求消息数据的入参对象进行数据合法性校验,并最终生成数据绑定结果 BindingResult对象。 Binding Result包含了已完成数据绑定的入参对象,还包含相应的校验错误对象,Spring MVC抽取BindingResult中的入参对象及校验错误对象,将它们赋给处理方法的相应入参。
后面我们将对数据绑定过程中发生的数据转换、数据格式化及数据校验等工作进行详细的阐述。
2 数据转换
在第5章中,我们已经学习了如何编写并装配自定义属性编辑器的知识。Java标准的Poperty Editor的核心功能是将一个字符串转换为一个Java对象,以便根据界面的输入或配置文件中的配置字符串构造出一个WM内部的Java对象。
但是Java原生的 Property Editor存在以下不足:
- 只能用于字符串和Java对象的转换,不适用于任意两个Java类型之间的转换
- 对源对象及目标对象所在的上下文信息(如注解、所在宿主类的结构等)不敏感,在类型转换时不能利用这些上下文信息实施高级转换逻辑
有鉴于此, Spring3.0在核心模型中添加了一个通用的类型转换模块,类型转换模块位于org. springframeworkcoreconvert包中 。Spring希望用这个类型转换体系替换Java标准的 Property Editor。但由于历史原因, Spring将同时支持两者,在Bean配置、 Spring MVC处理方法入参绑定中使用它们。
Conversion Service
ConversionService是 Spring类型转换体系的核心接口,它位于 org.springframework.core.convert包中,亦是该报中的唯一一个接口。ConversionSService接口定义了以下4个方法:
boolean can Convert(Class<? source /ype, Class<> targetType):判断是否可以将一个Java类转换为另一个Java类
Boolean can Convert(TypeDescriptor sourceType, TypeDescriptor targetType):需转换的类将以成员变量的方式出现在宿主类中, TypeDescriptor不但描述了需转换类的信息,还描述了从宿主类的上下文信息:如成员变量上的注解,成员变量是否以数组、集合或M的方式呈现等。类型转换逻辑可以利用这些信息做出各种灵活的控制。
<T> T convert(Object source, Class<T> targetType,)将原类型对象转换为目标类型
Object convert( Object source, Type Descriptor source Type, Type Descriptor targetType): 对象从原类型对象转换为目标类型对象,此时往往会利用到宿主类中的上下文信息。
第1和第3个接口方法类似于 Property Editor,它不关注类型对象所在的上下文信息,只简单地完成两个类型对象的转换,唯一区别在于这两个方法支持任意两个类型的转换
而第2和第4个接口方法,会参考类型对象所在宿主类的上下文信息,并利用这些信息进行类型转换
可以利用 org.springframeworkcontext.support.ConversionServiceFactoryBean在 Spring的上下文中定义一个 ConversionService。 Spring将自动识别出上下文中的ConversionService,并在Bean属性配置及 Spring MVC处理方法入参绑定等场合使用它进行数据的转换。具体配置如下所示
<bean id="conversionService" class=org.springframework.context.support.ConversionServiceFactoryBean">
该 FactoryBean创建 ConversionService内建了很多的转换器,可完成大多数Java类型的转换工作除了包括将Sing对象转换为各种基础类型的对象外,还包括 String、 Number、Aray、 Collection、Map、 Properties及object之间的转换器。
可通过 ConversionServiceFactoryBean的 converters属性注册自定义的类型转换器,如下所示
<bean id=“conversionService class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<list>
<bean class="com. baobaotao.MyCustomConverter1"/>
<bean class="com. baobaotao.MyCustomConverter2"/>
</list>
</property>
</bean>
在52节中,我们知道,通过 CustomEditor Configurer注册的自定义属性编辑器必须实现 PropertyEditor接口。现在注册到 ConversionServiceFactoryBean中的自定义转换器必须实现哪些接口,并满足哪些约定呢?我们将在接下来的小节中进行说明。
Spring支持哪些转换器
Spring在 org.springframework.core.convert.converter包中定义了3种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到 ConversionService-FactoryBean中。这3种类型的转换器接口分别为
- Converter<S, T>
- GenericConverter
- ConverterFactory
首先,我们要了解的转换器接口是 Converter接口,它是 Spring最简单的一个转换器接口,仅包括一个接口方法。接口定义如下
package org.springframework.core.convert.converter;
public interface Converter<S,T>{
T converter(S source);
}
T converter(S source)负责将S类型的对象转换为T类型的对象。如果希望将一种类型的对象转换为另一种类型及其子类的对象,举例来说,将String转换为 Number及 Number子类( Integer、Long、 Double等)对象,就需要一系列的 Converter,如 StringTolnteger、StringToLong及 StringToDouble等。 Spring提供了一个将相同系列多个“同质” Converter封装在一起的 ConverterFactory接口,定义如下:
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R>{
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
S为转换的源类型,R为目标类型的基类,而T为扩展于R基类的类型。如Spring的StringToNumberConverterFactory就实现了 ConverterFactory接口,封装了String转换到各种数据类型的Converter。
Converter只是负责将一个类型的对象转换为另一个类型的对象,并没有考虑类型对象所在宿主类上下文的信息,因此并不能完成“复杂”类型转换的工作。 GenericConverter接口会根据源类对象及目标类对象所在宿主类中的上下文信息进行类型转换工作,其接口定义如下
package org.springframework.core.convert.converter;
public interface GenericConverter{
public Set< ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
ConvertiblePair封装了源类型和目标类型,组成一个“对子”,而 TypeDescriptor包含了需转换类型对象所在宿主类的信息,因此 Generic Converter的 converto接口方法可以利用这些上下文信息进行类型转换的工作。
ConditionalGenericConverter扩展于 GenericConverter接口,并添加了一个接口方法:
boolean matches(Type Descriptor source Type, TypeDescriptor targetType
在Spring MVC中使用ConversionService
参考类型转换文档:思路为实现Converter接口,然后在xml中配置
四、数据校验
应用程序在执行业务逻辑前,必须通过数据校验保证接收到的输入数据是正确合法的,如代表生日的日期应该是一个过去的时间,工资的数值必须是一个正数。很多时候同样的数据校验会出现在不同的层中,违反了DRY原则,为了避免数据的冗余校验,将验证逻辑和相应的域模型进行绑定,将代码验证的逻辑集中起来管理。
JSR 303是Java为Bean数据合法性校验提供的标准框架,它已经包含在JavaEE 6.0中 ,JSR 303通过在Bean属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证,SpringMVC可通过Hibernate Validator(JSR303的参考实现)进行JSR303校验
Spring拥有自己独立的数据校验框架,同时支持JSR-303标准的校验框架。Spring的DataBinder在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC中,可直接通过注解驱动的方式进行数据校验。
Spring的org.springframework.validation是校验框架所在的包,Validator接口拥有以下两个方法:
- boolean supports(Class<?> clazz):该校验器能够对clazz类型的对象进行校验
- void validate(Object target,Errors erros):对目标类target进行校验,并将校验错误记录在errors中
LocalValidatorFactoryBean既实现了Spring的Validator接口,也实现了JSR-303的Validator接口,只要再Spring容器中定义一个LocalValidatorFactoryBean,即可将其注入需要数据校验的Bean中。定义一个LocalValidatorFactoryBean非常简单
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
注意:Spring本身没有提供JSR-303的实现,所以必须将JSR-303的实现者(如Hibernate Validator)的jar文件放到类路径下,Spring将自动加载并装配好JSR 303的实现者。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.0.Final</version>
</dependency>
<mvc:annotaion-driver/>
会默认装配好一个LocalValidatorFactoryBean,通过在处理方法的入参上标注@Valid注解即可让SpringMVC在完成数据绑定后执行数据校验工作。
public class User {
//加入 JSR303 验证注解
@NotNull
private Integer id;
private String name;
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birthday;
private String address;
//省略 setter getter......
}
@RequestMapping(value="testConversion",method=RequestMethod.POST)
//此处注意 :@Valid 注解表示其后面的java bean注入需要JSR303验证,如果其验证错误
//那么必须在其后面紧跟一个BindingResult(或Error)类型参数,不可打乱顺序,验证bean和BindingResult间不能有其他参数
public String testConversion(@Valid User user,BindingResult result){
if(result.hasErrors()){
for (FieldError error : result.getFieldErrors()) {
System.out.println(error.getField()+"--"+error.getDefaultMessage());
}
}
System.out.println(user);
return SUCCESS;
}
在已经标注了JSR-303注解的入参对象前添加@Valid注解,Spring MVC框架在将请求数据绑定到该入参对象之后,就会调用校验框架根据注解声明的校验规则实施校验。
BindingResult紧随校验入参对象之后,成对出现,它们之间不允许有其他入参。