zoukankan      html  css  js  c++  java
  • 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种方案.

  • 相关阅读:
    清北学堂2019.7.18 & 清北学堂2019.7.19
    清北学堂2019.7.17
    清北学堂2019.7.16
    清北学堂2019.7.15
    清北学堂2019.7.14
    清北学堂2019.7.13
    【洛谷P1383 高级打字机】
    考试整理
    考试整理
    【洛谷P5018 对称二叉树】
  • 原文地址:https://www.cnblogs.com/abcwt112/p/5302469.html
Copyright © 2011-2022 走看看