Ein Hinweis bei der Untersuchung von Javalin

Überblick

** Was ist Javalin **

Javalin ist ein leichtes Web Framework für Java und Kotlin. Aus einem Web Framework namens Spark Framework heraus wurde die Version 0.0.1 im Mai 2017 und die Version 3.4.1 ab August 2019 veröffentlicht. Die Hauptfunktionen werden von GitHub unten zitiert. (Japanische Übersetzung ist Google Übersetzung)

https://github.com/tipsy/javalin

Javalin is more of a library than a framework. Some key points:

Javelin ist eher eine Bibliothek als ein Framework. Einige wichtige Punkte:

Die erste Hälfte dieses Artikels ist ein Memo über die Implementierung einer Anwendung, in der "Hello World" mit der Mindestkonfiguration angezeigt wird, und die zweite Hälfte ist ein Memo dessen, was ich bei der Implementierung einer einfachen Webanwendung untersucht / versucht habe.

Umgebung

Referenz

Hallo Welt mit minimaler Konfiguration

Projektstruktur

Dies ist ein normales Maven-Projekt.

[project_root]
 |
 `--- /src
 |      |
 |      `--- /main
 |      |      |
 |      |      `--- /java
 |      |      |      |
 |      |      |      `--- com.example.demo (package)
 |      |      |            |
 |      |      |            `--- App.java
 |      |      |
 |      |      `--- /resources
 |      |
 |      `--- /test
 |             |
 |             `--- /java
 |             |
 |             `--- /resources
 |
 `--- pom.xml

pom.xml

