Une note lors de l'examen de Javalin

Aperçu

** Qu'est-ce que Javalin **

Javalin est un framework Web léger pour Java et Kotlin. Tiré du Web Framework appelé Spark Framework, la version 0.0.1 est sortie en mai 2017 et la 3.4.1 en août 2019. Les principales fonctionnalités sont citées ci-dessous sur GitHub. (La traduction japonaise est la traduction de Google)

https://github.com/tipsy/javalin

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

Javelin est une bibliothèque plutôt qu'un framework. Quelques points clés:

La première moitié de cet article est un mémo sur l'implémentation d'une application qui affiche "Hello World" avec la configuration minimale, et la seconde moitié est un mémo de ce que j'ai étudié / essayé en implémentant une application Web simple.

environnement

référence

Hello World avec une configuration minimale

Structure du projet

C'est un projet Maven normal.

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

pom.xml

Les dépendances requises pour une application Javalin minimale sont javalin et 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>

Implémentation du point de lancement de l'application

Implémentez le point de lancement de l'application Javalin dans la méthode principale de la classe App (le nom de la classe est arbitraire).

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

Lancer depuis l'IDE

Lorsque vous exécutez la méthode App.main, Javalin démarre et le journal suivant est sorti sur la console. Comme vous pouvez le voir dans le journal, il a fallu moins d'une seconde pour démarrer dans mon environnement de développement. Lorsque vous accédez à http: // localhost: 7000, vous obtiendrez une réponse textuelle / simple de" 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/

Commencer à partir du fichier jar

Lorsque vous construisez, un fichier jar appelé demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar sera généré dans le répertoire cible. L'application Javalin utilise la Jetty intégrée, vous pouvez donc démarrer l'application Web avec java -jar <fichier jar>.

> 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

Voici un exemple de modification des paramètres régionaux par défaut avec les paramètres JVM.

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

Ceci termine l'implémentation de l'application "Hello World avec configuration minimale".

À propos des fonctionnalités de Javalin

À partir de là, ce sera un mémo lors de la mise en œuvre d'une application Web simple avec Javalin.

Comment implémenter un gestionnaire HTTP

Le gestionnaire de requêtes GET (modèle 1 ci-dessous) écrit ci-dessus peut également être implémenté en tant que modèle 2 et modèle 3.

** modèle 1 **

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

** Motif 2 **

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

}

** Motif 3 **

Dans le tutoriel sur le site officiel, il y a aussi un exemple décrit dans le champ statique.

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

}

Gestionnaire HTTP

Outre GET, il existe POST, PUT, PATCH, DELETE, HEAD et OPTIONS.

Méthode HTTP Signature
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)

avant / après le point de terminaison

Vous pouvez définir les points de terminaison avant / après pour qu'ils s'exécutent avant et après le gestionnaire HTTP.

point final Signature
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)

Regroupement de gestionnaires HTTP

point final Méthode HTTP une fonction
/api/v1/users GET Obtenir la liste des utilisateurs
/api/v1/user/:id GET Obtenir l'utilisateur en spécifiant son ID
/api/v1/user POST Enregistrer l'utilisateur
/api/v1/user/:id DELETE Supprimer l'utilisateur en spécifiant son ID
/api/v2/users GET Obtenir la liste des utilisateurs
/api/v2/user/:id GET Obtenir l'utilisateur en spécifiant son ID
/api/v2/user POST Enregistrer l'utilisateur
/api/v2/user/:id DELETE Supprimer l'utilisateur en spécifiant son ID

Si vous avez deux de ces points de terminaison, v1 et v2, vous pouvez regrouper et définir des gestionnaires HTTP comme indiqué ci-dessous.

