换种方式看后端参数接收、建议躺着看!!!

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情 常用的接收参数注解

@RequestParam
@PathVariable
@RequestBody
复制代码

先看个例子

@RestController
public class testController {
    @RequestMapping(value = "/demo1",method = RequestMethod.GET)
    public String uploadFolder(String username,String password) {
        System.out.println(username);
        System.out.println(password);
        return "ok";
    }
}
复制代码

写了个很简单的例子传递两个参数一个是用户名一个是密码、请求方式是get请求、那我们肯定是直接访问一下这个127.0.0.1:8080/demo1?username=abcd&password=123请求就能接收到前端输入的

测试.gif

@RequestParam

可以看到这种简单简单的传参是很方便的、你也可以去使用@RequestParam()注解去去一个接收参数的别名、如

@RequestMapping(value = "/demo1",method = RequestMethod.GET)
public String uploadFolder(@RequestParam("username1") String username,
                           @RequestParam("password1") String password) {
    System.out.println(username);
    System.out.println(password);
    return "ok";
}
复制代码

则请求参数就是127.0.0.1:8080/demo1?username1=abcd&password1=123就是以这种形式传递到后端。

在html中页面如何提交?

<form action="/demo1" method="get">
    <input type="text" name="username">
    <input type="text" name="password">
    <button type="submit">提交</button>
</form>
复制代码

html提交.gif

@PathVariable

再来说另一种简单的就是@PathVariable()注解使用、这种你就可以理解为它回去找url中的内容、而不是作为?或者&拼接传递过来的内容。

@RequestMapping(value = "/demo2/{id}",method = RequestMethod.GET)
public String demo2(@PathVariable(value = "id") String id,
                           @RequestParam("username") String username,
                           @RequestParam("password") String password) {
    System.out.println(id);
    System.out.println(username);
    System.out.println(password);
    return "ok";
}
复制代码

可以看到他标记了id为url拼接的参数、http://127.0.0.1:8981/demo2/243843?username=abcd&password=123、如果请求是这个http://127.0.0.1:8981/demo2?username=abcd&password=123会怎么样?肯定是找不到对应请求......

PathVariable.gif

那上面这种如何在html中使用?

<form action="/demo2/213124" method="get">
    <input type="text" name="username">
    <input type="text" name="password">
    <button type="submit">提交</button>
</form>
复制代码

也就是在action填写参数的占位内容找到具体的Controller接口方法。

@RequestBody

还有一种接收的方式@RequestBody这种是接收请求体中的内容、下面这方式是将接收一个json字符串的格式数据

@RequestMapping(value = "/demo3",method = RequestMethod.GET)
public String demo3(@RequestBody String body) {
    System.out.println(body);
    return "ok";
}
复制代码

RequestBody.gif

Map接收

或者换成Map做接收的参数、就会将数据解析成键值对。

@RequestMapping(value = "/demo3",method = RequestMethod.GET)
public String demo3(@RequestBody Map<String,Object> body) {
    System.out.println(body);
    return "ok";
}
复制代码

对象接收

最最常见的就是发送json后端使用实体类来接收、并且Header要是"Content-Type", "application/json"

@RequestMapping(value = "/demo4",method = RequestMethod.GET)
public String demo4(@RequestBody User user) {
    System.out.println(user);
    return "ok";
}
复制代码

要注意的就是User对象要写get、set方法。

表单提交

然后这种在html提交需要怎么弄?

<form action="/demo4" method="get">
    <input type="text" name="id">
    <input type="text" name="name">
    <button type="submit">提交</button>
</form>
复制代码
@RequestMapping(value = "/demo4")
public String demo4(User user) {
    System.out.println(user);
    return "ok";
}
复制代码

@RequestBody去掉后在form表单提交时不管是post还是get请求都能将数据映射成功。

由此可以理解为@RequestBody只接受请求体中的数据、并且会校验Content-Typeapplication/json的数据。

@RequestMapping(value = "/demo4")
public String demo4(@RequestBody User user) {
    System.out.println(user);
    return "ok";
}
复制代码

如果加上@RequestBody后from表单使用get请求会出现

2022-10-11 10:13:05.902  WARN 11440 --- [nio-8981-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public java.lang.String com.wei.controller.testController.demo4(com.wei.pojo.User)]
复制代码

使用get请求的话参数是跟在url后的所以他会找不到请求体的内容。

如果加上@RequestBody后from表单使用post请求会出现

2022-10-11 10:10:30.073  WARN 12856 --- [nio-8981-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported]
复制代码

而post请求参数是携带在请求体中的、但是它的Content-Typeapplication/x-www-form-urlencoded不是application/json所以会导致不支持的类型。

组合方式

这三种方式也可以组合使用@PathVariable+@RequestParam@PathVariable+@RequestBody@PathVariable+@RequestParam+@RequestBody

玩法很多种、但别瞎玩、瞎玩就是折磨自己!!!

