Spring中控制器方法返回字符串的一些细节

本文深入探讨了SpringMVC框架中重定向与转发的实现原理,包括如何通过配置和代码实现重定向和转发,以及在不同场景下选择使用重定向还是转发的考量因素。重点介绍了RedirectView的内部机制,特别是如何通过配置RedirectView来影响重定向行为,以及在实际应用中避免常见陷阱的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring MVC 中,我们在返回逻辑视图时,框架会通过 viewResolver 来解析得到具体的 View,然后向浏览器渲染。假设逻辑视图名为 hello,通过配置,我们配置某个 ViewResolver 如下:
Xml代码 收藏代码

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">  
        <description>  
            假如逻辑试图名为 "hello",因此 viewResolver 将解析成 /WEB-INF/jsp/hello.jsp  
        </description>  
        <property name="order" value="10" />  
        <property name="prefix" value="/WEB-INF/jsp/" />  
        <property name="suffix" value=".jsp" />  
    </bean>
    实际上,框架还是通过 forward 的方式转发到了 /WEB-INF/jsp/hello.jsp。如果逻辑视图名是 /hello,实际还是转发到了 /WEB-INF/jsp/hello.jsp,即 /WEB-INF/jsp//hello.jsp 等同于 /WEB-INF/jsp/hello.jsp。

    现在有个问题,如果 /hello 就是某个 controller 的映射,我想转发到这个 controller,怎么办?我们可以通过 forward 前缀来达到转发到其它资源的目的:
    public String handle() {  
        // return "forward:/hello" => 转发到能够匹配 /hello 的 controller 上  
        // return "hello" => 实际上还是转发,只不过是框架会找到该逻辑视图名对应的 View 并渲染  
        // return "/hello" =>return "hello"  
        return "forward:/hello";  
    }  
    同理,如果我们想重定向到某个资源,我们可以通过 redirect 前缀来达到重定向到其它资源的目的:
    public String handle() {  
        // 重定向到 /hello 资源  
        return "redirect:/hello";  
    }
    还记得 java web 中的转发和重定向 这篇文章吗?我强调过,如果想做转发操作,不需要写 contextPath;如果想做重定向操作,推荐写包括 contextPath 在内的 url。因此,在使用 Spring MVC 的 redirect 前缀时,里面是有坑的!

    仍然假设应用程序的 contextPath 为 /ctx。我们来看看 RedirectView.renderMergedOutputModel 的片段:
    protected void renderMergedOutputModel(  
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)  
        throws IOException {  

      // Prepare target URL.  
      StringBuilder targetUrl = new StringBuilder();  
      if (this.contextRelative && getUrl().startsWith("/")) {  
        // Do not apply context path to relative URLs.  
        targetUrl.append(request.getContextPath());  
      }  
      targetUrl.append(getUrl());  

      // ...  

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

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

      if (http10Compatible) {  
        // Always send status code 302.  
        response.sendRedirect(response.encodeRedirectURL(targetUrl));  
      }  
      else {  
        HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);  
        response.setStatus(statusCode.value());  
        response.setHeader("Location", response.encodeRedirectURL(targetUrl));  
      }  
    }  
    sendRedirect 方法没什么特别的,它就是调用 HttpServletResponse 的 sendRedirect 方法而已。因此,关键点就是 renderMergedOutputModel 方法对转发的资源的 url 进行处理了。最终的 url 与 contextRelative 和你要重定向的资源是否以 / 开头有关!当且仅当 renderMergedOutputModel 为 true,并且你要重定向的资源是以 / 开头,spring 会在该资源前添加 contextPath。

    response.sendRedirect() 的参数,如果不以 / 开头,那么容器最终计算出来的资源是相对于做重定向操作的资源的 url;如果以 / 开头,容器将它视为相对于主机的 url。如此说来,spring 的 RedirectView 怎么着都只能将资源重定向到当前应用程序上。将 url 开头的 / 去掉不是解决之道,因此本机的其它应用程序的 contextPath 必定是以 / 开头,因此我们要想办法设置 contextRelative 了。

    RedirectView 自身持有 contextRelative 属性,用于在程序中通过 new 操作符来构造一个 RedirectView 并可以设置 contextRelative。当处理请求的方法返回类型为 String 时,是通过 viewResolver 来解析得到 View 的。UrlBasedViewResolver 就是能够解析出 RedirectView 的 viewResolver。该 viewResolver 持有 redirectContextRelative 属性,当它发现逻辑视图名以 "redirect:" 开头时,会将自身持有的 redirectContextRelative 传入 RedirectView 的构造函数以创建 RedirectView。因此我们通过注册 UrlBasedViewResolver 时设置 redirectContextRelative 以达到控制 RedirectView 修改 url 的行为。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());  
        return 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);  
    }  
    UrlBasedViewResolver 的 redirectContextRelative 的默认值为 true,这意味着,只要重定向的资源以 / 开头,那么 spring 会帮你添加 contextPath。站在 Spring MVC 的角度上来说,/ 开头的资源就是相对于当前应用程序,这和 forward 一样了。因此,如果你确定重定向操作是在同一应用程序中操作,那就使用 Spring MVC 的默认值吧,这样就不需要你写 contextPath 了。注意,这样做有隐患!当重定向的资源是其它应用程序时,除非你了解机制,否则请不要这么做!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值