[3/4] Docker: SpringBoot, Hibernate & Web API


In previous episodes (Part1, Part2) we saw how to create Java Maven project from scratch using SpringBoot – followed by how to deploy the application to docker and then I also demonstrated how can you set up MySQL database server, with automated initialization of a fresh database instance.

Now, let’s see how to set up RESTful Web API to display data from database using Hibernate ORM. We’ll also see how to set up a local development environment and docker deployment environment to quickly switch between the two, and establish an efficient work-flow for your project. Next, we’d set up our project to perform CRUD operations using Hibernate. And finally, we will create our Web API endpoints which can serve the requests made from the browser, in JSON format.

Let’s start by cleaning up the docker workspace (optional step).

Following commands delete images and containers.

You have been warned.

# deletes all docker containers
docker rm $(docker ps --no-trunc -aq)
# deletes dangling docker images (older version of the images)
docker images -q -f "dangling=true"
# deletes all docker images
docker rmi $(docker images -q)
# list all docker images
docker image ls -a
# list all docker containers
docker container ls -a
# you can pre-dwonload the docker images if you want
docker pull mysql
docker pull openjdk:8

For now, we want to create two environments or profiles, a local profile for your local development and container profile for your docker container. Evidently, you can create as many profiles for different environments as you need. e.g. QA, UAT, Staging etc.

Here is my current directory structure

./
├── database
│   ├── 01_schema.sql
│   └── 02_data.sql
├── docker-compose.yml
├── epidemicsweb.iml
├── pom.xml
├── src
│   ├── Docker
│   │   └── Dockerfile
│   ├── main
│   │   ├── java
│   │   │   └── edu
│   │   │   └── gatech
│   │   │   └── epidemics
│   │   │   ├── api
│   │   │   │   └── HomeController.java
│   │   │   ├── edu
│   │   │   │   └── gatech
│   │   │   │   └── epidemics
│   │   │   └── EpidemicsApplication.java
│   │   └── resources
│   └── test
│   └── java
└── target

Under ./src/main/resources folder, let’s create 3 different files as following:

resources
├── application-container.properties
├── application-local.properties
└── application.properties

Here in this example we want to create two environments or profiles, a local profile for your local development and container profile for your docker container, but you can create profiles for your different environments like QA, UAT, Staging etc. as many as you please.

(I dumped the content of these 3 files in one code-block below)

>> application.properties <<
spring.profiles.active=container
>> application-local.properties <<
# Local
#MySQL configuration
db_driver=com.mysql.jdbc.Driver
db_url=jdbc:mysql://localhost:3306/epidemics
db_username=root
db_password=epidemics
db_platform=epidemics
db_initialize=false
#Spring DB configuration
spring.datasource.driver=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/epidemics?useSSL=false
spring.datasource.username=epidemics
spring.datasource.password=epidemics
>> application-container.properties <<
# Container
#MySQL configuration
db_driver=com.mysql.jdbc.Driver
db_url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
db_username=${DATABASE_USER}
db_password=${DATABASE_PASSWORD}
db_platform=mysql
db_initialize=true
#Spring DB configuration
spring.datasource.driver=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}?useSSL=false
spring.datasource.username=${DATABASE_USER}
spring.datasource.password=${DATABASE_PASSWORD}

To be able to read these configurations, we need to add a maven dependency, as following:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

To actually, read the configurations, let’s create a class in package edu.gatech.epidemics

package edu.gatech.epidemics;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
/**
* @author atalati
*/
@Component
@PropertySource("application-${spring.profiles.active}.properties")
@ConfigurationProperties
public class AppConfigBean {
@Value("${db_driver}")
private String db_driver;
@Value("${db_url}")
private String db_url;
@Value("${db_username}")
private String db_username;
@Value("${db_password}")
private String db_password;
@Value("${db_platform}")
private String db_platform;
// getters and setters ...
// toString() override
}

Next, we need to verify if we can read the configurations:

package edu.gatech.epidemics;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author atalati
*/
@SpringBootApplication
public class EpidemicsApplication implements CommandLineRunner{
@Autowired
AppConfigBean appConfigBean;
public static void main(String[] args) {
SpringApplication app = new SpringApplication(EpidemicsApplication.class);
app.run();
}
@Override
public void run(String... args) throws Exception {
System.out.println(appConfigBean.getDb_url());
}
}

And it works!

  • local (from my IDE)
    See Line# 16 below
    . ____ _ __ _ _
    /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
    \\/ ___)| |_)| | | | | || (_| | ) ) ) )
    ' |____| .__|_| |_|_| |_\__, | / / / /
    =========|_|==============|___/=/_/_/_/
    :: Spring Boot :: (v2.0.0.RELEASE)
    2018-03-27 07:24:43.032 INFO 7696 --- [ main] e.gatech.epidemics.EpidemicsApplication : Starting EpidemicsApplication on atalati-devhost with PID 7696 (/home/atalati/Documents/epidemicsweb/target/classes started by atalati in /home/atalati/Documents/epidemicsweb)
    2018-03-27 07:24:43.077 INFO 7696 --- [ main] e.gatech.epidemics.EpidemicsApplication : The following profiles are active: local
    2018-03-27 07:24:43.229 INFO 7696 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@12d4bf7e: startup date [Tue Mar 27 07:24:43 EDT 2018]; root of context hierarchy
    .
    .
    2018-03-27 07:24:46.994 INFO 7696 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
    2018-03-27 07:24:46.999 INFO 7696 --- [ main] e.gatech.epidemics.EpidemicsApplication : Started EpidemicsApplication in 4.79 seconds (JVM running for 7.688)
    jdbc:mysql://localhost:3306/epidemics
    2018-03-27 07:25:37.055 INFO 7696 --- [ Thread-13] ConfigServletWebServerApplicationContext : Closing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@12d4bf7e: startup date [Tue Mar 27 07:24:43 EDT 2018]; root of context hierarchy
    2018-03-27 07:25:37.058 INFO 7696 --- [ Thread-13] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
    view raw console.log hosted with ❤ by GitHub
  • container (from docker terminal)

