SpringMVC详解(三)
数据验证
前面在处理器逻辑中谈到了参数的转换,转换参数出来之后,紧跟着的往往需要验证参数的合法性,因此SpringMVC也提供了验证参数的机制。
一方面,它可以支持JSR-303注解验证,在默认的情况下SpringBoot会引入关于HibernateValidator机制来支持JSR-303验证规范;另外一方面,因为业务会比较复杂,所以需要自定义验证规则。
SpringMVC提供了相关的验证机制:
JSR-303验证
JSR-303验证主要是通过注解的方式进行的。这里先定义一个需要验证的POJO,此时需要在其属性中加入相关的注解:
验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Data public class ValidatorPojo {
@NotNull(message = "id不能为空") private Long id;
@Future(message = "需要一个将来日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @NotNull private Date date;
@NotNull @DecimalMin(value = "0.1") @DecimalMax(value = "10000.00") private Double doubleValue = null;
@Min(value = 1, message = "最小值为1") @Max(value = 88, message = "最大值为88") @NotNull 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; }
|
类中的属性带着各种各样验证注解,并且代码己经在注释中说明其作用,JSR-303验证就是通过这些注解来执行验证的。
测试
为此需要一个页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>测试JSR-303</title> <script src="https://code.jquery.com/jquery-3.2.0.js"></script> <script type="text/javascript"> $(document).ready(function() { var pojo = { id : null, date : '2017-08-08', doubleValue : 999999.09, integer : 100, range : 1000, email : 'email', size : 'adv1212', regexp : 'a,b,c,d' } $.post({ url : "./validate", contentType : "application/json", data : JSON.stringify(pojo), success : function(result) { } }); }); </script> </head> <body></body> </html>
|
再需要一控制器去响应这个Ajax请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @GetMapping("/page") public String validPage() { return "pojo"; }
@RequestMapping(value = "/validate") @ResponseBody public Map<String, Object> validate( @Valid @RequestBody ValidatorPojo vp, 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); } return errMap; }
|
代码中使用@RequestBody代表着接收一个JSON参数,这样Spring就会获页面通过Ajax提交的JSON请求体,然后@Valid注解则表示启动验证机,这样Spring就会启用JSR-303验证机制进行验证。它会自动地将最后的验证结果放入Errors对象中,这样就可以从中得到相验证过后的信息。
在浏览器输入:http://localhost:8888/my/page
结果如图:
显然这里的验证成功了。但是有时验证规则并不是那么简单,比如一些业务逻辑的验证。例如,假设需要验证购买商品的总价格,那么就应该是:总价格=单价×数量,这样的逻辑验证就不能通过JSR-303验证了。为此Spring还提供了自己的验证机制,下面来介绍它。
参数验证机制
为了能够更加灵活地提供验证机制,Spring还提供自己的验证机制。在参数转换时,可以看到在SpringMVC中,存在WebDataBinder机制进行管理,在默认的情况下Spring会自动地根据上下文通过注册了的转换器转换出控制器所需的参数。在WebDataBinder中除了可以注册转换器外,还允许注册验证器(Validator)。
在Spring控制器中,它还允许使用注解@InitBinder,这个注解的作用是允许在进入控制器方法前修改WebDataBinder机制。
下面在验证机制和日期格式绑定的场景下演示,不过在此之前,需要稍微认识一下SpringMVC的验证机制。
Validator源码
1 2 3 4 5 6 7 8 9 10 11
| public interface Validator{
booleansupports(Class<?>clazz);
Void validate(Object target,Errors errors); }
|
这就是Spring所定义的验证器接口,它定义了两个方法,其中supports方法参数为需要验证的POJO类型,如果该方法返回true,则Spring会使用当前验证器的validation方法去验证POJO。
而validation方法包含需要的target对象和l错误对象errors,其中target是参数绑定后的POJO,这样便可以通过这个参数对象进行业务逻辑的自定义验证。如果发现错误,则可以保存到errors对象中,然后返回给控制器。
下面以实例进行说明:
测试
自定义用户验证器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class UserValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return clazz.equals(User.class); }
@Override public void validate(Object target, Errors errors) { if (target == null) { errors.rejectValue("", null, "用户不能为空"); return; } User user = (User) target; if (StringUtils.isEmpty(user.getUserName())) { errors.rejectValue("userName", null, "用户名不能为空"); } } }
|
有了这个验证器,Spring还不会自动启用它,因为还没有绑定给WebDataBinder机制。在SpringMVC中提供了一个注解@TnitBinder,它的作用是在执行控制器方法前,处理器会先执行表@lnitBinder标注的方法。
这时可以将WebDataBinder对象作为参数传递到方法中,通过这层关系得到WebDataBinder对象,这个对象有一个setValidator方法,它可以绑定自定义的验证器,这样就可以在获取参数之后,通过自定义的验证器去验证参数,只是WebDataBinder除了可以绑定验证器外,还可以进行参数的自定义,例如,不使用@DateTirneFormat获取日期参数。假设还继续使用StringToUserConverter转换器,再来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
@InitBinder public void initBinder(WebDataBinder binder) { binder.setValidator(new UserValidator()); binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false)); }
@GetMapping("/validator") @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 oe : oes) { if (oe instanceof FieldError) { FieldError fe = (FieldError) oe; map.put(fe.getField(), fe.getDefaultMessage()); } else { map.put(oe.getObjectName(), oe.getDefaultMessage()); } } } return map; }
|
里的initBinder方法因为标注注解@lnitBinder,因此会在控制器方法前被执行,并且将WebDataBinder对象传递进去,在这个方法里绑定了自定义的验证器UserValidator,而且设置了日期的格式,所以在控制器方法中己经不再需要使用@DateTimeForrnat去定义日期格式化。
通过这样的自定义,在使用注解@Valid标注User参数后,SpringMVC就会去遍历对应的验证器,当遍历到UserValidator时,会去执行它的supports方法。因为该方法会返回true,所以SpringMVC会用这个验证器去验证User类的数据。对于日期类型也指定了对应的格式,这样控制器的Date类型的参数也不需要再使用注解的协作。
这里还要关注一下控制器方法中的Errors参数。它是SpringMVC通过验证器验证后得到的错误信息,由SpringMVC执行完验证规则后进行传递。这里首先是判断是否存在错误,如果存在错误,则遍历错误,然后将错误信息放入Map中返回,因为方法标注了@ResponseBody,所以最后会转化为ISON响应请求。
下面输http://localhost:8888/user/validator?user=1--note_1&date=2018-01-01。
请注意,这里的userNam巳已经传递为空,所以在进行用户验证时会存在错误信息的显示这个请求的结果截图如图:
可以看到,用户名的验证己经成功,也就是说验证器己经起到作用,而且日期也是成功的,它返回了一个日期的Long型整数(时间参数与1970-01-0100:00:00之间的毫秒数)。
数据模型
上面只是谈到了参数的获取和转换,通过这些处理器终于可以调用控制器了。在SpringMVC流程中,控制器是业务逻辑核心内容,而控制器的核心内容之一就是对数据的处理。
可以得知的是:允许控制器自定义模型和视图(ModelAndView),其中模型是存放数据的地方,视图则是展示给用户。
这里暂时把视图放下,先来讨论数据模型的问题。数据模型的作用是绑定数据,为后面的视图渲染做准备。首先对SpringMVC使用的模型接口和类设计进行探讨,如图所示:
从图可以看到,在类ModelAndView中存在一个Mode!Map类型的属性,ModelMap继承了LinkedHashmap类,所以它具备Map接口的一切特性,除此之外它还可以增加数据属性。
在SpringMVC的应用中,如果在控制器方法的参数中使用ModelAndView、Model或者Mode!Map作为参数类型,SpringMVC会自动创建数据模型对象:
使用数据模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @RequestMapping("/data") @Controller public class DataModelController { @Autowired private UserService userService = null; @GetMapping("/model") public String useModel(Long id, Model model) { User user = userService.getUser(id); model.addAttribute("user", user); return "user"; } @GetMapping("/modelMap") public ModelAndView useModelMap(Long id, ModelMap modelMap) { User user = userService.getUser(id); ModelAndView mv = new ModelAndView(); mv.setViewName("user"); modelMap.put("user", user); return mv; } @GetMapping("/mav") public ModelAndView useModelAndView(Long id, ModelAndView mv) { User user = userService.getUser(id); mv.addObject("user", user); mv.setViewName("user"); return mv; } }
|
这段代码中可以看出SpringMVC还是比较智能的。例如,useModel方法里,只是返回一个字符串,SpringMVC会自动生成对应的视图,并且绑定数据模型。又如,useModelMap方法,返回了ModelAndView对象,但是它没有绑定ModelMap对象,SpringMVC又会自动地绑定它。
测试
首先需要一个视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>用户信息</title> </head> <body> <table> <tr> <td>编号</td> <td th:text="${user.id}">id</td> </tr> <tr> <td>用户名</td> <td th:text="${user.userName}">userName</td> </tr> <tr> <td>备注</td> <td th:text="${user.note}">note</td> </tr> </table> </body> </html>
|
结果图: