前言
参数校验其实是开发中一个很重要的点,而做参数校验的方法也是千差万别。
当然,作为开发者,我们肯定希望参数校验的过程越简单越好,不到万不得已肯定是不会一个一个手写参数校验的。
参数校验失败也是需要给出提示的,而参数校验失败的提示大多都比较相似,过程也很像,如果有全局处理方法,肯定会大大减小工作量和出现bug的可能。
关于参数校验的需求以及解决方案
对于参数校验,我们可以提出以下需求:
- 参数校验最好可以自动化进行,没有特殊情况不需要手写参数校验。
- 参数校验失败的异常需要统一的处理方法。
解决方案也很简单:
- 自动化参数校验可以使用
java.validation
包下的一些注解来进行,这样既可靠,又不会太麻烦。 - 参数校验失败的全局异常处理可以使用
@ControllerAdvice
注解和@ExceptionHandler
注解进行。
参数校验
以User
实体类为例:
/**
* 用户实体类
*
* @author jiangwen
*/
@Data
@NoArgsConstructor
@Accessors(chain = true)
@ToString
@Entity
@Table(name = "wendev_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
@NotNull(message = "用户名不能为空")
@NotEmpty(message = "用户名不能为空字符串")
@NotBlank(message = "用户名不能为空")
private String username;
@Column
@NotNull(message = "用户昵称不能为空")
@NotEmpty(message = "用户昵称不能为空字符串")
@NotBlank(message = "用户昵称不能为空")
private String nickname;
@Column
@NotNull(message = "邮箱不能为空")
@NotEmpty(message = "邮箱不能为空字符串")
private String email;
@Column
@NotNull(message = "密码不能为空")
@NotEmpty(message = "密码不能为空字符串")
private String password;
/**
* 用户权限
* 这个字段是为了以后可能对接Spring Security保留的
*/
@Column
@NotNull(message = "用户角色不能为空")
@NotEmpty(message = "用户角色不能为空字符串")
private String role;
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
@Temporal(TemporalType.TIMESTAMP)
private Date updateTime;
/**
* 该用户所发表的全部评论
*/
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
@Version
private Long version;
}
因为我们的参数校验需求比较简单——非空、非空字符串,所以也就需要两个注解:@NotNull
表示这个属性不能没有,@NotEmpty
表示不可以为空字符串。
用户名和昵称上使用了@NotBlank
注解,这个注解表示调用trim()
方法后不为空,防止有人注册全是空格的用户名和昵称。密码没有调用是因为一句话不说随便把别人密码里的空格弄没了太不厚道了。
除此之外,还有针对数值范围的@Min
,@Max
,@Positive
,@Negative
等注解、表示必须为空的@Null
注解等,都可以用来很方便地规定数值的合法范围,具体可以看文档。
有了这些注解,我们就不用一个一个if地写参数合法性校验了。
而在Controller里接受参数时,加上@Valid
注解就可以校验了。当然最后发现不加其实也可以校验。
那么参数校验失败会发生什么呢?会抛出一个参数校验失败的异常javax.validation.ConstraintViolationException
,内含我们在message
里写明的提示信息,比如:
所以,针对参数校验失败的情况,我们只要写一个统一的方法,把这里面的message
在前端显示出来就可以了。
参数校验异常的统一处理
在Spring Boot
中进行全局异常处理,可以使用一个叫@ControllerAdvice
的注解。这个注解一看就与Controller有关——加上它的类就变成了Controller的增强器,可以做一些很神奇的事情,比如全局数据处理、全局异常处理等。在这里我们就用它做全局异常处理。
当然,全局异常处理自然少不了@ExceptionHandler
注解,可以指定用它处理哪一种异常。
@ControllerAdvice
public class ControllerExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHandler(HttpServletRequest request, Exception e) throws Exception {
logger.error("Request URL: {}; Exception: {}", request.getRequestURL(), e);
e.printStackTrace();
// 如果是指定404、500等状态码的返回页面,就不处理直接把e抛出,交给SpringBoot处理
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
throw e;
}
var mv = new ModelAndView();
mv.addObject("url", request.getRequestURL());
mv.addObject("exception", e);
mv.setViewName("error/error");
return mv;
}
@ExceptionHandler(ConstraintViolationException.class)
public ModelAndView validationExceptionHandler(HttpServletRequest request,
ConstraintViolationException e,
RedirectAttributes attributes) {
var mv = new ModelAndView("redirect:" + request.getRequestURI());
attributes.addFlashAttribute("errInfo", e.getConstraintViolations().iterator().next().getMessage());
return mv;
}
}
第一个方法是用来处理所有异常的——出现异常后重定向到一个专门的错误页面并显示提示信息。用来做参数校验异常处理的是第二个方法。
处理逻辑其实很简单:
首先根据用户的请求获取到用户请求的URI,并以此新建一个包含重定向的ModelAndView
,出现参数校验异常时把用户重定向回去;然后给页面添加一个重定向属性:errInfo
,这个是我们在页面中预先放置好的(下面会有页面的代码),把异常信息中的第一条拿出来(e.getConstraintViolations()
返回的是一个HashSet
,后面的方法是把它的第一项取出来,再取出消息。因为参数校验失败的信息可能有多条,但是最好还是一次返回一条,所以就只把第一条拿出来返回,当然返回多条也是可以的,那就变成了对HashSet
的遍历)。
最后,把这个ModelAndView
返回,也就相当于把用户重定向回参数校验失败的页面,并且在页面上显示一条错误消息。这就达到了我们统一处理异常的目的。
当然,这样写有一个约定条件:页面上需要有一个标签来显示错误信息,且错误信息的名称必须是errInfo
。这也是约定大于配置这种思想的好处——方便快捷。
页面上显示错误信息的标签如下(Thymeleaf
语法):
<div class="alert alert-danger" role="alert" th:if="${errInfo != null}" th:text="${errInfo}"></div>
这样出现参数校验失败的情况下,就可以很方便地处理并返回错误信息了。
实际效果:
注册页面:
文章类型管理页面:
都是用了上面那一行代码实现的显示错误信息。