Java >> Java Tutorial >  >> Tag >> Spring

Spring From the Trenches:Erstellen von PDF-Dokumenten mit Wkhtmltopdf

Wenn wir eine Webanwendung schreiben, sehen wir uns oft einer Anforderung gegenüber, die besagt, dass unsere Anwendung ihren Benutzern Berichte bereitstellen muss.

Typischerweise möchten die Benutzer unserer Anwendung diese Berichte auf der Benutzeroberfläche sehen und die Möglichkeit haben, sie als Excel- und/oder PDF-Dokumente zu exportieren.

Das Problem ist, dass das Erstellen von PDF-Dokumenten nicht gerade ein Kinderspiel ist. Es gibt Bibliotheken, die PDF-Dokumente aus HTML-Markup erstellen können, aber ich war nie ganz zufrieden damit. Allerdings musste ich ihre Mängel hinnehmen, weil ich keine Wahl hatte. Dann hörte ich von einem Befehlszeilentool namens wkhtmltopdf und habe es nie bereut.

Dieser Blogbeitrag beschreibt, wie wir einen Microservice erstellen können, der HTML-Dokumente mithilfe von Java 8, Spring Boot und Wkhtmltopdf in PDF-Dokumente umwandelt.

Bevor wir unseren Microservice implementieren, werfen wir einen kurzen Blick auf den PDF-Erstellungsprozess. Es besteht aus drei Schritten:

  1. Ein Client sendet eine HTTP-Anfrage an unseren Microservice und gibt die URL des HTML-Dokuments und den Dateinamen der erstellten PDF-Datei an.
  2. Unser Microservice ruft das Befehlszeilentool wkhtmltopdf auf, das das HTML-Dokument liest und in ein PDF-Dokument umwandelt.
  3. Unser Microservice liest das erstellte PDF-Dokument und schreibt es in den Hauptteil der HTTP-Antwort.

Beginnen wir mit der Installation von wkhtmltopdf.

Wkhtmltopdf installieren

Als erstes müssen wir das Befehlszeilentool wkhtmltopdf installieren. Wir können die Installationspakete einfach von der Website herunterladen und installieren.

Nachdem wir das Befehlszeilentool wkhtmltopdf installiert haben, können wir unseren Microservice implementieren. Beginnen wir mit der Implementierung der Komponente, die HTML-Dokumente in PDF-Dokumente umwandelt.

PDF-Dokumente aus HTML-Dokumenten erstellen

Bevor wir die Komponente implementieren können, die HTML-Dokumente in PDF-Dokumente umwandelt und die erstellten PDF-Dokumente in den Hauptteil der HTTP-Antwort schreibt, müssen wir eine Klasse erstellen, die verwendet wird, um die erforderlichen Konfigurationsparameter an diese Komponente zu übergeben.

Wir können das tun, indem wir eine PdfFileRequest erstellen Klasse, die zwei Felder hat:

  • Der Dateiname enthält den Dateinamen des erstellten PDF-Dokuments.
  • Die sourceHtmlUrl enthält die URL-Adresse des konvertierten HTML-Dokuments.

Der Quellcode der PdfFileRequest Klasse sieht wie folgt aus:

public class PdfFileRequest {

    private String fileName;
    private String sourceHtmlUrl;

    PdfFileRequest() {}

    public String getFileName() {
        return fileName;
    }

    public String getSourceHtmlUrl() {
        return sourceHtmlUrl;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public void setSourceHtmlUrl(String sourceHtmlUrl) {
        this.sourceHtmlUrl = sourceHtmlUrl;
    }
}

Wir können jetzt die Komponente erstellen, die PDF-Dokumente erstellt, indem Sie diesen Schritten folgen:

  1. Erstellen Sie einen PdfFileCreator class und kommentieren Sie die erstellte Klasse mit dem @Service Anmerkung.
  2. Fügen Sie einen statischen finalen Logger hinzu Feld zur erstellten Klasse. Wir werden diesen Logger verwenden, um eine Fehlermeldung in das Protokoll zu schreiben, wenn das PDF-Dokument nicht erstellt werden kann.
  3. Fügen Sie ein writePdfToResponse() hinzu -Methode auf die erstellte Klasse. Diese Methode benötigt zwei Methodenparameter:
    • Die PdfFileRequest Objekt, das die Konfiguration des PDF-Erstellungsprozesses enthält.
    • Die HttpServletResponse Objekt, in das das erstellte PDF-Dokument geschrieben wird.
  4. Implementieren Sie writePdfToResponse() Methode, indem Sie die folgenden Schritte ausführen:
    1. Stellen Sie sicher, dass der Dateiname des erstellten PDF-Dokuments und die URL des HTML-Dokuments gültig sind.
    2. Erstellen Sie den Befehl, der zum Aufrufen des Befehlszeilentools wkhtmltopdf verwendet wird. Dieser Befehl besteht aus drei Teilen:
      1. Der Name des aufgerufenen Programms (wkhtmltopdf )
      2. Die URL des HTML-Dokuments.
      3. Die Ausgabedatei. Der String '-' teilt wkhtmltopdf mit, dass es die erstellte PDF-Datei nach STDOUT schreiben muss .
    3. Starten Sie den wkhtmltopdf-Prozess.
    4. Lesen Sie das erstellte PDF-Dokument von STDOUT und schreiben Sie es in den Hauptteil der HTTP-Antwort.
    5. Warten Sie, dass der wkhtmltopdf-Prozess beendet wird, bevor Sie mit dem aktuellen Thread fortfahren.
    6. Stellen Sie sicher, dass die PDF-Datei erfolgreich erstellt wurde.
    7. Fügen Sie die erforderlichen Metadaten (Inhaltstyp und den Dateinamen der erstellten PDF-Datei) zur HTTP-Antwort hinzu.
    8. Wenn das PDF-Dokument nicht erstellt werden konnte, lesen Sie die Fehlermeldung von STDERR und ins Log schreiben.
    9. Zerstören Sie den wkhtmltopdf-Prozess.

Der Quellcode des PdfFileCreator Klasse sieht wie folgt aus:

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
class PdfFileCreator {

    private static final Logger LOGGER = LoggerFactory.getLogger(PdfFileCreator.class);

    void writePdfToResponse(PdfFileRequest fileRequest, HttpServletResponse response) {
        String pdfFileName = fileRequest.getFileName();
        requireNotNull(pdfFileName, "The file name of the created PDF must be set");
        requireNotEmpty(pdfFileName, "File name of the created PDF cannot be empty");

        String sourceHtmlUrl = fileRequest.getSourceHtmlUrl();
        requireNotNull(sourceHtmlUrl, "Source HTML url must be set");
        requireNotEmpty(sourceHtmlUrl, "Source HTML url cannot be empty");

        List<String> pdfCommand = Arrays.asList(
                "wkhtmltopdf",
                sourceHtmlUrl,
                "-"
        );

        ProcessBuilder pb = new ProcessBuilder(pdfCommand);
        Process pdfProcess;

        try {
            pdfProcess = pb.start();

            try(InputStream in = pdfProcess.getInputStream()) {
                writeCreatedPdfFileToResponse(in, response);
                waitForProcessBeforeContinueCurrentThread(pdfProcess);
                requireSuccessfulExitStatus(pdfProcess);
                setResponseHeaders(response, fileRequest);
            }
            catch (Exception ex) {
                writeErrorMessageToLog(ex, pdfProcess);
                throw new RuntimeException("PDF generation failed");
            }
            finally {
                pdfProcess.destroy();
            }
        }
        catch (IOException ex) {
            throw new RuntimeException("PDF generation failed");
        }
    }

    private void requireNotNull(String value, String message) {
        if (value == null) {
            throw new IllegalArgumentException(message);
        }
    }

    private void requireNotEmpty(String value, String message) {
        if (value.isEmpty()) {
            throw new IllegalArgumentException(message);
        }
    }

    private void writeCreatedPdfFileToResponse(InputStream in, HttpServletResponse response) throws IOException {
        OutputStream out = response.getOutputStream();
        IOUtils.copy(in, out);
        out.flush();
    }

    private void waitForProcessBeforeContinueCurrentThread(Process process) {
        try {
            process.waitFor(5, TimeUnit.SECONDS);
        }
        catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
    }

    private void requireSuccessfulExitStatus(Process process) {
        if (process.exitValue() != 0) {
            throw new RuntimeException("PDF generation failed");
        }
    }

    private void setResponseHeaders(HttpServletResponse response, PdfFileRequest fileRequest) {
        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + fileRequest.getFileName() + "\"");
    }

    private void writeErrorMessageToLog(Exception ex, Process pdfProcess) throws IOException {
        LOGGER.error("Could not create PDF because an exception was thrown: ", ex);
        LOGGER.error("The exit value of PDF process is: {}", pdfProcess.exitValue());

        String errorMessage = getErrorMessageFromProcess(pdfProcess);
        LOGGER.error("PDF process ended with error message: {}", errorMessage);
    }

