怎么编写API接口

网友投稿 164 2024-02-02


怎么编写API接口

本文主要介绍"如何编写API接口",希望能够解决您遇到有关问题,下面我们一起来看这篇 "如何编写API接口" 文章。

思考

业务接口通常都包含最基础的CRUD操作,怎么尽可能的统一这部分接口?API接口部分会使用到HttpServletRequest和HttpServletResponse里的数据,如何方便的获取到这两个对象?接口的参数有时通过单个参数传递,有时会通过对象传递,如何方便的检验这些参数,而不用频繁的使用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     @ResponseBody     public R selectList(Map s) {         return null;     }     @Override     @GetMapping("{id}")     @ResponseBody     public R selectOne(String id) {         return null;     }     @Override     @PostMapping     @ResponseBody     public R add(Map t) {         return null;     }     @Override     @PutMapping("{id}")     @ResponseBody     public R upp(String id, Map t) {         return null;     }     @Override     @DeleteMapping("{id}")     @ResponseBody     public 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 上午      */     @GetMapping     R 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 下午      */     @PostMapping     R 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> {     @Override     public R selectList(UserSearch userSearch) {         return null;     }     @Override     public R selectOne(String id) {         return null;     }     @Override     public R add(User user) {         return null;     }     @Override     public R upp(String id, User user) {         return null;     }     @Override     public 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 上午      */     @GetMapping     R selectList(@Validated @ModelAttribute S s);     /**      * @description: 新增单条数据      * @author: chenyunxuan      * @updateTime: 2020/12/18 1:39 下午      */     @PostMapping     R 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不能为空")     @Email     private String email;     @NotNull     @Min(18)     @Max(100)     private Integer age;     @DateTimeFormat(pattern = "MM/dd/yyyy")     //不可为空且必须是在系统时间之前     @NotNull     @Past     private LocalDateTime birthday; }

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

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

预设的校验有时是不可以满足业务校验要求的,比如电话号码校验,身份证校验等等.好在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}$"     );     @Override     public void initialize(MobileVail constraintAnnotation) {     }     @Override     public 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中注明他的分组即可.

@PostMapping     R 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)     @ResponseBody     public 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)     @ResponseBody     public 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)     @ResponseBody     public 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接口部分会使用到HttpServletRequest和HttpServletResponse里的数据,如何方便的获取到这两个对象,常规做法就是每次用到的时候加在对应的控制层方法入参里.

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

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

/**  * @description: 控制层基类  * @author: chenyunxuan  * @updateTime: 2020/12/18 3:36 下午  */ public abstract class BaseController {     @Autowired     protected HttpServletRequest request;     @Autowired     protected HttpServletResponse response; }public class UserController extends BaseController implements BaseCrud<User, UserSearch> {     @Override     public R selectList(UserSearch userSearch) {         request.getRequestURI();         return null;     } }

编程技术 和 程序设计

本文主要介绍"什么是AQS、ReentrantLock",希望能够解决您遇到有关问题,下面我们一起来看这篇 "什么是AQS、ReentrantLock" 文章。(一)AQS概述Java并发编程的核心 ...


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:axios接口管理优化操作详解
下一篇:java怎么实现Callable接口创建线程类
相关文章

 发表评论

暂时没有评论,来抢沙发吧~