ZetCode

Spring Boot iText

最后修改于 2020 年 7 月 13 日

在本教程中,我们将介绍如何使用 iText 和 Spring Boot 创建 PDF 报告。数据将从 H2 内存数据库中的表中加载。

iText 是一个用于在 Java 中创建和操作 PDF 文件的开源库。

Spring 是一个用于开发 Java 企业应用程序的 Java 应用程序框架。它还有助于集成各种企业组件。Spring Boot 使创建具有最少设置要求的 Spring 驱动的生产级应用程序和服务变得容易。

H2 是一个完全用 Java 实现的开源关系数据库管理系统。它可以嵌入到 Java 应用程序中,也可以以客户端-服务器模式运行。它占用的空间小,易于部署和安装。它包含一个基于浏览器的控制台应用程序,用于查看和编辑数据库表。

JdbcTemplate 是一个 Spring 库,它帮助程序员创建与关系数据库和 JDBC 协同工作的应用程序。它负责许多乏味且容易出错的低级细节,例如处理事务、清理资源以及正确处理异常。JdbcTemplate 包含在 Spring 的 spring-jdbc 模块中。

应用程序

以下 Spring Boot 应用程序从数据库表中加载数据,并使用 iText 库从此生成 PDF 报告。该应用程序在嵌入式 Tomcat 服务器上运行。

├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── zetcode
    │   │           ├── Application.java
    │   │           ├── bean
    │   │           │   └── Car.java
    │   │           ├── conf
    │   │           │   └── AppConfig.java
    │   │           ├── controller
    │   │           │   └── MyController.java
    │   │           ├── service
    │   │           │   ├── CarService.java
    │   │           │   └── ICarService.java
    │   │           └── view
    │   │               ├── AbstractPdfView.java
    │   │               └── MyPdfView.java
    │   └── resources
    │       ├── application.yml
    │       ├── data-h2.sql
    │       ├── schema-h2.sql
    │       └── static
    │           └── index.html
    └── test
        └── java

这是项目结构。

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.zetcode</groupId>
    <artifactId>SpringBootItext</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.lowagie</groupId>
            <artifactId>itext</artifactId>
            <version>4.2.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Maven pom.xml 文件包含 iText 库、H2 驱动程序和 Spring 框架的依赖项。

com/zetcode/Car.java
package com.zetcode.bean;

public class Car {

    private Long id;
    private String name;
    private int price;

    public Car() {}

    public Car(Long id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Car{" + "id=" + id + ", name=" + name + ", price=" + price + '}';
    }
}

这是 Car bean 类。它包含项目 ID、名称和价格。

application.yml
datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:
    driverClassName: org.h2.Driver

spring:
    datasource:
        platform: h2
    h2:
        console:
            enabled: true
            path: /console/

application.yml 是主要的 Spring Boot 配置文件。它包含数据源和 MVC 设置。我们选择了 H2 作为数据库系统。数据库在内存中运行。我们启用了基于浏览器的控制台应用程序。

url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE

数据库名称为 testdb,它是在内存中创建的。(Spring Boot 在 Maven POM 文件中检测到 H2 时会自动创建一个内存数据库,但我们也会展示如何显式创建。)

spring:
    datasource:
        platform: h2

platform 值用于 SQL 初始化脚本:schema-${platform}.sqldata-${platform}.sql

h2:
    console:
        enabled: true
        path: /console/

H2 Web 控制台应用程序已启用;它可以通过 localhost:8080/console/ 路径访问。

schema-h2.sql
CREATE TABLE Cars(ID BIGINT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(30), PRICE INT);

此 SQL 脚本创建 Cars 表。

data-h2.sql
INSERT INTO Cars(Name, Price) VALUES('Audi', 52642);
INSERT INTO Cars(Name, Price) VALUES('Mercedes', 57127);
INSERT INTO Cars(Name, Price) VALUES('Skoda', 9000);
INSERT INTO Cars(Name, Price) VALUES('Volvo', 29000);
INSERT INTO Cars(Name, Price) VALUES('Bentley', 350000);
INSERT INTO Cars(Name, Price) VALUES('Citroen', 21000);
INSERT INTO Cars(Name, Price) VALUES('Hummer', 41400);
INSERT INTO Cars(Name, Price) VALUES('Volkswagen', 21600);

此脚本用数据填充表。这两个脚本都位于类路径的根目录。

com/zetcode/AppConfig.java
package com.zetcode.conf;

import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class AppConfig {

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "datasource")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

AppConfig 是一个 Java 配置类。它从 application.yml 配置文件创建数据源 bean。

com/zetcode/AbstractPdfView.java
package com.zetcode.view;

import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.pdf.PdfWriter;
import java.io.ByteArrayOutputStream;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.view.AbstractView;

public abstract class AbstractPdfView extends AbstractView {

