How to map requests to HTML file in Spring MVC?

Background of the problem

First thing to understand is following: it is NOT spring which renders the jsp files. It is JspServlet (org.apache.jasper.servlet.JspServlet) which does it. This servlet comes with Tomcat (jasper compiler) not with spring. This JspServlet is aware how to compile jsp page and how to return it as html text to the client. The JspServlet in tomcat by default only handles requests matching two patterns: *.jsp and *.jspx.

Now when spring renders the view with InternalResourceView (or JstlView), three things really takes place:

  1. get all the model parameters from model (returned by your controller handler method i.e. "public ModelAndView doSomething() { return new ModelAndView("home") }")
  2. expose these model parameters as request attributes (so that it can be read by JspServlet)
  3. forward request to JspServlet. RequestDispatcher knows that each *.jsp request should be forwarded to JspServlet (because this is default tomcat’s configuration)

When you simply change the view name to home.html tomcat will not know how to handle the request. This is because there is no servlet handling *.html requests.

Solution

How to solve this. There are three most obvious solutions:

  1. expose the html as a resource file
  2. instruct the JspServlet to also handle *.html requests
  3. write your own servlet (or pass to another existing servlet requests to *.html).

Initial configuration (only handling jsp)

First let’s assume we configure spring without xml files (only basing on @Configuration annotation and spring’s WebApplicationInitializer interface).

Basic configuration would be following

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("https://stackoverflow.com/");
    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;
  }

}

In above example I’m using UrlBasedViewResolver with backing view class JstlView, but you can use InternalResourceViewResolver as in your example it does not matter.

Above example configures application with only one view resolver which handles jsp files ending with .jsp. NOTE: As stated in the beginning JstlView really uses tomcat’s RequestDispatcher to forward the request to JspSevlet to compile the jsp to html.

Implementation on solution 1 – expose the html as a resource file:

We modify the WebConfig class to add new resources matching. Also we need to modify the jstlViewResolver so that it does not take neither prefix nor suffix:

@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;
//  }
}

By adding this we say that every that every request going to http://my.server/someurl/resources/ is mapped to resources directory under your web directory. So if you put your home.html in resources directory and point your browser to http://my.server/someurl/resources/home.html the file will be served. To handle this by your controller you then return the full path to the resource:

@Controller
public class HomeController {

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

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

}

If you place in the same directory some jsp files (not only *.html files), say home_dynamic.jsp in the same resources directory you can access it similar way, but you need to use the actual path on the server. The path does not start with /someurl/ because this is the mapping only for html resources ending with .html). In this context jsp is dynamic resource which in the end is accessed by JspServlet using actual path on disk. So correct way to access the jsp is:

@Controller
public class HomeController {

    @RequestMapping(value = "https://stackoverflow.com/", 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 

}

To achieve this in xml based config you need to use:

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

and modify your jstl view resolver:

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

Implementation on solution 2:

In this option we use the tomcat’s JspServlet to handle also static files. As a consequence you can use jsp tags in your html files:) It’s of course your choice if you do it or not. Most probably you want to use plain html so simply do not use jsp tags and the content will be served as if it was static html.

First we delete prefix and suffix for view resolver as in previous example:

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

}

Now we add JspServlet for handling also *.html files:

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");
  }

}

Important is that to make this class available you need to add the jasper.jar from your tomcat’s installation just for compilation time. If you have maven app this is realtively easy by using the scope=provided for the jar. The dependency in maven will look like:

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

If you want to do it in xml way. You would need to register jsp servlet to handle *.html requests, so you need to add following entry to your 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>

Now in your controller you can access both html and jsp files just like in previous example. The advantage is that there is no “/someurl/” extra mapping which was needed in Solution 1. Your controller will look like:

@Controller
public class HomeController {

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

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

}

To point to your jsp you are doing exactly the same:

@Controller
public class HomeController {

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

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

}

Implementation on solution 3:

Third solution is somewhat a combination of solution 1 and solution 2. So in here we want to pass all the requests to *.html to some other servlet. You can write your own or look for some good candidate of already existing servlet.

As above first we clean up the prefix and suffix for the view resolver:

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

}

Now instead of using the tomcat’s JspServlet we write our own servlet (or reuse some existing):

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();
        }
      }
    }
  }
}

We now instruct the spring to pass all requests to *.html to our 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");

  }

}

The advantage (or disadvantage, depends on what you want) is that jsp tags will obviously not be processed. You controller looks as usual:

@Controller
public class HomeController {

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

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

}

And for jsp:

@Controller
public class HomeController {

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

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

}

Leave a Comment