⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 捡田螺的小男孩 「捡田螺的小男孩」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

日常工作中,我们开发接口时,一般都会涉及到参数校验、异常处理、封装结果返回等处理。如果每个后端开发在参数校验、异常处理等都是各写各的,没有统一处理的话,代码就不优雅,也不容易维护。所以,作为一名合格的后端开发工程师,我们需要统一校验参数,统一异常处理、统一结果返回,让代码更加规范、可读性更强、更容易维护。

  • 使用注解,优雅进行参数校验
  • 统一结果返回
  • 统一异常处理
  • 唠叨几句

1. 使用注解,统一参数校验

假设小田螺实现一个注册用户的功能,在controller 层,他会先进行校验参数,如下:

@RestController
@RequestMapping
public class UserController {

@RequestMapping("addUser")
public String addUser(UserParam userParam) {

if (StringUtils.isEmpty(userParam.getUserName())) {
return "用户名不能为空";
}
if (StringUtils.isEmpty(userParam.getPhone())) {
return "手机号不能为空";
}
if (userParam.getPhone().length() > 11) {
return "手机号不能超过11";
}
if (StringUtils.isEmpty(userParam.getEmail())) {
return "邮箱不能为空";
}

//省略其他参数校验

//todo 插入用户信息表
return "SUCCESS";
}

}

以上代码有什么问题嘛?其实没什么问题,就是校验有点辣眼睛。正常的添加用户业务还没写,参数校验就一大堆啦。假设后来,小田螺又接了一个需求:编辑用户信息。实现编辑用户信息前,也是先校验信息,如下:

@RequestMapping("editUser")
public String editUser(UserParam userParam) {

if (StringUtils.isEmpty(userParam.getUserName())) {
return "用户名不能为空";
}
if (StringUtils.isEmpty(userParam.getPhone())) {
return "手机号不能为空";
}
if (userParam.getPhone().length() > 11) {
return "手机号不能超过11";
}

if (StringUtils.isEmpty(userParam.getEmail())) {
return "邮箱不能为空";
}

//省略其他参数校验

//todo 编辑用户信息表
return "SUCCESS";
}

我们可以使用注解的方式,来进行参数校验,这样代码更加简洁,也方便统一管理。实际上, spring boot有个validation的组件,我们可以拿来即用。引入这个包即可:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

引入包后,参数校验就非常简洁啦,如下:

public class UserParam {

@NotNull(message = "用户名不能为空")
private String userName;

@NotNull(message = "手机号不能为空")
@Max(value = 11)
private String phone;

@NotNull(message = "邮箱不能为空")
private String email;

然后在UserParam参数对象中,加入@Validated注解哈,把错误信息接收到BindingResult对象,代码如下:

@RequestMapping("addUser")
public String addUser(@Validated UserParam userParam, BindingResult result) {

List<FieldError> fieldErrors = result.getFieldErrors();
if (!fieldErrors.isEmpty()) {
return fieldErrors.get(0).getDefaultMessage();
}

//todo 插入用户信息表
return "SUCCESS";
}

2. 接口统一响应对象返回

如果你在你们项目代码中,看到controller 层报文返回结果,有这样的:

@RequestMapping("/hello")
public String getStr(){
return "hello,捡田螺的小男孩";
}

//返回
hello,捡田螺的小男孩

也有这样的:

@RequestMapping("queryUser")
public UserVo queryUser(String userId) {
return new UserVo("666", "捡田螺的小男孩");
}
//返回:
{"userId":"666","name":"捡田螺的小男孩"}

显然,如果接口返回结果不统一,前端处理就不方便,我们代码也不好维护。再比如小田螺喜欢用Result处理结果,大田螺喜欢用Response处理结果,可以想象一下,这些代码有多乱。

所以作为后端开发,我们项目的响应结果,需要统一标准的返回格式。一般一个标准的响应报文对象,都有哪些属性呢?