    public AbstractPdfView() {

        initView();
    }

    private void initView() {

        setContentType("application/pdf");
    }

    @Override
    protected boolean generatesDownloadContent() {
        return true;
    }

    @Override
    protected final void renderMergedOutputModel(Map<String, Object> model,
            HttpServletRequest request, HttpServletResponse response) throws Exception {

        ByteArrayOutputStream baos = createTemporaryOutputStream();

        Document document = new Document(PageSize.A4);
        PdfWriter writer = PdfWriter.getInstance(document, baos);
        prepareWriter(model, writer, request);
        buildPdfMetadata(model, document, request);

        document.open();
        buildPdfDocument(model, document, writer, request, response);
        document.close();

        writeToResponse(response, baos);
    }

    protected void prepareWriter(Map<String, Object> model, PdfWriter writer,
            HttpServletRequest request) throws DocumentException {
        writer.setViewerPreferences(getViewerPreferences());
    }

    protected int getViewerPreferences() {
        return PdfWriter.ALLOW_PRINTING | PdfWriter.PageLayoutSinglePage;
    }

    protected void buildPdfMetadata(Map<String, Object> model, Document document,
            HttpServletRequest request) {
    }

    protected abstract void buildPdfDocument(Map<String, Object> model,
            Document document, PdfWriter writer, HttpServletRequest request,
            HttpServletResponse response) throws Exception;
}

Spring 的 AbstractPdfView 基于旧的 iText 库。因此,我们需要创建自己的抽象类,它基本上是一个带有更新导入的副本。

com/zetcode/MyPdfView.java
package com.zetcode.view;

import com.itextpdf.text.Document;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.FontFactory;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;
import com.zetcode.bean.Car;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyPdfView extends AbstractPdfView {

    @Override
    protected void buildPdfDocument(Map<String, Object> model,
            Document document, PdfWriter writer, HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        List<Car> cars = (List<Car>) model.get("cars");

        PdfPTable table = new PdfPTable(3);
        table.setWidthPercentage(60);
        table.setWidths(new int[] {1, 3, 3});

        Font headFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD);

        PdfPCell hcell;
        hcell = new PdfPCell(new Phrase("Id", headFont));
        hcell.setHorizontalAlignment(Element.ALIGN_CENTER);
        table.addCell(hcell);

        hcell = new PdfPCell(new Phrase("Name", headFont));
        hcell.setHorizontalAlignment(Element.ALIGN_CENTER);
        table.addCell(hcell);

        hcell = new PdfPCell(new Phrase("Price", headFont));
        hcell.setHorizontalAlignment(Element.ALIGN_CENTER);
        table.addCell(hcell);

        for (Car car : cars) {

            PdfPCell cell;

            cell = new PdfPCell(new Phrase(car.getId().toString()));
            cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
            cell.setHorizontalAlignment(Element.ALIGN_CENTER);
            table.addCell(cell);

            cell = new PdfPCell(new Phrase(car.getName()));
            cell.setPaddingLeft(5);
            cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
            cell.setHorizontalAlignment(Element.ALIGN_LEFT);
            table.addCell(cell);

            cell = new PdfPCell(new Phrase(String.valueOf(car.getPrice())));
            cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
            cell.setHorizontalAlignment(Element.ALIGN_RIGHT);
            cell.setPaddingRight(5);
            table.addCell(cell);
        }

        document.add(table);
    }
}

