SpringMVC学习记录3
这次的主题
最近一直在学习SpringMVC..(这句话我已经至少写了3,4遍了....).这次的研究主要是RequestMappingHandlerAdapter中的各种ArgumentsResolver....
前面写了太多理论的东西...这次我想来点实践的....
SpringMVC自定义的@Controller的方法参数如果有多个,并且有重复的属性名称的话是默认不支持的..这点和Struts2很不一样..可能很多用过Struts2的朋友都不习惯SpringMVC的这种用法..
确实,我也是觉得Struts2那样方便很多.所以我想介绍下如何增强SpringMVC的功能,达到我们的目的.
注:具体步骤涉及了太多的类...在这篇文章中我不想弄的太复杂...不会介绍很多原理....
方法:改源码
具体方案
这方面网上也有不少例子....但是貌似都不简单..我先来介绍一种最简单粗暴的方法...直接改源码.
真真真最简单...源码基础上加一行即可!
1 package org.springframework.web.method.annotation; 2 3 /* 4 * Copyright 2002-2015 the original author or authors. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 import java.lang.annotation.Annotation; 20 import java.util.Map; 21 22 import org.apache.commons.logging.Log; 23 import org.apache.commons.logging.LogFactory; 24 25 import org.springframework.beans.BeanUtils; 26 import org.springframework.core.MethodParameter; 27 import org.springframework.core.annotation.AnnotationUtils; 28 import org.springframework.validation.BindException; 29 import org.springframework.validation.Errors; 30 import org.springframework.validation.annotation.Validated; 31 import org.springframework.web.bind.WebDataBinder; 32 import org.springframework.web.bind.annotation.ModelAttribute; 33 import org.springframework.web.bind.support.WebDataBinderFactory; 34 import org.springframework.web.bind.support.WebRequestDataBinder; 35 import org.springframework.web.context.request.NativeWebRequest; 36 import org.springframework.web.method.support.HandlerMethodArgumentResolver; 37 import org.springframework.web.method.support.HandlerMethodReturnValueHandler; 38 import org.springframework.web.method.support.ModelAndViewContainer; 39 40 /** 41 * Resolves method arguments annotated with {@code @ModelAttribute} and handles 42 * return values from methods annotated with {@code @ModelAttribute}. 43 * 44 * <p>Model attributes are obtained from the model or if not found possibly 45 * created with a default constructor if it is available. Once created, the 46 * attributed is populated with request data via data binding and also 47 * validation may be applied if the argument is annotated with 48 * {@code @javax.validation.Valid}. 49 * 50 * <p>When this handler is created with {@code annotationNotRequired=true}, 51 * any non-simple type argument and return value is regarded as a model 52 * attribute with or without the presence of an {@code @ModelAttribute}. 53 * 54 * @author Rossen Stoyanchev 55 * @since 3.1 56 */ 57 public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { 58 59 protected final Log logger = LogFactory.getLog(getClass()); 60 61 private final boolean annotationNotRequired; 62 63 64 /** 65 * @param annotationNotRequired if "true", non-simple method arguments and 66 * return values are considered model attributes with or without a 67 * {@code @ModelAttribute} annotation. 68 */ 69 public ModelAttributeMethodProcessor(boolean annotationNotRequired) { 70 this.annotationNotRequired = annotationNotRequired; 71 } 72 73 74 /** 75 * Returns {@code true} if the parameter is annotated with {@link ModelAttribute} 76 * or in default resolution mode, and also if it is not a simple type. 77 */ 78 @Override 79 public boolean supportsParameter(MethodParameter parameter) { 80 if (parameter.hasParameterAnnotation(ModelAttribute.class)) { 81 return true; 82 } 83 else if (this.annotationNotRequired) { 84 return !BeanUtils.isSimpleProperty(parameter.getParameterType()); 85 } 86 else { 87 return false; 88 } 89 } 90 91 /** 92 * Resolve the argument from the model or if not found instantiate it with 93 * its default if it is available. The model attribute is then populated 94 * with request values via data binding and optionally validated 95 * if {@code @java.validation.Valid} is present on the argument. 96 * @throws BindException if data binding and validation result in an error 97 * and the next method parameter is not of type {@link Errors}. 98 * @throws Exception if WebDataBinder initialization fails. 99 */ 100 @Override 101 public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 102 NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 103 104 String name = ModelFactory.getNameForParameter(parameter); 105 Object attribute = (mavContainer.containsAttribute(name) ? 106 mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest)); 107 108 WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); 109 if (binder.getTarget() != null) { 110 binder.setFieldDefaultPrefix(parameter.getParameterName() + "!"); 111 bindRequestParameters(binder, webRequest); 112 validateIfApplicable(binder, parameter); 113 if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { 114 throw new BindException(binder.getBindingResult()); 115 } 116 } 117 118 // Add resolved attribute and BindingResult at the end of the model 119 Map<String, Object> bindingResultModel = binder.getBindingResult().getModel(); 120 mavContainer.removeAttributes(bindingResultModel); 121 mavContainer.addAllAttributes(bindingResultModel); 122 123 return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); 124 } 125 126 /** 127 * Extension point to create the model attribute if not found in the model. 128 * The default implementation uses the default constructor. 129 * @param attributeName the name of the attribute (never {@code null}) 130 * @param methodParam the method parameter 131 * @param binderFactory for creating WebDataBinder instance 132 * @param request the current request 133 * @return the created model attribute (never {@code null}) 134 */ 135 protected Object createAttribute(String attributeName, MethodParameter methodParam, 136 WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception { 137 138 return BeanUtils.instantiateClass(methodParam.getParameterType()); 139 } 140 141 /** 142 * Extension point to bind the request to the target object. 143 * @param binder the data binder instance to use for the binding 144 * @param request the current request 145 */ 146 protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { 147 ((WebRequestDataBinder) binder).bind(request); 148 } 149 150 /** 151 * Validate the model attribute if applicable. 152 * <p>The default implementation checks for {@code @javax.validation.Valid}, 153 * Spring's {@link org.springframework.validation.annotation.Validated}, 154 * and custom annotations whose name starts with "Valid". 155 * @param binder the DataBinder to be used 156 * @param methodParam the method parameter 157 */ 158 protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) { 159 Annotation[] annotations = methodParam.getParameterAnnotations(); 160 for (Annotation ann : annotations) { 161 Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); 162 if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { 163 Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); 164 Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); 165 binder.validate(validationHints); 166 break; 167 } 168 } 169 } 170 171 /** 172 * Whether to raise a fatal bind exception on validation errors. 173 * @param binder the data binder used to perform data binding 174 * @param methodParam the method argument 175 * @return {@code true} if the next method argument is not of type {@link Errors} 176 */ 177 protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) { 178 int i = methodParam.getParameterIndex(); 179 Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes(); 180 boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1])); 181 return !hasBindingResult; 182 } 183 184 /** 185 * Return {@code true} if there is a method-level {@code @ModelAttribute} 186 * or if it is a non-simple type when {@code annotationNotRequired=true}. 187 */ 188 @Override 189 public boolean supportsReturnType(MethodParameter returnType) { 190 if (returnType.getMethodAnnotation(ModelAttribute.class) != null) { 191 return true; 192 } 193 else if (this.annotationNotRequired) { 194 return !BeanUtils.isSimpleProperty(returnType.getParameterType()); 195 } 196 else { 197 return false; 198 } 199 } 200 201 /** 202 * Add non-null return values to the {@link ModelAndViewContainer}. 203 */ 204 @Override 205 public void handleReturnValue(Object returnValue, MethodParameter returnType, 206 ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { 207 208 if (returnValue != null) { 209 String name = ModelFactory.getNameForReturnValue(returnValue, returnType); 210 mavContainer.addAttribute(name, returnValue); 211 } 212 } 213 214 }
增加的就是第110行
1 binder.setFieldDefaultPrefix(parameter.getParameterName() + "!");
parameter.getParameterName()返回的是你@Controller里2RequestMapping方法参数的名字
"!"是我区分成员域与对象名的分解符...这个可以自己设置.你想设置成.也可以!也可以#也OK
只要自己能区分就行了.
测试
URL:
1 http://localhost:8080/quick-start/test18?context!stateCode=91&a!name=hehe&context!exception.message=error&a!stateCode=84
后台打印参数:
1 com.labofjet.web.ContextDTO@9344568[stateCode=91,data=<null>,exception=com.labofjet.exception.BaseException: error] 2 com.labofjet.dto.ADTO@814d736[id=<null>,name=hehe,age=<null>,value=<null>,b=0,stateCode=84]
Controller的方法签名:
1 @RequestMapping("/test18") 2 public Object index18(ContextDTO context, ADTO a) throws IOException { 3 System.out.println(context); 4 System.out.println(a); 5 return null; 6 }
原理
先简明说下原理..具体的理论我想后面等我整理下思路,介绍RequestMappingHandlerAdapter的时候再讲(反正没人看...)
SpringMVC里@Controller里的各种参数由各种argumentsResolver来解析.
解析自定义的这种DTO的argumentsResolver是ServletModelAttributeMethodProcessor这个类.
收到参数以后他怎么解析呢?
很简单呀.在我测试例子中,比如我要初始化一个context对象,SpringMVC就先把context包装成BeanWrapperImpl对象..然后把Request里的各种key-value包装成MutablePropertyValues..然后set进去就可以了.利用的是反射的知识,你有一个key,那我就看BeanWrapperImpl warp的那个对象有没有对应的属性.有就设置进去.
既然原理是这样,那怎么达到我们的目的呢?
这里又有至少2种方法:
第一种就是像我那样: binder.setFieldDefaultPrefix(parameter.getParameterName() + "!"); 这样在解析MutablePropertyValues的时候会去掉你set进去的Feild的Prefix.
第二种方法: ServletRequestDataBinder在绑定的参数的时候需要先把request转化成MutablePropertyValues,做法是:
1 public void bind(ServletRequest request) { 2 MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); 3 MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class); 4 if (multipartRequest != null) { 5 bindMultipart(multipartRequest.getMultiFileMap(), mpvs); 6 } 7 addBindValues(mpvs, request); 8 doBind(mpvs); 9 }
也就是说:
1 MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
而MutablePropertyValues 其实还有一种构造方法:
1 public ServletRequestParameterPropertyValues(ServletRequest request, String prefix) { 2 this(request, prefix, DEFAULT_PREFIX_SEPARATOR); 3 }
这种方法允许你填入一个prefix...那原理和前面是一样的..
但是这种方法需要改很多类...所以没有方法一简单....
为什么不使用继承
可能会有朋友想问.改进的ArgumentResolver和原本Spring自带的基本一样,只是多了一步,为什么不继承自原本的ServletModelAttributeMethodProcessor? 毕竟修改源码方案不太好.
原因有很多,主要有2个原因:
原因1:
ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor
resolveArgument在ModelAttributeMethodProcessor里的定义是:
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { ........... }
是final啊!final!!!!!!!!!!!
所以我修改不了.
原因2:
一般父类定义了一个处理流程的话不能修改的话,会在子类给我们留一个扩展接口...
没错,那就是:
1 @Override 2 protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { 3 ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); 4 ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; 5 servletBinder.bind(servletRequest); 6 }
这个是在ServletModelAttributeMethodProcessor里覆盖了父类的方法,我们可以继承ServletModelAttributeMethodProcessor再覆盖这个bindRequestParameters方法..
但是......
这里只有2个参数啊!!!!我们需要的@Controller的参数名称的信息不在这里....我们需要这个变量:
1 public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 2 NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 3 ............. 4 }
参数的信息都在这里里面..可是bindRequestParameters方法里没有传入这个参数...所以坑爹的是我们在bindRequestParameters里并不能知道@Controller里参数的名字到底是什么...
所以我们没有办法设置一个通用的绑定方法...
方法:利用其它的HandlerMethodArgumentResolver
具体方法
不改源码最简单的方法可能是不自己写ArgumentResolver,而是利用SpringMVC原有的Resolver了吧..
我们可以利用RequestResponseBodyMethodProcessor这个ArgumentResolver..
具体请参考我的另外一篇文章:
http://www.cnblogs.com/abcwt112/p/5169250.html
原理就是利用HttpMessageConverter和其它的json转化工具将request里的参数转化成java bean.这也是很简单的.
基本只要在参数前加一个@RequestBody...
方法:利用@InitBinder注解
具体:
请大家看这篇文章:
http://jinnianshilongnian.iteye.com/blog/1888474
缺点:
1.原理和方法:改源码是差不多的....都是通过修改binder设置额外属性来达到目的的,但是没传入MethodParameter parameter,所以还是不知道你的@Controller里的参数名字..只能手动指定前缀
2.貌似要绑定的时候每个Controller里都要写@InitBinder,稍微有点麻烦..当然好处是更灵活...
方法N:自己实现HandlerMethodArgumentResolver
这个方法就太多了......
请参考:
http://jinnianshilongnian.iteye.com/blog/1717180
简单总结
方法有太多太多了..不同方法可能适合不同场景,但是我觉得最简单的还是@InitBinder和@RequestBody这2种方案.