《SpringMVC系列》第五章:视图解析

重点

  1. 当目标方法返回是页面重定向时,如何处理
  2. 当目标方法返回是请求转发是,代码如何处理

四、处理页面重定向redirect

上一章我们介绍了经过当响应体经过@ResponseBody修饰,返回结果为JSON时是如何处理的,

那么当我们的请求返回的是一个重定向,那么这个是如何处理的

@Controller
public class WebController {

    @GetMapping(value = "/index1")
    public String index1(User user, Model model) {
        model.addAttribute("user", user);
        return "redirect:/index1";
    }

    @GetMapping(value = "/index2")
    public String index2() {
        return "index2";
    }

}

调用目标方法处理业务逻辑的之前已经介绍了,那么从调用返回值处理器开始介绍

之前测试的都是浏览器直接返回例如JSON / xml的数据,存在@ResponseBody注解的直接通过RequestResponseBodyMethodProcessor来处理,那么现在返回的是视图,这个时候就给借助ViewNameMethodReturnValueHandler来处理了

	@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

		HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
		if (handler == null) {
			throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
		}
		handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
	}

ViewNameMethodReturnValueHandler

视图名方法返回值处理器:

  1. 判断是否可以应用此处理器的条件是:返回值类型为字符串
  2. 返回值处理方法,会判断是否为重定向
public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

	@Nullable
	private String[] redirectPatterns;

	public void setRedirectPatterns(@Nullable String... redirectPatterns) {
		this.redirectPatterns = redirectPatterns;
	}

	@Nullable
	public String[] getRedirectPatterns() {
		return this.redirectPatterns;
	}

	// 判断是否可以应用此返回值处理器的条件是: 返回值类型是字符串
	@Override
	public boolean supportsReturnType(MethodParameter returnType) {
		Class<?> paramType = returnType.getParameterType();
		return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
	}

	@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
		
		if (returnValue instanceof CharSequence) {
             // 获取视图名
			String viewName = returnValue.toString();
			mavContainer.setViewName(viewName);
             // 判断是否为重定向视图
			if (isRedirectViewName(viewName)) {
				mavContainer.setRedirectModelScenario(true);
			}
		}
		else if (returnValue != null) {
			// should not happen
			throw new UnsupportedOperationException("Unexpected return type: " +
					returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
		}
	}
	// 判断是否为重定向视图的条件为: 
    // 1.已经配置的重定向视图是否包含这个
    // 2.返回字符串是否已 `redirect:` 开头
	protected boolean isRedirectViewName(String viewName) {
		return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"));
	}

}

processDispatchResult()

上面方法返回值处理器执行完毕以后,然后又回到doDispatch(),在该方法的末尾,会通过processDispatchResult()处理结果

	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {

		boolean errorView = false;

		if (exception != null) {
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}

		// Did the handler return a view to render?
		if (mv != null && !mv.wasCleared()) {
             // 处理结果 继续调用
			render(mv, request, response);
			if (errorView) {
				WebUtils.clearErrorRequestAttributes(request);
			}
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace("No view rendering, null ModelAndView returned.");
			}
		}

		if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
			// Concurrent handling started during a forward
			return;
		}

		if (mappedHandler != null) {
			// Exception (if any) is already handled..
			mappedHandler.triggerAfterCompletion(request, response, null);
		}
	}

render()

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		// Determine locale for request and apply it to the response.
		Locale locale =
				(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
		response.setLocale(locale);

		View view;
         // 获取视图名
		String viewName = mv.getViewName();
		if (viewName != null) {
			// We need to resolve the view name.
             // 解析视图名
			view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
			if (view == null) {
				throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
						"' in servlet with name '" + getServletName() + "'");
			}
		}
		else {
			// No need to lookup: the ModelAndView object contains the actual View object.
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
						"View object in servlet with name '" + getServletName() + "'");
			}
		}

		// Delegate to the View object for rendering.
		if (logger.isTraceEnabled()) {
			logger.trace("Rendering view [" + view + "] ");
		}
		try {
			if (mv.getStatus() != null) {
				request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
				response.setStatus(mv.getStatus().value());
			}
			view.render(mv.getModelInternal(), request, response);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Error rendering view [" + view + "]", ex);
			}
			throw ex;
		}
	}

resolveViewName()

	@Nullable
	protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
			Locale locale, HttpServletRequest request) throws Exception {
		
		if (this.viewResolvers != null) {
             // 遍历视图解析器
             // 默认加载的视图解析器有5个,在第1个里面包含两外4个,并且在第1个视图解析器里面也会触发方法
             // 使用后面4个视图解析器  所以这里遍历也就执行1遍
			for (ViewResolver viewResolver : this.viewResolvers) {
				View view = viewResolver.resolveViewName(viewName, locale);
				if (view != null) { // 如果能处理即返回
					return view;
				}
			}
		}
		return null;
	}

