自定义对象参数绑定原理
# 260.自定义对象参数绑定原理
我们可以给方法传入一个自定义的类,然后 SpringBoot 会自动帮我们绑定;接下来就讲讲其原理
# 新增 JavaBean
我们新增两个类,用来演示。
package com.peterjxl.boot.bean;
import lombok.Data;
@Data
public class Pet2 {
private String name;
private int age;
}
2
3
4
5
6
7
8
package com.peterjxl.boot.bean;
import lombok.Data;
import java.util.Date;
@Data
public class Person2 {
private String userName;
private Integer age;
private Date birth;
private Pet2 pet;
}
2
3
4
5
6
7
8
9
10
11
# 新增表单
新建 resources/haha/customClass.html,文件内容:
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
测试复杂类型(封装POJO)<br/>
<form action="/saveUser" method="post">
姓名:<input name="userName" value="zhangsan"><br/>
年龄:<input name="age" value="18"><br/>
生日:<input name="birth" value="2022/05/20"><br/>
宠物姓名:<input name="pet.name" value="阿猫"><br/>
宠物年龄:<input name="pet.age" value="5"><br/>
<input type="submit" value="保存"><br/>
</form>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 新增 controller
在 ParameterTestController
类中新增方法:
@PostMapping("/saveUser")
public Person2 saveUser(Person2 person2) {
return person2;
}
2
3
4
重启项目,访问 localhost: 8888/customClass.html (opens new window),点击提交,可以看到能返回 person2 对象
# debug
我们在 DispatcherServlet
类中的 doDispatch
方法上打个断点,然后以 debug 的方式启动,观察封装的过程。
来到 doDispatch
方法后,我们执行到这行代码,然后步入进去:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
然后来到 AbstractHandlerMethodAdapter
类的 handleInternal
方法,再步入:
public final ModelAndView handle(
HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}
2
3
4
5
然后来到 RequestMappingHandlerAdapter
类的 handleInternal
方法,执行到该行代码(787 行左右)并步入:
mav = invokeHandlerMethod(request, response, handlerMethod);
然后来到 RequestMappingHandlerAdapter
类的 invokeHandlerMethod
方法,这个方法就是初始化的,例如参数解析器,mavContainer 等数据,然后我们执行到第 878 行,并步入:
invocableMethod.invokeAndHandle(webRequest, mavContainer);
然后来到 invokeAndHandle
方法:
public void invokeAndHandle( ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
// .....
2
3
4
5
第一行就是调用我们的处理器方法,然后会拿到方法的返回值,因此封装参数就是在 invokeForRequest
里完成的
invokeForRequest
源码如下:
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
2
3
4
5
6
7
8
9
我们步入到 getMethodArgumentValues
方法:
首先会获取到所有参数,其中包含了返回值类型和参数类型
接下来就是判断是否支持解析当前的参数类型,我们步入进去:
然后会发现其调用的是 getArgumentResolver 方法:
@Override
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
2
3
4
我们再次步入进去,可以看到里面是一个 for 循环,也就是遍历所有参数解析器,观察是否支持当前参数。
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
我们打断点到最后一行,可以看到返回的参数解析器是 ServletModelAttributeMethodProcessor
:
也就是我们自定义类型的参数,是由该解析器解析的。那么其是如何判断是否支持自定义类型的呢?我们重新打个断点到第 133 行,然后步入到 supportsParameter
方法中:
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
2
3
4
5
可以看到是一个判断,首先判断是否用了 ModelAttribute 注解,这里我们没有用
然后判断是否必输的,这里也不是必输;
然后判断是不是非简单属性(BeanUtils.isSimpleProperty
),是则返回 true
所谓的简单类型,就是这样判断的:逐个判断是否 Java 的基本类型。
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
2
3
4
5
6
7
8
9
10
11
12
13
知道怎么判断是否支持方法的参数类型后,接下来就是赋值了,我们来步入:
resolveArgument
方法源码如下:
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer,webRequest, binderFactory);
}
2
3
4
5
6
7
8
9
我们步入到最后一行,该方法会帮我们创建一个空的 Person2 实例(成员变量都是 null)
接下来比较关键的就是绑定数据了。我们继续执行下一步,会看到创建了一个绑定器(第 4 行),传入的参数有 request 对象,attribute(也就是 Person2 的空对象):
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# converter
在 binder 中,还有很多的转换器(converter):
因为 HTTP 是超文本传输协议,传的都是文本,我们需要将这些文本转为 Java 中的数据类型。如果是文件上传请求,还有将 byte 转为 File 类型的转换器。我们后续也可以自己添加 Converter。
接下来我们看到第 7 行的代码:
bindRequestParameters(binder, webRequest);
这段代码就是绑定 request 中的数据,到 person 对象的,我们步入 bind 方法:
@Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
Assert.state(servletRequest != null, "No ServletRequest");
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
servletBinder.bind(servletRequest);
}
2
3
4
5
6
7
bind 方法源码:
public void bind(ServletRequest request) {
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
addBindValues(mpvs, request);
doBind(mpvs);
}
2
3
4
5
6
7
8
9
第二行:获取 request 域中的所有参数,以键值对的方式保存到 mpvs 变量中:
然后的 if 判断是判断是否文件上传请求,这里不是,因此忽略;然后 addBindValues 就是一些初始化的功能,我们不表。我们步入最后一行 dobind 方法中:
@Override
protected void doBind(MutablePropertyValues mpvs) {
checkFieldDefaults(mpvs);
checkFieldMarkers(mpvs);
super.doBind(mpvs);
}
2
3
4
5
6
该方法会调用 2 个检查的方法,最后调用父类的 dobind 方法:
protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
2
3
4
5
我们步入到 applyPropertyValues 方法:
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// Bind request parameters onto target object.
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
// Use bind error processor to create FieldErrors.
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
然后我们步入 setPropertyValues 方法:
可以看到有一个 for 循环,其中有个 setPropertyValue 方法,我们步入进去:
首先会通过反射,获取到 BeanWrapper 对象;然后调用 setPropertyValue
方法,我们步入进去:
protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
if (tokens.keys != null) {
processKeyedProperty(tokens, pv);
}
else {
processLocalProperty(tokens, pv);
}
}
2
3
4
5
6
7
8
然后我们再步入 processLocalProperty
方法:在该方法中,会调用 convertForProperty
方法,因为我们 HTTP 是超文本传输协议,需要转换器,转为 Java 中的 Integer 类型
我们步入进去:
@Nullable
protected Object convertForProperty(String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) throws TypeMismatchException {
return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);
}
2
3
4
5
再步入 convertIfNecessary 方法:
@Nullable
private Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<?> requiredType, @Nullable TypeDescriptor td) throws TypeMismatchException {
Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
try {
return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);
}
//....
2
3
4
5
6
7
8
然后第 6 行就是返回转换后的变量,propertyName 是变量名,这里是 age;oldvalue 是旧的值,newValue 是转换后的值。我们步入进去
接下来,看到 typeConverterDelegate.convertIfNecessary
方法:首先会判断能否进行转换
canConvert 源码如下:可以看到第 7 行 getConverter
方法,是通过传入 sourceType(原始的数据类型,这里是 String)和 targetType(要转换的数据类型,这里是 Integer)两个参数,来获取一个转换器;如果转换器不为空,则说明支持转换,因此我们步入到 getConverter
方法
@Override
public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
return true;
}
GenericConverter converter = getConverter(sourceType, targetType);
return (converter != null);
}
2
3
4
5
6
7
8
9
getConverter 方法如下:
@Nullable
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
GenericConverter converter = this.converterCache.get(key);
if (converter != null) {
return (converter != NO_MATCH ? converter : null);
}
converter = this.converters.find(sourceType, targetType);
if (converter == null) {
converter = getDefaultConverter(sourceType, targetType);
}
if (converter != null) {
this.converterCache.put(key, converter);
return converter;
}
this.converterCache.put(key, NO_MATCH);
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
首先会从缓存中找,找到了就返回;当然,我们项目刚启动,缓存是没有数据的,因此调用 converters.find
方法来找,converters
就是我们之前说的所有转换器(124 个)。
步入 find
方法,此时会来到 GenericConversionService
类里 find
方法:
@Nullable
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Search the full type hierarchy
List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
for (Class<?> sourceCandidate : sourceCandidates) {
for (Class<?> targetCandidate : targetCandidates) {
ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
if (converter != null) {
return converter;
}
}
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以看到是一个遍历循环 converter
,判断是否支持当前的类型转换。我们执行多几次,可以看到能找到一个转换器:也就是 String 类型转 Number 类型的
拿到转换器,下一步就是转换值了。我们一点点的步出,直到调用 convert 方法:
convert
方法源码:
@Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
return handleResult(null, targetType, convertNullSource(null, targetType));
}
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" +sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
之前的代码都是一些校验 null 的,注意第 18 行,就是调用工具类的方法去转换,我们步入进去:
@Nullable
public static Object invokeConverter(GenericConverter converter, @Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
try {
return converter.convert(source, sourceType, targetType);
}
catch (ConversionFailedException ex) {
throw ex;
}
catch (Throwable ex) {
throw new ConversionFailedException(sourceType, targetType, source, ex);
}
}
2
3
4
5
6
7
8
9
10
11
12
可以看到调用了 converter.convert
方法,继续步入:
@Override
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return convertNullSource(sourceType, targetType);
}
return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
}
2
3
4
5
6
7
8
继续步入 this.converterFactory.getConverter
方法:
最后,NumberUtils.parseNumber
方法,其底层就是逐个判断要转换的类型,然后调用基本类型的 decode 方法来转换为数字类型:
public static <T extends Number> T parseNumber(String text, Class<T> targetClass) {
Assert.notNull(text, "Text must not be null");
Assert.notNull(targetClass, "Target class must not be null");
String trimmed = StringUtils.trimAllWhitespace(text);
if (Byte.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Byte.decode(trimmed) : Byte.valueOf(trimmed));
}
else if (Short.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Short.decode(trimmed) : Short.valueOf(trimmed));
}
else if (Integer.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed));
}
else if (Long.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Long.decode(trimmed) : Long.valueOf(trimmed));
}
else if (BigInteger.class == targetClass) {
return (T) (isHexNumber(trimmed) ? decodeBigInteger(trimmed) : new BigInteger(trimmed));
}
else if (Float.class == targetClass) {
return (T) Float.valueOf(trimmed);
}
else if (Double.class == targetClass) {
return (T) Double.valueOf(trimmed);
}
else if (BigDecimal.class == targetClass || Number.class == targetClass) {
return (T) new BigDecimal(trimmed);
}
else {
throw new IllegalArgumentException("Cannot convert String [" + text + "] to target class [" + targetClass.getName() + "]");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 设置值
我们一点点的步出,直到最开始,调用 convert 方法的地方:此时可以看到值已经被转换好了。然后下一步就是设置值了(setValue):
这里仅仅演示了一个属性的设置,其他属性同理,这里就不再演示了;
# 最后
本文篇幅较长,希望读者能慢慢地一步步 debug,加深阅读和调试源码的能力;并对如下对象有基本的认知:
DataBinder:web 数据绑定器,将请求参数的值绑定到指定的 JavaBean 里面。其在绑定过程中,会调用
GenericConversionService
,进行类型转换。GenericConversionService:在设置每一个值的时候,找它里面的所有 converter 那个可以将这个数据类型(request 带来参数的字符串)转换到指定的类型。
Converter:只是一个接口,有很多的实现类,用来转换数据类型。源码:
@FunctionalInterface public interface Converter<S, T> { @Nullable T convert(S source); }
1
2
3
4
5
我们在《Web 开发简介》这一篇博客中,也说明了 SpringBoot 帮我们自动配置了 converter
,DataBinder
,这是 ConfigurableWebBindingInitializer
中配置的:
@Override
public void initBinder(WebDataBinder binder) {
binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
if (this.directFieldAccess) {
binder.initDirectFieldAccess();
}
if (this.messageCodesResolver != null) {
binder.setMessageCodesResolver(this.messageCodesResolver);
}
if (this.bindingErrorProcessor != null) {
binder.setBindingErrorProcessor(this.bindingErrorProcessor);
}
if (this.validator != null && binder.getTarget() != null &&
this.validator.supports(binder.getTarget().getClass())) {
binder.setValidator(this.validator);
}
if (this.conversionService != null) {
binder.setConversionService(this.conversionService);
}
if (this.propertyEditorRegistrars != null) {
for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {
propertyEditorRegistrar.registerCustomEditors(binder);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
例如在 18 行初始化了 conversionService,这里面就包含了很多的 converter。
已将本文源码上传到 Gitee (opens new window) 或 GitHub (opens new window) 的分支 demo14,读者可以通过切换分支来查看本文的示例代码