请求参数解析

请求参数想必是很熟悉的,不过还是有许多的需要注意的。

以下问题是否注意过?

下面结合项目详解一下。

如ReqRecordFilter中就对请求参数进行了解析,输出到req-log中。

再比如 BodyReaderHttpServletRequestWrapper会对请求参数的流进行封装,避免请求因为i日志的打印提前消耗掉。

下面,从get请求参数解析说起。

Get请求参数解析

Get请求是Http协议中的一种请求方法,通常用于请求访问指定的资源,现在老是给搞不懂Get请求和Post请求。这里的一个理解是,如果过一个请求不会导致服务器上任何资源的状态变化,就可以使用Get请求。

HTTPServletRequest

看一个具体的例子。indexController方法:

@Controller
public class IndexController extends BaseViewController {
    @Autowired
    private IndexRecommendHelper indexRecommendHelper;

    @GetMapping(path = {"/", "", "/index", "/login"})
    public String index(Model model, HttpServletRequest request) {
        String activeTab = request.getParameter("category");
        IndexVo vo = indexRecommendHelper.buildIndexVo(activeTab);
        model.addAttribute("vo", vo);
        return "views/home/index";
    }
}

对应的业务,点击首页中的分类,会按照分类查询对应的页面信息。

这种情况就非常适合用Get请求,使用起来比较方便简单。

@GetMapping来定义GET请求路径

HttpServletRequest.getParameter方法来解析GET请求。这个方法接受一个字符串参数name,返回与之对应的请求参数的值,如果请求中没有name的参数,那么此方法返回null,如果请求中有多个名为name的参数,那么此方法返回其中的第一个值。

此时,请求的URL是这样的:http://127.0.0.1:8080/?category=后端,实际上,后端会被编码为http://127.0.0.1:8080/?category=%E5%90%8E%E7%AB%AF

@RequestParam

除了使用HttpServletRequest,还可以使用@RequestParam注解,@RequestParam是SpringMVC框架中的一个注解,用于从请求中获取参数。它将请求参数绑定到你的控制器方法的参数上。

一共四个参数,常用的是name(参数名)和required(是否必须),比如

ArticleSettingRestController的operate方法。

    @Permission(role = UserRole.ADMIN)
    @GetMapping(path = "operate")
    public ResVo<String> operate(@RequestParam(name = "articleId") Long articleId, @RequestParam(name = "operateType") Integer operateType) {
        OperateArticleEnum operate = OperateArticleEnum.fromCode(operateType);
        if (operate == OperateArticleEnum.EMPTY) {
            return ResVo.fail(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, operateType + "非法");
        }
        articleSettingService.operateArticle(articleId, operate);
        return ResVo.ok("ok");
    }

在pai-coding的前端,我们对GET请求进行了封装,主要通过axios来完成。

export const operateArticleApi = (params: object | undefined) => {
	return http.get<Login.ResAuthButtons>(`${PORT1}/article/operate`, params);
};

下面是一些复杂类型的参数,枚举、Map、List的实例。

写一个控制器,请求路径为get,然后通过crul进行测试。

/**
 * @ClAssName ParamTestController
 * @Description 测试解析请求参数用的控制器
 * 
 *
 */
@RestController
public class ParamTestController {

    public  enum  TYPE{
        A ,B ,C;
    }

    @GetMapping(path = "enum")
    public  String  enumParam(TYPE type){
        return type.name();
    }

    @GetMapping("eunm2")
    public  String  enumParam2(@RequestParam TYPE type){
        return type.name();
    }


    @GetMapping("mapper")
    public  String mapperParam(@RequestParam Map<String ,Object> parames){

        return parames.toString();
    }

    //这种写法无法获取请求参数,用于和上面做对比
    @GetMapping("mapper2")
    public  String mapperParam2(Map<String ,Object> parames ){
        return parames.toString();
    }

    @GetMapping("ano1")
    public String anoParam1(@RequestParam(name = "names")List<String> names){

        return "name: " + names;
    }


    //下面这个也无法正常解析数组
    @GetMapping("ano3")
    public String anoParam2(List<String> names){

        return "name: " + names;
    }




}

cURL 是一个功能强大的命令行工具,它可以用命令的形式来发送各种类型的 HTTP 请求。虽然它没有图形界面,但是却非常灵活,深受开发者们的喜爱。例如发送一个  

# curl http://paicoding.com

传递方式如下所示:

# curl -i http://localhost:9001/get/enum?type=A
GET http://localhost:9001/get/enum?type=A

