源码剖析Spring MVC如何将请求映射到Controller?

Posted 秃秃爱健身

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码剖析Spring MVC如何将请求映射到Controller?相关的知识,希望对你有一定的参考价值。

文章目录

一、前言

最近有朋友问我:Spring MVC 中如何将请求映射到指定的Controller中的;

结合博主之前的 Spring MVC的请求执行流程 一文,这里做一个更细粒度的分析。

从Spring MVC的请求执行流程来看,DispatcherServlet#doDispatch()方法中会做请求的映射;具体体现在获取请求对应的HandlerExecutionChain逻辑中。

二、核心链路分析

1、确定请求映射的入口

DispatcherServlet#getHandler()

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception 
    // 在Spring初始化的时候会加载所有的handlerMappings
    if (this.handlerMappings != null) 
        for (HandlerMapping mapping : this.handlerMappings) 
            // 获取请求对应的HandlerExecutionChain
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) 
                return handler;
            
        
    
    return null;

1)HandlerMapping注入Spring容器

Spring启动时会加载所有HandlerMapping类型的Bean到IOC容器中,默认有5个,分别为:

  1. RequestMappingHandlerMapping,请求处理器
  2. BeanNameHandlerMapping
  3. RouterFunctionMapping
  4. ResourceHandlerMapping
  5. WelcomePageHandlerMapping

针对HTTP GET、POST、PUT等普通请求,RequestMappingHandlerMapping负责处理,并且其中包含了 所有可以处理的请求路径、以及 请求和相应Controller(具体的类、方法)的映射Mapping。

2)HandlerMethod注册到MappingRegistry

Spring启动时会加载RequestMappingHandlerMapping到IOC容器中,RequestMappingHandlerMapping中使用MappingRegistry 保存了所有的请求映射关系,使用HandlerMethod保存了一个请求映射

RequestMappingInfoHandlerMapping 继承了 抽象类 AbstractHandlerMethodMapping, AbstractHandlerMethodMapping又实现了InitializingBean接口、重写了InitializingBean#afterPropertiesSet()方法;

因此实例化RequestMappingInfoHandlerMapping时会进入到AbstractHandlerMethodMapping#afterPropertiesSet()方法

进入到initHandlerMethods()方法之后,会遍历IOC容器中所有的Bean,如果Bean被@Controller@RequestMapping 注解标注,则将Class中被@RequesMapping 注解标注的方法解析为HandlerMapping、并注册到MappingRegistry中。

1> 判断Class是否为一个Handler

所谓的判断Class是否为一个Handler,即判断Class是否被@Controller 或 @RequestMapping 注解标注;

AbstractHandlerMethodMapping#isHandler()方法用于判断某个类是否为一个拥有 MethodHandler的处理器。

具体的实现在其子类RequestMappingHandlerMapping中:

仅仅判断类有没有被@Controller 或 @RequestMapping 注解标注。

2> 解析Class中的所有HandlerMethod 并注册到MappingRegistry中

确定一个类被@Controller 或 @RequestMapping 注解标注 后,需要进一步确定Class类中的哪几个方法是HandlerMethod(被@RequestMapping注解标注)、可以处理什么URL路径;

遍历类的方法,Spring封装了几层,具体的执行链路如下:

解析方法的HandlerMethod信息 的逻辑 被封装到函数式接口(@FunctionalInterface)一路往下传递(从放在MetadataLookup中 到 MethodCallback 中)。

(1) 解析方法的HandlerMethod信息:

如果方法没有被@RequestMapping注解标注,则返回null,否则返回具体的HandlerMapping信息,比如:

(2) 将HandlerMethod注册到MappingRegistry中:

注册完一个HandlerMethod之后,MappingRegistry的内容如下:

2、请求路径匹配

请求进入到DispatcherServlet之后的时序图:

对应的代码执行链路如下:

1)解析请求路径

UrlPathHelper#getLookupPathForRequest()方法中会对请求进行路径解析;其中会从两个维度进行路径解析:

  1. mvc层面,处理请求本身的路径;
  2. SERVLET层面,处理请求 和 Servlet配置的Mapping 的关系;比如:
    servlet mapping = “/test/*”; request URI = “/test/a” -> “/a”.

1> MVC层面请求路径解析


这里主要做三件事:

(1)获取请求的ContextPath:

  • 就一般请求而言,请求的contextPath都为“”;
  • 如果在RequestDispatcher include中调用,则检测include请求URL;
/**
	 * Return the context path for the given request, detecting an include request
	 * URL if called within a RequestDispatcher include.
	 * <p>As the value returned by @code request.getContextPath() is <i>not</i>
	 * decoded by the servlet container, this method will decode it.
	 * @param request current HTTP request
	 * @return the context path
	 */
public String getContextPath(HttpServletRequest request) 
	String contextPath = (String) request.getAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE);
	if (contextPath == null) 
		contextPath = request.getContextPath();
	
	if (StringUtils.matchesCharacter(contextPath, '/')) 
		// Invalid case, but happens for includes on Jetty: silently adapt it.
		contextPath = "";
	
	return decodeRequestString(request, contextPath);

(2)获取请求HttpServletRequest的URI:

/**
	 * Return the request URI for the given request, detecting an include request
	 * URL if called within a RequestDispatcher include.
	 * <p>As the value returned by @code request.getRequestURI() is <i>not</i>
	 * decoded by the servlet container, this method will decode it.
	 * <p>The URI that the web container resolves <i>should</i> be correct, but some
	 * containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"
	 * in the URI. This method cuts off such incorrect appendices.
	 * @param request current HTTP request
	 * @return the request URI
	 */
public String getRequestUri(HttpServletRequest request) 
	String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE);
	if (uri == null) 
		uri = request.getRequestURI();
	
	return decodeAndCleanUriString(request, uri);

(3)获取requestUri和contextPath的差值:

  • 将给定的“mapping”(即:contextPath)与“requestUri”的开头匹配,如果匹配,则返回额外的部分;
  • 该方法用于解决 HttpServletRequest返回的上下文路径和servlet路径中没有分号内容的问题。
/**
 * Match the given "mapping" to the start of the "requestUri" and if there
 * is a match return the extra part. This method is needed because the
 * context path and the servlet path returned by the HttpServletRequest are
 * stripped of semicolon content unlike the requestUri.
 */
@Nullable
private String getRemainingPath(String requestUri, String mapping, boolean ignoreCase) 
	int index1 = 0;
	int index2 = 0;
	for (; (index1 < requestUri.length()) && (index2 < mapping.length()); index1++, index2++) 
		char c1 = requestUri.charAt(index1);
		char c2 = mapping.charAt(index2);
		if (c1 == ';') 
			index1 = requestUri.indexOf('/', index1);
			if (index1 == -1) 
				return null;
			
			c1 = requestUri.charAt(index1);
		
		if (c1 == c2 || (ignoreCase && (Character.toLowerCase(c1) == Character.toLowerCase(c2)))) 
			continue;
		
		return null;
	
	if (index2 != mapping.length()) 
		return null;
	
	else if (index1 == requestUri.length()) 
		return "";
	
	else if (requestUri.charAt(index1) == ';') 
		index1 = requestUri.indexOf('/', index1);
	
	return (index1 != -1 ? requestUri.substring(index1) : "");

2> Servlet层面请求路径解析

如果配置了alwaysUseFullPath,则不会做Servlet层面的请求路径解析;

此处不对Servlet层面请求路径的解析进行过多讲解,一般不会走进去。

2)根据请求路径找到相应的HandlerMethod

在解析完请求的路径之后,对MappingRegistry加一个读锁,然后再做路径匹配;

真正的请求路径匹配逻辑在AbstractHandlerMethodMapping#lookupHandlerMethod()方法中;

  • 根据请求路径lookupPath从MappingRegistry的urlLookup缓存中找到路径对应的请求映射信息RequestMappingInfo
  • 由于REST ful风格接口的缘故,可能根据请求路径会找到多个RequestMappingInfo,因此需要再对RequestMappingInfo做条件匹配,找到相应RequestMethod的RequestMappingInfo;
  • 然后再根据找到的唯一的RequestMappingInfo从MappingRegistry的mappingLookup缓存中找到请求映射信息对应的HandlerMethod,也就找到了具体的某个Controller中的某个方法。

1> 查找RequestMappingInfo

因为Rest ful风格的缘故,可能会找到多个RequestMappingInfo。

MappingRegistry的urlLookup缓存是在SpringBoot启动时初始化的,见文章上半部分。

动态地址做正则匹配

如果请求是动态地址,例如:@GetMapping("/get/orderId"),则无法从MappingRegistry的urlLookup缓存中获取到请求路径对应的RequestMappingInfo,此时需要遍历所有的RequestMappingInfo,做正则匹配,进而找到具体的HandlerMethod

2> 查找HandlerMethod

因为请求的RequestMethod为GET,所以GET类型的RequestMappingInfo符合条件;


MappingRegistry的mappingLookup缓存也是在SpringBoot启动时初始化的,见文章上半部分。

最后将获取到的HandlerMethod一路向上返回;

三、总结

Spring启动时会加载HandlerMapping的所有实现类;包括:负责处理HTTP请求的RequestMappingHandlerMapping

  • RequestMappingHandlerMapping中使用MappingRegistry 保存了所有的请求映射关系,使用HandlerMethod保存了一个请求映射

  • RequestMappingInfoHandlerMapping间接实现了InitializingBean接口,因此RequestMappingInfoHandlerMapping实例化时会进去到重写后的InitializingBean#afterPropertiesSet()逻辑;

    1. 遍历IOC容器中所有的Bean,如果Bean被@Controller@RequestMapping 注解标注,则将Class中被@RequesMapping 注解标注的方法解析为HandlerMapping、并注册到MappingRegistry中。

      • 将请求路径 和 RequestMappingInfo 作为KV保存在urlLookup缓存中;

        private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
        
      • 将RequestMappingInfo 和 HandlerMethod 作为kv保存在mappingLookup缓存中。

        private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
        
    2. 请求打过来之后,首先会对请求路径从两个维度进行处理;

      • APP层面,处理请求本身的路径;
        • 将requestURI路径和请求头的contextPath做差值。
        • 比如:requestURI是“/test/a”,contextPath是“/test”,则返回的请求路径为“/a”。
      • SERVLET层面,处理请求 和 Servlet配置的Mapping 的关系;
        • 比如:servlet mapping = “/test/*”,假如requestURI是“/test/a”,则会去找“/a”路径。
    3. 然后根据解析后的请求路径去MappingRegistry中的urlLookup缓存找RequestMappingInfo;

      • 由于Rest ful风格的存在,可能根据一个请求路径找到多个RequestMappingInfo;

      • 所以需要进一步通过RequestMethod找到执行类型(GET/POST/PUT/DELTE)的RequestMappingInfo

      • 动态地址无法根据请求路径找到具体的RequestMappingInfo,需要遍历所有的RequestMappingInfo做正则匹配,找到具体的RequestMappingInfo。

        比如:@GetMapping("/get/orderId")

    4. 然后再根据RequestMappingInfoMappingRegistry中的mappingLookup缓存找HandlerMethod。

      • HandlerMethod中保存了请求需要执行的Class(Controller)、Method;

如何在 Spring MVC 中将请求映射到 HTML 文件?

【中文标题】如何在 Spring MVC 中将请求映射到 HTML 文件?【英文标题】:How to map requests to HTML file in Spring MVC? 【发布时间】:2013-05-12 00:07:17 【问题描述】:

基本配置文件看起来不直观。

如果我创建简单的 hello world 示例,然后将 home.jsp 重命名为 home.html 并从以下位置编辑 servlet-context.xml 文件

<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <beans:property name="prefix" value="/WEB-INF/views/" />
    <beans:property name="suffix" value=".jsp" />
</beans:bean> 

<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <beans:property name="prefix" value="/WEB-INF/views/" />
    <beans:property name="suffix" value=".html" />
</beans:bean>

我开始遇到错误

WARN : org.springframework.web.servlet.PageNotFound - No mapping found for HTTP request with URI [/myapp/WEB-INF/views/home.html] in DispatcherServlet with name 'appServlet'

为什么? suffix 属性是什么意思?

更新

我的控制器如下。如您所见,它不包含文件扩展名

@Controller
public class HomeController 

    private static final Logger logger = LoggerFactory.getLogger(HomeController.class);

    /**
     * Simply selects the home view to render by returning its name.
     */
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(Locale locale, Model model) 
        logger.info("Welcome home! The client locale is .", locale);

        Date date = new Date();
        DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

        String formattedDate = dateFormat.format(date);

        model.addAttribute("serverTime", formattedDate );

        return "home";
    


