请求参数想必是很熟悉的,不过还是有许多的需要注意的。
以下问题是否注意过?
下面结合项目详解一下。
如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 来获取上传的文件。