A


GET http://localhost:9001/get/enum2?type=A
Accept: application/json

A

GET http://localhost:9001/get/mapper?type=A&age=3
Accept: application/json

{
  type=A,
  age=3
}

GET http://localhost:9001/get/mapper2?type=A&age=3
Accept: application/json

{}

GET http://localhost:9001/get/ano1?names=ou,ni,jia
Accept: application/json

name: [ou, ni, jia]


GET http://localhost:9001/get/ano3?names=ou,ni,jia
Accept: application/json

HTTP/1.1 500 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 20 Apr 2024 14:25:35 GMT
Connection: close

@PathVariable

@PathVariable是SpringMVC中的一个注解,可以将URL中的占位符参数绑定到控制器上处理方法的参数上。

来看 ArticleViewController的 detail 方法:

@GetMapping("detail/{articleId}")中的{articleId}是一个占位符,可以表示任何文章的ID
@PathVariable(name = "articleId") Long articleId获取这个文章的ID,并将其值绑定到articleId参数上。

对应的业务,访问任意一篇文章。

POST请求参数解析

与GET请求用于获取资源不同,POST请求主要用于提交数据到服务器端,尤其表单提交、文件上传等场景。

POST请求的数据通常放在请求体中,而不是像GET请求一样放在URL中,这意味着我们可以发送大量的数据,数据类型也更加丰富,如文本、JSON、XML、二进制数据等。

HttpServletRequest

同样可以像GET请求那样使用HttpServletRequest获取POST的请求参数,我们在TestController中新增一个方法,如下所示:

    @PostMapping("testPost")
    public  String testPost(HttpServletRequest request){
        String namre = request.getParameter("name");
        
        String age = request.getParameter("age");
        
        return  "name="  + namre  + ",  age = " +age;
    }
@PostMapping("testPost")指定POST请求路径
HttpServletRequest.getParameter()方法获取name 和age参数。

然后我们在Intellij IDEA中打开搜索对话框输入curl打开HTTP Client 工具。

输入以下内容进行访问。

POST http://localhost:9001/get/testPost
Content-Type: application/x-www-form-urlencoded

name=好小子&age=18

可以看到参数正常解析出来啦

回到开头的提问:POST请求的参数为JSON字符串的时候,HttpServletRequest能正确获取到参数吗?

来验证一下,增jia一个testPostJson方法:

    @PostMapping("testPostJson")
    public  String testPostJson( HttpServletRequest request){
        return JSONUtil.toJsonStr(request.getParameterMap());
        
    }

通过JSON的方式传递参数:

POST http://localhost:9001/get/testPostJson
Content-Type: application/json

{
  "name" : "好小子" ,
  "age" : 18
}

执行POST请求,我们会发现结果没有输出出来。

原因是啥?

HttpServletRequest.getParameter(String name)方法可以用于获取HTTP请求中的参数,但是这个方法通常于获取GET请求中的参数或者  Content-Type: application/x-www-form-urlencoded 中编码的POST请求参数。

如果POST请求中的参数是JSON格式的,那么这些参数通常位于请求的body中,getParameter()方法无法直接获取这些参数。