  • code :响应状态码
  • message :响应结果描述
  • data:返回的数据

响应状态码一般用枚举表示哈:

public enum CodeEnum {

/**操作成功**/
SUCCESS("0000","操作成功"),
/**操作失败**/
ERROR("9999","操作失败"),;

/**
* 自定义状态码
**/
private String code;
/**自定义描述**/
private String message;

CodeEnum(String code, String message){
this.code = code;
this.message = message;
}

public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}

因为返回的数据类型不是确定的,我们可以使用泛型,如下:

/**
* @author 捡田螺的小男孩
* @param <T>
*/
public class BaseResponse<T> {

/**
* 响应状态码(0000表示成功,9999表示失败
*/
private String code;

/**
* 响应结果描述
*/
private String message;

/**
* 返回的数据
*/
private T data;

/**
* 成功返回
* @param data
* @param <T>
* @return
*/
public static <T> BaseResponse<T> success(T data) {
BaseResponse<T> response= new BaseResponse<>();
response.setCode(CodeEnum.SUCCESS.getCode());
response.setMessage(CodeEnum.SUCCESS.getMessage());
response.setData(data);
return response;
}

/**
* 失败返回
* @param code
* @param message
* @param <T>
* @return
*/
public static <T> BaseResponse<T> fail(String code, String message) {
BaseResponse<T> response = new BaseResponse<>();
response.setCode(code);
response.setMessage(message);
return response;
}

public void setCode(String code) {
this.code = code;
}

public void setMessage(String message) {
this.message = message;
}

public void setData(T data) {
this.data = data;
}
}

有了统一的响应体,我们就可以优化一下controller 层的代码啦:

@RequestMapping("/hello")
public BaseResponse<String> getStr(){
return BaseResponse.success("hello,捡田螺的小男孩");
}
//output
{"code":"0000","message":"操作成功","data":"hello,捡田螺的小男孩"}

@RequestMapping("queryUser")
public BaseResponse<UserVo> queryUser(String userId) {
return BaseResponse.success(new UserVo("666", "捡田螺的小男孩"));
}
//output
{"code":"0000","message":"操作成功","data":{"userId":"666","name":"捡田螺的小男孩"}}

3. 统一异常处理

日常开发中,我们一般都是自定义统一的异常类,如下:

public class BizException extends RuntimeException {

private String retCode;

private String retMessage;

public BizException() {
super();
}

public BizException(String retCode, String retMessage) {
this.retCode = retCode;
this.retMessage = retMessage;
}

public String getRetCode() {
return retCode;
}

public String getRetMessage() {
return retMessage;
}
}

在controller 层,很可能会有类似代码:

@RequestMapping("/query")
public BaseResponse<UserVo> queryUserInfo(UserParam userParam) {
try {
return BaseResponse.success(userService.queryUserInfo(userParam));
} catch (BizException e) {
//doSomething
} catch (Exception e) {
//doSomething
}
return BaseResponse.fail(CodeEnum.ERROR.getCode(),CodeEnum.ERROR.getMessage());
}

这块代码,没什么问题哈,但是如果try...catch太多,不是很优雅。

可以借助注解@RestControllerAdvice,让代码更优雅。@RestControllerAdvice是一个应用于Controller层的切面注解,它一般配合@ExceptionHandler注解一起使用,作为项目的全局异常处理。我们来看下demo代码哈。

还是原来的UserController,和一个会抛出异常的userService的方法,如下:

@RestController
public class UserController {

@Autowired
private UserService userService;

@RequestMapping("/query")
public BaseResponse<UserVo> queryUserInfo1(UserParam userParam) {
return BaseResponse.success(userService.queryUserInfo(userParam));
}
}

@Service
public class UserServiceImpl implements UserService {
//抛出异常
@Override
public UserVo queryUserInfo(UserParam userParam) throws BizException {
throw new BizException("6666", "测试异常类");
}
}

我们再定义一个全局异常处理器,用@RestControllerAdvice注解,如下:

@RestControllerAdvice(annotations = RestController.class)
public class ControllerExceptionHandler {
}

我们有想要拦截的异常类型,比如想拦截BizException类型,就新增一个方法,使用@ExceptionHandler注解修饰,如下:

@RestControllerAdvice(annotations = RestController.class)
public class ControllerExceptionHandler {

@ExceptionHandler(BizException.class)
@ResponseBody
public BaseResponse<Void> handler(BizException e) {
System.out.println("进入业务异常"+e.getRetCode()+e.getRetMessage());
return BaseResponse.fail(CodeEnum.ERROR.getCode(), CodeEnum.ERROR.getMessage());
}
}

唠叨几句

本文大家学到了哪些知识呢?

  1. 为了写出更优雅、更简洁、更容易维护的代码,我们需要统一参数校验、统一响应对象返回、统一异常处理
  2. 参数校验更简洁,可以使用注解实现。
  3. 如何统一响应对象返回,一般要包括状态码、描述信息、返回数据。
  4. Controller层如何统一全局异常处理?@RestControllerAdvice+@ExceptionHandler
  5. 进阶篇?大家可以自己实现自定义注解哈,也建议去看看@RestControllerAdvice实现原理,它其实就是一个切面注解,看下它的源码即可。
文章目录
  1. 1. 1. 使用注解,统一参数校验
  2. 2. 2. 接口统一响应对象返回
  3. 3. 3. 统一异常处理
  4. 4. 唠叨几句