為啥前端給到的參數(shù)又不對(duì),該如何傳遞呢?
在Spring Boot開(kāi)發(fā)過(guò)程中,前端與后端之間的傳參是一個(gè)核心且常見(jiàn)的問(wèn)題。本文將詳細(xì)探討前端如何向后端傳遞參數(shù)、后端如何接收參數(shù)、接收參數(shù)的原理,以及在實(shí)際開(kāi)發(fā)中如何進(jìn)行合理的配置與設(shè)置,確保參數(shù)能夠正確、安全地傳輸和處理。
六種常見(jiàn)的方式
URL 查詢參數(shù)
最常見(jiàn)的一種傳輸參數(shù)的方式,URL 查詢參數(shù)是指附加在 URL 后面的以鍵值對(duì)形式傳遞的參數(shù),通常用于 GET 請(qǐng)求中向服務(wù)器傳遞簡(jiǎn)單的數(shù)據(jù)。
// 前端
axios.get('/api/users', {
params: {
name: 'John',
age: 30
}
});
// 后端
@GetMapping("/users")
public ResponseEntity<?> getUser(@RequestParam("name") String name,
@RequestParam("age") Integer age) {
// 處理業(yè)務(wù)邏輯
return ResponseEntity.ok("User: " + name + ", age: " + age);
}
注意:我們可以去省略這個(gè)@RequestParam 注解,使用這個(gè)注解的好處就是可以設(shè)置參數(shù)的默認(rèn)值defaultValue。
路徑參數(shù)
直接將參數(shù)嵌入到 URL 路徑當(dāng)中的一種傳遞方式,對(duì)于后端而言需要指定特殊的標(biāo)識(shí)符和注解才能使用。
// 前端
axios.get('/api/users/123');
// 后端
@GetMapping("/users/{id}")
public ResponseEntity<?> getUserById(@PathVariable("id") Long id) {
// 根據(jù) id 查詢用戶
return ResponseEntity.ok("User ID: " + id);
}
請(qǐng)求體參數(shù)
主要用于 POST、PUT 等請(qǐng)求,常傳遞 JSON 數(shù)據(jù)或表單數(shù)據(jù)。
// 前端
axios.post('/api/users', {
name: 'John',
age: 30
});
// 后端
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody User user) {
// user 對(duì)象由 JSON 數(shù)據(jù)自動(dòng)反序列化而來(lái)
return ResponseEntity.ok("User created: " + user.getName());
}
注意:請(qǐng)求頭需要設(shè)置 Content-Type: application/json,主要用于解析 JSON 數(shù)據(jù) 。
表單數(shù)據(jù)參數(shù)
表單數(shù)據(jù)指的是通過(guò) HTML 表單或 application/x-www-form-urlencoded 方式提交的參數(shù),主要用于 POST、PUT 請(qǐng)求。后端通常使用 @RequestParam 或 @ModelAttribute 來(lái)解析。
// 前端
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) {
// 處理文件上傳和其他參數(shù)
return ResponseEntity.ok("File uploaded by: " + username);
}
post 方式只有一個(gè)屬性
@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 請(qǐng)求傳遞數(shù)組
@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 參數(shù)的解析由 RequestParamMethodArgumentResolver 負(fù)責(zé)。它是 HandlerMethodArgumentResolver 的實(shí)現(xiàn)類之一,專門用于解析 @RequestParam 注解的參數(shù)。
// 判斷是否由該類進(jìn)行解析
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;
}
解析條件:首先調(diào)用supportsParameter 方法判斷是否需要該類進(jìn)行解析,判斷邏輯參數(shù)上存在@RequestParam 注解或者未標(biāo)@RequestParam注解,但是 useDefaultResolutinotallow=true 也會(huì)嘗試解析。
解析邏輯:調(diào)用該類父類AbstractNamedValueMethodArgumentResolver 的resolveArgument 方法解析參數(shù)。
關(guān)鍵點(diǎn):該類的resolveName 方法是從request.getParameter()獲取參數(shù)值。
PathVariableMethodArgumentResolver 類
@PathVariable 注解的解析由 PathVariableMethodArgumentResolver 負(fù)責(zé),它的解析邏輯與 @RequestParam 類似,但它解析的是 路徑參數(shù),而非查詢參數(shù)。
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;
}
解析條件:首先調(diào)用supportsParameter 方法判斷是否需要該類進(jìn)行解析,判斷邏輯參數(shù)上存在@PathVariable 注解。
解析邏輯:與前面@RequestParam的一致。
關(guān)鍵點(diǎn):該類的resolveName 方法是從 HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE 獲取參數(shù)值。
RequestResponseBodyMethodProcessor 類
該類是用來(lái) 處理 @RequestBody 和 @ResponseBody注解的,它主要用于解析請(qǐng)求體(@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);
}
解析條件:首先調(diào)用supportsParameter 方法判斷是否需要該類進(jìn)行解析,判斷邏輯是存在@RequestBody 注解。
解析邏輯:直接自身的resolveArgument 方法,通過(guò)readWithMessageConverters 方法進(jìn)行請(qǐng)求體轉(zhuǎn)換,獲取參數(shù)名稱,進(jìn)行數(shù)據(jù)綁定和校驗(yàn),適配參數(shù)。
拓展分析
如果內(nèi)置的參數(shù)綁定方式無(wú)法滿足特定的要求,我們可以通過(guò)自定義 HandlerMethodArgumentResolver 來(lái)實(shí)現(xiàn)獨(dú)特的參數(shù)方式,這樣可以做到更加的安全可靠,需要將自定義解析器加入鏈條當(dāng)中。
public class CustomArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 根據(jù)條件判斷是否支持該參數(shù)解析
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());
}
}
具體的實(shí)現(xiàn)邏輯可以根據(jù)需求去添加,該方式適合于復(fù)雜的數(shù)據(jù)轉(zhuǎn)換和自定義注解綁定邏輯,相對(duì)于統(tǒng)一的傳參方式更加隱秘安全。