编写令人愉悦的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

相关文章

SpringBoot+Redis防止接口重复提交问题

《SpringBoot+Redis防止接口重复提交问题》:本文主要介绍SpringBoot+Redis防止接口重复提交问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录前言实现思路代码示例测试总结前言在项目的使用使用过程中,经常会出现某些操作在短时间内频繁提交。例

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

HTML5 getUserMedia API网页录音实现指南示例小结

《HTML5getUserMediaAPI网页录音实现指南示例小结》本教程将指导你如何利用这一API,结合WebAudioAPI,实现网页录音功能,从获取音频流到处理和保存录音,整个过程将逐步... 目录1. html5 getUserMedia API简介1.1 API概念与历史1.2 功能与优势1.3

spring中的ImportSelector接口示例详解

《spring中的ImportSelector接口示例详解》Spring的ImportSelector接口用于动态选择配置类,实现条件化和模块化配置,关键方法selectImports根据注解信息返回... 目录一、核心作用二、关键方法三、扩展功能四、使用示例五、工作原理六、应用场景七、自定义实现Impor

MybatisPlus service接口功能介绍

《MybatisPlusservice接口功能介绍》:本文主要介绍MybatisPlusservice接口功能介绍,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友... 目录Service接口基本用法进阶用法总结:Lambda方法Service接口基本用法MyBATisP

python编写朋克风格的天气查询程序

《python编写朋克风格的天气查询程序》这篇文章主要为大家详细介绍了一个基于Python的桌面应用程序,使用了tkinter库来创建图形用户界面并通过requests库调用Open-MeteoAPI... 目录工具介绍工具使用说明python脚本内容如何运行脚本工具介绍这个天气查询工具是一个基于 Pyt

MyBatis编写嵌套子查询的动态SQL实践详解

《MyBatis编写嵌套子查询的动态SQL实践详解》在Java生态中,MyBatis作为一款优秀的ORM框架,广泛应用于数据库操作,本文将深入探讨如何在MyBatis中编写嵌套子查询的动态SQL,并结... 目录一、Myhttp://www.chinasem.cnBATis动态SQL的核心优势1. 灵活性与可

Mybatis嵌套子查询动态SQL编写实践

《Mybatis嵌套子查询动态SQL编写实践》:本文主要介绍Mybatis嵌套子查询动态SQL编写方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、实体类1、主类2、子类二、Mapper三、XML四、详解总结前言MyBATis的xml文件编写动态SQL

Java中的Closeable接口及常见问题

《Java中的Closeable接口及常见问题》Closeable是Java中的一个标记接口,用于表示可以被关闭的对象,它定义了一个标准的方法来释放对象占用的系统资源,下面给大家介绍Java中的Clo... 目录1. Closeable接口概述2. 主要用途3. 实现类4. 使用方法5. 实现自定义Clos

java对接第三方接口的三种实现方式

《java对接第三方接口的三种实现方式》:本文主要介绍java对接第三方接口的三种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录HttpURLConnection调用方法CloseableHttpClient调用RestTemplate调用总结在日常工作