zoukankan      html  css  js  c++  java
  • 7、SpringMVC源码分析(2):分析HandlerAdapter.handle方法,了解handler方法的调用细节以及@ModelAttribute注解

      从上一篇 SpringMVC源码分析(1) 中我们了解到在DispatcherServlet.doDispatch方法中会通过 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) 这样的方式来执行request的handler方法。

      先来分析一下ha.handle方法的调用过程:HandlerAdapter接口有一个抽象实现类AbstractHandlerMethodAdapter,在该抽象类中通过具体方法handle调用抽象方法handleInternal:

     1 public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {
     2 
     3     private int order = Ordered.LOWEST_PRECEDENCE;
     4         @Override
     5     public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
     6             throws Exception {
     7         return handleInternal(request, response, (HandlerMethod) handler);
     8     }
     9         //抽象方法,由具体的Adapter实现
    10         protected abstract ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;
    11 
    12 }

       RequestMappingHandlerAdapter类继承了抽象类AbstractHandlerMethodAdapter,实现了抽象方法 handleInternal,下面看看handleInternal方法的具体实现(需要注意,handler方法在synchronizeOnSession为true的情况下会放在同步代码块中进行执行):

     1 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
     2     
     3     //省略若干代码...
     4     
     5     @Override
     6     protected ModelAndView handleInternal(HttpServletRequest request,
     7             HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
     8 
     9         if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
    10             // Always prevent caching in case of session attribute management.
    11             checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true);
    12         }
    13         else {
    14             // Uses configured default cacheSeconds setting.
    15             checkAndPrepare(request, response, true);
    16         }
    17 
    18         // Execute invokeHandlerMethod in synchronized block if required.
    19         /*
    20          * synchronizeOnSession默认为false,如果其为true,那么request对于的handler将会被放置在同步代码块
    21          * 中进行执行。问题:什么时候???通过怎样的方式将synchronizeOnSession设置为true???
    22          */
    23         if (this.synchronizeOnSession) {
    24             HttpSession session = request.getSession(false);
    25             if (session != null) {
    26                 Object mutex = WebUtils.getSessionMutex(session);
    27                 synchronized (mutex) {
    28                     return invokeHandleMethod(request, response, handlerMethod);
    29                 }
    30             }
    31         }
    32         // 不在同步块中执行handler方法
    33         return invokeHandleMethod(request, response, handlerMethod);
    34     }   
    35 }

       现在就分析上面代码块中的 invokeHandleMethod(request, response, handlerMethod) 方法的执行流程,看看在调用handler前后又完成了什么工作,同时分析出@ModelAttribute的作用。先来总体看看,然后再各个部分分别做 分析,一共分为6个步骤(step1 ~ step6):

     1 /**
     2  * Invoke the @RequestMapping handler method preparing a @ModelAndView
     3  * if view resolution is required.
     4  */
     5 private ModelAndView invokeHandleMethod(HttpServletRequest request,
     6         HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
     7 
     8     ServletWebRequest webRequest = new ServletWebRequest(request, response);
     9 
    10     WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
    11     ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
    12     ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);
    13 
    14     //step 1
    15     //新建一个mavContainer,用于存放所有可能会用到的ModelAndView
    16     ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    17     
    18     //step 2
    19     /*
    20      * Attributes can be set two ways. The servlet container may set attributes
    21      * to make available custom information about a request. For example, for
    22      * requests made using HTTPS, the attribute
    23      * <code>javax.servlet.request.X509Certificate</code> can be used to
    24      * retrieve information on the certificate of the client. Attributes can
    25      * also be set programatically using {@link ServletRequest#setAttribute}.
    26      * This allows information to be embedded into a request before a
    27      * {@link RequestDispatcher} call.
    28      *
    29      * RequestContextUtils.getInputFlashMap(request)可以获取到request中的attribute,
    30      * 并且将所有的request中的attribute放置在mavContainer中
    31      */
    32     mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
    33     
    34     //step 3
    35     /*
    36      * 会在这个方法里将所有标注了@ModelAttribute的方法调用一遍,并且将该方法相关的ModelAndView放入到mavContainer中:
    37      * 1、最常见的就是@ModelAttribute标注的方法入参中有Map,Model
    38      * 2、如果方法有返回值,那么也会结果处理后放入到mavContainer中(是否只有ModelAndView类型的返回值才能放,有待考察??)
    39      */
    40     modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
    41     mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
    42 
    43     //step 4
    44     //许多和 asyncManager 相关的东西,这个貌似和拦截器有关。
    45     AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
    46     asyncWebRequest.setTimeout(this.asyncRequestTimeout);
    47 
    48     final WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    49     asyncManager.setTaskExecutor(this.taskExecutor);
    50     asyncManager.setAsyncWebRequest(asyncWebRequest);
    51     asyncManager.registerCallableInterceptors(this.callableInterceptors);
    52     asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
    53 
    54     if (asyncManager.hasConcurrentResult()) {
    55         Object result = asyncManager.getConcurrentResult();
    56         mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
    57         asyncManager.clearConcurrentResult();
    58 
    59         if (logger.isDebugEnabled()) {
    60             logger.debug("Found concurrent result value [" + result + "]");
    61         }
    62         requestMappingMethod = requestMappingMethod.wrapConcurrentResult(result);
    63     }
    64     
    65     //step 5
    66     /*
    67      * Invokes the method and handles the return value through one of the configured {@link HandlerMethodReturnValueHandler}s.
    68      * 在这里调用handler方法
    69      */
    70     requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
    71 
    72     //step 6
    73     //要么返回ModelAndView,要么返回null
    74     if (asyncManager.isConcurrentHandlingStarted()) {
    75         return null;
    76     }
    77     return getModelAndView(mavContainer, modelFactory, webRequest);
    78 }

       

      下面从step1~step6逐一的进行分析。

      step1:ModelAndViewContainer mavContainer = new ModelAndViewContainer();

      首先看一下ModelMap是如何定义的,这对理解ModelAndViewContainer以及后面的代码有帮助,主要是理解:ModelMap实际上就是一个LinkedHashMap,而且这个Map的”值“是Object类型,能够放置所有类型的Java对象

    /**
     * 可以看出ModelMap实际上就是一个LinkedHashMap,且“值”为超类Object类型
     * 能够放置所有的Java对象
     */
    public class ModelMap extends LinkedHashMap<String, Object> {
        public ModelMap() {
        }
    
        // 调用这个构造函数之前会先调用其父类构造函数,得到一个Map对象
        public ModelMap(String attributeName, Object attributeValue) {
            addAttribute(attributeName, attributeValue);
        }
        
        // 就是将attributeValue对象放置到Map末尾,同时指定键值为attributeName
        public ModelMap addAttribute(String attributeName, Object attributeValue) {
            put(attributeName, attributeValue);
            return this;
        }
        
        // attributes是一个Map集合;所谓merge无非是将attributes这个集合放置到现有集合的末尾
        public ModelMap mergeAttributes(Map<String, ?> attributes) {
            if (attributes != null) {
                for (Map.Entry<String, ?> entry : attributes.entrySet()) {
                    String key = entry.getKey();
                    if (!containsKey(key)) {
                        put(key, entry.getValue());
                    }
                }
            }
            return this;
        }
        
        //省略一些方法的定义...
    }

      再来看ModelAndViewContainer到底是个什么东西,从下面的ModelAndViewContainer定义中不难理解其含有两个ModelMap对象defaultModel和redirectModel默认情况下使用defaultModel,也可以通过其方法设置使用redirectModel。

     1 /**
     2  * 定义了两个ModelMap对象:defaultModel和redirectModel,实际上也就是两个Map<String, Object>
     3  * 其中defaultModel已经完成了初始化。默认使用defaultModel。
     4  * 也可以通过其中的方法来设置使用redirectModel,这个是方便移植和使用其它框架而设定的。
     5  */
     6 public class ModelAndViewContainer {
     7 
     8     private boolean ignoreDefaultModelOnRedirect = false;
     9   // view的用法值得去探究
    10     private Object view;
    11     
    12     /* BindingAwareModelMap实际上也就是一个Map,
    13      * 看看定义 public class BindingAwareModelMap extends ModelMap implements Model
    14      *
    15      * 从这里可以看出,ModelAndViewContainer对象都有一个默认的ModelMap
    16      */
    17     private final ModelMap defaultModel = new BindingAwareModelMap();
    18 
    19     // 这个为方便移植其它框架的Model而设置的,SpringMVC本身使用的是defaultModel
    20     private ModelMap redirectModel;
    21 
    22     private boolean redirectModelScenario = false;
    23 
    24     private final SessionStatus sessionStatus = new SimpleSessionStatus();
    25 
    26     private boolean requestHandled = false;
    27     
    28     /**
    29      * Set a view name to be resolved by the DispatcherServlet via a ViewResolver.
    30      * Will override any pre-existing view name or View.
    31      */
    32     public void setViewName(String viewName) {
    33         this.view = viewName;
    34     }
    35     
    36     /**
    37      * 返回"default" 或者是 "redirect" 模型,具体根据redirectModelScenario等
    38      * 属性的值来确定(具体用法参看javadoc)
    39      */
    40     public ModelMap getModel() {
    41         if (useDefaultModel()) {
    42             return this.defaultModel;
    43         }
    44         else {
    45             return (this.redirectModel != null) ? this.redirectModel : new ModelMap();
    46         }
    47     }
    48     
    49     // 返回默认的Model,而不考虑其它的属性值如何
    50     public ModelMap getDefaultModel() {
    51         return this.defaultModel;
    52     }
    53     
    54 }

      到现在为止,step1完成的工作已经分析完全。

      step2:  mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request))

    /* * 
    * 检索request域中的attribute,并将其放置在mavContainer末尾 * * RequestContextUtils.getInputFlashMap(request)会调用HttpServletRequest.getAttribute方法。 * 可以获取到request中的attribute属性。这个attribute就是我们属性的attribute,它可以通过两 * 种方式来设置:①servlet容器为request设置的;②通过ServletRequest.setAttribute方法来设置。 * */ mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));

      step3modelFactory.initModel(webRequest, mavContainer, requestMappingMethod)

      这个方法的作用在javadoc中描述得很清楚:

      Populate the model in the following order:

    1. Retrieve "known" session attributes listed as @SessionAttributes.
    2. Invoke @ModelAttribute methods
    3. Find @ModelAttribute method arguments also listed as @SessionAttributes and ensure they're present in the model raising an exception if necessary.

      也就是说初始化模型的时候会按顺序完成三件事情

      ①、检索现有的session域中的attributes,并将其放置于mavContainer末尾;

      ②、调用所有@ModelAttribute注解标注的方法;

      ③、找出handler方法中使用@ModelAttribute注解修饰的入参(主要是@ModelAttribute指定的value属性值,如果没有指定则是类名第一个字母小写得到,我们假定它为V),如果V同时被@SessionAttributes的value属性值指定,那么就必须保证在此时的mavContainer中必须含有”key“为V的对象。如果没有,则会抛出一个异常

      

      带着这个印象我们分析代码就会容易很多:

     1 /*
     2  * 会在这个方法里将所有标注了@ModelAttribute的方法调用一遍,并且将该方法相关的ModelAndView放入到mavContainer中:
     3  * 1、最常见的就是@ModelAttribute标注的方法入参中有Map,Model
     4  * 2、如果方法有返回值,那么也会结果处理后放入到mavContainer中(是否只有ModelAndView类型的返回值才能放,有待考察??)
     5  */
     6 modelFactory.initModel(webRequest, mavContainer, requestMappingMethod){
     7     //完成①:检索现有的session域中的attributes,并将其放置于mavContainer末尾
     8     Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
     9     mavContainer.mergeAttributes(sessionAttributes);
    10 
    11     //完成②:将所有的@ModelAttribute标注的方法都调用一遍。调用完了以后,将调用方法的结果放置到mavContainer中
    12     invokeModelAttributeMethods(request, mavContainer){
    13         //modelMethods中包含了所有@ModelAttribute标注的方法,在这个while循环中将会把所有@ModelAttribute标注
    14         //的方法都调用一遍
    15         while (!this.modelMethods.isEmpty()) {
    16             InvocableHandlerMethod attrMethod = getNextModelMethod(mavContainer).getHandlerMethod();
    17             String modelName = attrMethod.getMethodAnnotation(ModelAttribute.class).value();
    18             //如果,mavContainer中已经包含有modelName名的attribute,那么,将不会调用@ModelAttribute标注的方法
    19             if (mavContainer.containsAttribute(modelName)) {
    20                 continue;
    21             }
    22 
    23             //真正的调用@ModelAttribute标注的方法
    24             Object returnValue = attrMethod.invokeForRequest(request, mavContainer);
    25             
    26             //如果@ModelAttribute标注的方法不是void类型,则将其返回结果转换成returnValueName;如果mavContainer中
    27             //没有包含returnValueName,则将方法返回的结果放置到mavContainer中。
    28             if (!attrMethod.isVoid()){
    29                 String returnValueName = getNameForReturnValue(returnValue, attrMethod.getReturnType());
    30                 if (!mavContainer.containsAttribute(returnValueName)) {
    31                     mavContainer.addAttribute(returnValueName, returnValue);
    32                 }
    33             }
    34         }
    35     };    
    36     
    37 
    38      // 完成③:
    39      
    40      /*
    41      * 找出handler方法中使用@ModelAttribute注解修饰的入参(主要是@ModelAttribute指定的value属性值,
    42      * 如果没有指定则是类名第一个字母小写得到,我们假定它为V),如果V同时被@SessionAttributes的value
    43      * 属性值指定,则将这样的V放入到nameList中。
    44      *
    45      */
    46     List<String> nameList = findSessionAttributeArguments(handlerMethod){
    47         List<String> result = new ArrayList<String>();
    48         //遍历处理方法的所有参数
    49         for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
    50             //如果处理方法有@ModelAttribute标注
    51             if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
    52                 //得到参数的String型的名字
    53                 String name = getNameForParameter(parameter);
    54                 //如果在处理方法所在的类定义处使用了@SessionAttributes注解,那么就将该参数放入到List中
    55                 if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, parameter.getParameterType())) {
    56                     result.add(name);
    57                 }
    58             }
    59         }
    60         return result;
    61     };
    62     
    63     // 保证nameList中记录的V必须在mavContainer中存在,如果不存在则会抛出异常
    64     for (String name : nameList) {
    65         //如果mavContainer中没有包含有name名字的attribute,那么再一次检查request中是否包含了name名字的attribute
    66         if (!mavContainer.containsAttribute(name)) {
    67             //再一次检查request中是否包含了name名字的attribute
    68             Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
    69             //如果没有检测到,则会抛出一个异常
    70             if (value == null) {
    71                     throw new HttpSessionRequiredException("Expected session attribute '" + name + "'");
    72             }
    73             //如果检测到了,那么会将这个attribute放入到mavContainer中
    74             mavContainer.addAttribute(name, value);
    75         }
    76     }
    77 };

      step4: 暂时不做分析...

      step5: 调用handler方法同时处理返回结果

      1 requestMappingMethod.invokeAndHandle(webRequest, mavContainer){
      2         //一、 调用处理方法,并的到返回结果
      3         Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs){
      4             //为调用处理方法准备参数
      5             Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs){
      6                 
      7                 //...
      8                 
      9                 //从这里可以看出@ModelAttribute能够修饰handler的入参
     10                 String name = ModelFactory.getNameForParameter(parameter){
     11                     ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
     12                     //如果当前参数使用了@ModelAttribute标注,则获取到该标签的value属性值
     13                     String attrName = (annot != null) ? annot.value() : null;
     14                     //如果attrName有text,则返回该attrName值,也就是@ModelAttribute的value属性值。
     15                     //反之,则返回参数类型第一个字母小写后得到的字符串
     16                     return StringUtils.hasText(attrName) ? attrName :  Conventions.getVariableNameForParameter(parameter);    
     17                 };
     18                 
     19                 //检测mavContainer中是否包含有name关键字的ModelAndView,如果包含有,则获取它并返回。如果没有,则创建一个attribute
     20                 Object attribute = (mavContainer.containsAttribute(name) ?
     21                         mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest));
     22         
     23                 //对于WebDataBinder而言,最重要的参数就是name和attribute
     24                 //将请求域中表单数据绑定到上面的到的attribute中:request域中有的就重新赋值,没有的就保持原有的属性值不变。
     25                 /*
     26                 *  结合前面name和attribute的获取过程可以分析出request表单数据绑定的过程:
     27                 *  ①、如果handler的入参使用了@ModelAttribute,同时还指定了其value属性值,那么attrName就是其value属性值;
     28                 *  ②、如果handler的入参处没有指定@ModelAttribute的value属性值,或者是根本就没有使用该注解,那么其
     29                 *      attrName就是参数类名第一个字母小写的到;
     30                 *  ③、搜索mavContainer中是否有键值为attrName的attribute对象,如果有,则将这个对象作为表单数据绑定的对象;
     31                 *      如果没有,则新创建一个作为数据绑定的对象;
     32                 */
     33                 WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
     34                 if (binder.getTarget() != null) {
     35                     //绑定参数
     36                     bindRequestParameters(binder, webRequest);
     37                     validateIfApplicable(binder, parameter);
     38                     if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
     39                         throw new BindException(binder.getBindingResult());
     40                     }
     41                 }
     42                 
     43                 // Add resolved attribute and BindingResult at the end of the model
     44                 Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
     45                 //删除原有的attribute
     46                 mavContainer.removeAttributes(bindingResultModel);
     47                 //将处理过的attribute添加到末尾,这样mavContainer中相对应的attribute就是更新以后的attribute
     48                 mavContainer.addAllAttributes(bindingResultModel);
     49 
     50                 
     51                 //返回需要格式的args
     52                 return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
     53             };
     54             
     55             //**真正的调用处理方法,此时的args是依据request表单数据更新过的args
     56             Object returnValue = doInvoke(args);
     57             
     58             //返回处理方法返回的结果,这个结果将会进一步处理
     59             return returnValue;
     60         };
     61         
     62         //二、处理目标方法的返回结果
     63         //设置应答状态
     64         setResponseStatus(webRequest);
     65 
     66         if (returnValue == null) {
     67             if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) {
     68                 mavContainer.setRequestHandled(true);
     69                 return;
     70             }
     71         }
     72         else if (StringUtils.hasText(this.responseReason)) {
     73             mavContainer.setRequestHandled(true);
     74             return;
     75         }
     76 
     77         mavContainer.setRequestHandled(false);
     78         try {
     79             //处理返回结果
     80             this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest){
     81                     //获取返回结果对应的处理方法
     82                     HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType);
     83                     Assert.notNull(handler, "Unknown return value type [" + returnType.getParameterType().getName() + "]");
     84                     //利用获取到的结果处理方法来处理结果
     85                     handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest){
     86                         if (returnValue == null) {
     87                             return;
     88                         }
     89                         else if (returnValue instanceof String) {
     90                             String viewName = (String) returnValue;
     91                             mavContainer.setViewName(viewName);
     92                             if (isRedirectViewName(viewName)) {
     93                                 mavContainer.setRedirectModelScenario(true);
     94                             }
     95                         }
     96                         else {
     97                             // should not happen
     98                             throw new UnsupportedOperationException("Unexpected return type: " +
     99                                     returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
    100                         }    
    101                     };        
    102             };
    103         }
    104         
    105 };

      从上面的代码分析可以得出表单数据绑定的流程:

      1、request的表单数据绑定首先需要创建一个WebDataBinder对象: binder = binderFactory.createBinder(webRequest, attribute, name)。表单数据在webRequest中,还要确定两个关键的参数:attribute【Object类型】, name【String类型】。

      2、确定name(也就是attrName):

        ①、如果handler的入参处使用了@ModelAttribute注解,同时该注解还制定了value属性值,那么name就是value的属性值;

        ②、如果handler的入参数使用了@ModelAttribute注解,但是没有指定value属性值;或者是,入参处根本就没有使用@ModelAttribute注解;那么这2种情况下其name值就是handler入参类名第一个字母小写得到的String;

      3、确定attribute:

        ①、查看mavContainer中是否包含有key=name的attribute对象(mavContainer.getModel()实际上得到的是一个Map<String, Object>)。如果有,则attribute就是该对象;如果没有,则新创建一个对象赋给attribute。其代码如下:

        Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest));

      4、通过已经确定好的attribute和name就能够完成数据的绑定了。

      step6: 返回ModelAndView

    1 if (asyncManager.isConcurrentHandlingStarted()) {
    2     return null;
    3 }
    4 
    5 return getModelAndView(mavContainer, modelFactory, webRequest);

      返回有两种情况,第一个貌似和同步机制有关,asyncManager的工作机制后续继续分析。这里主要是分析getModelAndView(mavContainer, modelFactory, webRequest)方法的源码。

     1 private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
     2         ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
     3 
     4     modelFactory.updateModel(webRequest, mavContainer);
     5     if (mavContainer.isRequestHandled()) {
     6         return null;
     7     }
     8     ModelMap model = mavContainer.getModel();
     9     ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model);
    10     if (!mavContainer.isViewReference()) {
    11         mav.setView((View) mavContainer.getView());
    12     }
    13     if (model instanceof RedirectAttributes) {
    14         Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
    15         HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
    16         RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
    17     }
    18     return mav;
    19 }

      视图模型是一个大的主题,后面再仔细分析。

      

  • 相关阅读:
    学习新东西 方法
    转 Dock 外 命令解析
    转 Dockerfile 常用指令
    RPC应用的java实现(转)
    link with editor
    org.xml.sax.SAXParseException: prolog 中不允许有内容
    webservice
    logging.xml file setfile(null,true) call failed
    log4j配置 logging.xml (转载)
    tomcat server.xml docbase workdir
  • 原文地址:https://www.cnblogs.com/lj95801/p/4964393.html
Copyright © 2011-2022 走看看