SpringBoot三招组合拳,手把手教你打出优雅的后端接口
前言
一个后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建这几个部分每个公司要求都不同,没有什么“一定是最好的”标准,但一个优秀的后端接口和一个糟糕的后端接口对比起来差异还是蛮大的,其中最重要的关键点就是看「是否规范!」本文就一步一步演示如何构建起一个优秀的后端接口体系,体系构建好了自然就有了规范,同时再构建新的后端接口也会十分轻松。「在文章末尾贴上了项目演示的github地址,clone下来即可运行,并且我将每一次的优化记录都分别做了代码提交,你可以清晰的看到项目的改进过程!」
所需依赖包
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
参数校验
业务层校验
public String addUser(User user) {
if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
return "对象或者对象字段不能为空";
}
if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
return "不能输入空字符串";
}
if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
return "账号长度必须是6-11个字符";
}
if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
return "密码长度必须是6-16个字符";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return "邮箱格式不正确";
}
// 参数校验完毕后这里就写上业务逻辑
return "success";
}
Validator + BindResult进行校验
@Data
public class User {
@NotNull(message = "用户id不能为空")
private Long 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;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
for (ObjectError error : bindingResult.getAllErrors()) {
return error.getDefaultMessage();
}
return userService.addUser(user);
}
}
public String addUser(User user) {
// 直接编写业务逻辑
return "success";
}
{
"account": "12345678",
"email": "123@qq.com",
"id": 0,
"password": "123"
}
简化代码,之前业务层那么一大段校验代码都被省略掉了。 使用方便,那么多校验规则可以轻而易举的实现,比如邮箱格式验证,之前自己手写正则表达式要写那么一长串,还容易出错,用Validator直接一个注解搞定。(还有更多校验规则注解,可以自行去了解哦) 减少耦合度,使用Validator能够让业务层只关注业务逻辑,从基本的参数校验逻辑中脱离出来。
Validator + 自动抛出异常
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
return userService.addUser(user);
}
MethodArgumentNotValidException
异常了:全局异常处理
基本使用
@ControllerAdvice
或@RestControllerAdvice
注解,这个类就配置成全局处理类了。(这个根据你的Controller层用的是@Controller
还是@RestController
来决定)
然后在类中新建方法,在方法上加上@ExceptionHandler
注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!我们现在就来演示一下对参数校验失败抛出的MethodArgumentNotValidException
全局处理:@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return objectError.getDefaultMessage();
}
}
@Valid
注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!自定义异常
是捕获异常( try...catch
)还是抛出异常(throws
)是在 controller
层做处理还是在service
层处理又或是在dao
层做处理处理异常的方式是啥也不做,还是返回特定数据,如果返回又返回什么数据 不是所有异常我们都能预先进行捕捉,如果发生了没有捕捉到的异常该怎么办?
throw new RuntimeException("异常信息")
了,不过使用自定义会更好一些:自定义异常可以携带更多的信息,不像这样只能携带一个字符串。 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
@Getter //只要getter方法,无需setter
public class APIException extends RuntimeException {
private int code;
private String msg;
public APIException() {
this(1001, "接口错误");
}
public APIException(String msg) {
this(1001, msg);
}
public APIException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
}
@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
return e.getMsg();
}
Exception
的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。现在全局异常处理和自定义异常已经弄好了,不知道大家有没有发现一个问题,就是当我们抛出自定义异常的时候全局异常处理只响应了异常中的错误信息msg给前端,并没有将错误代码code返回。这就要引申出我们接下来要讲的东西了:数据统一响应数据统一响应
自定义统一响应体
@Getter
public class ResultVO<T> {
/**
* 状态码,比如1000代表响应成功
*/
private int code;
/**
* 响应信息,用来说明响应情况
*/
private String msg;
/**
* 响应的具体数据
*/
private T data;
public ResultVO(T data) {
this(1000, "success", data);
}
public ResultVO(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
// 注意哦,这里返回类型是自定义响应体
return new ResultVO<>(e.getCode(), "响应失败", e.getMsg());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 注意哦,这里返回类型是自定义响应体
return new ResultVO<>(1001, "参数校验失败", objectError.getDefaultMessage());
}
@GetMapping("/getUser")
public ResultVOgetUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("123@qq.com");
return new ResultVO<>(user);
}
响应码枚举
@Getter
public enum ResultCode {
SUCCESS(1000, "操作成功"),
FAILED(1001, "响应失败"),
VALIDATE_FAILED(1002, "参数校验失败"),
ERROR(5000, "未知错误");
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
public ResultVO(T data) {
this(ResultCode.SUCCESS, data);
}
public ResultVO(ResultCode resultCode, T data) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
this.data = data;
}
@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
// 注意哦,这里传递的响应码枚举
return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 注意哦,这里传递的响应码枚举
return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}
全局处理响应数据
ResponseBodyAdvice
接口重写其中的方法,即可对我们的controller
进行增强操作,具体看代码和注释:@RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"}) // 注意哦,这里要加上需要扫描的包
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> aClass) {
// 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
return !returnType.getParameterType().equals(ResultVO.class);
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class extends HttpMessageConverter>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
// String类型不能直接包装,所以要进行些特别的处理
if (returnType.getGenericParameterType().equals(String.class)) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 将数据包装在ResultVO里后,再转换为json字符串响应给前端
return objectMapper.writeValueAsString(new ResultVO<>(data));
} catch (JsonProcessingException e) {
throw new APIException("返回String类型错误");
}
}
// 将原本的数据包装在ResultVO里
return new ResultVO<>(data);
}
}
controller
将数据进行返回前进行增强操作,supports
方法要返回为true
才会执行beforeBodyWrite
方法,所以如果有些情况不需要进行增强操作可以在supports
方法里进行判断。对返回数据进行真正的操作还是在beforeBodyWrite
方法中,我们可以直接在该方法里包装数据,这样就不需要每个接口都进行数据包装了,省去了很多麻烦。@GetMapping("/getUser")
public User getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("123@qq.com");
// 注意哦,这里是直接返回的User类型,并没有用ResultVO进行包装
return user;
}
注意: beforeBodyWrite
方法里包装数据无法对String类型的数据直接进行强转,所以要进行特殊处理,这里不讲过多的细节,有兴趣可以自行深入了解。
总结
通过Validator + 自动抛出异常来完成了方便的参数校验 通过全局异常处理 + 自定义异常完成了异常操作的规范 通过数据统一响应完成了响应数据的规范 多个方面组装非常优雅的完成了后端接口的协调,让开发人员有更多的经历注重业务逻辑代码,轻松构建后端接口
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️
评论