在Spring Boot开发过程中,前端与后端之间的传参是一个核心且常见的问题。本文将详细探讨前端如何向后端传递参数、后端如何接收参数、接收参数的原理,以及在实际开发中如何进行合理的配置与设置,确保参数能够正确、安全地传输和处理。
六种常见的方式
URL 查询参数
最常见的一种传输参数的方式,URL 查询参数是指附加在 URL 后面的以键值对形式传递的参数,通常用于 GET 请求中向服务器传递简单的数据。
// 前端
axios.get('/api/users', {
params: {
name: 'John',
age: 30
}
});
// 后端
@GetMapping("/users")
public ResponseEntity<?> getUser(@RequestParam("name") String name,
@RequestParam("age") Integer age) {
// 处理业务逻辑
return ResponseEntity.ok("User: " + name + ", age: " + age);
}
注意:我们可以去省略这个@RequestParam 注解,使用这个注解的好处就是可以设置参数的默认值defaultValue。
路径参数
直接将参数嵌入到 URL 路径当中的一种传递方式,对于后端而言需要指定特殊的标识符和注解才能使用。
// 前端
axios.get('/api/users/123');
// 后端
@GetMapping("/users/{id}")
public ResponseEntity<?> getUserById(@PathVariable("id") Long id) {
// 根据 id 查询用户
return ResponseEntity.ok("User ID: " + id);
}
请求体参数
主要用于 POST、PUT 等请求,常传递 JSON 数据或表单数据。
// 前端
axios.post('/api/users', {
name: 'John',
age: 30
});
// 后端
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody User user) {
// user 对象由 JSON 数据自动反序列化而来
return ResponseEntity.ok("User created: " + user.getName());
}
注意:请求头需要设置 Content-Type: application/json,主要用于解析 JSON 数据 。
表单数据参数
表单数据指的是通过 HTML 表单或 application/x-www-form-urlencoded 方式提交的参数,主要用于 POST、PUT 请求。后端通常使用 @RequestParam 或 @ModelAttribute 来解析。
// 前端
let formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('username', 'John');
axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
// 后端
// 普通表单接收方式
@PostMapping("/users/form")
public ResponseEntity<?> createUserForm(@ModelAttribute User user) {
return ResponseEntity.ok("User created via form: " + user.getName());
}
// 涉及文件上传表单接收方式
@PostMapping("/upload")
public ResponseEntity<?> handleFileUpload(@RequestPart("file") MultipartFile file,
@RequestParam("username") String username) {
// 处理文件上传和其他参数
return ResponseEntity.ok("File uploaded by: " + username);
}
post 方式只有一个属性
@PostMapping("/string")
public ResponseEntity<?> receiveString(@RequestParam("text") String text) {
return ResponseEntity.ok("Received: " + text);
}
axios.post('/string', 'text=Hello%20World', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
@PostMapping("/string")
public ResponseEntity<?> receiveString(@RequestBody String text) {
return ResponseEntity.ok("Received: " + text);
}
axios.post('/string', 'Hello World', {
headers: {
'Content-Type': 'text/plain'
}
})
注意:@RequestParam 方式(不可也可以,但是加上更清晰可读)下可直接放在 Url 后面,@RequestBody 方式下使用 json 格式传递 。
图片
post 请求传递数组
@PostMapping("/list")
public ResponseEntity<?> receiveList(@RequestBody List<String> list) {
return ResponseEntity.ok("Received: " + list);
}
axios.post('/list', ["apple", "banana", "cherry"], {
headers: { 'Content-Type': 'application/json' }
})
@PostMapping("/list")
public ResponseEntity<?> receiveList(@RequestParam List<String> list) {
return ResponseEntity.ok("Received: " + list);
}
axios.post('/list', null, { params: { list: ["apple", "banana", "cherry"] } })
.then(response => console.log(response.data))
.catch(error => console.error(error));
http://localhost:8080/list?list=apple&list=banana&list=cherry
图片
图片
原理分析
图片
RequestParamMethodArgumentResolver 类
在 Spring MVC 中,@RequestParam 参数的解析由 RequestParamMethodArgumentResolver 负责。它是 HandlerMethodArgumentResolver 的实现类之一,专门用于解析 @RequestParam 注解的参数。
// 判断是否由该类进行解析
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && StringUtils.hasText(requestParam.name()));
}
else {
return true;
}
}
else {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
else if (this.useDefaultResolution) {
return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
}
else {
return false;
}
}
}
// 具体解析逻辑
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
// Check for null value after conversion of incoming argument value
if (arg == null && namedValueInfo.defaultValue == null &&
namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
}
}
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null) {
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
return mpArg;
}
}
Object arg = null;
MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
if (multipartRequest != null) {
List<MultipartFile> files = multipartRequest.getFiles(name);
if (!files.isEmpty()) {
arg = (files.size() == 1 ? files.get(0) : files);
}
}
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
解析条件:首先调用supportsParameter 方法判断是否需要该类进行解析,判断逻辑参数上存在@RequestParam 注解或者未标@RequestParam注解,但是 useDefaultResolutinotallow=true 也会尝试解析。
解析逻辑:调用该类父类AbstractNamedValueMethodArgumentResolver 的resolveArgument 方法解析参数。
关键点:该类的resolveName 方法是从request.getParameter()获取参数值。
PathVariableMethodArgumentResolver 类
@PathVariable 注解的解析由 PathVariableMethodArgumentResolver 负责,它的解析逻辑与 @RequestParam 类似,但它解析的是 路径参数,而非查询参数。
public boolean supportsParameter(MethodParameter parameter) {
if (!parameter.hasParameterAnnotation(PathVariable.class)) {
return false;
}
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
}
return true;
}
解析条件:首先调用supportsParameter 方法判断是否需要该类进行解析,判断逻辑参数上存在@PathVariable 注解。
解析逻辑:与前面@RequestParam的一致。
关键点:该类的resolveName 方法是从 HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE 获取参数值。
RequestResponseBodyMethodProcessor 类
该类是用来 处理 @RequestBody 和 @ResponseBody注解的,它主要用于解析请求体(@RequestBody)和返回值(@ResponseBody),并完成 JSON/XML 的序列化和反序列化。
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
解析条件:首先调用supportsParameter 方法判断是否需要该类进行解析,判断逻辑是存在@RequestBody 注解。
解析逻辑:直接自身的resolveArgument 方法,通过readWithMessageConverters 方法进行请求体转换,获取参数名称,进行数据绑定和校验,适配参数。
拓展分析
如果内置的参数绑定方式无法满足特定的要求,我们可以通过自定义 HandlerMethodArgumentResolver 来实现独特的参数方式,这样可以做到更加的安全可靠,需要将自定义解析器加入链条当中。
public class CustomArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 根据条件判断是否支持该参数解析
return parameter.hasParameterAnnotation(MyCustomAnnotation.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
// 自定义解析逻辑
return ...;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CustomArgumentResolver());
}
}
具体的实现逻辑可以根据需求去添加,该方式适合于复杂的数据转换和自定义注解绑定逻辑,相对于统一的传参方式更加隐秘安全。