Spring Cloud step-by-step (part 1)

By Marcin Piczkowski Comment
Subscribe

This post is a first in the series about Spring Cloud - a set of Spring libraries for building “cloud native” applications. What are “cloud native” applications? It means applications that have 12 factors. In short: these are applications that are easy to deploy on available cloud providers, easy to scale up/down, monitor and release (with CI/CD).

I will show you how to start from a simple Spring Web project to create a Spring Cloud application at the end.

The example application which we’re going to build is a rest API for renting goods (e.g videos). It will return price according to a rental plan and duration.

The project is available at github with each step as a separate branch.

In this post we will create spring-cloud-1 branch.

We will first start from standard Spring web application generated with Spring Initializr at http://start.spring.io

Then we will keep adding cloud features to it, such as:

Let’s generate a basic Spring web project.

image

Then we will add REST controller, at the beginning it will just respond with zero price.

package com.example.cloud.pricingservice.v1;

import com.example.cloud.pricingservice.v1.dto.PriceRequest;
import com.example.cloud.pricingservice.v1.dto.PriceResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@RequestMapping(path = "/v1")
public class PriceController {

    @RequestMapping(path = "/prices", method = RequestMethod.POST)
    public ResponseEntity<PriceResponse> getPriceAmount(@RequestBody PriceRequest priceRequest) {

        return new ResponseEntity<>(new PriceResponse(BigDecimal.ZERO), HttpStatus.OK);
    }

}

Start the project with

mvn spring-boot:run

Now we can test it with curl

curl -X POST -H 'Content-Type:application/json' -d '{"plan":"vip", "start": "2011-12-03T10:15:30+01:00","end" : "2011-12-05T12:15:30+01:00"}' http://localhost:8080/v1/prices

It would respond with

{"price":0}

Let’s assume we have a static configuration of daily prices per rental in yaml file, like:

plans: 
   BASIC 10
   STANDARD 20
   VIP 50

We will replace application.properties with application.yaml as Spring supports both formats, and add the plans there:

server:
  port=8080

pricing:
  plan:
    basic: 10
    standard: 20
    vip: 50

Personally I find this format more readable than properties, but it’s a matter of taste. You can use properties if you like so.

In order to get these properties as a map in our application we need to write some code:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.HashMap;

@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "pricing")
public class PlanConfig {

    private HashMap<String, BigDecimal> plan;

    public HashMap<String, BigDecimal> getPlan() {
        return plan;
    }

    public void setPlan(HashMap<String, BigDecimal> plan) {
        this.plan = plan;
    }

}

Now we have a spring-managed bean which we can inject to a service and calculate prices based on it. Let’s create a service class like below:

package com.example.cloud.pricingservice.services;

import com.example.cloud.pricingservice.config.PlanConfig;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;

@Component
public class PriceCalculator {

    private final PlanConfig planConfig;

    public PriceCalculator(PlanConfig planConfig) {
        this.planConfig = planConfig;
    }

    /**
     * Calculates price according to plans. The minimal unit of price calculation is one day.
     * If the rent time overlaps next day the whole price for another day is calculated.
     *
     * @return calculated price
     */
    public BigDecimal calculatePrice(ZonedDateTime rentStartDate, ZonedDateTime rentEndDate, String planName) {
        Assert.notNull("Plan name is required", planName);

        long exactDaysCount = ChronoUnit.DAYS.between(rentStartDate, rentEndDate);
        long daysCount = exactDaysCount + (isOverlappingNextDay(exactDaysCount, rentStartDate, rentEndDate) ? 1 : 0);

        BigDecimal pricePerDay = planConfig.getPlan().getOrDefault(planName.toLowerCase(), BigDecimal.ZERO);
        return pricePerDay.multiply(new BigDecimal(daysCount));
    }

    private boolean isOverlappingNextDay(long exactDaysCount, ZonedDateTime rentStartDate, ZonedDateTime rentEndDate) {
        ZonedDateTime exactEndTime = rentStartDate.plus(exactDaysCount, ChronoUnit.DAYS);
        Duration overlappedDayDuration = Duration.between(exactEndTime, rentEndDate);
        return overlappedDayDuration.getSeconds() > 0;
    }

}

Next, let’s update our controller and use calculatePrice method in it.

@RestController
@RequestMapping(path = "/v1")
public class PriceController {
    private final PriceCalculator priceCalculator;

    public PriceController(PriceCalculator priceCalculator) {
        this.priceCalculator = priceCalculator;
    }

    @RequestMapping(path = "/prices", method = RequestMethod.POST)
    public ResponseEntity<PriceResponse> getPriceAmount(@RequestBody PriceRequest priceRequest) {

        return new ResponseEntity<>(
                new PriceResponse(priceCalculator.calculatePrice(priceRequest.getStart(), priceRequest.getEnd(), priceRequest.getPlan())),
                HttpStatus.OK);
    }

}

If we now build and start the application again and execute the same curl command the result should be different depending on how many days the rental would last. E.g.:

curl -X POST -H 'Content-Type:application/json' -d '{"plan":"basic", "start": "2011-12-03T10:15:30+01:00","end" : "2011-12-05T12:15:30+01:00"}' http://localhost:8080/v1/prices

returns

{"price":30}

Now we have a sample REST API implemented with Spring Web and we can work on it in next post to transform it into Spring Cloud application.

comments powered by Disqus