前言
项目开发中难免会碰见各种非空
校验,比如用户登录
、用户注册
等。
通常的操作再接收处理的接口中,添加各种if ... if ..
判断,不够优雅,如何才能做一个简洁易懂的接口呢?
环境搭建
本次测试使用的是零配置Springboot
项目。
依赖引入:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.14.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 阿里巴巴json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
配置文件编写:
server.port=80
定义一个数据接受类User
@Data
public class User implements Serializable {
private static final long serialVersionUID = -2460669663541910243L;
@NotNull(message = "用户id不能为空")
private Integer id;
@NotNull(message = "账号不允许为空")
@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
private String account;
@NotNull(message = "密码不允许为空")
@Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
private String password;
}
定义一个拦截器TestController
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.example.vo.User;
@RestController
public class TestController {
private Logger log = LoggerFactory.getLogger(TestController.class);
@RequestMapping(value = "/addUser",method = RequestMethod.POST)
public String addUser(@RequestBody @Valid User user,
BindingResult bindingResult) {
// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
for (ObjectError error : bindingResult.getAllErrors()) {
log.info("--->"+error.getDefaultMessage());
return error.getDefaultMessage();
}
return "6666";
}
}
使用PostMan
发送一个指定的post
请求测试:
localhost/addUser
获取到的结果返回为:
这样再接口请求时(@RequestBody标识为一个json对象),就通过(@Valid注解标识)javax.validation 实现了基本的数据校验操作。
如果出现不符合规范的错误信息(@NotNull、@Size返回的message信息),则会将信息保存至org.springframework.validation.BindingResult对象中返回。
但是,是否想过一个问题:
每次写一个新的接口,都需要给接口中接受对象处标注@Valid,同时增加一个BindingResult类做错误信息返回,过于累赘!
如何才能尽可能将接口开发显得更加简洁优雅美观些呢?
去掉BindingResult
再平时开发过程中,出现异常
时,程序不会继续进行下去,同时也能将异常报错信息返回至请求的客户端。
参照这个思路,可以再将代码进行小小的优化。
去掉 BindingResult对象接受错误信息,请求接口测试:
@RequestMapping(value = "/addUser1",method = RequestMethod.POST)
public String addUser1(@RequestBody @Valid User user) {
return "6666";
}
{
"timestamp": "2020-07-21T08:27:17.793+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Size.user.account",
"Size.account",
"Size.java.lang.String",
"Size"
],
"arguments": [
{
"codes": [
"user.account",
"account"
],
"arguments": null,
"defaultMessage": "account",
"code": "account"
},
11,
6
],
"defaultMessage": "账号长度必须是6-11个字符",
"objectName": "user",
"field": "account",
"rejectedValue": "12345",
"bindingFailure": false,
"code": "Size"
}
],
"message": "Validation failed for object='user'. Error count: 1",
"path": "/addUser1"
}
并且再控制台也出现了一个错误信息提示:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.example.controller.TestController.addUser1(com.example.vo.User): [Field error in object ‘user’ on field ‘account’: rejected value [12345]; codes [Size.user.account,Size.account,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.account,account]; arguments []; default message [account],11,6]; default message [账号长度必须是6-11个字符]] ]
既然是一个异常,参照之前的想法,大胆的构思一种新的方式———全局异常接收回执信息。
使用全局异常
定义一个 RestControllerAdvice(或 ControllerAdvice)
类,对指定的异常信息进行全局监听处理。
import java.util.List;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* spring 3.2中,新增了@ControllerAdvice 注解,<br>
* 可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,<br>
* 并应用到所有@RequestMapping中。
* @author 765199214
*
*/
@RestControllerAdvice
//@ControllerAdvice //根据需要监测的controller上的注解区分使用
public class ExceptionAdvice {
/**
* 针对 MethodArgumentNotValidException 异常,做全局监听处理
* @param me
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public String MethodArgumentNotValidException(MethodArgumentNotValidException me) {
BindingResult bindingResult = me.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
ObjectError objectError = allErrors.get(0);
return objectError.getDefaultMessage();
}
}
再次请求 /addUser1
接口测试:
合适的接口规范
现在的开发,通常采取json风格,携带状态码code、状态信息msg、结果信息data。
比如:参数不符合规范,返回code 10000,msg为提示信息等,修改全局异常返回数据结构。
/**
* 针对 MethodArgumentNotValidException 异常,做全局监听处理
* @param me
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public JSONObject MethodArgumentNotValidException(MethodArgumentNotValidException me) {
log.info("--->com.example.ex.ExceptionAdvice.MethodArgumentNotValidException");
BindingResult bindingResult = me.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
ObjectError objectError = allErrors.get(0);
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 10000);
jsonObject.put("msg", objectError.getDefaultMessage());
jsonObject.put("data", "");
return jsonObject;
}
这样的操作,很简洁明了,也适合前后分离等项目的开发。
国际化规范
上面的案例操作,依旧欠缺开发规范,每种参数的校验提示参数都写在每个对应的类中,并不利于后期的维护更改操作,如何更加优雅的做操作呢?
在 src/main/resources
包中创建一个新的包文件il8n
。并新增一个Messages.properties
文件。其中内容为:
test.msg.id=用户id不能为空
test.msg.account=账号不允许为空
test.msg.account.size=账号长度必须是6-11个字符
test.msg.password=密码不允许为空
test.msg.password.size=密码长度必须是6-16个字符
在配置文件中增加扫描配置操作:
server.port=80
# 多个文件时,采取“,”拼接
spring.messages.basename=il8n/Messages
重新定义一个新的操作对象类 TestVo
,修改其中的message信息。
import java.io.Serializable;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
@Data
public class TestVo implements Serializable {
private static final long serialVersionUID = 7563633519927149180L;
@NotNull(message = "test.msg.id")
private Integer id;
@NotNull(message = "test.msg.account")
@Size(min = 6, max = 11, message = "test.msg.account.size")
private String account;
@NotNull(message = "test.msg.password")
@Size(min = 6, max = 11, message = "test.msg.password.size")
private String password;
}
新建一个读取message文件的抽象类AbstractBaseController
:
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
public abstract class AbstractBaseController {
@Autowired
private MessageSource messageSource;
public String getMessage(String key,String... strings ) {
return this.messageSource.getMessage(key, strings, Locale.getDefault());
}
}
修改全局异常监听类,采取获取message的状态码的方式,读取指定配置文件获取对应的提示信息:
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.alibaba.fastjson.JSONObject;
import com.example.enums.CallBackEnum;
import com.example.vo.ResultVO;
/**
* spring 3.2中,新增了@ControllerAdvice 注解,<br>
* 可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,<br>
* 并应用到所有@RequestMapping中。
* @author 765199214
*
*/
@RestControllerAdvice
//@ControllerAdvice //根据需要监测的controller上的注解区分使用
public class ExceptionAdvice extends AbstractBaseController {
private Logger log = LoggerFactory.getLogger(ExceptionAdvice.class);
/**
* 针对 MethodArgumentNotValidException 异常,做全局监听处理
* @param me
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public JSONObject MethodArgumentNotValidException(MethodArgumentNotValidException me) {
log.info("--->com.example.ex.ExceptionAdvice.MethodArgumentNotValidException");
BindingResult bindingResult = me.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
ObjectError objectError = allErrors.get(0);
String key = objectError.getDefaultMessage();
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 10000);
jsonObject.put("msg", super.getMessage(key, null));
jsonObject.put("data", "");
return jsonObject;
}
}
新建一个测试接口:
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.example.vo.TestVo;
import com.example.vo.User;
@RestController
public class TestController {
private Logger log = LoggerFactory.getLogger(TestController.class);
@RequestMapping(value = "/test",method = RequestMethod.POST)
public String test(@RequestBody @Valid TestVo testVo) {
return "6666";
}
}
请求测试接口:
文章参考资料:
《用SpringBoot手把手教你写出优雅的后端接口》
《@RestControllerAdvice作用及原理》
《@Valid注解是什么》
文章代码下载:
《github下载地址》