Die erforderlichen Abhängigkeiten für eine minimale Javalin-Anwendung sind Javalin und slf4j-simple.

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo-java12-javalin3</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo java12 javalin 3.4.1</name>
    <description>Demo project for Javalin 3 with Java12</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>12</java.version>
        <maven.clean.version>3.1.0</maven.clean.version>
        <maven.resources.version>3.1.0</maven.resources.version>
        <maven.compiler.version>3.8.1</maven.compiler.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <maven.compiler.showWarnings>true</maven.compiler.showWarnings>
        <maven.jar.version>3.1.1</maven.jar.version>
        <maven.assembly.version>3.1.1</maven.assembly.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.javalin</groupId>
            <artifactId>javalin</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.26</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>${maven.clean.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>${maven.resources.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.version}</version>
                <configuration>
                    <compilerArgs>
                        <arg>-Xlint:all</arg>
                    </compilerArgs>
                    <release>${java.version}</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>${maven.jar.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>${maven.assembly.version}</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>com.example.demo.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Implementierung des Anwendungsstartpunkts

Implementieren Sie den Startpunkt der Javalin-Anwendung in der Hauptmethode der App-Klasse (Klassenname ist beliebig).

import io.javalin.Javalin;

public class App {
  public static void main(String ... args) {
    // (1)
    Javalin app = Javalin.create().start(7000);
    // (2)
    app.get("/", ctx -> ctx.result("Hello World"));
  }
}

Starten Sie von IDE

Wenn Sie die App.main-Methode ausführen, wird Javalin gestartet und das folgende Protokoll wird an die Konsole ausgegeben. Wie Sie im Protokoll sehen können, dauerte das Booten in meiner Entwicklungsumgebung weniger als eine Sekunde. Wenn Sie auf "http: // localhost: 7000" zugreifen, erhalten Sie eine Text- / Klartextantwort von "Hello World".

[main] INFO io.javalin.Javalin - 
           __                      __ _
          / /____ _ _   __ ____ _ / /(_)____
     __  / // __ `/| | / // __ `// // // __ \
    / /_/ // /_/ / | |/ // /_/ // // // / / /
    \____/ \__,_/  |___/ \__,_//_//_//_/ /_/

        https://javalin.io/documentation

[main] INFO org.eclipse.jetty.util.log - Logging initialized @827ms to org.eclipse.jetty.util.log.Slf4jLog
[main] INFO io.javalin.Javalin - Starting Javalin ...
[main] INFO io.javalin.Javalin - Listening on http://localhost:7000/
[main] INFO io.javalin.Javalin - Javalin started in 731ms \o/

Beginnen Sie mit der JAR-Datei

Beim Erstellen wird eine JAR-Datei mit dem Namen "demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar" unter dem Zielverzeichnis generiert. Die Javalin-Anwendung verwendet Embedded Jetty, sodass Sie die Webanwendung mit "java -jar " starten können.

> java --version
openjdk 12.0.2 2019-07-16
OpenJDK Runtime Environment (build 12.0.2+10)
OpenJDK 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing)

> java -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar

Im Folgenden finden Sie ein Beispiel für das Ändern des Standardgebietsschemas mit JVM-Parametern.

> java -Duser.language=en -Duser.country=US -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar

Damit ist die Implementierung der Anwendung "Hallo Welt mit minimaler Konfiguration" abgeschlossen.

Über Javalin-Funktionen

Von hier aus wird es ein Memo sein, wenn eine einfache Webanwendung mit Javalin implementiert wird.

So implementieren Sie einen HTTP-Handler

Der oben beschriebene GET-Anforderungshandler (Muster 1 unten) kann auch als Muster 2 und Muster 3 implementiert werden.

app.get("/", ctx -> ctx.result("Hello World"));
public class App {

  void index2(@NotNull Context ctx) {
    ctx.result("Hello World2");
  }

  public static void main(String ... args) {
    Javalin app = Javalin.create().start(7000);
    App handler = new App();
    app.get("/2", handler::index2);
  }

}
public class App {

  static Handler index3 = ctx -> {
    ctx.result("Hello World3");
  };

  public static void main(String ... args) {
    Javalin app = Javalin.create().start(7000);
    app.get("/3", App.index3);
  }

}

** Muster 2 ** * Der Einfachheit halber haben wir der App-Klasse eine Methode hinzugefügt, aber normalerweise implementieren wir eine Handler-Klasse separat. ** Muster 3 ** Im offiziellen Site-Tutorial gibt es auch ein Beispiel, das im statischen Feld beschrieben wird. Zusätzlich zum HTTP-Handler GET gibt es POST, PUT, PATCH, DELETE, HEAD und OPTIONS.

HTTP-Methode Unterschrift
GET public Javalin get(@NotNull String path, @NotNull Handler handler)
POST public Javalin post(@NotNull String path, @NotNull Handler handler)
PUT public Javalin put(@NotNull String path, @NotNull Handler handler)
PATCH public Javalin patch(@NotNull String path, @NotNull Handler handler)
DELTE public Javalin delete(@NotNull String path, @NotNull Handler handler)
HEAD public Javalin head(@NotNull String path, @NotNull Handler handler)
OPTIONS public Javalin options(@NotNull String path, @NotNull Handler handler)

vor / nach dem Endpunkt

Sie können vor / nach Endpunkten festlegen, dass sie vor und nach dem HTTP-Handler ausgeführt werden sollen.

Endpunkt Unterschrift
before public Javalin before(@NotNull String path, @NotNull Handler handler)
before public Javalin before(@NotNull Handler handler)
after public Javalin after(@NotNull String path, @NotNull Handler handler)
after public Javalin after(@NotNull Handler handler)

Gruppierung von HTTP-Handlern

Endpunkt HTTP-Methode Funktion
/api/v1/users GET Benutzerliste abrufen
/api/v1/user/:id GET Holen Sie sich den Benutzer durch Angabe der ID
/api/v1/user POST Benutzer registrieren
/api/v1/user/:id DELETE Löschen Sie den Benutzer durch Angabe der ID
/api/v2/users GET Benutzerliste abrufen
/api/v2/user/:id GET Holen Sie sich den Benutzer durch Angabe der ID
/api/v2/user POST Benutzer registrieren
/api/v2/user/:id DELETE Löschen Sie den Benutzer durch Angabe der ID

Wenn Sie zwei solcher Endpunkte haben, v1 und v2, können Sie HTTP-Handler wie unten gezeigt gruppieren und definieren.

UserController v1Controller = /*Implementierung von Version 1*/
UserController v2Controller = /*Implementierung von Version 2*/

app.routes(() -> {

  // version 1
  ApiBuilder.path("/api/v1", () -> {
    // GET /api/v1/users
    ApiBuilder.get("/users", v1Controller::findAll);
    ApiBuilder.path("/user", () -> {
      // GET /api/v1/user/:id
      ApiBuilder.get(":id", v1Controller::findById);
      // POST /api/v1/user
      ApiBuilder.post(v1Controller::store);
      // DELETE /api/v1/user/:id
      ApiBuilder.delete(":id", v1Controller::remove);
    });
  });

  // version 2
  ApiBuilder.path("/api/v2", () -> {
    // GET /api/v2/users
    ApiBuilder.get("/users", v2Controller::findAll);
    ApiBuilder.path("/user", () -> {
      // GET /api/v2/user/:id
      ApiBuilder.get(":id", v2Controller::findById);
      // POST /api/v2/user
      ApiBuilder.post(v2Controller::store);
      // DELETE /api/v2/user/:id
      ApiBuilder.delete(":id", v2Controller::remove);
    });
  });

});

Über io.javalin.http.Context

Der HTTP-Handler-Methodenparameter io.javalin.http.Context bietet die Funktionalität und Daten, die für HTTP-Anforderungen und -Antworten erforderlich sind. Das Folgende ist ein Zitat aus der Dokumentseite der offiziellen Website.

https://javalin.io/documentation#context

The Context object provides you with everything you need to handle a http-request. It contains the underlying servlet-request and servlet-response, and a bunch of getters and setters. The getters operate mostly on the request-object, while the setters operate exclusively on the response object.

Der Kontext enthält auch Anforderungs- und Res-Felder, die Instanzen von HttpServletRequest und HttpServletResponse enthalten.

javax.servlet.http.HttpServletRequest request = ctx.req;
javax.servlet.http.HttpServletResponse response = ctx.res;

So empfangen Sie Anforderungsparameter

Parameter abfragen

Eine Methode, die die in der Abfragezeichenfolge angegebenen Parameter empfängt

Unterschrift
String queryParam(String key)
String queryParam(String key, String default)
List<String> queryParams(String key)
Map<String, List<String>> queryParamMap()
Validator<T> queryParam(String key, Class<T> clazz)

Formularparameter

Formulare verfügen über ähnliche Methoden wie Abfrageparameter. Leider können Sie die Felder des gesamten Formulars nicht an ein Objekt binden.

Unterschrift
String formParam(String key)
String formParam(String key, String default)
List<String> formParams(String key)
Map<String, List<String>> formParamMap()
Validator<T> formParam(String key, Class<T> clazz)

Pfadparameter

Eine Methode, die einen Teil davon als Parameter von einem Pfad wie "/ api / user / 1" empfängt

Unterschrift
String pathParam(String key)
Map<String, String> pathParamMap()
Validator<T> pathParam(String key, Class<T> clazz)

Der Teil des Anforderungshandlerpfads mit einem Doppelpunkt (:) wird als Pfadparameter identifiziert.

app.get("/api/user/:id", ctx -> {
  Long userId = ctx.pathParam("id", Long.class).get();
});

Konfiguration der Speeranwendung

Sie kann angepasst werden, indem eine Instanz der JavalinConfig-Klasse an die Javalin.create-Methode übergeben wird. Sie können eine Instanz von JavalinConfig nicht wie unten gezeigt übergeben, sondern auch in einen Lambda-Ausdruck schreiben.

Beispiel

configuration


JavalinConfig config = new JavalinConfig();
// configuration
Javalin
    .create(config)
// ...Kürzung...

** Lambda-Ausdruck **

configuration


Javalin
    .create(config -> {
        // configuration
    })
// ...Kürzung...

Hauptkonfigurationen

Kontextpfadeinstellung (Standard ist "/")

config.contextPath = "/app";

Festlegen des Standardinhaltstyps (Standard ist "Text / Nur")

config.defaultContentType = "application/json";

ETag-Einstellung (Standard ist false)

config.autogenerateEtags = true;

Die Ausgabe des Anforderungsprotokolls kann mit JavalinConfig.requestLogger festgelegt werden. Das zweite Argument (execTime) ist die Zeit, die für die Anforderung benötigt wird (ms).

configuration


config.requestLogger((ctx, execTime) -> {
  LOG.debug("[{}] {} - {} ms.", ctx.fullUrl(), ctx.userAgent(), execTime);
});

Entwicklungskonfiguration

Einige Funktionen sind nützlich, um sie während der Entwicklung zu aktivieren.

enableDevLogging

configuration


config.enableDevLogging();

Da es viele Informationen gibt, denke ich, dass sie verwendet werden, um sie zu aktivieren, wenn untersucht wird, wann ein Problem auftritt, anstatt sie ständig aktiviert zu lassen.

[qtp2092769598-18] INFO io.javalin.Javalin - JAVALIN REQUEST DEBUG LOG:
Request: GET [/overview]
    Matching endpoint-handlers: [GET=/overview]
    Headers: {Cookie=Idea-c29ae3c1=53d6d99f-1169-42ed-8d69-a63cfe80a87a, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3, Upgrade-Insecure-Requests=1, Connection=keep-alive, User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36, Sec-Fetch-Site=none, Host=localhost:7000, Sec-Fetch-User=?1, DNT=1, Accept-Encoding=gzip, deflate, br, Accept-Language=ja,en-US;q=0.9,en;q=0.8,en-GB;q=0.7, Sec-Fetch-Mode=navigate}
    Cookies: {Idea-c29ae3c1=53d6d99f-1169-42ed-8d69-a63cfe80a87a}
    Body: 
    QueryString: null
    QueryParams: {}
    FormParams: {}
Response: [200], execution took 26.37 ms
    Headers: {Server=Javalin, Content-Encoding=gzip, Date=Wed, 21 Aug 2019 13:44:46 GMT, Content-Type=text/html}
    Body is gzipped (4464 bytes, not logged)
----------------------------------------------------------------------------------

RouteOverviewPlugin

configuration


config.registerPlugin(new RouteOverviewPlugin("/overview"));

Sie können die Liste der Anforderungshandler anzeigen, die Ihrer Javalin-Anwendung hinzugefügt wurden, indem Sie dieses Plugin registrieren und auf "http: // localhost: 7000 / summary" zugreifen. overview.png

Passen Sie die Fehlerantwort an

Es gibt eine Ausnahmeklasse, die dem HTTP-Status entspricht, der Javalins HttpResponseException erbt.

Status Ausnahmeklasse Botschaft
302 RedirectResponse Redirected
400 BadRequestResponse Bad request
401 UnauthorizedResponse Unauthorized
403 ForbiddenResponse Forbidden
404 NotFoundResponse Not found
405 MethodNotAllowedResponse Method not allowed
409 ConflictResponse Conflict
410 GoneResponse Gone
500 InternalServerErrorResponse Internal server error
502 BadGatewayResponse Bad gateway
503 ServiceUnavailableResponse Service unavailable
504 GatewayTimeoutResponse Gateway timeout

Behandeln Sie eindeutige Ausnahmen

Sie können anwendungsspezifische Ausnahmen behandeln und beliebige Fehlerantworten zurückgeben. Wenn eine anwendungsspezifische Ausnahme namens AppServiceException unten ausgelöst wird,

AppServiceException


public class AppServiceException extends RuntimeException {
  public AppServiceException() {
  }
  public AppServiceException(String message) {
    super(message);
  }
  public AppServiceException(String message, Throwable cause) {
    super(message, cause);
  }
}

Um diese Ausnahme zu behandeln und den HTTP-Status zurückzugeben, ordnen Sie die Ausnahme Ihrer Javalin-Anwendung zu. Dieses Beispiel gibt nur den Status 500 zurück.

app.exception(AppServiceException.class, (e, ctx) -> {
  ctx.status(500);
});

Implementieren Sie Folgendes, um eine beliebige Fehlerantwortnachricht und einen HTTP-Status zurückzugeben. Sie können auch die json-Methode anstelle der Ergebnismethode verwenden, um eine Nachricht im json-Format zurückzugeben.

app.exception(AppServiceException.class, (e, ctx) -> {
  ctx.result("Application Error : " + e.getMessage()).status(500);
});

Behandle json in Anfrage / Antwort

Fügen Sie der Abhängigkeit Jackson hinzu, damit die Anforderung / Antwort json verarbeiten kann. Der zweite "jackson-datatype-jsr310" ist erforderlich, wenn Sie mit der Java 8-API für Datum und Uhrzeit arbeiten.

pom.xml


<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.0.pr1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.10.0.pr1</version>
</dependency>

Implementieren Sie einen GET-Anforderungshandler, der auf json reagiert. Verwenden Sie die json-Methode, um auf json zu antworten.

public static void main(String ... args) {
  // ...Kürzung

  // (1)
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.findAndRegisterModules();
  objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"));
  // (2)
  JavalinJackson.configure(objectMapper);

  // (3)
  app.get("/json", ctx -> {
    Map<String, Object> model = new HashMap<>();
    model.put("message", "Hello World");
    model.put("now", LocalDateTime.now());
    //Verwenden Sie die JSON-Methode.
    ctx.json(model);
  });

  // (4)
  app.post("/json", ctx -> {
    Map<String, Object> model = ctx.bodyAsClass(Map.class);
    //Gibt den HTTP-Status 200 zurück.
    ctx.status(200);
  });

}

Liefern Sie statische Dateien

Legen Sie in JavalinConfig fest, welche Dateien im Klassenpfad abgelegt werden sollen.

[project_root]
 |
 `--- /src
        |
        `--- /main
               |
               `--- /resources
                      |
                      `--- /public
                             |
                             `--- index.html

Geben Sie den Pfad in JavalinConfig.addStaticFiles an, um die an der obigen Position platzierte Datei "index.html" zu liefern.

configuration


config.addStaticFiles("/public");

Der GET-Anforderungshandler und der Pfad für das /, das in der Anwendung "Hallo Welt mit minimaler Konfiguration" hinzugefügt wurde, werden behandelt. Kommentieren Sie daher den folgenden Code aus.

// app.get("/", ctx -> ctx.result("Hello World"));

Wenn Sie die Javalin-Anwendung in diesem Status starten und auf "http: // localhost: 7000 / index.html" oder "http: // localhost: 7000" zugreifen, wird public / index.html zurückgegeben.

Sie können auch mit Verzeichnissen außerhalb des Klassenpfads arbeiten. Wenn Sie beispielsweise Dateien im Verzeichnis "C: \ var \ static" wie unten gezeigt verarbeiten möchten,

C:\var
    |
    `--- \static
           |
           `--- \images
                  |
                  `--- sample_1.jpg

Geben Sie den absoluten Pfad in JavalinConfig.addStaticFiles an. Wenn auf den hier angegebenen Pfad nicht zugegriffen werden kann, tritt beim Starten der Anwendung ein Fehler auf.

configuration


config.addStaticFiles("C:\\var\\static", Location.EXTERNAL);

Geben Sie den Pfad in der HTML-Datei wie unten gezeigt an.

<img src="/images/sample_1.jpg " />

Verwenden Sie WebJars

Fügen Sie die Bibliothek hinzu, die Sie für die Abhängigkeit von pom.xml verwenden möchten. In diesem Beispiel haben wir jQuery verwendet.

pom.xml


<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>

Dies allein aktiviert jQuery nicht. Aktivieren Sie Webjars in JavalinConfig.enableWebjars, um sie Ihrer Anwendung zur Verfügung zu stellen.

configuration


config.enableWebjars();

Geben Sie den Pfad in der HTML-Datei wie unten gezeigt an.

<script src="/webjars/jquery/3.4.1/jquery.min.js"></script>

Verwenden Sie Logback für die Protokollierung

Verwenden Sie slf4j und logback für die Protokollierung. Da slf4j-api bereits von Javalin abhängt, müssen wir nur logback-classic hinzufügen. Kommentieren Sie auch das zuerst hinzugefügte slf4j-simple aus.

pom.xml


<!--
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.26</version>
</dependency>
-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
    <scope>runtime</scope>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Erstellen Sie die Logback-Konfigurationsdatei logback.xml am folgenden Speicherort.

[project_root]
 |
 `--- /src
        |
        `--- /main
               |
               `--- /resources
                      |
                      `--- logback.xml

logback.xml

logback.xml


<?xml version="1.0" encoding="UTF-8"?>
<configuration status="INFO">
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Pattern>
        </encoder>
    </appender>

    <logger name="com.example.demo" level="debug" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="org.jooq" level="info" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
    <logger name="org.thymeleaf" level="info" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
    <root level="info">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

Ausgabe des Anwendungsprotokolls

Definieren Sie den Logger in der Klasse, in der Sie das Log ausgeben möchten, wie folgt.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
  static final Logger LOG = LoggerFactory.getLogger(App.class);

  // ...Kürzung...

}

