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

25

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

如果我创建一个简单的“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'
为什么?什么是“后缀”属性的意思?
更新:
我的控制器如下所示。正如您所看到的,它不包含文件扩展名。
@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静态资源映射。 - Arun P Johny
Ex <mvc:resources mapping="/WEB-INF/views/home.html" location="/WEB-INF/views/home.html" /> - Arun P Johny
你可以删除InternalResourceViewResolver,因为没有需要处理的内容。 - Arun P Johny
@ArunPJohny 不是,我只想尝试一下文件扩展名,并希望HTML通过jsp​​传递的所有内容。 - Suzan Cioc
在这种情况下,您需要创建一个控制器并将URL“/WEB-INF/views/home.html”映射到它,它应该返回视图“/WEB-INF/views/home.html”。 - Arun P Johny
显示剩余2条评论
7个回答

32

问题背景

首先要明白的是:渲染 jsp 文件的不是 Spring,而是 JspServlet(org.apache.jasper.servlet.JspServlet)。这个 Servlet 是随 Tomcat(jasper 编译器)一起提供的,而不是 Spring。JspServlet 知道如何编译 jsp 页面,并将其作为 html 文本返回给客户端。Tomcat 中的 JspServlet 默认只处理两种模式的请求:*.jsp 和 *.jspx。

当 Spring 使用 InternalResourceView(或 JstlView)渲染视图时,实际上会发生三件事:

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

如果你仅仅将视图名称更改为 home.html,则 Tomcat 将无法处理该请求。这是因为没有 Servlet 处理 *.html 请求。

解决方案

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

  1. 将 html 公开为资源文件
  2. 指示 JspServlet 也处理 *.html 请求
  3. 编写自己的 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;
  }

}

在上面的例子中,我使用了基于URL的视图解析器UrlBasedViewResolver和支持JstlView的后备视图类,但是你可以像你的例子一样使用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/的请求都映射到您的网站目录下的资源目录。因此,如果你把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应用程序,可以通过使用范围=提供程序轻松实现此操作。在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方式完成此操作,则需要在web.xml中添加以下内容,以注册jsp servlet处理*.html请求。
<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文件。优点是不需要Solution 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或查找一些已经存在的良好候选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<jsp-config><jsp-property-group><url-pattern> 的配置。在 Spring Boot MVC 中,难道没有一种方法可以对 现有 的 JSP servlet 执行等效的配置吗?请参阅我的问题 https://dev59.com/27Pma4cB1Zd3GeqPzPgX。 - Garret Wilson

5

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页面设置为静态资源。 - zygimantus

4

纯 .html 文件是静态的,不需要特殊的 ViewResolver。 您应该设置一个静态文件夹来存放您的 html 页面,如此处所示:链接

例如:

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

2
好的,看起来你没有设置视图的顺序。
例如,如果您的项目有像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的Java配置,您可以轻松转换为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和其他类型的视图可能使用内容协商,您可以在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等),这样就可以了!
希望这对你有用!

1

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

如果我说错了,请纠正我,但似乎您想要一个index.html作为首页。为了实现这一点,您应该有一个Controller(比如IndexController)映射到/index.html。然后,您应该在web.xml中配置您的欢迎文件是index.html。这样,每当您指向您的应用程序的根目录时,您的容器将查找一个"/index.html",并且会查找映射到/index.html URL的Controller。

因此,您的Controller应该看起来像这样:

@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>

希望这有所帮助。

1
但是如何将许多URL(例如/、/index、/home)映射到一个静态页面? - zygimantus

0

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

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


0
你遇到这个问题是因为可能没有为 *.html 映射任何 servlet。 因此,调用最终会使用“默认servlet”,该servlet已注册为 / 的 servlet-mapping,这可能是您的 DispatcherServlet。 现在,Dispatcher servlet 找不到控制器来处理 home.html 请求,因此您看到的消息。 要解决此问题,您可以将 *.html 扩展名注册为由 JSPServlet 处理,然后它应该能够正常工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接