In this article, we’ll create a simple Spring Boot API application with integration tests, and then build it in a Jenkins pipeline every time a change is pushed to version control.
The full series of articles includes:
-
Part 1: writing a Spring Boot application and setting up a Jenkins pipeline to build it (this article)
-
Part 3: deploying the Docker image as a container from Jenkins into AWS
After this series you’ll have a solid foundation in the best practices to bring continuous integration to microservice architectures.
Ready? Let’s get right into it!
Creating a Spring Boot application
Spring Boot is a framework for easily writing Java web applications. In our case, we’ll use it to create an API service. This is a common requirement of microservice architectures, especially those that have a UI that communicates with a backend API.
Just for fun, we’ll base the application around theme park rides. 🎢 It will be geared towards doing basic get & create operations on theme park rides, comprising a:
-
GET
API at/ride
to get all the rides the application knows about -
GET
API at/ride/{id}
to get details of a specific ride by id -
POST
API at/ride
to add a new ride
Code repo: following along with the code snippets and creating the project from scratch is the best way to get more familiar with Spring Boot. If you want a fast-pass ticket though, here’s all the code in GitHub.
Application design
We’ll use a fairly typical Spring Boot Model-View-Controller format, which normally looks like this:
In our case though, we won’t implement a service as there is no business logic to speak of.
Build file
We’ll be using the popular Gradle tool to build this application. Make sure you’ve got a recent version installed on your machine before proceeding.
Create a new directory for the project, and navigate into it.
Run gradle init
to start the Gradle setup wizard, choosing to create a basic Groovy project with the default name.
This creates a skeleton of a Gradle project, including the Gradle wrapper used for interacting with the application.
A build.gradle file is automatically generated. Modify it so it looks like the one below:
plugins {
id 'java'
id 'io.spring.dependency-management' version "1.0.11.RELEASE"
id 'org.springframework.boot' version '2.5.0'
id 'pl.allegro.tech.build.axion-release' version '1.13.2'
}
version = scmVersion.version
sourceCompatibility = 8
repositories {
mavenCentral()
jcenter()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-devtools'
implementation group: 'com.h2database', name: 'h2', version: '1.4.200'
compileOnly 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
-
the Spring
dependency-management
andboot
plugins bring Spring Boot functionality to our build -
the
axion-release
plugin adds versioning using tags -
we’re got several Spring Boot dependencies to allow us to use the Spring Boot web framework with databases
-
the
h2
dependency will enable us to use an in memory database -
Lombok has been configured as an annotation processor to reduce boilerplate code through code generation
Now let’s create some Java classes. This, and all the following classes, should be created in src/main/java in the package that appears at the top of each individual class definition.
Entity class
The entity class represents each instance of a theme park ride.
Let’s create a ThemeParkRide
class:
package com.tomgregory.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotEmpty;
@Entity
@Getter
@ToString
@NoArgsConstructor
public class ThemeParkRide {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@NotEmpty
private String name;
@NotEmpty
private String description;
private int thrillFactor;
private int vomitFactor;
public ThemeParkRide(String name, String description, int thrillFactor, int vomitFactor) {
this.name = name;
this.description = description;
this.thrillFactor = thrillFactor;
this.vomitFactor = vomitFactor;
}
}
-
this is a simple POJO (plain old Java object) class with
id
,name
,description
,thrillFactor
, andvomitFactor
fields 🤮 -
the class is annotated with
@Entity
so Spring Boot knows what kind of class it is -
we’re using the Lombok annotation processor to generate a default constructor, a
toString()
method, and getter methods for each field -
@NotEmpty
annotations are applied to the name and description fields for validation when we create a theme park ride
Repository class
We’ll use a repository interface to read and write ThemeParkRide
entities from/to the database.
The beauty of the spring-boot-starter-data-jpa library that we imported in build.gradle is that you only have to define the method signatures of the interactions you want with the database, and the library does the rest.
Create an interface called ThemeParkRideRepository
:
package com.tomgregory.repository;
import com.tomgregory.entity.ThemeParkRide;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ThemeParkRideRepository extends CrudRepository<ThemeParkRide, Long> {
List<ThemeParkRide> findByName(String name);
}
-
the
@Repository
annotation lets Spring Boot know what kind of component this class is -
extending
CrudRepository
provides some default methods such assave
andfindById
-
the
findByName
method definition will be implemented automatically by Spring Boot, matching to thename
field of theThemeParkRide
entity class
Controller class
A controller class defines an API and how it interacts with the rest of the application.
Create a class called ThemeParkRideController
:
package com.tomgregory.controller;
import com.tomgregory.repository.ThemeParkRideRepository;
import com.tomgregory.entity.ThemeParkRide;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import javax.validation.Valid;
@RestController
public class ThemeParkRideController {
private final ThemeParkRideRepository themeParkRideRepository;
public ThemeParkRideController(ThemeParkRideRepository themeParkRideRepository) {
this.themeParkRideRepository = themeParkRideRepository;
}
@GetMapping(value = "/ride", produces = MediaType.APPLICATION_JSON_VALUE)
public Iterable<ThemeParkRide> getRides() {
return themeParkRideRepository.findAll();
}
@GetMapping(value = "/ride/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ThemeParkRide getRide(@PathVariable long id){
return themeParkRideRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, String.format("Invalid ride id %s", id)));
}
@PostMapping(value = "/ride", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ThemeParkRide createRide(@Valid @RequestBody ThemeParkRide themeParkRide) {
return themeParkRideRepository.save(themeParkRide);
}
}
-
the
@RestController
annotation tells Spring Boot what type of component this is -
for the
getRides
method we use the@GetMapping
annotation to tell Spring Boot we’re expecting aGET
request at the specified path -
the
getRide
method has a@PathVariable
annotation so we can pass the ride id in the path/ride/{id}
-
the
createRide
method uses the@RequestBody
annotation to automatically map the JSON request to theThemeParkRide
entity. -
the
@Valid
annotation is included on the incomingThemeParkRide
entity to validate it according to any annotations on its fields (e.g.@NotEmpty
)
Application class
The final class we have to add to have a fully working (but untested) application will be ThemeParkApplication
:
package com.tomgregory;
import com.tomgregory.entity.ThemeParkRide;
import com.tomgregory.repository.ThemeParkRideRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class ThemeParkApplication {
public static void main(String[] args) {
SpringApplication.run(ThemeParkApplication.class);
}
@Bean
public CommandLineRunner sampleData(ThemeParkRideRepository repository) {
return (args) -> {
repository.save(new ThemeParkRide("Rollercoaster", "Train ride that speeds you along.", 5, 3));
repository.save(new ThemeParkRide("Log flume", "Boat ride with plenty of splashes.", 3, 2));
repository.save(new ThemeParkRide("Teacups", "Spinning ride in a giant tea-cup.", 2, 4));
};
}
}
-
the
@SpringBootApplication
annotation tells Spring Boot that this class is defining our main application -
the
main
method means that when we run this class the application will start -
the
sampleData
bean definition adds three default rides for us
Trying out our Spring Boot application
Run the ThemeParkApplication
class or ./gradlew bootRun
to start the application:
Let’s make some requests to the application. I recommend the Postman tool for easily making requests via a nice UI.
Get all rides
Let’s call GET
/ride
to get all rides
We can see the three rides are returned as per the sampleData
bean definition in ThemeParkApplication
.
Create ride
Let’s POST
some JSON data to /ride
to create a new ride:
{
"name":"Monorail",
"description":"Sedate ride that takes you around the park.",
"thrillFactor":2,
"vomitFactor":1
}
We’ll need to remember to also add a Content-Type
header of application/json
.
That’s a 200
OK
response.
So far, so good.
Get ride
Let’s GET
the ride we just added calling /ride/4
:
The Monorail ride is returned as expected!
Adding a test
It’s great that everything’s working, but let’s add an automated test to validate this functionality so our fingers don’t get too tired. 🤞
What follows is an integration test class ThemeParkApplicationIT
that should be added to src/test/java
. We’ll use it to test interactions with the three APIs all the way through our application to the database.
package com.tomgregory;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@ExtendWith( SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ThemeParkApplicationIT {
@Autowired
private MockMvc mockMvc;
@Test
public void getsAllRides() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/ride")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
}
@Test
public void getsSingleRide() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/ride/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
}
@Test
public void returnsNotFoundForInvalidSingleRide() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/ride/4")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andReturn();
}
@Test
public void addsNewRide() throws Exception {
String newRide = "{\"name\":\"Monorail\",\"description\":\"Sedate travelling ride.\",\"thrillFactor\":2,\"vomitFactor\":1}";
mockMvc.perform(MockMvcRequestBuilders.post("/ride")
.contentType(MediaType.APPLICATION_JSON)
.content(newRide)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
}
}
-
the 3 class level annotations are required so that Spring Boot initialises itself, creates the beans, and configures the
MockMvc
class -
MockMvc
can be used to make requests into our application. For example, in the first test we’re making aGET
request to/ride
and expecting astatus().isOk()
response
Run the test by executing ./gradlew test
. Open up build/reports/tests/test/index.html
to see your test report.
Everything’s A-OK!
Running Jenkins
The next stage is to automatically build our Spring Boot application on commit, or in other words to use continuous integration. This will ensure that our master branch is always in a good state, and anything that gets deployed into production is a working version of our application.
We’ll use the popular build tool Jenkins for this purpose, and will set it up to run as a Docker container on our local machine. For the following steps you’ll need Docker, which you can get from the Docker website.
Bootstrapping Jenkins
A good devops philosophy is to define all infrastructure and configuration as code. So, we’ll bootstrap Jenkins to start up and configure whatever jobs we need automatically, with minimal manual setup required.
We’ll build on top of a code example, found in this GitHub repository and cloned like this:
git clone https://github.com/jenkins-hero/jenkins-demo.git
Make sure the Spring Boot application is stopped as Jenkins uses the same port. Then build and run Jenkins:
./gradlew build docker dockerRun
Navigate to http://localhost:8080 and you’ll have an instance of Jenkins running:
You can see we already have one job called seed-job automatically available. This job is responsible for creating any other jobs that we need. Let’s run it by clicking on the name and then clicking Build Now.
Once it finishes, go back to the Jenkins home page and you’ll see that seed-job has generated an additional pipelineJob for us.
Build that job and you’ll see we have a successful pipeline execution, with a Build and Test stage:
If you click on the blue circle to the left of #1 in Build History, you can see the console output:
These two stages just print out some text for now. The point is that we can very quickly have a working instance of Jenkins available. In the next section, we’ll add a new pipeline for our theme park application.
Adding a Jenkins pipeline job to build the Spring Boot application
Let’s modify the jenkins-demo project to include a new pipeline to build our Spring Boot application. The diagram below summarises the flow from when we initially run seed-job to when our pipeline job will run against our theme park application.
Once again, make the code changes yourself for the best learning experience. To do this you’ll need to create and push two of your own repositories:
-
a repository for the Spring Boot theme park application (everything created in sections 1 and 2 of this article). Once created, you’ll need to substitute your own Git repository URL in the createJobs.groovy code snippet below.
-
a repository for the Jenkins code. This should be based on the jenkins-demo project (you can fork the project if you like). Once created, edit the seedJob.xml, replacing the existing Git repository on line 10 with your own e.g.
<url>https://github.com/<your-username>/jenkins-demo.git</url>
If you don’t want to create any repositories and instead prefer to use the ones I’ve made available for you, I’ll also explain how to do that below.
Creating a new job
Open up the createJobs.groovy file, which is responsible for creating any jobs when seed-job is run. We’ll add the following section which defines an additional pipeline job:
pipelineJob('theme-park-job') {
definition {
cpsScm {
scm {
git {
remote {
url 'https://gthub.com/jenkins-hero/spring-boot-api-example.git'
}
branch 'master'
}
}
}
}
}
This definition uses the Jenkins Job DSL Plugin syntax to create a pipeline job from a Jenkinsfile in the spring-boot-api-example repository (or use your own if you prefer). A Jenkinsfile is a pipeline definition that lives inside the repository of the project that it applies to.
Push the above change to your own repository, as Jenkins has to fetch the createJobs.groovy file remotely.
If you don’t want to set that up, you can modify seedJob.xml and change the branch name on line 15 from master to theme-park-job.
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/theme-park-job</name>
</hudson.plugins.git.BranchSpec>
</branches>
This way it will fetch the updated createJobs.groovy file from a branch I’ve made available just for you.
Adding a Jenkinsfile to spring-boot-api-example
We’ll create a Jenkinsfile in the top level of the project with the following contents:
pipeline {
agent any
triggers {
pollSCM '* * * * *'
}
stages {
stage('Build') {
steps {
sh './gradlew assemble'
}
}
stage('Test') {
steps {
sh './gradlew test'
}
}
}
}
This is a basic pipeline that checks for any changes to our code every minute, then runs two stages:
- Build - where we assemble the project
- Test - where we test the project
Push these changes to your own repository, or just rely on the repository I’ve already added with the required Jenkinsfile.
Running the new job
Stop the previous Jenkins instance by running ./gradlew dockerStop
then bring up a fresh one with:
./gradlew build docker dockerRun
First run the seed-job again to create the new theme-park-job:
Then click on the new job and hit Build Now:
After some time you should have a successful build. Jenkins will highlight in green the two stages we defined, as well as the initial checkout.
Final thoughts
Getting Jenkins running and building a Spring Boot project can be quite straightforward. Obviously in a proper setup you won’t run Jenkins on your local machine, as you’ll need high availability and most likely access by other team members. If you’re using AWS consider running Jenkins in an ECS or EKS cluster (see Deploy your own production-ready Jenkins in AWS ECS).
In part 2 of this series we’ll move our Spring Boot application into Docker, build the Docker image in Jenkins, and then push that to a remote repository.
Resources
GITHUB Check out the sample Spring Boot application for the theme park API Here you can find the code for the jenkins-demo project, for bringing up an instance of Jenkins. The theme-park-job branch contains the updated version of createJobs.groovy.
GRADLE Gradle Tutorial - getting started for complete beginners