Spring Boot 和 JSP(Java Server Pages)

1、概览

在构建 Java Web 应用时,可以使用 Java Server Pages(JSP)作为 HTML 页面模板。

Spring Boot 是一个流行的框架,可以用它来快速开发 Java Web 应用。 但是,在 Spring Boot 中使用 JSP 有一定的局限性,应该考虑用 ThymeleafFreeMarker 来替代 JSP。

2、Maven 依赖

首先来看看在 Spring Boot 中使用 JSP 需要哪些依赖。

2.1、作为独立应用运行

首先,添加 spring-boot-starter-web 依赖。

该依赖提供了使用 Spring Boot 和默认的嵌入式 Tomcat Servlet 容器来运行 Web 应用的所有核心依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.4.4</version>
</dependency>

注意,使用 Undertow 作为嵌入式 Servlet 容器使用时不支持 JSP。

接下来,需要添加 tomcat-embed-jasper 依赖,以便应用能够编译和渲染 JSP 页面:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <version>9.0.44</version>
</dependency>

虽然可以手动提供上述两个依赖,但通常最好让 Spring Boot 管理这些依赖的版本,而我们只需管理 Spring Boot 版本即可。

版本管理可以通过使用 Spring Boot Parent POM 或使用 Dependency Management 来实现。

最后,需要加入 jstl 库,它为 JSP 页面提供 JSTL Tag 支持:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

2.2、在 Web 容器(Tomcat)中运行

在 Tomcat Web 容器中运行时,仍然需要上述依赖项。

不过,为了避免应用提供的依赖与 Tomcat 运行时提供的依赖发生冲突,需要设置两个具有 provided scope 的依赖:

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <version>9.0.44</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <version>2.4.4</version>
    <scope>provided</scope>
</dependency>

注意,必须明确定义 spring-boot-starter-tomcat,并用 provided scope 标记它。这是因为 spring-boot-starter-web 已经提供了一个传递依赖。

3、视图解析器配置

按照惯例,将 JSP 文件放在 ${project.basedir}/main/webapp/WEB-INF/jsp/ 目录中。

需要在 application.properties 文件中配置两个属性,让 Spring 知道这些 JSP 文件的位置:

spring.mvc.view.prefix: /WEB-INF/jsp/
spring.mvc.view.suffix: .jsp

编译时,Maven 将确保生成的 WAR 文件在 WEB-INF 目录中包含上述 jsp 目录。

4、启动应用

Application 类根据部署方式不同,也有差异。

当作为独立应用程序运行时,Application 类是一个简单的 @SpringBootApplication 注解类,带有一个 main 方法:

@SpringBootApplication(scanBasePackages = "com.baeldung.boot.jsp")
public class SpringBootJspApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootJspApplication.class);
    }
}

但是,如果需要在 Web 容器中部署,就需要继承 SpringBootServletInitializer

这将把应用的 ServletFilterServletContextInitializer 与运行时服务器绑定,这是应用运行所必需的:

@SpringBootApplication(scanBasePackages = "com.baeldung.boot.jsp")
public class SpringBootJspApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(SpringBootJspApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringBootJspApplication.class);
    }
}

5、提供一个简单的 WEB 页面

JSP 页面依靠 Java Server Pages 标准标签库(JSTL)来提供常用的模板功能,如分支、迭代和格式化,甚至还提供了一组预定义函数。

接下来,创建一个简单的网页,显示应用中保存的 Book 列表。

假设已有一个 BookService,可以检索所有 Book 对象:

public class Book {
    private String isbn;
    private String name;
    private String author;

    // get / set / 构造函数省略
}

public interface BookService {
    Collection<Book> getBooks();
    Book addBook(Book book);
}

编写一个 Spring MVC Controller,将其渲染到 Web 页面。

@Controller
@RequestMapping("/book")
public class BookController {

    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping("/viewBooks")
    public String viewBooks(Model model) {
        model.addAttribute("books", bookService.getBooks());
        return "view-books";
    }
}

BookController 将返回一个名为 view-books 的视图模板。根据之前在 application.properties 中的配置,Spring MVC 将在 /WEB-INF/jsp/ 目录中查找 view-books.jsp

需要在该位置创建此文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
    <head>
        <title>View Books</title>
        <link href="<c:url value="/css/common.css"/>" rel="stylesheet" type="text/css">
    </head>
    <body>
        <table>
            <thead>
                <tr>
                    <th>ISBN</th>
                    <th>Name</th>
                    <th>Author</th>
                </tr>
            </thead>
            <tbody>
                <c:forEach items="${books}" var="book">
                    <tr>
                        <td>${book.isbn}</td>
                        <td>${book.name}</td>
                        <td>${book.author}</td>
                    </tr>
                </c:forEach>
            </tbody>
        </table>
    </body>
</html>

上面的示例展示了如何使用 JSTL <c:url> 标签来链接 JavaScript 和 CSS 等外部资源。通常将这些资源放在 ${project.basedir}/main/resources/static/ 目录下。

还可以看到 JSTL <c:forEach> 标签是如何用于遍历 BookController 提供的 books Model 属性的。

6、处理表单提交

