Fork me on GitHub

SpringMVC学习8-Validation和Log

Spring MVC

SpringMVC是基于MVC思想的JAVA WEB实现框架,是Spring家族的一员,它基于前置控制器来接收并分发请求,支持参考验证、请求参数封装、拦截、Restful等功能,是目前较为流行的MVC框架

本系列学习笔记包含如下的课程内容:

  • MVC思想
  • Hello案例
  • 请求和响应处理
  • 文件上传和下载处理
  • 参数验证
  • 请求拦截
  • RESTful风格
  • 日志

Bean Validation

在web项目开发的过程中,有相当多的请求是带有参数的,而这些参数需要进行各种的校验.
比如:

- 非空 -> 该填的有没有填
- 长度 -> 填了的是否超出字段上线.
- 格式 -> 邮箱格式,手机格式,身份证等.
- 有效区间段
- 唯一性 -> 邮箱格式,手机格式,身份证只能登记一次

典型的误区

初学者可能以为在 页面/前端 使用javascript 校验过了,那么在服务器端就无需校验了.

原因:
浏览器端有非常多的手段/方式来屏蔽/禁用 js 脚本.这样一来,校验的代码/逻辑再多也是白写.

原则:

  1. 所有来自客户端的请求都是不可信的,服务器端必须做所有的参数校验.
  2. 浏览器端有条件还是要写校验的逻辑,因为可以减少服务器端接收无效请求的概率.

传统的写法

写起来代码量很大,且效率低下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String name = user.getName();
if(name == null || name.trim().equals("") ){
errors.put("name", "name不能为空!");
}else{
if(name.trim().length()>20 ){
errors.put("name", "name长度不能超过20!");
}else{
if(name 不满足某个格式){
errors.put("name", "name格式不正确!");
}else{
if(userService.getByName(name.trim()) != null){
errors.put("name", "用户名已被使用!");
}
}
}
}

  • JSR-303 是JAVA EE 6 中的一项子规范,称为Bean Validation,官方参考实现是Hibernate Validator.
  • 此实现与Hibernate ORM 没有任何关系, JSR303 用于对Java Bean 的属性值值进行验证。
  • Spring MVC 支持 JSR-303,可以在控制器中对表单提交的数据方便地验证。

使用步骤

配置 pom.xml

1
2
3
4
5
6
<!--JSR303-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.3.0.Final</version>
</dependency>

配置 MessageSource

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
@Configuration
@ComponentScan
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Bean
public MessageSource validationMessageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
//字符编码
messageSource.setDefaultEncoding("UTF-8");
//Set an array of basenames
messageSource.setBasenames("validationMessages");
//whether to use the message code as default message
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}

@Override
public Validator getValidator() {
LocalValidatorFactoryBean lvfb = new LocalValidatorFactoryBean();
lvfb.setValidationMessageSource(validationMessageSource());
return lvfb;
}

}

准备校验逻辑

  • 彻底理解需求
  • 整理请求参数列表
  • 整理校验逻辑

准备资源文件

基于 i18n 国际化技术,需要提供多语言环境下的各种资源文件

validationMessages_en.properties
1
2
3
user.name.invalid=Username must be between 6 and 12
user.password.invalid=The password must be between 6 and 12
user.age.invalid=Age must be between 18 and 80.
validationMessages_zh.properties
1
2
3
user.name.invalid=用户名必须在6到12之间
user.password.invalid=密码必须在6到12之间
user.age.invalid=年龄必须在18到200之间

定义校验封装类

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
48
49
50
51
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.GroupSequence;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
* User Data Transfer Object
*/
public class UserDto {

@NotNull
@Size(min = 6, max = 12, message = "{user.name.invalid}")
private String name;

@NotNull
@Size(min = 6, max = 12, message = "{user.password.invalid}")
private String password;

@Min(value = 18, message = "{user.age.invalid}")
@Max(value = 80, message = "{user.age.invalid}")
private int age;

public UserDto() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

控制器

1
2
3
4
5
6
7
8
9
10
@RequestMapping(value = "add", method = RequestMethod.POST)
public String test(Model model, @Valid UserDto user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
model.addAttribute("user", user);
model.addAttribute("errors", getErrorMap(bindingResult));
return "userAdd";
}
model.addAttribute("user", user);
return "result";
}