Verwenden Sie Thymeleaf als Template-Engine

Javalin macht es einfach, Template-Engines wie Velocity, Freemarker und Thymeleaf zu verwenden. In diesem Beispiel wird Thymeleaf verwendet.

pom.xml


<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.0.9.RELEASE</version>
</dependency>

Platzieren Sie eine Vorlagendatei mit dem Namen "hello_world.html" an der folgenden Stelle. Eine Eigenschaftendatei mit demselben Dateinamen ist eine Nachrichtenressource.

[project_root]
 |
 `--- /src
        |
        `--- /main
               |
               `--- /resources
                      |
                      `--- /WEB-INF
                             |
                             `--- /templates
                                   |
                                   `--- hello_world.html
                                   `--- hello_world.properties
                                   `--- hello_world_ja.properties
                                   `--- hello_world_en.properties

hello_world.html

<!doctype html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script th:src="@{/webjars/jquery/3.4.1/jquery.min.js}"></script>
    <title>Hello World</title>
</head>
<body>
<div>
    <p th:text="${message}">message</p>
    <p th:text="${now}">now</p>
    <p th:text="#{msg.hello}">message</p>
</div>
</body>
</html>

Definition der Nachrichtenressource

hello_world.properties


msg.hello = Hello World (default)

hello_world_en.properties


msg.hello = Hello World

hello_world_ja.properties


msg.hello =Hallo Welt

Verwenden Sie die Rendermethode, um mit der Template-Engine zu antworten. Welche Template-Engine verwendet wird, hängt von der Erweiterung der Template-Datei ab. Im Fall von Thymeleaf gelten ".html", ".tl", ".thyme", ".thymeleaf".

app.get("/hello", ctx -> {
  Map<String, Object> model = new HashMap<>();
  model.put("message", "Hello World");
  model.put("now", LocalDateTime.now());
  ctx.render("/WEB-INF/templates/hello_world.html", model);
});

Wenn Sie auf localhost: 7000 / hello zugreifen, wird die von der Template-Engine gezeichnete HTML-Seite angezeigt.

Wechseln Sie Nachrichten mit Accept-Language in Ihrem Browser

Standardmäßig bestimmt das Standardgebietsschema beim Start der Anwendung die Nachrichtenressource. Um die Nachrichtenressource einer bestimmten Sprache zu verwenden, geben Sie beim Starten der Javalin-Anwendung das Gebietsschema in der Option JVM an. Geben Sie beispielsweise die folgenden Optionen an, um die Nachrichtenressource von en zu verwenden.

> java -Duser.language=en -Duser.country=US -jar <JAR-Datei>

Erstellen Sie eine Klasse, die FileRenderer implementiert, um die der Accept-Language des Browsers entsprechende Nachrichtenressource wie Spring Boot zu verwenden.

CustomThymeleafRenderer


import io.javalin.http.Context;
import io.javalin.plugin.rendering.FileRenderer;
import org.jetbrains.annotations.NotNull;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;

import java.util.Map;
import java.util.concurrent.TimeUnit;

public class CustomThymeleafRenderer implements FileRenderer {

  private final TemplateEngine templateEngine;

  public CustomThymeleafRenderer() {
    templateEngine = templateEngine();
  }

  @Override
  public String render(@NotNull String filePath, @NotNull Map<String, Object> model, @NotNull Context ctx) {
    WebContext context = new WebContext(ctx.req, ctx.res, ctx.req.getServletContext(), ctx.req.getLocale());
    context.setVariables(model);
    return templateEngine.process(filePath, context);
  }

  private TemplateEngine templateEngine() {
    TemplateEngine templateEngine = new TemplateEngine();
    templateEngine.setTemplateResolver(templateResolver());
    templateEngine.addDialect(new Java8TimeDialect());
    return templateEngine;
  }

  private ITemplateResolver templateResolver() {
    ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
    templateResolver.setPrefix("/WEB-INF/templates/");
    templateResolver.setSuffix(".html");
    templateResolver.setCharacterEncoding("UTF-8");
    templateResolver.setCacheable(true);
    templateResolver.setCacheTTLMs(TimeUnit.MINUTES.toMillis(30));
    templateResolver.setTemplateMode(TemplateMode.HTML);
    return templateResolver;
  }

}

Registrieren Sie diesen CustomThymeleafRenderer mit Ihrer Javalin-Anwendung.

JavalinRenderer.register(new CustomThymeleafRenderer(), ".html");

Bei dieser Anpassung ist es nicht erforderlich, den Pfad der Vorlagendatei anzugeben, sodass die Handler-Implementierung wie folgt lautet.

// ctx.render("/WEB-INF/templates/hello_world.html", model);
ctx.render("hello_world.html", model);

Der standardmäßig registrierte Renderer lautet übrigens wie folgt.

JavalinRenderer


object JavalinRenderer {

  init {
    register(JavalinVelocity, ".vm", ".vtl")
    register(JavalinFreemarker, ".ftl")
    register(JavalinMustache, ".mustache")
    register(JavalinJtwig, ".jtwig", ".twig", ".html.twig")
    register(JavalinPebble, ".peb", ".pebble")
    register(JavalinThymeleaf, ".html", ".tl", ".thyme", ".thymeleaf")
    register(JavalinCommonmark, ".md", ".markdown")
  }

  // ...Kürzung...

}

Verwenden Sie die Datenbank

Ich habe MySQL 8.0 für die Datenbank, jOOQ für das ORM und HikariCP für den Verbindungspool verwendet.

Datenbankvorbereitung

Erstellen Sie die Datenbank "sample_db" und den Benutzer "test_user" zur Verwendung in Ihrer Javalin-Anwendung als Benutzer mit Administratorrechten.

CREATE DATABASE IF NOT EXISTS sample_db
  CHARACTER SET = utf8mb4
  COLLATE = utf8mb4_general_ci
;

CREATE USER IF NOT EXISTS 'test_user'@'localhost'
  IDENTIFIED BY 'test_user'
  PASSWORD EXPIRE NEVER
;

GRANT ALL ON sample_db.* TO 'test_user'@'localhost';

Erstellen Sie dann eine "user" -Tabelle mit dem Benutzer "test_user".

DROP TABLE IF EXISTS user;
CREATE TABLE user (
  id BIGINT AUTO_INCREMENT                    COMMENT 'Benutzeridentifikation',
  nick_name VARCHAR(60) NOT NULL              COMMENT 'Spitzname',
  sex CHAR(1) NOT NULL                        COMMENT 'Geschlecht M.:Männlich F.:Weiblich',
  prefecture_id TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Präfektur 0:Unbekannt, 1:Hokkaido- 8:Kyushu-Okinawa',
  email VARCHAR(120)                          COMMENT 'Mail Adresse',
  memo TEXT                                   COMMENT 'Spalte Bemerkungen',
  create_at DATETIME NOT NULL DEFAULT NOW(),
  update_at DATETIME NOT NULL DEFAULT NOW(),
  PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8MB4
COMMENT = 'Benutzertabelle';

CREATE INDEX idx_sex on user (sex) USING BTREE;
CREATE INDEX idx_pref on user (prefecture_id) USING BTREE;

Abhängigkeit hinzufügen

Bearbeiten Sie die Datei pom.xml und fügen Sie die folgenden Abhängigkeiten hinzu.

pom.xml


<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.11.11</version>
</dependency>
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.3.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.17</version>
    <scope>runtime</scope>
</dependency>

Automatische Codegenerierung

jOOQ hat eine Funktion zum automatischen Generieren von Modellcode (Entitätscode) aus einem Datenbankschema, das als "jooq-codegen-maven" -Plug-In bezeichnet wird.

<plugin>
    <groupId>org.jooq</groupId>
    <artifactId>jooq-codegen-maven</artifactId>
    <version>3.11.11</version>
    <executions>
        <execution>
            <id>jooq-codegen</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>
    </dependencies>
    <configuration>
        <jdbc>
            <url>jdbc:mysql://localhost:3306</url>
            <user>test_user</user>
            <password>test_user</password>
        </jdbc>
        <generator>
            <database>
                <name>org.jooq.meta.mysql.MySQLDatabase</name>
                <includes>.*</includes>
                <inputSchema>sample_db</inputSchema>
                <!-- Configure type overrides for generated fields, attributes, sequences, parameters. -->
                <forcedTypes>
                    <forcedType>
                        <!--Specify the Java type of your custom type. This corresponds to the Converter's <U> type. -->
                        <userType>com.example.demo.converter.Prefecture</userType>
                        <!-- A converter implementation for the {@link #getUserType(). -->
                        <converter>com.example.demo.converter.PrefectureConverter</converter>
                        <!-- regex to match the column name -->
                        <expression>PREFECTURE_ID</expression>
                        <types>.*</types>
                    </forcedType>
                    <forcedType>
                        <userType>com.example.demo.converter.Sex</userType>
                        <converter>com.example.demo.converter.SexConverter</converter>
                        <expression>SEX</expression>
                        <types>.*</types>
                    </forcedType>
                </forcedTypes>
            </database>
            <target>
                <packageName>com.example.demo.model</packageName>
                <directory>src/main/java</directory>
            </target>
            <generate>
                <!-- A flag indicating whether Java 8's java.time types should be used by the source code generator, rather than JDBC's java.sql types. -->
                <javaTimeTypes>true</javaTimeTypes>
                <!-- Generate index information -->
                <indexes>true</indexes>
                <!-- Primary key / foreign key relations should be generated and used. This is a prerequisite for various advanced features -->
                <relations>true</relations>
                <!-- Generate deprecated code for backwards compatibility -->
                <deprecated>false</deprecated>
                <!-- Generate the {@link javax.annotation.Generated} annotation to indicate jOOQ version used for source code -->
                <generatedAnnotation>false</generatedAnnotation>
                <!-- Generate Sequence classes -->
                <sequences>true</sequences>
                <!-- Generate Key classes -->
                <keys>true</keys>
                <!-- Generate Table classes -->
                <tables>true</tables>
                <!-- Generate TableRecord classes -->
                <records>true</records>
                <!-- Generate POJOs -->
                <pojos>true</pojos>
                <!-- Generate basic equals() and hashCode() methods in POJOs -->
                <pojosEqualsAndHashCode>true</pojosEqualsAndHashCode>
                <!-- Generate basic toString() methods in POJOs -->
                <pojosToString>true</pojosToString>
                <!-- Generate serializable POJOs -->
                <serializablePojos>true</serializablePojos>
                <!-- Generated interfaces to be implemented by records and/or POJOs -->
                <interfaces>false</interfaces>
                <!-- Turn off generation of all global object references -->
                <globalObjectReferences>true</globalObjectReferences>
                <!-- Turn off generation of global catalog references -->
                <globalCatalogReferences>true</globalCatalogReferences>
                <!-- Turn off generation of global schema references -->
                <globalSchemaReferences>true</globalSchemaReferences>
                <!-- Turn off generation of global table references -->
                <globalTableReferences>true</globalTableReferences>
                <!-- Turn off generation of global sequence references -->
                <globalSequenceReferences>true</globalSequenceReferences>
                <!-- Turn off generation of global UDT references -->
                <globalUDTReferences>true</globalUDTReferences>
                <!-- Turn off generation of global routine references -->
                <globalRoutineReferences>true</globalRoutineReferences>
                <!-- Turn off generation of global queue references -->
                <globalQueueReferences>true</globalQueueReferences>
                <!-- Turn off generation of global database link references -->
                <globalLinkReferences>true</globalLinkReferences>
                <!-- Turn off generation of global key references -->
                <globalKeyReferences>true</globalKeyReferences>
                <!-- Generate fluent setters in records, POJOs, interfaces -->
                <fluentSetters>true</fluentSetters>
            </generate>
        </generator>
    </configuration>
</plugin>

Generieren Sie Code mit dem folgenden Befehl mvn. (Wird auch vom MVN-Paket generiert)

> mvn jooq-codegen:generate

Der Code wird im Paket com.example.demo.model ausgegeben. In diesem Beispiel wurde der folgende Code generiert.

[project_root]
 |
 `--- /src
        |
        `--- /main
               |
               `--- /java
                     |
                     `--- com.example.demo.model
                     |      |
                     |      `--- DefaultCatalog.java
                     |      `--- Indexes.java
                     |      `--- Keys.java
                     |      `--- SampleDb.java
                     |      `--- Tables.java
                     |
                     `--- com.example.demo.model.tables
                     |      |
                     |      `--- User.java                          // (1)
                     |
                     `--- com.example.demo.model.tables.records
                     |      |
                     |      `--- UserRecord.java                    // (2)
                     |
                     `--- com.example.demo.model.tables.pojos
                            |
                            `--- User.java                          // (3)

