1. 概述

在构建 Web 应用时,JavaServer Pages (JSP) 是一种我们可以用于 HTML 页面模板化的机制。

另一方面,Spring Boot 是一个流行的框架,可用于快速启动 Web 应用。

本教程将介绍如何将 JSP 与 Spring Boot 结合使用来构建 Web 应用程序。

我们将首先了解如何设置应用以适应不同的部署场景,然后介绍一些常见的 JSP 使用方式,最后探讨打包应用时的多种选项。

⚠️ 注意:JSP 本身存在一定的局限性,尤其是在与 Spring Boot 结合使用时更为明显。因此,建议优先考虑 ThymeleafFreeMarker 等现代模板引擎作为替代方案。

2. Maven 依赖配置

接下来我们看看支持 Spring Boot 使用 JSP 所需的依赖项。

同时还会说明以独立应用方式运行和部署到 Web 容器之间的细微差别。

2.1. 作为独立应用程序运行

首先引入 spring-boot-starter-web 依赖:

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

这个依赖提供了运行 Spring Boot Web 应用所需的核心功能,并默认集成了嵌入式 Tomcat Servlet 容器。

更多关于配置其他嵌入式容器(如 Jetty、Undertow)的信息,请参考文章:Comparing Embedded Servlet Containers in Spring Boot

⚠️ 需要注意的是,Undertow 作为嵌入式容器时不支持 JSP。

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

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

虽然上述两个依赖可以手动管理,但通常建议让 Spring Boot 自动管理这些版本,我们只需维护 Spring Boot 的主版本即可。

可以通过使用 Spring Boot 父 POM(如 Spring Boot Tutorial – Bootstrap a Simple Application 中所示)或通过自定义依赖管理(如 Spring Boot Dependency Management With a Custom Parent 中所示)实现版本控制。

最后,需要引入 jstl 库,用于提供 JSP 页面中所需的 JSTL 标签支持:

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

2.2. 部署到 Web 容器(Tomcat)

部署到 Tomcat 时仍然需要上述依赖项。

但是,为了避免应用提供的依赖与 Tomcat 运行时提供的依赖发生冲突,需要将以下两个依赖设置为 provided 范围

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

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

注意,我们显式地定义了 spring-boot-starter-tomcat,并将其标记为 provided。这是因为它是 spring-boot-starter-web 的传递依赖,默认是 runtime 范围。

3. 视图解析器配置

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

需要在 application.properties 文件中配置两个属性,告知 Spring 在哪里查找这些 JSP 文件:

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

Maven 编译后会确保最终的 WAR 包中将上述 jsp 目录放置在 WEB-INF 目录内,供应用使用。

4. 启动类配置

是否作为独立应用运行,会影响我们的主启动类。

作为独立应用运行时,主类是一个简单的 @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. 渲染简单网页

JSP 页面依赖于 JavaServer Pages Standard Tag Library (JSTL) 来实现常见的模板功能,如条件判断、循环和格式化等,甚至提供了一些预定义函数。

我们创建一个简单的页面来展示应用中的书籍列表。

假设我们有一个 BookService 用于查询所有 Book 对象:

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

    //getters, setters, constructors and toString
}

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

我们可以编写一个 Spring MVC Controller 将其暴露为网页:

@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> 标签链接外部资源(如 CSS、JS),这些资源通常放在 ${project.basedir}/src/main/resources/static/ 目录下。
  • 如何使用 <c:forEach> 标签遍历由 BookController 提供的 books 模型属性。

6. 处理表单提交

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

BookController 需要提供两个接口:一个用于显示添加书籍的表单页面,另一个用于处理提交的数据:

public class BookController {

    //existing code

    @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 参数将 BookController 中的 book 属性绑定到表单上,从而在提交时自动填充数据。

由于使用了这个标签,我们需要单独定义表单的 action URL,因为不能在标签内部嵌套标签。同时,我们还使用 form:input 标签的 path 属性将每个输入字段绑定到 Book 对象的属性上。

更多关于 Spring MVC 表单处理的细节,请参阅文章:Getting Started With Forms in Spring MVC

7. 错误处理

由于 Spring Boot 与 JSP 结合使用时存在限制,我们无法通过自定义 error.html 来覆盖默认的 /error 映射。 相反,我们需要创建自定义错误页面来处理不同类型的错误。

7.1. 静态错误页面

如果我们想为不同的 HTTP 错误显示自定义静态错误页面,可以直接在 ${project.basedir}/src/main/resources/static/error/ 目录下放置对应的 HTML 文件。

例如,为所有 4xx 错误提供统一的错误页面,只需创建一个名为 4xx.html 的文件即可。

当应用抛出 4xx HTTP 错误时,Spring 会自动解析并返回该页面。

7.2. 动态错误页面

我们可以使用 @ControllerAdvice@ExceptionHandler 注解来自定义异常处理逻辑,并返回上下文相关的错误页面。

假设我们的应用定义了一个 DuplicateBookException 异常:

public class DuplicateBookException extends RuntimeException {
    private final Book book;

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

    // getter methods
}

同时,BookServiceImpl 类会在尝试添加相同 ISBN 的书籍时抛出该异常:

@Service
public class BookServiceImpl implements BookService {

    private final BookRepository bookRepository;

    // constructors, other override methods

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

    // conversion logic
}

然后我们定义 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;
    }
}

我们需要在 ${project.basedir}/src/main/webapp/WEB-INF/jsp/ 目录下创建 error-book.jsp 文件,这样上面的错误就会被正确解析。

8. 打包应用

如果我们要将应用部署到 Web 容器(如 Tomcat),打包方式很简单,直接使用 war 包即可。

但需要注意的是,如果使用 JSP 和 Spring Boot 并以内嵌容器方式运行,不能使用 jar 打包方式。因此,即使作为独立应用运行,也只能选择 war 打包。

无论哪种情况,我们的 pom.xml 都需要设置打包方式为 war

<packaging>war</packaging>

如果没有使用 Spring Boot 父 POM 来管理依赖,还需要引入 spring-boot-maven-plugin 插件,以确保生成的 war 文件可以作为独立应用运行

现在我们可以使用内嵌容器运行独立应用,或者直接将生成的 war 文件放入 Tomcat 中部署运行。

9. 总结

本教程涵盖了多个关键点,我们再回顾一下:

✅ JSP 本身有局限性,建议优先使用 Thymeleaf 或 FreeMarker。

✅ 如果部署到 Web 容器,记得将相关依赖设置为 provided

❌ Undertow 作为嵌入式容器时不支持 JSP。

✅ 若部署到 Web 容器,*@SpringBootApplication* 类应继承 SpringBootServletInitializer 并进行必要配置。

❌ 不能通过 JSP 覆盖默认的 /error 页面,需提供自定义错误页面。

❌ 使用 JSP 时不能使用 JAR 打包。

完整示例代码可从 GitHub 获取:Spring Boot JSP 示例代码


原始标题:Spring Boot With JavaServer Pages (JSP)