编写令人愉悦的API接口(二)

2024-02-09 13:10
文章标签 接口 编写 api 愉悦 令人

本文主要是介绍编写令人愉悦的API接口(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

引言

制定API接口的规范在设计接口时尤为重要,统一的格式规范会减少联调的难度,也能让后续维护者身心愉悦.在编写接口时,运用适合的参数检验技巧也能提高代码的健壮性.本文将围绕这两点详解一些我的一些经验.

思考

  1. 业务接口通常都包含最基础的CRUD操作,怎么尽可能的统一这部分接口?
  2. API接口部分会使用到HttpServletRequestHttpServletResponse里的数据,如何方便的获取到这两个对象?
  3. 接口的参数有时通过单个参数传递,有时会通过对象传递,如何方便的检验这些参数,而不用频繁的使用if判断去甄别临界值或者判空?

带着这些疑问,我们一步步去解决.

整体类图

统一基础接口路径

在前一篇文章中有提到过,如何用正确的姿势使用不同的协议,GET,POST,PUT,PATCH,DELETE这五种协议在日常CRUD开发中很常用.不同的业务基本都会罗列出这些API接口,那么我们能不能尝试把他们抽离成接口,然后让不同的业务控制层去实现这个接口,从而规范基础的接口路径呢?理论存在,实践开始

实践

v1版本

先定义一个普通的java接口类,写入四个方法,分别对应增删查改四种操作.
注:PATCH接口修改单个属性值,因为不同业务中字段存在太大差异,所以她比较适合单独实现,这个接口中就不涵盖这个接口了.

public interface BaseCrud {R selectList(@ModelAttribute Map s);R selectOne(@PathVariable String id);R add(@RequestBody Map t);R upp(@PathVariable String id, @RequestBody Map t);R del(@PathVariable String id);
}

相信大家看到这五个接口一目了然,这不就是增删查改加上一个分页接口么.有了这个接口我们编写某种业务的控制层不就简单了么,直接实现他,然后再实现不同的逻辑,完美.

//这里以用户相关业务和User用户相关业务为例
//教师业务控制层
@Controller
@RequestMapping("user")
public class UserController implements BaseCrud {@Override@GetMapping@ResponseBodypublic R selectList(Map s) {return null;}@Override@GetMapping("{id}")@ResponseBodypublic R selectOne(String id) {return null;}@Override@PostMapping@ResponseBodypublic R add(Map t) {return null;}@Override@PutMapping("{id}")@ResponseBodypublic R upp(String id, Map t) {return null;}@Override@DeleteMapping("{id}")@ResponseBodypublic R del(String id) {return null;}
}
//教师业务控制层,跟上面一样的操作,这里节省篇幅,先省略....

通过postMan测试,分别用teacher和user前缀就能调用各自业务的接口,简单的增删查改统一路径便实现了.但是这也会出现一个缺陷,因为我们都是用Map接收的,map键值对因为不确定字段,后期如果不debug是很难维护的,甚至连字段掉了也可以调通,不便于排查,其次如果我们用Mybatis操作时,还需要转换为对象,虽然统一了接口路径,但是后面的操作还是很繁琐,所以我们还是得进一步优化.

v2版本

相信大家对泛型都不陌生.我们这里也可用泛型去优化.因为搜索分页需要携带页码,排序等等,我们把他作为两个泛型去实现.

//需要注意一点小细节,因为这五个接口的路径协议都是可以统一的,所以我们在定义接口的时候就把后面拼接的路径写在接口中.
public interface BaseCrud<T, S> {/*** @description: 分页查询接口* @author: chenyunxuan* @updateTime: 2020/12/18 11:40 上午*/@GetMappingR selectList(@ModelAttribute S s);/*** @description: 根据id查询单条数据* @author: chenyunxuan* @updateTime: 2020/12/18 11:44 上午*/@GetMapping("{id}")R selectOne(@PathVariable String id);/*** @description: 新增单条数据* @author: chenyunxuan* @updateTime: 2020/12/18 1:39 下午*/@PostMappingR add(@RequestBody T t);/*** @description: 修改单条数据* @author: chenyunxuan* @updateTime: 2020/12/18 1:39 下午*/@PutMapping("{id}")R upp(@PathVariable String id, @RequestBody T t);/*** @description: 删除单条数据* @author: chenyunxuan* @updateTime: 2020/12/18 1:40 下午*/@DeleteMapping("{id}")R del(@PathVariable String id);
}

定义两个不同的业务实体类.分别用于分页和新增修改.

@Data
//对应泛型T,用于新增修改
public class User {private String mobile;private String name;private String email;private Integer age;private LocalDateTime birthday;
}
@Data
//对应泛型S,用于搜索
public class UserSearch {private Integer pageNum;private String mobile;private String name;private String email;
}

接下来是业务控制层实现.

@RestController 
@RequestMapping("teacher")
public class UserController implements BaseCrud<User, UserSearch> {@Overridepublic R selectList(UserSearch userSearch) {return null;}@Overridepublic R selectOne(String id) {return null;}@Overridepublic R add(User user) {return null;}@Overridepublic R upp(String id, User user) {return null;}@Overridepublic R del(String id) {return null;}
}

可以看到这个版本比V1版本又方便了不少,优化了传输对象,不同业务可以创建不同对象进行传输.有一个小细节,方法上的@ResponseBody被我省略了,因为@RestController里已经包含了@ResponseBody注解.至此一个通用的API接口统一demo已经完成.

统一的参数效验

实践

人总是不断追求完美的,我也不例外,上面的v2版本虽然把基础接口都统一了,但是看到这么多字段需要效验也是爱不起来啊.比如在新增时,用户的昵称不可为空.用户的年龄最少也要是一岁等等效验再一次充斥着我的代码,看着满满的if判断,我得想办法优化一番.

v3版本

我们先引入一个spring的参数检验组件validation,该组件可以用注解很方便的校验入参,如果异常也可以直接通过捕捉相应的异常信息抛出.

    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>

下面列举一些常用的验证约束注解

  • @Null 被注解的元素必须为null
  • @NotNull 被注解的元素必须不为null
  • @AssertTure 被注解的元素必须为ture
  • @AssertFalse 被注解的元素必须为false
  • @Min(value) 被注解的元素必须是数字且必须大于等于指定值
  • @Max(value) 被注解的元素必须是数字且必须小于等于指定值
  • @DecimalMin(value) 被注解的元素必须是数字且必须大于等于指定值
  • @DecimalMax(value) 被注解的元素必须是数字且必须小于等于指定值
  • @Size(max, min) 被注解的元素必须在指定的范围内
  • @Digits(integer, fraction) 被注解的元素必须是数字且其值必须在给定的范围内
  • @Past 被注解的元素必须是一个过去的日期
  • @Future 被注解的元素必须是一个将来的日期
  • @Pattern(value) 被注解的元素必须符合给定正则表达式
  • @Email 被注解的元素必须是Email地址
  • @Length(min, max) 被注解的元素长度必须在指定的范围内
  • @NotEmpty 被注解的元素必须不为空,空字符串也不可以
  • @Range 被注解的元素(可以是数字或者表示数字的字符串)必须在给定的范围内
  • @URL 被注解的元素必须是URL
  • @Valid 对实体类进行校验

接下来我们开始改造我们的接口,BaseCrud中用到实体的地方可以加一下注解

    /*** @description: 分页查询接口* @author: chenyunxuan* @updateTime: 2020/12/18 11:40 上午*/@GetMappingR selectList(@Validated @ModelAttribute S s);/*** @description: 新增单条数据* @author: chenyunxuan* @updateTime: 2020/12/18 1:39 下午*/@PostMappingR add(@Validated @RequestBody T t);/*** @description: 修改单条数据* @author: chenyunxuan* @updateTime: 2020/12/18 1:39 下午*/@PutMapping("{id}")R upp(@PathVariable String id, @Validated @RequestBody T t);

同时我们的实体类也要做相应的改造,加入你想要的校验注解

public class User {/*** @description: 自定义参数效验(电话号码校验)* @author: chenyunxuan* @updateTime: 2019-12-18 17:30*/@MobileVail(groups = {Add.class})private String mobile;//用户名称最短两位,最长30位//这里的group分组后面会介绍其作用@Size(min = 2, max = 30, groups = {Upp.class})private String name;/*** @description: 自定义错误信息* @author: chenyunxuan* @updateTime: 2019-12-18 17:30*///校验注解都可以自定义message,可配合异常拦截返回你想要message@NotEmpty(message = "自定义错误信息,Email不能为空")@Emailprivate String email;@NotNull@Min(18)@Max(100)private Integer age;@DateTimeFormat(pattern = "MM/dd/yyyy")//不可为空且必须是在系统时间之前@NotNull@Pastprivate LocalDateTime birthday;
}

由上面的代码我们可以得出以下结论

  1. 校验注解可以叠加使用
  2. 在预设校验注解满足不了的时候可以自定义注解
  3. 可用分组实现不同业务需求,不同的校验方式
  4. 抛出的校验信息可自行设置
自定义校验注解

预设的校验有时是不可以满足业务校验要求的,比如电话号码校验,身份证校验等等.好在validation也想到了这部分需求,提供了ConstraintValidator接口可自定义匹配规则.

首先我们得自定义一个注解以及一个校验规则类

@Documented
// 指定真正实现校验规则的类
@Constraint(validatedBy = MobileValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface MobileVail {String message() default "不是正确的手机号码";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };@Target({ElementType.METHOD,ElementType.FIELD,ElementType.PACKAGE})@Retention(RetentionPolicy.RUNTIME)@Documented@interface List {MobileVail[] value();}
}//ConstraintValidator接口使用了泛型,需要指定两个参数,第一个自定义注解类,第二个为需要校验的数据类型。
public class MobileValidator  implements ConstraintValidator<MobileVail, String> {//这里是具体的匹配规则private static final Pattern PHONE_PATTERN = Pattern.compile("^((13[0-9])|(15[^4])|(18[0,2,3,5-9])|(17[0-8])|(147))\\d{8}$");@Overridepublic void initialize(MobileVail constraintAnnotation) {}@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {//实现验证方法if ( value == null || value.length() == 0 ) {return false;}Matcher m = PHONE_PATTERN.matcher(value);return m.matches();}
}

做完这步后只需要在需要验证的字段上加上@MobileVail就可以愉快的使用了

分组验证

看v3实现的代码在验证校验name和mobile属性时候,我们用到了分组功能,这是为了不同的业务场景采用不同的校验方式.

    ......@MobileVail(groups = {Add.class})private String mobile;@Size(min = 2, max = 30, groups = {Upp.class})private String name;......

Add和Upp类定义比较简单,一个空的注解类就OK

public @interface Add {}
public @interface Upp {}

完成这一步后只需要在不同业务中加入不同的组就会把校验按组隔离开,比如上面用户的例子,在新增数据时,我们需要校验电话号码的正确性而修改的时候则不需要验证,在修改的时候我们需要验证用户的昵称长度,新增的时候不需要验证,我们只需要在@Validated中注明他的分组即可.

    @PostMappingR add(@Validated(value = Add.class) @RequestBody T t);@PutMapping("{id}")R upp(@PathVariable String id, @Validated(value = Upp.class) @RequestBody T t);
捕捉异常

在使用validation后,如果不自定义捕捉异常,抛出的异常信息很详细,给客户端提示不友好,所以我们需要拦截这部分异常,自定义抛出message.这需要用到上篇文章提到的统一异常拦截类.

@ControllerAdvice
@Log4j2
public class GlobalExceptionHandler {....../*** @description: JSON传值出现异常(对应@RequestBody传值错误)* @author: chenyunxuan* @date: 2019-12-18 16:37* @version: 1.0.0* @updateTime: 2019-12-18 16:37*/@ExceptionHandler(value = MethodArgumentNotValidException.class)@ResponseBodypublic R handleMethodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {BindingResult bindingResult = e.getBindingResult();StringBuilder sb = new StringBuilder();sb.append("url=");sb.append(req.getRequestURI().replace("/", ""));sb.append(",");for (FieldError fieldError : bindingResult.getFieldErrors()) {sb.append("field=");sb.append(fieldError.getObjectName());sb.append(".");sb.append(fieldError.getField());sb.append(",error=");sb.append(fieldError.getDefaultMessage());sb.append(";");}String msg = sb.toString();log.error(String.format("MethodArgumentNotValidException RequestURI:%s msg:%s", req.getRequestURI(), msg), e);return ResultUtil.error(400, bindingResult.getFieldError().getDefaultMessage());}/*** @title: 单个参数参数异常(对应单个参数传值错误)* @author: chenyunxuan* @date: 2019-12-18 16:37* @version: 1.0.0* @updateTime: 2019-12-18 16:37*/@ExceptionHandler(value = ConstraintViolationException.class)@ResponseBodypublic R handleMethodArgumentNotValidException(HttpServletRequest req, ConstraintViolationException e) {log.error(String.format("ConstraintViolationException RequestURI:%s", req.getRequestURI()), e);return ResultUtil.error(400, e.getMessage());}/*** @title: 提交FORM参数异常(对应form表单传值错误)* @author: chenyunxuan* @date: 2019-12-18 16:41* @version: 1.0.0* @updateTime: 2019-12-18 16:41*/@ExceptionHandler(value = BindException.class)@ResponseBodypublic R handleBindException(HttpServletRequest req, BindException e) throws BindException {// ex.getFieldError():随机返回一个对象属性的异常信息。如果要一次性返回所有对象属性异常信息,则调用ex.getAllErrors()FieldError fieldError = e.getFieldError();StringBuilder sb = new StringBuilder();sb.append(fieldError.getDefaultMessage());// 生成返回结果log.error("BindException requestURI:{} paramName:{} msg:{}", req.getRequestURI(), e.getObjectName(), fieldError.getDefaultMessage());return ResultUtil.error(400, fieldError.getDefaultMessage());}
}
v4版本

API接口部分会使用到HttpServletRequestHttpServletResponse里的数据,如何方便的获取到这两个对象,常规做法就是每次用到的时候加在对应的控制层方法入参里.

public R selectOne(String id, HttpServletRequest request) {return null;
}

这样在切面打印入参的时候用多出一个request对象,多处用到的话也要一搜索全都是相同HttpServletRequest对象,这里我选择用一个抽象类去注入这两个对象

/*** @description: 控制层基类* @author: chenyunxuan* @updateTime: 2020/12/18 3:36 下午*/
public abstract class BaseController {@Autowiredprotected HttpServletRequest request;@Autowiredprotected HttpServletResponse response;
}

然后在控制层继承这个抽象类,就可以直接在方法里愉快的使用request和response对象了

public class UserController extends BaseController implements BaseCrud<User, UserSearch> {@Overridepublic R selectList(UserSearch userSearch) {request.getRequestURI();return null;}
}

后记

经过了四个版本的优化,给大家呈现的是一个完整的业务控制层脚手架

  1. 统一基础CRUD请求地址
  2. 优化入参校验
  3. 优化request和response对象获取方式

有更多技巧可以在评论区交流,希望能和大家交流编写更令人愉悦的API接口.下篇文章准备从接口版本控制入手,讲述接口新旧接口版本控制上的小技巧.欢迎持续关注.

这篇关于编写令人愉悦的API接口(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/694317

相关文章

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

利用Python编写一个简单的聊天机器人

《利用Python编写一个简单的聊天机器人》这篇文章主要为大家详细介绍了如何利用Python编写一个简单的聊天机器人,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 使用 python 编写一个简单的聊天机器人可以从最基础的逻辑开始,然后逐步加入更复杂的功能。这里我们将先实现一个简单的

Java后端接口中提取请求头中的Cookie和Token的方法

《Java后端接口中提取请求头中的Cookie和Token的方法》在现代Web开发中,HTTP请求头(Header)是客户端与服务器之间传递信息的重要方式之一,本文将详细介绍如何在Java后端(以Sp... 目录引言1. 背景1.1 什么是 HTTP 请求头?1.2 为什么需要提取请求头?2. 使用 Spr

使用PyQt5编写一个简单的取色器

《使用PyQt5编写一个简单的取色器》:本文主要介绍PyQt5搭建的一个取色器,一共写了两款应用,一款使用快捷键捕获鼠标附近图像的RGB和16进制颜色编码,一款跟随鼠标刷新图像的RGB和16... 目录取色器1取色器2PyQt5搭建的一个取色器,一共写了两款应用,一款使用快捷键捕获鼠标附近图像的RGB和16

使用SpringBoot创建一个RESTful API的详细步骤

《使用SpringBoot创建一个RESTfulAPI的详细步骤》使用Java的SpringBoot创建RESTfulAPI可以满足多种开发场景,它提供了快速开发、易于配置、可扩展、可维护的优点,尤... 目录一、创建 Spring Boot 项目二、创建控制器类(Controller Class)三、运行

使用Java编写一个文件批量重命名工具

《使用Java编写一个文件批量重命名工具》这篇文章主要为大家详细介绍了如何使用Java编写一个文件批量重命名工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录背景处理1. 文件夹检查与遍历2. 批量重命名3. 输出配置代码片段完整代码背景在开发移动应用时,UI设计通常会提供不

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo

Java 后端接口入参 - 联合前端VUE 使用AES完成入参出参加密解密

加密效果: 解密后的数据就是正常数据: 后端:使用的是spring-cloud框架,在gateway模块进行操作 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.0-jre</version></dependency> 编写一个AES加密

【LabVIEW学习篇 - 21】:DLL与API的调用

文章目录 DLL与API调用DLLAPIDLL的调用 DLL与API调用 LabVIEW虽然已经足够强大,但不同的语言在不同领域都有着自己的优势,为了强强联合,LabVIEW提供了强大的外部程序接口能力,包括DLL、CIN(C语言接口)、ActiveX、.NET、MATLAB等等。通过DLL可以使用户很方便地调用C、C++、C#、VB等编程语言写的程序以及windows自带的大

如何更优雅地对接第三方API

如何更优雅地对接第三方API 本文所有示例完整代码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/third 我们在日常开发过程中,有不少场景会对接第三方的API,例如第三方账号登录,第三方服务等等。第三方服务会提供API或者SDK,我依稀记得早些年Maven还没那么广泛使用,通常要对接第三方