SpringMVC源码解读 --- 视图(View)的结构及源码分析

  实现我们直接来看下View接口

    1、View(视图)

public interface View {

   String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
   String PATH_VARIABLES = View.class.getName() + ".pathVariables";
   String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";

   @Nullable
   default String getContentType() {
      return null;
   }
   void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
         throws Exception;
}

    可以看到其主要是有两个方法getContentType(获取请求头)、render。下面我们来看下其的实现类

   2、AbstractView

     1、结构

public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware {

     可以看到AbstractView也和前面一样有继承WebApplicationObjectSupport这个主要就是与Http也一些缓存、头部处理等相关。

    2、setAttributesCSV

public void setAttributesCSV(@Nullable String propString) throws IllegalArgumentException {
     .......
}

   这个CSV现在还不是很了解,想欠下。

   3、render

@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
      HttpServletResponse response) throws Exception {

   Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
   prepareResponse(request, response);
   renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

   4、createMergedOutputModel

protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model,
      HttpServletRequest request, HttpServletResponse response) {

   @SuppressWarnings("unchecked")
   Map<String, Object> pathVars = (this.exposePathVariables ?
         (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null);
     .............
   Map<String, Object> mergedModel = new LinkedHashMap<>(size);
   mergedModel.putAll(this.staticAttributes);
   if (pathVars != null) {
      mergedModel.putAll(pathVars);
   }
   if (model != null) {
      mergedModel.putAll(model);
   }
   if (this.requestContextAttribute != null) {
      mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
   }

   return mergedModel;
}
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
private final Map<String, Object> staticAttributes = new LinkedHashMap<>();
private String requestContextAttribute;

   这个View.PATH_VARIABLES key的使用是在这个ArgumentResolver使用的,所以这里就是通过exposePathVariables这个全局变量,来控制,看要不要将@PathVariable注解解析获取的内容放到Model中。再之后是将staticAttributes全局Map放到mergedModel中。再之后是看有没有设置this.requestContextAttribute的值。这个this.requestContextAttribute的是在是在UrlBasedViewResolver中创建View的时候设置的。

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
   ........
   AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
   .........
   view.setRequestContextAttribute(getRequestContextAttribute());
    ...........
   return view;
}

   而这个的是在就需要像我们配置视图解析器一样,设置到xml文件中。例如:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="jspViewResolver">
    <property value="org.springframework.web.servlet.view.JstlView" name="viewClass"/>
    <property value="/WEB-INF/" name="prefix"/>
    <property value=".jsp" name="suffix"/>
    <property name="requestContextAttribute" value="requestContextAttributeTest"/>
</bean>
protected RequestContext createRequestContext(
      HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) {
   return new RequestContext(request, response, getServletContext(), model);
}

   可以看到这里,如果有主动设置requestContextAttribute,就会创建RequestContext对象,可以看到其的初始化参数,这就说明你能通过Model获取到这个RequestContext来使用这些参数去处理相应的数据了。

   5、prepareResponse

protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
   if (generatesDownloadContent()) {
      response.setHeader("Pragma", "private");
      response.setHeader("Cache-Control", "private, must-revalidate");
   }
}
protected boolean generatesDownloadContent() {
   return false;
}

   可以看到这个方法就是设置与缓存相关的内容,但没人为false。

   6、renderMergedOutputModel

protected abstract void renderMergedOutputModel(
      Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

   这个方法是抽象方法,需要其子类去实现。

   7、exposeModelAsRequestAttributes

protected void exposeModelAsRequestAttributes(Map<String, Object> model,
      HttpServletRequest request) throws Exception {

   model.forEach((modelName, modelValue) -> {
      if (modelValue != null) {
         request.setAttribute(modelName, modelValue);
          ..........
      }
      else {
         request.removeAttribute(modelName);
      }
   });
}

   这个就是遍历,看这个model的key有没有值,如果没有就从request中移除,有的话,将将其设置到request中。这里就是处理request的Attribute的内容,移除或者设置(可能覆盖掉原来的value)。

  8、writeToResponse

protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos) throws IOException {
   response.setContentType(getContentType());
   response.setContentLength(baos.size());

   // Flush byte array to servlet output stream.
   ServletOutputStream out = response.getOutputStream();
   baos.writeTo(out);
   out.flush();
}

    这个就是将baos中的内容写入到response的输出流中。

  3、AbstractUrlBasedView

      1、结构

public abstract class AbstractUrlBasedView extends AbstractView implements InitializingBean {

     其继承AbstractView,然后有实现InitializingBean接口

