摘要:本文演示如何构建起一个优秀的后端接口体系,体系构建好了天然就有了规范,同时再构建新的后端接口也会十分轻松。
一个后端接口大体分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建这几个部分每一个公司要求都不一样,没有什么“必定是最好的”标准,但一个优秀的后端接口和一个糟糕的后端接口对比起来差别仍是蛮大的,其中最重要的关键点就是看是否规范!html
本文就一步一步演示如何构建起一个优秀的后端接口体系,体系构建好了天然就有了规范,同时再构建新的后端接口也会十分轻松。前端
这里用的是SpringBoot配置项目,本文讲解的重点是后端接口,因此只须要导入一个spring-boot-starter-web包就能够了:web
<!--web依赖包,web应用必备--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
本文还用了swagger来生成API文档,lombok来简化类,不过这二者不是必须的,可用可不用。正则表达式
一个接口通常对参数(请求数据)都会进行安全校验,参数校验的重要性天然没必要多说,那么如何对参数进行校验就有讲究了。spring
首先咱们来看一下最多见的作法,就是在业务层进行参数校验:json
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"; }
这样作固然是没有什么错的,并且格式排版整齐也一目了然,不过这样太繁琐了,这尚未进行业务操做呢光是一个参数校验就已经这么多行代码,实在不够优雅。后端
咱们来改进一下,使用Spring Validator和Hibernate Validator这两套Validator来进行方便的参数校验!这两套Validator依赖包已经包含在前面所说的web依赖包里了,因此能够直接使用。安全
Validator能够很是方便的制定校验规则,并自动帮你完成校验。首先在入参里须要校验的字段加上注解,每一个注解对应不一样的校验规则,并可制定校验失败后的信息:app
@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 : bindingResult.getAllErrors()) { return error.getDefaultMessage(); } return userService.addUser(user); } }
这样当请求数据传递到接口的时候Validator就自动完成校验了,校验的结果就会封装到BindingResult中去,若是有错误信息咱们就直接返回给前端,业务逻辑代码也根本没有执行下去。ide
此时,业务层里的校验代码就已经不须要了:
public String addUser(User user) { // 直接编写业务逻辑 return "success"; }
如今能够看一下参数校验效果。咱们故意给这个接口传递一个不符合校验规则的参数,先传递一个错误数据给接口,故意将password这个字段不知足校验条件:
{ "account": "12345678", "email": "123@qq.com", "id": 0, "password": "123" }
再来看一下接口的响应数据:
这样是否是方便不少?不难看出使用Validator校验有以下几个好处:
(1)简化代码,以前业务层那么一大段校验代码都被省略掉了。
(2)使用方便,那么多校验规则能够垂手可得的实现,好比邮箱格式验证,以前本身手写正则表达式要写那么一长串,还容易出错,用Validator直接一个注解搞定。(还有更多校验规则注解,能够自行去了解哦)
(3)减小耦合度,使用Validator可以让业务层只关注业务逻辑,从基本的参数校验逻辑中脱离出来。
使用Validator+ BindingResult已是很是方便实用的参数校验方式了,在实际开发中也有不少项目就是这么作的,不过这样仍是不太方便,由于你每写一个接口都要添加一个BindingResult参数,而后再提取错误信息返回给前端。
这样有点麻烦,而且重复代码不少(尽管能够将这个重复代码封装成方法)。咱们可否去掉BindingResult这一步呢?固然是能够的!
咱们彻底能够将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); // 而后提取错误提示信息进行返回 return objectError.getDefaultMessage(); } }
咱们再来看下此次校验失败后的响应数据:
没错,此次返回的就是咱们制定的错误提示信息!咱们经过全局异常处理优雅的实现了咱们想要的功能!之后咱们再想写接口参数校验,就只须要在入参的成员变量上加上Validator校验规则注解,而后在参数上加上@Valid注解便可完成校验,校验失败会自动返回错误提示信息,无需任何其余代码!更多的校验思路:SpringBoot实现通用的接口参数校验
全局处理固然不会只能处理一种异常,用途也不只仅是对一个参数校验方式进行优化。在实际开发中,如何对异常处理实际上是一个很麻烦的事情。传统处理异常通常有如下烦恼:
以上这些问题均可以用全局异常处理来解决,全局异常处理也叫统一异常处理,全局和统一处理表明什么?表明规范!规范有了,不少问题就会迎刃而解!
全局异常处理的基本使用方式你们都已经知道了,咱们接下来更进一步的规范项目中的异常处理方式:自定义异常。
在不少状况下,咱们须要手动抛出异常,好比在业务层当有些条件并不符合业务逻辑,我这时候就能够手动抛出异常从而触发事务回滚。那手动抛出异常最简单的方式就是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返回。这就要引伸出咱们接下来要讲的东西了:数据统一响应
如今咱们规范好了参数校验方式和异常处理方式,然而尚未规范响应数据!好比我要获取一个分页信息数据,获取成功了呢天然就返回的数据列表,获取失败了后台就会响应异常信息,即一个字符串,就是说前端开发者压根就不知道后端响应过来的数据会是啥样的!因此,统一响应数据是先后端规范中必需要作的!
统一数据响应第一步确定要作的就是咱们本身自定义一个响应体类,不管后台是运行正常仍是发生异常,响应给前端的数据格式是不变的!那么如何定义响应体呢?关于异常的设计:如何更优雅的设计异常
能够参考咱们自定义异常类,也来一个响应信息代码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.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()); }
咱们再来看一下此时若是发生异常了会响应什么数据给前端:
OK,这个异常信息响应就很是好了,状态码和响应说明还有错误提示数据都返给了前端,而且是全部异常都会返回相同的格式!异常这里搞定了,别忘了咱们到接口那也要修改返回类型,咱们新增一个接口好来看看效果:
@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); }
看一下若是响应正确返回的是什么效果:
这样不管是正确响应仍是发生异常,响应数据的格式都是统一的,十分规范!
数据格式是规范了,不过响应码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.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()); }
这样响应码和响应信息只能是枚举规定的那几个,就真正作到了响应数据格式、响应码和响应信息规范化、统一化!这些能够参考:Java项目构建基础:统一结果,统一异常,统一日志
接口返回统一响应体 + 异常也返回统一响应体,其实这样已经很好了,但仍是有能够优化的地方。要知道一个项目下来定义的接口搞个几百个太正常不过了,要是每个接口返回数据时都要用响应体来包装一下好像有点麻烦,有没有办法省去这个包装过程呢?固然是有滴,仍是要用到全局处理。
首先,先建立一个类加上注解使其成为全局处理类。而后继承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.getGenericParameterType().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类型的数据直接进行强转,因此要进行特殊处理,这里不讲过多的细节,有兴趣能够自行深刻了解。
自此整个后端接口基本体系就构建完毕了
本文做者RudeCrab ,原文转载请连接RudeCrab受权