DEV Community

loading...
Split Software

Containerization with Spring Boot and Docker

split_blog_62269a20361f19 profile image Split Blog Originally published at split.io on ・25 min read

In this tutorial, you’re going to build a Spring Boot web application containerized with Docker that uses feature flags and Split to allow runtime updates to application behavior. The app you’re going to develop manages a donut factory. The donut factory will have a donut data model that will track donut inventories and various important donut attributes. Your customers will be able to buy donuts via a web controller endpoint. You’ll also be able to Create, Read, Update, and Delete (CRUD) donuts via an auto-generated Hypermedia as the Engine of Application State (HATEOAS) REST API.

Spring Boot is the application framework you’ll use to provide the web application features. It’s a Java framework that is enterprise-ready and backed by a codebase proven by decades of use in projects worldwide. You’ll use another Spring framework, Spring Data, to map your Donut Java class to a persistence store (the database). This will allow you to save and load Donut class instances simply by annotating the data model Java class and by creating a straightforward data model repository.

Next, you’re going to use Split to add feature flags to the code. Imagine your factory started rolling out a new line of chocolate donuts, and you decided on a limited release, so you limit the sales of chocolate donuts to a few states initially. But after only a few weeks, the donuts are a hit (because who doesn’t like chocolate?), and your factory figures out a way to ramp up production. Fortunately for you, you considered this possibility. You implemented feature flags controlling the availability of chocolate donuts, allowing you to update your code in real-time without rebuilding and redeploying the application.

You can think of a feature flag as a decision point in your code, a “flag” as one might have used in old-school coding. This is a place where features can be turned on and off based on the flag state. The flag can be a boolean, in its most basic form, but can also be multivariate with arbitrary string values. Split provides a service that makes it easy to implement feature flags. You’ll use their Java SDK to integrate their client into the Spring Boot application. This client talks to the Split servers to update the flag state, and you will use their dashboard to configure the split (or feature flag) and the treatment (customized state of the flag based on input from the application).

Once all that is done, you’re going to use Docker to containerize the application, ensuring that the application has a consistent operating system environment. This is super easy with Spring Boot, and there’s even a Gradle and Maven task for building Docker images. As the last step, you’re going to see how to use Docker Compose to migrate from the in-memory database to a more production-ready MySQL database configured and deployed with only a few lines of code in a yaml file.

Requirements for Spring Boot and Docker

You need to install a couple of requirements before you get started.

Java : I used Java 12 for this tutorial. You can download and install Java by going to the AdaptOpenJdk website. Or you can use a version manager like SDKMAN or even Homebrew.

Split : Sign up for a free Split account if you don’t already have one. This is how you’ll implement the feature flags.

HTTPie : This is a powerful command-line HTTP request utility you’ll use to test the Spring Boot server. Install it according to the docs on their site.

Docker Engine : You need to have Docker Engine installed on your computer. The Docker website has instructions for installing it. If you install Docker Desktop (OSX and Windows), you will need to have it running for the Docker portion of the tutorial.

Docker Compose : This allows you to manage multiple Docker images, allowing you to deploy entire application systems. Install it according to the docs on the Docker website. It is a separate install from Docker Engine.