现在来看看如何使用 JSP 处理表单提交。

BookController 中提供添加 Book 的端点,处理表单提交:

public class BookController {

    // 已存在的代码省略...

    @GetMapping("/addBook")
    public String addBookView(Model model) {
        model.addAttribute("book", new Book());
        return "add-book";
    }

    @PostMapping("/addBook")
    public RedirectView addBook(@ModelAttribute("book") Book book, RedirectAttributes redirectAttributes) {
        final RedirectView redirectView = new RedirectView("/book/addBook", true);
        Book savedBook = bookService.addBook(book);
        redirectAttributes.addFlashAttribute("savedBook", savedBook);
        redirectAttributes.addFlashAttribute("addBookSuccess", true);
        return redirectView;
    } 
}

创建以下 add-book.jsp 文件(切记将其放在正确的目录下):

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Add Book</title>
    </head>
    <body>
        <c:if test="${addBookSuccess}">
            <div>Successfully added Book with ISBN: ${savedBook.isbn}</div>
        </c:if>
    
        <c:url var="add_book_url" value="/book/addBook"/>
        <form:form action="${add_book_url}" method="post" modelAttribute="book">
            <form:label path="isbn">ISBN: </form:label> <form:input type="text" path="isbn"/>
            <form:label path="name">Book Name: </form:label> <form:input type="text" path="name"/>
            <form:label path="author">Author Name: </form:label> <form:input path="author"/>
            <input type="submit" value="submit"/>
        </form:form>
    </body>
</html>

使用 form:form 标签提供的 modelAttribute 参数,将 BookControlleraddBookView() 方法中添加的 book 属性绑定到表单,然后在提交表单时填写该属性。

使用该标签后,需要单独定义表单操作 URL,因为不能把标签放在标签内(不能嵌套)。还使用 form:input 标签中的 path 属性,将每个输入字段与 Book 对象中的属性绑定。

7、错误处理

由于使用 Spring Boot 和 JSP 时的现有限制,无法提供自定义 error.html 来定制默认的 /error mapping。相反,需要创建自定义错误页面来处理不同的错误。

7.1、静态错误页面

如果想针对不同的 HTTP 错误显示自定义错误页面,可以提供静态错误页面。

比方说,需要为应用抛出的所有 4xx 错误提供一个错误页面。只需在 ${project.basedir}/main/resources/static/error/ 目录下放置一个名为 4xx.html 的文件即可。

如果应用出现 4xx HTTP 错误,Spring 将解析该错误并返回所提供的 4xx.html 页面。

7.2、动态错误页面

我们可以通过多种方法来处理异常,从而提供定制的错误页面和上下文(Context)信息。

例如:使用 @ControllerAdvice@ExceptionHandler 注解。

假设应用定义了一个 DuplicateBookException

public class DuplicateBookException extends RuntimeException {
    private final Book book;

    public DuplicateBookException(Book book) {
        this.book = book;
    }

    // get 方法省略
}

如果我们试图添加两本具有相同 ISBN 的 Book 的话,BookServiceImpl 类将抛出上述 DuplicateBookException 异常

@Service
public class BookServiceImpl implements BookService {

    private final BookRepository bookRepository;

    // 构造函数和其他方法忽略...

    @Override
    public Book addBook(Book book) {
        final Optional<BookData> existingBook = bookRepository.findById(book.getIsbn());
        if (existingBook.isPresent()) {
            throw new DuplicateBookException(book);
        }

        final BookData savedBook = bookRepository.add(convertBook(book));
        return convertBookData(savedBook);
    }

    // 转换逻辑
}

然后,使用 LibraryControllerAdvice 类定义要处理哪些错误,以及如何处理每个错误:

@ControllerAdvice
public class LibraryControllerAdvice {

    @ExceptionHandler(value = DuplicateBookException.class)
    public ModelAndView duplicateBookException(DuplicateBookException e) {
        final ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("ref", e.getBook().getIsbn());
        modelAndView.addObject("object", e.getBook());
        modelAndView.addObject("message", "Cannot add an already existing book");
        modelAndView.setViewName("error-book");
        return modelAndView;
    }
}

我们需要定义 error-book.jsp 文件,用于解决上述错误。请确保将其放在 ${project.basedir}/main/webapp/WEB-INF/jsp/ 目录下,因为这不再是静态 HTML,而是需要编译的 JSP 模板。

8、总结

本教程涉及多个主题,关键点如下:

  • JSP 包含一些固有的限制。请考虑使用 Thymeleaf 或 FreeMarker。
  • 如果在 Web Container 上部署,请记住将必要的依赖项标记为 provided
  • 如果作为嵌入式 Servlet 容器使用,Undertow 不支持 JSP。
  • 如果在 Web 容器中部署,@SpringBootApplication 注解类应继承 SpringBootServletInitializer 并提供必要的配置选项
  • 不能用 JSP 覆盖默认的 /error 页面。相反,需要提供自定义错误页面。

另外,你可以参考 《在 Spring Boot 中使用 JSP》 来了解如何在 Spring Boot 3 中使用 JSP。


Ref:https://www.baeldung.com/spring-boot-jsp