【问题讨论】:

如果是静态文件则需要使用spring mvc静态资源映射 &lt;mvc:resources mapping="/WEB-INF/views/home.html" location="/WEB-INF/views/home.html" /&gt; 您可以删除InternalResourceViewResolver,因为不需要处理 @ArunPJohny 没有。不,我只是想尝试文件扩展名;并希望 HTML 传递 jsp 传递的所有管道。 在这种情况下,您需要创建一个控制器并将 URL /WEB-INF/views/home.html 映射到它,它应该返回视图 /WEB-INF/views/home.html 【参考方案1】:

问题背景

首先要了解的是:渲染jsp文件的不是spring。是 JspServlet (org.apache.jasper.servlet.JspServlet) 做的。这个 servlet 带有 Tomcat(jasper 编译器)而不是 spring。这个 JspServlet 知道如何编译 jsp 页面以及如何将其作为 html 文本返回给客户端。 tomcat 中的 JspServlet 默认只处理匹配两种模式的请求:*.jsp 和 *.jspx。

现在,当 spring 使用 InternalResourceView(或 JstlView)渲染视图时,真正发生了三件事:

    从模型中获取所有模型参数(由您的控制器处理程序方法返回,即"public ModelAndView doSomething() return new ModelAndView("home") ") 将这些模型参数公开为请求属性(以便 JspServlet 可以读取) 将请求转发到 JspServlet。 RequestDispatcher 知道每个 *.jsp 请求都应该转发给 JspServlet(因为这是默认的 tomcat 配置)

当您简单地将视图名称更改为 home.html 时,tomcat 将知道如何处理请求。这是因为没有 servlet 处理 *.html 请求。

解决方案

如何解决这个问题。有三个最明显的解决方案:

    将 html 公开为资源文件 指示 JspServlet 也处理 *.html 请求 编写您自己的servlet(或将请求传递给另一个现有的servlet 到*.html)。

初始配置(只处理jsp)

首先假设我们在没有xml文件的情况下配置spring(仅基于@Configuration注解和spring的WebApplicationInitializer接口)。

基本配置如下

public class MyWebApplicationContext extends AnnotationConfigWebApplicationContext 
  private static final String CONFIG_FILES_LOCATION = "my.application.root.config";

  public MyWebApplicationContext() 
    super();
    setConfigLocation(CONFIG_FILES_LOCATION);
  



public class AppInitializer implements WebApplicationInitializer 

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException 
    WebApplicationContext context = new MyWebApplicationContext();
    servletContext.addListener(new ContextLoaderListener(context));

    addSpringDispatcherServlet(servletContext, context);

  

  private void addSpringDispatcherServlet(ServletContext servletContext, WebApplicationContext context) 
    ServletRegistration.Dynamic dispatcher = servletContext.addServlet("DispatcherServlet",
      new DispatcherServlet(context));
    dispatcher.setLoadOnStartup(2);
    dispatcher.addMapping("/");
    dispatcher.setInitParameter("throwExceptionIfNoHandlerFound", "true");
  


