Spring MVC中的处理器映射
控制器中使用注解@RequestMapping处理映射的过程:在Spring MVC项目中,项目启动阶段会将注解@RequestMapping所配置的内容保存到处理映射器(HandlerMapping)中,然年等待请求的发送,通过拦截请求信息与HandlerMapping进行匹配,找到对应的处理器(包含控制器的逻辑),并将处理器以及拦截器保存到HandlerExecutionChain对象中,放回给DispatcherServlet,这样DispatcherServlet就可以运行它们了。可以看出HandlerMapping的主要任务就是请求定位到具体的处理器上。
下面为@RequestMapping注解的源码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
可以看出,在该注解中可以设置如下的属性值:
- path和value的属性:设置URL映射的路径
- method:限定响应的HTTP请求类型,如GET,POST,HEAD,OPTIONS,PUT,TRACE等,默认情况下可以接受所有请求
- params:当存在对应的HTTP参数时才进行响应
- headers:限定请求头中存在对应的参数时进行响应
- consumes:限定HTTP请求体提交类型,如application/json,text/html
- produces:限定返回的内容类型,仅当HTTP请求头中的类型包含Accept类型才返回
其中value属性是必须的配置项,而为了简化method的配置,在Spring4.3之后新增了如下注解分别对应不同的请求类型:
- GetMapping
- PostMapping
- PatchMapping
- PutMapping
- DeleteMapping
其中PatchMapping,PutMapping和DeleteMapping常用在REST风格的项目设计中。
获取控制器参数
Spring MVC中处理器是控制器的包装,其在运行过程中会调用控制器中的方法,并在此之前对HTTP的参数和上下文信息进行解析,将它们转化为控制器所需的参数。下面对控制器如何获取请求的参数进行示例说明。
无注解下的参数传递
由于Spring中已经提供了大量的参数转化规则,所以在很多情况下并不需要对参数的获取进行设置。在默认情况下,参数运行为空,唯一要求为参数名称与Controller中方法的参数名保持一致:
@Controller
@RequestMapping("/param")
public class ParamController {
/**
* 默认参数传递
*/
@RequestMapping("/no/annotation")
@ResponseBody
public Map<String, Object> noAnnotation(Integer intVal, Long longVal, String str) {
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put("inVal", intVal);
paramsMap.put("longVal", longVal);
paramsMap.put("str", str);
return paramsMap;
}
}
进行如下请求时:
http://localhost:8080/param/no/annotation?intVal=10&longVal=200
返回结果为
使用@RequestParam获取参数
当前端URL中的参数名与Controller方法中的参数名不一致时,可以使用@RequestParam注解来指定URL参数与方法参数之间的映射关系:
/**
* 参数对应关系设置
*/
@RequestMapping("/annotation")
@ResponseBody
public Map<String, Object> requestParam(@RequestParam(value = "int_val", required = false) Integer intVal,
@RequestParam(value = "long_val", required = false) Long longVal,
@RequestParam(value = "str_val", required = false) String str) {
Map<String, Object> params = new HashMap<>();
params.put("intVal", intVal);
params.put("longVal", longVal);
params.put("str", str);
return params;
}
在使用@RequestParam注解时,默认参数值不能为空,否则会出现异常信息。可以使用required属性指明参数是否运行为空即可。使用如下的URL进行请求:
http://localhost:8080/param/annotation?int_val=10&long_val=20&str_val=yitian
返回结果为:
传递数组
Spring MVC中允许以数组的方式进行参数的传递。数组类型的参数以逗号(,)分隔:
/**
* 数组参数传递
*/
@RequestMapping("/requestArray")
@ResponseBody
public Map<String, Object> requestArray(int[] intArr, Long[] longArr, String[] strArr) {
Map<String, Object> params = new HashMap<>();
params.put("intArr", intArr);
params.put("longArr", longArr);
params.put("strArr", strArr);
return params;
}
请求URL为:
http://localhost:8080/param/requestArray?intArr=10,11,12&longArr=20,30,40&strArr=yitian1,yitian2
返回结果为:
JSON数据的传递
在使用JSON格式进行数据传递时,简单建立一个用户注册的页面register.jsp:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Register</title>
<script type="text/javascript">
$(document).ready(function() {
$("#submit").click(function () {
var userName = $("#userName").val();
var note = $("#note").val();
var sex = $("#sex").val();
if (userName == '' || sex == '' || note == '') {
alert("表单参数需填满!");
return;
}
var params = {
userName: userName,
sex: sex,
note: note
};
$.post({
url: "./createUser",
contentType: "application/json",
data: JSON.stringify(params),
success: function (result) {
if (result == null || result.userName == null) {
alert("注册失败!");
return;
}
alert("注册成功!");
}
});
});
});
</script>
</head>
<body>
<form id="insertForm" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input id="userName" name="userName"></td>
</tr>
<tr>
<td>性别:</td>
<td><input id="sex" name="sex"></td>
</tr>
<tr>
<td>备注:</td>
<td><input id="note" name="note"></td>
</tr>
<tr>
<td></td>
<td align="right" style="height: 32px;"><input id="submit" type="button" value="SUBMIT"></td>
</tr>
</table>
</form>
<div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>
controller类中代码如下:
/**
* 注册用户
*/
@RequestMapping(value = "/createUser", method = RequestMethod.POST)
@ResponseBody
public User createUser(@RequestBody User user) {
System.out.println(user);
userService.addUser(user);
return user;
}
提交表单后,请求后的返回值如下:
使用URL传递参数
在REST风格的请求中,常将参数在URL中以/param的方式进行传递,这时就需要使用@PathVariable注解从URL中获取相应的参数值:
/**
* URL路径参数传递
*/
@RequestMapping("/{id}")
@ResponseBody
public Long get(@PathVariable("id") Long id) {
return id;
}
相应的请求URL为:
http://localhost:8080/param/1
返回值即为:
获取格式化的参数
当使用日期或金额作为参数进行传递时,需要对日期和数字类型进行转换以格式化参数值。日期的参数处理注解为@DateTimeFormat,数字的为@NumberFormat。简单的使用下面的页面进行测试:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Formatter Request</title>
</head>
<body>
<form id="insertForm" method="post" action="./formatter/commit">
<table>
<tr>
<td>日期:(yyyy-MM-dd)</td>
<td><input id="date" name="date" type="text" value="2020-01-30"></td>
</tr>
<tr>
<td>金额:(#,###,###.##)</td>
<td><input id="number" name="number" type="text" value="1,234,567.89"></td>
</tr>
<tr>
<td align="right" style="height: 32px;"><input type="submit" value="SUBMIT"></td>
</tr>
</table>
</form>
<div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>
服务端方法为:
/**
* 日期和数字格式化传递
*/
@RequestMapping("/formatter/commit")
@ResponseBody
public Map<String, Object> formatCommit(@DateTimeFormat(iso= DateTimeFormat.ISO.DATE) Date date,
@NumberFormat(pattern = "#,###,###.##") Double number) {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("date", date);
dataMap.put("number", number);
return dataMap;
}
其中分别使用注解@DateTimeFormat和@NumberFormat注解约定参数的传入格式 ,提交表单,可以看到格式化的数据。
{date=Sun Feb 16 00:00:00 CST 2020, number=1234567.89}
此外,在Spring Boot中,日期的格式化可以不使用@DateTimeFormat注解,直接在application.properties配置文件中指明时间的格式:
# 日期格式
spring.mvc.date-format=yyyy-MM-dd
自定义参数转换规则
在使用一些特殊格式进行参数传递时,Spring MVC无法自动进行参数转换,此时需要自定义参数转换规则来对参数进行转换。HTTP的请求中包括请求头Header,请求体Body,URL和参数等内容,服务其中还包括上下文环境和交互会话Session,这里的消息转换指的为Body的转换。
在开始自定义参数转换规则之前,首先大致了解下Spring MVC中处理器的参数转换过程。Spring MVC通过WebDataBinder绑定机制来获取请求参数,其主要作用是解析HTTP请求的上下文,然后在控制器的方法调用之前转换参数并提供验证的功能,为调用控制器方法做准备。处理器会从HTTP请求中获取数据,然后通过三种接口来进行各类参数转换,这三种接口分别是:
- Converter:普通的转换器,将一中类型转换为另一种类型,例如String->Integer
- Formatter:格式化转换器,用于类似日期,数字等参数格式的转化
- GenericConverter:将HTTP参数转化为数组
在Spring MVC中已经使用注册的机制实现了很多转换器,从而可以在传入String时,将其转换为Integer,Long等类型。并且对于上述接口,Spring MVC提供了服务机制(ConversionService接口)去管理,Converter,Formatter和GenericConverter可以通过注册机接口进行注册,这样处理器就可以获取对应的转换器来实现参数的转换,而这些过程是由Spring为我们完成的。因此,对于开发者而言,只需要自定义Converter,Formatter或GenericConverter接口,并将其注入到Spring IoC容器中就可以了。
实现字符串User转换为User对象的自定义参数转换器
下面使用Converter接口,实现将自定义的字符串{id}-{userName}-{note}参数转换为User对象的自定义转换器。首先看一下Converter接口中的内容:
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S var1);
}
只包含一个convert方法,下面实现上述的自定义转换器:
@Component
public class StringToUserConverter implements Converter<String, User> {
@Override
public User convert(String userStr) {
String userName = null;
Integer sex = null;
String note = null;
String[] strArr = userStr.split("-");
if (strArr.length == 3) {
userName = strArr[0];
if (!strArr[1].equals("")) {
sex = Integer.parseInt(strArr[1]);
}
note = strArr[2];
} else if (strArr.length == 2) {
userName = strArr[0];
if (!strArr[1].equals("")) {
sex = Integer.parseInt(strArr[1]);
}
}
User user = new User(userName, sex, note);
return user;
}
}
上述代码中使用@Component注解将实现的转化器装配到IoC容器中,这样Spring Boot就可以在初始化是将这里的转换器自动加入到转化注册列表中,用于参数的处理。使用下面的URL进行测试:
http://localhost:8080/param/converter?user=username1-2-thisisanote
服务端代码:
/**
* 自定义参数格式传递:cn.zyt.springbootlearning.component.StringToUserConverter
*/
@GetMapping("/converter")
@ResponseBody
public User getUserByConverter(User user) {
return user;
}
返回结果:
使用自定义的集合和数组转换器GenericConverter
借助上述定义的StringToUserConverter转换器,数组转换器GenericConverter可以实现多个用户字符串的转化,服务端代码如下:
/**
* 借助自定义参数传递,进行批量自定义参数传递
* 实现GenericConverter数组转化器
*/
@GetMapping("/list")
@ResponseBody
public List<User> getUserListByConverter(List<User> userList) {
return userList;
}
请求路径为:
http://localhost:8080/param/requestArray?intArr=10,11,12&longArr=20,30,40&strArr=yitian1,yitian2
返回结果为:
Spring Boot中的参数验证
处理器在将参数进行转换之后,会对需要验证的参数进行合法性的验证,Spring MVC也提供了参数验证的机制。一方面可以使用JSR-303注解验证,在默认情况下Spring Boot会引入关于Hibernate Validator机制来支持JSR-303参数验证规范。另一方面,当验证逻辑比较复杂时,可以自定义验证规范。
通过注解使用Validator验证机制
首先创建一个用于参数传递的POJO:
@Data
public class ValidatorPojo {
@NotNull(message = "id不能为空")
private Long id;
@NotNull
@Future(message = "需要一个未来的日期")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date date;
@NotNull
@DecimalMin(value = "0.1")
@DecimalMax(value = "10000.00")
private Double doubleValue;
@NotNull
@Min(value = 1, message = "最小值为1")
@Max(value = 88, message = "最大值为88")
private Integer integer;
@Range(min = 1, max = 888, message = "取值范围为1到888")
private Long range;
@Email(message = "邮箱格式错误")
private String email;
@Size(min = 20, max = 30, message = "字符串长度要求为[20, 30]")
private String size;
}
对于对象中的每一个属性的验证信息都已经使用注解进行了标注,并设置了相应的提示信息。当某个字段的参数值没有满足该验证规则时,即会返回相应的提示信息。下面创建一个简单的POST请求来测试参数的验证,带有POST请求的页面为:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Register</title>
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<script src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
$(document).ready(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader(header, token);
});
$("#valid").click(function () {
var pojo = {
id: null,
date: '2020-01-30',
doubleValue: 100.09,
integer: 100,
range: 1000,
email: 'email',
size: "adv2323",
regexp: 'a,b,c,d'
}
$.post({
url: './validate',
contentType: 'application/json',
data: JSON.stringify(pojo),
success: function (result) {
}
});
});
});
</script>
</head>
<body>
<p>This page for validating params.</p><br>
<input id="valid" name="valid" type="button" value="Click for Vaild"><br>
<div><a href="http://localhost:8080/web/index">返回首页</a></div>
</body>
</html>
服务端方法为:
/**
* POST请求验证参数,返回验证信息
*/
@RequestMapping("/valid/validate")
@ResponseBody
public Map<String, Object> validate(@Valid @RequestBody ValidatorPojo validatorPojo, Errors errors) {
Map<String, Object> errMap = new HashMap<>();
// 获取错误信息列表
List<ObjectError> oes = errors.getAllErrors();
for (ObjectError oe : oes) {
String key = null;
String msg = null;
// 字段错误
if (oe instanceof FieldError) {
FieldError fe = (FieldError) oe;
key = fe.getField(); // 获取错误验证字段名
} else {
// 非字段错误,获取验证对象名称
key = oe.getObjectName();
}
// 错误信息
msg = oe.getDefaultMessage();
errMap.put(key, msg);
}
System.out.println(errMap);
return errMap;
}
代码中使用@RequestBody代表接收一个JSON参数,这样Controller就可以获取通过Ajax提交的JSON请求体。然后使用@Valid注解表示启动参数验证,这样Spring就会使用JSR-303验证机制对参数进行验证,验证的规则即是上述POJO中使用注解对每个字段进行标注的规则。在验证结束后,错误信息会放到Errors对象中,通过判断并遍历该对象中的错误信息,将错误信息返回。
点击页面中的验证链接,得到的返回结果为:
自定义参数验证机制
除了上述的默认提供的验证注解外,Spring提供了自定义的验证器的实现和注册机制。在Spring MVC中,WebDataBinder不仅注册参数转换器外,还允许注册验证器。
为了自定义验证器,可以实现如下的Spring提供的Validator接口,该接口中的内容如下:
public interface Validator {
boolean supports(Class<?> var1);
void validate(Object var1, Errors var2);
}
其中supports方法参数为需要验证的POJO类型,如果该方法返回true,则表示Spring会使用当前的验证器的validate方法对其进行验证。validate方法中包含了待验证的var1对象,和保存错误信息的Errors对象。在该方法中就可以自定义对待验证对象的验证逻辑,并将验证后的错误信息保存到Errors对象中。下面实现一个对User对象进行验证的自定义验证器:
public class UserValidator implements Validator {
/**
* 这里的自定义验证器仅针对User对象进行验证
*/
@Override
public boolean supports(Class<?> aClass) {
return aClass.equals(User.class);
}
/**
* 验证逻辑
*/
@Override
public void validate(Object o, Errors errors) {
if (o == null) {
errors.rejectValue("", null, "用户不能为空");
return;
}
User user = (User) o;
if (StringUtils.isEmpty(user.getUserName())) {
errors.rejectValue("userName", null, "userName不能为空");
}
}
}
上述的验证器中,supports方法指定了仅对User类型的参数对象进行验证,validate方法中对user对象本身已经其中的userName字段进行了验证。
在上面定义完成了Validator之后,还需要将其注册到WebDataBinder中。Spring MVC提供了一个注解@InitBinder,,它可以在执行控制器方法之前执行该注解下的方法,修改WebDataBinder并绑定自定义的验证器。下面将上述定义的UseValidator验证器进行绑定:
@InitBinder
public void initBinder(WebDataBinder binder) {
// 添加自定义的用户验证器
binder.addValidators(new UserValidator());
// 定义日期参数格式,参数不在需要注解@DateTimeFormat, boolean参数表示是否允许为空
binder.registerCustomEditor(Date.class,
new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
}
其中,使用addValidators方法新增了自定义的验证器,此外该方法与setValidator方法的区别在于:
用setValidator()方法设置验证器,原来的JSR-303校验失效仅使用自定义验证器
用addValidators()方法,原来的JSR-303校验依旧有效且自定义校验也有效(推荐)
下面使用两个URL和两个服务端方法来进行测试(下面方法依赖于上述StringToUserConverter参数转换器,具体参考上述代码)。
1. 验证自定义验证器的使用
服务端代码:
/**
* 使用自定义用户验证器,无需进行DATE格式的验证
*
* @param user User对象使用StringToUserConverter(自定义的转换器)进行自动转换
* @param errors 验证器返回的错误信息
* @param date 因为WebDataBinder已经绑定的了Date的格式,因此这里不在需要@DateTimeFormat注解
* @return 验证错误信息
*/
@RequestMapping(value = "/valid/user-validator", method = RequestMethod.GET)
@ResponseBody
public Map<String, Object> validator(@Valid User user, Errors errors, Date date) {
Map<String, Object> map = new HashMap<>();
map.put("user", user);
map.put("date", date);
if (errors.hasErrors()) {
List<ObjectError> oes = errors.getAllErrors();
for (ObjectError error : oes) {
if (error instanceof FieldError) {
FieldError fe = (FieldError) error;
map.put(fe.getField(), fe.getDefaultMessage());
} else {
map.put(error.getObjectName(), error.getDefaultMessage());
}
}
}
return map;
}
请求URL为:
http://localhost:8080/param/valid/user-validator?user=-1-note1&date=2020-02-01
返回结果为:
2. 验证加入自定义验证器后,原有的JSR-303验证规则
首先看一下User类中设置的验证规则:
@Data
@Alias(value = "user")
public class User implements Serializable {
private Long id;
@NotNull(message = "用户名不能为空")
private String userName;
@NotNull(message = "备注不能为空")
private String note;
@NotNull(message = "性别不能为空")
private SexEnum sex;
}
该测试的服务端方法为:
@RequestMapping("/valid/user")
@ResponseBody
public Map<String, Object> validator(@Valid User user, Errors errors) {
Map<String, Object> resultMap = new HashMap<>();
if (errors.hasErrors()) {
List<ObjectError> oes = errors.getAllErrors();
for (ObjectError error : oes) {
if (error instanceof FieldError) {
FieldError fe = (FieldError) error;
resultMap.put(fe.getField(), fe.getDefaultMessage());
} else {
resultMap.put(error.getObjectName(), error.getDefaultMessage());
}
}
}
return resultMap;
}
请求URL为:
http://localhost:8080/param/valid/user?user=-1-
可以得到返回值如下:
可以看到在使用addValidators方法时,自定义验证器和原有的JSR-303验证规则都可以生效。