实现我们直接来看下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中获取到