    private String getErrorMessageFromProcess(Process pdfProcess) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(pdfProcess.getErrorStream()));
            StringWriter writer = new StringWriter();

            String line;
            while ((line = reader.readLine()) != null) {
                writer.append(line);
            }

            return writer.toString();
        }
        catch (IOException ex) {
            LOGGER.error("Could not extract error message from process because an exception was thrown", ex);
            return "";
        }
    }
}
Wenn Sie eine echte Webanwendung schreiben, dürfen Sie anonymen Benutzern keinen Zugriff auf die HTML-Berichte gewähren . Stattdessen sollten Sie den Benutzer konfigurieren, der von wkhtmltopdf verwendet wird, wenn es das PDF-Dokument erstellt. Sie können dies tun, indem Sie eine der folgenden Optionen an den wkhtmltopdf-Prozess übergeben:cookie , benutzerdefinierter Header und benutzerdefinierte Header-Propagation .

Als Nächstes erstellen wir den Controller, der die öffentliche REST-API unseres Microservice bereitstellt.

Implementieren der REST-API

Wir können die REST-API unseres Microservices erstellen, indem wir diesen Schritten folgen:

  1. Erstellen Sie einen PdfController class und kommentieren Sie die erstellte Klasse mit dem @RestController
  2. Fügen Sie einen privaten PdfFileCreator hinzu -Feld in die erstellte Klasse und fügen Sie seinen Wert mithilfe der Konstruktorinjektion ein.
  3. Fügen Sie ein createPdf() hinzu -Methode an die Controller-Klasse. Diese Methode hat zwei Methodenparameter:
    1. Die PdfFileRequest -Objekt wird aus dem Anforderungstext gelesen und konfiguriert den PDF-Erstellungsprozess.
    2. Die HttpServletRequest Objekt ist die HTTP-Antwort, in die das erstellte PDF-Dokument geschrieben wird.
  4. Konfigurieren Sie createPdf() Methode zur Behandlung von POST Anfragen, die an die URL gesendet werden:'/api/pdf'.
  5. Implementieren Sie createPdf() Methode durch Aufrufen von writePdfToResponse() Methode des PdfFileCreator Klasse.

Der Quellcode des PdfController Klasse sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;

@RestController
class PdfController {

    private final PdfFileCreator pdfFileCreator;

    @Autowired
    PdfController(PdfFileCreator pdfFileCreator) {
        this.pdfFileCreator = pdfFileCreator;
    }

    @RequestMapping(value = "/api/pdf", method = RequestMethod.POST)
    void createPdf(@RequestBody PdfFileRequest fileRequest, HttpServletResponse response) {
        pdfFileCreator.writePdfToResponse(fileRequest, response);
    }
}

Wir haben jetzt unseren Microservice implementiert, der HTML-Dokumente mithilfe des Befehlszeilentools wkhtmltopdf in PDF-Dokumente umwandelt. Lassen Sie uns herausfinden, wie wir unseren neuen Microservice nutzen können.

Mit unserem Microservice

Wir können unseren Microservice verwenden, indem Sie diesen Schritten folgen:

  1. Senden Sie einen POST Anfrage an die URL:'/api/pdf'.
  2. Konfigurieren Sie den PDF-Erstellungsprozess mithilfe von JSON, das im Text der Anfrage gesendet wird.

Wenn wir beispielsweise die Startseite von google.com in ein PDF-Dokument umwandeln möchten, müssen wir einen POST senden Anfrage an die URL:'/api/pdf' und schreiben Sie das folgende JSON-Dokument in den Anfragetext:

{
	"fileName": "google.pdf",
	"sourceHtmlUrl": "http://www.google.com"
}

