在 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 报告。它使用 ResponseEntity
和 InputStreamResource
将 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
这是项目结构。
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 库的依赖项。
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
和 @Id
。 spring.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
提供了为主键值生成策略的规范。
spring: main: banner-mode: "off" sql: init: platform=h2 logging: level: org: springframework: ERROR
application.yml
是主要的 Spring Boot 配置文件。使用 banner-mode
属性,我们关闭 Spring 横幅。 spring 框架日志记录设置为 ERROR。我们告知我们使用 H2 数据库。
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
文件以填充表数据。
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
。这样我们就不必编写大量的样板代码。
package com.zetcode.service; import com.zetcode.model.City; import java.util.List; public interface ICityService { List<City> findAll(); }
ICityService
提供了一个契约方法,用于从数据库中获取所有城市。
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
方法返回城市列表。
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
。
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
返回。
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 来访问数据。