UserController v1Controller = /*implémentation de la version 1*/
UserController v2Controller = /*Implémentation de la 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);
    });
  });

});

À propos de io.javalin.http.Context

La classe du paramètre de méthode de gestionnaire HTTP ʻio.javalin.http.Context` fournit les fonctionnalités et les données nécessaires aux requêtes et réponses HTTP. Ce qui suit est une citation de la page de document du site officiel.

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.

Le contexte a également des champs req et res qui contiennent des instances de HttpServletRequest et HttpServletResponse.

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

Comment recevoir les paramètres de demande

Paramètres de requête

Une méthode qui reçoit les paramètres spécifiés dans la chaîne de requête

Signature
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)

Paramètres de formulaire

Les formulaires ont des méthodes similaires aux paramètres de requête. Malheureusement, il semble que vous ne puissiez pas lier les champs du formulaire entier à un objet.

Signature
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)

Paramètres de chemin

Une méthode qui en reçoit une partie en tant que paramètre d'un chemin tel que / api / user / 1

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

La partie du chemin du gestionnaire de requêtes avec un deux-points (:) est identifiée comme paramètre de chemin.

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

Configuration de l'application Javelin

Il peut être personnalisé en passant une instance de la classe JavalinConfig à la méthode Javalin.create. En plus de transmettre une instance de JavalinConfig comme indiqué ci-dessous, vous pouvez également l'écrire dans une expression lambda.

exemple

configuration


JavalinConfig config = new JavalinConfig();
// configuration
Javalin
    .create(config)
// ...réduction...

** expression lambda **

configuration


Javalin
    .create(config -> {
        // configuration
    })
// ...réduction...

Principales configurations

Paramètre de chemin de contexte (la valeur par défaut est "/")

config.contextPath = "/app";

Définition du type de contenu par défaut (la valeur par défaut est "text / plain")

config.defaultContentType = "application/json";

Paramètre ETag (la valeur par défaut est false)

config.autogenerateEtags = true;

La sortie du journal des requêtes peut être définie avec JavalinConfig.requestLogger. Le deuxième argument (execTime) est le temps nécessaire à la requête (ms).

configuration


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

Configuration de développement

Certaines fonctionnalités sont utiles à activer pendant le développement.

enableDevLogging

configuration


config.enableDevLogging();

Puisqu'il y a beaucoup d'informations, je pense qu'elles seront utilisées pour l'activer lors de l'enquête lorsqu'un problème survient, plutôt que de toujours l'activer.

[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"));

Vous pouvez voir la liste des gestionnaires de requêtes ajoutés à votre application Javalin en enregistrant ce plugin et en accédant à http: // localhost: 7000 / overview. overview.png

Personnaliser la réponse aux erreurs

Il existe une classe d'exception correspondant à l'état HTTP qui hérite de l'exception HttpResponseException de Javalin.

Status Classe d'exception message
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

Gérer les exceptions uniques

Vous pouvez gérer les exceptions spécifiques à l'application et renvoyer des réponses d'erreur arbitraires. Si une exception spécifique à l'application appelée AppServiceException ci-dessous est levée,

AppServiceException


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

Pour gérer cette exception et renvoyer l'état HTTP, mappez l'exception à votre application Javalin. Cet exemple ne renvoie que 500 statuts.

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

Pour renvoyer un message de réponse d'erreur arbitraire et un état HTTP, procédez comme suit. Vous pouvez également renvoyer un message au format json en utilisant la méthode json au lieu de la méthode result.

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

Gérer json dans la requête / réponse

Ajoutez jackson à la dépendance afin que la requête / réponse puisse gérer json. Le deuxième jackson-datatype-jsr310 est requis lorsque vous travaillez avec l'API Java 8 Date and Time.

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>

Implémentez un gestionnaire de requêtes GET qui répond à json. Utilisez la méthode json pour répondre à json.

public static void main(String ... args) {
  // ...réduction

  // (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());
    //Utilisez la méthode json.
    ctx.json(model);
  });

  // (4)
  app.post("/json", ctx -> {
    Map<String, Object> model = ctx.bodyAsClass(Map.class);
    //Renvoie l'état HTTP 200.
    ctx.status(200);
  });

}

Livrer des fichiers statiques

Pour distribuer les fichiers placés sur le chemin de classe, définissez dans JavalinConfig.

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

Pour livrer ʻindex.html` placé à l'emplacement ci-dessus, spécifiez le chemin dans JavalinConfig.addStaticFiles.

configuration


config.addStaticFiles("/public");

Le gestionnaire de requêtes GET et le chemin pour le / ajouté dans l'application "Hello World avec configuration minimale" seront couverts, alors commentez le code suivant.

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

Si vous démarrez l'application Javalin dans cet état et accédez à http: // localhost: 7000 / index.html ou http: // localhost: 7000, public / index.html sera renvoyé.

Vous pouvez également travailler avec des répertoires en dehors du chemin de classe. Par exemple, si vous souhaitez gérer des fichiers dans le répertoire C: \ var \ static comme indiqué ci-dessous,

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

Spécifiez le chemin absolu dans JavalinConfig.addStaticFiles. Si le chemin spécifié ici n'est pas accessible, une erreur se produira au démarrage de l'application.

configuration


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

Spécifiez le chemin dans le fichier html comme indiqué ci-dessous.

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

Utiliser WebJars

Ajoutez la bibliothèque que vous souhaitez utiliser pour la dépendance de pom.xml. Dans cet exemple, nous avons utilisé jQuery.

pom.xml


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

Cela seul n'activera pas jQuery. Activez les Webjars dans JavalinConfig.enableWebjars pour les rendre disponibles pour votre application.

configuration


config.enableWebjars();

Spécifiez le chemin dans le fichier html comme indiqué ci-dessous.

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

Utiliser la journalisation pour la journalisation

Utilisez slf4j et logback pour la journalisation. Puisque slf4j-api dépend déjà de Javalin, il suffit d'ajouter logback-classic. Mettez également en commentaire le slf4j-simple qui a été ajouté en premier.

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>

Créez le fichier de configuration de journalisation logback.xml à l'emplacement suivant.

[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>

Sortie du journal d'application

Définissez l'enregistreur dans la classe dans laquelle vous souhaitez générer le journal comme suit.

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

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

  // ...réduction...

}

Utilisez Thymeleaf comme moteur de modèle

Javalin facilite l'utilisation de moteurs de modèles tels que Velocity, Freemarker et Thymeleaf. Cet exemple utilise Thymeleaf.

pom.xml


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

Placez un fichier modèle appelé hello_world.html à l'emplacement suivant. Un fichier de propriétés portant le même nom de fichier est une ressource de message.

[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>

Définition de la ressource de message

hello_world.properties


msg.hello = Hello World (default)

hello_world_en.properties


msg.hello = Hello World

hello_world_ja.properties


msg.hello =Bonjour le monde

Utilisez la méthode de rendu pour répondre à l'aide du moteur de modèle. Le moteur de modèle utilisé est déterminé par l'extension du fichier de modèle. Dans le cas de Thymeleaf, «.html», «.tl», «.thyme», «.thymeleaf» sont applicables.

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

Lorsque vous accédez à localhost: 7000 / hello, vous verrez la page html dessinée par le moteur de modèle.

Changer de message avec Accept-Language dans votre navigateur

Par défaut, la ressource de message est déterminée par les paramètres régionaux par défaut au démarrage de l'application. Pour utiliser la ressource de message d'une certaine langue, spécifiez les paramètres régionaux dans l'option JVM lors du démarrage de l'application Javalin. Par exemple, pour utiliser la ressource de message de en, spécifiez les options suivantes.

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

Pour utiliser la ressource de message correspondant à Accept-Language du navigateur comme Spring Boot, créez une classe qui implémente FileRenderer.

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

}

Enregistrez ce CustomThymeleafRenderer avec votre application Javalin.

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

Avec cette personnalisation, il n'est pas nécessaire de spécifier le chemin du fichier modèle, l'implémentation du gestionnaire est donc la suivante.

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

Au fait, le moteur de rendu enregistré par défaut est le suivant.

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")
  }

  // ...réduction...

}

Utilisez la base de données

J'ai utilisé MySQL 8.0 pour la base de données, jOOQ pour l'ORM et HikariCP pour le pool de connexion.

Préparation de la base de données

Créez la base de données sample_db et l'utilisateur test_user à utiliser dans votre application Javalin en tant qu'utilisateur avec des privilèges d'administrateur.

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';

Créez ensuite la table ʻuser avec l'utilisateur test_user`.

DROP TABLE IF EXISTS user;
CREATE TABLE user (
  id BIGINT AUTO_INCREMENT                    COMMENT 'Identifiant d'utilisateur',
  nick_name VARCHAR(60) NOT NULL              COMMENT 'surnom',
  sex CHAR(1) NOT NULL                        COMMENT 'Sexe M:Homme F:Femme',
  prefecture_id TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Préfecture 0:Inconnu, 1:Hokkaido- 8:Kyushu-Okinawa',
  email VARCHAR(120)                          COMMENT 'adresse mail',
  memo TEXT                                   COMMENT 'Colonne Remarques',
  create_at DATETIME NOT NULL DEFAULT NOW(),
  update_at DATETIME NOT NULL DEFAULT NOW(),
  PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8MB4
COMMENT = 'Table des utilisateurs';

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

Ajouter une dépendance

Modifiez pom.xml et ajoutez les dépendances suivantes.

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>

Génération de code automatique

jOOQ a une fonction pour générer automatiquement du code de modèle (entité) à partir d'un schéma de base de données appelé le plug-in jooq-codegen-maven.

<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>

Générez du code avec la commande mvn suivante. (Également généré par le package mvn)

> mvn jooq-codegen:generate

Le code est sorti dans le package com.example.demo.model. Dans cet exemple, le code suivant a été généré.

[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)

Implémentation côté application Javalin

Fichier de propriétés de connexion

Les informations de connexion sont gérées dans le fichier de propriétés.

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

}

Créer un DSLContext

Pour accéder à la base de données avec jOOQ, vous avez besoin d'une instance d'une classe appelée DSLContext pour jOOQ. J'ai implémenté une classe de détenteur de singleton qui contient une instance de DSLContext comme suit: Le DSLContext est thread-safe.

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

}

Exemple d'implémentation de service

Ceci est un exemple d'implémentation d'une classe de service qui accède à la base de données à l'aide du DSLContext de jOOQ et du code généré automatiquement (classe d'entité).

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.");
    }
  }

}

Exemple d'implémentation du contrôleur

Ceci est un exemple d'implémentation d'un contrôleur qui utilise les services ci-dessus.

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

}

Le gestionnaire était lié au point de lancement de l'application Javalin.

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

  // ...réduction...

  //Générer DSLContext
  final DSLContext dsl = DslContextHolder.getInstance().getDslContext();

  //Lier le gestionnaire à l'application
  new UserController(new UserServiceImpl(dsl)).bindEndpoints(app);

  // ...réduction...
}

Code de test

J'ai utilisé JUnit 5 pour le framework de test et AssertJ pour l'assertion.

Ajouter une dépendance

Modifiez pom.xml et ajoutez les dépendances suivantes.

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>

Test de classe de service

Le test de classe de service est implémenté en tant que test unitaire. Utilisez H2 pour les bases de données de ressources externes dont dépend le service. Préparez l'environnement de test en créant un schéma avant de démarrer le test, en chargeant les données de test pour chaque méthode de test et en supprimant le schéma une fois le test terminé.

Fichier de propriétés de connexion pour le test

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 =

Créer un DSLContext de test

Implémentez le code qui crée le DSLContext pour H2.

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

}

Classe de test de service Ce code est un modèle qui utilise H2 en mémoire. (Le modèle d'utilisation de H2 en mode serveur sera décrit plus tard.)

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

}

Utiliser H2 en mode serveur

En mode serveur, les schémas, les tables et les données de test sont créés en exécutant des scripts SQL placés aux emplacements suivants. Lorsque vous exécutez le test, un fichier de base de données pour la base de données H2 est créé en tant que fichier sample_db.mv.db.

[project_root]
 |
 `--- /h2
        |
        `--- /sql
        |      |
        |      `--- 01_schema.sql     //Créer un schéma
        |      `--- 02_table.sql      //Créer une table
        |      `--- 03_data.sql       //Insérer des données
        |      `--- 04_table.sql      //Déposez la table
        |      `--- 05_schema.sql     //Supprimer le schéma
        |
        `--- 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();
  }

}

Modifiez la classe de test de service pour utiliser le serveur H2. Les parties à modifier sont les méthodes setupSchema et 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();
  }

  //...réduction...

}

Test de classe de contrôleur

Le test de classe de contrôleur est implémenté en tant que test d'intégration. La base de données utilise le même MySQL que dans le monde réel, vous devez donc accéder au serveur MySQL lors de l'exécution du test.

Classe de test du contrôleur

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

Une note lors de l'examen de Javalin
Une note lorsque la commande heroku devient indisponible
Remarques sur Java GC
Remarques sur la portée
Note privée sur AtomicReference
Une note quand j'étais accro à la conversion d'Ubuntu sur WSL1 en WSL2
Remarques sur les familles de colonnes dans RocksDB
Précautions lors de l'écriture d'un programme: 3e partie
Réfléchir lors de l'introduction d'une nouvelle bibliothèque
Créer une application Web avec Javalin
Notes de personnes qui ont trébuché en essayant de créer un projet Rails