Microservices: Chuyển File PDF Thành File Ảnh
Trong công việc thực tế có những lúc bạn phải cung cấp một bản xem trước, thumbnail, ... của một tài liệu có tính bảo mật cao đến khách hàng. Để làm được việc đó chúng ta có khá nhiều cách làm, thậm chí bạn có thể dùng các website free trên mạng, ... tuy nhiên khi làm trong dự án thực thì các giải pháp đó không thể thực hiện được vì bạn phải kết nối với các thành phần khác của dự án và do tính bảo mật của tài liệu bạn không thể sử dụng các công cụ chưa được kiểm định chặt chẽ.
Hôm nay mình sẽ trình bày cách để xây dựng service như vậy bằng Java, Spring và Docker. Nếu bạn chưa quen thuộc với các công nghệ đó cũng không đáng quan ngại cứ tiếp tục thôi, bài này sẽ không đi sâu vào các công nghệ đó mà phân tích cách làm service đó.
Nếu bạn muốn tìm hiểu về Spring/SpringBoot, bạn có thể tham khảo tài liệu này, khá đơn giản cho một sự bắt đầu Springboot tutorial
Nếu bạn muốn tìm hiểu về Docker và cách cài đặt thì cũng có rất nhiều tài liệu cho bạn tham khảo trên mạng.
Cách hoạt động và thiết kế
Service này mình muốn xây dựng độc lập để dễ dàng deploy, tích hợp với ứng dụng đang có và có thể sử dụng lại cho các ứng dụng khác mà không cần thay đổi gì nhiều.
Các service khác muốn sử dụng service này sẽ call qua RESTful API được cung cấp bởi pdf2image service(1) và kết quả được lưu vào một thư mục được ánh xạ (mount) vào cho docker container (2), sau khi có kết quả từ API trả về, các service sẽ đọc các file ảnh từ thư mục đã cấu hình trước để xử lý các bước tiếp theo (3).
Khá đơn giản đúng không, tiếp theo chung ta code service nhé.
Code PDF2Image service
Service được xây dựng với Java và Spring nên sẽ có các phần chính sau:
- Configuration: phần này là không thể thiếu ở hầu hết các ứng dụng, configuration là phần tách biệt với ứng dụng, nó sẽ không được đóng gói cùng ứng dụng, thường các ứng dụng sẽ có một hoặc nhiều file configuration kèm theo.
- Docker: là phần riêng của service này, trong đó chưa các file cần thiết và Dockerfile để bạn có thể đóng gói ứng dụng thành docker image.
- Code của service
Chúng ta đi vào chi tiết từng phần nhé
Configuration
server.port = 8888
spring.application.name = PDF2Image
converted.images.dir = D:/Tmp/PDF2Image
Ở phần này bạn chú ý:
- servcer.port: là port của web server phía trong docker container và sẽ được forward ra ngoài docker container để các service khác có thể thấy và sử dụng nó
- converted.images.dir: là thư mục báo cho pdf2image service nơi lưu trữ kết quả của các file ảnh. Thư mục này sẽ là 1 ánh xạ của thư mục ở trên máy host và docker container.
Docker
Chúng ta sẽ nói rõ hơn ở phần kế tiếp
Code service
Ứng dụng RESTful API của chúng ta có 3 phần chính sau:
- Model: là nơi chứa các object input, output, object chứa dữ liệu trao đổi giữa các services, …
- Controller: là phần giao tiếp giữa service hiện tại và các service khác bên ngoài
- Services: là code chứa logic của ứng dụng bạn đang làm, nó sẽ được call bởi các controller
PDF2Image service
Việc đọc và xử lý một file PDF khá là phức tạp, tuy nhiên hiện nay có khá nhiều Open Source cho công việc đó, ở bài này mình sẽ sử dụng Open Source PDFBox, thư viện này có khá nhiều tính năng, bạn tham khảo thêm API ở https://pdfbox.apache.org/ nhé
/**
* Chuyển file PDF thành các file ảnh.
* Mỗi trang PDF sẽ được chuyển thành một file ảnh, có thứ tự là -1 đến -N
*
* Ví dụ file PDF có 2 trang
* Thì đầu ra là:
* - file-1.png
* - file-2.png
*
* @param luongDuLieuPDF Luồng dữ liệu của file PDF
* @param thuMucChuaFile Thư mục chứa file ảnh
*
* @return Tên file ảnh và số lượng file được sinh ra.
* @throws IOException
*/
public PDF2ImageOut convert(InputStream luongDuLieuPDF, String thuMucChuaFile) throws IOException {
// Tạo file name theo chuẩn UUID để đảm bảo sẽ không trùng lặp
String tenFile = UUID.randomUUID().toString();
// Đọc dữ liệu từ luồng dữ liệu
PDDocument pdfFile = PDDocument.load(luongDuLieuPDF);
PDFRenderer pdfRenderer = new PDFRenderer(pdfFile);
// Duyệt qua toàn bộ các trang PDF
for (int trang = 0; trang < pdfFile.getNumberOfPages(); trang++) {
// Lấy dữ liệu ảnh
// Bạn muốn kích thước ảnh nhỏ hơn thì tùy chỉnh dpi và kiểu ảnh lại
BufferedImage anhPDF = pdfRenderer.renderImageWithDPI(trang, 300, ImageType.RGB);
// Ghi ảnh ra file với hậu tố là -trang.png
ImageIOUtil.writeImage(anhPDF, thuMucChuaFile + "/" + tenFile + "-" + (trang + 1) + ".png", 300);
}
// Chuẩn bị kết quả trả về
PDF2ImageOut out = new PDF2ImageOut();
out.setFileName(tenFile);
out.setCount(pdfFile.getNumberOfPages());
pdfFile.close();
return out;
}
Tiếp theo chúng ta sẽ xây dụng các API để giao tiếp với Client
@RestController
public class PDF2ImageController {
/**
* Thư mục chứa dữ liệu file ảnh được sinh ra
* Thư mục này được map từ file cấu hình application.properties
*/
@Value("${converted.images.dir}")
private String thuMucChuaFile;
/**
* Inject pdf2image service vào để dùng
*/
@Autowired
private IPDF2ImageConverter pdf2ImageService;
/**
* Ping là hàm khá đặc biệt, thường dùng để kiểm tra trạng thái của service xem còn hoạt động không
* nó thường được dùng trong việc thiết kế các hệ thống tự động scale
* @return
*/
@GetMapping(path = "/ping")
public String ping() {
return "Hello, you are here!";
}
/**
* Mình sẽ public API /convert, dữ liệu đầu vào là 1 luồng dữ liệu, với cách cài đặt này yêu cầu phía
* Client phải đọc file dữ liệu, đẩy dữ liệu vào body và post lên cho API
*
* @param luongDuLieuPDF Luồng dữ liệu
* @return JSON chứa tên file và số file được sinh ra
* @throws Exception
*/
@PostMapping("/convert")
public ResponseEntity<PDF2ImageOut> convertPdf2Image(InputStream luongDuLieuPDF) throws Exception {
try {
PDF2ImageOut out = pdf2ImageService.convert(luongDuLieuPDF, thuMucChuaFile);
return ResponseEntity.ok(out);
} catch (Exception ex) {
// Controller là điểm giao tiếp với các ứng dụng khác, nên bạn ưu tiên bắt toàn bộ các
// lỗi có thể xảy ra của ứng dụng mình nhé, bạn có thể xử lý nó hoặc đơn giản là trả về một thông báo
// lỗi nào đó thay vì trả toàn bộ dấu vết về phía Client. Đây là một best practice.
return ResponseEntity.badRequest().body(new PDF2ImageOut());
}
}
/**
* API /convert/file sẽ đọc dữ liệu từ file thay vì từ stream
* Như đã đề cập, service sẽ chạy trong Docker container nên bạn chú ý điểm này nhé.
*
* @param filePDF Đường dẫn file PDF
* @return JSON chứa tên file và số file được sinh ra
* @throws Exception
*/
@PostMapping("/convert/file")
public ResponseEntity<PDF2ImageOut> convertPdf2Image(String filePDF) throws Exception {
try {
InputStream dataStream = new FileInputStream(filePDF);
PDF2ImageOut out = pdf2ImageService.convert(dataStream, thuMucChuaFile);
return ResponseEntity.ok(out);
} catch (Exception ex) {
return ResponseEntity.badRequest().body(new PDF2ImageOut());
}
}
}
Về cơ bản service của chúng ta chỉ có hai phần đó.
Trước khi bước qua phần Docker chúng ta thử chạy ứng dụng mình với Java xem thế nào nhé
Bạn cần cài đặt Java 8 và Maven 3 để chạy thử, việc cài đặt này khá căn bản nên mình không trình bày thêm, các bạn tìm kiếm trên google nghen.
Chú ý: các câu lệnh được kèm theo ở phần README.md trên repository
Build
Nếu lần đầu bạn phải đợi khá lâu để maven tải các thư viện cần thiết về
Chạy thử
Nếu kết quả bạn nhận được như hình thì có nghĩa là bạn đã thành công, service bạn đang chạy ở http://localhost:8888/ping nghen.
OK bước tiếp theo chúng ta cần có một công cụ để gọi API với phương thức POST mà không cần phải code, các bạn có thể dùng Postman
Các bạn chú ý phần highlight ở kết quả trên để biết cách thực hiện với Postman.
Về cơ bản service của chúng ta đã hoàn thành, bước tiếp theo chúng ta sẽ làm cho service dễ tích hợp hơn với các hệ thống khác bằng Docker
Tích hợp với Docker
Để thực hiện các phần tiếp theo, các bạn cần phải cài đặt Docker vào môi trường phát triển của mình. Mình khuyên bạn nên thực hiện điều này ở môi trường Linux để tránh 1 số lỗi không cần thiết, môi trường mình đã test là Ubuntu 18.4 LTS và Docker 19. Bạn làm theo hướng dẫn ở trang này nghen https://docs.docker.com/engine/install/ubuntu/, bạn chỉ cần thực hiện một số lệnh là đã có môi trường Docker rồi
# Cài đặt docker repository
$ sudo apt-get update$ sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add –
$ sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
# Thực hiện cài đặt docker
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
# Cho phép user hiện tại chạy với docker
$ sudo usermod -aG docker $USER
Bạn cần restart lại máy sau khi cài đặt xong
Để xem docker đã chạy chưa bạn thực hiện lệnh sau
$ sudo service docker status
Kết quả như hình là OK
Tạo Docker image
Dockerfile
# Docker image cơ sở
FROM openjdk:8-jre-alpine
# Chuyển thư mục làm việc đến đây
WORKDIR /home/lapth
# Copy file jar và sh từ máy gốc sang Docker image
# Mỗi lần build lại file jar bạn phải build lại image này
COPY pdf2image.jar .
COPY run.sh .
CMD chmod +x run.sh
# Thực hiện lệnh này khi chạy docker image
CMD sh ./run.sh
Trước khi build docker image bạn nhớ build service như đã nói ở trên và copy file pdf2image.jar vào thư mục docker nhé.
Để build docker image bạn chạy các lệnh sau
Việc push và pull docker image nó phụ thuộc vào mỗi môi trường dự án nên phần này mình không nói tiếp, mình sẽ lấy image id thay vì image thực để làm ví dụ
Để chạy thử service ở môi trường docker, bạn thực hiện như sau, bạn chú ý chuyển vào thư mục release trước khi thực hiện lệnh này nhé
Nếu kết quả như hình thì bạn đã thành công chạy service của chúng ta với Docker container rồi đó, từ giờ bạn có thể tích hợp service với bất kỳ một hệ thống nào dùng bất kỳ ngôn ngữ lập trình nào .Net, NodeJS, … mà không bị phụ thuộc vào những gì chúng ta đã làm.
Kết luận
Thông qua bài viết này ngoài việc giới thiệu đến các bạn một Open Source khá mạnh trong việc xử lý PDF, mình muốn giới thiệu các bạn cách làm một service trong kiến trúc Microservices khá hot hiện nay.
Các bạn có thể tải source code từ repository của mình: https://github.com/lapth/pdf2image.git
Chúc các bạn thành công! và nhớ share, tặng * cho bài viết để tác giả có động lực hơn để viết bài nghen ^_^