• 《优化接口设计的思路》系列:第一篇—接口参数的一些弯弯绕绕


    前言

    大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

    作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。

    接口参数是导致很多BUG产生的始作俑者,原因在于接口参数有3多:接口参数的取值地方多,如查询参数(Query Parameters)、路径参数(Path Parameters)、请求体(Request Body)等;数据类型多,如数字、字符、日期、文件等;判断情况多,如空值判断、格式判断、大小判断等;

    本文参考项目源码地址:summo-springboot-interface-demo

    一、接口参数的取值

    1. 放在查询参数和请求体里

    a、方法参数

    示例代码如下:

    @GetMapping("/testParams1")
    public ResponseEntity testParams1(String param1, Integer param2) {
      return ResponseEntity.ok(MessageFormat.format("param1:[{0}];param2:[{1}]", param1, param2));
    }
    

    调用请求:http://localhost:8080/testParams1?param1=111¶m2=222
    返回如下:

    没啥坑。

    b、请求对象

    GET请求

    示例代码如下

    @GetMapping("/testParams2")
    public ResponseEntity testParams2(ParamsReq paramsReq) {
      return ResponseEntity.ok(MessageFormat.format("param1:[{0}];param2:[{1}]", paramsReq.getParam1(), paramsReq.getParam2()));
    }
    

    ParamsReq.java

    public class ParamsReq {
    
        private String param1;
    
        private String param2;
    
        public ParamsReq() {
        }
    
        public ParamsReq(String param1, String param2) {
            this.param1 = param1;
            this.param2 = param2;
        }
    
        public String getParam1() {
            return param1;
        }
    
        public void setParam1(String param1) {
            this.param1 = param1;
        }
    
        public String getParam2() {
            return param2;
        }
    
        public void setParam2(String param2) {
            this.param2 = param2;
        }
    
        @Override
        public String toString() {
            return "ParamsReq{" +
                "param1='" + param1 + '\'' +
                ", param2='" + param2 + '\'' +
                '}';
        }
    }
    
    

    调用请求:http://localhost:8080/testParams2?param1=111¶m2=222
    返回如下:

    这种有一个坑,Spring默认使用无参构造函数来实例化对象,所以ParamsReq不能是接口、抽象类等特殊类。

    POST请求

    示例代码如下:

    @PostMapping("/testParams3")
    public ResponseEntity testParams3(ParamsReq paramsReq) {
      return ResponseEntity.ok(MessageFormat.format("param1:[{0}];param2:[{1}]", paramsReq.getParam1(), paramsReq.getParam2()));
    }
    

    ParamsReq类代码同上

    • 调用方式1:参数放在链接上

    和GET请求类似,没啥坑。

    • 调用方式2:放在Form表单中,content-type为application/x-www-form-urlencoded

    没啥坑。

    • 调用方式3:放在body参数中,content-type为application/json

    这里有坑了,当content-type为application/json时,接口参数取值为空。这时就需要在参数前加上一个注解:@RequestBody,原因是通过@RequestBody注解,Spring Boot可以自动地将请求体中的JSON数据转换为Java对象,从而方便地进行数据的处理和转换。如果不加@RequestBody注解,Spring Boot默认会将请求体中的JSON数据作为普通的表单数据来处理,而不会自动转换为Java对象。:

    改成这样就行:public ResponseEntity testParams3(@RequestBody ParamsReq paramsReq)

    2. 放在路径参数上

    示例代码如下:

    @GetMapping("/testParams4/{pathParam}")
    public ResponseEntity testParams4(@PathVariable("pathParam") String pathParam) {
      return ResponseEntity.ok(MessageFormat.format("pathParam:[{0}];", pathParam));
    }
    

    参数使用{}框起来,然后使用@PathVariable即可获取到值,坑不多。

    3. 放在请求头和Cookie

    这两种情况里面的参数主要是标识类的参数如userToken,一般都是不变的,业务中很少使用到。

    二、接口参数的类型

    1. 数字、字符串

    没啥坑。

    2. 日期

    示例代码如下:

    @GetMapping("/testParams5")
    public ResponseEntity testParams5(Date date) {
      return ResponseEntity.ok(MessageFormat.format("pathParam:[{0}];", date));
    }
    

    这里有个问题,这样的接口前端怎么传这个date值,字符串?时间戳?我已经替大家试过了,都不行,接口直接报400。

    正确的做法是在日期参数前加上@DateTimeFormat注解,改成这样就行了:public ResponseEntity testParams5(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date date)
    传参的话传字符串:2023-09-14 00:00:00 即可

    3. 列表

    示例代码如下:

    @GetMapping("/testParams6")
    public ResponseEntity testParams6(List paramList) {
      return ResponseEntity.ok(MessageFormat.format("paramList:[{0}];", paramList));
    }
    

    这串代码不用测试,它本身就是错误的,前面说过Spring默认使用无参构造函数来实例化对象,但是List是一个接口,没有无参构造函数。
    为了解决这个问题,可以使用Spring的@RequestParam注解来指定参数名,并将多个参数值绑定到一个List对象中。

    修改代码如下:public String testParams6(@RequestParam("paramList") List paramList) 即可
    然后,通过使用逗号分隔的参数值来访问接口,如:http://localhost:8080/testParams6?paramList=1,2,3
    这样就可以成功传递参数列表并访问接口了。

    4. 文件

    先写一个简单上传界面
    upload.html

    html>
    <html>
    <head>
        <title>File Upload Demotitle>
    head>
    <body>
        <h1>File Upload Demoh1>
        <form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
            <input type="file" name="file" />
            <br/><br/>
            <input type="submit" value="Upload" />
        form>
    body>
    html>
    

    后端上传代码
    FileUploadController.java

    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.multipart.MultipartFile;
    
    @Controller
    public class FileUploadController {
    
        @PostMapping("/upload")
        public ResponseEntity uploadFile(@RequestParam("file") MultipartFile file) {
            // Check if file is empty
            if (file.isEmpty()) {
                return new ResponseEntity<>("File is empty", HttpStatus.BAD_REQUEST);
            }
    
            // Save the file
            try {
                byte[] bytes = file.getBytes();
                // Logic to save the file to a desired location
    
                return new ResponseEntity<>("File uploaded successfully", HttpStatus.OK);
            } catch (Exception e) {
                return new ResponseEntity<>("Failed to upload file", HttpStatus.INTERNAL_SERVER_ERROR);
            }
        }
    }
    

    上传接口后端其实还好,主要是前端需要处理的内容多一些,由于MultipartFile类也是一个接口,所以这里也需要加上@RequestParam注解。

    三、接口参数的判断

    前面提到的@RequestBody@RequestParam注解都是SpringBoot自带的,它们主要的功能是将请求参数转换为我们接口定义的变量或者Java对象,而校验参数值是否合法通常有下面几种做法:

    • 自己写校验逻辑,一般是配合使用Assert进行参数校验
    • 使用javax.validation包的校验注解,如@NotNull@NotBlank

    这里主要讲一下javax.validation如何使用!

    1. pom.xml引入

    
    <dependency>
      <groupId>javax.validationgroupId>
      <artifactId>validation-apiartifactId>
      <version>2.0.1.Finalversion>
    dependency>
    

    2. 注解分类

    a. 空值检查

    注解 说明 使用频率
    @NotNull 不能为null,常用于数字、日期 常用
    @NotBlank 不能为null也不能为空,常用于字符串 常用
    @NotEmpty 集合不能为空,常用于List、Map、Set 常用

    b. 数值检查

    注解 说明 使用频率
    @Max 被注释的元素必须小于等于指定的值 常用
    @Min 被注释的元素必须大于等于指定的值 常用
    @Positive 被注释的元素必须是正数 不常用
    @Negative 被注释的元素必须是负数 不常用

    c. Boolean 检查

    注解 说明 使用频率
    @AssertFalse 被注释的元素必须是false 常用
    @AssertTrue 被注释的元素必须是true 常用

    d. 日期检查

    注解 说明 使用频率
    @Future 被注释的元素必须是将来的日期 不常用
    @Past 被注释的元素必须是过去的日期 不常用

    e. 日期检查

    注解 说明 使用频率
    @Email 被注释的元素必须是电子邮箱地址 常用
    @Pattern 被注释的元素必须是符合正则表达式,我经常使用这个判断手机号是否合法 常用

    3. 使用方法

    下面是一个经典的案例

    @Data
    public class StudentReq {
        @NotBlank(message = "主键不能为空")
        private String id;
        @NotBlank(message = "名字不能为空")
        @Size(min = 2, max = 4, message = "名字字符长度必须为 2~4个")
        private String name;
        @Pattern(regexp = "^1[3456789]\\d{9}$", message = "手机号格式错误")
        private String phone;
        @Email(message = "邮箱格式错误")
        private String email;
        @Past(message = "生日必须早于当前时间")
        private Date birth;
        @Min(value = 0, message = "年龄必须为 0~100")
        @Max(value = 100, message = "年龄必须为 0~100")
        private Integer age;
        @PositiveOrZero
        private Double score;
    }
    

    这些东西看看就行了,用的时候翻一下文档就行,记也记不住。

    四、一些可以直接获取到的参数

    • HttpServletRequest:用于获取HTTP请求的相关信息,包括请求头、请求参数、请求方法等。
    • HttpServletResponse:用于控制HTTP响应,包括设置响应状态码、设置响应头、发送响应内容等。
    • HttpSession:用于获取当前会话的信息,可以用来存储和获取会话级别的数据。
    • Principal:用于获取当前用户的身份信息,通常用于认证和授权。
    • Model/ModelMap:用于在请求处理方法中传递数据给前端视图。
    • BindingResult:用于获取请求参数绑定和验证的结果,包含了校验的错误信息。
    • Locale:用于获取当前请求的语言环境,可以用来进行国际化处理。
    • MultipartFile(或者List):用于处理上传的文件,包括文件的名称、大小、内容等。
    • RedirectAttributes:用于在重定向时传递数据给目标页面。
    • ServletRequest/ServletResponse:HttpServletRequest/HttpServletResponse的父类,可以使用其提供的通用方法。
    • @ModelAttribute注解:用于获取请求参数,并将其绑定到一个对象上。

    这些对象可以直接在接口参数上使用,通过框架自动注入的方式获取其实例。在使用时,需要保证框架已经正确配置和启用了对应的注解和拦截器。用的最多的就是HttpServletRequest和HttpServletResponse了。

    作者:不若为止
    欢迎任何形式的转载,但请务必注明出处。
    限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

  • 相关阅读:
    wireshark通常无法抓取交换机所有端口报文
    图形学-着色频率与渲染管线
    linux最佳入门(笔记)
    vue设置cookie和路由守卫
    Google protobuf使用技巧和经验总结
    基于物联网的自动灌溉系统的设计与实现
    Python实现单例模式
    点击微信公众号菜单发送图片
    JavaIO系列——字节缓冲流,对象流,序列化与反序列化
    【python海洋专题十九】找范围的语句进阶版本
  • 原文地址:https://www.cnblogs.com/wlovet/p/17700353.html