那如何用HttpServletRequest来获取呢?我们需要读取整个请求体。


    /**
     * POST请求,使用HttpServletRequest来获取JSON请求参数
     * @param request
     * @return
     */
    @PostMapping("testPostJson2")
    public  String  testPostJson2( HttpServletRequest request) {

        StringBuilder builder = new StringBuilder();
        BufferedReader reader = null;
        try {
            reader = request.getReader();
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return  builder.toString();
    }

此时就可以获取到参数啦。

HttpServletRequestWrapper

还有一个问题是:如果在切面日志中通过InputSteam中读取了参数打印,业务控制器中还能正确获取到参数吗?

答案是,如果不做处理,读不到。

底层涉及到Java的输入输出流,当我们从输入流中读取数据时,实际上是在消耗数据,一旦被读出来,就不能再次读取了。

从技术的角度来看,当我们从输入流中读取数据时,读取的位置(通常称为“游标” 或 “指针”)会向前移动。当读完所有的数据后,游标就移动到了流的末尾,所以不能在读取更多的数据了。

为了验证这个结论,可以这样做。

将,ReqRecordFilter的initReqInfo方法 的 request = this.wrapperRequest(request, reqInfo);

这行代码注释。

第二步,在 TestController 中再增加一个 testPostJson3 的方法,里面读取两次request。

/**
 * @ClAssName TestController
 * @Description 里面读取两次request
 * @Author 欧神仙
 * @Date 2024/4/21 21:{MINUTE}
 */
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @PostMapping("testPostJson3")
    public  String testPostJson3(HttpServletRequest request){
        StringBuilder sb = new StringBuilder();

        try {
            BufferedReader reader = request.getReader();
            String line ;
            while ((line = reader.readLine())!= null){
                sb.append(line);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        log.info("testPostJson3 第一次: {}" , sb);

        StringBuilder sb1 = new StringBuilder();
        try{
            BufferedReader reader1 = request.getReader();
            String line;
            while ((line = reader1.readLine()) != null){
                sb1.append(line);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        log.info("testPostJson3  第二次: {} ," , sb1 );
        return sb1.toString(); //body中即是JSON格式的请求参数
    }

第三步,再次请求 testPostJson3 接口

POST http://localhost:8080/test/testPostJson3
Content-Type: application/json

{
  "name" : "好小子" ,
  "age" : 18
}

可以看到结果如下,没有解析到请求参数。

 

再来看一下日志,同样可以验证这个结论,第一次可以读到,第二次读取不到了,也就意味着,当处理HTTP请求时,我们只能读取请求body一次,然后数据就会被清除。

ReqRecordFilter,他会把请求参数解析保存到req-log日志中。那么再我们开放HttpServletRequestWrapper的处理。
request = this.wrapperRequest(request, reqInfo);

再次请求 testPostJson3 接口,就会发现,能读取到结果啦。

如图:

日志中也可验证:

那么HttpServletRequestWrapper为什么能做到这点呢?下面来说。

HttpServletRequestWrapper是一个实现了HttpServletRequest接口的类,他被设计成一个装饰器类,主要用于对HttpServletRequest对象进行包装,提供一种修改请求对象的方式。

HttpServletRequestWrapper类的主要作用是再不修改原始请求对象的情况下,提供一种方式来修改请求中的信息,例如请求头或请求参数。

技术派项目中就是一个案列,BodyReaderHttpServletRequestWrapper 就是HttpServletRequestWrapper 类的一个具体实现,主要用于读取和缓存HTTP请求的body。

来具体分析下。

(1) 构造方法,根据请求内容类型和方法,读取并缓存请求body。

    private final byte[] body;
    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);

        if (POST_METHOD.contains(request.getMethod()) && !isMultipart(request) && !isBinaryContent(request) && !isFormPost(request)) {
            bodyString = getBodyString(request);
            body = bodyString.getBytes(StandardCharsets.UTF_8);
        } else {
            bodyString = null;
            body = null;
        }
    }

(2)重写 getReader和  getInputStream 方法,使得每次调用都会返回一个新的输入流或读取器,指向同一个缓存的请求body。这样,我们就可以多次读取请求body了。


    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (body == null) {
            return super.getInputStream();
        }

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }

(3) hasPayload 方法返回一个布尔值,表示是否存在请求body。

    public boolean hasPayload() {
        return bodyString != null;
    }

(4)getBodyString 方法返回缓存的请求body

private String getBodyString(HttpServletRequest request) {
        BufferedReader br;
        try {
            br = request.getReader();
        } catch (IOException e) {
            logger.warn("Failed to get reader", e);
            return "";
        }

        String str;
        StringBuilder body = new StringBuilder();
        try {
            while ((str = br.readLine()) != null) {
                body.append(str);
            }
        } catch (IOException e) {
            logger.warn("Failed to read line", e);
        }

        try {
            br.close();
        } catch (IOException e) {
            logger.warn("Failed to close reader", e);
        }

        return body.toString();
    }

到此为封装的过程。

@RequestBody

@RequestBody 注解用于将请求体绑定到一个方法的参数上。通常用于处理POST、PUT

等方法的请求,这些请求通常会在请求体中发送数据。在方法参数上添加@RequestBody注解,Spring会使用一个HttpMessageConverter将请求体转换为对应的Java对象。

HttpMessageConverter使用请求头中的Content-Type字段来决定如何解析请求体,例如,如果

Content-Type是“application/json” ,那么将使用JSON转换器将请求体解析为Java对象。

技术派中使用的是MappingJackson2HttpMessageConverter和MappingJackson2XmlHttpMessageConverter,用于对JSON和XML进行转换。

MappingJackson2HttpMessageConverter转换器用于将HTTP请求或响应体中的JSON数据转换为Java对象,或将Java对象转换为JSON数据,它使用Jackson库来进行转换,Jackson是一个可以将Java对象转换为JSON字符串,也可以将JSON字符串转换为Java对象的库。

来看一个具体的用法。

    @PostMapping(path = "save")
    public ResVo<String> save(@RequestBody TagReq req) {
        tagSettingService.saveTag(req);
        return ResVo.ok("ok");
    }
@PostMapping用于指定POST请求路径
@RequestBody 注解用于将请求参数绑定到方法参数上

本例中的TagReq是一个使用了Lombok库的Requst对象。

@Data
public class TagReq implements Serializable {

    /**
     * ID
     */
    private Long tagId;

    /**
     * 标签名称
     */
    private String tag;

    /**
     * 类目ID
     */
    private Long categoryId;
}

对用的业务端是admin端保存标签。

逻辑:

参数id为空或为0,新增。已存在就更新。

    @Override
    public void saveTag(TagReq tagReq) {
        TagDO tagDO = ArticleConverter.toDO(tagReq);
        if (NumUtil.nullOrZero(tagReq.getTagId())) {
            tagDao.save(tagDO);
        } else {
            tagDO.setId(tagReq.getTagId());
            tagDao.updateById(tagDO);
        }
    }

MultipartFile

MultipartFile是Spring中处理文件上传的一个接口,当我们通过multipart/form-data形式的HTTP POST请求上传文件时,Spring MVC会将上传的文件包装为MultipartFile对象。

MultipartFile 接口提供了一系列方法,可以方便地获取上传文件的信息和内容:

  • getOriginalFilename():获取客户端发送的原始文件名
  • getName():获取表单中文件组件的名字
  • isEmpty():返回上传的文件是否为空,
  • getSize():返回上传的文件的大小,单位是字节
  • getContentType():返回文件的 MIME 类型
  • getBytes():返回文件的内容,以字节数组的形式,
  • getlnputStream():返回一个InputStream,可以从中读取文件的内容·
  • transferTo(File dest):将上传的文件保存到一个目标文件或目录。


我们可以通过 @RequestParam 注解的形式直接获取一个 MultipartFile 对象,例如:

    @PostMapping("/upload")
    public  String handleFileUpload(@RequestParam("file")MultipartFile file){
        String originalFilename = file.getOriginalFilename();
        //....

    }

项目中是通过MutipartHttpServletRequest的方式从POST请求中获取文件对象的。

MultipartHttpServletRequest 是 Spring MVC 提供的一个接口,它用于处理multipart/form-data 类型的 HTTP 请求,也就是常见的文件上传请求。

    @Override
    public String saveImg(HttpServletRequest request) {
        MultipartFile file = null;
        if (request instanceof MultipartHttpServletRequest) {
            file = ((MultipartHttpServletRequest) request).getFile("image");
        }
        if (file == null) {
            throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "缺少需要上传的图片");
        }

        // 目前只支持 jpg, png, webp 等静态图片格式
        String fileType = validateStaticImg(file.getContentType());
        if (fileType == null) {
            throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "图片只支持png,jpg,gif");
        }

        try {
            return ossUploader.upload(file.getInputStream(), fileType);
        } catch (IOException e) {
            log.error("Parse img from httpRequest to BufferedImage error! e:", e);
            throw ExceptionUtil.of(StatusEnum.UPLOAD_PIC_FAILED);
        }
    }

补充:

是的,名为 "image" 的文件是在之前的请求中存在的。在处理文件上传时,客户端会将文件以表单字段的形式发送给服务器。在这个例子中,"image" 是一个表单字段的名称,用于标识要上传的文件。当服务器接收到包含文件上传的请求时,它会解析请求中的表单数据,并使用 `getFile()` 方法获取指定字段名的文件。因此,这个名为 "image" 的文件是在之前的请求中存在的,并且被服务器接收和处理。

小结:

在一个 Web 应用中,处理 HTTP 请求参数是非常常见的任务。常见的解析请求参数的方法有:

  • 通过 HttpServletRequest:可以使用 request.getParameter(name)来获取 GET 或POST 请求中的参数。
  • 使用@RequestParam 注解来获取请求参数。
  • 如果 URL 中包含路径变量,比如/user/{id},你可以使用@PathVariable 注解获取这个路径变量的值。
  • 如果请求的 Content-Type为application/json,可以使用@RequestBody 注解将请求体中的 JSON 数据自动绑定到一个 Java 对象上,
  • 对于文件上传请求,可以使用 MultipartHttpServletRequest 或 MultipartFile 来获取上传的文件。
  • 26
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值