自定义转换方法getErrorMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 将参数错误信息封装到 map
*/
private Object getErrorMap(BindingResult bindingResult) {
Map<String, String> errorMap = new HashMap<>();
for (ObjectError objectError : bindingResult.getAllErrors()) {
FieldError fieldError = (FieldError) objectError;
String field = fieldError.getField();
String error = fieldError.getDefaultMessage();
errorMap.put(field, error);
}
return errorMap;
}

页面

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
48
49
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" language="java" %>
<html>
<head>
<title>Bean Validation (JSR303)</title>
</head>
<body>
<h1>Bean Validation (JSR303)</h1>
<hr/>
<p>演示提交一个User对象</p>

<form id="user" action="${path}/demo/add" method="post">
<table width="30%">
<tr>
<th>Name:</th>
<td>
<input id="name" name="name" type="text" value="${user.name}"/>
</td>
<td>
<span>${errors.name}</span>
</td>
</tr>
<tr>
<th>Password:</th>
<td>
<input id="password" name="password" type="password" value="${user.password}"/>
</td>
<td>
<span>${errors.password}</span>
</td>
</tr>
<tr>
<th>Age:</th>
<td>
<input id="age" name="age" type="text" value="${user.age}"/>
</td>
<td>
<span>${errors.age}</span>
</td>
</tr>
<tr>
<td style="text-align: center">
<input type="submit" value="提交">
</td>
</tr>
</table>
</form>

</body>
</html>

FAQ

要基于数据库的查询来校验

思路1:自定义注解类

详见官网文档,写起来较复杂,不推荐新人使用.

思路2:使用代码访问数据库进行校验

写起来简单易读,适合新人使用,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping(value = "/register", method = RequestMethod.POST)
//@Valid : 在封装之后,在使用之前,根据 JSR303 注解进行校验,并产生错误信息
public String register(@Valid User user,
BindingResult result, Model model) {
if(result.hasErrors()){
System.out.println("发现错误信息");
model.addAttribute("errors", getErrorMap(result));
return "/index.jsp";
}else{
//基于数据库的查询...
boolean flag = false;
if(flag){
// /如果校验不过
model.addAttribute("errors", getErrorMap(result));
return "/index.jsp";
}else{
// //如果校验通过
System.out.println("校验通过");
userService.save(user);
model.addAttribute("user", user);
return "/WEB-INF/jsp/result.jsp";
}
}
}

附录

JSR303提供

注解类 说明
@Null 必须为 null
@NotNull 必须不为 null
@AssertTrue 必须为 true
@AssertFalse 必须为 false
@Min(value) 必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 大小必须在指定的范围内
@Digits (integer, fraction) 必须是一个数字,其值必须在可接受的范围内
@Past 必须是一个过去的日期
@Future 必须是一个将来的日期
@Pattern(value) 必须符合指定的正则表达式

Hibernate Validator提供

注解类 说明
@Email 必须是电子邮箱地址
@Length 字符串的长度必须在指定的范围内
@NotEmpty 必须非空(有空格就不算Empty)
@NotBlank 字符串的必须非空(单纯空格算Blank)
@Range 必须在指定范围内

Spring MVC提供

注解类 说明
@Valid 数据需要校验

Log4j

在练习期间可以使用System.out.println()的方法,但是在实际项目的开发过程中,需要使用日志技术

pom.xml

1
2
3
4
5
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

log4.properties

1
2
3
4
5
6
7
8
9
10
#log level
log4j.rootLogger=info,stdout
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.EnhancedPatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %p %c:%L - %m%n
#log4j.logger.org.hibernate=warn
#log4j.logger.org.springframework=warn
#log4j.logger.com.tz=debug

使用日志

1
2
3
4
5
6
7
8
9
private static final Logger LOGGER = Logger.getLogger(AdminController.class);

@RequestMapping("/work1")
public String work1(Model model) {
LOGGER.debug("AdminController work1()...");
String result = "work 1";
model.addAttribute("result", result);
return "/WEB-INF/jsp/hello.jsp";
}