Starter project based on Spring Boot and Thymeleaf.
Component | Tech |
---|---|
Build & Deploy | Gradle, Docker, Docker Compose, AWS Elastic Beanstalk |
Languages | Kotlin, Java, HTML, CSS, JavaScript |
Frameworks | Spring Boot, Thymeleaf, Bootstrap |
Databases | MySQL, H2, Flyway |
The overall file structure is as follows:
thymeleaf-starter/
├── .elasticbeanstalk/
│ └── config.yml
├── ebextensions/
│ └── *.config
├── gradle/wrapper/
│ └── gradle.*.[jar|properties]
├── flyway/
│ ├── credentials/
│ | └── local.pass
│ └── local.conf
├── src/main/
│ ├── kotlin/
│ | └── com/jahnelgroup/
│ | └── *.[kt|java]
│ └── resources/
│ ├── db/
│ | ├── devdata/*.sql
│ | └── migration/*.sql
│ ├── static/
│ | └── *.[css|images|js]
│ ├── templates/
│ | └── *.html
│ └── application.[properties|yml]
├── src/test/
│ └── (testing framework)
├── Dockerfile
├── build.gradle
├── docker-compose.full.yml
├── docker-compose.yml
└── settings.gradle
Gradle is the build and dependency management tool used by this service. Most actions can be accomplished through Gradle tasks described in the sections below. Most of the requirements needed will be provided through Gradle but you'll still need to install Gradle itself. I suggest using SDKMAN! because it's referenced in the Spring Boot documentation or use any equivalent tool of your choosing.
If you've opted for SDKMAN! after installing it then proceed with these installations as well.
$ sdk install gradle
$ sdk install java
Gradle commands can be run one at a time like gradle clean
as well as combined like gradle clean build
. Here is a cheat sheet for important Gradle tasks.
Command | Description |
---|---|
gradle tasks | List all tasks |
Command | Description |
---|---|
gradle clean build | Deletes build artifacts and runs a fresh build. |
gradle test | Run tests. |
If you inspect the build.gradle file you'll notice plugins related to Spring. These will effectively run a normal build process but with further processing to make the JAR executable. Read more about this process Appendix E. The Executable Jar Format.
The only difference in this starter is the addition of the .ebextensions directory for Elastic Beanstalk support. Review the bootJar
task definition in build.gradle for how this is accomplished.
To start this application from the command line:
$ java -jar build/libs/app.jar
...
2019-04-25 20:27:58.558 INFO 1 --- [ main] com.jahnelgroup.App : Started App in 27.344 seconds (JVM running for 28.325)
Docker is a computer program that performs operating-system-level virtualization, also known as "containerization". It makes the installation and management of software easier to handle. Docker can assist in the local development process by making it easy to start dependencies like your database.
Follow these steps for installing docker and installing docker-compose. This application also leverages two Gradle plugin's to assist with working with docker. palantir/gradle-docker is used to build the image from the Dockerfile and avast/gradle-docker-compose-plugin is used to help start/stop the stacks described in docker-compose.yml and other compose files.
Command | Description |
---|---|
gradle clean build docker | Clean, fresh build, then build docker image. |
gradle fullComposeUp | Start everything including the app (must have built the container first) |
gradle fullComposeDown | ^ Stops the full stack |
gradle depsComposeUp | Start only the app dependencies |
gradle depsComposeDown | ^ Stops the dependency stack |
All of these tasks have their origins as pure docker commands but bringing them into the Gradle ecosystem allows for a more uniform experience. Additionally it provides a platform for more interesting task combinations in the future.
This application uses the palantir/gradle-docker Gradle plugin for building docker images. The library is powerful but we're just using it as a wrapping around invoking the Dockerfile. The architecture of this process was taken from the Spring Boot with Docker guide.
When you're actively developing an application it's useful to be able to run it from within an IDE. You can launch only the dependencies in docker containers leaving it up to you for how you'd like to launch the app, preferably from something like IntellIJ where it's easy to debug.
Run only the dependencies (i.e., just the database) in docker with gradle depsComposeUp
.
Then to stop the dependencies stack run gradle depsComposeDown
.
Sometimes you just want to launch the entire stack locally without an IDE, perhaps a demo or showcase. You can do this with a variation of compose by running gradle fullComposeUp
.
Then to stop the full stack run gradle fullComposeDown
.
Refer to the Docker Documentation for a detailed treatment on the available commands. Please note that you should use the Gradle docker plugin commands shown above for building the image.
Show here are a few native common commands that you'll still need.
List your docker images:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
com.jahnelgroup/thymeleaf-starter latest b86ac0cb2daa 25 minutes ago 151MB
List your running containers:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0383023e5aea mysql:8 "docker-entrypoint..." 25 minutes ago Up 25 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp db
557a8d58bb70 adminer:4 "entrypoint.sh doc..." 25 minutes ago Up 25 minutes 0.0.0.0:8081->8080/tcp adminer
165f4026652c com.jahnelgroup/thymeleaf-starter:latest "/bin/sh -c 'exec ..." 25 minutes ago Up 25 minutes 0.0.0.0:8080->8080/tcp thymeleaf-starter
Tail the logs of a particular container:
$ docker logs -f thymeleaf-starter
...
2019-04-25 20:27:58.556 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-04-25 20:27:58.558 INFO 1 --- [ main] com.jahnelgroup.App : Started App in 27.344 seconds (JVM running for 28.325)
Flyway is an open-source database migration tool. It strongly favors simplicity and convention over configuration. It is based around just a few basic commands that help you with the iterative development of a database. Being a Spring application it's very natural for us to leverage JPA/Hibernate for data persistence but it is important to note that Hibernate is not generating the DDL for us and rather we create them manually by hand and are they located in src/main/resources/db/migration. The properties that disable Hibernate DDL generation are located in src/main/resources/application.yml.
Command | Description |
---|---|
gradle flywayMigrate | Migrates the database |
gradle flywayClean | Drops all objects in the configured schemas |
gradle flywayInfo | Prints the details and status information about all the migrations |
gradle flywayValidate | Validates the applied migrations against the ones available on the classpath |
gradle flywayBaseline | Baselines an existing database, excluding all migrations up to and including baselineVersion |
gradle flywayRepair | Repairs the schema history table |
By default these Flyway commands are pointing to the local instance per the configuration found in build.gradle
.
flyway {
configFiles = [
'flyway/local.conf', 'flyway/credentials/local.pass'
]
}
If you need seed data it can easily be separated out from the Flyway DDL migrations by specifying it as an additional Flyway location. For example you can leverage Flyway callbacks to run the seed data as described in this article How to create database test data for Spring Boot applications with Flyway DB. It's important to note that the seed data must be idempotent, more specifically it should not produce errors if run multiple times. An example of this is to use insert ignore into
as opposed to insert into
.
To leverage this simply create more folders under the src/main/resources/db folder. Using Spring Profiles change the list of migrations to reflect the way you want to seed environments differently.
src/main/
└── resources/
├── db/
| ├── devdata/*.sql
| └── migration/*.sql
This application will use an embedded Flyway dependency to automatically migrate your database upon startup. However, sometimes you'll still want to inspect the Flyway status of a database yourself. In order to do this you'll use a Gradle Flyway plugin from the command line.
You'll need two configuration files in place to achieve this - one for the database URL and location of migrations, and the other for the database credentials. You will not commit the database credentials file to version control because it will be ignored by .gitignore.
Here is the base structure of the flyway directory.
thymeleaf-starter/
└── flyway/
├── credentials/
| ├── .gitignore // this will ignore your other *.pass files
| └── local.pass
└── local.conf
Now let's add the ability to connect to a remote database by adding two additional files.
thymeleaf-starter/
└── flyway/
├── credentials/
| ├── .gitignore
| ├── aws-dev.pass // secret credentials
| └── local.pass
├── aws-dev.conf // db url and migrations
└── local.conf
Here would be the contents of these files, replace the variables with your own values:
flyway/aws-dev.conf
flyway.url=jdbc:mysql://${DB_URL}:${DB_PORT}/${DB_SCHEMA}?useSSL=false
flyway.locations=classpath:db/migration
flyway/credentials/dev.pass
flyway.user=${DB_USER}
flyway.password=${DB_PASS}
To run the Gradle Flyway plugin commands against this database pass this argument in:
$ gradle -Dflyway.configFiles=flyway/aws-dev.conf,flyway/credentials/aws-dev.pass flywayInfo
Note: If you're running this against a remote environment behind a Firewall (i.e., AWS) you'll need to expose the port and whitelist your IP address to have access to it.
Here are a few useful HTTP testing tools:
Ensure you have Gradle, Java, and Docker installed based on the instructure above. Invoke the gradle fullComposeUp
command to initiate the entire which includes the application and the database.
$ gradle fullComposeUp
> Task :fullComposeUp
adminer uses an image, skipping
db uses an image, skipping
app uses an image, skipping
Creating network "e786c2c2a0474a08129fcf4561ff7928_thymeleaf-starter_full_frontend" with the default driver
Creating network "e786c2c2a0474a08129fcf4561ff7928_thymeleaf-starter_full_backend" with the default driver
Creating thymeleaf-starter ...
Creating db ...
Creating e786c2c2a0474a08129fcf4561ff7928_thymeleaf-starter_full_adminer_1_5beb9ccaa3e6 ...
Creating thymeleaf-starter ... done
Creating e786c2c2a0474a08129fcf4561ff7928_thymeleaf-starter_full_adminer_1_5beb9ccaa3e6 ... done
Creating db ... done
Will use 172.21.0.1 (network e786c2c2a0474a08129fcf4561ff7928_thymeleaf-starter_full_backend) as host of adminer
Will use 172.21.0.1 (network e786c2c2a0474a08129fcf4561ff7928_thymeleaf-starter_full_backend) as host of app
Will use 172.21.0.1 (network e786c2c2a0474a08129fcf4561ff7928_thymeleaf-starter_full_backend) as host of db
Probing TCP socket on 172.21.0.1:8081 of service 'adminer_1'
TCP socket on 172.21.0.1:8081 of service 'adminer_1' is ready
Probing TCP socket on 172.21.0.1:8080 of service 'thymeleaf-starter'
Waiting for TCP socket on 172.21.0.1:8080 of service 'thymeleaf-starter' (Connection refused (Connection refused))
Waiting for TCP socket on 172.21.0.1:8080 of service 'thymeleaf-starter' (Connection refused (Connection refused))
Waiting for TCP socket on 172.21.0.1:8080 of service 'thymeleaf-starter' (Connection refused (Connection refused))
TCP socket on 172.21.0.1:8080 of service 'thymeleaf-starter' is ready
Probing TCP socket on 172.21.0.1:3306 of service 'db'
TCP socket on 172.21.0.1:3306 of service 'db' is ready
Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/5.4/userguide/command_line_interface.html#sec:command_line_warnings
BUILD SUCCESSFUL in 50s
12 actionable tasks: 9 executed, 3 up-to-date
Verify that the docker image was generated:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
com.jahnelgroup/thymeleaf-starter latest b18d953e2ff5 About a minute ago 151MB
Verify that the docker containers are running:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0383023e5aea mysql:8 "docker-entrypoint..." 25 minutes ago Up 25 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp db
557a8d58bb70 adminer:4 "entrypoint.sh doc..." 25 minutes ago Up 25 minutes 0.0.0.0:8081->8080/tcp adminer
165f4026652c com.jahnelgroup/thymeleaf-starter:latest "/bin/sh -c 'exec ..." 25 minutes ago Up 25 minutes 0.0.0.0:8080->8080/tcp thymeleaf-starter
Connect the MySQL database and verify that the schema and tables were created (root/rootpassword):
$ mysql -uroot -p -h127.0.0.1
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 19
Server version: 8.0.14 MySQL Community Server - GPL
Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| jg_starter |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
mysql> use jg_starter;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+------------------------+
| Tables_in_jg_starter |
+------------------------+
| authorities |
| flyway_schema_history |
| task_lists |
| tasks |
| user_group_authorities |
| user_group_members |
| user_groups |
| users |
+------------------------+
8 rows in set (0.00 sec)
Navigate to http://localhost:8080 and login with steven / pass.
AWS Elastic Beanstalk is an easy-to-use service for deploying and scaling web applications and services developed with various technologies like Java and Docker.
The EB CLI is a command line interface for Elastic Beanstalk that provides interactive commands that simplify creating, updating and monitoring environments from a local repository. Use the EB CLI as part of your everyday development and testing cycle as an alternative to the AWS Management Console.
To use AWS Elastic Beanstalk you must have:
- An aws account with an aws_access_key_id and aws_access_key_id
- Your account must be privileged enough to use the Elastic Beanstalk service
- You must have installed and configured the AWS CLI
- You must have installed and configured the AWS Elastic Beanstalk CLI
If you're using multiple AWS accounts it's helpful to manage them with named profiles.
~/.aws/credentials
[jahnelgroup]
aws_access_key_id=YOUR_KEY
aws_secret_access_key=YOUR_SECRET
~/.aws/config
[profile jahnelgroup]
region = us-east-1
output = json
role_arn = arn:aws:iam::<your_account_id>:role/<your_role>
source_profile = jahnelgroup
Validate that you're configured correctly by checking your identity with the named profile:
$ aws sts get-caller-identity --profile jahnelgroup
{
"Account": "<your_account_id>",
"UserId": "<your_user_id>",
"Arn": "<your_arn>"
}
Type eb
to make sure it's installed:
$ eb
usage: eb (sub-commands ...) [options ...] {arguments ...}
...
We can use ebextensions to advise Elasitc Beanstalk on how to create an environment. You can have an arbitrary number of config files, here we've broken them up base on resource type and one for the environment variables.
thymeleaf-starter/
├── .elasticbeanstalk/
│ └── config.yml
├── ebextensions/
├── db-instance-options.config
├── ec2-instance-options.config
└── env.config
Make sure that you're using the named profile of --profile jahnelgroup or whatever you decided to call it. Read about the eb create to become familiar with it's options.
Make sure your application is built with the latest with gradle clean build
.
$ gradle clean build
$ eb create --database
Enter Environment Name
(default is thymeleaf-starter-dev):
Enter DNS CNAME prefix
(default is thymeleaf-starter-dev):
Enter an RDS DB username (default is "ebroot"):
Enter an RDS DB master password:
Retype password to confirm:
2019-02-22 21:14:33 INFO createEnvironment is starting.
2019-02-22 21:14:34 INFO Using elasticbeanstalk-us-east-1-12321471834 as Amazon S3 storage bucket for environment data.
2019-02-22 21:14:59 INFO Created security group named: awseb-e-fa2fszffed-stack-AWSEBSecurityGroup-13R271AAHRC7J
2019-02-22 21:15:15 INFO Created EIP: 3.208.109.212
2019-02-22 21:15:15 INFO Creating RDS database named: bb2czfhgfe6e1pzu. This may take a few minutes.
-- Events -- (safe to Ctrl+C)
Note that by databases created via Elastic Beanstalk will be permanently tied to this environment. If you delete the environment then the database will be destroyed. Also note that AWS will automatically inject your new database information (URL, DB name and credentials) for you which will be picked up by Spring with application-beanstalk.yml
You can launch the application from the terminal with:
$ eb open
List available environments:
$ eb list
* thymeleaf-starter-dev
thymeleaf-starter-prod
Switch between them:
$ eb use thymeleaf-starter-prod
Make sure your application is built with the latest with gradle clean build
.
Make sure you're using the correct Elastic Beanstalk environment with eb list
then redeploy:
$ eb deploy
$ eb terminate
The environment "thymeleaf-starter-dev" and all associated instances will be terminated.
To confirm, type the environment name: thymeleaf-starter-dev
2019-02-22 21:27:36 INFO terminateEnvironment is starting.
2019-02-22 21:27:53 INFO Waiting for EC2 instances to terminate. This may take a few minutes.
2019-02-22 21:31:26 INFO Deleted EIP: 3.208.109.212
You can easily get quick health statics about your application by running eb health
Spring Boot Dev Tools includes an additional set of tools that can make the application development experience a little more pleasant.
IDE's are fantastic but introduce complexity when it comes to watching file changes. As such Spring Boot Dev Tools can't easily automatically detect if files have changed within IntelliJ.
If you want changes to take effect then triggering a build within IntelliJ will cause Spring Boot to reload:
- [Build] then [Rebuild Project]
- Or use short cut
Ctrl + F9
If you really want every change to trigger a reload then follow additional steps described in this article.
- [Ctrl + Shift + A] then select Registry
- Enable "compiler.automake.allow.when.app.running" then close
- [File Menu] then [Settings]
- Expand [Build, Execution, Deployment] then select Compiler
- Enable "Build project automatically" then Apply then Ok
- Restart IntelliJ