package my.application.root.config
// (...)

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter 
  @Autowired
  @Qualifier("jstlViewResolver")
  private ViewResolver jstlViewResolver;

  @Bean
  @DependsOn( "jstlViewResolver" )
  public ViewResolver viewResolver() 
    return jstlViewResolver;
  

  @Bean(name = "jstlViewResolver")
  public ViewResolver jstlViewResolver() 
    UrlBasedViewResolver resolver = new UrlBasedViewResolver();
    resolver.setPrefix("/WEB-INF/internal/");
    resolver.setViewClass(JstlView.class);
    resolver.setSuffix(".jsp");
    return resolver;
  


在上面的示例中,我使用带有支持视图类 JstlView 的 UrlBasedViewResolver,但您可以使用 InternalResourceViewResolver,就像在您的示例中一样,这并不重要。

上面的例子只配置了一个视图解析器来处理以.jsp结尾的jsp文件。注意:正如开头所说的,JstlView确实是使用tomcat的RequestDispatcher将请求转发给JspSevlet,将jsp编译成html。

解决方案 1 的实现 - 将 html 公开为资源文件:

我们修改 WebConfig 类以添加新的资源匹配。我们还需要修改 jstlViewResolver 使其既不带前缀也不带后缀:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter 
  @Autowired
  @Qualifier("jstlViewResolver")
  private ViewResolver jstlViewResolver;

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) 
    registry.addResourceHandler("/someurl/resources/**").addResourceLocations("/resources/");

  

  @Bean
  @DependsOn( "jstlViewResolver" )
  public ViewResolver viewResolver() 
    return jstlViewResolver;
  

  @Bean(name = "jstlViewResolver")
  public ViewResolver jstlViewResolver() 
    UrlBasedViewResolver resolver = new UrlBasedViewResolver();
    resolver.setPrefix(""); // NOTE: no prefix here
    resolver.setViewClass(JstlView.class);
    resolver.setSuffix(""); // NOTE: no suffix here
    return resolver;
  

// NOTE: you can use InternalResourceViewResolver it does not matter 
//  @Bean(name = "internalResolver")
//  public ViewResolver internalViewResolver() 
//    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
//    resolver.setPrefix("");
//    resolver.setSuffix("");
//    return resolver;
//  

通过添加这个,我们说每个去往http://my.server/someurl/resources/ 的请求都映射到您的Web 目录下的资源目录。因此,如果您将 home.html 放在资源目录中并将浏览器指向http://my.server/someurl/resources/home.html,则将提供该文件。要由您的控制器处理此问题,您需要返回资源的完整路径:

@Controller
public class HomeController 

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView home(Locale locale, Model model) 
        // (...)

        return new ModelAndView("/someurl/resources/home.html"); // NOTE here there is /someurl/resources
    


如果你在同一个目录下放置一些jsp文件(不仅仅是*.html文件),比如home_dynamic.jsp在同一个资源目录下你可以类似的方式访问它,但是你需要使用服务器上的实际路径。路径以 /someurl/ 开头,因为这是仅用于以 .html 结尾的 html 资源的映射)。在这种情况下,jsp 是动态资源,最终由 JspServlet 使用磁盘上的实际路径访问。所以访问jsp的正确方法是:

@Controller
public class HomeController 

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView home(Locale locale, Model model) 
        // (...)

        return new ModelAndView("/resources/home_dynamic.jsp"); // NOTE here there is /resources (there is no /someurl/ because "someurl" is only for static resources 


要在基于 xml 的配置中实现这一点,您需要使用:

<mvc:resources mapping="/someurl/resources/**" location="/resources/" />

并修改您的 jstl 视图解析器:

<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <!-- Please NOTE that it does not matter if you use InternalResourceViewResolver or UrlBasedViewResolver as in annotations example -->
    <beans:property name="prefix" value="" />
    <beans:property name="suffix" value="" />
</beans:bean>

解决方案 2 的实施

在这个选项中,我们使用 tomcat 的 JspServlet 来处理静态文件。因此,您可以在 html 文件中使用 jsp 标记:) 当然,您是否可以选择。很可能您想使用纯 html,所以不要使用 jsp 标签,内容将像静态 html 一样提供。