You won’t need to install Gradle , the dependency and build manager you’ll use for this project, because the bootstrapped project will include a Gradle wrapper (version-locked instance of the Gradle executable included with the project itself. However, if you want to install Gradle or learn more about it, check out their website.

Use Spring Initizlizr To Bootstrap Your Project

Spring has been kind enough to create the Spring Initializer. This app allows you to quickly and easily configure Spring Boot projects. You can even browse the projects before you download them. It also allows you to share the configured project via a link.

Follow this link for the preconfigured starter for this tutorial.

Click the Generate button at the bottom of the page and download the project to your computer. Unzip it. This folder, demo, will be the root directory of the project.

The project configures some basic properties:

  • Gradle for the build tool
  • Java as the base language
  • Java 11 as the Java version
  • Spring Boot version 2.4.5 It also includes the following dependencies:
  • Lombok
  • Spring Web
  • Rest Repositories
  • Spring Data JPA
  • H2 Database

Project Lombok is an annotation-based helper that saves you (or me, really) from creating many getters, setters, and constructors. Check out their website to see all the features.

Spring Web provides the basic Spring MVC features that allow you to create web services.

Rest Repositories allows Spring Data to create REST APIs from JPA data models and repositories automatically.

Spring Data JPA includes the ability to interact with databases and map Java objects to data model instances and database structures.

H2 Database adds an in-memory database implementation that does not persist between sessions and is excellent for testing and projects like this.

For this example, we did not change any of the project metadata (group, artifact, name, description, etc…), but you can also configure that using the Spring Initializr.

You can go ahead and test your sample project. It won’t do much, but it should run without error. Open a bash shell and run the following command in the project root directory.


`./gradlew bootRun`

Enter fullscreen mode Exit fullscreen mode

Notice you’re using a local script, ./gradlew and not gradle itself. That’s the Gradle wrapper. It has a couple of nice advantages. First, it locks the Gradle version so that the build script is guaranteed to work with the Gradle version running it. Second, it means you don’t have to install Gradle locally.

Time To Make The Donuts!

Now you need to make a donut factory. This will demonstrate a number of key Spring Boot features:

  1. creating a persisted data model from a Java class (donuts, in our case),
  2. using CrudRepository to enable persistence of the data model instances,
  3. activating custom query methods for the data model,
  4. creating a REST controller that manipulates data model instances,
  5. how to persist enum data types, and
  6. using constructor dependency injection to inject the repository in our controllers.

Create a new Java file for the donut data model.

src/main/java/com/example/demo/Donut.java


`@Entity
@Data @NoArgsConstructor
public class Donut {

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;

    @Enumerated(EnumType.STRING)
    private DonutToppings toppings;

    public String name;
    public Double costDollars;
    public Integer numberAvailable;

    Donut(String name, DonutToppings toppings, Double costDollars, Integer numberAvailable) {
        this.name = name;
        this.toppings = toppings;
        this.costDollars = costDollars;
        this.numberAvailable = numberAvailable;
    }
}`
<small id="shcb-language-1"><span>Code language:</span> <span>Java</span> <span>(</span><span>java</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Also, create a DonutToppings enum class that is used in the donut data model.


`public enum DonutToppings {

    CHOCOLATE("Chocolate Icing"),
    SPRINKLES("Sprinkles"),
    MAPLE("Maple Icing"),
    GLAZED("Sugar Glaze"),
    BACON("Bacon"),
    POWDERED_SUGAR("Powdered Sugar"),
    NONE("None");

    private final String value;

    DonutToppings(String value) {
        this.value = value;
    }

}`
<small id="shcb-language-2"><span>Code language:</span> <span>C#</span> <span>(</span><span>cs</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

The donut data model has four attributes: id, name, costDollars, toppings, and numberAvailable. The first three are pretty self-explanatory. The property toppings is a little more complex because it’s an enumerated type, so you have to define a separate class for the enum. The last property, numberAvailable, is a simple inventory tracking that tells you how many of those types of donuts you have available.

The two annotations @Data @NoArgsConstructor are from Project Lombok and save you from defining getters and setters for all the properties as well as an empty, no-args constructor. The @Entity annotation is the Spring Boot Java Persistence API (JPA) annotation that tells Spring Boot that this is a data model class.

The next file is the repository. The data model alone does very little. It is the repository that can save and retrieve the data model instances from the database.

java/com/example/demo/DonutRepository.java


`public interface DonutRepository extends CrudRepository<Donut, Long> {  
    List<Donut> findByNameIgnoreCase(String name);  
}`
<small id="shcb-language-3"><span>Code language:</span> <span>Java</span> <span>(</span><span>java</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

The repository code is deceptively simple. There’s a ton of “auto-magicking” going on here. The main thing to know is that this is an instance of a CrudRepository which not only can persist and retrieve data model entities but also automatically enables (in conjunction with the Rest Repositories dependency) a REST API for those entities. If you don’t want this behavior, you can simply omit the Rest Repositories dependence.

The one method in the repository, findByNameIgnoreCase(), uses Spring’s natural-language query creation method. It maps method names to database queries based on a descriptive, natural-language system.

Note: For a complete look at this feature, check out the Spring Data JPA docs.

If you look at the interface for CrudRepository, you’ll see that it implements all of the basic methods needed for CREATE, READ, UPDATE, and DELETE-ing data models. These methods can be overridden to modify their behavior, disable them, or add role-based security.


`public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);
    <S extends T> Iterable<S> saveAll(Iterable<S> entities);
    Optional<T> findById(ID id);
    boolean existsById(ID id);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> ids);
    long count();
    void deleteById(ID id);
    void delete(T entity);
    void deleteAll(Iterable<? extends T> entities);
    void deleteAll();
}`
<small id="shcb-language-4"><span>Code language:</span> <span>Java</span> <span>(</span><span>java</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Notice, however, that the descriptive find method added in the DonutRepository subclass adds a simple way to perform a query based on a model property that didn’t exist in the parent class. This is part of the magic of JPA (ok, not magic, but reflection).

At this point, you have a Spring Boot application capable of creating, reading, updating, and deleting donuts. Run the app using the following command.


`./gradlew bootRun`
<small id="shcb-language-5"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

The console output should end with this:


`...
2021-01-17 13:20:45.752 INFO 12125 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-01-17 13:20:45.758 INFO 12125 --- [main] com.example.demo.DemoApplication : Started DemoApplication in 2.517 seconds (JVM running for 2.755)
<==========---> 80% EXECUTING [1m 28s]`
<small id="shcb-language-6"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Use HTTPie to make a GET request.


`http :8080`
<small id="shcb-language-7"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`HTTP/1.1 200 
...

{
    "_links": {
        "donuts": {
            "href": "http://localhost:8080/donuts"
        },
        "profile": {
            "href": "http://localhost:8080/profile"
        }
    }
}`
<small id="shcb-language-8"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

The /donuts endpoint listed is the controller generated by Spring Data for your data model. Make a request at that endpoint.


`http :8080/donuts`
<small id="shcb-language-9"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`HTTP/1.1 200 
...

{
    "_embedded": {
        "donuts": []
    },
    "_links": {
        "profile": {
            "href": "http://localhost:8080/profile/donuts"
        },
        "search": {
            "href": "http://localhost:8080/donuts/search"
        },
        "self": {
            "href": "http://localhost:8080/donuts"
        }
    }
}`
<small id="shcb-language-10"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

The generated rest interface is based on Hypermedia as the Engine of Application State (HATEOAS – rhymes with adios). The idea is to use hypermedia to describe a REST interface. I won’t dig into this here. Check out the Spring Docs on Spring HATEOAS if you want to get a more detailed explanation.

The main thing to notice above is the empty array "donuts": [].

A donut factory without any donuts is pretty sad. Add a donut. Or 124.


`http :8080/donuts name=Glazed costDollars=0.5 toppings=GLAZED numberAvailable=124`
<small id="shcb-language-11"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`HTTP/1.1 201 
...

{
    "_links": {
        "donut": {
            "href": "http://localhost:8080/donuts/1"
        },
        "self": {
            "href": "http://localhost:8080/donuts/1"
        }
    },
    "costDollars": 0.5,
    "name": "Glazed",
    "numberAvailable": 124,
    "toppings": "GLAZED"
}`
<small id="shcb-language-12"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

GET the root endpoint again, and you’ll see the donut you added.


`http :8080/donuts`
<small id="shcb-language-13"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`...

{
    "_embedded": {
        "donuts": [
            {
                "_links": {
                    "donut": {
                        "href": "http://localhost:8080/donuts/1"
                    },
                    "self": {
                        "href": "http://localhost:8080/donuts/1"
                    }
                },
                "costDollars": 0.5,
                "name": "Glazed",
                "numberAvailable": 124,
                "toppings": "GLAZED"
            }
        ]
    },
    ...
}`
<small id="shcb-language-14"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

You could also DELETE the donut. Notice the donut ID hidden in the link.


`http DELETE :8080/donuts/1`
<small id="shcb-language-15"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`HTTP/1.1 204`
<small id="shcb-language-16"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

That’s all using the auto-generated CrudRepository methods. In the next section, you’re going to add a custom controller and a buyDonut() method.

Add A Custom Controller To The Spring Boot App

Add a new Java file, DonutController.

java/com/example/demo/DonutController.java


`@Controller
@RequestMapping("/api")
public class DonutController {

    DonutRepository donutRepository;

    DonutController(DonutRepository donutRepository) {
        this.donutRepository = donutRepository;
    }

    @RequestMapping("/buy-donut")
    @ResponseBody
    String buyDonut(@RequestParam String donutName) {
        List<Donut> found = this.donutRepository.findByNameIgnoreCase(donutName);
        if (found.size() <= 0) {
            return "Wah. Wah. No donuts for you.";
        }
        else {
            Donut donut = found.get(0);
            if (donut.numberAvailable <= 0) {
                return "Sorry. All out of those.";
            }
            donut.numberAvailable = donut.numberAvailable - 1;
            this.donutRepository.save(donut);
            return "Enjoy your " + donutName + " donut. It costs $" + 
                       donut.costDollars + ". There are " + 
                       donut.numberAvailable + " of these remaining.";
        }
    }

}`
<small id="shcb-language-17"><span>Code language:</span> <span>Arduino</span> <span>(</span><span>arduino</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

This controller encompasses some business logic that uses the donut repository. It allows clients to request a donut by name. It checks to see if the donut exists and if there are donuts of that type available. If there are not, a sad message is returned. If there are donuts of that type available, it returns an exciting success message (but, sadly, not an actual donut because we’re still not living in Star Trek).

This controller method is available at /api/buy-donut, as determined by the two @RequestMapping methods.

Also, update your DemoApplication class. The code below bootstraps a few donut types into the database when the program loads.

java/com/example/demo/DemoApplication.java


`@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    DonutRepository donutRepository;

    DemoApplication(DonutRepository donutRepository) {
        this.donutRepository = donutRepository;
    }

    @PostConstruct
    public void initApplication() {
        if (donutRepository.count() > 1) return;
        donutRepository.save(new Donut("Chocolate", DonutToppings.NONE, 1.50, 10));
        donutRepository.save(new Donut("Maple", DonutToppings.MAPLE, 1.0, 5));
        donutRepository.save(new Donut("Glazed", DonutToppings.GLAZED, 0.75, 20));
    }
}`
<small id="shcb-language-18"><span>Code language:</span> <span>Java</span> <span>(</span><span>java</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Press control-c to stop your application, if you haven’t already, and restart it.


`./gradlew bootRun`
<small id="shcb-language-19"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Hit the root endpoint again, and you’ll see the bootstrapped data.


`http :8080/donuts`
<small id="shcb-language-20"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`HTTP/1.1 200 
...
{
    "_embedded": {
        "donuts": [
            {
                ...
                "costDollars": 1.5,
                "name": "Chocolate",
                "numberAvailable": 10,
                "toppings": "NONE"
            },
            {
                ...
                "costDollars": 1.0,
                "name": "Maple",
                "numberAvailable": 5,
                "toppings": "MAPLE"
            },
            {
                ...
                "costDollars": 0.75,
                "name": "Glazed",
                "numberAvailable": 20,
                "toppings": "GLAZED"
            }
        ]
    },
    ...
}`
<small id="shcb-language-21"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Now, buy a donut!


`http ":8080/api/buy-donut?donutName=Maple"`
<small id="shcb-language-22"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`HTTP/1.1 200 
...

Enjoy your Maple donut. It costs $1.0. There are 4 of these remaining.`
<small id="shcb-language-23"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Great. You got your donut factory. Now, since you don’t want your DevOps team upgrading your operating system or Linux distribution behind your back, crashing your factory, and depriving the world of donuts, you’re going to containerize your donut factory in Docker. This will provide a consistent and predictable operating environment, ensuring that the donuts flow and crisis are averted.

Dockerize the Spring Boot App

Dockerizing the app is very, very simple. Create a Dockerfile in your project root. This Docker file simply specifies using a source image with OpenJDK 12 and tells Docker to run the JAR file built from the project.

Dockerfile


`FROM openjdk:12
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]`
<small id="shcb-language-24"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

If you installed Docker Desktop, make sure it’s running. You need to have Docker Engine installed and running locally (you should be able to run docker -v and get a Docker version).

Stop your app (control-c).

Docker uses the .jar file, so you need to tell Gradle to build it.


`./gradlew build`
<small id="shcb-language-25"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Build the Docker image. Here I’m applying the tag splitdemo/spring-boot-docker to the image.


`docker build --build-arg JAR_FILE="build/libs/*.jar" -t splitdemo/spring-boot-docker .`
<small id="shcb-language-26"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Alternative, just so you know, you could both build the project and the Docker image simultaneously using Gradle (you don’t need to run the following command, it’s just an alternative to the previous two).


`./gradlew bootBuildImage --imageName=splitdemo/spring-boot-docker`
<small id="shcb-language-27"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`Sending build context to Docker daemon 66.18MB
Step 1/4 : FROM openjdk:12
 ---> e1e07dfba89c
Step 2/4 : ARG JAR_FILE=target/*.jar
 ---> Using cache
 ---> 085ee17b7ea0
Step 3/4 : COPY ${JAR_FILE} app.jar
 ---> 28447f07ad6f
Step 4/4 : ENTRYPOINT ["java","-jar","/app.jar"]
 ---> Running in 3ce1950c4acd
Removing intermediate container 3ce1950c4acd
 ---> f0551a6ea168
Successfully built f0551a6ea168
Successfully tagged splitdemo/spring-boot-docker:latest`
<small id="shcb-language-28"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Run the Docker image.


`docker run -p 8080:8080 splitdemo/spring-boot-docker`
<small id="shcb-language-29"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

In the command above, you told Docker to expose the 8080 port, mapped to the 8080 port inside the container, and you specified the tagged image you just built.

When it runs, you should see the same console output you saw previously.


`...
2021-01-17 21:57:52.785 INFO 1 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-01-17 21:57:52.794 INFO 1 --- [main] com.example.demo.DemoApplication : Started DemoApplication in 4.244 seconds (JVM running for 4.7)`
<small id="shcb-language-30"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

You should also be able to list and buy donuts.


`http :8080/donuts`
<small id="shcb-language-31"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`http ":8080/api/buy-donut?donutName=Maple"`
<small id="shcb-language-32"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Great. Your donut factory is now safe from overzealous sysops.

But, wait! You just realized your chocolate donuts are not ready for production yet. They weren’t supposed to be released yet. And when they are released, they’re supposed to be available only to a select number of users, at least at first, because of limited production. Ugh. Now you’ve got to fix the code, rebuild the image, re-test everything, and re-deploy.

This is where feature flags could have saved you a headache.

Implement Feature Flags In Your Spring Boot + Docker Project

As mentioned previously, feature flags are decision points in your code that can be changed remotely. Split provides the implementation of this service that you’re going to use here. If you have not already signed up for a free Split account, please do so now.

Add the Split Java SDK to your application by adding the dependency to the build.gradle file.


`dependencies {
    ...
    compile 'io.split.client:java-client:4.1.3'
}`
<small id="shcb-language-33"><span>Code language:</span> <span>JSON / JSON with Comments</span> <span>(</span><span>json</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Next, create the SplitConfig class. This class initializes the SplicClient bean that you will use in your controller class to retrieve the feature flag state.

java/com/example/demo/SplitConfig.java


`@Configuration
public class SplitConfig {

    @Value("#{ @environment['split.api-key'] }")
    private String splitApiKey;

    @Bean
    public SplitClient splitClient() throws Exception {
        SplitClientConfig config = SplitClientConfig.builder()
                .setBlockUntilReadyTimeout(1000)
                .enableDebug()
                .build();

        SplitFactory splitFactory = SplitFactoryBuilder.build(splitApiKey, config);
        SplitClient client = splitFactory.client();
        client.blockUntilReady();

        return client;
    }
}`
<small id="shcb-language-34"><span>Code language:</span> <span>Java</span> <span>(</span><span>java</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

You will need to implement the split in code using the client and add your Split API keys to the project configuration, but before you do that, you need to create the treatment on Split.

Create The Split Treatment

You should have already signed up for a Split account. Open your Split dashboard.

Remember that a split is the same thing as a feature flag : a decision point in your code that can be toggled at runtime, live, as opposed to being baked into the code in a way that requires rebuilding and redeploying. Every split can have multiple treatments. Treatments are the different states the feature flag can take depending on the user attributes according to the treatment rules. In this case, there are two treatments: on and off, a simple boolean, but you can have far more complex splits with multivariate treatments if you like.

Create a new split by clicking the blue Create Split button.

Name the split DonutFactory. Click Create.

Add a targeting rule to the split. Click the blue Add Rules button.

The default rule is a boolean rule with two values: on and off. The default value is off. Now you need to define when the rule changes from off to on. In this example, you’re going to use attributes to determine the flag state. Attributes are arbitrary metadata passed along with the user identifier that can be used to control splits (aka. feature flags).

Scroll down to Set targeting rules. Add a rule to match the image below. The user attribute should be location. The drop-down should be is in list. Add three values: Texas, Oregon, Alaska. Change the serve dropdown from Off to On. These are the states that will get the chocolate donuts.

Click the blue Save Changes button at the top. And click Confirm on the next page. Once you click confirm, the changes are live. That’s the whole point of the feature flags. However, if you make a mistake, you can affect an awful lot of users very quickly. This is why Split has wisely implemented change tracking and the confirmation screen. As they say: “With great responsibility comes the ability to really screw things up.”

You need to retrieve your API keys so that you can put them in the Java app. Click the rounded square icon in the top-left that says DE , and in the popup menu, click Admin Settings. Click API Keys in the side panel. You want to copy the staging-default SDK keys (not the Javascript SDK keys).

Back in the Java app, add this to your application.properties.

src/main/resources/application.properties


`split.api-key=<your API key>`
<small id="shcb-language-35"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Add the Feature Flag to Spring Boot

Now you need to update the DonutController to use the SplitClient. Just replace the contents of your file with the following contents.

java/com/example/demo/DonutController.java


`@Controller
@RequestMapping("/api")
public class DonutController {

    Logger logger = LoggerFactory.getLogger(DonutController.class);

    DonutRepository donutRepository;
    SplitClient splitClient;

    DonutController(DonutRepository donutRepository, SplitClient splitClient) {
        this.donutRepository = donutRepository;
        this.splitClient = splitClient;
    }

    @RequestMapping("/buy-donut")
    @ResponseBody
    String buyDonut(@RequestParam String donutName, @RequestParam String username, @RequestParam String userLocation) {

        // Create the attributes map

        Map<String, Object> attributes = Map.of("location", userLocation);
        // Get the treatment from the Split Client
        String treatment = this.splitClient.getTreatment(username,"DonutFactory", attributes);

        // Log the treatment, just for fun
        logger.info("Treatment="+treatment.toString());

        // Make sure only people with the treatment "on" get the chocolate donuts
        if (treatment.equals("off") && donutName.toLowerCase().equals("chocolate")) {
            return "Wah. Wah. No chocolate donuts for you.";
        }

        List<Donut> found = this.donutRepository.findByNameIgnoreCase(donutName);

        if (found.size() <= 0) {
            return "Wah. Wah. No donuts for you.";
        }
        else {
            Donut donut = found.get(0);
            if (donut.numberAvailable <= 0) {
                return "Sorry. All out of those.";
            }
            donut.numberAvailable = donut.numberAvailable - 1;
            this.donutRepository.save(donut);
            return "Enjoy your " + donutName + " donut. It costs $" + donut.costDollars + ". There are " + donut.numberAvailable + " of these remaining.";
        }
    }

}`
<small id="shcb-language-36"><span>Code language:</span> <span>Arduino</span> <span>(</span><span>arduino</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

In the new file, you use constructor dependency injection to pass the SplitClient to the DonutController, just like you did with the repository. There are also two new parameters to the method: username and userLocation. Now, just passing this information as a query parameter on an unsecured API is not exactly secure. In a real scenario, Russian and Chinese hackers would totally be ordering chocolate donuts in microseconds. However, for our hypothetical tutorial, it’s enough to assume the incoming data is accurate.

The actual treatment is requested and retrieved in this section of the code:


`// Create the attributes map
Map<String, Object> attributes = Map.of("location", userLocation);
// Get the treatment from the Split Client
String treatment = this.splitClient.getTreatment(username,"DonutFactory", attributes);`
<small id="shcb-language-37"><span>Code language:</span> <span>JavaScript</span> <span>(</span><span>javascript</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

You pass the splitClient.getTreatment() method three parameters: 1) the username, 2) the split name (DonutFactory), and 3) the map of attributes. The split returns the treatment (the state of the feature flag or split) based on these values. This happens very quickly because split values are cached locally and updated asynchronously instead of being requested when the client method is called. This means that the splitClient.getTreatment() method is very fast and is updated behind the scenes but doesn’t risk hanging waiting on a network response.

Rebuild the JAR file and the Docker image using either the Gradle task bootBuildImage or by building the JAR and using Docker to build the image.


`./gradlew build`
<small id="shcb-language-38"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`docker build --build-arg JAR_FILE="build/libs/*.jar" -t splitdemo/spring-boot-docker .`
<small id="shcb-language-39"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Run the Docker image.


`docker run -p 8080:8080 splitdemo/spring-boot-docker`
<small id="shcb-language-40"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Your donut factory is live.

Try the following request. Notice that you’re using HTTPie to simplify passing URL params by using == instead of =, which is used to denote HTTP POST form params. See HTTPie’s docs on query string parameters.

Texas gets chocolate donutes.


`http :8080/api/buy-donut donutName==Chocolate username==andrew userLocation==Texas`
<small id="shcb-language-41"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`Enjoy your Chocolate donut. It costs $1.5. There are 9 of these remaining.`
<small id="shcb-language-42"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

California doesn’t.


`http :8080/api/buy-donut donutName==Chocolate username==andrew userLocation==California`
<small id="shcb-language-43"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`Wah. Wah. No chocolate donuts for you.`
<small id="shcb-language-44"><span>Code language:</span> <span>Shell Session</span> <span>(</span><span>shell</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Now imagine your donut supply line ramps up, and you have more chocolate donuts, and people from California are calling and complaining that they aren’t getting chocolate donuts. They’re angry. They’re gonna sue. You need to update the donut availability NOW. You can’t afford to wait for a rebuild and redeployment of the code. All you have to do is go to your Split dashboard and update the targeting rules for your DonutFactory split.

Go to the Set targeting rules section. In the text field where you entered the previous states (Texas, Alaska, and Oregon), enter California.

Click Save Changes (at the top of the screen). Click Confirm.

In just moments, without changing a line of code, California now gets donuts.


`http :8080/api/buy-donut donutName==Chocolate username==andrew userLocation==California`
<small id="shcb-language-45"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`Enjoy your Chocolate donut. It costs $1.5. There are 8 of these remaining.`
<small id="shcb-language-46"><span>Code language:</span> <span>Shell Session</span> <span>(</span><span>shell</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Make Your Spring Boot App Production Ready With Docker Compose

The last step is to remove the H2 in-memory database and use a production-ready MySQL database. You’re going to use Docker Compose to run the app image and the database image simultaneously while also mapping their ports.

You can think of Docker Compose as a kind of “meta” Docker or a Docker orchestrator. It allows you to control the deployment of multiple Docker images so that you can do things like spin up a web application, a REDIS cache, an Elasticsearch instance, and a backing database simultaneously. In our somewhat simpler use-case, you’ll use it to deploy a MySQL database along with the Spring Boot web application.

In the build.gradle file, comment out or delete the h2database dependency and replace it with the MySQL connector.


`dependencies {
    // runtimeOnly 'com.h2database:h2' <-- DELETE ME OR COMMENT OUT
    runtimeOnly 'mysql:mysql-connector-java' <-- ADD ME
}`
<small id="shcb-language-47"><span>Code language:</span> <span>JSON / JSON with Comments</span> <span>(</span><span>json</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Download the wait-for-it.sh shell script from the project GitHub page or copy it from this tutorial’s GitHub repository. Copy it in the project root directory. Make it executable using the following command: sudo chmod +x wait-for-it.sh.

You have to use the wait-for-it.sh script because Docker Compose will start both of the containers, and Spring Boot will try to connect to the database before it’s ready, leading to an error. This script causes the execution of the Spring Boot application to wait on the availability of the database. You’ll see below you update the command in the Dockerfile to use this script.

Another project that does a similar thing to the wait-for-it.sh script is Dockerize. I chose not to use it for this example project, but it also allows you to wait for other services to load in a Docker Compose script, so I thought I’d mention it. You can take a look at the Dockerize GitHub page for more info.

Change the Dockerfile in the project root.


`FROM openjdk:12
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
COPY wait-for-it.sh wait-for-it.sh
ENTRYPOINT ["./wait-for-it.sh", "db:3306", "--", "java", "-jar", "app.jar"]`
<small id="shcb-language-48"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Create a docker-compose.yml file in the project root.


`version: '3'  
services:  
  db:  
    image: mysql:8.0  
    restart: always  
    environment:  
      - MYSQL_ROOT_PASSWORD=root  
      - MYSQL_DATABASE=donuts  
      - MYSQL_USER=splitdemo  
      - MYSQL_PASSWORD=splitdemo  
    ports:  
      - 3306:3306  
    volumes:  
      - db-data:/var/lib/mysql  
  app:  
    build: ./  
    restart: on-failure  
    depends_on:  
      - db  
    ports:  
      - 8080:8080  
    environment:  
      - DATABASE_HOST=db  
      - DATABASE_USER=splitdemo  
      - DATABASE_PASSWORD=splitdemo  
      - DATABASE_NAME=donuts  
      - DATABASE_PORT=3306  
volumes:  
  db-data:`
<small id="shcb-language-49"><span>Code language:</span> <span>YAML</span> <span>(</span><span>yaml</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Take note that the docker-compose.yml file has your database passwords in it. Don’t check this file into a repository if you’re worried about application security.

Add to src/main/resources/application.properties the following properties. You’re getting all of the necessary database configuration from the environment variables defined in the docker-compose.yml file and passing it to Spring Data here.


`split.api-key=<Your API key>
spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
spring.datasource.username=${DATABASE_USER}
spring.datasource.password=${DATABASE_PASSWORD}
spring.jpa.hibernate.ddl-auto=update`
<small id="shcb-language-50"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

What you’ve done is configure two Docker services using two images. The first service, db, uses a MySQL 8.0 image and exposes the standard MySQL port 3306. The second is the Spring Boot app built from the Dockerfile you defined and just updated to use the wait-for-it.sh script. You also define the necessary database configuration information and a volume for the database so that the data is persisted.

The property spring.jpa.hibernate.ddl-auto is set to update because it will allow Spring to auto-create the database tables for the data models but won’t drop them between sessions. In an actual production scenario, you would want to set this to none to turn this feature off and manage your database creation separately.

Build the application JAR file. Run the following command from a shell in your project root.


`./gradlew build`
<small id="shcb-language-51"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Build the Docker containers.


`docker-compose build`
<small id="shcb-language-52"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Start the app and the database using Docker Compose.


`docker-compose up`
<small id="shcb-language-53"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

You should be able to buy donuts. From a separate shell, run the following command.


`http :8080/api/buy-donut donutName==Glazed username==andrew userLocation==Alaska`
<small id="shcb-language-54"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>


`HTTP/1.1 200 
...

Enjoy your Glazed donut. It costs $0.75. There are 19 of these remaining.`
<small id="shcb-language-55"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

To stop the app, press control-c in the shell running Docker Compose.

You can delete the containers using the following command (run from the project root directory).


`docker-compose down`
<small id="shcb-language-56"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

You can delete the app containers AND the database volume using the next command.


`docker-compose down --volumes`
<small id="shcb-language-57"><span>Code language:</span> <span>Bash</span> <span>(</span><span>bash</span><span>)</span></small>
Enter fullscreen mode Exit fullscreen mode

Learn More About Spring Boot, Docker, and Feature Flags

In this tutorial, you created a Spring Boot web application that used Spring Data to persist your data model. You saw how Spring Boot with Spring Data allows you to create a fully-featured REST API from your data model easily. You also added a custom web controller and used dependency injection to inject your data model repository into your controller. You used Split to add a feature flag to your application and saw how it could be used to make changes to application behavior at runtime without having to rebuild or redeploy. Finally, you saw how you could use Docker and Docker Compose to containerize your application.

Ready to learn more? We’ve got some additional resources covering all of these topics and more! Check out:

And as always, we’d love to have you follow us on YouTube and Twitter for more great content!

Discussion (0)

Forem Open with the Forem app