MyPdfView 继承自自定义的 AbstractPdfView。在 buildPdfDocument 方法中,我们构建 PDF 文件。

List<Car> cars = (List<Car>) model.get("cars");

首先,我们从模型中获取数据。

PdfPTable table = new PdfPTable(3);

我们将把数据放在一个表中;为此,我们有 PdfPTable 类。该表有三列:Id、Name 和 Price。

Font headFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD);

对于表头,我们使用粗体 Helvetica 字体。

PdfPCell hcell;
hcell = new PdfPCell(new Phrase("Id", headFont));
hcell.setHorizontalAlignment(Element.ALIGN_CENTER);
table.addCell(hcell);

数据放置在表单元格中,由 PdfPCell 表示。文本使用 setHorizontalAlignment 方法进行水平对齐。

document.add(table);

最后,将表插入 PDF 文档。

com/zetcode/MyController.java
package com.zetcode.controller;

import com.zetcode.bean.Car;
import com.zetcode.service.ICarService;
import com.zetcode.view.MyPdfView;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MyController {

    @Autowired
    private ICarService carService;

    @RequestMapping(path = "/report", method = RequestMethod.GET)
    public ModelAndView report() {

        Map<String, Object> model = new HashMap<>();

        List<Car> cars = carService.findAll();
        model.put("cars", cars);

        return new ModelAndView(new MyPdfView(), model);
    }
}

MyController 中,我们有一个映射。

@Autowired
private ICarService carService;

我们将 CarService 对象注入到属性中。该服务对象用于从数据库检索数据。

@RequestMapping(path = "/report", method = RequestMethod.GET)
public ModelAndView report() {

    Map<String, Object> model = new HashMap<>();

    List<Car> cars = carService.findAll();
    model.put("cars", cars);

    return new ModelAndView(new MyPdfView(), model);
}

report 方法中,我们使用 findAll 方法查找所有汽车。我们将自定义的 MyPdfView 返回给客户端。

com/zetcode/ICarService.java
package com.zetcode.service;

import com.zetcode.bean.Car;
import java.util.List;

public interface ICarService {

    public List<Car> findAll();
}

ICarService 提供了一个契约方法,用于从数据源获取所有汽车。

com/zetcode/CarService.java
package com.zetcode.service;

import com.zetcode.bean.Car;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class CarService implements ICarService {

    @Autowired
    private JdbcTemplate jtm;

    @Override
    public List<Car> findAll() {

        String sql = "SELECT * FROM Cars";

        List<Car> cars = jtm.query(sql, new BeanPropertyRowMapper(Car.class));

        return cars;
    }
}

CarService 包含 findAll 方法的实现。我们借助 JdbcTemplateCars 表中检索所有汽车。

@Autowired
private JdbcTemplate jtm;

JdbcTemplate 已注入。

String sql = "SELECT * FROM Cars";

这是要执行的 SQL。我们从 Cars 表中选择所有汽车。

List<Car> cars = jtm.query(sql, new BeanPropertyRowMapper(Car.class));

BeanPropertyRowMapper 将一行转换为指定映射目标类的新实例。

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Home Page</title>
    </head>
    <body>
        <a href="/report.html">Generate report</a>
    </body>
</html>

index.html 文件包含一个用于生成 PDF 报告的链接。静态文件从预定义的目录提供;其中之一是 static,位于 src/main/resources

com/zetcode/Application.java
package com.zetcode;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

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

`Application` 设置了 Spring Boot 应用程序。

$ mvn spring-boot:run

我们启动 Spring Boot 应用程序。

导航到 https://:8080/ 来测试应用程序。H2 控制台应用程序可通过 https://:8080/console/ 访问。控制台应用程序的 JDBC URL 为 jdbc:h2:mem:testdb。密码为空。

在本教程中,我们使用 iText 从 H2 数据库中的表创建了 PDF 报告。该应用程序使用了 Spring Boot 框架,并在 Web 环境中运行。