Spring Boot 三招组合拳,手把手教你打出优雅的后端接口
作者:RudeCrab; 链接:suo.im/5YMt2A
一、前言
二、所需依赖包
<!--web依赖包,web应用必备-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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 {@Autowiredprivate 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);
}
四、全局异常处理
基本使用
@RestControllerAdvicepublic class ExceptionControllerAdvice {@ExceptionHandler(MethodArgumentNotValidException.class)public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {// 从异常对象中拿到ObjectError对象 ObjectError objectError = e.getBindingResult().getAllErrors().get(0);// 然后提取错误提示信息进行返回return objectError.getDefaultMessage(); }}
自定义异常
是捕获异常 try...catch 还是抛出异常 throws? 是在 Controller 层做处理还是在 Service 层处理又或是在 DAO 层做处理? 处理异常的方式是啥也不做,还是返回特定数据?如果返回又返回什么数据? 不是所有异常我们都能预先进行捕捉,如果发生了没有捕捉到的异常该怎么办?
自定义异常可以携带更多的信息,不像这样只能携带一个字符串; 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式; 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
@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();}
五、数据统一响应
自定义统一响应体
@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 ResultVO<User> getUser() {
User user = new User();
user.setId(1L);
user.setAccount('12345678');
user.setPassword('12345678');
user.setEmail('123@qq.com');
return new ResultVO<>(user);
}
响应码枚举
@Getterpublic 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());}
全局处理响应数据
@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);
}
}
@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;}
六、总结
通过 Validator + 自动抛出异常来完成了方便的参数校验; 通过全局异常处理 + 自定义异常完成了异常操作的规范; 通过数据统一响应完成了响应数据的规范; 多个方面组装非常优雅的完成了后端接口的协调,让开发人员有更多的经历注重业务逻辑代码,轻松构建后端接口。
赞 (0)