SpringBoot写后端接⼝,看这⼀篇就够了!
⼀个后端接⼝⼤致分为四个部分组成:接⼝地址(url)、接⼝请求⽅式(get、post等)、请求数据(request)、响应数据(response)。如何构建这⼏个部分每个公司要求都不同,没有什么“⼀定是最好的”标准,但⼀个优秀的后端接⼝和⼀个糟糕的后端接⼝对⽐起来差异还是蛮⼤的,其中最重要的关键点就是看是否规范!
本⽂就⼀步⼀步演⽰如何构建起⼀个优秀的后端接⼝体系,体系构建好了⾃然就有了规范,同时再构建新的后端接⼝也会⼗分轻松。
所需依赖包
这⾥⽤的是SpringBoot配置项⽬,本⽂讲解的重点是后端接⼝,所以只需要导⼊⼀个spring-boot-starter-web包就可以了:
<!--web依赖包,web应⽤必备-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
本⽂还⽤了swagger来⽣成API⽂档,lombok来简化类,不过这两者不是必须的,可⽤可不⽤。
参数校验
⼀个接⼝⼀般对参数(请求数据)都会进⾏安全校验,参数校验的重要性⾃然不必多说,那么如何对参数进⾏校验就有讲究了。
业务层校验
⾸先我们来看⼀下最常见的做法,就是在业务层进⾏参数校验:
public String addUser(User user) {
if (user == null || Id() == null || Account() == null || Password() == null || Email() == null) {
return "对象或者对象字段不能为空";
}
if (StringUtils.Account()) || StringUtils.Password()) || StringUtils.Email())) {
return "不能输⼊空字符串";
}
if (Account().length() < 6 || Account().length() > 11) {
return "账号长度必须是6-11个字符";
}
if (Password().length() < 6 || Password().length() > 16) {
return "密码长度必须是6-16个字符";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$", Email())) {
return "邮箱格式不正确";
}
// 参数校验完毕后这⾥就写上业务逻辑
return "success";
}
这样做当然是没有什么错的,⽽且格式排版整齐也⼀⽬了然,不过这样太繁琐了,这还没有进⾏业务操作呢光是⼀个参数校验就已经这么多⾏代码,实在不够优雅。
我们来改进⼀下,使⽤Spring Validator和Hibernate Validator这两套Validator来进⾏⽅便的参数校验!这两套Validator依赖包已经包含在前⾯所说的web依赖包⾥了,所以可以直接使⽤。
Validator + BindResult进⾏校验
Validator可以⾮常⽅便的制定校验规则,并⾃动帮你完成校验。⾸先在⼊参⾥需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:
@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;
}
校验规则和错误提⽰信息配置完毕后,接下来只需要在接⼝需要校验的参数上加上@Valid注解,并添加BindResult参数即可⽅便完成验证:
@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 : AllErrors()) {
DefaultMessage();
}
return userService.addUser(user);
}
}
这样当请求数据传递到接⼝的时候Validator就⾃动完成校验了,校验的结果就会封装到BindingResult中去,如果有错误信息我们就直接返回给前端,业务逻辑代码也根本没有执⾏下去。
此时,业务层⾥的校验代码就已经不需要了:
public String addUser(User user) {
// 直接编写业务逻辑
return "success";
}
现在可以看⼀下参数校验效果。我们故意给这个接⼝传递⼀个不符合校验规则的参数,先传递⼀个错误数据给接⼝,故意将password这个字段不满⾜校验条件:
{
"account": "12345678",
"email": "123@qq",
"id": 0,
"password": "123"
}
再来看⼀下接⼝的响应数据:
这样是不是⽅便很多?不难看出使⽤Validator校验有如下⼏个好处:
(1)简化代码,之前业务层那么⼀⼤段校验代码都被省略掉了。
(2)使⽤⽅便,那么多校验规则可以轻⽽易举的实现,⽐如邮箱格式验证,之前⾃⼰⼿写正则表达式要写那么⼀长串,还容易出错,⽤Validator直接⼀个注解搞定。(还有更多校验规则注解,可以⾃⾏去了解哦)
(3)减少耦合度,使⽤Validator能够让业务层只关注业务逻辑,从基本的参数校验逻辑中脱离出来。
使⽤Validator+ BindingResult已经是⾮常⽅便实⽤的参数校验⽅式了,在实际开发中也有很多项⽬就是这么做的,不过这样还是不太⽅便,因为你每写⼀个接⼝都要添加⼀个BindingResult参数,然后再提取错误信息返回给前端。
这样有点⿇烦,并且重复代码很多(尽管可以将这个重复代码封装成⽅法)。我们能否去掉BindingResult这⼀步呢?当然是可以的!
Validator + ⾃动抛出异常
我们完全可以将BindingResult这⼀步给去掉:
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
return userService.addUser(user);
}
去掉之后会发⽣什么事情呢?直接来试验⼀下,还是按照之前⼀样故意传递⼀个不符合校验规则的参数给接⼝。此时我们观察控制台可以发现接⼝已经引发MethodArgumentNotValidException异常了:
其实这样就已经达到我们想要的效果了,参数校验不通过⾃然就不执⾏接下来的业务逻辑,去掉BindingResult后会⾃动引发异常,异常发⽣了⾃然⽽然就不会执⾏业务逻辑。也就是说,我们完全没必要添加相关BindingResult相关操作嘛。
不过事情还没有完,异常是引发了,可我们并没有编写返回错误信息的代码呀,那参数校验失败了会响应什么数据给前端呢?
我们来看⼀下刚才异常发⽣后接⼝响应的数据:
没错,是直接将整个错误对象相关信息都响应给前端了!这样就很难受,不过解决这个问题也很简单,就是我们接下来要讲的全局异常处理!
全局异常处理
参数校验失败会⾃动引发异常,我们当然不可能再去⼿动捕捉异常进⾏处理,不然还不如⽤之前BindingResult⽅式呢。⼜不想⼿动捕捉这个异常,⼜要对这个异常进⾏处理,那正好使⽤SpringBoot全局异常处理来达到⼀劳永逸的效果!
基本使⽤
⾸先,我们需要新建⼀个类,在这个类上加上@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);
// 然后提取错误提⽰信息进⾏返回
DefaultMessage();
}
}
我们再来看下这次校验失败后的响应数据:
没错,这次返回的就是我们制定的错误提⽰信息!我们通过全局异常处理优雅的实现了我们想要的功能!以后我们再想写接⼝参数校验,就只需要在⼊参的成员变量上加上Validator校验规则注解,然后在参数上加上@Valid注解即可完成校验,校验失败会⾃动返回错误提⽰信息,⽆需任何其他代码!更
多的校验思路:SpringBoot实现通⽤的接⼝参数校验
⾃定义异常
全局处理当然不会只能处理⼀种异常,⽤途也不仅仅是对⼀个参数校验⽅式进⾏优化。在实际开发中,如何对异常处理其实是⼀个很⿇烦的事情。传统处理异常⼀般有以下烦恼:
是捕获异常(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.msg = msg;
}
}
在刚才的全局异常处理类中记得添加对我们⾃定义异常的处理:
@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
Msg();
}
这样就对异常的处理就⽐较规范了,当然还可以添加对Exception的处理,这样⽆论发⽣什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项⽬上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了⽅便调试还是不要这样做。
现在全局异常处理和⾃定义异常已经弄好了,不知道⼤家有没有发现⼀个问题,就是当我们抛出⾃定义异常的时候全局异常处理只响应了异常中的错误信息msg给前端,并没有将错误代码code返回。这就要引申出我们接下来要讲的东西了:数据统⼀响应
数据统⼀响应
现在我们规范好了参数校验⽅式和异常处理⽅式,然⽽还没有规范响应数据!⽐如我要获取⼀个分页信息数据,获取成功了呢⾃然就返回的数据列表,获取失败了后台就会响应异常信息,即⼀个字符串,就是说前端开发者压根就不知道后端响应过来的数据会是啥样的!所以,统⼀响应数据是前后端规范中必须要做的!
⾃定义统⼀响应体
统⼀数据响应第⼀步肯定要做的就是我们⾃⼰⾃定义⼀个响应体类,⽆论后台是运⾏正常还是发⽣异常,响应给前端的数据格式是不变的!那么如何定义响应体呢?关于异常的设计:如何更优雅的设计异常
可以参考我们⾃定义异常类,也来⼀个响应信息代码code和响应信息说明msg:
@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.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, "参数校验失败", DefaultMessage());
}
我们再来看⼀下此时如果发⽣异常了会响应什么数据给前端:
OK,这个异常信息响应就⾮常好了,状态码和响应说明还有错误提⽰数据都返给了前端,并且是所有异常都会返回相同的格式!异常这⾥搞定了,别忘了我们到接⼝那也要修改返回类型,我们新增⼀个接⼝好来看看效果:
@GetMapping("/getUser")
public ResultVO<User> getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("123@qq");
return new ResultVO<>(user);
}
看⼀下如果响应正确返回的是什么效果:
这样⽆论是正确响应还是发⽣异常,响应数据的格式都是统⼀的,⼗分规范!
数据格式是规范了,不过响应码code和响应信息msg还没有规范呀!⼤家发现没有,⽆论是正确响应,还是异常响应,响应码和响应信息是想怎么设置就怎么设置,要是10个开
发⼈员对同⼀个类型的响应写10个不同的响应码,那这个统⼀响应体的格式规范就毫⽆意义!所以,必须要将响应码和响应信息给规范起来。
响应码枚举
要规范响应体中的响应码和响应信息⽤枚举简直再恰当不过了,我们现在就来创建⼀个响应码枚举类:
@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.msg = msg;
}
}
然后修改响应体的构造⽅法,让其只准接受响应码枚举来设置响应码和响应信息:
public ResultVO(T data) {
this(ResultCode.SUCCESS, data);
}
public ResultVO(ResultCode resultCode, T data) {
this.msg = Msg();
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, DefaultMessage());
}
这样响应码和响应信息只能是枚举规定的那⼏个,就真正做到了响应数据格式、响应码和响应信息规范化、统⼀化!这些可以参考:Java项⽬构建基础:统⼀结果,统⼀异常,
统⼀⽇志
全局处理响应数据
接⼝返回统⼀响应体 + 异常也返回统⼀响应体,其实这样已经很好了,但还是有可以优化的地⽅。要知道⼀个项⽬下来定义的接⼝搞个⼏百个太正常不过了,要是每⼀个接⼝返
回数据时都要⽤响应体来包装⼀下好像有点⿇烦,有没有办法省去这个包装过程呢?当然是有滴,还是要⽤到全局处理。
⾸先,先创建⼀个类加上注解使其成为全局处理类。然后继承ResponseBodyAdvice接⼝重写其中的⽅法,即可对我们的controller进⾏增强操作,具体看代码和注释:
@RestControllerAdvice(basePackages = {"com.ller"}) // 注意哦,这⾥要加上需要扫描的包
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
// 如果接⼝返回的类型本⾝就是ResultVO那就没有必要进⾏额外的操作,返回false
return !GenericParameterType().equals(ResultVO.class);
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) { // String类型不能直接包装,所以要进⾏些特别的处理
if (GenericParameterType().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⽅法中,我们可以直接在该⽅法⾥包装数据,这样就不需要每个接⼝都进⾏数据包装了,省去了很
多⿇烦。
我们可以现在去掉接⼝的数据包装来看下效果:
springboot是啥@GetMapping("/getUser")
public User getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("123@qq");
// 注意哦,这⾥是直接返回的User类型,并没有⽤ResultVO进⾏包装
return user;
}
然后我们来看下响应数据:
成功对数据进⾏了包装!
注意:beforeBodyWrite⽅法⾥包装数据⽆法对String类型的数据直接进⾏强转,所以要进⾏特殊处理,这⾥不讲过多的细节,有兴趣可以⾃⾏深⼊了解。总结
⾃此整个后端接⼝基本体系就构建完毕了
通过Validator + ⾃动抛出异常来完成了⽅便的参数校验
通过全局异常处理 + ⾃定义异常完成了异常操作的规范
通过数据统⼀响应完成了响应数据的规范
多个⽅⾯组装⾮常优雅的完成了后端接⼝的协调,让开发⼈员有更多的经历注重业务逻辑代码,轻松构建后端接⼝
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论