** What is Javalin **
Javalin is a lightweight web framework for Java and Kotlin. Forked from a web framework called Spark Framework, version 0.0.1 was released in May 2017, and 3.4.1 was released as of August 2019. The main features are quoted from GitHub below. (Japanese translation is Google Translate)
https://github.com/tipsy/javalin
Javalin is more of a library than a framework. Some key points:
Javelin is a library rather than a framework. Some key points:
The first half of this article is a memo about the implementation of an application that displays "Hello World" with the minimum configuration, and the second half is a memo of what I investigated / tried in the implementation of a simple Web application.
environment
reference
It's a normal Maven project.
[project_root]
|
`--- /src
| |
| `--- /main
| | |
| | `--- /java
| | | |
| | | `--- com.example.demo (package)
| | | |
| | | `--- App.java
| | |
| | `--- /resources
| |
| `--- /test
| |
| `--- /java
| |
| `--- /resources
|
`--- pom.xml
pom.xml
The required dependencies for a minimal Javalin application are javalin and 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>
Implement the Javalin application launch point in the main method of the App class (class name is arbitrary).
/
to your Javalin application.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"));
}
}
When you execute the App.main method, Javalin will start and the following log will be output to the console. As you can see in the log, it took less than a second to boot in my development environment.
When you access http: // localhost: 7000
, you will get a text / plain response that says" 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/
When you build, a jar file called demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar
will be generated under the target directory.
Javalin applications use embedded Jetty, so you can start your web application with java -jar <jar file>
.
> 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
The following is an example of changing the default locale with the JVM parameters.
> java -Duser.language=en -Duser.country=US -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar
This completes the implementation of the "Hello World with minimum configuration" application.
From here, it will be a memo when implementing a simple web application with Javalin.
The GET request handler (Pattern 1 below) written above can also be implemented as Pattern 2 and Pattern 3.
pattern 1
app.get("/", ctx -> ctx.result("Hello World"));
** Pattern 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);
}
}
** Pattern 3 **
In the tutorial on the official site, there is also an example described in the static field.
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);
}
}
Besides GET, there are POST, PUT, PATCH, DELETE, HEAD and OPTIONS.
HTTP method | 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) |
You can set before / after endpoints to run before and after the HTTP handler.
end point | 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) |
end point | HTTP method | function |
---|---|---|
/api/v1/users | GET | Get user list |
/api/v1/user/:id | GET | Get user by specifying ID |
/api/v1/user | POST | Register user |
/api/v1/user/:id | DELETE | Delete user by specifying ID |
/api/v2/users | GET | Get user list |
/api/v2/user/:id | GET | Get user by specifying ID |
/api/v2/user | POST | Register user |
/api/v2/user/:id | DELETE | Delete user by specifying ID |
If you have two such endpoints, v1 and v2, you can group and define HTTP handlers as shown below.
UserController v1Controller = /*Version 1 implementation*/
UserController v2Controller = /*Version 2 implementation*/
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);
});
});
});
The HTTP handler method parameter ʻio.javalin.http.Context` class provides the functionality and data needed for HTTP requests and responses. The following is a quote from the document page of the official site.
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.
The Context also has req and res fields that hold instances of HttpServletRequest and HttpServletResponse.
javax.servlet.http.HttpServletRequest request = ctx.req;
javax.servlet.http.HttpServletResponse response = ctx.res;
A method that receives the parameters specified in the query string
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) |
Forms have methods similar to query parameters. Unfortunately it doesn't seem to be possible to bind the fields of the entire form to any object.
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) |
A method that receives a part of it as a parameter from a path like / api / user / 1
Signature |
---|
String pathParam(String key) |
Map<String, String> pathParamMap() |
Validator<T> pathParam(String key, Class<T> clazz) |
The part of the request handler path with a colon (:) is identified as the path parameter.
app.get("/api/user/:id", ctx -> {
Long userId = ctx.pathParam("id", Long.class).get();
});
You can customize it by passing an instance of the JavalinConfig class to the Javalin.create method. In addition to the method of passing an instance of JavalinConfig as shown below, there is also a method of writing with a lambda expression.
instance
configuration
JavalinConfig config = new JavalinConfig();
// configuration
Javalin
.create(config)
// ...abridgement...
** lambda expression **
configuration
Javalin
.create(config -> {
// configuration
})
// ...abridgement...
Context path setting (default is "/")
config.contextPath = "/app";
Setting the default content type (default is "text / plain")
config.defaultContentType = "application/json";
ETag setting (default is false)
config.autogenerateEtags = true;
The output of the request log can be set with JavalinConfig.requestLogger. The second argument (execTime) is the time taken for the request (ms).
configuration
config.requestLogger((ctx, execTime) -> {
LOG.debug("[{}] {} - {} ms.", ctx.fullUrl(), ctx.userAgent(), execTime);
});
Some features are useful to enable during development.
enableDevLogging
configuration
config.enableDevLogging();
Since there is a lot of information, I think that it will be used to enable it when investigating when a problem occurs, rather than always enabling it.
[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"));
You can see the list of request handlers added to your Javalin application by registering this plugin and accessing http: // localhost: 7000 / overview
.
There is an exception class corresponding to the HTTP status that inherits Javalin's HttpResponseException.
Status | Exception class | 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 |
You can handle application-specific exceptions and return arbitrary error responses. If an application-specific exception called AppServiceException below is thrown,
AppServiceException
public class AppServiceException extends RuntimeException {
public AppServiceException() {
}
public AppServiceException(String message) {
super(message);
}
public AppServiceException(String message, Throwable cause) {
super(message, cause);
}
}
To handle this exception and return the HTTP status, map the exception to your Javalin application. This example returns only 500 status.
app.exception(AppServiceException.class, (e, ctx) -> {
ctx.status(500);
});
To return an arbitrary error response message and HTTP status, implement as follows. You can also return a json format message by using the json method instead of the result method.
app.exception(AppServiceException.class, (e, ctx) -> {
ctx.result("Application Error : " + e.getMessage()).status(500);
});
Add jackson to the dependency so that the request / response can handle json.
The second jackson-datatype-jsr310
is needed when working with the Java 8 Date and Time API.
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>
Implement a GET request handler that responds to json. Use the json method to respond to json.
public static void main(String ... args) {
// ...abridgement
// (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());
//Use the json method.
ctx.json(model);
});
// (4)
app.post("/json", ctx -> {
Map<String, Object> model = ctx.bodyAsClass(Map.class);
//Returns HTTP Status 200.
ctx.status(200);
});
}
To distribute the files placed on the classpath, set in JavalinConfig.
[project_root]
|
`--- /src
|
`--- /main
|
`--- /resources
|
`--- /public
|
`--- index.html
To deliver ʻindex.html` placed in the above location, specify the path in JavalinConfig.addStaticFiles.
configuration
config.addStaticFiles("/public");
Comment out the following code because the GET request handler and path for the /
added in the "Hello World with minimum configuration" application will be covered.
// app.get("/", ctx -> ctx.result("Hello World"));
If you start the Javalin application in this state and access http: // localhost: 7000 / index.html
or http: // localhost: 7000
, public / index.html will be returned.
You can also work with directories outside the classpath.
For example, if you want to handle files under the C: \ var \ static
directory as shown below,
C:\var
|
`--- \static
|
`--- \images
|
`--- sample_1.jpg
Specify the absolute path in JavalinConfig.addStaticFiles. If the path specified here cannot be accessed, an error will occur when the application is started.
configuration
config.addStaticFiles("C:\\var\\static", Location.EXTERNAL);
Specify the path in the html file as shown below.
<img src="/images/sample_1.jpg " />
Add the library you want to use for the dependency of pom.xml. In this example, we used jQuery.
pom.xml
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
This alone will not enable jQuery. Enable Webjars in JavalinConfig.enableWebjars to make them available to your application.
configuration
config.enableWebjars();
Specify the path in the html file as shown below.
<script src="/webjars/jquery/3.4.1/jquery.min.js"></script>
Use slf4j and logback for logging. Since slf4j-api
already depends on Javalin, we only need to add logback-classic
.
Also, comment out the slf4j-simple
that was added first.
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>
Create the logback configuration file logback.xml in the following location.
[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>
Define the logger in the class you want to output the log as follows.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
static final Logger LOG = LoggerFactory.getLogger(App.class);
// ...abridgement...
}
Javalin makes it easy to use template engines such as Velocity, Freemarker, and Thymeleaf. This example uses Thymeleaf.
pom.xml
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.9.RELEASE</version>
</dependency>
Place a template file called hello_world.html
in the following location. The property file with the same file name is the message resource.
[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>
Message resource definition
hello_world.properties
msg.hello = Hello World (default)
hello_world_en.properties
msg.hello = Hello World
hello_world_ja.properties
msg.hello =Hello World
Use the render method to respond using the template engine.
Which template engine is used is determined by the extension of the template file. In the case of Thymeleaf, .html
, .tl
, .thyme
, .thymeleaf
are applicable.
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);
});
When you access localhost: 7000 / hello
, you will see the html page drawn by the template engine.
By default, the default locale when the application starts determines the message resource. To use the message resource of a certain language, specify the locale in the JVM option when starting the Javalin application. For example, to use the message resource of en, specify the following options.
> java -Duser.language=en -Duser.country=US -jar <jar file>
To use the message resource corresponding to Accept-Language of the browser like Spring Boot, create a class that implements 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;
}
}
Register this CustomThymeleafRenderer with your Javalin application.
JavalinRenderer.register(new CustomThymeleafRenderer(), ".html");
With this customization, it is not necessary to specify the path of the template file, so the handler implementation is as follows.
// ctx.render("/WEB-INF/templates/hello_world.html", model);
ctx.render("hello_world.html", model);
By the way, the renderer registered by default is as follows.
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")
}
// ...abridgement...
}
I used MySQL 8.0 for the database, jOOQ for the ORM, and HikariCP for the connection pool.
Create the sample_db
database and the test_user
user for use in your Javalin application as a user with administrator privileges.
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';
Then create the ʻuser table with the
test_user` user.
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT AUTO_INCREMENT COMMENT 'User ID',
nick_name VARCHAR(60) NOT NULL COMMENT 'nickname',
sex CHAR(1) NOT NULL COMMENT 'Gender M:Male F:Female',
prefecture_id TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Prefecture 0:Unknown, 1:Hokkaido- 8:Kyushu-Okinawa',
email VARCHAR(120) COMMENT 'mail address',
memo TEXT COMMENT 'Remarks column',
create_at DATETIME NOT NULL DEFAULT NOW(),
update_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8MB4
COMMENT = 'User table';
CREATE INDEX idx_sex on user (sex) USING BTREE;
CREATE INDEX idx_pref on user (prefecture_id) USING BTREE;
Edit pom.xml and add the following dependencies.
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 has a function to automatically generate model (entity) code from a database schema called jooq-codegen-maven
plugin.
<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>
Generate code with the following mvn command. (Also generated by mvn package)
> mvn jooq-codegen:generate
The code is output in the com.example.demo.model
package. In this example, the following code was generated.
[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)
Connection information is managed in the property file.
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);
}
}
}
To access the database with jOOQ, you need an instance of a class called DSLContext for jOOQ. I implemented a singleton holder class that holds an instance of DSLContext as follows. The DSLContext is 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);
}
}
This is an implementation example of a service class that accesses a database using jOOQ's DSLContext and automatically generated code (entity class).
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.");
}
}
}
This is an implementation example of a controller that uses the above services.
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);
});
});
}
}
The handler was bound at the Javalin application launch point.
public static void main(String... args) {
Javalin app = Javalin
.create(config -> {
// configuration
})
.start(7000);
// ...abridgement...
//Generate DSLContext
final DSLContext dsl = DslContextHolder.getInstance().getDslContext();
//Bind the handler to your application
new UserController(new UserServiceImpl(dsl)).bindEndpoints(app);
// ...abridgement...
}
I used JUnit 5 for the testing framework and AssertJ for the assertions.
Edit pom.xml and add the following dependencies.
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>
The service class test is implemented as a unit test. Use H2 for databases of external resources on which the service depends. Prepare the test environment by creating a schema before starting the test, loading test data for each test method, and deleting the schema after the test is completed.
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 =
Implement the code that creates the DSLContext for 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);
}
}
Service test class This code is a pattern that uses in-memory H2. (The pattern of using H2 in server mode will be described later.)
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);
}
}
}
In server mode, schemas, tables, and test data are created by executing SQL scripts placed in the following locations.
Running the test creates a database file for the H2 database as a sample_db.mv.db
file.
[project_root]
|
`--- /h2
|
`--- /sql
| |
| `--- 01_schema.sql //Create a schema
| `--- 02_table.sql //Create a table
| `--- 03_data.sql //Insert data
| `--- 04_table.sql //Drop the table
| `--- 05_schema.sql //Delete the 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();
}
}
Modify the service test class to use the H2 server. The parts to be modified are the setupSchema and tearDownSchema methods.
@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();
}
//...abridgement...
}
The controller class test is implemented as an integration test. The database uses the same MySQL as in the real world, so you need access to the MySQL server when running the test.
Controller test class
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