如何利用Jaspersoft在Spring Boot应用中生成报告
我们在日常活动中使用报表。我们在购买产品、服务或管理交易时可能会用到它们。这包括摘要报告、财务报告、库存报告和预算报告。
这些报告有助于对一个组织产生洞察力,并根据从中产生的知识做出正确的决定。例如,库存报告有助于决定适当的再订购水平。
在本教程中,读者将学习如何设计库存报告以及如何将其集成到Spring Boot应用程序中。读者将使用自定义产品来生成一个标准报告,供企业使用。
先决条件
读者需要了解以下内容
- [Spring Boot]方面的知识。
- [Thymeleaf]方面的知识。
- [JDK 11以上]
- 在您的计算机上安装[Intellij IDEA]。
- 在你的电脑上安装[Jaspersoft Studio]。
- 一个[MySQL连接器]jar文件。
创建一个Spring Boot应用程序
在你的浏览器上,进入spring initializr并创建一个新的应用程序。添加依赖项。Spring Web,MySQL Driver,Thymeleaf,Lombok, 和Spring Data JPA 。

将压缩文件解压到所需的文件夹中,在Intellij中导入应用程序。Maven会从网上下载所有的依赖项。
添加数据库配置属性
在application.properties 文件中添加以下属性。主要属性包括数据库URL、用户名和密码。
#Database connection properties
spring.datasource.url=jdbc:mysql://localhost:3306/jaspersoftdb
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto 属性影响到数据库表的创建和管理方式。值create-drop 告诉Spring在启动时创建表,但在应用程序终止时放弃它们。spring.jpa.show-sql 属性会打印由Hibernate生成的查询,这样我们就能知道何时发生错误。
spring.jpa.properties.hibernate.format_sql 属性对 SQL 语句进行了格式化,以利于阅读。spring.jpa.properties.hibernate.dialect 属性告诉应用程序要使用哪种SQL方言。
创建一个产品模型
创建一个名为Product 的类,其字段为name,description,productType,price, 和createdAt 。其中price 是一个BigDecimal ,productType 是一个枚举,createdAt 是一个LocalDate 。
public enum ProductType {
PHONE,
COMPUTER
}
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
@Table(name = "product")
@Data
@NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "description")
private String description;
@Column(name = "product_type")
@Enumerated(EnumType.STRING)
private ProductType productType;
@Column(name = "price")
private BigDecimal price;
@Column(name = "created_at")
private LocalDate createdAt;
public Product(String name,
String description,
ProductType productType,
BigDecimal price,
LocalDate createdAt) {
this.name = name;
this.description = description;
this.productType = productType;
this.price = price;
this.createdAt = createdAt;
}
}
@Data 注解产生了getter和setter方法。这确保我们可以在Thymeleaf页面上显示产品列表。@NoArgsConstructor 生成了一个空的构造函数。
创建一个产品库,按日期查找所有产品
创建一个名为ProductRepository 的接口,它扩展了JpaRepository 。然后,在接口中创建一个方法,返回一个按日期查询的产品集合。
import com.reports.jaspersoft.jasperreports.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
public interface ProductRepository extends JpaRepository<Product,Long> {
List<Product> findAllByCreatedAt(LocalDate localDate);
}
创建一个报告服务,按类型过滤产品
创建一个名为ReportService 的接口并添加两个方法。第一个方法返回报告链接,给定日期和文件格式。另一个方法从数据库中返回一个产品集合。
JRException 来自net.sf.jasperreports 库,该库将在以后的应用程序中添加。当用数据库中的数据编译或填充报告时出现错误,这个类将被抛出。
import com.reports.jaspersoft.jasperreports.model.Product;
import net.sf.jasperreports.engine.JRException;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
public interface ReportService {
String generateReport(LocalDate localDate, String fileFormat) throws JRException, IOException;
List<Product> findAllProducts();
}
使用Jaspersoft模板设计一个报告
让我们打开JasperSoft studio。在我们创建报告的设计之前,我们将把MySQL连接器添加到classpath中。这将确保我们能够连接到我们的数据库并从我们的表中生成所需的字段。
在Jaspersoft studio中,点击帮助,安装新软件,管理。一旦你点击管理,一个名为偏好的新面板就会打开。使用面板提供的搜索字段来搜索java。搜索将返回不同的结果,但在我们的例子中,点击构建路径。
在构建路径下,点击classpath变量。classpath变量为我们提供了使用新建、编辑和删除按钮添加、编辑和删除变量的能力。点击新建,并在打开的窗口中添加jar文件,其名称如下图所示。

