IntelliJ+Kotlin/Java for SpringBoot Note: ThymeLeaf, H2 Database, PostgreSQL, PMVC-Test
IntelliJ is a fine IDE. Especially, the Android Studio is built on it. Right here, I show you that we can use IntelliJ with Kotlin to edit our backend such as SpringBoot Server instead of Java. Do you like this combination? Cool, isn’t it?! This will make our works shorter and easy to read. I show my notes when I build a project with SpringBoot with ThymeLeaf, with the Hibernate H2 and PostgreSQL database. You can find your trouble section in the menu.
— === MeNu === —
👍1. IntelliJ — Kotlin
➰2. Spring Initializr
🌐3. First Words: Hello Foods
👁🗨4. Controller => Template => Web
🍂5. Config Thymeleaf Templates
📝6. Note of ThymeLeaf
🖋🖋🖋 → Form Action
🖋🖋🖋 → Error of Backing Bean
📄7. Logging: Logback
⭐8. Hibernate — H2 Embedded Database
🛡🛡🛡 → Entity
🛡🛡🛡 → Repository
🐘9. Static Database (PostgreSQL)
🏷🏷🏷 → Docker
🏷🏷🏷 → Import Schema & Data
🔬10. MVC Test
👍1. IntelliJ — Kotlin
< === Menu
Download and install IntelliJ Community Edition:
How to create a Kotlin project in IJ is simple.
IJ: File => New => Project
Let’s choose the Kotlin.
Project: NewKotlin
pom.xml: for Kotlin and Java
<properties>
<java.version>11</java.version>
<kotlin.version>1.4.10</kotlin.version>
</properties><dependencies>
...
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies><build>
<plugins>
...
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
🚄2. Spring Initializr
< === Menu
We can generate the SpringBoot project at, https://start.spring.io/
It’s the fastest way to build your project with your dependencies.
Of course, I will use Kotlin. Let’s choose the dependencies.
Hit generate, your web browser will download the zip file to your download folder. Now, please unzip that file to your IdeaProjects directory.
So you can open the spring boot project in IJ.
Upper is the Maven version. I like to use Gradle because I am an Android developer.
Gradle dependencies:
implementation ("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
developmentOnly("org.springframework.boot:spring-boot-devtools")
implementation("org.springframework.boot:spring-boot-starter-log4j2")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
🌐3. First Words: Hello Foods
< === Menu
Hello-World is too common in the coding world. Let’s try Hello-Foods.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SpringBoot Page</title>
</head>
<body>
<H1> Hello Foods! </H1>
</body>
</html>
Run the application:
localhost:8080
At HTTP://localhost:8080, you will found the result.
👁🗨4. Controller => Template => Web
< === Menu
Let’s try our second Hello-Foods by Spring controller.
Template
Copy&edit the content from index.html to helloFoods.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Second SpringBoot Page</title>
</head>
<body>
<H1>Hello</H1><H2>Foods!</H2><H3>in my controller!</H3>
</body>
</html>
Controller
In Kotlin, your package folder. Let’s add a new package called “control”. In the control, you can add a new Kotlin class called MainController.kt.
@Controller
class MainController {
@RequestMapping("helloFoods") // endpoint
fun helloFoods(): ModelAndView = ModelAndView("helloFoods")
}
Let’s turn to the auto-restart function.
When you update the HTML file, the server will not update automatically. You have to restart the app. In IJ, you can turn on the auto-restart function to update the data.
Setting => Compiler => Build project automatically(checked)
Press: Ctrl+Shift+Alt+/
Choose Registry.
“compiler.automake.allow.when.app.running” = checked.
Now, run the app. I want to make a mistake with the controller.
fun helloFoods():
ModelAndView = ModelAndView("helloFoods2") //html
Refresh the web page.
The server responds with error code 500. After that, I fix the error and change HTML content,
<H1>Hello</H1><H2>Foods!</H2><H3>in my controller!</H3>
<h4>New change!</h4>
This time, without the restart, let’s refresh the webpage.
You’ll see that the automated update is working.
🍂5. Config Thymeleaf Templates
< === Menu
Package + config
Optional for your server:
Let’s create a “config” under your package. Add the configuration: You can define your template path, formatter for an object/bean, endpoint →HTML setup.
@Configuration
class WebConfig : WebMvcConfigurer {
@Bean
@Description("Thymeleaf template resolver serving HTML 5")
fun templateResolver(): ClassLoaderTemplateResolver {
val templateResolver = ClassLoaderTemplateResolver()
templateResolver.prefix = "mytemplates/"
templateResolver.isCacheable = false
templateResolver.suffix = ".html"
templateResolver.setTemplateMode("HTML5")
templateResolver.characterEncoding = "UTF-8"
return templateResolver
}
@Bean
@Description("Thymeleaf template engine with Spring integration")
fun templateEngine(): SpringTemplateEngine {
val templateEngine = SpringTemplateEngine()
templateEngine.setTemplateResolver(templateResolver())
return templateEngine
}
@Bean
@Description("Thymeleaf view resolver")
fun viewResolver(): ViewResolver {
val viewResolver = ThymeleafViewResolver()
viewResolver.templateEngine = templateEngine()
viewResolver.characterEncoding = "UTF-8"
return viewResolver
}
override fun addViewControllers(registry: ViewControllerRegistry) {
registry.addViewController("/").setViewName("index")
}
}
> templateResolver.prefix = “mytemplates/” <
tells the server to change from “templates” to “mytemplates”. You can add all new Thymeleaf HTML template into this folder.
📝6. Note of ThymeLeaf
< === Menu
HTML header:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns:th="http://www.thymeleaf.org" >
“${ attr }”: attribute → object, matched by model.
HTML: I just eat a <span th:text="${ fruit }" />.
SBoot: model.addAttribute("fruit", "apple");
Spring →ModelAndView: Matching ${attributes} in the Thymeleaf template
model: mapOf(“attr” to object)
val model = mapOf(
"bean" to MyBean(), // bean object
"cargo" to cargoRepository.findAll(), // from DB
"prieces" to 1..100, // list
"barcodeIni" to 'A'..'P' // list
)
return ModelAndView(
"cargoBooking", // HTML page
model
)
Don’t you think that is the faster way to create model by mapOf?
Form Action: shopping.html
- th:action →Go to shoppingCart endpoint to deal with data.
- th:object → The object(merchandise) carries the form data.
<form action="#" th:action="@{/shoppingCart}" th:object="${merchandise}" method="POST">
Controller: From shopping endpoint → shoppingCart endpoint.
@RequestMapping("/shopping") // endpoint
fun shopping(): ModelAndView {
return ModelAndView(
"shopping", // html page
"merchandise", // attribute
MerchandiseBean() // object
)
}@PostMapping("/shoppingCart") // endpoint
fun addToCart( bean: MerchandiseBean ): ModelAndView {...}
List object: The fruits of object-mBean lists data in the option box and returns variable, selectedFruit.
Return value: th:field= “*{ attr }”
<select th:field="*{selectedFruit}">
<option th:each="n : ${mBean.fruits}" th:value="${n}" th:text="${n}" />
</select>
The n represents one item of the list by th:each.
If condition = th:If …th:unless
<td>
<span th:if="${customer.gender} == 'M'" th:text="Male" />
<span th:unless="${customer.gender} == 'M'" th:text="Female" />
</td>
Switch and case condition
<td th:switch="${customer.gender}">
<span th:case="'M'" th:text="Male" />
<span th:case="'F'" th:text="Female" />
</td>
Error:
<ul>
<li th:each="err : ${#fields.errors('*')}" th:text="${err}" />
</ul>
Conversion: ${{ value }}
<tr th:each="cargo: ${ cargos }">
<td th:text="${{ cargo.detail }}" />
</tr>
Load formatter in Configuration( /config/MyConfig.kt):
@Configuration
class MyConfig: WebMvcConfigurer {
...
@Description("Cargo Detail Conversion")
override fun addFormatters(registry: FormatterRegistry) {
registry.addFormatter(CargoDetailFormater())
}
}
The YourVar of backing bean has a different variable name with the one in Thymeleaf; for example: fruitNane ≠ fruitName
Controller:
class BackingBean {
var fruitNane: Fruit? = null
...
}HTML:
<form ... th:object="${bean}" method="POST">
<input type="hidden" th:field="*{fruitName}"/>
📄7. Logging: Logback
< === Menu
Gradle:
configurations {
all {
exclude(group="org.springframework.boot", module="spring-boot-starter-logging")
}
}
Logger
The logger has already existed in the Spring Boot framework. Let’s load it in the class, such as MainController:
var logger: Logger = LoggerFactory.getLogger(MainController::class.java)
Now you can use it to do logger.info, logger.debug, logger.error, logger.warn and logger.trace. For example,
fun getSeat(): ModelAndView {
logger.debug("Checking the seat availability.")
...
}fun checkSeatInfo(...): ModelAndView {
logger.debug("Searching result of seat.")
...
}
To show only your package information, you need to change application.properties,
logging.level.root=INFO
logging.level.YourPackage=TRACE
Let’s check the console:
⭐8. Hibernate — H2 Embedded Database
< === Menu
Gradle plugin:
plugins {
id("org.springframework.boot") version "2.3.4.RELEASE"
id("io.spring.dependency-management") version "1.0.10.RELEASE"
//kotlin plugins
kotlin("jvm") version "1.4.10"
kotlin("plugin.jpa") version "1.4.10"
kotlin("plugin.spring") version "1.4.10"
kotlin("plugin.noarg") version "1.4.10"
kotlin("plugin.allopen") version "1.4.10"
}
Gradle dependencies:
implementation ("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")
pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
application.properties: For example
#logging
logging.level.org.springframework.jdbc.datasource.init.ScriptUtils=debug#h2 database
spring.datasource.url=jdbc:h2:file:~/foods
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=homan
spring.datasource.password=huang
spring.jpa.properties.hibernate.hbm2dll.auto=update
Match with your application.properties:
Entity
@Entity
data class Fruit(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long,
...)
No column name as “ROW”
Or you have:
org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "CREATE TABLE ... ROW[*] CHAR(255) NOT NULL, PRIMARY KEY (ID))"; expected "identifier"
LiveTemplate for Kotlin: ID
@Id @Column(name="$id$")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val $id2$: Long,$END$
LiveTemplate for Java: ID
@Id @Column(name="$id$")
@GeneratedValue(strategy = GenerationType.AUTO)
private long $id2$; $END$
LiveTemplate for Kotlin: set Column
@Column(name="$name$")
val $name2$: $type$,
$END$
LiveTemplate for Java: set Column
@Column(name="$name$")
private $type$ $name2$;
$END$
Repostiory
JpaRespository ←PagingAndSortingRepository←CrudRepository
Due to their inheritance nature, JpaRepository will have all the behaviors of CrudRepository and PagingAndSortingRepository. So if you don’t need the repository to have the functions provided by JpaRepository and PagingAndSortingRepository, use CrudRepository.
Simple, one line, this code links Fruit with id.
@Repository
interface FruitRepository: JpaRepository<Fruit, Long>
Now re-run your application and connect to H2-console to search for the empty table for each entity.
- getOne: Find one by id. | findAll: Select * from the table
- save/saveAll: Save or update data to the cache until you flush them to the database. | saveAndFlush: save one straight to the database.
- delete/deleteById: Delete one from the table. | deleteAll/deleteInBatch/deleteAllInBatch: Delete a list from the table.
- count: count the rows
- equals/exists/existsById: Boolean
🐘9. Static Database (PostgreSQL)
< === Menu
Install PostgreSQL
Download:
Install from Docker container: create, display, and stop
> docker run --name postgres-docker-name -e POSTGRES_PASSWORD=YourPass -e POSTGRES_DB=YourDatabase -d postgres> docker ps --format '{{.Names}}'
...> docker stop postgres-docker-name
Import schema & data:
Run:
SpringBoot →pom.xml
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
SpringBoot →Gradle:
//runtimeOnly("com.h2database:h2")
implementation("org.postgresql:postgresql")
Create database:
application.properties: Connect to PostgreSQL
spring.jpa.database=POSTGRESQL
spring.datasource.platform=postgres
spring.datasource.url=jdbc:postgresql://localhost:5432/YOUR-DB
spring.datasource.username=postgres
spring.datasource.password=your-password
🔬10. MVC Test
< === Menu
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
Gradle:
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
Spring Test Annotation:
@RunWith(SpringJUnit4ClassRunner.class)
@WebMvcTest(YourWebController.class)
MVC(Java):
@Autowired
private MockMvc mockMvc;
Mockito(Java): given( mocked_service_call ) willReturn ( your_result )
@MockBean
private ShoppingService mService;
...given(mService.getShopList(date)).willReturn(myShopList);
Test endpoint status and string compare(Java):
mockMvc.perform( get("/shoppingCart?customerId=383833") )
.andExpect(status().isOk())
.andExpect(content().string(containsString("Apple: Jazz")));