0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Java】Spring BootとRabbitMQを使ってPDFを出力する方法

Posted at

Spring Bootで、Thymeleafの「PDF出力ボタン」クリックをトリガーにして、RabbitMQを使って非同期にPDFを生成するサンプル構成を以下に示します。

完成イメージ

タイトルと内容を入力してボタンをクリックすると、
image.png

PDFの出力画面が表示されます。
image.png

今回はデスクトップにPDFを出力しました。こんな感じです。
下記のようにPDFが出力されます。
image.png

RabbitMQにもキューが格納されています。
image.png

🔧 使用する技術スタック

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-amqpitext-corejackson-databindのモジュールを組み込みます。

pom.xml
<?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設定)

application.yml
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

3. DTO: PdfRequest.java

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

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 を使ってメッセージを送信する際も、このコンバータが利用されることを確認しています。

PdfRequestSender.java
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

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

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 メソッド内で、デシリアライズ処理やその他のエラーをキャッチして適切にハンドリングすることが重要です。

RabbitConfig.java
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

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() メソッドを使って追加します。

修正後のコードはこちら⇩

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();
		}
	}
}

日本語だとPDFに表示されないです。日英どちらの表記であってもPDFに表示されるようにしたい

(原因)

日本語のテキストをPDFに表示するためには、iTextでデフォルトのフォント(例:Helvetica)では日本語を表示することができません。

解決方法

日本語を表示するには、TrueTypeフォント(.ttf)を使って日本語に対応するフォントを指定する必要があります。

例えば、Noto Sans CJK IPAゴシックMS Gothicなどの日本語フォントを使うと、日本語も表示できるようになります。

🌟ちなみにWindowsではこれらのフォントファイルは「C:\Windows\Fonts」フォルダにあります。

■Noto Sans CJKのダウンロードサイト

参考にしたサイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?