Javalin anwendungsseitige Implementierung

Verbindungseigenschaftendatei

Verbindungsinformationen werden in der Eigenschaftendatei verwaltet.

datasource.properties


datasource.jdbcUrl = jdbc:mysql://localhost:3306/sample_db
datasource.userName = test_user
datasource.password = test_user

DbPropertyLoader


import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Properties;

public class DbPropertyLoader {
  private final Properties prop;

  private static final String JDBC_URL = "datasource.jdbcUrl";
  private static final String USER_NAME = "datasource.userName";
  private static final String PASSWORD = "datasource.password";

  public DbPropertyLoader(String fileName) {
    prop = loadProperties(fileName);
  }

  public String getJdbcUrl() {
    return prop.getProperty(JDBC_URL);
  }

  public String getUserName() {
    return prop.getProperty(USER_NAME);
  }

  public String getPassword() {
    return prop.getProperty(PASSWORD);
  }

  private Properties loadProperties(String fileName) {
    try (InputStream stream = getClass().getClassLoader().getResourceAsStream(fileName)) {
      if (stream == null) {
        throw new IOException("Not Found : " + fileName);
      }
      Properties prop = new Properties();
      prop.load(stream);
      return prop;
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

}

Erstellen eines DSLContext

Um mit jOOQ auf die Datenbank zuzugreifen, benötigen Sie eine Instanz einer Klasse namens DSLContext für jOOQ. Ich habe eine Singleton-Holder-Klasse implementiert, die eine Instanz von DSLContext wie folgt enthält: Der DSLContext ist threadsicher.

DSLContext is thread safety?

DslConfigure


import org.jooq.Configuration;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.TransactionProvider;
import org.jooq.conf.Settings;
import org.jooq.impl.DefaultConfiguration;

import javax.sql.DataSource;
import java.util.Optional;

public interface DslConfigure {
  DSLContext getDslContext();

  DataSource getDataSource();
  SQLDialect getSqlDialect();
  Settings getSettings();

  // optional settings
  Optional<ConnectionProvider> getConnectionProvider();
  Optional<TransactionProvider> getTransactionProvider();

  default Configuration configuration() {
    Configuration config = new DefaultConfiguration();
    config
        .set(getSqlDialect())
        .set(getSettings());
    getConnectionProvider().ifPresentOrElse(cp -> {
      config.set(cp);
      getTransactionProvider().ifPresent(tp -> {
        config.set(tp);
      });
    }, () -> {
      config.set(getDataSource());
    });
    return config;
  }
}

DslContextHolder


import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.TransactionProvider;
import org.jooq.conf.Settings;
import org.jooq.conf.StatementType;
import org.jooq.impl.DSL;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultTransactionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.io.Serializable;
import java.util.Optional;

public class DslContextHolder implements DslConfigure, Serializable {
  private static final long serialVersionUID = -8276859195238889686L;
  private static final Logger LOG = LoggerFactory.getLogger(DslContextHolder.class);

  private static DslContextHolder instance;

  private final DSLContext dslContext;
  private final DataSource dataSource;
  private final Settings settings;
  private final ConnectionProvider connectionProvider;
  private final TransactionProvider transactionProvider;

  private DslContextHolder() {
    dataSource = createDataSource();
    settings = createSettings();
    connectionProvider = new DataSourceConnectionProvider(dataSource);
    transactionProvider = new DefaultTransactionProvider(connectionProvider, true);
    // transactionProvider = new ThreadLocalTransactionProvider(connectionProvider, true);
    dslContext = DSL.using(configuration());
  }

  public static DslContextHolder getInstance() {
    if (instance == null) {
      synchronized (DslContextHolder.class) {
        if (instance == null) {
          instance = new DslContextHolder();
          LOG.debug("DSL Context create : {}", instance.getDslContext().toString());
        }
      }
    }
    return instance;
  }

  public static void destroy() {
    if (instance != null) {
      synchronized (DslContextHolder.class) {
        if (instance != null) {
          LOG.debug("DSL Context destroy : {}", instance.getDslContext().toString());
          instance.dslContext.close();
          instance = null;
        }
      }
    }
  }

  DataSource createDataSource() {
    DbPropertyLoader prop = new DbPropertyLoader("datasource.properties");
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(prop.getJdbcUrl());
    config.setUsername(prop.getUserName());
    config.setPassword(prop.getPassword());
    config.setPoolName("hikari-cp");
    config.setAutoCommit(false);
    config.setConnectionTestQuery("select 1");
    config.setMaximumPoolSize(10);
    DataSource dataSource = new HikariDataSource(config);
    return dataSource;
  }

  Settings createSettings() {
    return new Settings()
        .withStatementType(StatementType.STATIC_STATEMENT)
        .withQueryTimeout(10)
        .withMaxRows(1000)
        .withFetchSize(20)
        .withExecuteLogging(true);
  }

  @Override
  public DSLContext getDslContext() {
    return this.dslContext;
  }

  @Override
  public DataSource getDataSource() {
    return this.dataSource;
  }

  @Override
  public SQLDialect getSqlDialect() {
    return SQLDialect.MYSQL_8_0;
  }

  @Override
  public Settings getSettings() {
    return this.settings;
  }

  @Override
  public Optional<ConnectionProvider> getConnectionProvider() {
    //
    return Optional.ofNullable(this.connectionProvider);
  }

  @Override
  public Optional<TransactionProvider> getTransactionProvider() {
    //
    return Optional.ofNullable(this.transactionProvider);
  }

}

Beispiel für die Service-Implementierung

Dies ist ein Implementierungsbeispiel für eine Serviceklasse, die über den DSLContext von jOOQ und den automatisch generierten Code (Entitätsklasse) auf die Datenbank zugreift.

UserService


import com.example.demo.model.tables.pojos.User;

import java.util.List;
import java.util.Optional;

public interface UserService {
  Optional<User> findById(Long id);
  List<User> findByNickName(String nickName);
  List<User> findAll();
  User save(User user);
  void remove(Long id);
}

UserServiceImpl


import com.example.demo.AppServiceException;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.model.tables.records.UserRecord;
import com.example.demo.service.UserService;
import org.jooq.DSLContext;
import org.jooq.Result;
import org.jooq.impl.DSL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static com.example.demo.model.Tables.USER;

public class UserServiceImpl implements UserService {
  private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
  private final DSLContext dsl;

  public UserServiceImpl(DSLContext dsl) {
    this.dsl = dsl;
  }

  @Override
  public Optional<User> findById(Long id) {
    LOG.debug("find by Id : {}", id);
    UserRecord record = dsl
        .selectFrom(USER)
        .where(USER.ID.eq(id))
        .fetchOne();
    if (record != null) {
      return Optional.of(record.into(User.class));
    }
    return Optional.empty();
  }

  @Override
  public List<User> findByNickName(String nickName) {
    LOG.debug("find by nickName : {}", nickName);
    Result<UserRecord> result = dsl
        .selectFrom(USER)
        .where(USER.NICK_NAME.contains(nickName))
        .orderBy(USER.NICK_NAME.asc())
        .fetch();
    return result.into(User.class);
  }

  @Override
  public List<User> findAll() {
    LOG.debug("findAll");
    Result<UserRecord> result = dsl
        .selectFrom(USER)
        .orderBy(USER.ID.desc())
        .fetch();
    return result.into(User.class);
  }

  @Override
  public User save(User user) {
    LOG.debug("save : {}", user);
    UserRecord result = dsl.transactionResult(conf -> {
      if (user.getId() == null || user.getId().equals(0L)) {
        return DSL.using(conf)
            .insertInto(USER,
                USER.NICK_NAME,
                USER.SEX,
                USER.PREFECTURE_ID,
                USER.EMAIL,
                USER.MEMO)
            .values(
                user.getNickName(),
                user.getSex(),
                user.getPrefectureId(),
                user.getEmail(),
                user.getMemo())
            .returning()
            .fetchOne();
      } else {
        return DSL.using(conf)
            .update(USER)
            .set(USER.NICK_NAME, user.getNickName())
            .set(USER.SEX, user.getSex())
            .set(USER.PREFECTURE_ID, user.getPrefectureId())
            .set(USER.EMAIL, user.getEmail())
            .set(USER.MEMO, user.getMemo())
            .set(USER.UPDATE_AT, LocalDateTime.now())
            .where(USER.ID.eq(user.getId()))
            .returning()
            .fetchOne();
      }
    });
    LOG.debug("save result : {}", result);
    if (result == null) {
      throw new AppServiceException("[save] Not Found saved user record.");
    }
    return result.into(User.class);
  }

  @Override
  public void remove(Long id) {
    LOG.debug("remove by id : {}", id);
    int result = dsl.transactionResult(conf -> {
      int count = DSL.using(conf)
          .selectCount()
          .from(USER)
          .where(USER.ID.eq(id))
          .execute();
      if (count != 1) {
        return count;
      }
      return DSL.using(conf)
          .deleteFrom(USER)
          .where(USER.ID.eq(id))
          .execute();
    });
    LOG.debug("remove result : {}", result);
    if (result == 0) {
      throw new AppServiceException("[remove] Not Found delete user record.");
    }
  }

}

Beispiel für die Controller-Implementierung

Dies ist ein Beispiel für die Implementierung eines Controllers, der die oben genannten Dienste verwendet.

UserController


import com.example.demo.model.tables.pojos.User;
import com.example.demo.service.UserService;
import io.javalin.Javalin;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.http.Context;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

public class UserController {
  private static final Logger LOG = LoggerFactory.getLogger(UserController.class);
  private final UserService userService;

  public UserController(UserService userService) {
    this.userService = userService;
  }

  void findById(@NotNull Context ctx) {
    Long id = ctx.pathParam("id", Long.class).get();
    userService.findById(id).ifPresentOrElse(user -> {
      ctx.json(user);
    },() -> {
      ctx.status(404);
    });
  }

  void findAll(@NotNull Context ctx) {
    List<User> users = userService.findAll();
    ctx.json(users);
  }

  void store(@NotNull Context ctx) {
    User user = ctx.bodyAsClass(User.class);
    User storedUser = userService.save(user);
    ctx.json(storedUser);
  }

  void remove(@NotNull Context ctx) {
    Long id = ctx.pathParam("id", Long.class).get();
    userService.remove(id);
    ctx.status(200);
  }

  public void bindEndpoints(@NotNull Javalin app) {
    app.routes(() -> {
      ApiBuilder.path("api/user", () -> {
        // GET /api/user
        ApiBuilder.get(this::findAll);
        // GET /api/user/:id
        ApiBuilder.get(":id", this::findById);
        // POST /api/user
        ApiBuilder.post(this::store);
        // DELETE /api/user/:id
        ApiBuilder.delete(":id", this::remove);
      });
    });
  }

}

Der Handler war am Startpunkt der Javalin-Anwendung gebunden.

public static void main(String... args) {
  Javalin app = Javalin
    .create(config -> {
      // configuration
    })
    .start(7000);

  // ...Kürzung...

  //Generieren Sie DSLContext
  final DSLContext dsl = DslContextHolder.getInstance().getDslContext();

  //Binden Sie den Handler an die Anwendung
  new UserController(new UserServiceImpl(dsl)).bindEndpoints(app);

  // ...Kürzung...
}

Testcode

Ich habe JUnit 5 für das Testframework und AssertJ für die Assertion verwendet.

Abhängigkeit hinzufügen

Bearbeiten Sie die Datei pom.xml und fügen Sie die folgenden Abhängigkeiten hinzu.

pom.xml


<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.13.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <!--<version>1.4.199</version>-->
    <version>1.4.193</version>
    <scope>test</scope>
</dependency>

Serviceklassentest

Der Serviceklassentest wird als Komponententest implementiert. Verwenden Sie H2 für Datenbanken externer Ressourcen, von denen der Dienst abhängt. Bereiten Sie die Testumgebung vor, indem Sie vor Beginn des Tests ein Schema erstellen, die Testdaten für jede Testmethode laden und das Schema nach Beendigung des Tests löschen.

Verbindungseigenschaftendatei zum Testen

test-datasource.properties


#datasource.driver = org.h2.Driver
datasource.jdbcUrl = jdbc:h2:mem:sample_db;MODE=MySQL;DB_CLOSE_DELAY=-1;
#datasource.jdbcUrl = jdbc:h2:tcp://localhost:9092/./sample_db;MODE=MySQL;DATABASE_TO_UPPER=false
datasource.userName = sa
datasource.password =

Erstellen eines Test-DSLContext

Implementieren Sie den Code, der den DSLContext für H2 erstellt.

TestDslContextHolder


import com.example.demo.config.DbPropertyLoader;
import com.example.demo.config.DslConfigure;
import org.h2.jdbcx.JdbcDataSource;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.TransactionProvider;
import org.jooq.conf.Settings;
import org.jooq.conf.StatementType;
import org.jooq.impl.DSL;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultTransactionProvider;

import javax.sql.DataSource;
import java.io.Serializable;
import java.util.Optional;

public class TestDslContextHolder implements DslConfigure, Serializable {
  private static final long serialVersionUID = -5728144007357037196L;

  private static TestDslContextHolder instance;

  private final DSLContext dslContext;
  private final DataSource dataSource;
  private final Settings settings;
  private final ConnectionProvider connectionProvider;
  private final TransactionProvider transactionProvider;

  public TestDslContextHolder() {
    dataSource = createDataSource();
    settings = createSettings();
    connectionProvider = new DataSourceConnectionProvider(dataSource);
    transactionProvider = new DefaultTransactionProvider(connectionProvider, true);
    // transactionProvider = new ThreadLocalTransactionProvider(connectionProvider, true);
    dslContext = DSL.using(configuration());
  }

  public static TestDslContextHolder getInstance() {
    if (instance == null) {
      synchronized (TestDslContextHolder.class) {
        if (instance == null) {
          instance = new TestDslContextHolder();
        }
      }
    }
    return instance;
  }

  public static void destroy() {
    if (instance != null) {
      synchronized (TestDslContextHolder.class) {
        if (instance != null) {
          instance.dslContext.close();
          instance = null;
        }
      }
    }
  }

  DataSource createDataSource() {
    DbPropertyLoader prop = new DbPropertyLoader("test-datasource.properties");
    JdbcDataSource dataSource = new JdbcDataSource();
    dataSource.setURL(prop.getJdbcUrl());
    dataSource.setUser(prop.getUserName());
    dataSource.setPassword(prop.getPassword());
    return dataSource;
  }

  Settings createSettings() {
    return new Settings()
        .withStatementType(StatementType.STATIC_STATEMENT)
        .withQueryTimeout(10)
        .withMaxRows(1000)
        .withFetchSize(20)
        .withExecuteLogging(true)
        .withDebugInfoOnStackTrace(true)
        .withRenderFormatted(true);
  }

  @Override
  public DSLContext getDslContext() {
    return this.dslContext;
  }

  @Override
  public DataSource getDataSource() {
    return this.dataSource;
  }

  @Override
  public SQLDialect getSqlDialect() {
    return SQLDialect.H2;
  }

  @Override
  public Settings getSettings() {
    return this.settings;
  }

  @Override
  public Optional<ConnectionProvider> getConnectionProvider() {
    //
    return Optional.ofNullable(this.connectionProvider);
  }

  @Override
  public Optional<TransactionProvider> getTransactionProvider() {
    //
    return Optional.ofNullable(this.transactionProvider);
  }

}

Servicetestklasse Dieser Code ist ein Muster, das In-Memory H2 verwendet. (Das Muster der Verwendung von H2 im Servermodus wird später beschrieben.)

TestService


import org.jooq.DSLContext;
import org.jooq.Queries;
import org.jooq.Query;
import org.jooq.Schema;
import org.jooq.Table;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;

import java.util.List;

public interface TestService {

  DSLContext getDSLContext();
  void setDSLContext(DSLContext dsl);

  default void createSchema(Schema schema) {
    Queries queries = getDSLContext().ddl(schema);
    for (Query query : queries.queries()) {
      getDSLContext().execute(query);
    }
  }

  default void dropSchema(Schema schema) {
    List<Table<?>> tables = schema.getTables();
    for (Table<?> table : tables) {
      getDSLContext().dropTableIfExists(table).execute();
    }
    getDSLContext().dropSchemaIfExists(schema).execute();
  }

  default void cleanUp(List<Table<?>> tables) {
    getDSLContext().transaction(conf -> {
      for (Table table : tables) {
        DSL.using(conf).deleteFrom(table).execute();
      }
    });
  }

  default void testDataRollback() {
    throw new DataAccessException("rollback");
  }

  default void isFailed(RuntimeException e) {
    if (!e.getMessage().equals("rollback")) {
      System.err.println(e);
      throw e;
    }
  }

}

UserServiceImplTests


import com.example.demo.TestDslContextHolder;
import com.example.demo.converter.Prefecture;
import com.example.demo.converter.Sex;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.model.tables.records.UserRecord;
import com.example.demo.service.TestService;
import com.example.demo.service.UserService;
import org.assertj.core.groups.Tuple;
import org.jooq.DSLContext;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static com.example.demo.model.SampleDb.SAMPLE_DB;
import static com.example.demo.model.Tables.*;

@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserServiceImplTests implements TestService {

  private DSLContext dsl;

  @Override
  public DSLContext getDSLContext() {
    return dsl;
  }

  @Override
  public void setDSLContext(DSLContext dsl) {
    this.dsl = dsl;
  }

  @BeforeAll
  public void setupSchema() {
    System.out.println("setup class");
    setDSLContext(TestDslContextHolder.getInstance().getDslContext());
    createSchema(SAMPLE_DB);
  }

  @AfterAll
  public void tearDownSchema() {
    System.out.println("teardown class");
    dropSchema(SAMPLE_DB);
    TestDslContextHolder.destroy();
  }

  @BeforeEach
  public void setup() {
    System.out.println("setup");
  }

  @AfterEach
  public void tearDown() {
    System.out.println("teardown");
    cleanUp(List.of(USER));
  }

  @Test
  public void findById() {
    System.out.println("findById");
    try {
      dsl.transaction(conf -> {
        // setup test data
        DSL.using(conf)
            .insertInto(USER,
                USER.ID,
                USER.NICK_NAME,
                USER.SEX,
                USER.PREFECTURE_ID,
                USER.EMAIL,
                USER.MEMO)
            .values(1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "[email protected]", "memo aaa")
            .values(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "[email protected]", null)
            .values(3L, "nick name CCC", Sex.FEMALE, Prefecture.KANTOU, "[email protected]", "memo ccc")
            .execute();

        // exercise
        UserService sut = new UserServiceImpl(DSL.using(conf));
        Optional<User> actual = sut.findById(1L);

        // verify
        assertThat(actual.isPresent()).isTrue();
        actual.ifPresent(user -> {
          assertThat(user)
              .extracting(
                  "id",
                  "nickName",
                  "sex",
                  "prefectureId",
                  "email",
                  "memo")
              .contains(
                  1L,
                  "nick name AAA",
                  Sex.FEMALE,
                  Prefecture.HOKKAIDO,
                  "[email protected]",
                  "memo aaa");
        });

        testDataRollback();
      });
    } catch (DataAccessException e) {
      isFailed(e);
    }
  }

  @Test
  public void findByNickName() {
    System.out.println("findByNickName");
    try {
      dsl.transaction(conf -> {
        // setup test data
        DSL.using(conf)
            .insertInto(USER,
                USER.ID,
                USER.NICK_NAME,
                USER.SEX,
                USER.PREFECTURE_ID,
                USER.EMAIL,
                USER.MEMO)
            .values(1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "[email protected]", "memo aaa")
            .values(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "[email protected]", null)
            .values(3L, "nick name CCC", Sex.FEMALE, Prefecture.KANTOU, "[email protected]", "memo ccc")
            .execute();

        // exercise
        UserService sut = new UserServiceImpl(DSL.using(conf));
        List<User> actual = sut.findByNickName("BBB");

        // verify
        assertThat(actual).hasSize(1);
        assertThat(actual)
            .extracting(
                "id",
                "nickName",
                "sex",
                "prefectureId",
                "email",
                "memo")
            .contains(Tuple.tuple(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "[email protected]", null));

        testDataRollback();
      });
    } catch (DataAccessException e) {
      isFailed(e);
    }
  }

  @Test
  public void save() {
    try {
      dsl.transaction(conf -> {
        // exercise
        User testData = new User(null, "nick name DDD", Sex.MALE, Prefecture.KANTOU, "[email protected]", null, null, null);
        UserService sut = new UserServiceImpl(DSL.using(conf));
        User expected = sut.save(testData);

        // verify
        Optional<User> actual = sut.findById(expected.getId());
        assertThat(actual.isPresent()).isTrue();
        actual.ifPresent(user -> {
          assertThat(user)
              .extracting(
                  "id",
                  "nickName",
                  "sex",
                  "prefectureId",
                  "email",
                  "memo")
              .contains(
                  expected.getId(),
                  expected.getNickName(),
                  expected.getSex(),
                  expected.getPrefectureId(),
                  expected.getEmail(),
                  expected.getMemo());
        });

        testDataRollback();
      });
    } catch (DataAccessException e) {
      isFailed(e);
    }
  }

  @Test
  public void remove() {
    try {
      dsl.transaction(conf -> {
        // setup
        Long userId = 1L;
        DSL.using(conf)
            .insertInto(USER,
                USER.ID,
                USER.NICK_NAME,
                USER.SEX,
                USER.PREFECTURE_ID,
                USER.EMAIL,
                USER.MEMO)
            .values(
                userId,
                "nick name EEE",
                Sex.MALE,
                Prefecture.CHUBU,
                "[email protected]",
                "memo eee")
            .execute();

        // exercise
        UserService sut = new UserServiceImpl(DSL.using(conf));
        sut.remove(userId);

        // verify
        UserRecord actual = DSL.using(conf)
            .selectFrom(USER)
            .where(USER.ID.eq(userId))
            .fetchOne();
        assertThat(actual).isNull();

        testDataRollback();
      });
    } catch (DataAccessException e) {
      isFailed(e);
    }
  }

}

Verwenden Sie H2 im Servermodus

Im Servermodus werden Schemas, Tabellen und Testdaten durch Ausführen von SQL-Skripten erstellt, die an den folgenden Speicherorten abgelegt werden. Durch Ausführen des Tests wird eine Datenbankdatei für die H2-Datenbank als "sample_db.mv.db" -Datei erstellt.

[project_root]
 |
 `--- /h2
        |
        `--- /sql
        |      |
        |      `--- 01_schema.sql     //Erstellen Sie ein Schema
        |      `--- 02_table.sql      //Erstellen Sie eine Tabelle
        |      `--- 03_data.sql       //Daten einfügen
        |      `--- 04_table.sql      //Lass den Tisch fallen
        |      `--- 05_schema.sql     //Löschen Sie das Schema
        |
        `--- sample_db.mv.db

TestH2Server


import com.example.demo.config.DbPropertyLoader;
import org.h2.tools.RunScript;
import org.h2.tools.Server;

import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

public class TestH2Server implements AutoCloseable {

  private final Server server;

  private final String jdbcUrl;
  private final String userName;
  private final String password;

  public TestH2Server() {
    DbPropertyLoader prop = new DbPropertyLoader("test-datasource.properties");
    jdbcUrl = prop.getJdbcUrl();
    userName = prop.getUserName();
    password = prop.getPassword();
    String[] params = new String[] {"-tcp", "-tcpPort", "9092", "-baseDir", "./h2"};
    try {
      server = Server.createTcpServer(params);
    } catch (SQLException e) {
      System.err.println(e.getMessage());
      throw new RuntimeException(e);
    }
  }

  public void start() {
    System.out.println("server start");
    try {
      server.start();
      status();
      Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdown()));
    } catch (SQLException e) {
      throw new RuntimeException(e);
    }
  }

