需求,有一個Teacher類和Student類,他們都有屬性name和age:
前端form表單爲:
- <form action="/test/two" method="post" >
- <input type="text" name="teacher.name" value="張三">
- <input type="text" name="teacher.age" value=88>
- <input type="text" name="student.name" value="李四">
- <input type="text" name="student.age" value=89>
- <input type="submit" value="提交">
- </form>
希望後臺能這樣接收這樣的參數:
解決方案有很多:
方案一:
新建一個類,融合這兩個類。如
- public class Father {
- private Teacher teacher;
- private Student student;
- public Teacher getTeacher() {
- return teacher;
- }
- public void setTeacher(Teacher teacher) {
- this.teacher = teacher;
- }
- public Student getStudent() {
- return student;
- }
- public void setStudent(Student student) {
- this.student = student;
- }
- }
在後臺這樣接收參數:
- @RequestMapping(value="/test/father",method=RequestMethod.POST)
- @ResponseBody
- public Map<String,Object> testFather(@RequestBody Father f){
- //略
- }
即使用@RequestBody來接受這樣的參數。下面還要說說這樣做的兩個問題,你或許可以試猜一下:
使用form表單來進行提交,運行:
問題一:
首先會遇到415 Unsupported Media Type,如下:
我們的form表單默認是以application/x-www-form-urlencoded方式提交的,而@RequestBody又採用的是RequestResponseBodyMethodProcessor這個HandlerMethodArgumentResolver,RequestResponseBodyMethodProcessor內部的處理原理就是用一系列的HttpMessageConverter來進行數據的轉換的。這時候就需要找到支持MediaType類型爲application/x-www-form-urlencoded和數據的類型爲Father的HttpMessageConverter,當然就找不到了。我們本意是想讓MappingJackson2HttpMessageConverter來處理的,但是它僅僅支持的MediaType類型爲:
- public MappingJackson2HttpMessageConverter() {
- super(new MediaType("application", "json", DEFAULT_CHARSET),
- new MediaType("application", "*+json", DEFAULT_CHARSET));
- }
即application/json或者application/*+json。所以此時就需要我們更改提交的content-type。然而form表單目前的僅僅支持三種content-type即application/x-www-form-urlencoded、multipart/form-data、text/plain。所以我們需要更換成ajax提交,如下:
- function postFather1(){
- var url='/test/father';
- var data={
- 'teacher.name':'張三' ,
- 'teacher.age':88 ,
- 'student.name':'李四' ,
- 'student.age':89 ,
- };
- $.ajax({
- url:url,
- type:'POST',
- data:JSON.stringify(data),
- dataType:'json',
- contentType:"application/json;charset=utf-8",
- success:function(result){
- }
- });
- }
此時又有一個問題,teacher.name這樣的形式並不能正確解析成Father。仍然需要變換格式:
- var data={
- 'teacher':{
- 'name':'張三',
- 'age':88
- },
- 'student':{
- 'name':'李四',
- 'age':89
- }
- };
這樣的json形式才能夠被正確解析出來。
所以說方案一有很多的地方要修改,並不是那麼優雅。
方案二:
我們仍然使用form表單提交:
- <form action="/test/two" method="post">
- <input type="text" name="teacher.name" value="張三">
- <input type="text" name="teacher.age" value=88>
- <input type="text" name="student.name" value="李四">
- <input type="text" name="student.age" value=89>
- <input type="submit" value="提交">
- </form>
服務器端的變化爲:
- @InitBinder("teacher")
- public void initBinder1(WebDataBinder binder) throws Exception {
- binder.setFieldDefaultPrefix("teacher.");
- }
- @InitBinder("student")
- public void initBinder2(WebDataBinder binder) throws Exception {
- binder.setFieldDefaultPrefix("student.");
- }
- @RequestMapping(value="/test/two",method=RequestMethod.POST)
- @ResponseBody
- public Map<String,Object> testrequestHeader(Teacher a,Student b){
- Map<String,Object> map=new HashMap<String,Object>();
- map.put("name","lg");
- map.put("age",23);
- map.put("date",new Date());
- return map;
- }
大體上來說就是在解析每個參數時加上前綴限制。下面就要看看這個過程的源碼分析:
到底選擇哪個HandlerMethodArgumentResolver來解析我們的參數呢?它最終會選擇ServletModelAttributeMethodProcessor,看下它的判斷條件:
- /**
- * @return true if the parameter is annotated with {@link ModelAttribute}
- * or in default resolution mode also if it is not a simple type.
- */
- @Override
- public boolean supportsParameter(MethodParameter parameter) {
- if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
- return true;
- }
- else if (this.annotationNotRequired) {
- return !BeanUtils.isSimpleProperty(parameter.getParameterType());
- }
- else {
- return false;
- }
- }
這裏說明了它可以支持兩種情況,一種情況爲含有@ModelAttribute註解的參數,另一種情況就是雖然不含@ModelAttribute註解,但它並不是簡單類型,如常用的String、Date等。你會發現spring會註冊兩個ServletModelAttributeMethodProcessor,一個annotationNotRequired爲false,另一個爲true。這主要是因爲調用HandlerMethodArgumentResolver的解析順序的原因,如果只有一個ServletModelAttributeMethodProcessor,當它判斷參數不含@ModelAttribute註解,那它就把參數作爲非簡單類型來處理,這樣的話,後面很多的HandlerMethodArgumentResolver將無法發揮作用。所以annotationNotRequired=true的ServletModelAttributeMethodProcessor是在最後才調用的。
然後再具體看看ServletModelAttributeMethodProcessor的處理過程:
- public final Object resolveArgument(
- MethodParameter parameter, ModelAndViewContainer mavContainer,
- NativeWebRequest request, WebDataBinderFactory binderFactory)
- throws Exception {
- String name = ModelFactory.getNameForParameter(parameter);
- Object attribute = (mavContainer.containsAttribute(name)) ?
- mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
- WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
- if (binder.getTarget() != null) {
- bindRequestParameters(binder, request);
- validateIfApplicable(binder, parameter);
- if (binder.getBindingResult().hasErrors()) {
- if (isBindExceptionRequired(binder, parameter)) {
- throw new BindException(binder.getBindingResult());
- }
- }
- }
- // Add resolved attribute and BindingResult at the end of the model
- Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
- mavContainer.removeAttributes(bindingResultModel);
- mavContainer.addAllAttributes(bindingResultModel);
- return binder.getTarget();
- }
首先就是獲取參數名的過程,String name = ModelFactory.getNameForParameter(parameter);具體內容如下:
- public static String getNameForParameter(MethodParameter parameter) {
- ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
- String attrName = (annot != null) ? annot.value() : null;
- return StringUtils.hasText(attrName) ? attrName : Conventions.getVariableNameForParameter(parameter);
- }
這裏先嚐試從@ModelAttribute註解中獲取參數名,若沒有則根據參數類型來獲取參數名
- public static String getVariableNameForParameter(MethodParameter parameter) {
- Assert.notNull(parameter, "MethodParameter must not be null");
- Class<?> valueClass;
- boolean pluralize = false;
- if (parameter.getParameterType().isArray()) {
- valueClass = parameter.getParameterType().getComponentType();
- pluralize = true;
- }
- else if (Collection.class.isAssignableFrom(parameter.getParameterType())) {
- valueClass = GenericCollectionTypeResolver.getCollectionParameterType(parameter);
- if (valueClass == null) {
- throw new IllegalArgumentException(
- "Cannot generate variable name for non-typed Collection parameter type");
- }
- pluralize = true;
- }
- else {
- valueClass = parameter.getParameterType();
- }
- String name = ClassUtils.getShortNameAsProperty(valueClass);
- return (pluralize ? pluralize(name) : name);
- }
- public static String getShortNameAsProperty(Class<?> clazz) {
- String shortName = ClassUtils.getShortName(clazz);
- int dotIndex = shortName.lastIndexOf('.');
- shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName);
- return Introspector.decapitalize(shortName);
- }
獲取類的簡單名稱如Teacher,然後再進行處理
- public static String decapitalize(String name) {
- if (name == null || name.length() == 0) {
- return name;
- }
- if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
- Character.isUpperCase(name.charAt(0))){
- return name;
- }
- char chars[] = name.toCharArray();
- chars[0] = Character.toLowerCase(chars[0]);
- return new String(chars);
- }
有了類的簡單名稱,如果類的簡單名稱第一個和第二個字母都大寫則不進行處理直接返回類的簡單名稱,否則僅僅將類的第一個大寫變成小寫。就此獲取到了參數名爲teacher。
然後就是獲取或者創建我們要綁定的Teacher對象。它首先嚐試從要返回的model中能否找到屬性名爲teacher的model,如找不到,就需要去創建一個:
- protected final Object createAttribute(String attributeName,
- MethodParameter parameter,
- WebDataBinderFactory binderFactory,
- NativeWebRequest request) throws Exception {
- String value = getRequestValueForAttribute(attributeName, request);
- if (value != null) {
- Object attribute = createAttributeFromRequestValue(value, attributeName, parameter, binderFactory, request);
- if (attribute != null) {
- return attribute;
- }
- }
- return super.createAttribute(attributeName, parameter, binderFactory, request);
- }
先嚐試從request參數中能否找到teacher這一個參數,找到了就進行綁定和轉換。未找到,就需要自己來實例化一個Teacher對象,此時並沒有綁定相應的參數值。
有個返回的目標,然後就是創建WebDataBinder實現綁定的過程:
WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
- public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName)
- throws Exception {
- WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
- if (this.initializer != null) {
- this.initializer.initBinder(dataBinder, webRequest);
- }
- initBinder(dataBinder, webRequest);
- return dataBinder;
- }
這一個過程,我們之前已經分析過。就是調度執行一些@InitBinder方法註冊一些PropertyEditor。我們繼續要來看看initBinder(dataBinder, webRequest);執行了那些@InitBinder方法:
- public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception {
- for (InvocableHandlerMethod binderMethod : this.binderMethods) {
- if (isBinderMethodApplicable(binderMethod, binder)) {
- Object returnValue = binderMethod.invokeForRequest(request, null, binder);
- if (returnValue != null) {
- throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod);
- }
- }
- }
- }
- protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder binder) {
- InitBinder annot = initBinderMethod.getMethodAnnotation(InitBinder.class);
- Collection<String> names = Arrays.asList(annot.value());
- return (names.size() == 0 || names.contains(binder.getObjectName()));
- }
當@InitBinder指定了value值的時候,只有那些value值含有binder.getObjectName()的纔會執行,而此時的binder.getObjectName()就是我們辛辛苦苦找出來的參數名teacher。所以本例中@InitBinder("teacher")會執行,而@InitBinder("student")則不會執行。
之後對四個參數 teacher.name='張三'、teacher.age=88、student.name='李四'、student.age=89 通過前綴進行過濾等其他操作實現了參數綁定。此過程不再分析,有興趣的可以繼續研究。
方案三:
使用自定義的HandlerMethodArgumentResolver:
表單提交的內容爲:
- <form action="/test/two" method="post">
- <input type="text" name="teacher.name" value="張三">
- <input type="text" name="teacher.age" value=88>
- <input type="text" name="teacher.date" value="2014---09---04 05:23:00">
- <input type="text" name="teacher.love" value="乒乓球,籃球">
- <input type="text" name="student.name" value="李四">
- <input type="text" name="student.age" value=89>
- <input type="text" name="student.date" value="2014---09---05 05:23:00">
- <input type="text" name="student.love" value="羽毛球,檯球">
- <input type="submit" value="提交">
- </form>
其中Teacher和Student做了相應的修改,加大了數據的複雜性。如下:
- public class Teacher {
- private String name;
- private int age;
- private Date date;
- private List<String> love;
- public Teacher() {
- super();
- }
- public Teacher(String name, int age) {
- super();
- this.name = name;
- this.age = age;
- }
- //get set 省略
- }
請求的的處理函數爲:
- @InitBinder
- public void initBinder(WebDataBinder binder) throws Exception {
- DateFormat df = new SimpleDateFormat("yyyy---MM---dd HH:mm:ss");
- CustomDateEditor dateEditor = new CustomDateEditor(df, true);
- binder.registerCustomEditor(Date.class, dateEditor);
- }
- @RequestMapping(value="/test/two",method=RequestMethod.POST)
- @ResponseBody
- public Map<String,Object> testrequestHeader(@MyForm Teacher a,@MyForm Student b){
- Map<String,Object> map=new HashMap<String,Object>();
- map.put("name","lg");
- map.put("age",23);
- map.put("date",new Date());
- return map;
- }
經過測試,通過。
自定義了兩個東西,一個就是標籤MyForm,另一個就是MyHandlerMethodArgumentResolver,並且我們從上一篇文章中知道如何將自定義HandlerMethodArgumentResolver加入HandlerMethodArgumentResolver大軍中。如下:
- <mvc:annotation-driven >
- <!--其他省略 -->
- <mvc:argument-resolvers>
- <bean class="com.lg.mvc.MyHandlerMethodArgumentResolver"/>
- </mvc:argument-resolvers>
- </mvc:annotation-driven>
我們來具體分析下這個過程,首先是註解 MyForm:
- package com.lg.annotation;
- import java.lang.annotation.Documented;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- @Target(ElementType.PARAMETER)
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface MyForm {
- String value() default "";
- }
只有有一個value屬性,用來指定from表單的中字段的前綴,若不指定,我將採取類名首字母小寫的規則來默認前綴。如@MyForm Teacher a,默認前綴是teacher。
然後就是MyHandlerMethodArgumentResolver,專門用來解析@MyForm註解的:
- package com.lg.mvc;
- import java.lang.reflect.Field;
- import org.springframework.core.MethodParameter;
- import org.springframework.util.ClassUtils;
- import org.springframework.web.bind.WebDataBinder;
- import org.springframework.web.bind.support.WebDataBinderFactory;
- import org.springframework.web.context.request.NativeWebRequest;
- import org.springframework.web.method.support.HandlerMethodArgumentResolver;
- import org.springframework.web.method.support.ModelAndViewContainer;
- import com.lg.annotation.MyForm;
- public class MyHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver{
- @Override
- public boolean supportsParameter(MethodParameter parameter) {
- if (parameter.hasParameterAnnotation(MyForm.class)) {
- return true;
- }
- return false;
- }
- @Override
- public Object resolveArgument(MethodParameter parameter,
- ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
- WebDataBinderFactory binderFactory) throws Exception {
- if (binderFactory==null) {
- return null;
- }
- Class<?> targetType=parameter.getParameterType();
- MyForm myForm=parameter.getParameterAnnotation(MyForm.class);
- String prefix=getprefix(myForm,targetType);
- Object arg=null;
- Field[] fields=targetType.getDeclaredFields();
- Object target=targetType.newInstance();
- WebDataBinder binder = binderFactory.createBinder(webRequest, null,prefix);
- for(Field field:fields){
- field.setAccessible(true);
- String fieldName=field.getName();
- Class<?> fieldType=field.getType();
- arg = binder.convertIfNecessary(webRequest.getParameter(prefix+"."+fieldName),fieldType, parameter);
- field.set(target,arg);
- }
- return target;
- }
- private String getprefix(MyForm myForm,Class<?> targetType) {
- String prefix=myForm.value();
- if(prefix.equals("")){
- prefix=getDefaultClassName(targetType);
- }
- return prefix;
- }
- private String getDefaultClassName(Class<?> targetType) {
- return ClassUtils.getShortNameAsProperty(targetType);
- }
- }
其實也挺簡單的。對於supportsParameter方法就是看看有沒有MyForm註解,若有則處理。
重點就在resolveArgument方法上:targetType就是MyForm所修飾的Teacher類或Student類,這裏以Teacher爲例。首先就是調用Teacher的無參的構造函數創建一個Teacher對象。然後由綁定工廠創建出綁定類,WebDataBinder binder = binderFactory.createBinder(webRequest, null,prefix);這一過程已在方案二中分析過了,就是執行那些符合的@InitBinder方法,這裏我們傳的值爲prefix,即MyForm的value,若沒指定就是類名的首字母小寫,在這裏就是teacher。也就是說那些@InitBinder的value值中含有teacher或者@InitBinder沒有指定value值的方法纔會被執行。因此我們這裏註冊的日期轉換CustomDateEditor會被註冊進去。然後就是執行綁定的過程。這個過程就是利用已註冊的PropertyEditor和Converter來進行Field類型的轉換。如下分析
遍歷它的Field,如String name,fieldType爲String。binder.convertIfNecessary(webRequest.getParameter(prefix+"."+fieldName),fieldType, parameter);這裏就是把teacher.name參數值轉換成fieldType,都是String,所以就不需要轉換器。對於Date date,就是把teacher.date參數的字符串值轉換成Date類型,然後就用到了我們註冊的CustomDateEditor,成功的進行了轉換。對於 List<String> love,就是把teacher.love參數的字符串值轉換成List集合,使用的是Spring已經註冊的StringToArrayConverter,字符串默認是以','分割。
該方案只能進行簡單類型的轉換(Teacher中field都是些簡單類型),還不支持Teacher中包含複雜類型如包含其他屬性類。其實也可以做成支持的,就是再稍加改造些,對於Field的處理先判斷是否是簡單類型,如Address類,若不是則遞歸調用上面的處理過程即對Address再次遍歷Field來實現Address中簡單類型的綁定。關鍵就是執行個遞歸調用,其他也沒什麼,有興趣的可以自行研究。本例中的自定義文件可在後面下載。
方案四:
根據方案二我們其實就可以想到更改下方案二所用到的ServletModelAttributeMethodProcessor,就可以達到我們想要的結果。即如下:
- public final Object resolveArgument(
- MethodParameter parameter, ModelAndViewContainer mavContainer,
- NativeWebRequest request, WebDataBinderFactory binderFactory)
- throws Exception {
- String name = ModelFactory.getNameForParameter(parameter);
- Object attribute = (mavContainer.containsAttribute(name)) ?
- mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
- //重點在這裏在這裏在這裏在這裏在這裏在這裏在這裏
- WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
- if (binder.getTarget() != null) {
- bindRequestParameters(binder, request);
- validateIfApplicable(binder, parameter);
- if (binder.getBindingResult().hasErrors()) {
- if (isBindExceptionRequired(binder, parameter)) {
- throw new BindException(binder.getBindingResult());
- }
- }
- }
- // Add resolved attribute and BindingResult at the end of the model
- Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
- mavContainer.removeAttributes(bindingResultModel);
- mavContainer.addAllAttributes(bindingResultModel);
- return binder.getTarget();
- }
WebDataBinder binder = binderFactory.createBinder(request, attribute, name);在創建出WebDataBinder後,調用下binder.setFieldDefaultPrefix(prefix);就可以大功告成了。然而,我們會看到該方法是final,不可覆蓋的,我就複製粘貼了一份,出來,新建了一個自定義的MyServletModelAttributeMethodProcessor以及它對應的註解標籤MyServletModelForm,代碼如下:
MyServletModelForm內容爲:
- @Target(ElementType.PARAMETER)
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface MyServletModelForm {
- String value() default "";
- }
MyServletModelAttributeMethodProcessor的主要內容爲:
- public boolean supportsParameter(MethodParameter parameter) {
- if (parameter.hasParameterAnnotation(MyServletModelForm.class)) {
- return true;
- }
- return false;
- }
- @Override
- public Object resolveArgument(MethodParameter parameter,
- ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
- WebDataBinderFactory binderFactory) throws Exception {
- String name = ModelFactory.getNameForParameter(parameter);
- Object attribute = (mavContainer.containsAttribute(name)) ?
- mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest);
- WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
- String prefix=getFieldDefaultPrefix(parameter);
- if(!prefix.equals("")){
- binder.setFieldDefaultPrefix(prefix+".");
- }
- if (binder.getTarget() != null) {
- bindRequestParameters(binder, webRequest);
- validateIfApplicable(binder, parameter);
- if (binder.getBindingResult().hasErrors()) {
- if (isBindExceptionRequired(binder, parameter)) {
- throw new BindException(binder.getBindingResult());
- }
- }
- }
- Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
- mavContainer.removeAttributes(bindingResultModel);
- mavContainer.addAllAttributes(bindingResultModel);
- return binder.getTarget();
- }
處理函數代碼爲:
- @RequestMapping(value="/test/two",method=RequestMethod.POST)
- @ResponseBody
- public Map<String,Object> testrequestHeader(@MyServletModelForm Teacher a,@MyServletModelForm Student b){
- Map<String,Object> map=new HashMap<String,Object>();
- map.put("name","lg");
- map.put("age",23);
- map.put("date",new Date());
- return map;
- }
並把MyServletModelAttributeMethodProcessor在xml文件中進行配置:
- <mvc:annotation-driven >
- <mvc:message-converters register-defaults="true">
- <bean class="org.springframework.http.converter.StringHttpMessageConverter">
- <constructor-arg value="UTF-8"/>
- </bean>
- </mvc:message-converters>
- <mvc:argument-resolvers>
- <bean class="com.lg.mvc.MyHandlerMethodArgumentResolver"/>
- <bean class="com.lg.mvc.MyServletModelAttributeMethodProcessor"/>
- </mvc:argument-resolvers>
- </mvc:annotation-driven>
轉載自:http://blog.csdn.net/z69183787/article/details/52817479