骚操作

通过form表单提交数据、映射数据到对象中、还是上班后发现的能这样玩、但是我在b站看MVC的教程发现别人有教!淦、我之前看的MVC视频都没教我这样玩!!!

@RequestMapping(value = "/demo6")
public String demo6(ParameterDTO parameterDTO) {
    System.out.println(parameterDTO);
    return "ok";
}
复制代码
@Data
public class ParameterDTO {
    private Long id;
    private List<Long> idS;
    private Map<String,String> map;
    private MultipartFile multipartFile;
    private List<MultipartFile> multipartFiles;
    private ExtraSingle extraSingle;
    private List<ExtraMore> extraMores;

    @Override
    public String toString() {
        return "ParameterDTO{" +
                "id=" + id +
                ", idS=" + idS +
                 ", map=" + map +
                ", multipartFile=" + multipartFile.getResource().getFilename() +
                ", multipartFiles=" + multipartFiles.stream().map((Function<MultipartFile, Object>) multipartFile -> multipartFile.getResource().getFilename()).collect(Collectors.toList()) +
                ", extraSingle=" + extraSingle +
                ", extraMores=" + extraMores +
                '}';
    }
}

@Data
class ExtraSingle {
    private String extraSingleName;
    private List<Long> extraSingleIdS;
    private MultipartFile extraSingleMultipartFile;
    private List<MultipartFile> extraSingleMultipartFiles;

    @Override
    public String toString() {
        return "ExtraSingle{" +
                "extraSingleName='" + extraSingleName +
                ", extraSingleIdS=" + extraSingleIdS +
                ", extraSingleMultipartFile=" + extraSingleMultipartFile.getResource().getFilename() +
                ", extraSingleMultipartFiles=" + extraSingleMultipartFiles.stream().map((Function<MultipartFile, Object>) multipartFile -> multipartFile.getResource().getFilename()).collect(Collectors.toList()) +
                '}';
    }
}

@Data
class ExtraMore {
    private String extraMoreName;
    private List<Long> extraMoreIdS;
    private MultipartFile extraMoreMultipartFile;
    private List<MultipartFile> extraMoreMultipartFiles;

    @Override
    public String toString() {
        return "ExtraMore{" +
                "extraMoreName='" + extraMoreName +
                ", extraMoreIdS=" + extraMoreIdS +
                ", extraMoreMultipartFile=" + extraMoreMultipartFile.getResource().getFilename() +
                ", extraMoreMultipartFiles=" + extraMoreMultipartFiles.stream().map((Function<MultipartFile, Object>) multipartFile -> multipartFile.getResource().getFilename()).collect(Collectors.toList()) +
                '}';
    }
}
复制代码
<form action="/demo6" method="post" enctype="multipart/form-data">
    <input type="text" name="id" value="2022">
    <input type="text" name="idS" value="2020,2021,2022">
    <input type="text" name="map[abc]" value="giao">
    <input type="text" name="map[abcKey]" value="giaogiao">
    <input type="file" name="multipartFile">
    <input type="file" name="multipartFiles[0]">
    <input type="file" name="multipartFiles[1]">

    <br>
    <input type="text" name="extraSingle.extraSingleName" value="extraSingleName名称">
    <input type="text" name="extraSingle.extraSingleIdS" value="123">
    <input type="file" name="extraSingle.extraSingleMultipartFile">
    <input type="file" name="extraSingle.extraSingleMultipartFiles[0]">
    <input type="file" name="extraSingle.extraSingleMultipartFiles[1]">


    <br>
    <input type="text" name="extraMores[0].extraMoreName" value="extraMoreName名称1">
    <input type="text" name="extraMores[0].extraMoreIdS" value="1,2,3">
    <input type="file" name="extraMores[0].extraMoreMultipartFile">
    <input type="file" name="extraMores[0].extraMoreMultipartFiles[0]">
    <input type="file" name="extraMores[0].extraMoreMultipartFiles[1]">
    <br>
    <input type="text" name="extraMores[1].extraMoreName" value="extraMoreName名称2">
    <input type="text" name="extraMores[1].extraMoreIdS" value="4,5,6">
    <input type="file" name="extraMores[1].extraMoreMultipartFile">
    <input type="file" name="extraMores[1].extraMoreMultipartFiles[0]">
    <input type="file" name="extraMores[1].extraMoreMultipartFiles[1]">
    <button type="submit">提交</button>
</form>
复制代码

测试结果