    2、afterPropertiesSet

@Override
public void afterPropertiesSet() throws Exception {
   if (isUrlRequired() && getUrl() == null) {
      throw new IllegalArgumentException("Property 'url' is required");
   }
}
protected boolean isUrlRequired() {
   return true;
}

    通过前面视图解析器我们知道一个View在创建的时候是会调用initializeBean方法的,在这个方法中就会调用InitializingBean接口,所以这里就会校验看这个View有没有Url,如果没有就IllegalArgumentException。

  3、checkResource

public boolean checkResource(Locale locale) throws Exception {
   return true;
}

   我们可以看到其的实现又这些 ,我们看下我们比较眼熟的两个JstlView、RedirectView。

             

     JstlView继承与InternalResourceView,所以我们先看下InternalResourceView

   4、InternalResourceView

      1、结构

public class InternalResourceView extends AbstractUrlBasedView {

   private boolean alwaysInclude = false;

   private boolean preventDispatchLoop = false;
    ..........

    2、renderMergedOutputModel

@Override
protected void renderMergedOutputModel(
      Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
   exposeModelAsRequestAttributes(model, request);
   exposeHelpers(request);

   String dispatcherPath = prepareForRendering(request, response);
   RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
   if (rd == null) {
      throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
            "]: Check that the corresponding file exists within your web application archive!");
   }
   if (useInclude(request, response)) {
      response.setContentType(getContentType());
      }
      rd.include(request, response);
   }

   else {
      rd.forward(request, response);
   }
}

    可以看到这里先是调用父类AbstractView   exposeModelAsRequestAttributes方法。然后获取RequestDispatcher,再判断其是需要使用include或者forward。我们知道这个都是跳转到其他的地址,关键是这两种的区别,(不深入进入tomcat源码分析,简单来说就是一个处理后直接关闭输出流,而另一个不会)。

 例如tomcat中对于doForward方法的处理

private void doForward(ServletRequest request, ServletResponse response)
    throws ServletException, IOException
{
         .........
        processRequest(request,response,state);
          .......
            PrintWriter writer = response.getWriter();
            writer.close();
         ..........
}
private void processRequest(ServletRequest request,ServletResponse response,State state)
    throws IOException, ServletException {
           ........
                invoke(state.outerRequest, response, state);
            ............
}
private void invoke(ServletRequest request, ServletResponse response,
        State state) throws IOException, ServletException {
              .........
            servlet = wrapper.allocate();
        }
         ..........
    ApplicationFilterChain filterChain =
            ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
           ...........
           filterChain.doFilter(request, response);
         .........
}

  而doInclude方法是没有关闭的,通过上面的源码我们可以看到forward方法就是从新去创建属于该Url的Servlet,再去执行其的调用链。

  3、prepareForRendering

protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response)
      throws Exception {
   String path = getUrl();
   if (this.preventDispatchLoop) {
      String uri = request.getRequestURI();
      if (path.startsWith("/") ? uri.equals(path) : uri.equals(StringUtils.applyRelativePath(uri, path))) {
         throw new ServletException("Circular view path [" + path + "]: would dispatch back " +
               "to the current handler URL [" + uri + "] again. Check your ViewResolver setup! " +
               "(Hint: This may be the result of an unspecified view, due to default view name generation.)");
      }
   }
   return path;
}

   这个方法我们可以看到其跳转后还是不是就是本身请求的地址,来进行死循环跳转判断。

   4、getRequestDispatcher

@Nullable
protected RequestDispatcher getRequestDispatcher(HttpServletRequest request, String path) {
   return request.getRequestDispatcher(path);
}

5、useInclude

protected boolean useInclude(HttpServletRequest request, HttpServletResponse response) {
   return (this.alwaysInclude || WebUtils.isIncludeRequest(request) || response.isCommitted());
}
private boolean alwaysInclude = false;
public static boolean isIncludeRequest(ServletRequest request) {
   return (request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE) != null);
}
public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";

   可以看到要使用Include方法要主动去设置alwaysInclude、或者response,不然默认使用的是forward。

  这里我们再复习下前面ViewResolver关于View创建的内容:

@Override
protected View createView(String viewName, Locale locale) throws Exception {
    ...........
   if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
      String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
      RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
     ..........
   }
   // Check for special "forward:" prefix.
   if (viewName.startsWith(FORWARD_URL_PREFIX)) {
      String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
      return new InternalResourceView(forwardUrl);
   }
   // Else fall back to superclass implementation: calling loadView.
   return super.createView(viewName, locale);
}

       然后站在tomcat的角度来看,DispatcherServlet本身就是一个Servlet,它分发它所管理的url。然后这个请求是去请求一个例如JSP,或者其他的Servlet、资源(JSP就是一个特殊的Servlet),其就通过forward方法再触发一次该JSP所代表的Servlet的执行链。这里简单来说,就是没有DispatcherServlet前,你请求一个JSP,主要在web.xml中将这个映射写上就能直接请求。而有了DispatcherServlet后,对于这个DispatcherServlet就是一个中介了(需要他来重新触发获取JSP的流程,当前你也可以在web.xml将这个映射写上,同样其也不会通过DispatcherServlet),把DispatcherServlet当一个普通的Servlet。这段话在前面分析DispatcherServlet的init方法的时候就是说明,结合那一篇应该更好明白。  

 下面我们来看InternalResourceView的子类JstlView

  6、JstlView

public class JstlView extends InternalResourceView {
   @Nullable
   private MessageSource messageSource;
     ..........   
   public JstlView(String url) {
      super(url);
   }

   public JstlView(String url, MessageSource messageSource) {
      this(url);
      this.messageSource = messageSource;
   }
   @Override
   protected void initServletContext(ServletContext servletContext) {
      if (this.messageSource != null) {
         this.messageSource = JstlUtils.getJstlAwareMessageSource(servletContext, this.messageSource);
      }
      super.initServletContext(servletContext);
   }

   @Override
   protected void exposeHelpers(HttpServletRequest request) throws Exception {
      if (this.messageSource != null) {
         JstlUtils.exposeLocalizationContext(request, this.messageSource);
      }
      else {
         JstlUtils.exposeLocalizationContext(new RequestContext(request, getServletContext()));
      }
   }
}
public static void exposeLocalizationContext(RequestContext requestContext) {
   Config.set(requestContext.getRequest(), Config.FMT_LOCALE, requestContext.getLocale());
   TimeZone timeZone = requestContext.getTimeZone();
   if (timeZone != null) {
      Config.set(requestContext.getRequest(), Config.FMT_TIME_ZONE, timeZone);
   }
   MessageSource messageSource = getJstlAwareMessageSource(
         requestContext.getServletContext(), requestContext.getMessageSource());
   LocalizationContext jstlContext = new SpringLocalizationContext(messageSource, requestContext.getRequest());
   Config.set(requestContext.getRequest(), Config.FMT_LOCALIZATION_CONTEXT, jstlContext);
}
  public static void set(ServletRequest request, String name, Object value) {
request.setAttribute(name + REQUEST_SCOPE_SUFFIX, value);
   }

     可以看到这里主要是两个方法就是将一些内容设置到request中,就不深入探讨了,主要逻辑是其父类InternalResourceView 。

 5、RedirectView

   RedirectView是通过前面的ResolverView可以知道在createView方法中当viewName前面加了"redirect:",就是创建的RedirectView。其也继承AbstractUrlBasedView

@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
      HttpServletResponse response) throws IOException {

   String targetUrl = createTargetUrl(model, request);
   targetUrl = updateTargetUrl(targetUrl, model, request, response);

   // Save flash attributes
   RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);

   // Redirect
   sendRedirect(request, response, targetUrl, this.http10Compatible);
}

       1、createTargetUrl

      这个是对url的获取,及处理,这里我们通过一个demo来更好的说明:

    @RequestMapping(value = "/spring2/{name}", method = RequestMethod.GET)
    public RedirectView springMethod2(@PathVariable("name") String name) {
        RedirectView modelAndView = new RedirectView("/springMapping/{age}",true);
        modelAndView.getAttributesMap().put("age",123);
        modelAndView.getAttributesMap().put("name",name);
        return modelAndView;
    }
    @RequestMapping(value = "springMapping/{age}",method = RequestMethod.GET)
    public String  springMapping(@PathVariable("age") Integer name)
    {
        return "springMethod";
    }

  springMethod.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8" isELIgnored="false"%>

<h1>Hello World</h1>

  xml配置视图解析器

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="jspViewResolver">
    <property value="org.springframework.web.servlet.view.JstlView" name="viewClass"/>
    <property value="/WEB-INF/" name="prefix"/>
    <property value=".jsp" name="suffix"/>