Now let’s move towards the 2nd part of this episode, Web API and Hibernate ORM.

Say, I want to create an API for following table in MySQL database:

# MySQL Setup
SET default_storage_engine=InnoDB;
USE epidemics;
DROP TABLE IF EXISTS user;
CREATE TABLE user (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(25) NOT NULL,
`password` VARCHAR(50) ,
`active` BIT(1)
)ENGINE=InnoDB;

Now, back in our project we need to add maven dependencies. In pom.xml add following dependencies:


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

view raw

pom.xml

hosted with ❤ by GitHub

Create packages inside root as following:

  • edu.gatech.epidemics.dao
  • edu.gatech.epidemics.model
  • edu.gatech.epidemics.service

To avoid any pain, I recommend that you follow the project structure I am going for. It certainly makes things easier.

First let’s create our entity or model class as following:

package edu.gatech.epidemics.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* @author atalati
*/
@Entity(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
private Boolean active;
// default constructor
public User() { }
// parameterized constructor
public User(String username, String password, Boolean active) {
// ...
}
// getters and setters
// toString() override
}
view raw User.java hosted with ❤ by GitHub

Next is, our repository interface, and yes, it is actually that simple! Just be sure to extend CrudRepository.

package edu.gatech.epidemics.dao;
import edu.gatech.epidemics.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
/**
* @author atalati
*/
@Repository
public interface UserRepository extends CrudRepository<User, Integer> { }

Finally, we want to create our service as following:

package edu.gatech.epidemics.service;
import edu.gatech.epidemics.dao.UserRepository;
import edu.gatech.epidemics.model.User;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* @author atalati
*/
@Service
public class UserService {
@Autowired
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
List<User> users = new ArrayList<User>();
userRepository.findAll().forEach(users::add);
return users;
}
public User findById(Integer id) {
Optional<User> user = userRepository.findById(id);
if (user.isPresent()) {
return user.get();
} else {
return null;
}
}
}

Now, I do not have MySQL database server installed locally, so I am going to use guess what, a Docker container, instead! We’d wait for MySQL to be up and spinning. The wait time should not be too bad. Now let’s run the application and it works!

One more thing …

Let’s try something real quick. I would stop the application from my IDE and I would also stop the MySQL container. If you have installed the database on your local MySQL database instance, you can follow along by temporarily stopping MySQL service, to make your database temporarily inaccessible.

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_151]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_151]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_151]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_151]
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:990) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.MysqlIO.<init>(MysqlIO.java:341) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2186) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2219) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2014) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:776) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.JDBC4Connection.<init>(JDBC4Connection.java:47) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_151]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_151]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_151]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_151]
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:386) ~[mysql-connector-java-5.1.45.jar:5.1.45]
at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:330) ~[mysql-connector-java-5.1.45.jar:5.1.45]
view raw failure.log hosted with ❤ by GitHub

You can even repeat the same test with empty database (i.e. no tables) and your SpringBoot application would still fail to start.

This proves that before it starts, Hibernate from SpringBoot is expecting MySQL database to be reachable and the database schema to be in place.

So we must ensure the same for our docker configuration as well! To achieve our goal, we need to create a shell script file in our resources folder, along with the 3 files we just created, and let’s call it wrapper.sh

#!/bin/bash
while ! exec 6<>/dev/tcp/${DATABASE_HOST}/${DATABASE_PORT}; do
echo "Trying to connect to MySQL at ${DATABASE_HOST}:${DATABASE_PORT}..."
sleep 10
done
echo ">> connected to MySQL database! <<"
java -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=container -jar /app.jar
view raw wrapper.sh hosted with ❤ by GitHub

This also requires slight modification in our existing Dockerfile as following:

FROM openjdk:8
ADD epidemics-web.jar app.jar
ADD wrapper.sh wrapper.sh
RUN bash -c 'touch /app.war'
RUN bash -c 'chmod +x /wrapper.sh'
ENTRYPOINT ["/bin/bash", "/wrapper.sh"]
view raw Dockerfile hosted with ❤ by GitHub

Now, let’s go ahead and rebuild our project.

Kwel! We are now ready to create our API controller ./src/main/java/edu/gatech/epidemics/api/UserApiController.java as following and run the project one final time!

package edu.gatech.epidemics.api;
import edu.gatech.epidemics.model.User;
import edu.gatech.epidemics.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* @author atalati
* /api/user
*/
@RestController
public class UserApiController {
@Autowired
Environment environment;
@Autowired
private UserService userService;
/*
GET: /api/user/
Returns all users from the database
*/
@GetMapping(value = "/api/user")
public List<User> get() {
return userService.findAll();
}
@GetMapping(value = "/api/user/{id}")
public User get(@PathVariable int id) {
return userService.findById(id);
}
}

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s