# 在 Spring Boot 中创建 PDF 报告

> 原文： [http://zetcode.com/springboot/servepdf/](http://zetcode.com/springboot/servepdf/)

Spring Boot 创建 PDF 报告教程展示了如何在 Spring Boot Web 应用中提供 PDF 文件。 该报告是使用 iText 库生成的。

iText 是一个开放源代码库，用于在 Java 中创建和处理 PDF 文件。

Spring 是用于开发 Java 企业应用的 Java 应用框架。 它还有助于集成各种企业组件。 Spring Boot 使创建具有 Spring 动力的生产级应用和服务变得很容易，而对安装的要求却最低。

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

Spring Data JPA 是总括性 Spring Data 项目的一部分，该项目使实现基于 JPA 的存储库变得更加容易。 Spring Data JPA 使用 JPA 将数据存储在关系数据库中。 它可以在运行时从存储库接口自动创建存储库实现。

## Spring Boot 服务 PDF 示例

以下 Spring Boot 应用从数据库表中加载数据，并使用 iText 库从中生成 PDF 报告。 它使用`ResponseEntity`和`InputStreamResource`将 PDF 数据发送到客户端。

```java
pom.xml
src
├───main
│   ├───java
│   │   └───com
│   │       └───zetcode
│   │           │   Application.java
│   │           ├───controller
│   │           │       MyController.java
│   │           ├───model
│   │           │       City.java
│   │           ├───repository
│   │           │       CityRepository.java
│   │           ├───service
│   │           │       CityService.java
│   │           │       ICityService.java
│   │           └───util
│   │                   GeneratePdfReport.java
│   └───resources
│           application.yml
│           import.sql
└───test
    └───java

```

这是项目结构。

`pom.xml`

```java
<?xml version="1.0" encoding="UTF-8"?>
<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>springbootservepdf</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

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

    <dependencies>

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

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

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

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

    </dependencies>

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

</project>

```

这是 Maven 构建文件。

Spring Boot 启动器是一组有用的依赖项描述符，可大大简化 Maven 配置。 `spring-boot-starter-parent`具有 Spring Boot 应用的一些常用配置。 `spring-boot-starter-web`是使用 Spring MVC 构建 Web 应用的入门工具。 它使用 Tomcat 作为默认的嵌入式容器。 `spring-boot-starter-data-jpa`是将 Spring Data JPA 与 Hibernate 结合使用的入门工具。

此外，我们还包括 H2 数据库和 iText 库的依赖项。

`spring-boot-maven-plugin`在 Maven 中提供了 Spring Boot 支持，使我们可以打包可执行的 JAR 或 WAR 档案。 它的`spring-boot:run`目标运行 Spring Boot 应用。

`com/zetcode/model/City.java`

```java
package com.zetcode.model;

import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "cities")
public class City {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int population;

    public City() {
    }

    public City(String name, int population) {
        this.name = name;
        this.population = population;
    }

    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 getPopulation() {
        return population;
    }

    public void setPopulation(int population) {
        this.population = population;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 79 * hash + Objects.hashCode(this.id);
        hash = 79 * hash + Objects.hashCode(this.name);
        hash = 79 * hash + this.population;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final City other = (City) obj;
        if (this.population != other.population) {
            return false;
        }
        if (!Objects.equals(this.name, other.name)) {
            return false;
        }
        return Objects.equals(this.id, other.id);
    }

    @Override
    public String toString() {

        var builder = new StringBuilder();
        builder.append("City{id=").append(id).append(", name=")
                .append(name).append(", population=")
                .append(population).append("}");

        return builder.toString();
    }
}

```

这是`City`实体。 每个实体必须至少定义两个注解：`@Entity`和`@Id`。 `spring.jpa.hibernate.ddl-auto`属性的默认值为`create-drop`，这意味着 Hibernate 将根据该实体创建表架构。

```java
@Entity
@Table(name = "cities")
public class City {

```

`@Entity`注解指定该类是一个实体，并映射到数据库表。 `@Table`实体指定要用于映射的数据库表的名称。

```java
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

```

`@Id`注解指定实体的主键，`@GeneratedValue`提供规范主键值的生成策略。

`resources/application.yml`

```java
spring:
  main:
    banner-mode: "off"

logging:
  level:
    org:
      springframework: ERROR

```

`application.yml`是主要的 Spring Boot 配置文件。 使用`banner-mode`属性，我们可以关闭 Spring 横幅。 Spring 框架日志记录设置为`ERROR`。

`resources/import.sql`

```java
INSERT INTO cities(name, population) VALUES('Bratislava', 432000);
INSERT INTO cities(name, population) VALUES('Budapest', 1759000);
INSERT INTO cities(name, population) VALUES('Prague', 1280000);
INSERT INTO cities(name, population) VALUES('Warsaw', 1748000);
INSERT INTO cities(name, population) VALUES('Los Angeles', 3971000);
INSERT INTO cities(name, population) VALUES('New York', 8550000);
INSERT INTO cities(name, population) VALUES('Edinburgh', 464000);
INSERT INTO cities(name, population) VALUES('Suzhou', 4327066);
INSERT INTO cities(name, population) VALUES('Zhengzhou', 4122087);
INSERT INTO cities(name, population) VALUES('Berlin', 3671000);
INSERT INTO cities(name, population) VALUES('Brest', 139163);
INSERT INTO cities(name, population) VALUES('Bucharest', 1836000);

```

模式是由 Hibernate 自动创建的。 之后，将执行`import.sql`文件以将数据填充到表中。

`com/zetcode/repository/CityRepository.java`

```java
package com.zetcode.repository;

import com.zetcode.model.City;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CityRepository extends CrudRepository<City, Long> {

}

```

通过从 Spring `CrudRepository`扩展，我们为数据库实现了一些方法，包括`findAll()`和`findOne()`。 这样，我们不必编写很多样板代码。

`com/zetcode/service/ICityService.java`

```java
package com.zetcode.service;

import com.zetcode.model.City;
import java.util.List;

public interface ICityService {

    List<City> findAll();
}        

```

`ICityService`提供了一种从数据库中获取所有城市的契约方法。

`com/zetcode/service/CityService.java`

```java
package com.zetcode.service;

import com.zetcode.model.City;
import com.zetcode.repository.CityRepository;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CityService implements ICityService {

    @Autowired
    private CityRepository repository;

    @Override
    public List<City> findAll() {

        return (List<City>) repository.findAll();
    }
}

```

`CityService`包含`findAll()`方法的实现。 我们使用存储库从数据库检索数据。

```java
@Autowired
private CityRepository repository;

```

注入`CityRepository`。

```java
return (List<City>) repository.findAll();

```

存储库的`findAll()`方法返回城市列表。

`com/zetcode/controller/MyController.java`

```java
package com.zetcode.controller;

import com.zetcode.model.City;
import com.zetcode.service.ICityService;
import com.zetcode.util.GeneratePdfReport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.io.ByteArrayInputStream;
import java.util.List;

@Controller
public class MyController {

    @Autowired
    private ICityService cityService;

    @RequestMapping(value = "/pdfreport", method = RequestMethod.GET,
            produces = MediaType.APPLICATION_PDF_VALUE)
    public ResponseEntity<InputStreamResource> citiesReport() {

        var cities = (List<City>) cityService.findAll();

        ByteArrayInputStream bis = GeneratePdfReport.citiesReport(cities);

        var headers = new HttpHeaders();
        headers.add("Content-Disposition", "inline; filename=citiesreport.pdf");

        return ResponseEntity
                .ok()
                .headers(headers)
                .contentType(MediaType.APPLICATION_PDF)
                .body(new InputStreamResource(bis));
    }
}

```

`citiesReport()`方法返回生成的 PDF 报告。 `Resource`接口抽象了对低级资源的访问； `InputStreamResource`是它对流资源的实现。

```java
@Autowired
private ICityService cityService;

```

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

```java
var cities = (List<City>) cityService.findAll();

```

我们使用`findAll()`方法查找所有城市。

```java
ByteArrayInputStream bis = GeneratePdfReport.citiesReport(cities);

```

`GeneratePdfReport.citiesReport()`使用 iText 库从城市列表中生成 PDF 文件。

```java
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "inline; filename=citiesreport.pdf");

```

通过将`Content-Disposition`设置为`inline`，PDF 文件将直接显示在浏览器中。

```java
return ResponseEntity
        .ok()
        .headers(headers)
        .contentType(MediaType.APPLICATION_PDF)
        .body(new InputStreamResource(bis));

```

我们使用`ResponseEntity`创建响应。 我们指定标题，内容类型和主体。 内容类型为`MediaType.APPLICATION_PDF`。 身体是`InputStreamResource`。

`com/zetcode/util/GeneratePdfReport.java`

```java
package com.zetcode.util;

import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
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.model.City;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.List;

public class GeneratePdfReport {

    private static final Logger logger = LoggerFactory.getLogger(GeneratePdfReport.class);

    public static ByteArrayInputStream citiesReport(List<City> cities) {

        Document document = new Document();
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {

            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("Population", headFont));
            hcell.setHorizontalAlignment(Element.ALIGN_CENTER);
            table.addCell(hcell);

            for (City city : cities) {

                PdfPCell cell;

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

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

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

            PdfWriter.getInstance(document, out);
            document.open();
            document.add(table);

            document.close();

        } catch (DocumentException ex) {

            logger.error("Error occurred: {0}", ex);
        }

        return new ByteArrayInputStream(out.toByteArray());
    }
}

```

`GeneratePdfReport`根据提供的数据创建 PDF 文件。

```java
ByteArrayOutputStream out = new ByteArrayOutputStream();

```

数据将被写入`ByteArrayOutputStream`。

```java
PdfPTable table = new PdfPTable(3);

```

我们将数据放在表格中； 为此，我们有`PdfPTable`类。 该表具有三列：ID，名称和人口。

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

```

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

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

```

数据放置在表单元格内，由`PdfPCell`表示。 使用`setHorizontalAlignment()`方法将文本水平对齐。

```java
PdfWriter.getInstance(document, out);

```

使用`PdfWriter`，将文档写入`ByteArrayOutputStream`。

```java
document.open();
document.add(table);

```

该表将插入到 PDF 文档中。

```java
document.close();

```

为了将数据写入`ByteArrayOutputStream`，必须关闭文档。

```java
return new ByteArrayInputStream(out.toByteArray());

```

最后，数据返回为`ByteArrayInputStream`。

`com/zetcode/Application.java`

```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 应用。

```java
$ mvn spring-boot:run

```

我们启动 Spring Boot 应用。

我们导航到`http://localhost:8080/pdfreport`以生成报告。

在本教程中，我们展示了如何将生成的 PDF 文件发送回客户端。 PDF 报告是使用 iText 生成的，数据来自 H2 数据库。 我们使用 Spring Data JPA 访问数据。 您可能也对这些相关教程感兴趣： [Spring Boot 基本注解](/springboot/annotations/)， [Spring Boot H2 教程](/articles/springbooth2/)， [Spring Boot JasperReports Web 集成](/articles/jasperspringbootweb/)， [Java 教程](/lang/java/)或列出[所有 Spring Boot 教程](/all/#springboot)。