首先我们删除视图解析器的前缀和后缀,如上例所示:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter 
  @Autowired
  @Qualifier("jstlViewResolver")
  private ViewResolver jstlViewResolver;

  @Bean
  @DependsOn( "jstlViewResolver" )
  public ViewResolver viewResolver() 
    return jstlViewResolver;
  

  @Bean(name = "jstlViewResolver")
  public ViewResolver jstlViewResolver() 
    InternalResourceViewResolver resolver = new InternalResourceViewResolver(); // NOTE: this time I'm using InternalResourceViewResolver and again it does not matter :)
    resolver.setPrefix("");
    resolver.setSuffix("");
    return resolver;
  


现在我们添加 JspServlet 来处理 *.html 文件:

public class AppInitializer implements WebApplicationInitializer 

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException 
    WebApplicationContext context = new MyWebApplicationContext();
    servletContext.addListener(new ContextLoaderListener(context));

    addStaticHtmlFilesHandlingServlet(servletContext);
    addSpringDispatcherServlet(servletContext, context);

  

 // (...)

  private void addStaticHtmlFilesHandlingServlet(ServletContext servletContext) 
    ServletRegistration.Dynamic servlet = servletContext.addServlet("HtmlsServlet", new JspServlet()); // org.apache.jasper.servlet.JspServlet
    servlet.setLoadOnStartup(1);
    servlet.addMapping("*.html");
  


重要的是,要使此类可用,您需要从您的 tomcat 安装中添加 jasper.jar,以供编译时使用。如果你有 maven 应用程序,使用 jar 的 scope=provided 非常容易。 maven 中的依赖如下所示:

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper</artifactId>
    <version>$tomcat.libs.version</version>
    <scope>provided</scope> <!--- NOTE: scope provided! -->
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jsp-api</artifactId>
    <version>$tomcat.libs.version</version>
    <scope>provided</scope>
</dependency>

如果你想以 xml 方式进行。您需要注册 jsp servlet 来处理 *.html 请求,因此您需要将以下条目添加到您的 web.xml

<servlet>
    <servlet-name>htmlServlet</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <load-on-startup>3</load-on-startup>
</servlet>

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

现在在您的控制器中,您可以像之前的示例一样访问 html 和 jsp 文件。优点是没有解决方案 1 中需要的“/someurl/”额外映射。您的控制器将如下所示:

@Controller
public class HomeController 

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView home(Locale locale, Model model) 
        // (...)

        return new ModelAndView("/resources/home.html"); 


要指向您的 jsp,您所做的完全相同:

@Controller
public class HomeController 

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView home(Locale locale, Model model) 
        // (...)

        return new ModelAndView("/resources/home_dynamic.jsp");


解决方案 3 的实施

第三种解决方案在某种程度上是解决方案 1 和解决方案 2 的组合。所以在这里我们希望将所有对 *.html 的请求传递给其他一些 servlet。您可以自己编写或寻找已经存在的 servlet 的一些好的候选者。

如上所述,我们首先清理视图解析器的前缀和后缀:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter 
  @Autowired
  @Qualifier("jstlViewResolver")
  private ViewResolver jstlViewResolver;

  @Bean
  @DependsOn( "jstlViewResolver" )
  public ViewResolver viewResolver() 
    return jstlViewResolver;
  

  @Bean(name = "jstlViewResolver")
  public ViewResolver jstlViewResolver() 
    InternalResourceViewResolver resolver = new InternalResourceViewResolver(); // NOTE: this time I'm using InternalResourceViewResolver and again it does not matter :)
    resolver.setPrefix("");
    resolver.setSuffix("");
    return resolver;
  


现在我们不使用 tomcat 的 JspServlet,而是编写自己的 servlet(或重用一些现有的):

