** 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
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é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).
/
de votre application Javalin.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"));
}
}
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/
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".
À partir de là, ce sera un mémo lors de la mise en œuvre d'une application Web simple avec Javalin.
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);
}
}
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) |
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) |
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);
});
});
});
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;
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) |
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) |
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();
});
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...
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);
});
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
.
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 |
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);
});
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);
});
}
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 " />
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>
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>
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...
}
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.
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...
}
J'ai utilisé MySQL 8.0 pour la base de données, jOOQ pour l'ORM et HikariCP pour le pool de connexion.
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;
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>
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)
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);
}
}
}
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.
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);
}
}
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.");
}
}
}
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...
}
J'ai utilisé JUnit 5 pour le framework de test et AssertJ pour l'assertion.
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>
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é.
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 =
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);
}
}
}
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...
}
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