我是如何一步步解决问题 让Spring MVC返回HTML类型的视图

3,088 阅读11分钟

前言

这两天在折腾SSM,在捣鼓Spring MVC的时候,我想让Spring MVC的前端控制器(DispatcherServlet)给用户返回的是HTML类型的视图而不是JSP类型的视图,于是我按照常规的思路,把Spring MVC配置文件里面的视图解析器配置修改成HTML后缀的,然后就遇上了各种问题了......当然这些问题也都是我对Spring MVC不够了解才导致的,接下来详细说一下我遇到的问题以及解决过程。

遇上问题

为了将返回给用户的视图从JSP改成HTML嘛,我就寻思着不就是把Spring MVC配置文件的视图配置改一下,把.jsp改成.html嘛。

原来返回JSP的配置 Spring-MVC.xml:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value=""></property>
    <property name="suffix" value=".jsp"></property>
</bean>

因为我的JSP文件就是放在web根目录下,所以这里prefix就留空了。

修改成返回HTML的配置 Spring-MVC.xml:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value=""></property>
    <property name="suffix" value=".html"></property>
</bean>

然后写Controller将视图返回给前端控制器DispatchServlet看看能不能将HTML类型的视图返回给用户

package com.nChat.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class UserController {

    @RequestMapping(value = "/login")
    public String login(){
        return "/register";  //返回web根目录下的register.html
    }
}

emmmm配置文件修改好了,按照我的想法,运行肯定没“问题”吧,可现实总是打我脸...可能是我太年轻了吧,运行之后报错,页面显示404

24-Dec-2018 21:57:35.769 警告 [http-nio-8080-exec-3] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /
24-Dec-2018 21:57:35.778 警告 [http-nio-8080-exec-2] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /
24-Dec-2018 21:57:35.854 警告 [http-nio-8080-exec-1] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /
24-Dec-2018 21:57:38.542 警告 [http-nio-8080-exec-4] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /register.html

错误的意思大概是前端控制器DispatchServlet找不到请求相对应的mapping,所以抛出noHandlerFound的异常

问题分析

我们来分析看看为啥出现这个问题,首先贴出我们前端控制器DispatchServlet的工作流程先

流程用文字说明大概如下:

  1. 用户发送请求,被 SpringMVC 的前端控制器DispatcherServlet 拦截。
  2. DispatcherServlet 收到请求后自己不进行处理,而是将请求转发给处理器映射器HandlerMapping
  3. 处理器映射器根据请求的URL确定映射关系找出相应的处理器适配器,并且返回HandlerExecutionChain对象给前端控制器。 处理器映射器找到具体的处理器适配器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet。
  4. DispatcherServlet根据3返回的HandlerExecutionChain 调用相应的处理器适配器HandlerAdapter
  5. 经过处理器适配器HandlerAdapter调用具体的处理器(Controller,也叫后端控制器)。
  6. Controller将结果封装到ModelAndView返回给HandlerAdapter。
  7. HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
  8. DispatcherServlet将ModelAndView传给视图解析器ViewReslover,查询到相应的视图View。
  9. ViewReslover解析后返回具体的View。
  10. DispatcherServlet把Model交给View进行渲染(即将模型数据填充至视图中)。
  11. DispatcherServlet响应用户。

看完整个流程是不是知道问题出在哪了??org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /这个报错是出在了流程的第3步中,也就是DispatchServlet将请求转发给HandlerMapping后,HandlerMapping根据用户的请求找不到相应处理器映射器,所以就报了这个错误。那造成这个问题的原因会不会是我们没定义相应的Controller,导致也没有相应的处理器适配器,但是我们的Controller确实已经写好了,而且用返回JSP类型视图的代码测试数是正常的,没任何问题。

那我们一步步排错,我们在Controller中打印输出个字符,判断看看请求有没有进Controller

@RequestMapping(value = "/login")
public String login(){
    System.out.println("coming");
    return "/register";  //返回web根目录下的register.html
}

简单粗暴,添加个System.out.println("coming");如果请求进来了我们就可以看到打印coming的内容,我们再重新运行项目测试看看

coming
24-Dec-2018 23:07:38.974 警告 [http-nio-8080-exec-4] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /register.html

我们可以看到确实进来了,既然进来了,也就是说用户的请求至少已经执行到第5步了,那和前面说错误出现在第3步不是矛盾了吗??莫急,继续往下分析,既然肯定用户的请求前5步都没问题的,那打印完coming后为什么又出现了本该出现在第2的错误呢?org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /register.html,莫不是从第5步又跳到第2步了?

为了继续排查下去,我们继续添加个Controller对应register.html,看看他还报错不

@Controller
public class UserController {

    @RequestMapping(value = "/login")
    public String login(){
        System.out.println("coming");
        return "/register";  //返回web根目录下的register.html
    }

    @RequestMapping(value = "/register.html")
    public String  aa(){
        System.out.println("coming aa");
        return "/aa";
    }
}

重新启动项目运行看看,输出如下

coming
coming aa
24-Dec-2018 23:29:00.759 警告 [http-nio-8080-exec-4] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /aa.html

输出表明既进到了/login又进到了/register.html,然而还是继续报错org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /aa.html,一样的错误但是造成的错误原因不一样,上面的错误是因为找不到/register.html相应的mapping,然后我们把/register.html的Controller加上并且返回/aa.html,所以导致了这次错误找不到/aa.html相应的mapping。 看到这里是不是有点头绪了?我们在Controller返回ModelAndViewHandlerAdapterHandlerAdapter再把ModelAndView返回给DispatchServlet,然后DispatchServlet再把ModelAndView传给视图解析器ViewReslover解析,也就是图中对应的第6到第8步,到这里之前都是没问题的,问题就出现在了第9步身上了,ViewReslover返回的视图名给DispatchServlet重点来了!!!! DispatchServlet又把这个视图名当做一个新的请求,去交给HandlerMapping处理!!也就是图中的第2步,然后无限死循环下去......