视图解析器

public interface ViewResolver {

	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FanJzfYD-1652196565232)(E:\Gitee\spingboot\ViewResolver.png)]

ContentNegotiatingViewResolver

内容协商视图解析器

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
		implements ViewResolver, Ordered, InitializingBean {
    
    @Override
	@Nullable
	public View resolveViewName(String viewName, Locale locale) throws Exception {
		RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
		Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
         // 获取到请求能够接收的媒体类型
		List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
		if (requestedMediaTypes != null) {
             // 获取候选的视图
			List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
             // 根据请求的媒体类型找到最合适的视图
			View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
			if (bestView != null) {
				return bestView;
			}
		}

		String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
				" given " + requestedMediaTypes.toString() : "";

		if (this.useNotAcceptableStatusCode) {
			if (logger.isDebugEnabled()) {
				logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
			}
			return NOT_ACCEPTABLE_VIEW;
		}
		else {
			logger.debug("View remains unresolved" + mediaTypeInfo);
			return null;
		}
	}
    
 	// 省略部分代码... 
} 

getCandidateViews()

	private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
			throws Exception {

		List<View> candidateViews = new ArrayList<>();
		if (this.viewResolvers != null) {
			Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
             // 遍历视图解析器,上面有张图显示,在第1个视图解析器里面包含另外的4个视图解析器
             // 所以上面
			for (ViewResolver viewResolver : this.viewResolvers) {
                 // 视图解析器处理视图
				View view = viewResolver.resolveViewName(viewName, locale);
				if (view != null) {
                      // 存在则存入候选的视图
					candidateViews.add(view);
				}
                 // 内容协商
				for (MediaType requestedMediaType : requestedMediaTypes) {
					List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
					for (String extension : extensions) {
						String viewNameWithExtension = viewName + '.' + extension;
						view = viewResolver.resolveViewName(viewNameWithExtension, locale);
						if (view != null) {
							candidateViews.add(view);
						}
					}
				}
			}
		}
         // 判断是否添加默认视图
		if (!CollectionUtils.isEmpty(this.defaultViews)) {
			candidateViews.addAll(this.defaultViews);
		}
		return candidateViews;
	}

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;
}

view.render()

上面也有1个render()负责处理

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

		if (logger.isDebugEnabled()) {
			logger.debug("View " + formatViewName() +
					", model " + (model != null ? model : Collections.emptyMap()) +
					(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
		}

		Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
         // 准备响应
		prepareResponse(request, response);
         // 处理合并输出Model
		renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
	}

renderMergedOutputModel()

	@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);
	}

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);
		}
	}

五、处理请求转发forward

请求转发代码处理整体思路大体一直,只不过在最后处理肯定会有差别,下面就展示了renderMergedOutputModel()

看到方法中还是使用了我们Servler最熟悉的request.getRequestDispatcher(“success.jsp”).forward(request,response)

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

		// Expose the model object as request attributes.
		exposeModelAsRequestAttributes(model, request);

		// Expose helpers as request attributes, if any.
		exposeHelpers(request);

		// Determine the path for the request dispatcher.
		String dispatcherPath = prepareForRendering(request, response);

		// Obtain a RequestDispatcher for the target resource (typically a JSP).
         // 获取到转发器
		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 already included or response already committed, perform include, else forward.
		if (useInclude(request, response)) {
			response.setContentType(getContentType());
			if (logger.isDebugEnabled()) {
				logger.debug("Including [" + getUrl() + "]");
			}
			rd.include(request, response);
		}

		else {
			// Note: The forwarded resource is supposed to determine the content type itself.
			if (logger.isDebugEnabled()) {
				logger.debug("Forwarding to [" + getUrl() + "]");
			}
             // 这里直接进行请求转发
			rd.forward(request, response);
		}
	}

六、注意

doDispatch()中通过适配器获取到返回视图的时候,通过IDEA打断点可以看到

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

如果返回是JSON,那么mv=null,因为返回结果已经写入到了response中

如果是页面重定向,那么mv会保存重定向的视图

如果是请求转发,那么会保留参数,可以在mv中很清晰的看到目标方法的参数被携带过去

		if (logger.isDebugEnabled()) {
			logger.debug("Forwarding to [" + getUrl() + "]");
		}
         // 这里直接进行请求转发
		rd.forward(request, response);
	}
}

# 六、注意

在`doDispatch()`中通过适配器获取到返回视图的时候,通过IDEA打断点可以看到

```java
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

如果返回是JSON,那么mv=null,因为返回结果已经写入到了response中

如果是页面重定向,那么mv会保存重定向的视图

如果是请求转发,那么会保留参数,可以在mv中很清晰的看到目标方法的参数被携带过去

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为人师表好少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值