  public void runScript(List<String> scripts) {
    try {
      for (String script : scripts) {
        System.out.println("run script : " + script);
        RunScript.execute(jdbcUrl, userName, password, script, StandardCharsets.UTF_8, false);
      }
    } catch (SQLException e) {
      System.err.println("run script : " + e.getMessage());
      throw new RuntimeException(e);
    }
  }

  public void shutdown() {
    if (server.isRunning(true)) {
      System.out.println("server shutdown");
      server.shutdown();
    }
  }

  public void status() {
    System.out.println(server.getStatus());
  }

  // for test
  public static void main(String ... args) {
    try (TestH2Server h2 = new TestH2Server()) {
      h2.start();
      h2.status();
      h2.runScript(List.of("./h2/sql/01_schema.sql", "./h2/sql/02_table.sql", "./h2/sql/03_data.sql"));
      h2.test();
      h2.runScript(List.of("./h2/sql/04_table.sql", "./h2/sql/05_schema.sql"));
    }
    System.exit(0);
  }

  public void test() {
    System.out.println("test");
    try (Connection conn = DriverManager.getConnection(jdbcUrl, userName, password)) {
      PreparedStatement ps = conn.prepareStatement("select * from sample_db.user");
      ResultSet rs = ps.executeQuery();
      while (rs.next()) {
        System.out.println("id : " + rs.getLong("id"));
        System.out.println("nick_name : " + rs.getString("nick_name"));
        System.out.println("sex : " + rs.getString("sex"));
        System.out.println("prefecture_id : " + rs.getInt("prefecture_id"));
        System.out.println("email : " + rs.getString("email"));
        System.out.println("memo : " + rs.getString("memo"));
        System.out.println("createAt : " + rs.getTimestamp("create_at"));
        System.out.println("updateAt : " + rs.getTimestamp("update_at"));
      }
    } catch (SQLException e) {
      System.err.println("test error : " + e.getMessage());
      throw new RuntimeException(e);
    }
  }