遇上新问题

那问题又来了,为什么DispatchServlet会把它当成一个新的请求去处理呢?是不是我们Servlet配置的匹配规则写的不对,把返回的视图也拦截上了?我们来看看我们的Servlet配置 项目的web.xml

    <servlet>
        <servlet-name>Spring-MVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:*.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>Spring-MVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

匹配规则我写的是/,网上的很多文章也都说匹配规则/是不会拦截.jsp、html等格式的URL的,只会拦截/login这样的,而/*的匹配规则才会拦截所有请求包括/login、.jsp、html等,实践证明网上的这些观点都是错误的! 在这里我要更正一下,匹配规则//*都是会拦截所有请求(包括/login、.jsp、.html、.css等)也就是说他们俩的作用是一样的,既然说.jsp、.html都会拦截那为什么配置/规则的时候.jsp的能正常而.html的却被再次拦截导致死循环和配置/*不管是.jsp还是.html都再次被拦截从而进入死循环?

拨开云雾见月明

既然配置//*的作用一样,为啥结果不一样呢?玄机就在Tomcat上,在Tomcatconf/目录下,有个web.xml的文件。

这个web.xml在Tomcat启动的时候就被加载进来,对所有webapp都有效,至于Tomcat下的web.xml和我们自己项目下的web.xml的区别和联系请自行到参考文章查看。这里我们详细分析一下Tomcat下的web.xml里面有啥,打开conf/web.xml看到他里面定义了个拦截.jsp.jspx的Servlet
Tomcat的conf/web.xml

<!-- The mappings for the JSP servlet -->
<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

这个拦截规则也就是把所有的.jsp.jspx URL请求都拦截在servlet-name为jsp的servlet中,我们搜索<servlet-name>jsp</servlet-name>查找一下servlet使用的类

Tomcat的conf/web.xml

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
        <param-name>fork</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>xpoweredBy</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>

可以看到这个拦截.jsp.jspx的servlet使用的类是org.apache.jasper.servlet.JspServlet,也就是使用的类和我们项目下web.xml的servlet使用的org.springframework.web.servlet.DispatcherServlet类不一样,使用的类不一样也导致.jsp.jspx的URL请求都不会走上图中DispatchServlet的流程,而是走它使用的类的具体流程,想了解的可以查询这个类的相关资料。到这里我们可以明确一点的就是我们项目下web.xml的servlet规则定义成/或者/*的时候.jsp正常来说应该是会被我们的项目下web.xml的servlet拦截的,但是根据servlet-mapping的匹配规则,.jsp.jspx的URL都会先被Tomcat下web.xml里面的servlet拦截,而导致.jsp.jspx不会被我们项目的web.xml里面的servlet拦截。**这也就是为什么匹配规则写成/的时候.jsp会被忽略不进行拦截的原因,那问题又来了匹配规则写成/*的时候.jsp没有被忽略仍然进行拦截的呢?原因是规则/*会覆盖所有默认的servlet,从而将所有请求都拦截了下来,接下来我们可以修改Tomcat的web.xml下的servlet配置,让项目的web.xml下的servlet配置规则为/时候也支持返回html类型的视图 Tomcat的conf/web.xml

<!-- The mappings for the JSP servlet -->
<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
    <url-pattern>*.html</url-pattern>
</servlet-mapping>

添加个规则<url-pattern>*.html</url-pattern>,即URL是.html类型的话就走Tomcat的<servlet-name>jsp</servlet-name>这个servlet而不走我们项目的servlet,然后重新运行项目测试一下

(请忽略乱码问题o(╥﹏╥)o) 看到了吧,确实可以通过Controller返回html类型的视图了吧,也就是在执行完第9步后返回的视图不再是被org.springframework.web.servlet.DispatcherServlet拦截,而是被org.apache.jasper.servlet.JspServlet拦截,从而跳出了DispatchServlet的魔抓不会再死循环了。 那以后想要返回.html类型的视图是不是都要去修改Tomcat的web.xml?也不用这样,我们只要在我们的项目web.xml中配置一个相同名的servlet即可,它会自动覆盖Tomcat的web.xml的,如

<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>.html</url-pattern>
</servlet-mapping>

这样配,但是会报错Cannot resolve Servlet 'jsp',我也不懂为啥,知道的朋友可以补充下。Tomcat下的web.xml除了有名为jsp的servlet,还有一个大家应该都很熟悉的,就是名为default的servlet,它的作用和jsp的大概一样,大家知道它是拿来配置静态资源的,却很少了解它怎么来的 Tomcat的conf/web.xml

<servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param>
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param>
    <init-param>
        <param-name>listings</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
    
<!-- The mapping for the default servlet -->
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

我们也可以用它来代替jsp的servlet来达到不拦截.html类型的视图,同样我们在项目的web.xml下配置名为default的servlet

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.html</url-pattern>
</servlet-mapping>

这样即可让.html的请求都不会被前端控制器DispatchServlet拦截到。

总结

当然要返回.html类型的视图也不是只有这种方法,也可以通过更换视图解析器,但是我觉得这样没必要,因为InternalResourceViewResolver视图解析器本身就是支持.html的,只是我们没处理正确而已。通过这次出现的问题,引发我了对Spring MVC的进一步了解,也把分析、解决问题的过程记录下,希望能让自己印象更深刻点,也希望能帮助到大家。

参考文章

servlet-mapping url-pattern / 和 /*区别

Tomcat文件夹下的context.xml和web.xml

web.xml中出现default是什么意思?

转自:ddnd.cn/2018/12/24/…