Web工程使用Spring MVC,在启动阶段会将注解@RequestMapping所配置的内容保存到处理器映射(HandlerMapping)机制中去,然后等待请求的到来,
通过拦截请求信息与HandlerMapping进行匹配,找到对应的处理器(它包含控制器的逻辑),并将处理器及其拦截器保存到HandlerExecutionChain对象中,返回给DispatcherServlet,这样DispatcherServlet就可以运行它们了。
HandlerMapping:将请求定位到具体的处理器上,了解下它的源码。
package org.springframework.web.bind.annotation;
/**** imports ****/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
// 配置请求映射名称
String name() default "";
// 通过路径映射
@AliasFor("path")
String[] value() default {};
// 通过路径映射回path配置项
@AliasFor("value")
String[] path() default {};
// 限定只响应HTTP请求类型,如GET、POST、HEAD、OPTIONS、PUT、 TRACE等
// 默认的情况下,可以响应所有的请求类型
RequestMethod[] method() default {};
// 当存在对应的HTTP参数时才响应请求
String[] params() default {};
// 限定请求头存在对应的参数时才响应
String[] headers() default {};
// 限定HTTP请求体提交类型,如"application/json"、"text/html"
String[] consumes() default {};
// 限定返回的内容类型,仅当HTTP请求头中的(Accept)类型中包含该指定类型时才返回
String[] produces() default {};
}
value或者path:必须配置项,设置请求的URL,从而让对应的请求映射到控制器或其方法上(value和path可以通过正则表达式,但不建议,原则上希望一个路径对应一个方法,这样代码可读性更高,便于维护)。
method:常用的配置项,可以限定HTTP的请求类型,区分HTTP的GET或者POST等不同的请求。在Spring 4.3的版本之后,为了简化method配置项的配置新增了几个注解,如@GetMapping、@PostMapping、@PatchMapping、@PutMapping和@DeleteMapping。
@GetMapping对应的是HTTP的GET方法,@PostMapping对应的是HTTP的POST方法,其他的配置项则与@RequestMapping无太大的区别,通过它们就可以不再设置@RequestMapping的method配置项了。
处理器Handler是对控制器的包装,在处理器运行的过程中会调度控制器的方法,只是它在进入控制器方法之前会对HTTP的参数和上下文进行解析,将它们转换为控制器所需的参数。
针对HTTP的参数与方法参数的转换,Spring MVC已提供了大量的转换规则,处理器可以直接使用。
1.无注解下获取参数:在没有注解的情况下,Spring MVC也可以获取参数,参数允许为空,唯一的要求是参数名称和HTTP请求的参数名称保持一致。
/**
* 在无注解下获取参数,要求参数名称和HTTP请求参数名称一致
* @param intVal -- 整数
* @param longVal -- 长整型
* @param str --字符串
* @return 响应JSON参数
*/
@GetMapping("/no/annotation")
@ResponseBody
public Map noAnnotation(
Integer intVal, Long longVal, String str) {
Map paramsMap = new HashMap<>();
paramsMap.put("intVal", intVal);
paramsMap.put("longVal", longVal);
paramsMap.put("str", str);
return paramsMap;
}
测试:http://localhost:8080/paramTest/no/annotation?int_val=1&long_val=2&str_val=str
2.使用@RequestParam获取参数:注解@RequestParam可以指定HTTP参数和方法参数的映射关系,注意默认的情况下@RequestParam标注的参数是不能为空的,为了让它能够为空,可以配置其属性required为false,即@RequestParam(value=“str_val”, required = false) String strVal。
/**
* 通过注解@RequestParam获取参数
* @param intVal -- 整数
* @param longVal -- 长整型
* @param strVal --字符串
* @return 响应JSON数据集
*/
@GetMapping("/requestParam")
@ResponseBody
public Map requestParam(
@RequestParam("int_val") Integer intVal,
@RequestParam("long_val") Long longVal,
@RequestParam("str_val") String strVal) {
Map paramsMap = new HashMap<>();
paramsMap.put("intVal", intVal);
paramsMap.put("longVal", longVal);
paramsMap.put("strVal", strVal);
return paramsMap;
}
测试:http://localhost:8080/paramTest/requestParam?int_val=1&long_val=2&str_val=str
3.传递数组:Spring MVC内部能够支持用逗号分隔的数组参数
@GetMapping("/requestArray")
@ResponseBody
public Map requestArray(
int [] intArr, Long []longArr, String[] strArr) {
Map paramsMap = new HashMap<>();
paramsMap.put("intArr", intArr);
paramsMap.put("longArr", longArr);
paramsMap.put("strArr", strArr);
return paramsMap;
}
测试:http://localhost:8080/paramTest/requestArray?intArr=1,2,3&longArr=4,5,6&strArr=str1,str2,str3
4.传递JSON:方法的参数标注为@RequestBody,意味着它将接收前端提交的JSON请求体,而在JSON请求体与User类之间的属性名称是保持一致的,这样Spring MVC就会通过这层映射关系将JSON请求体转换为User对象。
@PostMapping("/insert")
@ResponseBody
public AppUserEntity insert(@RequestBody AppUserEntity user) {
//假装新增了
return user;
}
测试:http://localhost:8080/paramTest/insert
其中body:{“name”:“aa”}
注意:
1.User对象要有一个有参数的构造器,可以使用注解@AllArgsConstructor
2.请求头中要有:Content-Type:application/json,不然会报415,传入类型不对
5.通过URL传递参数:Spring MVC对此也提供了良好的支持,可以通过处理器映射和注解@PathVariable的组合获取URL参数
@GetMapping("/{id}")
@ResponseBody
public AppUserEntity get(@PathVariable("id") Integer id) {
return AppUserEntity.builder().id(id).build();
}
测试:http://localhost:8080/paramTest/2
6.获取格式化参数
在一些应用中,往往需要格式化数据,其中最为典型的当属日期和货币。
例如,在一些系统中日期格式约定为yyyy-MM-dd,金额约定为货币符号和用逗号分隔,如100万美元写作$1,000,000.00等。
Spring MVC也对此提供了良好的支持。对日期和数字类型的转换注解进行处理,分别是@DateTimeFormat和@NumberFormat。
@DateTimeFormat是针对日期进行格式化的,
@NumberFormat则是针对数字进行格式化的。
format方法参数加粗的代码使用了注解@DateTimeFormat和@NumberFormat,它们配置了格式化所约定的格式,所以Spring会根据约定的格式把数据转换出来,这样就可以完成参数的转换。
测试:http://localhost:8080/paramTest/format
在Spring Boot中,日期参数的格式化也可以不使用@DateTimeFormat,而只在配置文件application.properties中加入如下配置项即可:
spring.mvc.date-format=yyyy-MM-dd
HTTP的请求包含请求头(Header)、请求体(Body)、URL和参数等内容,服务器还包含其上下文环境和客户端交互会话(Session)机制,而这里的消息转换是指请求体的转换。
1.处理器获取参数逻辑
当一个请求来到时,在处理器Handler执行的过程中,它首先会从HTTP请求和上下文环境来得到参数。如果是简易的参数它会以简单的转换器进行转换,而这些简单的转换器是Spring MVC自身已经提供了的。
但是如果是转换HTTP请求体(Body),它就会调用HttpMessageConverter接口的方法对请求体的信息进行转换,首先它会先判断能否对请求体进行转换,如果可以就会将其转换为Java类型。
HttpMessageConverter接口源码:
package org.springframework.http.converter;
/**** imports ****/
public interface HttpMessageConverter {
// 是否可读,其中clazz为Java类型,mediaType为HTTP请求类型
boolean canRead(Class> clazz, MediaType mediaType);
// 判断clazz类型是否能够转换为mediaType媒体类型
// 其中clazz为java类型,mediaType为HTTP响应类型
boolean canWrite(Class> clazz, MediaType mediaType);
// 可支持的媒体类型列表
List getSupportedMediaTypes();
// 当canRead验证通过后,读入HTTP请求信息
T read(Class extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
//当canWrite方法验证通过后,写入响应
void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
这里需要讨论的是canRead和read方法,canWrite和write方法将在后续讨论。
如果代码中控制器方法的参数标注了@RequestBody,处理器会采用请求体(Body)的内容进行参数转换,而前端的请求体为JSON类型,所以首先它会调用canRead方法来确定请求体是否可读。
如果判定可读后,接着就是使用read方法,将前端提交的用户JSON类型的请求体转换为控制器的类型参数,这样控制器就能够得到参数了。
在Spring MVC中,是通过WebDataBinder机制来获取参数的,它的主要作用是解析HTTP请求的上下文,然后在控制器的调用之前转换参数并且提供验证的功能,为调用控制器方法做准备。
处理器会从HTTP请求中读取数据,然后通过三种接口来进行各类参数转换,这三种接口是Converter、Formatter和GenericConverter。
在Spring MVC的机制中这三种接口的实现类都采用了注册制,默认情况下Spring MVC已经在注册机内注册了许多的转换器,可以实现大部分的数据类型的转换,
所以大部分的情况下无须开发者再提供转换器,同样地,当需要自定义转换规则时,只需要在注册机上注册自己的转换器就可以了。
实际上,WebDataBinder机制还有一个重要的功能,那就是验证转换结果。关于验证机制,后面会再讨论。有了参数的转换和验证,最终控制器就可以得到合法的参数。
得到这些参数后,就可以调用控制器的方法了。
这个图严格来说是请求体转换的全流程,但是有些时候Spring MVC并不会走完全流程,而是根据现实情况来处理消息的转换。
控制器的参数是处理器通过Converter、Formatter和GenericConverter这三个接口转换出来的,先谈谈这三个接口的不同之处。
Converter:一个普通的转换器,例如,有一个Integer类型的控制器参数,而从HTTP对应的为字符串,对应的Converter就会将字符串转换为Integer类型;
Formatter:一个格式化转换器,类似那些日期字符串就是通过它按照约定的格式转换为日期的;
GenericConverter:将HTTP参数转换为数组。
对于数据类型转换,Spring MVC提供了一个服务机制去管理,它就是ConversionService接口。在默认的情况下,会使用这个接口的子类DefaultFormattingConversionService对象来管理这些转换类。
Converter、Formatter和GenericConverter可以通过注册机接口进行注册,这样处理器就可以获取对应的转换器来实现参数的转换。
上面讨论的是普通Spring MVC的参数转换规则,而在Spring Boot中还提供了特殊的机制来管理这些转换器。
Spring Boot的自动配置类WebMvcAutoConfiguration还定义了一个内部类WebMvcAutoConfigurationAdapter,
ConversionService转化机制设计
Spring Boot的自动注册机制
// 注册各类转换器,registry实际为DefaultFormattingConversionService对象
@Override
public void addFormatters(FormatterRegistry registry) {
// 遍历IoC容器,找到Converter类型的Bean注册到服务类中
for (Converter, ?> converter : getBeansOfType(Converter.class)) {
registry.addConverter(converter);
}
// 遍历IoC容器,找到GenericConverter类型的Bean注册到服务类中
for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
registry.addConverter(converter);
}
// 遍历IoC容器,找到Formatter类型的Bean注册到服务类中
for (Formatter> formatter : getBeansOfType(Formatter.class)) {
registry.addFormatter(formatter);
}
}
通过这个方法,在Spring Boot的初始化时,会将对应用户自定义的Converter、Formatter和GenericConverter的实现类所创建的Spring Bean自动地注册到DefaultFormattingConversionService对象中。
这样对于开发者只需要自定义Converter、Formatter和GenericConverter的接口的Bean,Spring Boot就会通过这个方法将它们注册到ConversionService对象中。只是格式化Formatter接口,在实际开发中使用率比较低,所以不再论述。
2.一对一转换器(Converter)
Converter是一对一的转化器,也就是从一种类型转换为另外一种类型。
Converter接口源码
package org.springframework.core.convert.converter;
public interface Converter {
// 转换方法,S代表原类型,T代表目标类型
T convert(S source);
}
这个接口的类型有源类型(S)和目标类型(T)两种,它们通过convert方法进行转换。
例如,HTTP的类型为字符串(String)型,而控制器参数为Long型,那么就可以通过Spring内部提供的StringToNumber进行转换。
假设前端要传递一个用户的信息,这个用户信息的格式是{id}-{userName}-{note},而控制器的参数是User类对象。因为这个格式比较特殊,Spring当前并没有对应的Converter进行转换,
因此需要自定义转换器。这里需要的是一个从String转换为User的转换器。
字符串用户转换器
package com.springboot.chapter10.converter;
/**** imports ****/
/**
* 自定义字符串用户转换器
*/
@Component
public class StringToUserConverter implements Converter {
/**
* 转换方法
*/
@Override
public User convert(String userStr) {
User user = new User();
String []strArr = userStr.split("-");
Long id = Long.parseLong(strArr[0]);
String userName = strArr[1];
String note = strArr[2];
user.setId(id);
user.setUserName(userName);
user.setNote(note);
return user;
}
}
这里的类标注为@Component,并且实现了Converter接口,这样Spring就会将这个类扫描并装配到IoC容器中。
对于Spring Boot,之前分析过它会在初始化时把这个类自动地注册到转换机制中,所以注册这步并不需要人工再处理。
这里泛型指定为String和User,这样Spring MVC就会通过HTTP的参数类型(String)和控制器的参数类型(User)进行匹配,就可以从注册机制中发现这个转换类,这样就能够将参数转换出来。
使用控制器方法接收用户参数
@GetMapping("/converter")
@ResponseBody
public User getUserByConverter(User user) {
return user;
}
测试:http://localhost:8080/user/converter?user=1-user_name_1-note_1
3.GenericConverter集合和数组转换
GenericConverter是数组转换器。因为Spring MVC自身提供了一些数组转换器,需要自定义的并不多,所以这里只介绍Spring MVC自定义的数组转换器。
假设需要同时新增多个用户,这样便需要传递一个用户列表(List)给控制器。此时Spring MVC会使用StringToCollectionConverter转换它,这个类实现了GenericConverter接口,并且是Spring MVC内部已经注册的数组转换器。
它首先会把字符串用逗号分隔为一个个的子字符串,然后根据原类型泛型为String、目标类型泛型为User类,找到对应的Converter进行转换,将子字符串转换为User对象。
上节我们已经自定义了转换器StringToUserConverter,这样它就可以发现这个转换器,从而将字符串转换为User类。这样控制器就能够得到List类型的参数。
使用集合(List)传递多个用户
@GetMapping("/list")
@ResponseBody
public List list(List userList) {
return userList;
}
这样只要在浏览器地址栏中输入URL请求这个方法:
http://localhost:8080/user/list?userList=1-user_name_1-note_1,2-user_name_2-note_2,3-user_name_3-note_3
这里的参数使用了一个个逗号分隔,StringToCollectionConverter在处理时也就通过逗号分隔,然后通过之前自定义的转换器StringToUserConverter将其变为用户类对象,再组成为一个列表(List)传递给控制器。