业务背景:日常工作中,我们开发接口时,一般都会涉及到参数校验、异常处理、封装结果返回等处理。而我们项目有时为了快速迭代,在这方面上有所疏忽,后续导致代码维护比较难,不同的开发人员的不同习惯,各式各样的接口返回,所以专门进行了一次业务整改,统一规范专项
如果每个后端开发在参数校验、异常处理等都是各写各的,没有统一处理的话,代码就不优雅,也不容易维护。所以,作为一名合格的后端开发工程师,我们需要统一校验参数,统一异常处理、统一结果返回,让代码更加规范、可读性更强、更容易维护。
作为后端开发,我们项目的响应结果,需要统一标准的返回格式。一般一个标准的响应报文对象,有一下几个属性
1)响应状态码一般用枚举表示:
包结构: com.xxx.common.core.domain.ResultCode
/**
* 响应状态码
**/
public enum ResultCode {
/**操作成功**/
SUCCESS("200","操作成功"),
/**操作失败**/
ERROR("500","操作失败"),;
/**
* 自定义状态码
**/
private String code;
/**自定义描述**/
private String message;
ResultCode(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
2)因为返回的数据类型不是确定的,我们可以使用泛型,如下:
包结构:com.xxx.common.core.domain.BaseResponse
/**
* 响应结果封装对象
**/
public class BaseResponse implements Serializable {
private static final long serialVersionUID = 1901152752394073986L;
/**
* 响应状态码
*/
private String code;
/**
* 响应结果描述
*/
private String message;
/**
* 返回的数据
*/
private T data;
/**
* 成功返回
* @param data
* @return: com.msb.hjycommunity.common.core.domain.BaseResponse
*/
public static BaseResponse success(T data){
BaseResponse response = new BaseResponse<>();
response.setCode(ResultCode.SUCCESS.getCode());
response.setMessage(ResultCode.SUCCESS.getMessage());
response.setData(data);
return response;
}
/**
* 失败返回
* @param message
* @return: com.msb.hjycommunity.common.core.domain.BaseResponse
*/
public static BaseResponse fail(String message){
BaseResponse response = new BaseResponse<>();
response.setCode(ResultCode.ERROR.getCode());
response.setMessage(message);
return response;
}
/**
* 失败返回
* @param message
* @return: com.msb.hjycommunity.common.core.domain.BaseResponse
*/
public static BaseResponse fail(String code, String message){
BaseResponse response = new BaseResponse<>();
response.setCode(code);
response.setMessage(message);
return response;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
3)测试
public class User {
private String userId;
private String username;
public User() {
}
public User(String userId, String username) {
this.userId = userId;
this.username = username;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/queryUserById")
public BaseResponse queryUserById(String userId){
if(userId != null){
return BaseResponse.success(new User(userId,"spike"));
}else{
return BaseResponse.fail("查询用户信息失败!");
}
}
}
http://localhost:8888/user/queryUserById?userId=1
//output
{
"code": "200",
"message": "操作成功",
"data": {
"userId": "1",
"username": "tom"
}
}
http://localhost:8888/user/queryUserById
//output
{
"code": "500",
"message": "查询用户信息失败!",
"data": null
}
我们在实际的开发过程中经常会遇到需要对参数进行校验的情况,比如在需要用户输入手机号的时候他是不是真的输入了一个合法的手机号,在需要用户输入一个邮箱的时候他是不是真的输入了一个合法的邮箱,用户输入的内容是不是超出了长度限制等等。
当一个表单的数据较多的时候,单纯的数据校验代码就会占到很大的幅度,所以简化基础数据的校验可以省去我们很多的工作,让我们能更专注于功能的实现。
接下来我们一起看一下 springboot中参数校验(validation)的使用,首先导入依赖
org.springframework.boot
spring-boot-starter-validation
Validator可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:
public class User {
private String userId;
@NotNull(message = "username 不能为空")
private String username;
}
校验规则和错误提示信息配置完毕后,接下来只需要在接口需要校验的参数上加上@Validated注解,并添加BindResult参数即可方便完成验证:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/addUser")
public BaseResponse addUser(@Validated User user, BindingResult bindingResult){
List fieldErrors = bindingResult.getFieldErrors();
//如果参数校验失败,会将错误信息封装成对象组装在 BindingResult
if(!fieldErrors.isEmpty()){
return BaseResponse.fail(fieldErrors.get(0).getDefaultMessage());
}
return BaseResponse.success("OK");
}
}
测试
http://localhost:8888/user/addUser?username="tom"
{
"code": "500",
"message": "userId 不能为空",
"data": null
}
http://localhost:8888/user/addUser?username="tom"&userId=1
{
"code": "200",
"message": "操作成功",
"data": "OK"
}
内置的校验注解有很多,罗列如下:
日常开发中,我们一般都是自定义统一的异常类
- 自定义异常可以携带更多的信息。
- 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
- 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
自定义异常类, 所在包结构: com.xxx.common.core.exception.BaseException
/**
* 基础异常
**/
public class BaseException extends RuntimeException{
/**
* 错误码
*/
private String code;
/**
* 错误消息
*/
private String defaultMessage;
public BaseException() {
}
public BaseException(String code, String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}
public String getCode() {
return code;
}
public String getDefaultMessage() {
return defaultMessage;
}
}
在controller 层,我们来使用这个自定义异常:
@RequestMapping("/queryUser")
public BaseResponse queryUser(User user){
//模拟查询失败抛出异常
throw new BaseException("500","测试异常类!");
}
这块代码,没什么问题,但是如果在很多地方都抛出这个异常,我们处理起来就会比较麻烦。
这时我们可以借助注解@RestControllerAdvice,让代码更优雅。@RestControllerAdvice是一个应用于Controller层的切面注解,它一般配合@ExceptionHandler注解一起使用,作为项目的全局异常处理。示例代码如下:
/**
* 全局异常处理器
**/
@RestControllerAdvice
public class GlobalExceptionHandler {
}
我们有想要拦截的异常类型,比如想拦截BaseException类型,就新增一个方法,使用@ExceptionHandler注解修饰,并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 基础异常
* @param e
* @return: com.xxx.common.core.domain.BaseResponse
*/
@ExceptionHandler(BaseException.class)
@ResponseBody
public BaseResponse baseExceptionHandler(BaseException e){
return BaseResponse.fail(e.getDefaultMessage());
}
}
测试
http://localhost:8888/hejiayun/user/queryUser
{
"code": "500",
"message": "测试异常类!",
"data": null
}
总结:整改的几个维度,但这里需要注意的是,全局异常设计,在个人看来并不一定是有效的,有时会封装过简单,还不易于排查代码问题。