添加MySQL连接器并运行spring boot应用程序。spring boot应用程序将生成Jaspersoft studio所需的表。从现有的模板中为我们的报告创建一个设计。在Jaspersoft的工具栏上,点击文件,新建,Jasper报告。
一个窗口打开,里面有一个模板列表可供选择。在本教程中,选择名为simple blue的模板并点击下一步。使用打开的窗口添加一个新的.jrxml 文件。这是Jaspersoft报告文件的一个扩展。对于我们的情况,将文件命名为products.jrxml ,然后点击下一步。
有些情况下,Jaspersoft studio仅通过使用上述步骤添加MySQL连接器而不能连接到数据库。ClassNotFoundException ,这是由于软件无法找到MySQL驱动类造成的。为了解决这个错误,我们应该在下一个窗口提供的驱动程序classpath菜单中添加MySQL连接器。
接下来打开的窗口要求我们提供我们的数据源。数据源是我们的数据库URL、用户名和密码,以连接到我们的数据库。点击新建,在数据适配器窗口中选择数据库JDBC连接并点击下一步。然后打开一个窗口,我们需要填写数据库连接属性,如下图所示。

连接并在下一个窗口中写一个查询,选择产品表的所有字段并点击下一步。

出现的新窗口显示我们数据库表中的所有字段。添加所有我们希望出现在报告中的字段。从左边的数据字段添加到右边的字段部分。你可以使用带有大于符号的按钮来做这件事。

点击 "完成 "按钮,生成最终报告。该报告有我们来自产品实体的字段,如下图所示。必要时编辑设计标题上的标题和描述。

将该设计添加到我们的spring boot应用程序中
我们创建的设计会生成描述报告结构的XML。通过点击Jaspersoft设计窗口底部的源按钮,获得它的代码。

删除源文件中字段标签中的属性。确保字段标签的类和名称属性与我们产品模型的属性相同。

productType 是一个枚举,当我们试图把它铸成一个字符串时,它会抛出一个ClassCastException 。为了避免这种情况,在产品类中添加一个getter方法,返回该枚举的字符串值。
public String getProductType() {
return String.valueOf(productType);
}
另外,确保细节部分的文本字段表达式与我们的产品字段的名称相同。