ParameterDTO{id=2022, idS=[2020, 2021, 2022], map={abcKey=giaogiao, abc=giao},multipartFile=1_2020-08-21_946.png, multipartFiles=[1_2020-08-21_946.png, 1_2020-08-21_946.png], extraSingle=ExtraSingle{extraSingleName='extraSingleName名称', extraSingleIdS=[123], extraSingleMultipartFile=1_2020-08-21_946.png, extraSingleMultipartFiles=[1_2020-08-21_946.png, 1_2020-08-21_946.png]}, extraMores=[ExtraMore{extraMoreName='extraMoreName名称1', extraMoreIdS=[1, 2, 3], extraMoreMultipartFile=1_2020-08-21_946.png, extraMoreMultipartFiles=[1_2020-08-21_946.png, 1_2020-08-21_946.png]}, ExtraMore{extraMoreName='extraMoreName名称2', extraMoreIdS=[4, 5, 6], extraMoreMultipartFile=1_2020-08-21_946.png, extraMoreMultipartFiles=[1_2020-08-21_946.png, 1_2020-08-21_946.png]}]}
复制代码

要注意的是一些版本可能不支持这样!要注意的是MultipartFile文件类型可能有些版本不会有自动设置值的操作可以换一种方式

Map<String, MultipartFile> fileMap = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class).getFileMap();
复制代码

拿到所有的文件对象、通过name拿你需要的文件对象就可以了。

解决疑惑

参数值是这么注入数据?Controller中的方法值每一个参数都去找到一个方法参数解析器也就xxxMethodArgumentResolver的一个类、如常见的在方法中写model对象或者request对象时都是通过对应的参数解析器把参数解析后设置对象供我们使用。

public String index(Model model,HttpServletRequest request){
    ...
}
复制代码

自己写一个参数解析器

/**
 *  自定义方法参数解析器
 * @author https://juejin.cn/user/844892408381735
 * @Date 2022/10/11 16:39
 */
public class CustomArgumentResolver implements HandlerMethodArgumentResolver {
    /**
     *  参数是否支持
     * @param parameter 方法参数类型信息、如对应的参数前的注解信息
     * @return 返回true 这表示使用该解析器、后续着会调用 下方resolveArgument(...)方法
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return User.class.equals(parameter.getParameterType());
    }

    /**
     *  处理并生成参数对象返回
     * @param parameter  方法参数对象
     * @param mavContainer  可以理解为model对象
     * @param webRequest  可以理解为是 HttpServletRequest 请求对象
     * @param binderFactory 数据绑定工厂
     * @return 返回参数对象
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        User user = new User();
        user.setName("自动注入....");
        return user;
    }
}
复制代码

添加到参数解析器中、设置为最前面方便测试。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    @PostConstruct
    public void init() {
        //获取到自定义requestMappingHandlerAdapter的属性(只读)
        List<HandlerMethodArgumentResolver> resolvers = requestMappingHandlerAdapter.getArgumentResolvers();
        //重新创建集合对象
        List<HandlerMethodArgumentResolver> newResolvers =
                new ArrayList<>(resolvers.size() + 1);
        // 添加 自定义解析器 到集合首位
        newResolvers.add(new CustomArgumentResolver());
        // 添加 已注册的 Resolver 对象集合
        newResolvers.addAll(resolvers);
        // 重新设置 Resolver 对象集合
        requestMappingHandlerAdapter.setArgumentResolvers(newResolvers);
    }
}
复制代码

测试

@RequestMapping(value = "/demo4")
public String demo4(@RequestBody User user) {
    System.out.println(user);
    return "ok";
}
复制代码

测试结果

User(id=null, name=自动注入...., age=null, sex=null)
复制代码

可以发现@RequestBody是不起作用的、直接是使用我们自定义的CustomArgumentResolver类中resolveArgument()方法返回的对象做为参数塞到Controller请求方法中的。

由此可以得出先找到的参数解析器会优先使用、为了测试自定义的参数解析器设置在集合的第一个位置、为啥会这样就得看源码咯。

主要的在这个类InvocableHandlerMethodgetMethodArgumentValues方法、这个方法是得到方法参数需要注入的值。

image.png

这个地方会处理每一个参数的值。

image.png 红色框、框柱的是获取一个参数解析器、如果最前面的解析器resolver.supportsParameter(parameter)返回的是true的情况下就使用当前解析器做为参数解析使用、所以我们自定义的解析器放在第一个的位置上就会优先使用自定义的。

绿色框、框柱的是解析器的处理参数的方法、最后返回方法参数。

问题1:我的参数是个对象、底层是这么设置对象属性的。

是通过反射执行对应的set方法设置的值、下方有个简洁版例子。

public static void main(String[] args) throws Exception {
    Method setId = User.class.getMethod("setId", Integer.class);
    User user = new User();
    setId.invoke(user, 21212);
    System.out.println(user);
}
复制代码

问题2:我Controller方法有两个参数、参数值都通过参数解析器解析出来了值、底层是怎么将值设置到Controller方法的?

也是通过反射执行对应的方法、也是调用方法的invoke()来设置参数的传递并执行Controller中的方法。

来源:https://juejin.cn/post/7156437660943253540
posted @ 2022-12-20 22:06  程序员小明1024  阅读(60)  评论(0编辑  收藏  举报