** Was ist Javalin **
Javalin ist ein leichtes Web Framework für Java und Kotlin. Aus einem Web Framework namens Spark Framework heraus wurde die Version 0.0.1 im Mai 2017 und die Version 3.4.1 ab August 2019 veröffentlicht. Die Hauptfunktionen werden von GitHub unten zitiert. (Japanische Übersetzung ist Google Übersetzung)
https://github.com/tipsy/javalin
Javalin is more of a library than a framework. Some key points:
Javelin ist eher eine Bibliothek als ein Framework. Einige wichtige Punkte:
Die erste Hälfte dieses Artikels ist ein Memo über die Implementierung einer Anwendung, in der "Hello World" mit der Mindestkonfiguration angezeigt wird, und die zweite Hälfte ist ein Memo dessen, was ich bei der Implementierung einer einfachen Webanwendung untersucht / versucht habe.
Umgebung
Referenz
Dies ist ein normales Maven-Projekt.
[project_root]
|
`--- /src
| |
| `--- /main
| | |
| | `--- /java
| | | |
| | | `--- com.example.demo (package)
| | | |
| | | `--- App.java
| | |
| | `--- /resources
| |
| `--- /test
| |
| `--- /java
| |
| `--- /resources
|
`--- pom.xml
pom.xml
Die erforderlichen Abhängigkeiten für eine minimale Javalin-Anwendung sind Javalin und slf4j-simple.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo-java12-javalin3</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo java12 javalin 3.4.1</name>
<description>Demo project for Javalin 3 with Java12</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>12</java.version>
<maven.clean.version>3.1.0</maven.clean.version>
<maven.resources.version>3.1.0</maven.resources.version>
<maven.compiler.version>3.8.1</maven.compiler.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
<maven.jar.version>3.1.1</maven.jar.version>
<maven.assembly.version>3.1.1</maven.assembly.version>
</properties>
<dependencies>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.26</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>${maven.clean.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>${maven.resources.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<compilerArgs>
<arg>-Xlint:all</arg>
</compilerArgs>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven.jar.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven.assembly.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.example.demo.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Implementieren Sie den Startpunkt der Javalin-Anwendung in der Hauptmethode der App-Klasse (Klassenname ist beliebig).
/
Ihrer Javalin-Anwendung einen GET-Anforderungshandler hinzu.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"));
}
}
Wenn Sie die App.main-Methode ausführen, wird Javalin gestartet und das folgende Protokoll wird an die Konsole ausgegeben. Wie Sie im Protokoll sehen können, dauerte das Booten in meiner Entwicklungsumgebung weniger als eine Sekunde. Wenn Sie auf "http: // localhost: 7000" zugreifen, erhalten Sie eine Text- / Klartextantwort von "Hello World".
[main] INFO io.javalin.Javalin -
__ __ _
/ /____ _ _ __ ____ _ / /(_)____
__ / // __ `/| | / // __ `// // // __ \
/ /_/ // /_/ / | |/ // /_/ // // // / / /
\____/ \__,_/ |___/ \__,_//_//_//_/ /_/
https://javalin.io/documentation
[main] INFO org.eclipse.jetty.util.log - Logging initialized @827ms to org.eclipse.jetty.util.log.Slf4jLog
[main] INFO io.javalin.Javalin - Starting Javalin ...
[main] INFO io.javalin.Javalin - Listening on http://localhost:7000/
[main] INFO io.javalin.Javalin - Javalin started in 731ms \o/
Beim Erstellen wird eine JAR-Datei mit dem Namen "demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar" unter dem Zielverzeichnis generiert.
Die Javalin-Anwendung verwendet Embedded Jetty, sodass Sie die Webanwendung mit "java -jar
> java --version
openjdk 12.0.2 2019-07-16
OpenJDK Runtime Environment (build 12.0.2+10)
OpenJDK 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing)
> java -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar
Im Folgenden finden Sie ein Beispiel für das Ändern des Standardgebietsschemas mit JVM-Parametern.
> java -Duser.language=en -Duser.country=US -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar
Damit ist die Implementierung der Anwendung "Hallo Welt mit minimaler Konfiguration" abgeschlossen.
Von hier aus wird es ein Memo sein, wenn eine einfache Webanwendung mit Javalin implementiert wird.
Der oben beschriebene GET-Anforderungshandler (Muster 1 unten) kann auch als Muster 2 und Muster 3 implementiert werden.
app.get("/", ctx -> ctx.result("Hello World"));
public class App {
void index2(@NotNull Context ctx) {
ctx.result("Hello World2");
}
public static void main(String ... args) {
Javalin app = Javalin.create().start(7000);
App handler = new App();
app.get("/2", handler::index2);
}
}
public class App {
static Handler index3 = ctx -> {
ctx.result("Hello World3");
};
public static void main(String ... args) {
Javalin app = Javalin.create().start(7000);
app.get("/3", App.index3);
}
}
** Muster 2 ** * Der Einfachheit halber haben wir der App-Klasse eine Methode hinzugefügt, aber normalerweise implementieren wir eine Handler-Klasse separat. ** Muster 3 ** Im offiziellen Site-Tutorial gibt es auch ein Beispiel, das im statischen Feld beschrieben wird. Zusätzlich zum HTTP-Handler GET gibt es POST, PUT, PATCH, DELETE, HEAD und OPTIONS.
HTTP-Methode | Unterschrift |
---|---|
GET | public Javalin get(@NotNull String path, @NotNull Handler handler) |
POST | public Javalin post(@NotNull String path, @NotNull Handler handler) |
PUT | public Javalin put(@NotNull String path, @NotNull Handler handler) |
PATCH | public Javalin patch(@NotNull String path, @NotNull Handler handler) |
DELTE | public Javalin delete(@NotNull String path, @NotNull Handler handler) |
HEAD | public Javalin head(@NotNull String path, @NotNull Handler handler) |
OPTIONS | public Javalin options(@NotNull String path, @NotNull Handler handler) |
Sie können vor / nach Endpunkten festlegen, dass sie vor und nach dem HTTP-Handler ausgeführt werden sollen.
Endpunkt | Unterschrift |
---|---|
before | public Javalin before(@NotNull String path, @NotNull Handler handler) |
before | public Javalin before(@NotNull Handler handler) |
after | public Javalin after(@NotNull String path, @NotNull Handler handler) |
after | public Javalin after(@NotNull Handler handler) |
Endpunkt | HTTP-Methode | Funktion |
---|---|---|
/api/v1/users | GET | Benutzerliste abrufen |
/api/v1/user/:id | GET | Holen Sie sich den Benutzer durch Angabe der ID |
/api/v1/user | POST | Benutzer registrieren |
/api/v1/user/:id | DELETE | Löschen Sie den Benutzer durch Angabe der ID |
/api/v2/users | GET | Benutzerliste abrufen |
/api/v2/user/:id | GET | Holen Sie sich den Benutzer durch Angabe der ID |
/api/v2/user | POST | Benutzer registrieren |
/api/v2/user/:id | DELETE | Löschen Sie den Benutzer durch Angabe der ID |
Wenn Sie zwei solcher Endpunkte haben, v1 und v2, können Sie HTTP-Handler wie unten gezeigt gruppieren und definieren.
UserController v1Controller = /*Implementierung von Version 1*/
UserController v2Controller = /*Implementierung von Version 2*/
app.routes(() -> {
// version 1
ApiBuilder.path("/api/v1", () -> {
// GET /api/v1/users
ApiBuilder.get("/users", v1Controller::findAll);
ApiBuilder.path("/user", () -> {
// GET /api/v1/user/:id
ApiBuilder.get(":id", v1Controller::findById);
// POST /api/v1/user
ApiBuilder.post(v1Controller::store);
// DELETE /api/v1/user/:id
ApiBuilder.delete(":id", v1Controller::remove);
});
});
// version 2
ApiBuilder.path("/api/v2", () -> {
// GET /api/v2/users
ApiBuilder.get("/users", v2Controller::findAll);
ApiBuilder.path("/user", () -> {
// GET /api/v2/user/:id
ApiBuilder.get(":id", v2Controller::findById);
// POST /api/v2/user
ApiBuilder.post(v2Controller::store);
// DELETE /api/v2/user/:id
ApiBuilder.delete(":id", v2Controller::remove);
});
});
});
Der HTTP-Handler-Methodenparameter io.javalin.http.Context
bietet die Funktionalität und Daten, die für HTTP-Anforderungen und -Antworten erforderlich sind.
Das Folgende ist ein Zitat aus der Dokumentseite der offiziellen Website.
https://javalin.io/documentation#context
The Context object provides you with everything you need to handle a http-request. It contains the underlying servlet-request and servlet-response, and a bunch of getters and setters. The getters operate mostly on the request-object, while the setters operate exclusively on the response object.
Der Kontext enthält auch Anforderungs- und Res-Felder, die Instanzen von HttpServletRequest und HttpServletResponse enthalten.
javax.servlet.http.HttpServletRequest request = ctx.req;
javax.servlet.http.HttpServletResponse response = ctx.res;
Eine Methode, die die in der Abfragezeichenfolge angegebenen Parameter empfängt
Unterschrift |
---|
String queryParam(String key) |
String queryParam(String key, String default) |
List<String> queryParams(String key) |
Map<String, List<String>> queryParamMap() |
Validator<T> queryParam(String key, Class<T> clazz) |
Formulare verfügen über ähnliche Methoden wie Abfrageparameter. Leider können Sie die Felder des gesamten Formulars nicht an ein Objekt binden.
Unterschrift |
---|
String formParam(String key) |
String formParam(String key, String default) |
List<String> formParams(String key) |
Map<String, List<String>> formParamMap() |
Validator<T> formParam(String key, Class<T> clazz) |
Eine Methode, die einen Teil davon als Parameter von einem Pfad wie "/ api / user / 1" empfängt
Unterschrift |
---|
String pathParam(String key) |
Map<String, String> pathParamMap() |
Validator<T> pathParam(String key, Class<T> clazz) |
Der Teil des Anforderungshandlerpfads mit einem Doppelpunkt (:) wird als Pfadparameter identifiziert.
app.get("/api/user/:id", ctx -> {
Long userId = ctx.pathParam("id", Long.class).get();
});
Sie kann angepasst werden, indem eine Instanz der JavalinConfig-Klasse an die Javalin.create-Methode übergeben wird. Sie können eine Instanz von JavalinConfig nicht wie unten gezeigt übergeben, sondern auch in einen Lambda-Ausdruck schreiben.
Beispiel
configuration
JavalinConfig config = new JavalinConfig();
// configuration
Javalin
.create(config)
// ...Kürzung...
** Lambda-Ausdruck **
configuration
Javalin
.create(config -> {
// configuration
})
// ...Kürzung...
Kontextpfadeinstellung (Standard ist "/")
config.contextPath = "/app";
Festlegen des Standardinhaltstyps (Standard ist "Text / Nur")
config.defaultContentType = "application/json";
ETag-Einstellung (Standard ist false)
config.autogenerateEtags = true;
Die Ausgabe des Anforderungsprotokolls kann mit JavalinConfig.requestLogger festgelegt werden. Das zweite Argument (execTime) ist die Zeit, die für die Anforderung benötigt wird (ms).
configuration
config.requestLogger((ctx, execTime) -> {
LOG.debug("[{}] {} - {} ms.", ctx.fullUrl(), ctx.userAgent(), execTime);
});
Einige Funktionen sind nützlich, um sie während der Entwicklung zu aktivieren.
enableDevLogging
configuration
config.enableDevLogging();
Da es viele Informationen gibt, denke ich, dass sie verwendet werden, um sie zu aktivieren, wenn untersucht wird, wann ein Problem auftritt, anstatt sie ständig aktiviert zu lassen.
[qtp2092769598-18] INFO io.javalin.Javalin - JAVALIN REQUEST DEBUG LOG:
Request: GET [/overview]
Matching endpoint-handlers: [GET=/overview]
Headers: {Cookie=Idea-c29ae3c1=53d6d99f-1169-42ed-8d69-a63cfe80a87a, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3, Upgrade-Insecure-Requests=1, Connection=keep-alive, User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36, Sec-Fetch-Site=none, Host=localhost:7000, Sec-Fetch-User=?1, DNT=1, Accept-Encoding=gzip, deflate, br, Accept-Language=ja,en-US;q=0.9,en;q=0.8,en-GB;q=0.7, Sec-Fetch-Mode=navigate}
Cookies: {Idea-c29ae3c1=53d6d99f-1169-42ed-8d69-a63cfe80a87a}
Body:
QueryString: null
QueryParams: {}
FormParams: {}
Response: [200], execution took 26.37 ms
Headers: {Server=Javalin, Content-Encoding=gzip, Date=Wed, 21 Aug 2019 13:44:46 GMT, Content-Type=text/html}
Body is gzipped (4464 bytes, not logged)
----------------------------------------------------------------------------------
RouteOverviewPlugin
configuration
config.registerPlugin(new RouteOverviewPlugin("/overview"));
Sie können die Liste der Anforderungshandler anzeigen, die Ihrer Javalin-Anwendung hinzugefügt wurden, indem Sie dieses Plugin registrieren und auf "http: // localhost: 7000 / summary" zugreifen.
Es gibt eine Ausnahmeklasse, die dem HTTP-Status entspricht, der Javalins HttpResponseException erbt.
Status | Ausnahmeklasse | Botschaft |
---|---|---|
302 | RedirectResponse | Redirected |
400 | BadRequestResponse | Bad request |
401 | UnauthorizedResponse | Unauthorized |
403 | ForbiddenResponse | Forbidden |
404 | NotFoundResponse | Not found |
405 | MethodNotAllowedResponse | Method not allowed |
409 | ConflictResponse | Conflict |
410 | GoneResponse | Gone |
500 | InternalServerErrorResponse | Internal server error |
502 | BadGatewayResponse | Bad gateway |
503 | ServiceUnavailableResponse | Service unavailable |
504 | GatewayTimeoutResponse | Gateway timeout |
Sie können anwendungsspezifische Ausnahmen behandeln und beliebige Fehlerantworten zurückgeben. Wenn eine anwendungsspezifische Ausnahme namens AppServiceException unten ausgelöst wird,
AppServiceException
public class AppServiceException extends RuntimeException {
public AppServiceException() {
}
public AppServiceException(String message) {
super(message);
}
public AppServiceException(String message, Throwable cause) {
super(message, cause);
}
}
Um diese Ausnahme zu behandeln und den HTTP-Status zurückzugeben, ordnen Sie die Ausnahme Ihrer Javalin-Anwendung zu. Dieses Beispiel gibt nur den Status 500 zurück.
app.exception(AppServiceException.class, (e, ctx) -> {
ctx.status(500);
});
Implementieren Sie Folgendes, um eine beliebige Fehlerantwortnachricht und einen HTTP-Status zurückzugeben. Sie können auch die json-Methode anstelle der Ergebnismethode verwenden, um eine Nachricht im json-Format zurückzugeben.
app.exception(AppServiceException.class, (e, ctx) -> {
ctx.result("Application Error : " + e.getMessage()).status(500);
});
Fügen Sie der Abhängigkeit Jackson hinzu, damit die Anforderung / Antwort json verarbeiten kann. Der zweite "jackson-datatype-jsr310" ist erforderlich, wenn Sie mit der Java 8-API für Datum und Uhrzeit arbeiten.
pom.xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0.pr1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.10.0.pr1</version>
</dependency>
Implementieren Sie einen GET-Anforderungshandler, der auf json reagiert. Verwenden Sie die json-Methode, um auf json zu antworten.
public static void main(String ... args) {
// ...Kürzung
// (1)
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"));
// (2)
JavalinJackson.configure(objectMapper);
// (3)
app.get("/json", ctx -> {
Map<String, Object> model = new HashMap<>();
model.put("message", "Hello World");
model.put("now", LocalDateTime.now());
//Verwenden Sie die JSON-Methode.
ctx.json(model);
});
// (4)
app.post("/json", ctx -> {
Map<String, Object> model = ctx.bodyAsClass(Map.class);
//Gibt den HTTP-Status 200 zurück.
ctx.status(200);
});
}
Legen Sie in JavalinConfig fest, welche Dateien im Klassenpfad abgelegt werden sollen.
[project_root]
|
`--- /src
|
`--- /main
|
`--- /resources
|
`--- /public
|
`--- index.html
Geben Sie den Pfad in JavalinConfig.addStaticFiles an, um die an der obigen Position platzierte Datei "index.html" zu liefern.
configuration
config.addStaticFiles("/public");
Der GET-Anforderungshandler und der Pfad für das /
, das in der Anwendung "Hallo Welt mit minimaler Konfiguration" hinzugefügt wurde, werden behandelt. Kommentieren Sie daher den folgenden Code aus.
// app.get("/", ctx -> ctx.result("Hello World"));
Wenn Sie die Javalin-Anwendung in diesem Status starten und auf "http: // localhost: 7000 / index.html" oder "http: // localhost: 7000" zugreifen, wird public / index.html zurückgegeben.
Sie können auch mit Verzeichnissen außerhalb des Klassenpfads arbeiten. Wenn Sie beispielsweise Dateien im Verzeichnis "C: \ var \ static" wie unten gezeigt verarbeiten möchten,
C:\var
|
`--- \static
|
`--- \images
|
`--- sample_1.jpg
Geben Sie den absoluten Pfad in JavalinConfig.addStaticFiles an. Wenn auf den hier angegebenen Pfad nicht zugegriffen werden kann, tritt beim Starten der Anwendung ein Fehler auf.
configuration
config.addStaticFiles("C:\\var\\static", Location.EXTERNAL);
Geben Sie den Pfad in der HTML-Datei wie unten gezeigt an.
<img src="/images/sample_1.jpg " />
Fügen Sie die Bibliothek hinzu, die Sie für die Abhängigkeit von pom.xml verwenden möchten. In diesem Beispiel haben wir jQuery verwendet.
pom.xml
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
Dies allein aktiviert jQuery nicht. Aktivieren Sie Webjars in JavalinConfig.enableWebjars, um sie Ihrer Anwendung zur Verfügung zu stellen.
configuration
config.enableWebjars();
Geben Sie den Pfad in der HTML-Datei wie unten gezeigt an.
<script src="/webjars/jquery/3.4.1/jquery.min.js"></script>
Verwenden Sie slf4j und logback für die Protokollierung. Da slf4j-api
bereits von Javalin abhängt, müssen wir nur logback-classic
hinzufügen.
Kommentieren Sie auch das zuerst hinzugefügte slf4j-simple
aus.
pom.xml
<!--
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.26</version>
</dependency>
-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
Erstellen Sie die Logback-Konfigurationsdatei logback.xml am folgenden Speicherort.
[project_root]
|
`--- /src
|
`--- /main
|
`--- /resources
|
`--- logback.xml
logback.xml
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="INFO">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Pattern>
</encoder>
</appender>
<logger name="com.example.demo" level="debug" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<logger name="org.jooq" level="info" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<logger name="org.thymeleaf" level="info" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<root level="info">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
Definieren Sie den Logger in der Klasse, in der Sie das Log ausgeben möchten, wie folgt.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
static final Logger LOG = LoggerFactory.getLogger(App.class);
// ...Kürzung...
}
Javalin macht es einfach, Template-Engines wie Velocity, Freemarker und Thymeleaf zu verwenden. In diesem Beispiel wird Thymeleaf verwendet.
pom.xml
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.9.RELEASE</version>
</dependency>
Platzieren Sie eine Vorlagendatei mit dem Namen "hello_world.html" an der folgenden Stelle. Eine Eigenschaftendatei mit demselben Dateinamen ist eine Nachrichtenressource.
[project_root]
|
`--- /src
|
`--- /main
|
`--- /resources
|
`--- /WEB-INF
|
`--- /templates
|
`--- hello_world.html
`--- hello_world.properties
`--- hello_world_ja.properties
`--- hello_world_en.properties
hello_world.html
<!doctype html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script th:src="@{/webjars/jquery/3.4.1/jquery.min.js}"></script>
<title>Hello World</title>
</head>
<body>
<div>
<p th:text="${message}">message</p>
<p th:text="${now}">now</p>
<p th:text="#{msg.hello}">message</p>
</div>
</body>
</html>
Definition der Nachrichtenressource
hello_world.properties
msg.hello = Hello World (default)
hello_world_en.properties
msg.hello = Hello World
hello_world_ja.properties
msg.hello =Hallo Welt
Verwenden Sie die Rendermethode, um mit der Template-Engine zu antworten. Welche Template-Engine verwendet wird, hängt von der Erweiterung der Template-Datei ab. Im Fall von Thymeleaf gelten ".html", ".tl", ".thyme", ".thymeleaf".
app.get("/hello", ctx -> {
Map<String, Object> model = new HashMap<>();
model.put("message", "Hello World");
model.put("now", LocalDateTime.now());
ctx.render("/WEB-INF/templates/hello_world.html", model);
});
Wenn Sie auf localhost: 7000 / hello
zugreifen, wird die von der Template-Engine gezeichnete HTML-Seite angezeigt.
Standardmäßig bestimmt das Standardgebietsschema beim Start der Anwendung die Nachrichtenressource. Um die Nachrichtenressource einer bestimmten Sprache zu verwenden, geben Sie beim Starten der Javalin-Anwendung das Gebietsschema in der Option JVM an. Geben Sie beispielsweise die folgenden Optionen an, um die Nachrichtenressource von en zu verwenden.
> java -Duser.language=en -Duser.country=US -jar <JAR-Datei>
Erstellen Sie eine Klasse, die FileRenderer implementiert, um die der Accept-Language des Browsers entsprechende Nachrichtenressource wie Spring Boot zu verwenden.
CustomThymeleafRenderer
import io.javalin.http.Context;
import io.javalin.plugin.rendering.FileRenderer;
import org.jetbrains.annotations.NotNull;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class CustomThymeleafRenderer implements FileRenderer {
private final TemplateEngine templateEngine;
public CustomThymeleafRenderer() {
templateEngine = templateEngine();
}
@Override
public String render(@NotNull String filePath, @NotNull Map<String, Object> model, @NotNull Context ctx) {
WebContext context = new WebContext(ctx.req, ctx.res, ctx.req.getServletContext(), ctx.req.getLocale());
context.setVariables(model);
return templateEngine.process(filePath, context);
}
private TemplateEngine templateEngine() {
TemplateEngine templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.addDialect(new Java8TimeDialect());
return templateEngine;
}
private ITemplateResolver templateResolver() {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setCacheable(true);
templateResolver.setCacheTTLMs(TimeUnit.MINUTES.toMillis(30));
templateResolver.setTemplateMode(TemplateMode.HTML);
return templateResolver;
}
}
Registrieren Sie diesen CustomThymeleafRenderer mit Ihrer Javalin-Anwendung.
JavalinRenderer.register(new CustomThymeleafRenderer(), ".html");
Bei dieser Anpassung ist es nicht erforderlich, den Pfad der Vorlagendatei anzugeben, sodass die Handler-Implementierung wie folgt lautet.
// ctx.render("/WEB-INF/templates/hello_world.html", model);
ctx.render("hello_world.html", model);
Der standardmäßig registrierte Renderer lautet übrigens wie folgt.
JavalinRenderer
object JavalinRenderer {
init {
register(JavalinVelocity, ".vm", ".vtl")
register(JavalinFreemarker, ".ftl")
register(JavalinMustache, ".mustache")
register(JavalinJtwig, ".jtwig", ".twig", ".html.twig")
register(JavalinPebble, ".peb", ".pebble")
register(JavalinThymeleaf, ".html", ".tl", ".thyme", ".thymeleaf")
register(JavalinCommonmark, ".md", ".markdown")
}
// ...Kürzung...
}
Ich habe MySQL 8.0 für die Datenbank, jOOQ für das ORM und HikariCP für den Verbindungspool verwendet.
Erstellen Sie die Datenbank "sample_db" und den Benutzer "test_user" zur Verwendung in Ihrer Javalin-Anwendung als Benutzer mit Administratorrechten.
CREATE DATABASE IF NOT EXISTS sample_db
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_general_ci
;
CREATE USER IF NOT EXISTS 'test_user'@'localhost'
IDENTIFIED BY 'test_user'
PASSWORD EXPIRE NEVER
;
GRANT ALL ON sample_db.* TO 'test_user'@'localhost';
Erstellen Sie dann eine "user" -Tabelle mit dem Benutzer "test_user".
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT AUTO_INCREMENT COMMENT 'Benutzeridentifikation',
nick_name VARCHAR(60) NOT NULL COMMENT 'Spitzname',
sex CHAR(1) NOT NULL COMMENT 'Geschlecht M.:Männlich F.:Weiblich',
prefecture_id TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Präfektur 0:Unbekannt, 1:Hokkaido- 8:Kyushu-Okinawa',
email VARCHAR(120) COMMENT 'Mail Adresse',
memo TEXT COMMENT 'Spalte Bemerkungen',
create_at DATETIME NOT NULL DEFAULT NOW(),
update_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8MB4
COMMENT = 'Benutzertabelle';
CREATE INDEX idx_sex on user (sex) USING BTREE;
CREATE INDEX idx_pref on user (prefecture_id) USING BTREE;
Bearbeiten Sie die Datei pom.xml und fügen Sie die folgenden Abhängigkeiten hinzu.
pom.xml
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.11.11</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.3.1</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
<scope>runtime</scope>
</dependency>
jOOQ hat eine Funktion zum automatischen Generieren von Modellcode (Entitätscode) aus einem Datenbankschema, das als "jooq-codegen-maven" -Plug-In bezeichnet wird.
<plugin>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<version>3.11.11</version>
<executions>
<execution>
<id>jooq-codegen</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
</dependencies>
<configuration>
<jdbc>
<url>jdbc:mysql://localhost:3306</url>
<user>test_user</user>
<password>test_user</password>
</jdbc>
<generator>
<database>
<name>org.jooq.meta.mysql.MySQLDatabase</name>
<includes>.*</includes>
<inputSchema>sample_db</inputSchema>
<!-- Configure type overrides for generated fields, attributes, sequences, parameters. -->
<forcedTypes>
<forcedType>
<!--Specify the Java type of your custom type. This corresponds to the Converter's <U> type. -->
<userType>com.example.demo.converter.Prefecture</userType>
<!-- A converter implementation for the {@link #getUserType(). -->
<converter>com.example.demo.converter.PrefectureConverter</converter>
<!-- regex to match the column name -->
<expression>PREFECTURE_ID</expression>
<types>.*</types>
</forcedType>
<forcedType>
<userType>com.example.demo.converter.Sex</userType>
<converter>com.example.demo.converter.SexConverter</converter>
<expression>SEX</expression>
<types>.*</types>
</forcedType>
</forcedTypes>
</database>
<target>
<packageName>com.example.demo.model</packageName>
<directory>src/main/java</directory>
</target>
<generate>
<!-- A flag indicating whether Java 8's java.time types should be used by the source code generator, rather than JDBC's java.sql types. -->
<javaTimeTypes>true</javaTimeTypes>
<!-- Generate index information -->
<indexes>true</indexes>
<!-- Primary key / foreign key relations should be generated and used. This is a prerequisite for various advanced features -->
<relations>true</relations>
<!-- Generate deprecated code for backwards compatibility -->
<deprecated>false</deprecated>
<!-- Generate the {@link javax.annotation.Generated} annotation to indicate jOOQ version used for source code -->
<generatedAnnotation>false</generatedAnnotation>
<!-- Generate Sequence classes -->
<sequences>true</sequences>
<!-- Generate Key classes -->
<keys>true</keys>
<!-- Generate Table classes -->
<tables>true</tables>
<!-- Generate TableRecord classes -->
<records>true</records>
<!-- Generate POJOs -->
<pojos>true</pojos>
<!-- Generate basic equals() and hashCode() methods in POJOs -->
<pojosEqualsAndHashCode>true</pojosEqualsAndHashCode>
<!-- Generate basic toString() methods in POJOs -->
<pojosToString>true</pojosToString>
<!-- Generate serializable POJOs -->
<serializablePojos>true</serializablePojos>
<!-- Generated interfaces to be implemented by records and/or POJOs -->
<interfaces>false</interfaces>
<!-- Turn off generation of all global object references -->
<globalObjectReferences>true</globalObjectReferences>
<!-- Turn off generation of global catalog references -->
<globalCatalogReferences>true</globalCatalogReferences>
<!-- Turn off generation of global schema references -->
<globalSchemaReferences>true</globalSchemaReferences>
<!-- Turn off generation of global table references -->
<globalTableReferences>true</globalTableReferences>
<!-- Turn off generation of global sequence references -->
<globalSequenceReferences>true</globalSequenceReferences>
<!-- Turn off generation of global UDT references -->
<globalUDTReferences>true</globalUDTReferences>
<!-- Turn off generation of global routine references -->
<globalRoutineReferences>true</globalRoutineReferences>
<!-- Turn off generation of global queue references -->
<globalQueueReferences>true</globalQueueReferences>
<!-- Turn off generation of global database link references -->
<globalLinkReferences>true</globalLinkReferences>
<!-- Turn off generation of global key references -->
<globalKeyReferences>true</globalKeyReferences>
<!-- Generate fluent setters in records, POJOs, interfaces -->
<fluentSetters>true</fluentSetters>
</generate>
</generator>
</configuration>
</plugin>
Generieren Sie Code mit dem folgenden Befehl mvn. (Wird auch vom MVN-Paket generiert)
> mvn jooq-codegen:generate
Der Code wird im Paket com.example.demo.model
ausgegeben. In diesem Beispiel wurde der folgende Code generiert.
[project_root]
|
`--- /src
|
`--- /main
|
`--- /java
|
`--- com.example.demo.model
| |
| `--- DefaultCatalog.java
| `--- Indexes.java
| `--- Keys.java
| `--- SampleDb.java
| `--- Tables.java
|
`--- com.example.demo.model.tables
| |
| `--- User.java // (1)
|
`--- com.example.demo.model.tables.records
| |
| `--- UserRecord.java // (2)
|
`--- com.example.demo.model.tables.pojos
|
`--- User.java // (3)
Verbindungsinformationen werden in der Eigenschaftendatei verwaltet.
datasource.properties
datasource.jdbcUrl = jdbc:mysql://localhost:3306/sample_db
datasource.userName = test_user
datasource.password = test_user
DbPropertyLoader
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Properties;
public class DbPropertyLoader {
private final Properties prop;
private static final String JDBC_URL = "datasource.jdbcUrl";
private static final String USER_NAME = "datasource.userName";
private static final String PASSWORD = "datasource.password";
public DbPropertyLoader(String fileName) {
prop = loadProperties(fileName);
}
public String getJdbcUrl() {
return prop.getProperty(JDBC_URL);
}
public String getUserName() {
return prop.getProperty(USER_NAME);
}
public String getPassword() {
return prop.getProperty(PASSWORD);
}
private Properties loadProperties(String fileName) {
try (InputStream stream = getClass().getClassLoader().getResourceAsStream(fileName)) {
if (stream == null) {
throw new IOException("Not Found : " + fileName);
}
Properties prop = new Properties();
prop.load(stream);
return prop;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Um mit jOOQ auf die Datenbank zuzugreifen, benötigen Sie eine Instanz einer Klasse namens DSLContext für jOOQ. Ich habe eine Singleton-Holder-Klasse implementiert, die eine Instanz von DSLContext wie folgt enthält: Der DSLContext ist threadsicher.
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);
}
}
Dies ist ein Implementierungsbeispiel für eine Serviceklasse, die über den DSLContext von jOOQ und den automatisch generierten Code (Entitätsklasse) auf die Datenbank zugreift.
UserService
import com.example.demo.model.tables.pojos.User;
import java.util.List;
import java.util.Optional;
public interface UserService {
Optional<User> findById(Long id);
List<User> findByNickName(String nickName);
List<User> findAll();
User save(User user);
void remove(Long id);
}
UserServiceImpl
import com.example.demo.AppServiceException;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.model.tables.records.UserRecord;
import com.example.demo.service.UserService;
import org.jooq.DSLContext;
import org.jooq.Result;
import org.jooq.impl.DSL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static com.example.demo.model.Tables.USER;
public class UserServiceImpl implements UserService {
private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
private final DSLContext dsl;
public UserServiceImpl(DSLContext dsl) {
this.dsl = dsl;
}
@Override
public Optional<User> findById(Long id) {
LOG.debug("find by Id : {}", id);
UserRecord record = dsl
.selectFrom(USER)
.where(USER.ID.eq(id))
.fetchOne();
if (record != null) {
return Optional.of(record.into(User.class));
}
return Optional.empty();
}
@Override
public List<User> findByNickName(String nickName) {
LOG.debug("find by nickName : {}", nickName);
Result<UserRecord> result = dsl
.selectFrom(USER)
.where(USER.NICK_NAME.contains(nickName))
.orderBy(USER.NICK_NAME.asc())
.fetch();
return result.into(User.class);
}
@Override
public List<User> findAll() {
LOG.debug("findAll");
Result<UserRecord> result = dsl
.selectFrom(USER)
.orderBy(USER.ID.desc())
.fetch();
return result.into(User.class);
}
@Override
public User save(User user) {
LOG.debug("save : {}", user);
UserRecord result = dsl.transactionResult(conf -> {
if (user.getId() == null || user.getId().equals(0L)) {
return DSL.using(conf)
.insertInto(USER,
USER.NICK_NAME,
USER.SEX,
USER.PREFECTURE_ID,
USER.EMAIL,
USER.MEMO)
.values(
user.getNickName(),
user.getSex(),
user.getPrefectureId(),
user.getEmail(),
user.getMemo())
.returning()
.fetchOne();
} else {
return DSL.using(conf)
.update(USER)
.set(USER.NICK_NAME, user.getNickName())
.set(USER.SEX, user.getSex())
.set(USER.PREFECTURE_ID, user.getPrefectureId())
.set(USER.EMAIL, user.getEmail())
.set(USER.MEMO, user.getMemo())
.set(USER.UPDATE_AT, LocalDateTime.now())
.where(USER.ID.eq(user.getId()))
.returning()
.fetchOne();
}
});
LOG.debug("save result : {}", result);
if (result == null) {
throw new AppServiceException("[save] Not Found saved user record.");
}
return result.into(User.class);
}
@Override
public void remove(Long id) {
LOG.debug("remove by id : {}", id);
int result = dsl.transactionResult(conf -> {
int count = DSL.using(conf)
.selectCount()
.from(USER)
.where(USER.ID.eq(id))
.execute();
if (count != 1) {
return count;
}
return DSL.using(conf)
.deleteFrom(USER)
.where(USER.ID.eq(id))
.execute();
});
LOG.debug("remove result : {}", result);
if (result == 0) {
throw new AppServiceException("[remove] Not Found delete user record.");
}
}
}
Dies ist ein Beispiel für die Implementierung eines Controllers, der die oben genannten Dienste verwendet.
UserController
import com.example.demo.model.tables.pojos.User;
import com.example.demo.service.UserService;
import io.javalin.Javalin;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.http.Context;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class UserController {
private static final Logger LOG = LoggerFactory.getLogger(UserController.class);
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
void findById(@NotNull Context ctx) {
Long id = ctx.pathParam("id", Long.class).get();
userService.findById(id).ifPresentOrElse(user -> {
ctx.json(user);
},() -> {
ctx.status(404);
});
}
void findAll(@NotNull Context ctx) {
List<User> users = userService.findAll();
ctx.json(users);
}
void store(@NotNull Context ctx) {
User user = ctx.bodyAsClass(User.class);
User storedUser = userService.save(user);
ctx.json(storedUser);
}
void remove(@NotNull Context ctx) {
Long id = ctx.pathParam("id", Long.class).get();
userService.remove(id);
ctx.status(200);
}
public void bindEndpoints(@NotNull Javalin app) {
app.routes(() -> {
ApiBuilder.path("api/user", () -> {
// GET /api/user
ApiBuilder.get(this::findAll);
// GET /api/user/:id
ApiBuilder.get(":id", this::findById);
// POST /api/user
ApiBuilder.post(this::store);
// DELETE /api/user/:id
ApiBuilder.delete(":id", this::remove);
});
});
}
}
Der Handler war am Startpunkt der Javalin-Anwendung gebunden.
public static void main(String... args) {
Javalin app = Javalin
.create(config -> {
// configuration
})
.start(7000);
// ...Kürzung...
//Generieren Sie DSLContext
final DSLContext dsl = DslContextHolder.getInstance().getDslContext();
//Binden Sie den Handler an die Anwendung
new UserController(new UserServiceImpl(dsl)).bindEndpoints(app);
// ...Kürzung...
}
Ich habe JUnit 5 für das Testframework und AssertJ für die Assertion verwendet.
Bearbeiten Sie die Datei pom.xml und fügen Sie die folgenden Abhängigkeiten hinzu.
pom.xml
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<!--<version>1.4.199</version>-->
<version>1.4.193</version>
<scope>test</scope>
</dependency>
Der Serviceklassentest wird als Komponententest implementiert. Verwenden Sie H2 für Datenbanken externer Ressourcen, von denen der Dienst abhängt. Bereiten Sie die Testumgebung vor, indem Sie vor Beginn des Tests ein Schema erstellen, die Testdaten für jede Testmethode laden und das Schema nach Beendigung des Tests löschen.
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 =
Implementieren Sie den Code, der den DSLContext für H2 erstellt.
TestDslContextHolder
import com.example.demo.config.DbPropertyLoader;
import com.example.demo.config.DslConfigure;
import org.h2.jdbcx.JdbcDataSource;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.TransactionProvider;
import org.jooq.conf.Settings;
import org.jooq.conf.StatementType;
import org.jooq.impl.DSL;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultTransactionProvider;
import javax.sql.DataSource;
import java.io.Serializable;
import java.util.Optional;
public class TestDslContextHolder implements DslConfigure, Serializable {
private static final long serialVersionUID = -5728144007357037196L;
private static TestDslContextHolder instance;
private final DSLContext dslContext;
private final DataSource dataSource;
private final Settings settings;
private final ConnectionProvider connectionProvider;
private final TransactionProvider transactionProvider;
public TestDslContextHolder() {
dataSource = createDataSource();
settings = createSettings();
connectionProvider = new DataSourceConnectionProvider(dataSource);
transactionProvider = new DefaultTransactionProvider(connectionProvider, true);
// transactionProvider = new ThreadLocalTransactionProvider(connectionProvider, true);
dslContext = DSL.using(configuration());
}
public static TestDslContextHolder getInstance() {
if (instance == null) {
synchronized (TestDslContextHolder.class) {
if (instance == null) {
instance = new TestDslContextHolder();
}
}
}
return instance;
}
public static void destroy() {
if (instance != null) {
synchronized (TestDslContextHolder.class) {
if (instance != null) {
instance.dslContext.close();
instance = null;
}
}
}
}
DataSource createDataSource() {
DbPropertyLoader prop = new DbPropertyLoader("test-datasource.properties");
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL(prop.getJdbcUrl());
dataSource.setUser(prop.getUserName());
dataSource.setPassword(prop.getPassword());
return dataSource;
}
Settings createSettings() {
return new Settings()
.withStatementType(StatementType.STATIC_STATEMENT)
.withQueryTimeout(10)
.withMaxRows(1000)
.withFetchSize(20)
.withExecuteLogging(true)
.withDebugInfoOnStackTrace(true)
.withRenderFormatted(true);
}
@Override
public DSLContext getDslContext() {
return this.dslContext;
}
@Override
public DataSource getDataSource() {
return this.dataSource;
}
@Override
public SQLDialect getSqlDialect() {
return SQLDialect.H2;
}
@Override
public Settings getSettings() {
return this.settings;
}
@Override
public Optional<ConnectionProvider> getConnectionProvider() {
//
return Optional.ofNullable(this.connectionProvider);
}
@Override
public Optional<TransactionProvider> getTransactionProvider() {
//
return Optional.ofNullable(this.transactionProvider);
}
}
Servicetestklasse Dieser Code ist ein Muster, das In-Memory H2 verwendet. (Das Muster der Verwendung von H2 im Servermodus wird später beschrieben.)
TestService
import org.jooq.DSLContext;
import org.jooq.Queries;
import org.jooq.Query;
import org.jooq.Schema;
import org.jooq.Table;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import java.util.List;
public interface TestService {
DSLContext getDSLContext();
void setDSLContext(DSLContext dsl);
default void createSchema(Schema schema) {
Queries queries = getDSLContext().ddl(schema);
for (Query query : queries.queries()) {
getDSLContext().execute(query);
}
}
default void dropSchema(Schema schema) {
List<Table<?>> tables = schema.getTables();
for (Table<?> table : tables) {
getDSLContext().dropTableIfExists(table).execute();
}
getDSLContext().dropSchemaIfExists(schema).execute();
}
default void cleanUp(List<Table<?>> tables) {
getDSLContext().transaction(conf -> {
for (Table table : tables) {
DSL.using(conf).deleteFrom(table).execute();
}
});
}
default void testDataRollback() {
throw new DataAccessException("rollback");
}
default void isFailed(RuntimeException e) {
if (!e.getMessage().equals("rollback")) {
System.err.println(e);
throw e;
}
}
}
UserServiceImplTests
import com.example.demo.TestDslContextHolder;
import com.example.demo.converter.Prefecture;
import com.example.demo.converter.Sex;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.model.tables.records.UserRecord;
import com.example.demo.service.TestService;
import com.example.demo.service.UserService;
import org.assertj.core.groups.Tuple;
import org.jooq.DSLContext;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static com.example.demo.model.SampleDb.SAMPLE_DB;
import static com.example.demo.model.Tables.*;
@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserServiceImplTests implements TestService {
private DSLContext dsl;
@Override
public DSLContext getDSLContext() {
return dsl;
}
@Override
public void setDSLContext(DSLContext dsl) {
this.dsl = dsl;
}
@BeforeAll
public void setupSchema() {
System.out.println("setup class");
setDSLContext(TestDslContextHolder.getInstance().getDslContext());
createSchema(SAMPLE_DB);
}
@AfterAll
public void tearDownSchema() {
System.out.println("teardown class");
dropSchema(SAMPLE_DB);
TestDslContextHolder.destroy();
}
@BeforeEach
public void setup() {
System.out.println("setup");
}
@AfterEach
public void tearDown() {
System.out.println("teardown");
cleanUp(List.of(USER));
}
@Test
public void findById() {
System.out.println("findById");
try {
dsl.transaction(conf -> {
// setup test data
DSL.using(conf)
.insertInto(USER,
USER.ID,
USER.NICK_NAME,
USER.SEX,
USER.PREFECTURE_ID,
USER.EMAIL,
USER.MEMO)
.values(1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "[email protected]", "memo aaa")
.values(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "[email protected]", null)
.values(3L, "nick name CCC", Sex.FEMALE, Prefecture.KANTOU, "[email protected]", "memo ccc")
.execute();
// exercise
UserService sut = new UserServiceImpl(DSL.using(conf));
Optional<User> actual = sut.findById(1L);
// verify
assertThat(actual.isPresent()).isTrue();
actual.ifPresent(user -> {
assertThat(user)
.extracting(
"id",
"nickName",
"sex",
"prefectureId",
"email",
"memo")
.contains(
1L,
"nick name AAA",
Sex.FEMALE,
Prefecture.HOKKAIDO,
"[email protected]",
"memo aaa");
});
testDataRollback();
});
} catch (DataAccessException e) {
isFailed(e);
}
}
@Test
public void findByNickName() {
System.out.println("findByNickName");
try {
dsl.transaction(conf -> {
// setup test data
DSL.using(conf)
.insertInto(USER,
USER.ID,
USER.NICK_NAME,
USER.SEX,
USER.PREFECTURE_ID,
USER.EMAIL,
USER.MEMO)
.values(1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "[email protected]", "memo aaa")
.values(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "[email protected]", null)
.values(3L, "nick name CCC", Sex.FEMALE, Prefecture.KANTOU, "[email protected]", "memo ccc")
.execute();
// exercise
UserService sut = new UserServiceImpl(DSL.using(conf));
List<User> actual = sut.findByNickName("BBB");
// verify
assertThat(actual).hasSize(1);
assertThat(actual)
.extracting(
"id",
"nickName",
"sex",
"prefectureId",
"email",
"memo")
.contains(Tuple.tuple(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "[email protected]", null));
testDataRollback();
});
} catch (DataAccessException e) {
isFailed(e);
}
}
@Test
public void save() {
try {
dsl.transaction(conf -> {
// exercise
User testData = new User(null, "nick name DDD", Sex.MALE, Prefecture.KANTOU, "[email protected]", null, null, null);
UserService sut = new UserServiceImpl(DSL.using(conf));
User expected = sut.save(testData);
// verify
Optional<User> actual = sut.findById(expected.getId());
assertThat(actual.isPresent()).isTrue();
actual.ifPresent(user -> {
assertThat(user)
.extracting(
"id",
"nickName",
"sex",
"prefectureId",
"email",
"memo")
.contains(
expected.getId(),
expected.getNickName(),
expected.getSex(),
expected.getPrefectureId(),
expected.getEmail(),
expected.getMemo());
});
testDataRollback();
});
} catch (DataAccessException e) {
isFailed(e);
}
}
@Test
public void remove() {
try {
dsl.transaction(conf -> {
// setup
Long userId = 1L;
DSL.using(conf)
.insertInto(USER,
USER.ID,
USER.NICK_NAME,
USER.SEX,
USER.PREFECTURE_ID,
USER.EMAIL,
USER.MEMO)
.values(
userId,
"nick name EEE",
Sex.MALE,
Prefecture.CHUBU,
"[email protected]",
"memo eee")
.execute();
// exercise
UserService sut = new UserServiceImpl(DSL.using(conf));
sut.remove(userId);
// verify
UserRecord actual = DSL.using(conf)
.selectFrom(USER)
.where(USER.ID.eq(userId))
.fetchOne();
assertThat(actual).isNull();
testDataRollback();
});
} catch (DataAccessException e) {
isFailed(e);
}
}
}
Im Servermodus werden Schemas, Tabellen und Testdaten durch Ausführen von SQL-Skripten erstellt, die an den folgenden Speicherorten abgelegt werden. Durch Ausführen des Tests wird eine Datenbankdatei für die H2-Datenbank als "sample_db.mv.db" -Datei erstellt.
[project_root]
|
`--- /h2
|
`--- /sql
| |
| `--- 01_schema.sql //Erstellen Sie ein Schema
| `--- 02_table.sql //Erstellen Sie eine Tabelle
| `--- 03_data.sql //Daten einfügen
| `--- 04_table.sql //Lass den Tisch fallen
| `--- 05_schema.sql //Löschen Sie das Schema
|
`--- sample_db.mv.db
TestH2Server
import com.example.demo.config.DbPropertyLoader;
import org.h2.tools.RunScript;
import org.h2.tools.Server;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
public class TestH2Server implements AutoCloseable {
private final Server server;
private final String jdbcUrl;
private final String userName;
private final String password;
public TestH2Server() {
DbPropertyLoader prop = new DbPropertyLoader("test-datasource.properties");
jdbcUrl = prop.getJdbcUrl();
userName = prop.getUserName();
password = prop.getPassword();
String[] params = new String[] {"-tcp", "-tcpPort", "9092", "-baseDir", "./h2"};
try {
server = Server.createTcpServer(params);
} catch (SQLException e) {
System.err.println(e.getMessage());
throw new RuntimeException(e);
}
}
public void start() {
System.out.println("server start");
try {
server.start();
status();
Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdown()));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void runScript(List<String> scripts) {
try {
for (String script : scripts) {
System.out.println("run script : " + script);
RunScript.execute(jdbcUrl, userName, password, script, StandardCharsets.UTF_8, false);
}
} catch (SQLException e) {
System.err.println("run script : " + e.getMessage());
throw new RuntimeException(e);
}
}
public void shutdown() {
if (server.isRunning(true)) {
System.out.println("server shutdown");
server.shutdown();
}
}
public void status() {
System.out.println(server.getStatus());
}
// for test
public static void main(String ... args) {
try (TestH2Server h2 = new TestH2Server()) {
h2.start();
h2.status();
h2.runScript(List.of("./h2/sql/01_schema.sql", "./h2/sql/02_table.sql", "./h2/sql/03_data.sql"));
h2.test();
h2.runScript(List.of("./h2/sql/04_table.sql", "./h2/sql/05_schema.sql"));
}
System.exit(0);
}
public void test() {
System.out.println("test");
try (Connection conn = DriverManager.getConnection(jdbcUrl, userName, password)) {
PreparedStatement ps = conn.prepareStatement("select * from sample_db.user");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
System.out.println("id : " + rs.getLong("id"));
System.out.println("nick_name : " + rs.getString("nick_name"));
System.out.println("sex : " + rs.getString("sex"));
System.out.println("prefecture_id : " + rs.getInt("prefecture_id"));
System.out.println("email : " + rs.getString("email"));
System.out.println("memo : " + rs.getString("memo"));
System.out.println("createAt : " + rs.getTimestamp("create_at"));
System.out.println("updateAt : " + rs.getTimestamp("update_at"));
}
} catch (SQLException e) {
System.err.println("test error : " + e.getMessage());
throw new RuntimeException(e);
}
}
@Override
public void close() {
shutdown();
}
}
Ändern Sie die Testklasse des Dienstes, um den H2-Server zu verwenden. Die zu ändernden Teile sind die Methoden setupSchema und tearDownSchema.
@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserServiceImplTests implements TestService {
private DSLContext dsl;
private TestH2Server h2;
private List<String> initScripts = List.of(
"./h2/sql/01_schema.sql",
"./h2/sql/02_table.sql"
);
private List<String> clearScripts = List.of(
"./h2/sql/04_table.sql",
"./h2/sql/05_schema.sql"
);
@Override
public DSLContext getDSLContext() {
return dsl;
}
@Override
public void setDSLContext(DSLContext dsl) {
this.dsl = dsl;
}
@BeforeAll
public void setupSchema() {
System.out.println("setup class");
h2 = new TestH2Server();
h2.start();
h2.runScript(initScripts);
setDSLContext(TestDslContextHolder.getInstance().getDslContext());
}
@AfterAll
public void tearDownSchema() {
System.out.println("teardown class");
TestDslContextHolder.destroy();
h2.runScript(clearScripts);
h2.shutdown();
}
//...Kürzung...
}
Der Controller-Klassentest wird als Integrationstest implementiert. Die Datenbank verwendet dasselbe MySQL wie in der realen Welt, sodass Sie beim Ausführen des Tests Zugriff auf den MySQL-Server benötigen.
Controller-Testklasse
import com.example.demo.config.DslContextHolder;
import com.example.demo.config.JacksonConfig;
import com.example.demo.converter.Prefecture;
import com.example.demo.converter.Sex;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.service.impl.UserServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.Javalin;
import io.javalin.plugin.json.JavalinJackson;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.assertj.core.api.Assertions.assertThat;
@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserControllerTests {
Javalin app;
int port;
ObjectMapper objectMapper;
@BeforeAll
public void setup() {
port = 7000;
app = Javalin.create().start(port);
objectMapper = JacksonConfig.getObjectMapper();
JavalinJackson.configure(objectMapper);
}
@AfterAll
public void tearDown() {
app.stop();
}
@Test
public void findById() throws Exception {
// setup
new UserController(new UserServiceImpl(DslContextHolder.getInstance().getDslContext()))
.bindEndpoints(app);
// exercise
Long userId = 1L;
String testTargetUrl = String.format("http://localhost:%d/api/user/%d", port, userId);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(testTargetUrl))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// verify
assertThat(response.statusCode()).isEqualTo(200);
User actual = objectMapper.readValue(response.body(), User.class);
assertThat(actual)
.extracting(
"id",
"nickName",
"sex",
"prefectureId",
"email",
"memo")
.contains(
1L,
"test user 1",
Sex.MALE,
Prefecture.HOKKAIDO,
"[email protected]",
"memo 1"
);
}
}
Recommended Posts