在我们的Spring Boot应用程序中复制整个源代码。将文件命名为products.jrxml ,放在资源包中。
在我们的Spring Boot应用程序中添加Jaspersoft依赖项
Jaspersoft依赖性将帮助我们添加功能。这些功能包括加载、编译、填充和生成文档。在pom.xml 文件中添加该依赖。Maven将为我们下载它,并将其添加到classpath中,我们可以调用其方法。
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>6.18.1</version>
</dependency>
加载并编译报告服务中的Jaspersoft设计源文件
创建一个名为ReportServiceImpl 的类,实现ReportService 接口。实现generateReport() 和findAllProducts() 方法。getJasperPrint() 方法处理编译jasper报告文件的功能。
这个方法接受我们用来填充报告的数据集合和一个字符串。这个字符串表示Jasper报告源文件的位置。当运行时,该函数返回一个JasperPrint 的实例。
import com.reports.jaspersoft.jasperreports.model.Product;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ReportServiceImpl implements ReportService{
private JasperPrint getJasperPrint(List<Product> phoneCollection, String resourceLocation) throws FileNotFoundException, JRException {
File file = ResourceUtils.getFile(resourceLocation);
JasperReport jasperReport = JasperCompileManager
.compileReport(file.getAbsolutePath());
JRBeanCollectionDataSource dataSource = new
JRBeanCollectionDataSource(phoneCollection);
Map<String, Object> parameters = new HashMap<>();
parameters.put("createdBy","David");
JasperPrint jasperPrint = JasperFillManager
.fillReport(jasperReport,parameters,dataSource);
return jasperPrint;
}
@Override
public String generateReport(LocalDate localDate, String fileFormat){
return null;
}
@Override
public List<Product> findAllProducts() {
return null;
}
}
创建一个文件夹来存储报告
在ReportServiceImpl 类中创建一个名为getUploadPath() 的方法。添加文件格式,从上述方法返回的JasperPrint ,以及文件名作为参数。如果指定的目录不存在,该方法将创建该目录。它还在文件夹中创建生成的PDF文件,文件名传递给它。
我们还将添加一个名为getPdfFileLink() 的方法,返回到我们生成的报告的链接。这个方法将把getUploadPath() 方法返回的文件路径作为一个字符串参数。我们将在目前返回null的generateReport() 方法中使用这两个方法getJasperPrint() 和getUploadPath() 来生成我们的报告。
import com.reports.jaspersoft.jasperreports.model.Product;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ReportServiceImpl implements ReportService{
private JasperPrint getJasperPrint(List<Product> phoneCollection, String resourceLocation) throws FileNotFoundException, JRException {
File file = ResourceUtils.getFile(resourceLocation);
JasperReport jasperReport = JasperCompileManager
.compileReport(file.getAbsolutePath());
JRBeanCollectionDataSource dataSource = new
JRBeanCollectionDataSource(phoneCollection);
Map<String, Object> parameters = new HashMap<>();
parameters.put("createdBy","David");
JasperPrint jasperPrint = JasperFillManager
.fillReport(jasperReport,parameters,dataSource);
return jasperPrint;
}
private Path getUploadPath(String fileFormat, JasperPrint jasperPrint, String fileName) throws IOException, JRException {
String uploadDir = StringUtils.cleanPath("./generated-reports");
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)){
Files.createDirectories(uploadPath);
}
//generate the report and save it in the just created folder
if (fileFormat.equalsIgnoreCase("pdf")){
JasperExportManager.exportReportToPdfFile(
jasperPrint, uploadPath+fileName
);
}
return uploadPath;
}
private String getPdfFileLink(String uploadPath){
return uploadPath+"/"+"products.pdf";
}
@Override
public String generateReport(LocalDate localDate, String fileFormat){
return null;
}
@Override
public List<Product> findAllProducts() {
return null;
}
}
创建一个资源处理程序来显示生成的PDF
创建一个名为AppWebConfig 的类,实现MvcConfigurer ,并重写addResourceHandlers() 方法。创建一个名为uploadPath() 的方法,返回绝对路径。该应用程序应该允许使用字符串/generated-reports/** 来请求我们的PDF链接。将该方法返回的绝对路径传递给addResourceLocations() 方法。
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.file.Path;
import java.nio.file.Paths;
@Configuration
public class AppWebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String reportPath = uploadPath("./generated-reports");
registry.addResourceHandler("/generated-reports/**")
.addResourceLocations("file:/"+reportPath+"/");
}
private String uploadPath(String directory){
Path uploadDirPath = Paths.get(directory);
return uploadDirPath.toFile().getAbsolutePath();
}
}
生成一个带有过滤产品的报告
我们使用ReportServiceImpl 类中的generateReport() 方法实现这一功能。这个方法接受两个参数,类型为LocalDate 和String 。要实现它,首先要注入ProductsRepository Bean。我们将使用它来搜索在该特定日期创建的产品。创建一个包含我们源文件的资源位置的字符串,即products.jrxml 。
然后调用getJasperPrint() 方法并传递产品的集合。创建一个字符串,包含要生成的PDF报告的名称,即:products.pdf 。接下来,用generateReport() 方法提供的fileFormat 参数调用getUploadPath() 方法。
现在,从getFilePdfLink() 方法返回值,将调用getUploadPath() 所返回的值作为一个字符串传递。这个方法返回一个包含我们生成的PDF文件的路径的链接到我们的控制器。
import com.reports.jaspersoft.jasperreports.model.Product;
import com.reports.jaspersoft.jasperreports.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class ReportServiceImpl implements ReportService{
private final ProductRepository productRepository;
private JasperPrint getJasperPrint(List<Product> phoneCollection, String resourceLocation) throws FileNotFoundException, JRException {
File file = ResourceUtils.getFile(resourceLocation);
JasperReport jasperReport = JasperCompileManager
.compileReport(file.getAbsolutePath());
JRBeanCollectionDataSource dataSource = new
JRBeanCollectionDataSource(phoneCollection);
Map<String, Object> parameters = new HashMap<>();
parameters.put("createdBy","David");
JasperPrint jasperPrint = JasperFillManager
.fillReport(jasperReport,parameters,dataSource);
return jasperPrint;
}
private Path getUploadPath(String fileFormat, JasperPrint jasperPrint, String fileName) throws IOException, JRException {
String uploadDir = StringUtils.cleanPath("./generated-reports");
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)){
Files.createDirectories(uploadPath);
}
//generate the report and save it in the just created folder
if (fileFormat.equalsIgnoreCase("pdf")){
JasperExportManager.exportReportToPdfFile(
jasperPrint, uploadPath+fileName
);
}
return uploadPath;
}
private String getPdfFileLink(String uploadPath){
return uploadPath+"/"+"products.pdf";
}
@Override
public String generateReport(LocalDate localDate, String fileFormat) throws JRException, IOException {
List<Product> phoneCollection = productRepository.findAllByCreatedAt(localDate);
//load the file and compile it
String resourceLocation = "classpath:products.jrxml";
JasperPrint jasperPrint = getJasperPrint(phoneCollection,resourceLocation);
//create a folder to store the report
String fileName = "/"+"products.pdf";
Path uploadPath = getUploadPath(fileFormat, jasperPrint, fileName);
//create a private method that returns the link to the specific pdf file
return getPdfFileLink(uploadPath.toString());
}
@Override
public List<Product> findAllProducts() {
return productRepository.findAll();
}
}
我们的产品页面将显示数据库中的产品列表。确保findAllProducts() 方法调用findAll() 方法,而不是返回null 。
创建一个报告控制器
允许任何对根路径的请求,并使用一个products 的模型遍历产品列表。设置这个模型的值为findAllProducts() 方法的返回值,ReportService 。
import com.reports.jaspersoft.jasperreports.service.ReportService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class ReportController {
private final ReportService reportService;
@GetMapping("/")
public String showProducts(Model model){
model.addAttribute("products",reportService.findAllProducts());
return "products";
}
}
创建一个带有日期和文件类型字段的产品页面
产品页面包含两个部分。一个有一个表格,我们在这里输入日期并选择要生成的文件类型(在我们的例子中是)PDF文件。这两个值是请求参数。充分利用th:name 属性,如下面表格的输入和选项标签所示。
<!-- products.html -->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Products</title>
<link th:href="@{products.css}" rel="stylesheet">
</head>
<body>
<section class="products-form-section">
<div class="container">
<div class="card">
<form th:action="@{/report}" method="post" class="form-control">
<div class="date-control">
<input type="date" th:name="date">
</div>
<div class="file-type-control">
<select th:name="fileFormat">
<option th:value="pdf" th:text="PDF"></option>
</select>
</div>
<div class="generate-btn">
<input type="submit" class="btn-primary" value="generate report">
</div>
</form>
</div>
</div>
</section>
<section class="products-section">
<div class="container">
<div class="card" th:each="product : ${products}">
<p th:text="${product.id}"></p>
<p th:text="${product.name}"></p>
<p th:text="${product.description}"></p>
<p th:text="${product.productType}"></p>
<p th:text="${product.price}"></p>
<p th:text="${product.createdAt}"></p>
</div>
</div>
</section>
</body>
</html>
/*products.css*/
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
body{
background-color: darkslategray;
}
.container{
padding: 20px;
margin: 20px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
}
.card{
width: 100%;
padding: 20px;
margin: 20px;
border-radius: 10px;
border: 1px solid lightslategray;
background-color: white;
}
.products-section .card{
background-color: black;
color: white;
}
.products-section .card:hover{
background-color: lightblue;
color: black;
}
.products-section .card{
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
}
.form-control{
display: flex;
flex-direction: row;
justify-content: space-between;
}
.btn-primary{
padding: 10px;
background-color: green;
color: white;
border-radius: 10px;
}
.btn-primary:hover{
cursor: pointer;
}
添加一个职位映射
当我们按下上述表单的生成报告按钮时,一个帖子请求会进入/report 路径。这个URL与控制器的generateReport() 方法相对应。@RequestParam 注解从请求中获取日期和文件格式。
它将这些值传递给我们的generateReport() 方法,ReportService 。返回一个字符串,包含我们前面讨论的生成PDF的路径。然后我们通过在控制器中重定向到该路径发出一个新的请求。这个重定向允许我们在浏览器中显示报告。注意,资源处理程序必须是可用的。
import com.reports.jaspersoft.jasperreports.service.ReportService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class ReportController {
private final ReportService reportService;
@GetMapping("/")
public String showProducts(Model model){
model.addAttribute("products",reportService.findAllProducts());
return "products";
}
@PostMapping("/report")
public String generateReport(@RequestParam("date") String date,
@RequestParam("fileFormat") String fileFormat) throws JRException, IOException {
LocalDate localDate = LocalDate.parse(date);
String fileLink = reportService.generateReport(localDate, fileFormat);
return "redirect:/"+fileLink;
}
}
测试应用程序
我们需要用假的数据来填充我们的产品表,以便测试。添加一个CommandLineRunner ,其中有一个产品列表,以便在我们的应用程序启动时创建。
import com.reports.jaspersoft.jasperreports.model.Product;
import com.reports.jaspersoft.jasperreports.model.ProductType;
import com.reports.jaspersoft.jasperreports.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@SpringBootApplication
public class JasperReportsApplication {
public static void main(String[] args) {
SpringApplication.run(JasperReportsApplication.class, args);
}
@Bean
public CommandLineRunner commandLineRunner(ProductRepository productRepository){
return args -> {
List<Product> products = List.of(
new Product("Samsung galaxy",
"4GB RAM",
ProductType.PHONE,
new BigDecimal("300"),
LocalDate.now()),
new Product("Techno Spark",
"2GB RAM",
ProductType.PHONE,
new BigDecimal("500"),
LocalDate.now()),
new Product("HP parvillion",
"250GB SSD",
ProductType.COMPUTER,
new BigDecimal("600"),
LocalDate.now().minusDays(1)),
new Product("Dell",
"DDR4 hard disk",
ProductType.COMPUTER,
new BigDecimal("700"),
LocalDate.now().minusDays(1)),
new Product("Acer",
"4GB RAM",
ProductType.COMPUTER,
new BigDecimal("200"),
LocalDate.now().minusDays(1)),
new Product("Huawei",
"high resolution camera",
ProductType.PHONE,
new BigDecimal("400"),
LocalDate.now().minusDays(1))
);
productRepository.saveAll(products);
};
}
}
运行Spring Boot应用程序,并访问localhost 8080的页面。我们的页面显示一个表单和一个产品列表。

输入当前日期,选择PDF作为文件类型,然后按下生成按钮。这样我们就会在一个新的浏览器标签上重定向到生成的PDF,如下图所示。
注意,根据你阅读这篇文章的时间,日期会有所不同。

我们可以使用当前和之前的日期来玩弄结果。一些创建的产品有LocalDate.now() ,以获得当前日期。有些产品有LocalDate.now().minusDays(1) ,以获得前一天的日期。
总结
在本教程中,我们已经学会了如何使用Jaspersoft studio生成PDF报告。我们使用了一个现有的模板,并根据我们的需要对它进行了定制。我们还学习了如何加载、编译、填充和生成不同类型的文件。