In this tutorial, you’re going to build a microservice in Java using Spring Boot and Spring Cloud. The service will be a simple weather service that returns the current temperature for a given zip code or city. It will consist of a public gateway API, a Eureka discovery server, and a private weather resource server. You will also use Split to implement a feature flag and see how this can be used to control application behavior at runtime. The weather resource server will retrieve current weather data from the OpenWeatherMap API.
There are a lot of parts to this project, so let’s take a closer look at them before you get started.
The API gateway will be the publicly accessible service. This is the service that will act as the doorway into the microservice. In this example application, it will return processed weather data, such as temperatures for zips and city names. The weather data will be retrieved from a private service in the microservice network, which we are calling the weather resource server.
The API gateway will not have the URI for the private weather resource server hardcoded into it. Instead, it will look up the address on a Eureka discovery server. In our small, single-instance microservice there’s no actual need for a discovery server but in a larger framework, the Eureka server allows server instances to register themselves as they are added to the network and de-registered as they go down. It’s like a phone book for the microservice network (for those of us who remember phone books), or a contacts list (for anyone born after about 1985).
The discovery server has two main benefits:
- It decouples service providers from clients without requiring DNS
- It enables client-side load-balancing
There’s a nice tutorial on the Spring website that covers load balancing with Spring Cloud and the reasons you might want to use client-side load-balancing over a traditional DNS load balancer. This tutorial won’t cover actually implementing load balancing. However, it will cover the benefits of client-side load balancing. Client-side load balancing is smarter because it has access to application state and because it can avoid the dogpiling effect that DNS caching can result in with traditional DNS load balancers.
Notice that the weather resource server will not generate the weather information itself. Instead, it will retrieve it from the OpenWeatherMap API.
The weather resource server will also implement two versions of the API, and you’ll see how to use Split’s feature flag service to dynamically enable and disable the V2 API in real-time without having to redeploy code.
You’ll also see how to implement HTTP Basic authentication on the API gateway using Spring Security 5.
Requirements
Before you get started, you’ll need to make sure you have a few tools installed.
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. Install it according to the docs on their site.
Java 11 : This project uses Java 11. OpenJDK 11 will work just as well. Instructions are found on the OpenJDK website. OpenJDK can also be installed using Homebrew. Alternatively, SDKMAN is another excellent option for installing and managing Java versions.
OpenWeatherMap : for demonstration purposes in this tutorial, you’ll need a free OpenWeatherMap API account. Sign up here.
Create the Weather Resource Server
The first component in the microservice network you’re going to create is the weather resource server. This is the private service that sits within the network that will be publicly accessed via the API gateway service. You’ll create the API gateway service in a moment.
Step 1 – For this service, you’re going to need your OpenWeatherMap API key. Make sure you’ve already signed up for a free account. You can find the API key by signing in to your OpenWeatherMap account and going to the API key page or by selecting My API Keys from the account drop-down.
Create a parent directory for the three Spring Boot projects your going to make, maybe something like split-spring-cloud-microservice
.
Use the Spring Initializr REST API to bootstrap your project by running the following command from the project parent directory.
http https://start.spring.io/starter.tgz \
bootVersion==2.6.2 \
javaVersion==11 \
group==com.example.weatherservice \
artifactId==weather-service \
name==weather-service \
dependencies==actuator,cloud-eureka,web,cloud-feign,devtools,lombok \
baseDir==weather-service | tar -xzvf -
This creates a Spring Boot project configured to use Maven as the build system and dependency manager with Java 11 and Spring Boot version 2.6.2
. The dependencies you’re including are:
-
actuator
: exposes some pre-defined endpoints for metadata about the application -
web
: Spring web framework that includes the MVC/non-reactive web tools necessary to create a REST API -
cloud-eureka
: service discovery framework that the API gateway will use to look up the URI for the weather resource server -
cloud-feign
: “declarative web service client” (as described in the Spring docs) that simplifies and structures how external web services are called -
lombok
: helper that includes annotations for automatically generating ceremony code such as constructors, getters, and setters -
devtools
: a Spring Boot helper for app development that includes things like live reload and automatic restart
Step 2 – From within the weather-service
directory, you need to create a few files and configure the application.
Open src/main/resources/application.properties
and add the following:
open-weather-api-key={yourOpenWeatherApiKey}
server.port=8090
spring.application.name=weather-service
Make sure you replace {yourOpenWeatherApiKey}
with your actual API key from the OpenWeatherMap website.
You’re setting the server port to 8090
so that the default ports on the services do not clash.
The spring.application.name
value will be the value that the service is registered under with the Eureka server. This value essentially takes the place of the service URI and is the value that the gateway will use to look up the weather service URI. As mentioned previously, this abstraction in the context of the microservice allows a lot of flexibility for service redundancy, load balancing, and emergency fail-over.
Update the main application file.
com/example/weatherservice/WeatherServiceApplication.java
package com.example.weatherservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class WeatherServiceApplication {
public static void main(String[] args) {
SpringApplication.run(WeatherServiceApplication.class, args);
}
}
The only change in this file is the addition of the @EnableFeignClients
annotation. This lets Spring Boot know that the service will use Feign clients and to look for the @FeignClient
annotation on interfaces.
Now you need a class to hold the response from the OpenWeatherMap API. Spring Boot will use Jackson deserialization to automatically map the JSON response from the OpenWeatherMap API to the Java objects described in this file.
com/example/weatherservice/OpenWeatherApiResponse.java
package com.example.weatherservice;
import lombok.Data;
import java.util.List;
@Data
class WeatherInfo {
private String main;
private String description;
}
@Data
class Main {
private Number temp;
private Number feels_like;
private Number pressure;
private Number humidity;
}
@Data
class OpenWeatherApiResponse {
private List<WeatherInfo> weather;
private Main main;
}
The specific OpenWeatherMap API you’ll be calling returns current weather data. You can take a look at the documentation for it on their website. You’ll notice that the API response actually returns a lot more data than is captured here (there just wasn’t any reason to try and deserialize and map all of the data from the returned JSON).
Step 3 – Create a file to hold the V1 version of the weather resource server.
com/example/weatherservice/WeatherRestControllerV1.java
package com.example.weatherservice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name="weather-client-v1", url="http://api.openweathermap.org/")
interface OpenWeatherClientV1 {
@RequestMapping(method = RequestMethod.GET, value = "data/2.5/weather?units=imperial&zip={zip}&appid={apiKey}", consumes = "application/json")
OpenWeatherApiResponse getWeatherbyZip(@RequestParam("zip") String zip, @RequestParam("apiKey") String apiKey);
}
@RestController()
@RequestMapping("api/v1")
public class WeatherRestControllerV1 {
@Value( "${open-weather-api-key}" )
private String apiKey;
Logger logger = LoggerFactory.getLogger(WeatherRestControllerV1.class);
private final OpenWeatherClientV1 weatherClient;
public WeatherRestControllerV1(OpenWeatherClientV1 weatherClient) {
this.weatherClient = weatherClient;
}
@GetMapping("/weather")
OpenWeatherApiResponse getWeatherByZip(@RequestParam("zip") String zip) {
logger.info("Getting weather for zip = " + zip);
OpenWeatherApiResponse weatherApiResponse = weatherClient.getWeatherbyZip(zip, apiKey);
return weatherApiResponse;
}
}
The interface at the top of this file is the Feign client that describes to Spring Boot how you want to make calls to the OpenWeatherMap API. That client is then injected into the WeatherRestControllerV1
, where it is used to retrieve weather information that is exposed by its getWeatherByZip(zip)
, which is what the API gateway will call.
The version 1 API has only one method that returns the weather by zip, which is mapped to api/v1/weather
and expects the zip
as a query param.
Step 4 – Create a controller for the V2 version of the weather resource server.
com/example/weatherservice/WeatherRestControllerV2.java
package com.example.weatherservice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
@FeignClient(name="weather-client-v2", url="http://api.openweathermap.org/")
interface OpenWeatherClientV2 {
@RequestMapping(method = RequestMethod.GET, value = "data/2.5/weather?units=imperial&zip={zip}&appid={apiKey}", consumes = "application/json")
OpenWeatherApiResponse getWeatherByZip(@RequestParam("zip") String zip, @RequestParam("apiKey") String apiKey);
@RequestMapping(method = RequestMethod.GET, value = "data/2.5/weather?units=imperial&q={cityStateCountry}&appid={apiKey}", consumes = "application/json")
OpenWeatherApiResponse getWeatherByCity(@RequestParam("cityStateCountry") String cityStateCountry, @RequestParam("apiKey") String apiKey);
}
@RestController("v2")
@RequestMapping("api/v2")
public class WeatherRestControllerV2 {
@Value( "${open-weather-api-key}" )
private String apiKey;
Logger logger = LoggerFactory.getLogger(WeatherRestControllerV2.class);
private final OpenWeatherClientV2 weatherClient;
public WeatherRestControllerV2(OpenWeatherClientV2 weatherClient) {
this.weatherClient = weatherClient;
}
@GetMapping("/weather")
OpenWeatherApiResponse getWeather(@RequestParam(required = false) String zip, @RequestParam(required = false) String cityStateCountry) {
if (zip != null) {
logger.info("Getting weather for zip = " + zip);
OpenWeatherApiResponse weatherApiResponse = weatherClient.getWeatherByZip(zip, apiKey);
return weatherApiResponse;
}
else if (cityStateCountry != null) {
logger.info("Getting weather for cityStateCountry = " + cityStateCountry);
OpenWeatherApiResponse weatherApiResponse = weatherClient.getWeatherByCity(cityStateCountry, apiKey);
return weatherApiResponse;
}
else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Must specify either `zip` or `cityStateCountry`");
}
}
}
The V2 API adds the ability to retrieve weather information based on city, state, and country. Notice that the private, weather resource server will simultaneously expose both API versions. When you implement the gateway, you will use feature flags and Split to control access to the V2 API, simulating a test rollout.
Create the API Gateway and the Eureka Server
Step 5 – Again use Spring Initializr to download a pre-configured starter. Run the command below from a Bash shell within the parent directory of the tutorial project (not within the weather resource server project directory).
http https://start.spring.io/starter.tgz \
bootVersion==2.6.2 \
javaVersion==11 \
group==com.example.gateway \
artifactId==api-gateway \
name==api-gateway \
dependencies==cloud-eureka,web,cloud-feign,devtools,lombok \
baseDir==api-gateway | tar -xzvf -
The dependencies and other parameters here are very similar, so I won’t explain them.
Add the following line to the application.properties
file.
src/main/resources/application.properties
spring.application.name=api-gateway
Update the ApiGatewayApplication
class to the following:
src/main/java/com/example/apigateway/ApiGatewayApplication.java
package com.example.apigateway;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name="weather-service", path = "api/v1", contextId = "v1")
interface WeatherServerClientV1 {
@RequestMapping(method = RequestMethod.GET, value = "weather", consumes = "application/json")
OpenWeatherApiResponse getWeatherByZip(@RequestParam("zip") String zip);
}
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@RestController("gateway-api")
@RequestMapping()
public class WeatherGatewayService {
Logger logger = LoggerFactory.getLogger(WeatherGatewayService.class);
private final WeatherServerClientV1 weatherClientV1;
public WeatherGatewayService(WeatherServerClientV1 weatherClientV1) {
this.weatherClientV1 = weatherClientV1;
}
@GetMapping("/temperature/zip/{zip}")
String getTempByZip(@PathVariable("zip") String zip) {
logger.info("Getting weather for zip = " + zip);
OpenWeatherApiResponse weatherApiResponse = weatherClientV1.getWeatherByZip(zip);
logger.info(weatherApiResponse.toString());
return weatherApiResponse.getMain().getTemp().toString()+"°F, feels like " + weatherApiResponse.getMain().getFeels_like().toString()+"°F";
}
}
}
This file has the Feign client interface for accessing the V1 weather resource server as well as a public method accessible at the endpoint /temperature/zip/{zip}
. This does not yet have any V2 methods or endpoints. In the next section, you’ll see how to add the V2 API methods and do a controlled rollout with Split and feature flags.
Step 6 – Create the data model file for the weather data.
src/main/java/com/example/apigateway/OpenWeatherApiResponse.java
package com.example.apigateway;
import lombok.Data;
import java.util.List;
@Data
class WeatherInfo {
private String main;
private String description;
}
@Data
class Main {
private Number temp;
private Number feels_like;
private Number pressure;
private Number humidity;
}
@Data
class OpenWeatherApiResponse {
private List<WeatherInfo> weather;
private Main main;
}
This is just a duplicate of the same file in the weather resource server.
Finally, the last piece, you need to create the Eureka discovery server. Again, from the parent directory, run the following command to download a starter project.
http https://start.spring.io/starter.tgz \
bootVersion==2.6.2 \
javaVersion==11 \
group==com.example.discovery \
artifactId==discovery-service \
name==discovery-service \
dependencies==cloud-eureka-server \
baseDir==discovery-service | tar -xzvf -
The only dependency here is cloud-eureka-server
, which brings in the necessary dependencies to creating a discovery service.
Update application.properties
to the following.
src/main/resources/application.properties
server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
logging.level.com.netflix.eureka=OFF
logging.level.com.netflix.discovery=OFF
Open DiscoveryServiceApplication
and make sure it matches the following. All you are doing is adding the @EnableEurekaServer
annotation.
package com.example.discoveryservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServiceApplication.class, args); }
}
That’s all you have to do to create the Eureka discovery server!
Test the Weather App Microservice
Step 7 – Start the projects using ./mvnw spring-boot:run
from a Bash shell. Start the Eureka server first, then the private weather resource server, and finally the API gateway. If you start things in the wrong order, the dead will definitely rise from their graves and take over the world, so don’t mix it up.
Once all three services are running, open the Spring Eureka dashboard at http://localhost:8761/.
You should see something like this, with two service instances running.
The ports for the different services you have running are:
- 8080: the public weather service port, the API gateway
- 8090: the private weather resource server within the microservice network
- 8761: the Eureka discovery server port
You can test the microservice by making the following request from a new Bash shell.
http :8080/temperature/zip/78025
HTTP/1.1 200
...
82.06°F, feels like 82.4°F`
You could also make a request to the private weather resource service. This is the JSON data that the private API is returning from the OpenWeatherMaps service.
http :8090/api/v1/weather?zip=78025
HTTP/1.1 200
...
{
"main": {
"feels_like": 81.82,
"humidity": 47,
"pressure": 1011,
"temp": 81.48
},
"weather": [
{
"description": "clear sky",
"main": "Clear"
}
]
}
This corresponds to the following request being made to the OpenWeatherMaps API.
http api.openweathermap.org/data/2.5/weather zip==78025 appid=={apiKey}
HTTP/1.1 200 OK
...
{
"base": "stations",
"clouds": {
"all": 3
},
"cod": 200,
"coord": {
"lat": 30.0731,
"lon": -99.269
},
"dt": 1640472064,
"id": 0,
"main": {
"feels_like": 300.66,
"grnd_level": 952,
"humidity": 47,
"pressure": 1011,
"sea_level": 1011,
"temp": 300.45,
"temp_max": 303.96,
"temp_min": 296.45
},
"name": "Ingram",
"sys": {
"country": "US",
"id": 2003197,
"sunrise": 1640439056,
"sunset": 1640475813,
"type": 2
},
"timezone": -21600,
"visibility": 10000,
"weather": [
{
"description": "clear sky",
"icon": "01d",
"id": 800,
"main": "Clear"
}
],
"wind": {
"deg": 278,
"gust": 2.67,
"speed": 2.54
}
}
Use Split And Feature Flags to Implement a Controlled Rollout
Step 8 – Now you’re going to use Split’s implementation of feature flags. Feature flags are a way to control code dynamically at runtime. You can think of them as dynamic variables whose state can be controlled in real-time, both manually and automatically, based on a large number of configurable parameters on the Split dashboard. In this example, you’ll use a feature flag to simulate a controlled roll-out of the V2 API, but there are lots of potential uses cases for feature flags.
They’re applicable any time you want to segment application behavior based on a user identity or any other application parameter. You could, for example, use it to give some users access to premium features, to selectively test features with certain users, or to expose admin and debug features only to certain accounts.
Make sure you’ve already signed up for a free Split developer account.
Before you get started, let’s go over a little terminology and some basic concepts that Split uses. A feature flag is also called a split , in that it is a decision point in the code. The split is a String key that lives on the Split servers whose value can be looked up using the Split SDK. As you’ll see, Split does some fancy caching so that you don’t have to worry about network calls slowing down your code when this lookup happens.
The value a split takes is a treatment. Treatments are strings. Splits can have any number of treatments from two to dozens. The treatment, or value, that a split has at any given point is determined by rules configured on the Split dashboard. These rules are called targeting rules. There are a lot of options, and changes made to a Split are propagated in real-time to your application. Thus they are like having dynamic, real-time, configurable switched in your application code.
One common way of determining a split’s treatment is by using a segment of the user population. A segment is a sub-group of your users. This could be your testers. This could be your freemium users. This could be the newest users. This could be everyone in Austin, Texas. As you’ll see, there’s a ton of flexibility in how the system is implemented. But the idea is, for example, that you can create groups of users, or segments, for whom the split has one treatment while the rest default to another treatment.
In the next section of the tutorial, you’re going to see how you can use Split to selectively expose the V2 API, which you’ll implement below, to a segment of users. You’ll see how this can be done in real-time, without redeploying any code, and how you can either roll the deployment back or expand the deployment, also dynamically without code changes, depending on how the rollout goes.
Create a Split Feature Flag
Step 9 – Log into your Split dashboard.
From the left-menu, under TARGET , click Splits. Click the blue Create split button.
Name the split v2-deployment
. Change the traffic type to user
.
Click Create.
You just created the split (or feature flag). Now you need to define the different treatments (or values) the split can take and some targeting rules to determine under what conditions each state is active.
In this section of the tutorial, you’re going to use two users to simulate a test deployment. user1
will have the new V2 API activated while user2
will only see the old V1 API. While here you are only using two users, it’s very simple to expand this to a list of users (a segment) that is either imported via a CSV file or dynamically assigned based on user attributes.
On the v2-deployment
split panel, click the Add Rules button.
This creates a new set of targeting rules for your split. It will open the targeting rules panel. Each environment has a different set of targeting rules. The environment defaults to Staging-Default
, which is a test environment. There is also a production environment called Prod-Default
pre-configured. Environments can be added and configured in the Environments section of the main menu (on the left of the dash).
The first header, Define treatments , is where the treatments are defined. The default treatment simulates a boolean on/off system. Remember, though, that these are just string values and can be given any name and can take on any arbitrary meaning in your application. Further, you can have more than two treatments per split.
Step 10 – Change the on
treatment to v2
and the off
treatment to `v1.
Defining it in this way means that the default rule and the default treatment will be assigned such that v1
is the default. This is what we would want in our test deployment situation. If you scroll down you’ll see that Set the default rule and Set the default treatment both have v1
as their value.
These two defaults are important to understand. The default rule is the treatment that will be served if none of the targeting rules apply. It’s the default value if the treatment request from the Split client works properly, but no case is defined in the targeting rules that applies to the user. The default treatment is what will be served if, for example, the Split client cannot reach the servers or some other fault occurs. It will also be served if the split is killed or the customers are excluded from the split.
Now, you need to define a targeting rule that serves the v2
treatment for the user1
user.
Under Set targeting rules , click Add rule.
You want the rule to read: If user is in list 'user1', serve 'v2'
Click the drop-down and select string and is in list. Add user1
to the text box to the right of the drop-down. Select v2
in the serve drop-down.
Click Save changes at the top of the panel.
Click Confirm.
Your split is configured and active. Now you need to add the Split SDK to the application and add the code that will expose the V2 API using the split. Before you do that, however, you’re going to add some simple authentication to the application to allow for different users.
Add Spring Security 5 Basic Auth to App
Step 11 – In order to have the users to work with, you’re going to add a very simple auth scheme to the API gateway application using Spring Security 5 configured for HTTP basic auth with an in-memory user store.
Open the API gateway project.
Add the Spring Security dependency to the pom.xml
file between the <dependencies></dependencies>
tags.
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>
Create a SecurityConfiguration
class and add the following contents:
src/main/java/com/example/apigateway/SecurityConfiguration.java
`
package com.example.apigateway;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user1").password("{noop}user1").roles("USER")
.and()
.withUser("user2").password("{noop}user2").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
`
This configures Spring Boot to authorize all requests on the resource server and to use HTTP Basic. It also adds our hard-coded, in-memory users: user1
and user2
. Hopefully needless to say, this auth scheme is not ready for production and is for the purposes of this tutorial. However, Spring Security is easily configured for use with OAuth 2.0 and OIDC providers, so adapting this to a live scenario would not be that difficult (but that’s a topic for a different tutorial).
Implement the V2 API and the Split
Step 12 – Still in the API gateway project, add the Split SDK dependency to the pom.xml
file between the <dependencies></dependencies>
tags.
<dependencies>
...
<dependency>
<groupId>io.split.client</groupId>
<artifactId>java-client</artifactId>
<version>4.2.1</version>
</dependency>
...
</dependencies>
Create a new class for to configure the Split client bean that you’ll inject into the controllers.
src/main/java/com/example/apigateway/SplitConfiguration.java
`
package com.example.apigateway;
import io.split.client.SplitClient;
import io.split.client.SplitClientConfig;
import io.split.client.SplitFactory;
import io.split.client.SplitFactoryBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SplitConfiguration {
@Value("#{ @environment['split.api.key'] }")
private String apiKey;
@Bean
public SplitClient splitClient() throws Exception {
SplitClientConfig config = SplitClientConfig.builder()
.setBlockUntilReadyTimeout(20000)
.enableDebug()
.build();
SplitFactory splitFactory = SplitFactoryBuilder.build(apiKey, config);
SplitClient client = splitFactory.client();
client.blockUntilReady();
return client;
}
}
`
This code is more-or-less straight out of the Split Java SDK docs.. You’re pulling your Split API key and building a client bean, setting some default values.
Step 13 – You need to add your Split API key to your application.properties
file. The API keys need to match the environment that you defined your split in. The environment should be Staging-Default
if you followed this tutorial.
To find the API keys, click on the DE icon at the top of the left menu. Click Admin settings. Select API keys.
You want the Staging-Default
and Server-side
keys. Java is a server-side language. Different keys are used for client-side Javascript apps because those keys are essentially public.
Step 14 – Click copy to copy the keys to your clipboard.
In the API gateway properties file, add the following line, replacing {yourSplitApiKey}
with your actual API key.
split.api.key={yourSplitApiKey}
Replace the contents of the main ApiGatewayApplication
class with the following:
src/main/java/com/example/apigateway/ApiGatewayApplication.java
`
package com.example.apigateway;
import io.split.client.SplitClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.server.ResponseStatusException;
import java.security.Principal;
// V1 Feign Client - used to access version 1 API on the weather resource server
@FeignClient(name="weather-service", path = "api/v1", contextId = "v1")
interface WeatherServerClientV1 {
@RequestMapping(method = RequestMethod.GET, value = "weather", consumes = "application/json")
OpenWeatherApiResponse getWeatherByZip(@RequestParam("zip") String zip);
}
// V2 Feign Client - used to access version 2 API on the weather resource server
@FeignClient(name="weather-service", path = "api/v2", contextId = "v2")
interface WeatherServerClientV2 {
@RequestMapping(method = RequestMethod.GET, value = "weather", consumes = "application/json")
OpenWeatherApiResponse getWeatherByZip(@RequestParam("zip") String zip);
@RequestMapping(method = RequestMethod.GET, value = "weather", consumes = "application/json")
OpenWeatherApiResponse getWeatherByCityStateCountry(@RequestParam("cityStateCountry") String cityStateCountry);
}
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@RestController("gateway-api")
@RequestMapping()
public class WeatherGatewayService {
Logger logger = LoggerFactory.getLogger(WeatherGatewayService.class);
private final WeatherServerClientV1 weatherClientV1;
private final WeatherServerClientV2 weatherClientV2;
// the Split Client bean configured in SplitConfiguration.java
private final SplitClient splitClient;
public WeatherGatewayService(WeatherServerClientV1 weatherClientV1, WeatherServerClientV2 weatherClientV2, SplitClient splitClient) {
this.weatherClientV1 = weatherClientV1;
this.weatherClientV2 = weatherClientV2;
this.splitClient = splitClient;
}
// Retrieves the username from the authenticated principal and
// passes this username to the splitClient.getTreatment() method,
// which is where the treatment is retrieved from Split (it's
// actually cached so that this call is really fast but still
// updated in near real-time)
String getTreatmentForPrincipal(Principal principal) {
String username = principal.getName();
logger.info("username = " + username);
String treatment = splitClient.getTreatment(username, "v2-deployment");
logger.info("treatment = " + treatment);
return treatment;
}
@GetMapping("/temperature/zip/{zip}")
String getTempByZip(@PathVariable("zip") String zip, Principal principal) {
String treatment = getTreatmentForPrincipal(principal);
logger.info("Getting weather for zip = " + zip);
OpenWeatherApiResponse weatherApiResponse = null;
// using the treatment to select the correct API version
if (treatment.equals("v1")) {
weatherApiResponse = weatherClientV1.getWeatherByZip(zip);
}
else if (treatment.equals("v2")){
weatherApiResponse = weatherClientV2.getWeatherByZip(zip);
}
else {
throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR);
}
logger.info(weatherApiResponse.toString());
return weatherApiResponse.getMain().getTemp().toString()+"°F, feels like " + weatherApiResponse.getMain().getFeels_like().toString()+"°F";
}
@GetMapping("/temperature/citystatecountry")
String getTempByCityStateCountry(
@RequestParam() String city,
@RequestParam(required = false) String state,
@RequestParam(required = false) String country,
Principal principal) {
String treatment = getTreatmentForPrincipal(principal);
// only the users with treatment "v2" should be able to access this method
if (treatment.equals("v2")) {
logger.info("Getting weather for city = " + city + ", state = " + state + ", country = " + country);
OpenWeatherApiResponse weatherApiResponse = weatherClientV2.getWeatherByCityStateCountry(city+","+state+","+country);
logger.info(weatherApiResponse.toString());
return weatherApiResponse.getMain().getTemp().toString()+"°F, feels like " + weatherApiResponse.getMain().getFeels_like().toString()+"°F";
}
else if (treatment.equals("v1")) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Method not found");
}
else {
throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/weather/zip/{zip}")
OpenWeatherApiResponse getWeatherByZip(@PathVariable("zip") String zip, Principal principal) {
String treatment = getTreatmentForPrincipal(principal);
// only the users with treatment "v2" should be able to access this method
if (treatment.equals("v2")) {
OpenWeatherApiResponse weatherApiResponse = weatherClientV2.getWeatherByZip(zip);
return weatherApiResponse;
}
else if (treatment.equals("v1")) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Method not found");
}
else {
throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
}
`
Step 15 – There’s a lot going on in this file, so I’ll point some things out. At the top of the file, you now have two Feign clients: one for the V1 API and one for the V2 API. Remember that these Feign clients are what structure the calls to the private weather resource service within the simple microservice (whose actual URI is discovered by the Eureka discovery server based on the name
attribute). Notice that I had to set a contextId
on these to allow using the same name for both. This name maps to the service name that is used by the Eureka gateway (defined by the spring.application.name
property in the weather resource server application.properties
file).
If you look at the constructor for the WeatherGatewayService
inner class, you’ll see that the Split client is being injected into the constructor along with the two Feign clients.
The splitClient
is used in the getTreatmentForPrincipal(Principal)
method. This method is called by each of the controller methods. This is where the authenticated username is used to retrieve the treatment for the split. This happens in the splitClient.getTreatment(username, treatment name)
. You can imagine that the split client is making a request to the Split servers here and looking up the correct value. However, that could potentially be very slow. The split client does some nice behind-the-scenes caching and polling. For that reason, it doesn’t really need to make a network call to return the treatment; and as such, the getTreatment()
method returns almost instantly. Treatment values are still updated within seconds of being changed on the Split dashboard.
If you look at the endpoints, you’ll see that the first endpoint (/temperature/zip/{zip}
) has both a V1 and a V2 definition. The treatment value is used to select the correct Feign client. The two new methods (the second and third endpoints, /temperature/citystatecountry
and /weather/zip/{zip}
) only have a V2 definition. As such, they throw a Method not found
exception if a user with the V1 treatment tries to access them. On all three endpoints, an internal service error is thrown if any other treatment value is returned. This may not be the desired behavior in a production scenario. Maybe instead you want to fallback to the V1 value,but that’s up to you.
Stop the API gateway project with control-c
and restart it.
./mvnw spring-boot:run
Make sure the Eureka discovery service and the weather resource service are also still running.
Use HTTPie to make some requests.
Both user1
and user2
should be able to make requests on the original endpoint. You’re using HTTPie to pass the the basic auth credentials (-a user1:user2
);
`
$: http -a user1:user1 :8080/temperature/zip/78025
HTTP/1.1 200
...
62.58°F, feels like 60.84°F
`
`
$: http -a user2:user2 :8080/temperature/zip/78025
HTTP/1.1 200
...
62.58°F, feels like 60.84°F
`
You can verify that an unauthenticated request will fail.
$: http :8080/temperature/zip/78025
HTTP/1.1 401
...
According to our treatment, user1
should be able to access the V2 endpoints while user2
should not.
`
$: http -a user1:user1 :8080/weather/zip/78025
...
{
"main": {
"feels_like": 60.84,
"humidity": 49,
"pressure": 1013,
"temp": 62.58
},
"weather": [
{
"description": "broken clouds",
"main": "Clouds"
}
]
}
`
$: http -a user2:user2 :8080/weather/zip/78025
HTTP/1.1 404
`
If you look at the console output for these requests, you’ll see that user1
is mapping to the v2
treatment and user2
is mapping to the v1
treatment, exactly as expected.
`
2021-12-26 11:40:32.384 INFO 162549 --- [ssionsManager-0] i.s.c.i.ImpressionsManagerImpl : Posting 1 Split impressions took 354 millis
2021-12-26 11:41:17.077 INFO 162549 --- [nio-8080-exec-3] GatewayApplication$WeatherGatewayService : username = user1
2021-12-26 11:41:17.077 INFO 162549 --- [nio-8080-exec-3] GatewayApplication$WeatherGatewayService : treatment = v2
2021-12-26 11:41:20.397 INFO 162549 --- [nio-8080-exec-5] GatewayApplication$WeatherGatewayService : username = user2
2021-12-26 11:41:20.398 INFO 162549 --- [nio-8080-exec-5] GatewayApplication$WeatherGatewayService : treatment = v1
2021-12-26 11:41:20.398 WARN 162549 --- [nio-8080-exec-5] .w.s.m.a.ResponseStatusExceptionResolver : Resolved [org.springframework.web.server.ResponseStatusException: 404 NOT_FOUND "Method not found"]
`
Now, imagine that the V2 deployment performs flawlessly for a few weeks and you’re ready to enable it for all users. That’s easy to do.
Open your Split dashboard. Go to TARGET and select Splits. Select the v2-deployment
split. There are a few ways you could do this. You could add user1
to the list of white-listed users in the targeting rule. However, since you now want V2 to become the standard for all users, you might as well just make it the default rule and default treatment.
Under both Set the default rule and Set the default treatment , select v2
.
At the top of the panel, click Save changes. Click Confirm.
Once you click confirm, your changes are live.
Try making a request on the V2 API with user2
.
`
$: http -a user2:user2 :8080/weather/zip/78025
HTTP/1.1 200
...
{
"main": {
"feels_like": 60.89,
"humidity": 50,
"pressure": 1012,
"temp": 62.58
},
"weather": [
{
"description": "broken clouds",
"main": "Clouds"
}
]
}
`
This request just previously failed with a 404 for user2
, but because now all users have been authorized to use the V2 API, it works. This was done without changing any code or making any deployments.
You could easily do the opposite, if you wanted. Imagine that the deployment didn’t work (this, of course, would never happen to you, but just for the sake of completeness). In that case, you would want to instead have all users hit the V1 API. To do that, you would delete the targeting rule (under Set targeting rules ) and change both the Set the default rule and Set the default treatment back to V1
. Save changes and Confirm.
You’re back to getting a 404
for user2
for the V2 API endpoints, but not even user1
will get an error as well.
`
$: http -a user2:user2 :8080/weather/zip/78025
HTTP/1.1 404
...
`
`
$: http -a user1:user1 :8080/weather/zip/78025
HTTP/1.1 404
...
`
Both users, however, can still access the V1 endpoint.
`
$: http -a user1:user1 :8080/temperature/zip/78025
HTTP/1.1 200
...
66.18°F, feels like 64.71°F
`
`
$: http -a user1:user1 :8080/temperature/zip/78025
HTTP/1.1 200
...
66.18°F, feels like 64.71°F
`
Split Data Logging
Step 16 – The last thing I want to demonstrate is Split’s ability to track events logged through the Split client. The Split client has a track()
method that can send arbitrary event data to the Split servers where it is logged. You can read more about this in the Split SDK docs. This is a deep feature that includes the ability to calculate metrics based on tracked events and to trigger alerts. This tutorial won’t go too deep into this ability, except to quickly demonstrate the track()
method and introduce you to the API.
Open the ApiGatewayApplication.java
file and add the following line to the getWeatherByZip()
method as shown below.
`
splitClient.track(principal.getName(), "user", "json-access");
`
`
@GetMapping("/weather/zip/{zip}")
OpenWeatherApiResponse getWeatherByZip(@PathVariable("zip") String zip, Principal principal) {
String treatment = getTreatmentForPrincipal(principal);
// only the users with treatment "v2" should be able to access this method
if (treatment.equals("v2")) {
///////////////////////////////////////////////////////////////////////////
splitClient.track(principal.getName(), "user", "json-access", zip); // <- ADD ME
///////////////////////////////////////////////////////////////////////////
OpenWeatherApiResponse weatherApiResponse = weatherClientV2.getWeatherByZip(zip);
return weatherApiResponse;
}
else if (treatment.equals("v1")) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Method not found");
}
else {
throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
`
This will log an event with Split every time a user uses the V2 API to access the JSON data. Useful, perhaps, if you wanted to know how many users were using the data API endpoint added in the V2 app.
The basic signature of the track()
method is as follows.
`
boolean trackEvent = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE", VALUE);
`
The method you’re adding above does not log a value. However, numerical values (a Java double
) can be logged with each event. You could, for example, track load times or method performance to see if a new release has slowed user access times.
Step 17 – To see this in action, open the Split dashboard. Click on Data hub from the main menu. Make sure the Staging-Default
environment is selected. On the Data type drop-down, select Events
. Click the blue Query button. Keep this browser tab open.
This allows you to monitor in real-time the entries created by the splitClient.track()
method calls.
Use control-c
to stop and restart your API gateway app with ./mvnw spring-boot:run
.
Make a few calls to the tracked method.
`
http -a user1:user1 :8080/weather/zip/78025;
http -a user1:user1 :8080/weather/zip/97229;
http -a user1:user1 :8080/weather/zip/77001;
`
It may take a few seconds, but you’ll see the track()
calls logged.
I won’t demonstrate it here, but if you change the Data type to Impressions
, you can track the splitClient.getTreatment()
calls as well. Watching all of this come in real-time is fun, but the real power is in the metrics. To learn more about Split event monitoring and metrics, dig into the Split docs.
Learn More About Spring Boot
In this tutorial, you covered a lot of ground. You saw how to use Spring Cloud to create a scalable, dynamic microservice network that uses a Eureka discovery server to register and lookup services; that implements a private web service that calls outside APIs and re-packages the data for downstream services; and that has a public API gateway that exposes the service to external clients.
You saw how to use Feign to create structured HTTP clients. You also saw how to implement HTTP Basic auth to secure the app using Spring Security 5, and how Split’s implementation of feature flags allows for application behavior to be dynamically changed at runtime. Finally, you had a quick introduction to Split’s event tracking API.
Ready to learn more? We’ve got some additional resources covering all of these topics and more! Check out:
Containerization with Spring Boot and Docker
A Simple Guide to Reactive Java with Spring Webflux
Get Started with Spring Boot and Vue.js
To stay up to date on all things in feature flagging and app building, follow us on Twitter @splitsoftware, and subscribe to our YouTube channel!
Top comments (0)