Как сопоставить запросы к файлу HTML в Spring MVC?

Базовые файлы конфигурации выглядят неинтуитивно.

Если я создаю простой пример приветствия, а затем переименуйте 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> 

to

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

UPDATE

Следующий мой контроллер. Как вы видите, он не содержит расширения файла

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

}

Ответы

Ответ 1

Предыстория проблемы

Первое, что нужно понять, следующее: это НЕ spring, который отображает файлы jsp. Это делает JspServlet (org.apache.jasper.servlet.JspServlet). Этот сервлет поставляется с компилятором Tomcat (jasper), а не с spring. Этот JspServlet знает, как скомпилировать jsp-страницу и как вернуть ее в виде html-текста клиенту. JspServlet в tomcat по умолчанию обрабатывает запросы, соответствующие двум шаблонам: *.jsp и *.jspx.

Теперь, когда spring отображает представление с помощью InternalResourceView (или JstlView), три вещи действительно имеют место:

  • получить все параметры модели из модели (возвращается методом обработчика контроллера, т.е. "public ModelAndView doSomething() { return new ModelAndView("home") }")
  • выставить эти параметры модели в качестве атрибутов запроса (чтобы их можно было прочитать JspServlet)
  • запрос вперед в JspServlet. RequestDispatcher знает, что каждый запрос *.jsp должен быть перенаправлен в JspServlet (поскольку это настройка по умолчанию tomcat)

Когда вы просто меняете имя вида на home.html, tomcat не будет знать, как обрабатывать запрос. Это связано с тем, что обработчики сервлета не обрабатываются *.html.

Решение

Как это решить. Существует три наиболее очевидных решения:

  • выставить html в качестве файла ресурсов
  • поручить JspServlet также обрабатывать запросы *.html
  • напишите свой собственный сервлет (или перейдите к другим существующим запросам сервлетов в *.html).

Начальная конфигурация (только обработка jsp)

Сначала предположим, что мы конфигурируем spring без XML файлов (только на основе аннотации @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;
  }

}

В приведенном выше примере я использую 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 preffix 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 для обработки также статических файлов. Как следствие, вы можете использовать теги jsp в ваших html файлах:) Конечно, ваш выбор, если вы это сделаете или нет. Скорее всего, вы хотите использовать простой html, поэтому просто не используйте теги jsp, и контент будет подан так, как если бы он был статическим html.

Сначала мы удаляем префикс и суффикс для вида 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;
  }

}

Теперь мы добавляем 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");
  }

}

Важно, чтобы для этого класса вам нужно было добавить jasper.jar из вашей установки tomcat только для времени компиляции. Если у вас есть приложение maven, это реально легко, используя scope =, предоставленный для банки. Зависимость в 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. Вам нужно будет зарегистрировать сервлет ssp для обработки запросов *.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 на другой сервлет. Вы можете написать свой собственный или найти хорошего кандидата уже существующего сервлета.

Как указано выше, мы очищаем префикс и суффикс для распознавателя вида:

@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 мы пишем наш собственный сервлет (или повторно используем некоторые существующие):

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 на наш сервлет

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

}
@Controller
public class HomeController {

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

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

}

Ответ 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", а затем переведено в JSP-представление, используя класс представления InternalResourceView (который предназначен для JSP). Если вы замените значение суффикса на ".html", Spring может получить конкретный ресурс "/WEB-INT/views/home.html", но не знает, как его сгенерировать.

Ответ 3

Обычные .html файлы являются статическими и не нуждаются в специальном ViewResolver. Вы должны настроить статическую папку для своих html-страниц, как показано здесь.

Например:

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

Ответ 4

ну, похоже, вы не указали порядок представления.

Например, если ваш проект имеет вид jsp, json, speed, freemarker и т.д., вы можете использовать их все (возможно, вам нужна новая версия spring, 3.1+), но только один вид будет выбран для рендеринга для клиента, который зависит от порядка вашего представления, , чем ниже порядок, тем предпочтительнее вид.

например, вы устанавливаете порядок представления jsp: 1, а порядок просмотра freemarker равен 2, оба их имени вида - "home", spring выбирает вид. jsp (если вы установите суффикс в .jsp). Ну, если ваше имя представления "index", не index.jsp, а index.ftl(предположим, что вы задаете вид freemarker на .ftl), spring выберет более позднюю версию.

пример кода с использованием java-конфигурации spring, вы можете легко преобразовать в 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 config:

@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 не позволяет отображать статические ресурсы над контроллерами. Как сказал Арун, его следует обслуживать через resources.

Исправьте меня, если я ошибаюсь, но, похоже, вам нужна index.html как первая страница. Для этого вы должны иметь контроллер (скажем, IndexController), сопоставленный с /index.html. Затем вы должны настроить в своем web.xml свой голос, чтобы ваш приветственный файл был index.html. Таким образом, всякий раз, когда вы указываете корень приложения, ваш контейнер будет искать "/index.html" и, в свою очередь, будет искать контроллер, сопоставленный с URL /index.html.

Итак, ваш контроллер должен выглядеть примерно так:

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

Надеюсь, что это поможет.

Ответ 6

Я думаю, что InternalResourceViewResolver поддерживает сервлеты и jsp файлы. Суффикс по Spring API javadocs - это то, что "добавляется для просмотра имен при создании URL". Это не расширение файла, хотя оно очень вводит в заблуждение. Я проверил класс UrlBasedViewResolver setSuffix().

Возможно, если они назовут его как viewSuffix, это может иметь смысл, я думаю.

Ответ 7

У вас есть эта проблема, потому что не может быть сервлета, зарегистрированного для сопоставления *.html.
поэтому вызов заканчивается "сервлетом по умолчанию", который регистрируется сервлет-отображением/, который, вероятно, ваш DispatcherServlet.
Теперь сервлет диспетчера не находит контроллер для обработки запроса для home.html и, следовательно, сообщения, которое вы видите.
Чтобы решить эту проблему, вы можете зарегистрировать расширение *.html, которое будет обрабатываться JSPServlet, а затем оно должно работать чисто.