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种方案.

 

posted @ 2016-03-21 16:23 abcwt112 阅读(...) 评论(...) 编辑 收藏