问题背景
首先要理解的是:渲染 jsp 文件的不是 spring。 JspServlet (org.apache.jasper.servlet.JspServlet) 就是这样做的。这个 servlet 是 Tomcat(jasper 编译器)附带的,而不是 spring 附带的。这个 JspServlet 知道如何编译 jsp 页面以及如何将其作为 html 文本返回给客户端。 tomcat中的JspServlet默认只处理匹配两种模式的请求:*.jsp和*.jspx。
现在当 spring 渲染视图时InternalResourceView
(or JstlView
),实际上发生了三件事:
- 从模型中获取所有模型参数(由控制器处理程序方法返回,即
"public ModelAndView doSomething() { return new ModelAndView("home") }"
)
- 将这些模型参数公开为请求属性(以便 JspServlet 可以读取)
- 将请求转发给 JspServlet。
RequestDispatcher
知道每个 *.jsp 请求都应该转发到 JspServlet (因为这是 tomcat 的默认配置)
当您只需将视图名称更改为 home.html tomcat 就会not知道如何处理请求。这是因为没有 Servlet 处理 *.html 请求。
Solution
怎么解决这个问题。有三个最明显的解决方案:
- 将 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;
}
}
在上面的示例中,我使用 UrlBasedViewResolver 和支持视图类 JstlView,但您可以使用 InternalResourceViewResolver,因为在您的示例中这并不重要。
上面的示例仅使用一个视图解析器配置应用程序,该视图解析器处理以结尾的 jsp 文件.jsp
。注意:正如开头所述,JstlView实际上使用tomcat的RequestDispatcher将请求转发到JspSevlet以将jsp编译为html。
解决方案1的实现- 将 html 公开为资源文件:
我们修改Web Config类来添加新的资源匹配。我们还需要修改 jstlView Resolver,使其既不带前缀也不带后缀:
@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/ http://my.server/someurl/resources/映射到您的 web 目录下的 resources 目录。因此,如果您将 home.html 放在 resources 目录中并将浏览器指向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,您可以以类似的方式访问它,但您需要使用服务器上的实际路径。该路径确实not以 /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,也可以寻找现有 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");
}