</bean>  
protected final String createTargetUrl(Map<String, Object> model, HttpServletRequest request)
      throws UnsupportedEncodingException {
   StringBuilder targetUrl = new StringBuilder();
   if (this.contextRelative && getUrl().startsWith("/")) {
      targetUrl.append(request.getContextPath());
   }
   targetUrl.append(getUrl());
   String enc = this.encodingScheme;
   if (enc == null) {
      enc = request.getCharacterEncoding();
   }
   if (enc == null) {
      enc = WebUtils.DEFAULT_CHARACTER_ENCODING;
   }
   if (this.expandUriTemplateVariables && StringUtils.hasText(targetUrl)) {
      Map<String, String> variables = getCurrentRequestUriVariables(request);
      targetUrl = replaceUriTemplateVariables(targetUrl.toString(), model, variables, enc);
   }
   if (isPropagateQueryProperties()) {
      appendCurrentQueryParams(targetUrl, request);
   }
   if (this.exposeModelAttributes) {
      appendQueryProperties(targetUrl, model, enc);
   }
   return targetUrl.toString();
}

   这个方法主要是5部分:

    1、看是不是相对路径(主要看是不是以"/"开头,contextRelative 我们通过构造方法设置为了true)。

    2、设置CharacterEncoding,如果没有就默认(ISO-8859-1)。

    3、expandUriTemplateVariables 这里主要是处理类似@PathVariable这样的参数

           

                 

    4、appendCurrentQueryParams 这个主要是将第一个url ?、#后面的参数拼接到当前重定向的url中,(默认是false)

    5、这给就是将Model中还没有处理完的参数以?的显示拼接到url中来。例如我们前面添加了两个key、value,但目前只消耗了age,所以会将name拼接。

                   

   2、sendRedirect

protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
      String targetUrl, boolean http10Compatible) throws IOException {

   String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl));
   if (http10Compatible) {
      HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
      if (this.statusCode != null) {
         response.setStatus(this.statusCode.value());
         response.setHeader("Location", encodedURL);
      }
      else if (attributeStatusCode != null) {
         response.setStatus(attributeStatusCode.value());
         response.setHeader("Location", encodedURL);
      }
      else {
         // Send status code 302 by default.
         response.sendRedirect(encodedURL);
      }
   }
   else {
      HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
      response.setStatus(statusCode.value());
      response.setHeader("Location", encodedURL);
   }
}
private boolean http10Compatible = true;

      这里是三个分支,1、当前View有设置statusCode ,就设置response的status后,不进行跳转了,也就是没有下面的else分支(response.sendRedirect(encodedURL))。2、如果有在request中设置View.RESPONSE_STATUS_ATTRIBUTE,也同样只设置status、header。3、这个才是正常的,即设置Redirect的url。

   这里的1-statusCode ,springMVC一般不会主动设置。同样2 一般也不会主动设置,但对于2来说,我们前面有提到,这里再复习一下。

   其的是在是在ServletInvocableHandlerMethod的setResponseStatus方法中。这里我们以前有提到,直接再方法上面加@ResponseStatus注解就能设置该Url的返回状态码:

public @interface ResponseStatus {

   @AliasFor("code")
   HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;

   @AliasFor("value")
   HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
   String reason() default "";
}
private void setResponseStatus(ServletWebRequest webRequest) throws IOException {
   HttpStatus status = getResponseStatus();
   if (status == null) {
      return;
   }

    ..........
   // To be picked up by RedirectView
   webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, status);
}

   假如没有设置,这里的status就为空,直接返回,不为空,就会将其设置到request中。然后在的sendRedirect方法,就不会跳转了。

   这里我们再用一个demo来简答说明一下,这个例子,我们请求就会跳转到百度。

 @RequestMapping(value = "/spring/{name}", method = RequestMethod.GET)
 public ModelAndView springMethod(@PathVariable("name") String name) {
     ModelAndView modelAndView = new ModelAndView();
     modelAndView.setViewName("redirect:http://www.baidu.com");
     return modelAndView;
  }

   这个例子,由于viewName前面有redirect:所以其在UrlBasedViewResolver创建是创建的就是RedirectView。

                            

                 

  假如我们在springMethod方法上面再加一个注解@ResponseStatus

    @RequestMapping(value = "/spring/{name}", method = RequestMethod.GET)
    @ResponseStatus(code = HttpStatus.NOT_MODIFIED)
    public ModelAndView springMethod(@PathVariable("name") String name) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("redirect:http://www.baidu.com");
        return modelAndView;
    }
    NOT_MODIFIED(304, "Not Modified"),

        

   这个时候,在ServletInvocableHandlerMethod执行这个responseStatus就有值了,就会走下面的设置逻辑,这个时候在RedirectView这边,也能从request中获取到

       

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值