Spring类型转换(Converter)
spring的类型转换
以前在面试中就有被问到关于spring数据绑定方面的问题,当时对它一直只是朦朦胧胧的概念,最近稍微闲下来有时间看了一下其中数据转换相关的内容,把相应的内容做个记录。
下面先说明如何去用,然后再放一下个人看参数绑定源码的一些笔记,可能由于实力不够,有些地方说的不是很正确,如果有纰漏还请各位指出。
conversionservice
原生的java是有一个可以提供数据转换功能的工具——propertyeditor
。但是它的功能有限,它只能将字符串转换为一个java对象。在web项目中,如果只看与前端交互的那一部分,这个功能的确已经足够了。但是在后台项目内部可就得重新想办法了。
spring针对这个问题设计了converter模块,它位于org.springframework.core.converter
包中。该模块足以替代原生的propertyeditor
,但是spring选择了同时支持两者,在spring mvc处理参数绑定时就用到了。
该模块的核心是conversionservice
接口,内容如下:
public interface conversionservice { boolean canconvert(@nullable class<?> sourcetype, class<?> targettype); boolean canconvert(@nullable typedescriptor sourcetype, typedescriptor targettype); @nullable <t> t convert(@nullable object source, class<t> targettype); @nullable object convert(@nullable object source, @nullable typedescriptor sourcetype, typedescriptor targettype); }
接口里的方法定义的还是比较直观的,见名知意。其中的typedescriptor
是spring自己定义的类,它提供了获取类型更多信息的便捷方法。比如是否含有注解、是否实现map接口、获取map的key与value的typedescriptor等等。
由此可见,converter模块不仅支持任意类型之间的转换,而且能更简单地获得更多的类型信息从而做出更细致的类型转换。
转换器
conversionservice
只是个service,对于每个类型转换的操作,它并不是最终的操作者,它会将相应操作交给对应类型的转换器。而在实际项目中,由于业务复杂,对类型转换的要求也不一样,因此spring提供了几个接口来方便自定义转换器。
converter<s, t>
接口定义如下:
@functionalinterface public interface converter<s, t> { @nullable t convert(s var1); }
该接口非常简单,只定义了一个转换方法,两个泛型参数则是需要转换的两个类型。在单独处理两个类型的转换时这是首选,即一对一,但是倘若有同一父类(或接口)的类型需要进行类型转化,为每个类型都写一个converter显然是十分不理智的。对于这种情况,spring提供了一个converterfactory
接口。
converterfactory<s, r>
public interface converterfactory<s, r> { <t extends r> converter<s, t> getconverter(class<t> var1); }
我们可以看到,该工厂方法可以生产从s类型到t类型的转换器,而t类型必定继承或实现r类型,我们可以形象地称为“一对多”,因此该接口更适合实现需要转换为同一类型的转换器。
对于大部分需求上面两个接口其实已经足够了(至少我感觉是),但是不是还没用到typedescriptor
吗?如果要实现更为复杂的转换功能的话,spring提供了拥有typedescriptor
参数的genericconverter
接口。
genericconverter
public interface genericconverter { @nullable set<convertiblepair> getconvertibletypes(); @nullable object convert(@nullable object source, typedescriptor sourcetype, typedescriptor targettype); final class convertiblepair { private final class<?> sourcetype; private final class<?> targettype; public convertiblepair(class<?> sourcetype, class<?> targettype) { assert.notnull(sourcetype, "source type must not be null"); assert.notnull(targettype, "target type must not be null"); this.sourcetype = sourcetype; this.targettype = targettype; } public class<?> getsourcetype() { return this.sourcetype; } public class<?> gettargettype() { return this.targettype; } // 省去了一些override方法 } }
genericconverter
中拥有一个内部类convertiblepair
,这个内部类的作用只是封装转换的源类型与目标类型。
对于genericconverter
,getconvertibletypes
方法就返回这个转换器支持的转换类型(一对一,一对多,多对多都可以满足),convert
方法和以前一样是负责处理具体的转换逻辑。
而且,如果你觉得对于一个转换器来说只通过判断源类型和目标类型是否一致来决定是否支持转换还不够,spring还提供了另一个接口conditionalgenericconverter
。
conditionalgenericconverter
public interface conditionalconverter { boolean matches(typedescriptor sourcetype, typedescriptor targettype); } public interface conditionalgenericconverter extends genericconverter, conditionalconverter { }
conditionalgenericconverter
接口继承了genericconverter
和conditionalconverter
接口,在matches
方法中就可以在源类型与目标类型已经匹配的基础上再进行判断是否支持转换。
spring官方实现conditionalgenericconverter
接口的转换器大多用来处理有集合或数组参与的转换,这其中的matches
方法就用来判断集合或数组中的元素是否能够成功转换。而且因为genericconverter
与conditionalgenericconverter
接口功能太类似,索性就直接实现conditionalgenericconverter
接口了。
如何使用
那么如何使用转换器呢,spring要求我们要把所有需要使用转换器注册到conversionservice
,这样spring在遇到类型转换的情况时,会去conversionservice
中寻找支持的转换器,进行必要的格式转换。
支持转换器注册的接口为converterregistry
public interface converterregistry { void addconverter(converter<?, ?> converter); <s, t> void addconverter(class<s> sourcetype, class<t> targettype, converter<? super s, ? extends t> converter); void addconverter(genericconverter converter); void addconverterfactory(converterfactory<?, ?> factory); void removeconvertible(class<?> sourcetype, class<?> targettype); }
但我们使用的是另一个继承了conversionservice
和converterregistry
的接口configurableconversionservice
,通过这个接口,就可以注册自定义的转换器了。
格式化
转换器提供的功能是一个类型到另一个类型的单向转换,而在web项目中,有些数据是需要经常做双向转换,最常见的就是日期时间了。将请求中一定格式的字符串转换为日期类型,而在返回的相应中将日期类型再做指定格式的格式化,spring中提供的工具就是formatter
接口。
formatter<t>
@functionalinterface public interface printer<t> { string print(t object, locale locale); } @functionalinterface public interface parser<t> { t parse(string text, locale locale) throws parseexception; } public interface formatter<t> extends printer<t>, parser<t> { }
formatter
接口中拥有两个方法,一个是解析字符串的parse
,一个是将字符串格式化的print
,两个方法都拥有locale
类型的参数,因此还可根据地区来做出相应的定制。
那么如何使用formatter
呢?由于注解的出现,大量需要在xml中的配置项都直接换为注解的方式,formatter
也是,spring提供了annotationformatterfactory
这个接口。
annotationformatterfactory<a extends annotation>
public interface annotationformatterfactory<a extends annotation> { set<class<?>> getfieldtypes(); printer<?> getprinter(a annotation, class<?> fieldtype); parser<?> getparser(a annotation, class<?> fieldtype); }
getfieldtypes
方法返回的是当这些类型有a注解的时候我才会做格式化操作,getprinter
方法和getparser
则分别获取相应的对象,我们也可以直接将formatter
对象返回。
如何使用
格式化的操作,本质上来说也是类型转换,即string => ? 和? => string。因此spring将转换器与格式化同质化,在代码实现中,formatter
也是被转换为相应的printer转换器和parser转换器,那么,formatter
也就可以注册到conversionservice
中了。
可以注册formatter
的接口为formatterregistry
,该接口继承自converterregistry
,将它与conversionservice
一起实现的类是formattingconversionservice
。
public interface formatterregistry extends converterregistry { void addformatter(formatter<?> formatter); void addformatterforfieldtype(class<?> fieldtype, formatter<?> formatter); void addformatterforfieldtype(class<?> fieldtype, printer<?> printer, parser<?> parser); void addformatterforfieldannotation(annotationformatterfactory<? extends annotation> annotationformatterfactory); }
令人非常遗憾的是,除了通过conversionservice
的convert
直接使用,formatter
的print
方法通过框架使用的条件比较特殊,它需要spring标签的支持才能做到在页面上的格式化,parse
只需要在相应字段上打上注解即可。
写写代码
说了这么多,自然还是来点代码更实在。
对于converter
和converterfactory
以及formatter
,使用在springmvc的参数绑定上的机会会更多,所以直接在web项目里写。而conditionalgenericconverter
接口官方实现的例子已经很丰富了,至少我没想到什么新的需求,想要看代码的话可以直接去看官方的源码(比如arraytocollectionconverter
),我就不自己写了。
以下代码基于springboot 2.1.1,对应的springmvc为5.1.3,使用了lombok
@restcontroller @requestmapping("test") public class testcontroller { @getmapping("/index") public userentity test(userentity user) { return user; } }
@configuration public class webconfig implements webmvcconfigurer { @override public void addformatters(formatterregistry registry) { // 为webmvc注册转换器 registry.addconverter(new string2statusenumconverter()); registry.addconverterfactory(new string2enumconverterfactory()); registry.addformatterforfieldannotation(new genderformatterfactory()); } }
@data @component public class userentity { private string username; private string password; // 加上注解的含义为使用枚举的name字段进行枚举的格式化,可改为id @genderenumformat("name") private genderenum gender; private statusenum status; }
public interface enuminterface { integer getid(); }
@getter @allargsconstructor public enum genderenum implements enuminterface { male(0, "男"), female(1, "女"), ; private integer id; private string name; }
@getter @allargsconstructor public enum statusenum implements enuminterface { on(1, "启用"), off(0, "停用"), ; private integer id; private string name; }
/** * string to statusenum 的转换器 */ public class string2statusenumconverter implements converter<string, statusenum> { @override public statusenum convert(string s) { // 注意,这里是通过id匹配 for (statusenum e : statusenum.values()) { if (e.getid().equals(integer.valueof(s))) { return e; } } return null; } }
/** * string to enuminterface 的转换器工厂 */ public class string2enumconverterfactory implements converterfactory<string, enuminterface> { @override public <t extends enuminterface> converter<string, t> getconverter(class<t> targettype) { return new string2enum<>(targettype); } /** * 转换器 */ private class string2enum<t extends enuminterface> implements converter<string, t> { private final class<t> targettype; private string2enum(class<t> targettype) { this.targettype = targettype; } @override public t convert(string source) { for (t enumconstant : targettype.getenumconstants()) { if (enumconstant.getid().tostring().equals(source)) { return enumconstant; } } return null; } } }
/** * 将打上注解的genderenum通过特定的字段转换为枚举 */ @target({elementtype.type, elementtype.field}) @retention(retentionpolicy.runtime) public @interface genderenumformat { string value(); }
public class genderformatterfactory implements annotationformatterfactory<genderenumformat> { @override public set<class<?>> getfieldtypes() { return collections.singleton(genderenum.class); } @override public printer<?> getprinter(genderenumformat annotation, class<?> fieldtype) { return new genderformatter(annotation.value()); } @override public parser<?> getparser(genderenumformat annotation, class<?> fieldtype) { return new genderformatter(annotation.value()); } final class genderformatter implements formatter<genderenum> { private final string fieldname; private method getter; private genderformatter(string fieldname) { this.fieldname = fieldname; } @override public genderenum parse(string text, locale locale) throws parseexception { if (getter == null) { try { getter = genderenum.class.getmethod("get" + fieldname.substring(0, 1).touppercase() + fieldname.substring(1)); } catch (nosuchmethodexception e) { throw new parseexception(e.getmessage(), 0); } } for (genderenum e : genderenum.values()) { try { if (getter.invoke(e).equals(text)) { return e; } } catch (illegalaccessexception | invocationtargetexception e1) { throw new parseexception(e1.getmessage(), 0); } } throw new parseexception("输入参数有误,不存在这样的枚举值:" + text, 0); } @override public string print(genderenum object, locale locale) { try { // 这里应该也判断一下getter是否为null然后选择进行初始化,但是因为print方法没有效果所以也懒得写了 return getter.invoke(object).tostring(); } catch (illegalaccessexception | invocationtargetexception e) { return e.getmessage(); } } } }
源码笔记
之前一直说类型转换在spring mvc的参数绑定中有用到,下面就放一下本人的一些笔记。由于实力问题有些地方也有些懵逼,也欢迎大家交流。
(看源码的时候突然遇到idea无法下载源码,搜出来的结果大致都是说更换maven版本,懒得更改就直接用maven命令下载源码了:
mvn dependency:sources -dincludeartifactids=spring-webmvc
不加参数的话会默认下载全部的源码)
public class invocablehandlermethod extends handlermethod { // ... protected object[] getmethodargumentvalues(nativewebrequest request, @nullable modelandviewcontainer mavcontainer, object... providedargs) throws exception { if (objectutils.isempty(this.getmethodparameters())) { return empty_args; } else { // 得到处理方法的方法参数 methodparameter[] parameters = this.getmethodparameters(); object[] args = new object[parameters.length]; for (int i = 0; i < parameters.length; ++i) { methodparameter parameter = parameters[i]; // 初始化,之后可以调用methodparameter对象的getparametername方法 parameter.initparameternamediscovery(this.parameternamediscoverer); // 如果providedargs包含当前参数的类型就赋值 args[i] = findprovidedargument(parameter, providedargs); if (args[i] == null) { // resolvers包含了所有的参数解析器(handlermethodargumentresolver的实现类,常见的比如requestparammethodargumentresolver,pathvariablemethodargumentresolver等,就是在参数前加的注解的处理类,有对应的注解的话就会用对应的解析器去处理参数绑定,如果没有注解的话通常会和有modelattribute注解一样使用servletmodelattributemethodprocessor,具体判断在每个实现类的supportsparameter方法里) if (!this.resolvers.supportsparameter(parameter)) { throw new illegalstateexception(formatargumenterror(parameter, "no suitable resolver")); } try { // 使用解析器开始解析参数 args[i] = this.resolvers.resolveargument(parameter, mavcontainer, request, this.databinderfactory); } catch (exception var10) { if (this.logger.isdebugenabled()) { string error = var10.getmessage(); if (error != null && !error.contains(parameter.getexecutable().togenericstring())) { this.logger.debug(formatargumenterror(parameter, error)); } } throw var10; } } } return args; } } // ... }
public abstract class abstractnamedvaluemethodargumentresolver implements handlermethodargumentresolver { // ... public final object resolveargument(methodparameter parameter, @nullable modelandviewcontainer mavcontainer, nativewebrequest webrequest, @nullable webdatabinderfactory binderfactory) throws exception { // 获取paramter的信息,namedvalueinfo包含参数的名称、是否必填、默认值,其实就是该参数在requestparam注解中的配置 namedvalueinfo namedvalueinfo = getnamedvalueinfo(parameter); // 如果parameter是optional类型,那么就产生一个指向相同参数对象但嵌套等级(nestinglevel)+1的methodparameter methodparameter nestedparameter = parameter.nestedifoptional(); // 先后解析配置项与spel表达式(即${}、#{}) object resolvedname = resolvestringvalue(namedvalueinfo.name); if (resolvedname == null) { throw new illegalargumentexception( "specified name must not resolve to null: [" + namedvalueinfo.name + "]"); } // 从请求(request)中获取对应名称的数据,如果非上传文件,就相当于servlet中的request.getparameter(),另外如果有多个符合name的值会返回string[] object arg = resolvename(resolvedname.tostring(), nestedparameter, webrequest); if (arg == null) { if (namedvalueinfo.defaultvalue != null) { // 请求中没有这个参数并且有默认值就将解析defaultvalue后值的设为参数 arg = resolvestringvalue(namedvalueinfo.defaultvalue); } else if (namedvalueinfo.required && !nestedparameter.isoptional()) { // 参数必填且方法的类型要求不是optional的话抛异常 handlemissingvalue(namedvalueinfo.name, nestedparameter, webrequest); } // 处理null值。如果参数类型(或者被optional包裹的类型)是boolean会转换成false,而如果参数类型是基本类型的话会抛出异常(因为基本类型值不能为null) arg = handlenullvalue(namedvalueinfo.name, arg, nestedparameter.getnestedparametertype()); } else if ("".equals(arg) && namedvalueinfo.defaultvalue != null) { // 如果有默认值将会把空字符串处理为默认值 arg = resolvestringvalue(namedvalueinfo.defaultvalue); } if (binderfactory != null) { // biner中有conversionservice的实例,而conversionservice中就包含着全部可用的转换器。 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()); } } // 钩子方法,重写这个方法的暂时只有pathvariablemethodargumentresolver handleresolvedvalue(arg, namedvalueinfo.name, parameter, mavcontainer, webrequest); return arg; } // ... }
class typeconverterdelegate { // ... /** * convert the value to the required type (if necessary from a string), * for the specified property. * * @param propertyname name of the property * @param oldvalue the previous value, if available (may be {@code null}) * @param newvalue the proposed new value * @param requiredtype the type we must convert to * (or {@code null} if not known, for example in case of a collection element) * @param typedescriptor the descriptor for the target property or field * @return the new value, possibly the result of type conversion * @throws illegalargumentexception if type conversion failed */ @suppresswarnings("unchecked") @nullable public <t> t convertifnecessary(@nullable string propertyname, @nullable object oldvalue, @nullable object newvalue, @nullable class<t> requiredtype, @nullable typedescriptor typedescriptor) throws illegalargumentexception { // 在当前的流程中propertyname、oldvalue为null,newvalue为前台传过来的真实参数值,requiredtype为处理方法要求的类型,typedescriptor为要求类型的描述封装类 // custom editor for this type? propertyeditor editor = this.propertyeditorregistry.findcustomeditor(requiredtype, propertyname); conversionfailedexception conversionattemptex = null; // no custom editor but custom conversionservice specified? conversionservice conversionservice = this.propertyeditorregistry.getconversionservice(); if (editor == null && conversionservice != null && newvalue != null && typedescriptor != null) { // 上述条件成立 // 在现在的逻辑里sourcetypedesc必然为string的typedescriptor typedescriptor sourcetypedesc = typedescriptor.forobject(newvalue); if (conversionservice.canconvert(sourcetypedesc, typedescriptor)) { // 可以转换 // canconvert实际上是尝试获取符合条件的genericconverter,如果有就说明可以转换 // 对于string -> integer的转换,会先将string类型拆为 [string,serializable,comparable,charsequence,object]的类型层,integer同样拆为自己的类型层,之后先后遍历每个类型来准确判断是否存在可以转换的转换器 try { // 最终会调用到自定义的转换器 return (t) conversionservice.convert(newvalue, sourcetypedesc, typedescriptor); } catch (conversionfailedexception ex) { // fallback to default conversion logic below // 转换失败,暂存异常,将会执行默认的转换逻辑 conversionattemptex = ex; } } } // 因为spring自带了很多常见类型的转换器,大部分都可以通过上面的转换器完成。 // 程序运行到这里没有结束的话很可能说明类型是没有定义转换器的自定义类型或者参数格式真的不正确 object convertedvalue = newvalue; // value not of required type? if (editor != null || (requiredtype != null && !classutils.isassignablevalue(requiredtype, convertedvalue))) { // 最后的条件为 当newvalue不是requiredtype的实例 if (typedescriptor != null && requiredtype != null && collection.class.isassignablefrom(requiredtype) && convertedvalue instanceof string) { // isassignablefrom用来判断collection是否为requiredtype的父类或者接口,或者二者是否为同一类型或接口 typedescriptor elementtypedesc = typedescriptor.getelementtypedescriptor(); if (elementtypedesc != null) { class<?> elementtype = elementtypedesc.gettype(); if (class.class == elementtype || enum.class.isassignablefrom(elementtype)) { // 相当于convertedvalue.split(",") convertedvalue = stringutils.commadelimitedlisttostringarray((string) convertedvalue); } } } if (editor == null) { editor = finddefaulteditor(requiredtype); } // 使用默认的editor进行转换,不过默认的editor的转换有可能与期望的不一致。(比如 "1,2,3,4" -> arraylist<string>{"1,2,3,4"},结果是只有一个元素的list) convertedvalue = doconvertvalue(oldvalue, convertedvalue, requiredtype, editor); } boolean standardconversion = false; // 加下来会根据requiredtype来做出相应的转换 if (requiredtype != null) { // try to apply some standard type conversion rules if appropriate. if (convertedvalue != null) { if (object.class == requiredtype) { // requiredtype是object return (t) convertedvalue; } else if (requiredtype.isarray()) { // requiredtype是数组 // array required -> apply appropriate conversion of elements. if (convertedvalue instanceof string && enum.class.isassignablefrom(requiredtype.getcomponenttype())) { convertedvalue = stringutils.commadelimitedlisttostringarray((string) convertedvalue); } return (t) converttotypedarray(convertedvalue, propertyname, requiredtype.getcomponenttype()); } else if (convertedvalue instanceof collection) { // 将convertedvalue转换为集合,内部对每个元素调用了convertifnecessary(即本方法) // convert elements to target type, if determined. convertedvalue = converttotypedcollection( (collection<?>) convertedvalue, propertyname, requiredtype, typedescriptor); standardconversion = true; } else if (convertedvalue instanceof map) { // 将convertedvalue转换为map // convert keys and values to respective target type, if determined. convertedvalue = converttotypedmap( (map<?, ?>) convertedvalue, propertyname, requiredtype, typedescriptor); standardconversion = true; } if (convertedvalue.getclass().isarray() && array.getlength(convertedvalue) == 1) { convertedvalue = array.get(convertedvalue, 0); standardconversion = true; } if (string.class == requiredtype && classutils.isprimitiveorwrapper(convertedvalue.getclass())) { // we can stringify any primitive value... return (t) convertedvalue.tostring(); } else if (convertedvalue instanceof string && !requiredtype.isinstance(convertedvalue)) { if (conversionattemptex == null && !requiredtype.isinterface() && !requiredtype.isenum()) { try { constructor<t> strctor = requiredtype.getconstructor(string.class); return beanutils.instantiateclass(strctor, convertedvalue); } catch (nosuchmethodexception ex) { // proceed with field lookup if (logger.istraceenabled()) { logger.trace("no string constructor found on type [" + requiredtype.getname() + "]", ex); } } catch (exception ex) { if (logger.isdebugenabled()) { logger.debug("construction via string failed for type [" + requiredtype.getname() + "]", ex); } } } string trimmedvalue = ((string) convertedvalue).trim(); if (requiredtype.isenum() && trimmedvalue.isempty()) { // it's an empty enum identifier: reset the enum value to null. return null; } // 尝试转换为枚举 convertedvalue = attempttoconvertstringtoenum(requiredtype, trimmedvalue, convertedvalue); standardconversion = true; } else if (convertedvalue instanceof number && number.class.isassignablefrom(requiredtype)) { convertedvalue = numberutils.convertnumbertotargetclass( (number) convertedvalue, (class<number>) requiredtype); standardconversion = true; } } else { // convertedvalue == null if (requiredtype == optional.class) { convertedvalue = optional.empty(); } } if (!classutils.isassignablevalue(requiredtype, convertedvalue)) { if (conversionattemptex != null) { // original exception from former conversionservice call above... throw conversionattemptex; } else if (conversionservice != null && typedescriptor != null) { // conversionservice not tried before, probably custom editor found // but editor couldn't produce the required type... typedescriptor sourcetypedesc = typedescriptor.forobject(newvalue); if (conversionservice.canconvert(sourcetypedesc, typedescriptor)) { return (t) conversionservice.convert(newvalue, sourcetypedesc, typedescriptor); } } // definitely doesn't match: throw illegalargumentexception/illegalstateexception stringbuilder msg = new stringbuilder(); msg.append("cannot convert value of type '").append(classutils.getdescriptivetype(newvalue)); msg.append("' to required type '").append(classutils.getqualifiedname(requiredtype)).append("'"); if (propertyname != null) { msg.append(" for property '").append(propertyname).append("'"); } if (editor != null) { msg.append(": propertyeditor [").append(editor.getclass().getname()).append( "] returned inappropriate value of type '").append( classutils.getdescriptivetype(convertedvalue)).append("'"); throw new illegalargumentexception(msg.tostring()); } else { msg.append(": no matching editors or conversion strategy found"); throw new illegalstateexception(msg.tostring()); } } } if (conversionattemptex != null) { if (editor == null && !standardconversion && requiredtype != null && object.class != requiredtype) { throw conversionattemptex; } logger.debug("original conversionservice attempt failed - ignored since " + "propertyeditor based conversion eventually succeeded", conversionattemptex); } return (t) convertedvalue; } // ... }
最后
看源码虽然很费时间,但是的确是能学到很多东西的,而且也能发现很多以前不知道的事情(比如requestparam注解的name和defaultname参数是可以嵌套引用配置文件中的内容,也可以写spel表达式),但其中还是有一些地方不是很清楚。
虽说现在项目都直接使用json做前后端交互,大部分类型转换的任务都交给了json序列化框架,但是参数绑定这里还是值得看一看,等到需要用的时候就可以直接拿出来用。