关于java:引看看人家那后端API接口写得那叫一个优雅

3次阅读

共计 10459 个字符,预计需要花费 27 分钟才能阅读完成。

作者:RudeCrab
起源:juejin.im/post/6844904101940117511

前言

一个后端接口大抵分为四个局部组成:接口地址(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 || 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 + 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 : bindingResult.getAllErrors()) {return error.getDefaultMessage();
        }
        return userService.addUser(user);
    }
}

这样当申请数据传递到接口的时候 Validator 就主动实现校验了,校验的后果就会封装到 BindingResult 中去,如果有错误信息咱们就间接返回给前端,业务逻辑代码也基本没有执行上来。此时,业务层里的校验代码就曾经不须要了:

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 这一步呢?当然是能够的!

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);
        // 而后提取谬误提示信息进行返回
        return objectError.getDefaultMessage();}
    
}

咱们再来看下这次校验失败后的响应数据:

没错,这次返回的就是咱们制订的谬误提示信息!咱们通过全局异样解决优雅的实现了咱们想要的性能!当前咱们再想写接口参数校验,就只须要在入参的成员变量上加上 Validator 校验规定注解,而后在参数上加上 @Valid 注解即可实现校验,校验失败会主动返回谬误提示信息,无需任何其余代码!

自定义异样

全局解决当然不会只能解决一种异样,用处也不仅仅是对一个参数校验形式进行优化。在理论开发中,如何对异样解决其实是一个很麻烦的事件。传统解决异样个别有以下懊恼:

  1. 是捕捉异样 (try...catch) 还是抛出异样(throws)
  2. 是在 controller 层做解决还是在 service 层解决又或是在 dao 层做解决
  3. 解决异样的形式是啥也不做,还是返回特定数据,如果返回又返回什么数据
  4. 不是所有异样咱们都能事后进行捕获,如果产生了没有捕捉到的异样该怎么办?

以上这些问题都能够用全局异样解决来解决,全局异样解决也叫对立异样解决,全局和对立解决代表什么?代表标准! 标准有了,很多问题就会迎刃而解!全局异样解决的根本应用形式大家都曾经晓得了,咱们接下来更进一步的标准我的项目中的异样解决形式:自定义异样。在很多状况下,咱们须要手动抛出异样,比方在业务层当有些条件并不合乎业务逻辑,我这时候就能够手动抛出异样从而触发事务回滚。那手动抛出异样最简略的形式就是 throw new RuntimeException("异样信息") 了,不过应用自定义会更好一些:

  1. 自定义异样能够携带更多的信息,不像这样只能携带一个字符串。
  2. 我的项目开发中常常是很多人负责不同的模块,应用自定义异样能够对立了对外异样展现的形式。
  3. 自定义异样语义更加清晰明了,一看就晓得是我的项目中手动抛出的异样。

咱们当初就来开始写一个自定义异样:

@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());
}

这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!

全局解决响应数据

接口返回对立响应体 + 异样也返回对立响应体,其实这样曾经很好了,但还是有能够优化的中央。要晓得一个我的项目下来定义的接口搞个几百个太失常不过了,要是每一个接口返回数据时都要用响应体来包装一下如同有点麻烦,有没有方法省去这个包装过程呢?当然是有滴,还是要用到全局解决。

首先,先创立一个类加上注解使其成为全局解决类。而后继承 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 + 主动抛出异样来实现了不便的参数校验
  • 通过全局异样解决 + 自定义异样实现了异样操作的标准
  • 通过数据对立响应实现了响应数据的标准
  • 多个方面组装十分优雅的实现了后端接口的协调,让开发人员有更多的经验重视业务逻辑代码,轻松构建后端接口

再次强调,我的项目体系该怎么构建、后端接口该怎么写都没有一个相对对立的规范,不是说肯定要依照本文的来才是最好的,你怎么都能够,本文每一个环节你都能够依照本人的想法来进行编码,我只是提供了一个思路!

我的项目源码地址

https://github.com/RudeCrab/r…

正文完
 0