ZetCode

在 Spring Boot 中创建 PDF 报告

最后修改于 2023 年 8 月 2 日

在本文中,我们将展示如何在 Spring Boot Web 应用程序中提供 PDF 文件。 报告使用 iText 库生成。

openpdf 是一个用于在 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 报告。它使用 ResponseEntityInputStreamResource 将 PDF 数据发送到客户端。

build.gradle
...
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

这是项目结构。

build.gradle
plugins {
    id 'org.springframework.boot' version '3.1.1'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'java'
}

group = 'com.zetcode'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.github.librepdf:openpdf:1.3.30'
    runtimeOnly 'com.h2database:h2'
}

这是 Gradle 构建文件。

Spring Boot starters 是一组有用的依赖描述符,它们极大地简化了应用程序配置。 spring-boot-starter-web 是一个用于使用 Spring MVC 构建 Web 应用程序的启动器。它使用 Tomcat 作为默认的嵌入式容器。 spring-boot-starter-data-jpa 是一个用于将 Spring Data JPA 与 Hibernate 一起使用的启动器。

此外,我们包含了 H2 数据库和 openpdf 库的依赖项。

com/zetcode/model/City.java
package com.zetcode.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.util.Objects;


@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@Idspring.jpa.hibernate.ddl-auto 属性的默认值是 create-drop,这意味着 Hibernate 将从此实体创建表模式。

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

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

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

@Id 注解指定了实体的标识符,@GeneratedValue 提供了为主键值生成策略的规范。

resources/application.yml
spring:
    main:
      banner-mode: "off"
    sql:
      init:
        platform=h2
  
  logging:
    level:
      org:
        springframework: ERROR

application.yml 是主要的 Spring Boot 配置文件。使用 banner-mode 属性,我们关闭 Spring 横幅。 spring 框架日志记录设置为 ERROR。我们告知我们使用 H2 数据库。

resources/import.sql
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
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,我们为我们的数据存储库实现了一些方法,包括 findAllfindOne。这样我们就不必编写大量的样板代码。

com/zetcode/service/ICityService.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
package com.zetcode.service;

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

import java.util.List;

@Service
public class CityService implements ICityService {

    private final CityRepository repository;

    @Autowired
    public CityService(CityRepository repository) {
        this.repository = repository;
    }

    @Override
    public List<City> findAll() {

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

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

private final CityRepository repository;

@Autowired
public CityService(CityRepository repository) {
    this.repository = repository;
}

CityRepository 被注入。

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

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

com/zetcode/controller/MyController.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 {

    private final ICityService cityService;

    @Autowired
    public MyController(ICityService cityService) {
        this.cityService = 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 是它对流资源的实现。

private final ICityService cityService;

@Autowired
public MyController(ICityService cityService) {
    this.cityService = cityService;
}

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

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

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

ByteArrayInputStream bis = GeneratePdfReport.citiesReport(cities);

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

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

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

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

我们使用 ResponseEntity 创建响应。我们指定标头、内容类型和正文。内容类型为 MediaType.APPLICATION_PDF。正文是 InputStreamResource

com/zetcode/util/GeneratePdfReport.java
package com.zetcode.util;

import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.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 文件。

ByteArrayOutputStream out = new ByteArrayOutputStream();

数据将被写入到 ByteArrayOutputStream

PdfPTable table = new PdfPTable(3);

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

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 方法水平对齐。

PdfWriter.getInstance(document, out);

使用 PdfWriter,文档被写入到 ByteArrayOutputStream

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

表格被插入到 PDF 文档中。

document.close();

为了将数据写入到 ByteArrayOutputStream,必须关闭文档。

return new ByteArrayInputStream(out.toByteArray());

最后,数据作为 ByteArrayInputStream 返回。

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 应用程序。

$ ./gradlew bootRun

我们启动 Spring Boot 应用程序。

我们导航到 https://:8080/pdfreport 来生成报告。

在本文中,我们展示了如何将生成的 PDF 文件发送回客户端。 PDF 报告是使用 iText 生成的,数据来自 H2 数据库。我们使用 Spring Data JPA 来访问数据。

作者

我的名字是 Jan Bodnar,我是一位充满激情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直在撰写编程文章。迄今为止,我撰写了 1,400 多篇文章和 8 本电子书。我拥有超过十年的编程教学经验。

列出 所有 Spring Boot 教程