Lassen Sie uns einen einfachen Spring MVC-Controller implementieren, der die Titelseite von google.com mithilfe unseres Microservices in ein PDF-Dokument umwandelt. Wir können dies tun, indem wir diesen Schritten folgen:

  1. Erstellen Sie einen GooglePdfController Klasse und kommentieren Sie sie mit @Controller Anmerkung.
  2. Fügen Sie ein endgültiges RestTemplate hinzu -Feld in die erstellte Klasse und fügen Sie seinen Wert mithilfe der Konstruktorinjektion ein.
  3. Fügen Sie ein createPdfFromGoogle() hinzu -Methode auf die erstellte Klasse und konfigurieren Sie sie, um GET zu verarbeiten Anfragen an die URL:'/pdf/google'. Diese Methode nimmt eine HttpServletResponse entgegen Objekt als Methodenparameter.
  4. Implementieren Sie createPdfFromGoogle() Methode, indem Sie die folgenden Schritte ausführen:
    1. Erstellen Sie eine neue PdfFileRequest -Objekt und legen Sie den Namen der erstellten PDF-Datei fest (google.pdf ) und die URL des HTML-Dokuments (http://www.google.com ).
    2. Senden Sie einen POST Anfrage an die URL:'http://localhost:8080/api/pdf' durch Aufrufen von postForObject() Methode des RestTemplate Klasse. Übergeben Sie die folgenden Methodenparameter an diese Methode:
      1. Die URL (http://localhost:8080/api/pdf ).
      2. Das Objekt, das in den Anforderungstext geschrieben wird (Die erstellte PdfFileRequest Objekt).
      3. Der Typ des Rückgabewerts (byte[].class ).
    3. Schreiben Sie das empfangene Byte-Array, das das erstellte PDF-Dokument enthält, in den Hauptteil der HTTP-Antwort.
    4. Setzen Sie den Inhaltstyp der Antwort auf:'application/json'.
    5. Setzen Sie den Dateinamen des erstellten PDF-Dokuments auf die HTTP-Antwort, indem Sie die Content-Disposition verwenden Kopfzeile.

Der Quellcode des GooglePdfController Klasse sieht wie folgt aus:

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

@Controller
class GooglePdfController {

    private final RestTemplate restTemplate;

    @Autowired
    GooglePdfController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @RequestMapping(value = "/pdf/google", method = RequestMethod.GET)
    void createPdfFromGoogle(HttpServletResponse response) {
        PdfFileRequest fileRequest = new PdfFileRequest();
        fileRequest.setFileName("google.pdf");
        fileRequest.setSourceHtmlUrl("http://www.google.com");

        byte[] pdfFile = restTemplate.postForObject("http://localhost:8080/api/pdf", 
				fileRequest, 
				byte[].class
		);
        writePdfFileToResponse(pdfFile, "google.pdf", response);
    }

    private void writePdfFileToResponse(byte[] pdfFile, 
										String fileName, 
										HttpServletResponse response) {
        try (InputStream in = new ByteArrayInputStream(pdfFile)) {
            OutputStream out = response.getOutputStream();
            IOUtils.copy(in, out);
            out.flush();

            response.setContentType("application/pdf");
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        }
        catch (IOException ex) {
            throw new RuntimeException("Error occurred when creating PDF file", ex);
        }
    }
}

Wir können jetzt ein GET senden Anfrage an die URL:'/pdf/google' und wir erhalten die Startseite von google.com als PDF-Dokument.

Es sieht ziemlich gut aus, aber wenn wir diese Technik in einer echten Webanwendung verwenden, müssen wir einige Dinge berücksichtigen. Diese Dinge sind:

  • Wkhtmltopdf ist nicht sehr fehlertolerant. Wenn beispielsweise ein Bild (oder eine andere Ressource wie eine .js- oder .css-Datei) nicht gefunden werden kann, wird die PDF-Datei nicht erstellt. Es schlägt einfach fehl und schreibt die Fehlermeldung in STDERR .
  • Die Fehlermeldungen von Wkhtmltopdf können ziemlich lang und etwas chaotisch sein. Mit anderen Worten, es ist nicht immer "einfach", herauszufinden, was falsch ist.
  • Obwohl Wkhtmltopdf HTML-Dokumente sehr gut in PDF-Dokumente umwandeln kann, müssen Sie möglicherweise separate Berichtsansichten erstellen, die nur für diesen Zweck verwendet werden. Außerdem müssen Sie diese Berichtsansichten manchmal auf dem Server rendern.
  • Die Leistung dieser Lösung hängt von wkhtmltopdf ab. Wir können es schneller machen, indem wir diese Regeln befolgen:
    • Entfernen Sie Bilder aus unseren HTML-Dokumenten, da sie wkhtmltopdf verlangsamen.
    • Vermeiden Sie teure CSS-Selektoren.
    • Vermeiden Sie CSS-Stile, die wirklich schlechte Leistungsprobleme verursachen (z. B. Farbverläufe, border-radius , und einige andere Stile).
    • Befolgen Sie die Ratschläge in dieser StackOverflow-Frage.

Einige dieser Nachteile sind ziemlich irritierend, aber ich denke trotzdem, dass die Verwendung von Wkhtmltopdf eine gute Idee ist. Wieso den? Nun, es ist die am wenigsten schlechte Option und hat viele Konfigurationsparameter, die die anderen Optionen nicht haben.

Fassen wir zusammen, was wir aus diesem Blogbeitrag gelernt haben.

Zusammenfassung

Dieser Blogbeitrag hat uns vier Dinge gelehrt:

  • Wir können das Befehlszeilentool wkhtmltopdf mithilfe von Java 8 aufrufen und es so konfigurieren, dass es das erstellte PDF-Dokument nach STDOUT schreibt .
  • Wir haben gelernt, wie wir das erstellte PDF-Dokument von STDOUT lesen können und in die HTTP-Antwort schreiben.
  • Wir haben gelernt, wie wir einen Microservice erstellen können, mit dem wir den PDF-Erstellungsprozess anpassen können.
  • Wkhtmltopdf ist kein perfektes Werkzeug. Es hat ein paar Nachteile, aber es ist immer noch die am wenigsten schlechte Option.

P.S. Sie können die Beispielanwendung dieses Blogbeitrags von Github erhalten.


Java-Tag