Spring Bootで、Thymeleafの「PDF出力ボタン」クリックをトリガーにして、RabbitMQを使って非同期にPDFを生成するサンプル構成を以下に示します。
完成イメージ
今回はデスクトップにPDF
を出力しました。こんな感じです。
下記のようにPDFが出力されます。
🔧 使用する技術スタック
Spring Boot
Thymeleaf
RabbitMQ
iText PDF (または OpenPDF)
Spring AMQP (RabbitMQ連携)
📦 プロジェクト構成
└── SpringAQMP ←プロジェクト名/
├── src/main/java/
│ └── com.packt.cardatabase/
│ ├── config/
│ │ └── RabbitConfig.java
│ ├── controller/
│ │ └── PdfController.java
│ ├── dto/
│ │ └── PdfRequest.java
│ ├── messaging/
│ │ ├── consumer/
│ │ │ └── PdfRequestListener.java
│ │ └── producer/
│ │ └── PdfRequestSender.java
│ ├── service/
│ │ └── Pdfservice.java
│ └── SpringAqmpApplication.java
├── src/main/resources/
│ ├── staticフォルダ
│ ├── templates/
│ │ └── index.html
│ ├── application.properties
│ └── application.yml
└── pom.xml
1. pom.xml
spring-boot-starter-amqp
、itext-core
、jackson-databind
のモジュールを組み込みます。
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.packt.cardatabase</groupId>
<artifactId>SpringAQMP</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringAQMP</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.itextpdf/itext-core -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-core</artifactId>
<version>9.2.0</version>
<type>pom</type>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Mavenリポジトリ
■Spring Boot Starter AMQP
■Jackson Databind
■IText Core
2. application.yml
(RabbitMQ設定)
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
3. DTO: PdfRequest.java
package com.packt.cardatabase.dto;
import java.io.Serializable;
public class PdfRequest implements Serializable{
private static final long serialVersionUID = 1L;
private String title;
private String content;
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
}
4. Controller: PdfController.java
package com.packt.cardatabase.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.packt.cardatabase.dto.PdfRequest;
import com.packt.cardatabase.messaging.producer.PdfRequestSender;
@Controller
public class PdfController {
private final PdfRequestSender sender;
public PdfController(PdfRequestSender sender) {
this.sender = sender;
}
@GetMapping("/")
public String index() {
return "index";
}
@PostMapping("/generate-pdf")
public String generatePdf(@RequestParam String title,
@RequestParam String content,
Model model) {
PdfRequest request = new PdfRequest();
request.setTitle(title);
request.setContent(content);
sender.send(request);
model.addAttribute("message", "PDF生成要求を送信しました!");
return "index";
}
}
5. メッセージ送信: PdfRequestSender.java
RabbitTemplate
での送信
PdfRequestSender
クラスのようにメッセージを送信する際も、Jackson2JsonMessageConverter
を適切に利用することで、シリアライズされたメッセージが送信されます。
これにより、PdfRequest
オブジェクトがJSON
形式で送信され、リスナーで受信したときに適切にデシリアライズされます。
結論
RabbitConfig
クラスの設定で、Jackson2JsonMessageConverter
を使用してメッセージのシリアライズおよびデシリアライズを行い、SimpleMessageListenerContainer
でメッセージをリスニングしています。
送信側(PdfRequestSender
)で RabbitTemplate
を使ってメッセージを送信する際も、このコンバータが利用されることを確認しています。
package com.packt.cardatabase.messaging.producer;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import com.packt.cardatabase.dto.PdfRequest;
@Component
public class PdfRequestSender {
private final RabbitTemplate rabbitTemplate;
public static final String QUEUE = "pdf.queue";
public PdfRequestSender(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void send(PdfRequest request) {
rabbitTemplate.convertAndSend(QUEUE,request);
}
}
6. メッセージ受信: PdfRequestListener.java
package com.packt.cardatabase.messaging.consumer;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.packt.cardatabase.dto.PdfRequest;
import com.packt.cardatabase.messaging.producer.PdfRequestSender;
import com.packt.cardatabase.service.PdfService;
@Component
public class PdfRequestListener {
private final PdfService pdfService;
public PdfRequestListener(PdfService pdfService) {
this.pdfService = pdfService;
}
@RabbitListener(queues = PdfRequestSender.QUEUE)
public void receive(PdfRequest request) {
pdfService.generatePdf(request);
}
}
7. PDF生成サービス: PdfService.java
package com.packt.cardatabase.service;
import java.io.File;
import java.io.FileOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.packt.cardatabase.dto.PdfRequest;
@Service
public class PdfService {
@Value("${file.path}")
private String fontFilePath;
public void generatePdf(PdfRequest request) {
try {
// ユーザーのデスクトップパスを取得
String userHome = System.getProperty("user.home");
String desktopPath = userHome + File.separator + "Desktop";
// 出力PDFのファイル名とパス
String fileName = "generated_" + System.currentTimeMillis() + ".pdf";
String outputPath = desktopPath + File.separator + fileName;
// PDF書き込み
PdfWriter writer = new PdfWriter(new FileOutputStream(outputPath));
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf);
// フォント設定(ボールド)
PdfFont boldFont = PdfFontFactory.createFont("c:\\windows\\fonts\\msgothic.ttc,0", "Identity-H");
//PdfFont boldFont = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
//タイトルをPDFにセットする
//Paragraph title = new Paragraph(request.getTitle()).setFont(boldFont);
Paragraph title = new Paragraph(request.getTitle())
.setFont(boldFont)
.setFontSize(10);
System.out.println("タイトルは、" + title);
document.add(title);
//内容(Content)をPDFニセットする
Paragraph content = new Paragraph(request.getContent()).setFont(boldFont).setFontSize(12);
System.out.println("内容は、" + content);
document.add(content);
//ドキュメントを閉じる
document.close();
System.out.println("PDF generated: " + fileName);
}catch(Exception e) {
e.printStackTrace();
}
}
}
8.RabbitConfigクラス
詳細な説明
Jackson2JsonMessageConverter
の設定:
Jackson2JsonMessageConverter
は、AMQP メッセージ(通常はバイト配列)を JSON に変換して、PdfRequest
などのオブジェクトに変換します。この設定を messageListenerContainer
のリスナー部分でも使用しています。
RabbitTemplate の設定:
RabbitTemplate
は、メッセージの送受信に使われ、ここでは Jackson2JsonMessageConverter
を使って、送信時にオブジェクトを JSON にシリアライズし、受信時に JSON をオブジェクトにデシリアライズします。
SimpleMessageListenerContainer の設定:
ここでリスナーを設定します。SimpleMessageListenerContainer
を使って、指定したキュー(pdf.queue
)からメッセージを受信し、そのメッセージをPdfRequest
オブジェクトにデシリアライズして処理します。
MessageListener インターフェースの実装:
MessageListener
を匿名クラスとして実装し、メッセージを受信した際にその処理を記述しています。メッセージはJackson2JsonMessageConverter
を使用して、PdfRequest
オブジェクトに変換されます。
エラーハンドリング:
onMessage
メソッド内で、デシリアライズ処理やその他のエラーをキャッチして適切にハンドリングすることが重要です。
package com.packt.cardatabase.config;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.packt.cardatabase.dto.PdfRequest;
import com.packt.cardatabase.service.PdfService;
@Configuration
@EnableRabbit
public class RabbitConfig {
// Jackson2JsonMessageConverter を設定してリスナーで使えるようにする
@Bean
public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
return new Jackson2JsonMessageConverter(); // Jackson を使用してメッセージの変換を行う
}
// RabbitTemplate を使ってメッセージ送信、受信の際にコンバータを適用する
@Bean
public RabbitTemplate rabbitTemplate(org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter()); // メッセージコンバータを設定
return rabbitTemplate;
}
// SimpleMessageListenerContainer の設定
@Bean
public SimpleMessageListenerContainer messageListenerContainer(org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("pdf.queue"); // メッセージキュー名を指定
PdfService pdfService = new PdfService();
// ここで MessageListener を匿名クラスで設定するのではなく、処理するメソッドを指定します
container.setMessageListener(message -> {
try {
// メッセージのデシリアライズ(Json から PdfRequest オブジェクトに変換)
PdfRequest request = (PdfRequest) jackson2JsonMessageConverter().fromMessage(message);
// PdfRequest を処理
System.out.println("Received PdfRequest: " + request);
// 実際の処理はここで行う
pdfService.generatePdf(request);
} catch (Exception e) {
e.printStackTrace();
// エラーハンドリング
}
});
return container;
}
}
9. HTMLテンプレート: templates/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>PDF生成</title>
</head>
<body>
<h1>PDF生成フォーム</h1>
<form th:action="@{/generate-pdf}" method="post">
<label>タイトル: <input type="text" name="title"/></label><br/>
<label>内容: <textarea name="content"></textarea></label><br/>
<button type="submit">PDF出力</button>
</form>
<p th:if="${message}" th:text="${message}"></p>
</body>
</html>
トラブルシューティング
出力したPDFにはタイトルと内容(Content)が表示されない。
(原因)
PdfService
クラスの generatePdf
メソッドで、PDFにタイトルとコンテンツ(content)が表示されない問題の原因は、Paragraph をPDFに追加していないことです。
title
とcontent
をPDFドキュメントに追加していないため、生成されるPDFは空になってしまっています。
なので、title と content をドキュメントに追加する:
Document
オブジェクトに title と content のParagraph
を追加する必要があります。document.add()
メソッドを使って追加します。
修正後のコードはこちら⇩
package com.packt.cardatabase.service;
import java.io.File;
import java.io.FileOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.packt.cardatabase.dto.PdfRequest;
@Service
public class PdfService {
@Value("${file.path}")
private String fontFilePath;
public void generatePdf(PdfRequest request) {
try {
// ユーザーのデスクトップパスを取得
String userHome = System.getProperty("user.home");
String desktopPath = userHome + File.separator + "Desktop";
// 出力PDFのファイル名とパス
String fileName = "generated_" + System.currentTimeMillis() + ".pdf";
String outputPath = desktopPath + File.separator + fileName;
// PDF書き込み
PdfWriter writer = new PdfWriter(new FileOutputStream(outputPath));
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf);
// フォント設定(ボールド)
PdfFont boldFont = PdfFontFactory.createFont("c:\\windows\\fonts\\msgothic.ttc,0", "Identity-H");
//PdfFont boldFont = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
//タイトルをPDFにセットする
//Paragraph title = new Paragraph(request.getTitle()).setFont(boldFont);
Paragraph title = new Paragraph(request.getTitle())
.setFont(boldFont)
.setFontSize(10);
System.out.println("タイトルは、" + title);
document.add(title);
//内容(Content)をPDFニセットする
Paragraph content = new Paragraph(request.getContent()).setFont(boldFont).setFontSize(12);
System.out.println("内容は、" + content);
document.add(content);
//ドキュメントを閉じる
document.close();
System.out.println("PDF generated: " + fileName);
}catch(Exception e) {
e.printStackTrace();
}
}
}
日本語だとPDFに表示されないです。日英どちらの表記であってもPDFに表示されるようにしたい
(原因)
日本語のテキストをPDFに表示するためには、iTextでデフォルトのフォント(例:Helvetica)では日本語を表示することができません。
解決方法
日本語を表示するには、TrueTypeフォント(.ttf)を使って日本語に対応するフォントを指定する必要があります。
例えば、Noto Sans CJK
やIPAゴシック
、MS Gothic
などの日本語フォントを使うと、日本語も表示できるようになります。
🌟ちなみにWindowsではこれらのフォントファイルは「C:\Windows\Fonts
」フォルダにあります。
■Noto Sans CJKのダウンロードサイト
参考にしたサイト