public class StaticFilesServlet extends HttpServlet 
  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException 
    response.setCharacterEncoding("UTF-8");

    String resourcePath = request.getRequestURI();
    if (resourcePath != null) 
      FileReader reader = null;
      try 
        URL fileResourceUrl = request.getServletContext().getResource(resourcePath);
        String filePath = fileResourceUrl.getPath();

        if (!new File(filePath).exists()) 
          throw new IllegalArgumentException("Resource can not be found: " + filePath);
        
        reader = new FileReader(filePath);

        int c = 0;
        while (c != -1) 
          c = reader.read();
          if (c != -1) 
            response.getWriter().write(c);
          
        

       finally 
        if (reader != null) 
          reader.close();
        
      
    
  

我们现在指示 spring 将所有对 *.html 的请求传递给我们的 servlet

public class AppInitializer implements WebApplicationInitializer 

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException 
    WebApplicationContext context = new MyWebApplicationContext();
    servletContext.addListener(new ContextLoaderListener(context));

    addStaticHtmlFilesHandlingServlet(servletContext);
    addSpringDispatcherServlet(servletContext, context);

  

 // (...)

  private void addStaticHtmlFilesHandlingServlet(ServletContext servletContext) 
    ServletRegistration.Dynamic servlet = servletContext.addServlet("HtmlsServlet", new StaticFilesServlet());
    servlet.setLoadOnStartup(1);
    servlet.addMapping("*.html");

  


优点(或缺点,取决于你想要什么)是jsp标签显然不会被处理。你的控制器看起来像往常一样:

@Controller
public class HomeController 

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView home(Locale locale, Model model) 
        // (...)

        return new ModelAndView("/resources/home.html");


对于jsp:

@Controller
public class HomeController 

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView home(Locale locale, Model model) 
        // (...)

        return new ModelAndView("/resources/home_dynamic.jsp");


【讨论】:

感谢您的广泛回答,但您没有触及web.xml 中的&lt;jsp-config&gt;&lt;jsp-property-group&gt;&lt;url-pattern&gt; 配置。没有办法在 Spring Boot MVC 中对 existing JSP servlet 执行等效配置吗?在***.com/q/56210281/421049 上查看我的问题。【参考方案2】:

Resolver 类用于解析视图类的资源,视图类依次从资源中生成视图。例如,一个典型的 InternalResourceViewResolver 如下:

<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <beans:property name="prefix" value="/WEB-INF/views/" />
        <beans:property name="suffix" value=".jsp" />
</beans:bean>

视图名称“home”将映射为“/WEB-INT/views/home.jsp”,然后使用视图类 InternalResourceView(用于 JSP)转换为 JSP 视图。如果将后缀值替换为“.html”,Spring可以获取具体资源“/WEB-INT/views/home.html”但不知道如何生成。

【讨论】:

@SuzanCioc 你实际上不需要 InternalResourceViewResolver,它是用于 jsp 文件的。您需要的是将您的 html 页面设置为静态资源。【参考方案3】:

普通的 .html 文件是静态的,不需要特殊的 ViewResolver。您应该为您的 html 页面设置一个静态文件夹,如 here 所示。

例如:

<mvc:resources mapping="/static/**" location="/static/" />

【讨论】:

【参考方案4】:

好吧,看来你没有设置视图的顺序

例如,如果你的项目有jsp、json、velocity、freemarker等视图,你可以全部使用(也许你需要新版本的spring,3.1+),但只有一个视图选择渲染到客户端,这取决于您的视图的顺序,顺序越低,视图越喜欢

例如,你设置jsp视图的顺序是1,freemarker视图的顺序是2,它们的视图名称都是“home”,spring会选择view.jsp(如果您将后缀设置为 .jsp)。好吧,如果你的视图名称是“index”,没有 index.jsp 而是 index.ftl(假设你将 freemarker 的视图设置为 .ftl),spring 会选择后者。

示例代码使用spring's java config,可以轻松转换为xml风格。

@Bean
public InternalResourceViewResolver jspViewResolver() 
    InternalResourceViewResolver jsp = new InternalResourceViewResolver();
    jsp.setOrder(4);
    jsp.setCache(true);
    jsp.setViewClass(org.springframework.web.servlet.view.JstlView.class);
    jsp.setPrefix("/WEB-INF/jsp/");
    jsp.setSuffix(".jsp");
    return jsp;


