這個問題非常有趣,不是SpringMVC 的問題,是實際開發中混合使用了兩種請求方式暴露出來的。
問題場景
功能模塊中,提供兩個 Http 服務。一個是列表查詢(application/json 請求),一個是列表導出(表單請求)。運行環境發現個問題:MVC model 新添加的屬性,類似的 Http 請求,一個有值,一個沒有
代碼如下:
/**
* application/json 請求。 這種情況 param.field2 有值 ✔
* @param param RequestResponseBodyMethodProcessr 處理 HttpServletRequest 參數
*/
@PostMapping(value = "query")
public ResponseResult<Page<SomeData>> queryByCondition(@RequestBody SomeParam param){
// 業務邏輯...
}
/**
* application/x-www-form-urlencoded 請求 這種情況 param.field2 沒有有賦值 ❌
* @param param ServletModelAttributeMethodProcessor 處理 HttpServletRequest 參數
*/
@PostMapping(value = "export")
public void exportExcel(SomeParam param) {
// 業務邏輯...
}
public class SomeParam {
// 這個是原有的,有 get set 方法
private String field1;
// 這個是新增的,沒有get set 方法 (這是一個巧合、意外)。 問題就出在這裏。
private String field2;
}
❓ 根據代碼分析,那應該是 SpringMVC 針對這兩種參數處理的機制不同。
針對上述的參數處理,可以參考:
RequestResponseBodyMethodProcessor、 ServletModelAttributeMethodProcessor
Insight RequestResponseBodyMethodProcessor
處理 Http Body 的數據。解析註解 RequestBody 的參數。
針對 MimeType 為 application/json 的請求,按照json 格式進行反序列化。
默認參數處理器
MappingJackson2HttpMessageConverter
string 反序列化為對象,使用的是
com.fasterxml.jackson.databind.ObjectMapper。
上述工程中,對 ObjectMapper 開啓 private 屬性檢測。新增的屬性可以正常反序列化。
ObjectMapper mapper = new ObjectMapper();
// 這又是一個巧合、意外 咋還有這個用法
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
Visibility 具體的用法示例參考: Jackson - Decide What Fields Get (De)Serialized | Baeldung
原理: 如果沒有 setter 方法,jackson 會操作 field 來完成賦值。
/**
* This concrete sub-class implements property that is set directly assigning to a Field.
*/
public final static class FieldProperty extends SettableBeanProperty {
@Override
public final void set(Object instance, Object value) throws IOException {
try {
_field.set(instance, value);
} catch (Exception e) {
_throwAsIOE(e, value);
}
}
}
Insight ServletModelAttributeMethodProcessor
自定義 Class 參數解析
通過解析 request parameters, 用來構造和初始化對應的方法入參。
主要通過
ServletRequestDataBinder.bind(request) 來完成。
/**
* Apply given property values to the target object.
* By default, unknown fields will be ignored.
*
* @see org.springframework.validation.DataBinder#applyPropertyValues
*/
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// Bind request parameters onto target object.
// 默認使用 BeanWrapperImpl.setPropertyValue()
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
// Use bind error processor to create FieldErrors.
}
}
public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException {
// 通過遍歷 request parameters 來嘗試對 target 進行賦值
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ? ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
for (PropertyValue pv : propertyValues) {
try {
// etPropertyValue 使用 JDK 的 Introspector 來進行序列化操作。
// 沒有setter 方法,自然沒法賦值。
setPropertyValue(pv);
}
}
}
總結
- 一件事情出錯,不是一處問題造成的。
- 工程開發要規範,用最常規、最穩定的辦法來實現。遇到稀奇古怪的問題就是冷門用法帶來的。
- 對於常用的框架和工具庫熟悉其底層原理,遇到問題可以很快定位。
作者:京東物流 楊攀
來源:京東雲開發者社區