  @Override
  public void close() {
    shutdown();
  }

}

Ändern Sie die Testklasse des Dienstes, um den H2-Server zu verwenden. Die zu ändernden Teile sind die Methoden setupSchema und tearDownSchema.

@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserServiceImplTests implements TestService {

  private DSLContext dsl;
  private TestH2Server h2;

  private List<String> initScripts = List.of(
      "./h2/sql/01_schema.sql",
      "./h2/sql/02_table.sql"
  );

  private List<String> clearScripts = List.of(
      "./h2/sql/04_table.sql",
      "./h2/sql/05_schema.sql"
  );

  @Override
  public DSLContext getDSLContext() {
    return dsl;
  }

  @Override
  public void setDSLContext(DSLContext dsl) {
    this.dsl = dsl;
  }

  @BeforeAll
  public void setupSchema() {
    System.out.println("setup class");
    h2 = new TestH2Server();
    h2.start();
    h2.runScript(initScripts);
    setDSLContext(TestDslContextHolder.getInstance().getDslContext());
  }

  @AfterAll
  public void tearDownSchema() {
    System.out.println("teardown class");
    TestDslContextHolder.destroy();
    h2.runScript(clearScripts);
    h2.shutdown();
  }

  //...Kürzung...

}

Controller-Klassentest