@Bean
public FreeMarkerViewResolver freeMarkerViewResolver() 
    FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver();
    viewResolver.setCache(true);
    viewResolver.setPrefix("");
    viewResolver.setSuffix(".ftl");
    viewResolver.setContentType(ViewConstants.MEDIA_TYPE_HTML);
    viewResolver.setRequestContextAttribute("request");
    viewResolver.setExposeSpringMacroHelpers(true);
    viewResolver.setExposeRequestAttributes(true);
    viewResolver.setExposeSessionAttributes(true);
    viewResolver.setOrder(2);
    return viewResolver;

请看setOrder()方法!

json、jsonp等类型的视图可能会用到ontentNegotiation,你可以在spring的文档中找到。

最后,html 视图,我的意思是,完全静态文件,spring 默认不支持。我想静态文件不需要由 java 渲染。您可以使用以下代码使用静态映射:

<mvc:resources mapping="/static/**" location="/static/" />

或使用 java 配置:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) 
    int cachePeriod = 3600 * 24 * 15;
    registry.addResourceHandler("/static/**").addResourceLocations("/static/").setCachePeriod(cachePeriod);
    registry.addResourceHandler("/favicon.ico").addResourceLocations("/").setCachePeriod(cachePeriod);
    registry.addResourceHandler("/robots.txt").addResourceLocations("/").setCachePeriod(cachePeriod);

并且在您的@RequestMapping 方法中,您应该重定向它!

好吧,如果您不想重定向,只需将 html 视图设置为 动态 视图(freemark、velecity 等),就可以了!

希望有用!

【讨论】:

【参考方案5】:

Spring MVC 不允许您通过控制器呈现静态资源。正如 Arun 所说,它应该通过resources 提供服务。

如果我错了,请纠正我,但您似乎希望将index.html 作为首页。为此,您应该有一个映射到/index.html 的控制器(比如IndexController)。然后,您应该在您的web.xml 中配置您的欢迎文件是index.html。这样,每当您指向应用程序的根目录时,您的容器都会查找“/index.html”,进而查找映射到/index.html URL 的控制器。

因此,您的控制器应该如下所示:

@Controller
@RequestMapping("/index.html")
public class MyIndexController 
    @RequestMapping(method=RequestMethod.GET)
    protected String gotoIndex(Model model) throws Exception       
        return "myLandingPage";
    

在你的 web.xml 中

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>

希望这会有所帮助。

【讨论】:

但是如何将多个url等/、/index、/home映射到一个静态页面呢?【参考方案6】:

我认为InternalResourceViewResolver 支持servlet 和jsp 文件。根据 Spring 的 API javadocs 的后缀是“在构建 URL 时附加到视图名称”。它不是文件的扩展名,即使它非常具有误导性。我检查了UrlBasedViewResolver setSuffix() 类。

如果他们将其命名为 viewSuffix,我猜它可能更有意义。

【讨论】:

【参考方案7】:

您遇到此问题是因为可能没有为映射 *.html 注册任何 servlet。 所以调用以“默认 servlet”结束,它注册了一个 / 的 servlet 映射,这可能是你的 DispatcherServlet 。 现在 Dispatcher servlet 找不到控制器来处理对 home.html 的请求以及您所看到的消息。 要解决此问题,您可以注册 *.html 扩展名以由 JSPServlet 处理,然后它应该可以干净地工作。

【讨论】:

以上是关于源码剖析Spring MVC如何将请求映射到Controller?的主要内容,如果未能解决你的问题,请参考以下文章

SpringMVC源码剖析-SpringMVC初始化

Spring 3 MVC - 将带有前缀的请求参数映射到单个 bean

从源码角度深度剖析 Spring MVC

一个请求过来,Spring MVC 是如何找到正确的 Controller 的?

Spring Mvc源码剖析

Spring Mvc源码剖析