SpringMVC中的HTTP跳转
项目开发中经常会碰到需要进行HTTP跳转的场景,比如用户请求一个需要登录之后才可以看到的页面,而此时需要跳转到登录页面,待登录成功之后在跳转会现在的页面。那么SpringMVC是怎样实现这样的跳转的呢?今天就让我们仔细的研究一下。
一,Servlet中forward与redirect
在原生的Servlet技术中有两种跳转方式,分别为forward和redirect。它们有着各自的特点和用途
1,forward重定向是在容器内部实现的同一个Web应用程序的重定向,所以forward方法只能重定向到同一个Web应用程序中的一个资源,redirect方法可以重定向到任何URL。
2,forward重定向后浏览器地址栏URL不变,redirect重定向后浏览器地址栏URL改变。
3,forward可以将请求中包含的数据传递到跳转后的地址。redirect则不能,如果想传递参数,只能放在请求行中。
3,forward可以将请求中包含的数据传递到跳转后的地址。redirect则不能,如果想传递参数,只能放在请求行中。
4,对于客户端来说,forward重定向的过程不可见。而redirect则需要客户端配合,再次请求跳转后的地址。
二,301与302跳转
二,301与302跳转
做服务端的不能不知道301和302跳转,这里的301、302指的是HTTP的响应的状态码。301的意思是原地址永久性的替换为新地址,302指当前的地址只是暂时的替换为新地址。它们实现的结果都是一样的,浏览器根据返回的新地址再次发起请求。但是,对于搜索引擎来说则意义大不相同,懂得SEO的人都会慎用301与302跳转。最后,这里说的跳转所指的都是上文说的redirect。
三,spring中redirect的实现
在spring中,当我们想进行redirect跳转的时候可以让Controller的方法返回“redirect:”开头的字符串,或者直接返回RedirectView。
若Controller方法返回的是“redirect:”开头的字符串(只要返回类型是String,都是同一个结果处理器),则对应的结果处理器是ViewNameMethodReturnValueHandler,来看看它是如何发现并处理这个跳转结果的:
public void handleReturnValue(
Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws Exception {
if (returnValue == null) {
return;
}
else if (returnValue instanceof String) {
String viewName = (String) returnValue;
mavContainer.setViewName(viewName);
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
else {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
上面方法中的isRedirectViewName就是判断返回的字符串是否以"redirect:"开头,如果是则设置一个跳转的标志到ModelAndViewContainer 中。若Controller方法返回的直接是
RedirectView,则对应的结果处理器就是ViewMethodReturnValueHandler,它和ViewNameMethodReturnValueHandler的处理过程差不多,代码如下:
public void handleReturnValue(
Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws Exception {
if (returnValue == null) {
return;
}
else if (returnValue instanceof View){
View view = (View) returnValue;
mavContainer.setView(view);
if (view instanceof SmartView) {
if (((SmartView) view).isRedirectView()) {
mavContainer.setRedirectModelScenario(true);
}
}
}
else {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
throws IOException {
String targetUrl = createTargetUrl(model, request);
targetUrl = updateTargetUrl(targetUrl, model, request, response);
FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
if (!CollectionUtils.isEmpty(flashMap)) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(targetUrl).build();
flashMap.setTargetRequestPath(uriComponents.getPath());
flashMap.addTargetRequestParams(uriComponents.getQueryParams());
}
FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
flashMapManager.saveOutputFlashMap(flashMap, request, response);
sendRedirect(request, response, targetUrl.toString(), this.http10Compatible);
}
renderMergedOutputModel最后悔调用sendRedirect来设置response。
protected void sendRedirect(
HttpServletRequest request, HttpServletResponse response, String targetUrl, boolean http10Compatible)
throws IOException {
String encodedRedirectURL = response.encodeRedirectURL(targetUrl);
if (http10Compatible) {
if (this.statusCode != null) {
response.setStatus(this.statusCode.value());
response.setHeader("Location", encodedRedirectURL);
}
else {
// Send status code 302 by default.
response.sendRedirect(encodedRedirectURL);
}
}
else {
HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
response.setStatus(statusCode.value());
response.setHeader("Location", encodedRedirectURL);
}
}
HTTP跳转响应非常简单,只需要设置跳转的状态码和需要跳转的地址。由上面代码可知,默认情况下spring发送的是302跳转,如果想发送301跳转可以在返回的
RedirectView中设置HTTP的状态码。通过ResponseStatus注解指定返回的状态码是没有用的。
四,spring对forward调整的处理
在spring中,可以通过返回以“forward:”开头的字符串的方式实现forward调整。在获取view之前,它与返回redirect字符串的处理过程都是一样的。而它们对应的View类型不同,上面说到处理redirect的view是RedirectView,而处理forward的view是InternalResourceView。导致它们不同原因是UrlBasedViewResolver创建View时做了区分,如下:
protected View createView(String viewName, Locale locale) throws Exception {
// If this resolver is not supposed to handle the given view,
// return null to pass on to the next resolver in the chain.
if (!canHandle(viewName, locale)) {
return null;
}
// Check for special "redirect:" prefix.
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
return applyLifecycleMethods(viewName, view);
}
// 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);
}
从InternalResourceView的名字可以直观的看出,forward是要获取内部资源。来看看它的renderMergedOutputModel方法:
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine which request handle to expose to the RequestDispatcher.
HttpServletRequest requestToExpose = getRequestToExpose(request);
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, requestToExpose);
// Expose helpers as request attributes, if any.
exposeHelpers(requestToExpose);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(requestToExpose, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(requestToExpose, 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(requestToExpose, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(requestToExpose, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
exposeForwardRequestAttributes(requestToExpose);
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.forward(requestToExpose, response);
}
}
上面的方法先获取一个RequestDispatcher,然后通过它将请求转发到了新的路径上。这些都是利用Servlet的标准,具体内部的实现可以在tomcat的源码中查找。
注:本文所述都是基于spring3.1.2的默认环境