Der Controller-Klassentest wird als Integrationstest implementiert. Die Datenbank verwendet dasselbe MySQL wie in der realen Welt, sodass Sie beim Ausführen des Tests Zugriff auf den MySQL-Server benötigen.

Controller-Testklasse

import com.example.demo.config.DslContextHolder;
import com.example.demo.config.JacksonConfig;
import com.example.demo.converter.Prefecture;
import com.example.demo.converter.Sex;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.service.impl.UserServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.Javalin;
import io.javalin.plugin.json.JavalinJackson;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.assertj.core.api.Assertions.assertThat;

@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserControllerTests {

  Javalin app;
  int port;
  ObjectMapper objectMapper;

  @BeforeAll
  public void setup() {
    port = 7000;
    app = Javalin.create().start(port);
    objectMapper = JacksonConfig.getObjectMapper();
    JavalinJackson.configure(objectMapper);
  }

  @AfterAll
  public void tearDown() {
    app.stop();
  }

  @Test
  public void findById() throws Exception {
    // setup
    new UserController(new UserServiceImpl(DslContextHolder.getInstance().getDslContext()))
        .bindEndpoints(app);

    // exercise
    Long userId = 1L;
    String testTargetUrl = String.format("http://localhost:%d/api/user/%d", port, userId);

    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(testTargetUrl))
        .header("Accept", "application/json")
        .GET()
        .build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

    // verify
    assertThat(response.statusCode()).isEqualTo(200);
    User actual = objectMapper.readValue(response.body(), User.class);
    assertThat(actual)
        .extracting(
            "id",
            "nickName",
            "sex",
            "prefectureId",
            "email",
            "memo")
        .contains(
            1L,
            "test user 1",
            Sex.MALE,
            Prefecture.HOKKAIDO,
            "[email protected]",
            "memo 1"
        );
  }

}

Recommended Posts

Ein Hinweis bei der Untersuchung von Javalin
Ein Hinweis, wenn der Heroku-Befehl nicht mehr verfügbar ist
Hinweise zu Java GC
Hinweise zum Umfang
Private Notiz über AtomicReference
Ein Hinweis, als ich süchtig danach war, Ubuntu auf WSL1 in WSL2 zu konvertieren
Hinweise zu Spaltenfamilien in RocksDB
Vorsichtsmaßnahmen beim Schreiben eines Programms: Teil 3
Denken bei der Einführung einer neuen Bibliothek
Erstellen Sie eine Webanwendung mit Javalin
Notizen von Personen, die beim Versuch, ein Rails-Projekt zu erstellen, gestolpert sind