`

SpringMVC源码总结(十)自定义HandlerMethodArgumentResolver

阅读更多
上一篇文章介绍了HandlerMethodArgumentResolver的来龙去脉,这篇就要说说自定义HandlerMethodArgumentResolver来解决我们的需求,本文提供了四种解决方案。

需求,有一个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>  

经测试,可以。附件中含有这两个类供下载。
  • 大小: 53.3 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics