How to build a Microservice Architecture with Spring Boot and Kubernetes?

Build Microservice Architecture With Kubernetes, Spring Boot , and Docker

Build Microservice Architecture With Kubernetes, Spring Boot , and Docker

In this article we learn how to start the Spring Boot microservice project and run it fast with Kubernetes and Docker

The topics covered in this article are:

  • Using Spring Boot 2.0 in cloud-native development

  • Providing service discovery for all microservices using a Spring Cloud Kubernetes project

  • Injecting configuration settings into application pods using Kubernetes Config Maps and Secrets

  • Building application images using Docker and deploying them on Kubernetes using YAML configuration files

  • Using Spring Cloud Kubernetes together with a Zuul proxy to expose a single Swagger API documentation for all microservices

Spring Cloud and Kubernetes may be threatened as competitive solutions when you build a microservices environment. Such components like Eureka, Spring Cloud Config, or Zuul provided by Spring Cloud may be replaced by built-in Kubernetes objects like services, config maps, secrets, or ingresses. But even if you decide to use Kubernetes components instead of Spring Cloud, you can take advantage of some interesting features provided throughout the whole Spring Cloud project.

The one really interesting project that helps us in development is Spring Cloud Kubernetes. Although it is still in the incubation stage, it is definitely worth dedicating some time to it. It integrates Spring Cloud with Kubernetes. I'll show you how to use an implementation of the discovery client, inter-service communication with the Ribbon client, and Zipkin discovery using Spring Cloud Kubernetes.

Before we proceed to the source code, let's take a look at the following diagram. It illustrates the architecture of our sample system. It is quite similar to the architecture presented in the mentioned article about microservices on Spring Cloud. There are three independent applications (employee-service, department-service, organization-service), which communicate with each other through a REST API. These Spring Boot microservices use some built-in mechanisms provided by Kubernetes: config maps and secrets for distributed configuration, etcd for service discovery, and ingresses for the API gateway.

Let's proceed to the implementation. Currently, the newest stable version of Spring Cloud is Finchley.RELEASE. This version of spring-cloud-dependencies should be declared as a BOM for dependency management.

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Spring Cloud Kubernetes is not released under Spring Cloud Release Trains, so we need to explicitly define its version. Because we use Spring Boot 2.0 we have to include the newest SNAPSHOT version of spring-cloud-kubernetes artifacts, which is 0.3.0.BUILD-SNAPSHOT.

The source code of sample applications presented in this article is available on GitHub in this repository.

Pre-Requirements

In order to be able to deploy and test our sample microservices, we need to prepare a development environment. We can realize that in the following steps:

  • You need at least a single node cluster instance of Kubernetes (Minikube) or Openshift (Minishift) running on your local machine. You should start it and expose the embedded Docker client provided by both of them. The detailed instructions for Minishift may be found in my Quick guide to deploying Java apps on OpenShift. You can also use that description to run Minikube — just replace word "minishift" with "minikube." In fact, it does not matter if you choose Kubernetes or Openshift — the next part of this tutorial will be applicable for both of them.

  • Spring Cloud Kubernetes requires access to the Kubernetes API in order to be able to retrieve a list of addresses for pods running for a single service. If you use Kubernetes, you should just execute the following command:

$ kubectl create clusterrolebinding admin --clusterrole=cluster-admin --serviceaccount=default:default

If you deploy your microservices on Minishift, you should first enable admin-user add-on, then log in as a cluster admin and grant the required permissions.

$ minishift addons enable admin-user
$ oc login -u system:admin
$ oc policy add-role-to-user cluster-reader system:serviceaccount:myproject:default
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mongodb
  labels:
    app: mongodb
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
      - name: mongodb
        image: mongo:latest
        ports:
        - containerPort: 27017
        env:
        - name: MONGO_INITDB_DATABASE
          valueFrom:
            configMapKeyRef:
              name: mongodb
              key: database-name
        - name: MONGO_INITDB_ROOT_USERNAME
          valueFrom:
            secretKeyRef:
              name: mongodb
              key: database-user
        - name: MONGO_INITDB_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mongodb
              key: database-password
---
apiVersion: v1
kind: Service
metadata:
  name: mongodb
  labels:
    app: mongodb
spec:
  ports:
  - port: 27017
    protocol: TCP
  selector:
    app: mongodb
1. Inject the Configuration With Config Maps and Secrets

When using Spring Cloud, the most obvious choice for realizing a distributed configuration in your system is Spring Cloud Config. With Kubernetes, you can use Config Map. It holds key-value pairs of configuration data that can be consumed in pods or used to store configuration data. It is used for storing and sharing non-sensitive, unencrypted configuration information. To use sensitive information in your clusters, you must use Secrets. Use of both these Kubernetes objects can be perfectly demonstrated based on the example of MongoDB connection settings. Inside a Spring Boot application, we can easily inject it using environment variables. Here's a fragment of application.yml file with URI configuration.

spring:
  data:
    mongodb:
      uri: mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb/${MONGO_DATABASE}

While username and password are sensitive fields, a database name is not, so we can place it inside the config map.

apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb
data:
  database-name: microservices

Of course, username and password are defined as secrets.

apiVersion: v1
kind: Secret
metadata:
  name: mongodb
type: Opaque
data:
  database-password: MTIzNDU2
  database-user: cGlvdHI=

To apply the configuration to the Kubernetes cluster, we run the following commands.

$ kubectl apply -f kubernetes/mongodb-configmap.yaml
$ kubectl apply -f kubernetes/mongodb-secret.yaml

After that, we should inject the configuration properties into the application's pods. When defining the container configuration inside the Deployment YAML file, we have to include references to environment variables and secrets, as shown below.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: employee
  labels:
    app: employee
spec:
  replicas: 1
  selector:
    matchLabels:
      app: employee
  template:
    metadata:
      labels:
        app: employee
    spec:
      containers:
      - name: employee
        image: piomin/employee:1.0
        ports:
        - containerPort: 8080
        env:
        - name: MONGO_DATABASE
          valueFrom:
            configMapKeyRef:
              name: mongodb
              key: database-name
        - name: MONGO_USERNAME
          valueFrom:
            secretKeyRef:
              name: mongodb
              key: database-user
        - name: MONGO_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mongodb
              key: database-password
2. Building Service Discovery With Kubernetes

We are usually running microservices on Kubernetes using Docker containers. One or more containers are grouped by pods, which are the smallest deployable units created and managed in Kubernetes. A good practice is to run only one container inside a single pod. If you would like to scale up your microservice, you would just have to increase the number of running pods. All running pods that belong to a single microservice are logically grouped with Kubernetes Service. This service may be visible outside the cluster and is able to load balance incoming requests between all running pods. The following service definition groups all pods labeled with the field app equal to employee.

apiVersion: v1
kind: Service
metadata:
  name: employee
  labels:
    app: employee
spec:
  ports:
  - port: 8080
    protocol: TCP
  selector:
    app: employee

Service can be used to access the application outsidethe Kubernetes cluster or for inter-service communication inside a cluster. However, the communication between microservices can be implemented more comfortably with Spring Cloud Kubernetes. First, we need to include the following dependency in the project pom.xml.

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes</artifactId>
<version>0.3.0.BUILD-SNAPSHOT</version>
</dependency>

Then we should enable the discovery client for an application, the same as we have always done for discovery in Spring Cloud Netflix Eureka. This allows you to query Kubernetes endpoints (services) by name. This discovery feature is also used by Spring Cloud Kubernetes Ribbon or Zipkin projects to fetch, respectively, the list of the pods defined for a microservice to be load balanced or the Zipkin servers available to send the traces or spans.

@SpringBootApplication
@EnableDiscoveryClient
@EnableMongoRepositories
@EnableSwagger2
public class EmployeeApplication {
 public static void main(String[] args) {
  SpringApplication.run(EmployeeApplication.class, args);
 }
 // ...
}

The last important thing in this section is to guarantee that the Spring application name will be exactly the same as the Kubernetes service name for the application. For the application employee-service, it is employee.

spring:
  application:
    name: employee
3. Building Microservices Using Docker and Deploying on Kubernetes

There is nothing unusual in our sample microservices. We have included some standard Spring dependencies for building REST-based microservices, integrating with MongoDB, and generating API documentation using Swagger2.

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

In order to integrate with MongoDB, we should create an interface that extends standard Spring Data CrudRepository.

public interface EmployeeRepository extends CrudRepository {
 List findByDepartmentId(Long departmentId);
 List findByOrganizationId(Long organizationId);
}

The entity class should be annotated with Mongo @Document and a primary key field with @Id.

@Document(collection = "employee")
public class Employee {
 @Id
 private String id;
 private Long organizationId;
 private Long departmentId;
 private String name;
 private int age;
 private String position;
 // ...
}

The repository bean has been injected to the controller class. Here's the full implementation of our REST API inside employee-service.

@RestController
public class EmployeeController {
 private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeController.class);
 @Autowired
 EmployeeRepository repository;
 @PostMapping("/")
 public Employee add(@RequestBody Employee employee) {
  LOGGER.info("Employee add: {}", employee);
  return repository.save(employee);
 }
 @GetMapping("/{id}")
 public Employee findById(@PathVariable("id") String id) {
  LOGGER.info("Employee find: id={}", id);
  return repository.findById(id).get();
 }
 @GetMapping("/")
 public Iterable findAll() {
  LOGGER.info("Employee find");
  return repository.findAll();
 }
 @GetMapping("/department/{departmentId}")
 public List findByDepartment(@PathVariable("departmentId") Long departmentId) {
  LOGGER.info("Employee find: departmentId={}", departmentId);
  return repository.findByDepartmentId(departmentId);
 }
 @GetMapping("/organization/{organizationId}")
 public List findByOrganization(@PathVariable("organizationId") Long organizationId) {
  LOGGER.info("Employee find: organizationId={}", organizationId);
  return repository.findByOrganizationId(organizationId);
 }
}

In order to run our microservices on Kubernetes, we should first build the whole Maven project with the mvn clean install command. Each microservice has a Dockerfile placed in the root directory. Here's the Dockerfile definition for employee-service.

FROM openjdk:8-jre-alpine
ENV APP_FILE employee-service-1.0-SNAPSHOT.jar
ENV APP_HOME /usr/apps
EXPOSE 8080
COPY target/$APP_FILE $APP_HOME/
WORKDIR $APP_HOME
ENTRYPOINT ["sh", "-c"]
CMD ["exec java -jar $APP_FILE"]

Let's build Docker images for all three sample microservices.

$ cd employee-service
$ docker build -t piomin/employee:1.0 .
$ cd department-service
$ docker build -t piomin/department:1.0 .
$ cd organization-service
$ docker build -t piomin/organization:1.0 .

The last step is to deploy Docker containers with applications on Kubernetes. To do that, just execute the commands kubectl apply on YAML configuration files. The sample deployment file for employee-service has been demonstrated in step 1. All required deployment fields are available inside the project repository in the kubernetes directory.

$ kubectl apply -f kubernetes\employee-deployment.yaml
$ kubectl apply -f kubernetes\department-deployment.yaml
$ kubectl apply -f kubernetes\organization-deployment.yaml
4. Communication Between Microservices With Spring Cloud Kubernetes Ribbon

All the microservices are deployed on Kubernetes. Now, it's worth it to discuss some aspects related to inter-service communication. The application employee-service, in contrast to other microservices, did not invoke any other microservices. Let's take a look at other microservices that call the API exposed by employee-service and communicate between each other ( organization-service calls department-service API).

First, we need to include some additional dependencies in the project. We use Spring Cloud Ribbon and OpenFeign. Alternatively, you can also use Spring@LoadBalancedRestTemplate.

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
<version>0.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Here's the main class of department-service. It enables Feign client using the @EnableFeignClients annotation. It works the same as with discovery based on Spring Cloud Netflix Eureka. OpenFeign uses Ribbon for client-side load balancing. Spring Cloud Kubernetes Ribbon provides some beans that force Ribbon to communicate with the Kubernetes API through Fabric8 KubernetesClient.

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableMongoRepositories
@EnableSwagger2
public class DepartmentApplication {
 public static void main(String[] args) {
  SpringApplication.run(DepartmentApplication.class, args);
 }
 // ...
}

Here's the implementation of Feign client for calling the method exposed by employee-service.

@FeignClient(name = "employee")
public interface EmployeeClient {
 @GetMapping("/department/{departmentId}")
 List findByDepartment(@PathVariable("departmentId") String departmentId);
}

Finally, we have to inject Feign client's beans into the REST controller. Now, we may call the method defined inside EmployeeClient, which is equivalent to calling REST endpoints.

@RestController
public class DepartmentController {
 private static final Logger LOGGER = LoggerFactory.getLogger(DepartmentController.class);
 @Autowired
 DepartmentRepository repository;
 @Autowired
 EmployeeClient employeeClient;
 // ...
 @GetMapping("/organization/{organizationId}/with-employees")
 public List findByOrganizationWithEmployees(@PathVariable("organizationId") Long organizationId) {
  LOGGER.info("Department find: organizationId={}", organizationId);
  List departments = repository.findByOrganizationId(organizationId);
  departments.forEach(d -> d.setEmployees(employeeClient.findByDepartment(d.getId())));
  return departments;
 }
}
5. Building API Gateway Using Kubernetes Ingress

Ingress is a collection of rules that allow incoming requests to reach the downstream services. In our microservices architecture, ingress is playing the role of an API gateway. To create it, we should first prepare a YAML description file. The descriptor file should contain the hostname under which the gateway will be available and mapping rules to the downstream services.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: gateway-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  backend:
    serviceName: default-http-backend
    servicePort: 80
  rules:
  - host: microservices.info
    http:
      paths:
      - path: /employee
        backend:
          serviceName: employee
          servicePort: 8080
      - path: /department
        backend:
          serviceName: department
          servicePort: 8080
      - path: /organization
        backend:
          serviceName: organization
          servicePort: 8080

You have to execute the following command to apply the configuration above to the Kubernetes cluster.

$ kubectl apply -f kubernetes\ingress.yaml

To test this solution locally, we have to insert the mapping between the IP address and hostname set in the ingress definition inside the hosts file, as shown below. After that, we can test services through ingress using defined hostname just like that: http://microservices.info/employee.

192.168.99.100 microservices.info

You can check the details of the created ingress just by executing the command kubectl describe ing gateway-ingress.

6. Enabling API Specification on the Gateway Using Swagger2

What if we would like to expose a single Swagger documentation for all microservices deployed on Kubernetes? Well, here things are getting complicated... We can run a container with Swagger UI, and map all paths exposed by the ingress manually, but it is not a good solution...

In that case, we can use Spring Cloud Kubernetes Ribbon one more time, this time together with Spring Cloud Netflix Zuul. Zuul will act as a gateway only for serving the Swagger API.
Here's the list of dependencies used in my gateway-service project.

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes</artifactId>
<version>0.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
<version>0.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

Kubernetes discovery client will detect all services exposed on the cluster. We would like to display documentation only for our three microservices. That's why I defined the following routes for Zuul.

zuul:
  routes:
    department:
      path: /department/**
    employee:
      path: /employee/**
    organization:
      path: /organization/**

Now we can use the ZuulProperties bean to get the routes' addresses from Kubernetes discovery and configure them as Swagger resources, as shown below.

@Configuration
public class GatewayApi {
 @Autowired
 ZuulProperties properties;
 @Primary
 @Bean
 public SwaggerResourcesProvider swaggerResourcesProvider() {
  return () -> {
   List resources = new ArrayList();
   properties.getRoutes().values().stream()
   .forEach(route -> resources.add(createResource(route.getId(), "2.0")));
   return resources;
  };
 }
 private SwaggerResource createResource(String location, String version) {
  SwaggerResource swaggerResource = new SwaggerResource();
  swaggerResource.setName(location);
  swaggerResource.setLocation("/" + location + "/v2/api-docs");
  swaggerResource.setSwaggerVersion(version);
  return swaggerResource;
 }
}

The application gateway-service should be deployed on the cluster the same as the other applications. You can see the list of running services by executing the command kubectl get svc. Swagger documentation is available under the address http://192.168.99.100:31237/swagger-ui.html.

Learn More

Thanks for reading !

Originally published by Piotr Mińkowski at dzone.com

Build a reactive Microservices Architecture using Spring Cloud Gateway, Spring Boot and Spring WebFlux

Build a reactive Microservices Architecture using Spring Cloud Gateway, Spring Boot and Spring WebFlux

In this article, you'll learn how you can build a reactive microservices architecture using Spring Cloud Gateway, Spring Boot, and Spring WebFlux.

Originally published by Matt Raible at https://developer.okta.com

So you wanna go full reactive, eh? Great! Reactive programming is an increasingly popular way to make your applications more efficient. Instead of making a call to a resource and waiting on a response, reactive applications asynchronously receive a response. This allows them to free up processing power, only perform processing when necessary, and scale more effectively than other systems.

The Java ecosystem has its fair share of reactive frameworks, including Play Framework, Ratpack, Vert.x, and Spring WebFlux. Like Reactive programming, a microservices architecture can help large teams scale quickly and is possible to build using any of the awesome aforementioned frameworks.

Today I’d like to show you how you can build a reactive microservices architecture using Spring Cloud Gateway, Spring Boot, and Spring WebFlux. We’ll leverage Spring Cloud Gateway as API gateways are often important components in a cloud-native microservices architecture, providing the aggregation layer for all your backend microservices.

This tutorial shows you how to build a microservice with a REST API that returns a list of new cars. You’ll use Eureka for service discovery and Spring Cloud Gateway to route requests to the microservice. Then you’ll integrate Spring Security so only authenticated users can access your API gateway and microservice.

Prerequisites: HTTPie (or cURL), Java 11+, and an internet connection.

Spring Cloud Gateway vs. Zuul

Zuul is Netflix’s API gateway. First released in 2013, Zuul was not originally reactive, but Zuul 2 is a ground-up rewrite to make it reactive. Unfortunately, Spring Cloud does not support Zuul 2 and it likely never will.

Spring Cloud Gateway is now the preferred API gateway implementation from the Spring Cloud Team. It’s built on Spring 5, Reactor, and Spring WebFlux. Not only that, it also includes circuit breaker integration, service discovery with Eureka, and is much easier to integrate with OAuth 2.0!

Let’s dig in.

Create a Spring Cloud Eureka Server Project

Start by creating a directory to hold all your projects, for example, spring-cloud-gateway. Navigate to it in a terminal window and create a discovery-service project that includes Spring Cloud Eureka Server as a dependency.

http https://start.spring.io/starter.zip javaVersion==11 artifactId==discovery-service \  name==eureka-service baseDir==discovery-service \  dependencies==cloud-eureka-server | tar -xzvf -
The command above uses HTTPie. I highly recommend installing it. You can also use curl. Run curl https://start.spring.io to see the syntax.

Add @EnableEurekaServer on its main class to enable it as a Eureka server.

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer

@SpringBootApplication

public class EurekaServiceApplication {...}

Add the following properties to the project’s src/main/resources/application.properties file to configure its port and turn off Eureka registration.

server.port=8761
eureka.client.register-with-eureka=false

To make the discovery-service run on Java 11+, add a dependency on JAXB.

<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
</dependency>

Start the project using ./mvnw spring-boot:run or by running it in your IDE.

Create a Spring Cloud Gateway Project

Next, create an api-gateway project that includes a handful of Spring Cloud dependencies.

http https://start.spring.io/starter.zip javaVersion==11 artifactId==api-gateway 
 name==api-gateway baseDir==api-gateway
 dependencies==actuator,cloud-eureka,cloud-feign,cloud-gateway,cloud-hystrix,webflux,lombok | tar -xzvf -

We’ll come back to configuring this project in a minute.

Create a Reactive Microservice with Spring WebFlux

The car microservice will contain a significant portion of this example’s code because it contains a fully-functional REST API that supports CRUD (Create, Read, Update, and Delete).

Create the car-service project using start.spring.io:

http https://start.spring.io/starter.zip javaVersion==11 artifactId==car-service 
 name==car-service baseDir==car-service
 dependencies==actuator,cloud-eureka,webflux,data-mongodb-reactive,flapdoodle-mongo,lombok | tar -xzvf -

The dependencies argument is interesting in this command. You can see that Spring WebFlux is included, as is MongoDB. Spring Data provides reactive drivers for Redis and Cassandra as well.

You may also be interested in R2DBC (Reactive Relational Database Connectivity) - an endeavor to bring a reactive programming API to SQL databases. I did not use it in this example because it’s not yet available on start.spring.io.

Build a REST API with Spring WebFlux

I’m a big fan of VWs, especially classic ones like the bus and the bug. Did you know that VW has a bunch of electric vehicles coming out in the next few years? I’m really excited by the ID Buzz! It has classic curves and is all-electric. It even has 350+ horsepower!

In case you’re not familiar with the ID Buzz, here’s a photo from Volkswagen.

Let’s have some fun with this API example and use the electric VWs for our data set. This API will track the various car names and release dates.

Add Eureka registration, sample data initialization, and a reactive REST API to src/main/java/…​/CarServiceApplication.java:

package com.example.carservice;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.LocalDate;
import java.time.Month;
import java.util.Set;
import java.util.UUID;

@EnableEurekaClient
@SpringBootApplication
@Slf4j
public class CarServiceApplication {

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

   @Bean
   ApplicationRunner init(CarRepository repository) {
       // Electric VWs from
https://www.vw.com/electric-concepts/
       // Release dates from
https://www.motor1.com/features/346407/volkswagen-id-price-on-sale/
       Car ID = new Car(UUID.randomUUID(), "ID.", LocalDate.of(2019, Month.DECEMBER, 1));
       Car ID_CROZZ = new Car(UUID.randomUUID(), "ID. CROZZ", LocalDate.of(2021, Month.MAY, 1));
       Car ID_VIZZION = new Car(UUID.randomUUID(), "ID. VIZZION", LocalDate.of(2021, Month.DECEMBER, 1));
       Car ID_BUZZ = new Car(UUID.randomUUID(), "ID. BUZZ", LocalDate.of(2021, Month.DECEMBER, 1));
       Set<Car> vwConcepts = Set.of(ID, ID_BUZZ, ID_CROZZ, ID_VIZZION);

       return args -> {
           repository
                   .deleteAll()
                   .thenMany(
                           Flux
                                   .just(vwConcepts)
                                   .flatMap(repository::saveAll)
                   )
                   .thenMany(repository.findAll())
                   .subscribe(car -> log.info("saving " + car.toString()));
       };
   }
}

@Document
@Data
@NoArgsConstructor
@AllArgsConstructor
class Car {
   @Id
   private UUID id;
   private String name;
   private LocalDate releaseDate;
}

interface CarRepository extends ReactiveMongoRepository<Car, UUID> {
}

@RestController
class CarController {

   private CarRepository carRepository;

   public CarController(CarRepository carRepository) {
       this.carRepository = carRepository;
   }

   @PostMapping("/cars")
   @ResponseStatus(HttpStatus.CREATED)
   public Mono<Car> addCar(@RequestBody Car car) {
       return carRepository.save(car);
   }

   @GetMapping("/cars")
   public Flux<Car> getCars() {
       return carRepository.findAll();
   }

   @DeleteMapping("/cars/{id}")
   public Mono<ResponseEntity<Void>> deleteCar(@PathVariable("id") UUID id) {
       return carRepository.findById(id)
               .flatMap(car -> carRepository.delete(car)
                       .then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK)))
               )
               .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
   }
}

  1. Add the @EnableEurekaClient annotation for service discovery
  2. @Slf4j is a handy annotation from Lombok to enable logging in a class
  3. ApplicationRunner bean to populate MongoDB with default data
  4. Delete all existing data in MongoDB so new data is not additive
  5. Subscribe to results so both deleteAll() and saveAll() are invoked
  6. Car class with Spring Data NoSQL and Lombok annotations to reduce boilerplate
  7. CarRepository interface that extends ReactiveMongoRepository, giving you CRUDability with hardly any code!
  8. CarController class that uses CarRepository to perform CRUD actions
  9. Spring WebFlux returns a Mono publisher for single objects
  10. Return a Flex publisher for multiple objects

You’ll also need to modify the car-service project’s application.properties to set its name and port.

spring.application.name=car-service
server.port=8081

Run MongoDB

The easiest way to run MongoDB is to remove the test scope from the flapdoodle dependency in car-service/pom.xml. This will cause your app to start an embedded MongoDB dependency.

<dependency>
    <groupId>de.flapdoodle.embed</groupId>
    <artifactId>de.flapdoodle.embed.mongo</artifactId>
    <!--<scope>test</scope>-->
</dependency>

You can also install and run MongoDB using Homebrew.

brew tap mongodb/brew
brew install [email protected]
mongod

Or, use Docker:

docker run -d -it -p 27017:27017 mongo

Stream Data with WebFlux

This completes everything you need to do to build a REST API with Spring WebFlux.

"But wait!" you might say. "I thought WebFlux was all about streaming data?"

In this particular example, you can still stream data from the /cars endpoint, but not in a browser.

A browser has no way to consume a stream other than using Server-Sent Events or WebSockets. Non-browser clients however can get a JSON stream by sending an Accept header with a value of application/stream+json .

You could test everything works at this point by firing up your browser and using HTTPie to make requests. However, it’s much better to write automated tests!

Test Your WebFlux API with WebTestClient

WebClient ships as part of Spring WebFlux and can be useful for making reactive requests, receiving responses, and populating objects with the payload. A companion class, WebTestClient, can be used to test your WebFlux API. It contains request methods that are similar to WebClient, as well as methods to check the response body, status, and headers.

Modify the src/test/java/…​/CarServiceApplicationTests.java class in the car-service project to contain the code below.

package com.example.carservice;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;

import java.time.LocalDate;
import java.time.Month;
import java.util.Collections;
import java.util.UUID;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = {"spring.cloud.discovery.enabled = false"})
public class CarServiceApplicationTests {

    @Autowired
    CarRepository carRepository;

    @Autowired
    WebTestClient webTestClient;

    @Test
    public void testAddCar() {
        Car buggy = new Car(UUID.randomUUID(), "ID. BUGGY", LocalDate.of(2022, Month.DECEMBER, 1));

        webTestClient.post().uri("/cars")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .body(Mono.just(buggy), Car.class)
                .exchange()
                .expectStatus().isCreated()
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBody()
                .jsonPath("$.id").isNotEmpty()
                .jsonPath("$.name").isEqualTo("ID. BUGGY");
    }

    @Test
    public void testGetAllCars() {
        webTestClient.get().uri("/cars")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBodyList(Car.class);
    }

    @Test
    public void testDeleteCar() {
        Car buzzCargo = carRepository.save(new Car(UUID.randomUUID(), "ID. BUZZ CARGO",
                LocalDate.of(2022, Month.DECEMBER, 2))).block();

        webTestClient.delete()
                .uri("/cars/{id}", Collections.singletonMap("id", buzzCargo.getId()))
                .exchange()
                .expectStatus().isOk();
    }
}

To prove it works, run ./mvnw test. Give yourself a pat on the back when your tests pass!

If you’re on Windows, use mvnw test.

Use Spring Cloud Gateway with Reactive Microservices

To edit all three projects in the same IDE window, I find it useful to create an aggregator pom.xml. Create a pom.xml file in the parent directory of your projects and copy the XML below into it.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.okta.developer</groupId>
    <artifactId>reactive-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>reactive-parent</name>
    <modules>
        <module>discovery-service</module>
        <module>car-service</module>
        <module>api-gateway</module>
    </modules>
</project>

After creating this file, you should be able to open it in your IDE as a project and navigate between projects easily.

In the api-gateway project, add @EnableEurekaClient to the main class to make it Eureka-aware.

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableEurekaClient
@SpringBootApplication
public class ApiGatewayApplication {...}

Then, modify the src/main/resources/application.properties file to configure the application name.

spring.application.name=gateway

Create a RouteLocator bean in ApiGatewayApplication to configure routes. You can configure Spring Cloud Gateway with YAML, but I prefer Java.

package com.example.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

@EnableEurekaClient
@SpringBootApplication
public class ApiGatewayApplication {

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

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("car-service", r -> r.path("/cars")
                        .uri("lb://car-service"))
                .build();
    }
}

After making these code changes, you should be able to start all three Spring Boot apps and hit http://localhost:8080/cars.

$ http :8080/cars
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
transfer-encoding: chunked

[
    {
        "id": "ff48f617-6cba-477c-8e8f-2fc95be96416",
        "name": "ID. CROZZ",
        "releaseDate": "2021-05-01"
    },
    {
        "id": "dd6c3c32-724c-4511-a02c-3348b226160a",
        "name": "ID. BUZZ",
        "releaseDate": "2021-12-01"
    },
    {
        "id": "97cfc577-d66e-4a3c-bc40-e78c3aab7261",
        "name": "ID.",
        "releaseDate": "2019-12-01"
    },
    {
        "id": "477632c8-2206-4f72-b1a8-e982e6128ab4",
        "name": "ID. VIZZION",
        "releaseDate": "2021-12-01"
    }
]

Add a REST API to Retrieve Your Favorite Cars

Create a /fave-cars endpoint that strips out cars that aren’t your favorite.

First, add a load-balanced WebClient.Builder bean.

@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
    return WebClient.builder();
}

Then add a Car POJO and a FaveCarsController below the ApiGatewayApplication class in the same file.

public class ApiGatewayApplication {...}
class Car {...}
class FaveCarsController {...}

Use WebClient to retrieve the cars and filter out the ones you don’t love.

@Data
class Car {
    private String name;
    private LocalDate releaseDate;
}

@RestController
class FaveCarsController {

    private final WebClient.Builder carClient;

    public FaveCarsController(WebClient.Builder carClient) {
        this.carClient = carClient;
    }

    @GetMapping("/fave-cars")
    public Flux<Car> faveCars() {
        return carClient.build().get().uri("lb://car-service/cars")
                .retrieve().bodyToFlux(Car.class)
                .filter(this::isFavorite);
    }

    private boolean isFavorite(Car car) {
        return car.getName().equals("ID. BUZZ");
    }
}

If you’re not using an IDE that auto-imports for you, you’ll want to copy/paste the following into the top of ApiGatewayApplication.java:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

Restart your gateway app to see the http://localhost:8080/fave-cars endpoint only returns the ID Buzz.

What about Failover with Hystrix?

Spring Cloud Gateway only supports Hystrix at the time of this writing. Spring Cloud deprecated direct support for Hystrix in favor of Spring Cloud Circuit Breaker. Unfortunately, this library hasn’t had a GA release yet, so I decided not to use it.

To use Hystrix with Spring Cloud Gateway, you can add a filter to your car-service route, like so:

.route("car-service", r -> r.path("/cars")
        .filters(f -> f.hystrix(c -> c.setName("carsFallback")
                .setFallbackUri("forward:/cars-fallback")))
        .uri("lb://car-service/cars"))
.build();

Then create a CarsFallback controller to handle the /cars-fallback route.

@RestController
class CarsFallback {

    @GetMapping("/cars-fallback")
    public Flux<Car> noCars() {
        return Flux.empty();
    }
}

First, restart your gateway and confirm http://localhost:8080/cars works. Then shut down the car service, try again, and you’ll see it now returns an empty array. Restart the car service and you’ll see the list populated again.

You’ve built a resilient and reactive microservices architecture with Spring Cloud Gateway and Spring WebFlux. Now let’s see how to secure it!

What about Feign with Spring Cloud Gateway?

If you’d like to use Feign in a WebFlux app, see the feign-reactive project. I did not have a need for Feign in this particular example.

Secure Spring Cloud Gateway with OAuth 2.0

OAuth 2.0 is an authorization framework for delegated access to APIs. OIDC (or OpenID Connect) is a thin layer on top of OAuth 2.0 that provides authentication. Spring Security has excellent support for both frameworks and so does Okta!

You can use OAuth 2.0 and OIDC without a cloud identity provider by building your own server or by using an open-source implementation. However, wouldn’t you rather just use something that’s always on, like Okta?

If you already have an Okta account, see the Create a Web Application in Okta sidebar below. Otherwise, we created a Maven plugin that configures a free Okta developer account + an OIDC app (in under a minute!).

To use it, add the following plugin repository to your gateway project’s pom.xml:

<pluginRepositories>
    <pluginRepository>
        <id>ossrh</id>
        <releases><enabled>false</enabled></releases>
        <snapshots><enabled>true</enabled></snapshots>
        <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    </pluginRepository>
</pluginRepositories>

Then run ./mvnw com.okta:okta-maven-plugin:setup to create an account and configure your Spring Boot app to work with Okta.

Create a Web Application in Okta

Log in to your Okta Developer account (or sign up if you don’t have an account).

  1. From the Applications page, choose Add Application.
  2. On the Create New Application page, select Web.
  3. Give your app a memorable name, add http://localhost:8080/login/oauth2/code/okta as a Login redirect URI, select Refresh Token (in addition to Authorization Code), and click Done.

Copy the issuer (found under API > Authorization Servers), client ID, and client secret into application.properties for both projects.

okta.oauth2.issuer=$issuer
okta.oauth2.client-id=$clientId
okta.oauth2.client-secret=$clientSecret

Next, add the Okta Spring Boot starter and Spring Cloud Security to your gateway’s pom.xml:

<dependency>
   <groupId>com.okta.spring</groupId>
   <artifactId>okta-spring-boot-starter</artifactId>
   <version>1.2.1</version>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-security</artifactId>
</dependency>

This is all you need to do to add OIDC login with Okta! Restart your Gateway app and navigate to http://localhost:8080/fave-cars in your browser to be redirected to Okta for user authorization.

Make Your Gateway an OAuth 2.0 Resource Server

You likely won’t build the UI for your app on the gateway itself. You’ll probably use a SPA or mobile app instead. To configure your gateway to operate as a resource server (that looks for an Authorization header with a bearer token), add a new SecurityConfiguration class in the same directory as your main class.

package com.example.apigateway;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        // @formatter:off
        http
            .authorizeExchange()
                .anyExchange().authenticated()
                .and()
            .oauth2Login()
                .and()
            .oauth2ResourceServer()
                .jwt();
        return http.build();
        // @formatter:on
    }
}

CORS with Spring Cloud Gateway

If you’re using a SPA for your UI, you’ll want to configure CORS as well. You can do this by adding a CorsWebFilter bean to this class.

@Bean
CorsWebFilter corsWebFilter() {
    CorsConfiguration corsConfig = new CorsConfiguration();
    corsConfig.setAllowedOrigins(List.of(""));
    corsConfig.setMaxAge(3600L);
    corsConfig.addAllowedMethod("
");
    corsConfig.addAllowedHeader("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfig);

    return new CorsWebFilter(source);
}

Make sure your imports match the ones below.

import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

Spring Cloud Gateway’s documentation explains how to configure CORS with YAML or with WebFluxConfigurer. Unfortunately, I was unable to get either one to work.

Test Your Gateway with WebTestClient and JWT

If you configured CORS in your gateway, you can test it works with WebTestClient. Replace the code in ApiGatewayApplicationTests with the following.

package com.example.apigateway;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = {"spring.cloud.discovery.enabled = false"})
public class ApiGatewayApplicationTests {

    @Autowired
    WebTestClient webTestClient;

    @MockBean
    ReactiveJwtDecoder jwtDecoder;

    @Test
    public void testCorsConfiguration() {
        Jwt jwt = jwt();
        when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));
        WebTestClient.ResponseSpec response = webTestClient.put().uri("/")
                .headers(addJwt(jwt))
                .header("Origin", "http://example.com")
                .exchange();

        response.expectHeader().valueEquals("Access-Control-Allow-Origin", "*");
    }

    private Jwt jwt() {
        return new Jwt("token", null, null,
                Map.of("alg", "none"), Map.of("sub", "betsy"));
    }

    private Consumer<HttpHeaders> addJwt(Jwt jwt) {
        return headers -> headers.setBearerAuth(jwt.getTokenValue());
    }
}

  1. Mock ReactiveJwtDecoder so you can set expectations and return mocks when it decodes
  2. Create a new JWT
  3. Return the same JWT when it’s decoded
  4. Add the JWT to the Authorization header with a Bearer prefix

I like how WebTestClient allows you to set the security headers so easily!

You’ve configured Spring Cloud Gateway to use OIDC login and function as an OAuth 2.0 resource server, but the car service is still available on port 8081. Let’s fix that so only the gateway can talk to it.

Secure Gateway to Microservice Communication

Add the Okta Spring Boot starter to car-service/pom.xml:

<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>1.2.1</version>
</dependency>

Copy the okta.* properties from the gateway’s application.properties to the car service’s. Then create a SecurityConfiguration class to make the app an OAuth 2.0 resource server.

package com.example.carservice;

import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        // @formatter:off
        http
            .authorizeExchange()
                .anyExchange().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt();

        Okta.configureResourceServer401ResponseBody(http);

        return http.build();
        // @formatter:on
    }
}

That’s it! Restart your car service application and it’s now protected from anonymous intruders.

$ http :8081/cars
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/plain
...

401 Unauthorized

Test Your Microservice with WebTestClient and JWT

The tests you added in the car-service project will no longer work now that you’ve enabled security. Modify the code in CarServiceApplicationTests.java to add JWT access tokens to each request.

package com.example.carservice;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;

import java.time.LocalDate;
import java.time.Month;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = {"spring.cloud.discovery.enabled = false"})
public class CarServiceApplicationTests {

    @Autowired
    CarRepository carRepository;

    @Autowired
    WebTestClient webTestClient;

    @MockBean
    ReactiveJwtDecoder jwtDecoder;

    @Test
    public void testAddCar() {
        Car buggy = new Car(UUID.randomUUID(), "ID. BUGGY", LocalDate.of(2022, Month.DECEMBER, 1));

        Jwt jwt = jwt();
        when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));

        webTestClient.post().uri("/cars")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .headers(addJwt(jwt))
                .body(Mono.just(buggy), Car.class)
                .exchange()
                .expectStatus().isCreated()
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBody()
                .jsonPath("$.id").isNotEmpty()
                .jsonPath("$.name").isEqualTo("ID. BUGGY");
    }

    @Test
    public void testGetAllCars() {
        Jwt jwt = jwt();
        when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));

        webTestClient.get().uri("/cars")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .headers(addJwt(jwt))
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBodyList(Car.class);
    }

    @Test
    public void testDeleteCar() {
        Car buzzCargo = carRepository.save(new Car(UUID.randomUUID(), "ID. BUZZ CARGO",
                LocalDate.of(2022, Month.DECEMBER, 2))).block();

        Jwt jwt = jwt();
        when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));

        webTestClient.delete()
               .uri("/cars/{id}", Map.of("id", buzzCargo.getId()))
                .headers(addJwt(jwt))
                .exchange()
                .expectStatus().isOk();
    }

    private Jwt jwt() {
        return new Jwt("token", null, null,
                Map.of("alg", "none"), Map.of("sub", "dave"));
    }

    private Consumer<HttpHeaders> addJwt(Jwt jwt) {
        return headers -> headers.setBearerAuth(jwt.getTokenValue());
    }
}

Run the test again and everything should pass!

Mock JWT Support in Spring Security 5.2

Kudos to Josh Cummings for his help with JWTs and WebTestClient. Josh gave me a preview of the mock JWT support coming in Spring Security 5.2.

this.webTestClient.mutateWith(jwt()).post(...)

Josh also provided an example test showing how to mock a JWT’s subject, scope, and claims. This code is based on new functionality in Spring Security 5.2.0.M3.

The future is bright for OAuth 2.0 and JWT support in Spring Security land! 😎

Relay the Access Token: Gateway to Microservice

You only need to make one small change for your gateway to talk to this protected service. It’s incredibly easy and I ❤️ it!

In ApiGatewayApplication.java, add a filter that applies the TokenRelayGatewayFilterFactory from Spring Cloud Security.

import org.springframework.cloud.security.oauth2.gateway.TokenRelayGatewayFilterFactory;

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder,
                                       TokenRelayGatewayFilterFactory filterFactory) {
    return builder.routes()
            .route("car-service", r -> r.path("/cars")
                    .filters(f -> f.filter(filterFactory.apply()))
                    .uri("lb://car-service/cars"))
            .build();
}

This relay factory does not automatically refresh access tokens (yet).

Restart your API gateway and you should be able to view http://localhost:8080/cars and have everything work as expected.

Pretty sweet, don’t you think?!

Thanks for reading

If you liked this post, share it with all of your programming buddies!

Follow us on Facebook | Twitter

Further reading about Microservices

An Introduction to Microservices

What is Microservices?

Build Spring Microservices and Dockerize Them for Production

Best Java Microservices Interview Questions In 2019

Build a microservices architecture with Spring Boot and Spring Cloud

Design patterns for microservices 🍂 🍂 🍂

Kotlin Microservices With Micronaut, Spring Cloud, and JPA

Build Spring Microservices and Dockerize Them for Production

Secure Service-to-Service Spring Microservices with HTTPS and OAuth 2.0

Build Secure Microservices with AWS Lambda and ASP.NET Core


Build a microservices architecture with Spring Boot and Spring Cloud

Build a microservices architecture with Spring Boot and Spring Cloud

This tutorial shows you how to build a microservices architecture with Spring Boot and Spring Cloud Config using JHipster.

Developing a microservice architecture with Java and Spring Boot is quite popular these days. It’s definitely one of the most popular combinations in the Java ecosystem. If you need any proof, just look at all of the similar frameworks that have cropped up in the last few years: MicroProfile, Micronaut, and Quarkus, just to name a few.

Spring Boot provided a much-needed spark to the Spring ecosystem when it was first released in 2014. Instead of making Java developers configure all aspects of their Spring beans, it provided "starters" that contained pre-configured beans with the default settings. This led to less Java code, and also provided the ability to override the defaults via an application.properties file. Yes, there are many ways to modify the defaults in a Spring Boot application, but I’ll skip over that for now.

Java Microservices with Spring Cloud Config

Spring Cloud Config is a project that provides externalized configuration for distributed systems. Spring Cloud Config has server and client components. You can configure the server to read its configuration from the file system or a source code repository, like Git. On the client, you configure things in a bootstrap configuration file to get configuration data from the server. In a microservices environment, this provides an elegant way to configure all your microservices from a central location.

Today I’d like to show you how this works and demo it using one of the hippest microservice solutions I’ve ever worked with.

Use JHipster to Generate a Java Microservices Architecture

JHipster is a development platform to generate, develop, and deploy Spring Boot + { Angular or React or Vue } applications. In addition, it supports creating Spring based microservice architectures. In fact, if you create microservices projects and choose OAuth 2.0 / OIDC for authentication, you’ll be using code that’s very similar to the aforementioned example.

To use JHipster, you’ll need to have Node.js installed. You can also use start.jhipster.tech, which is similar to start.spring.io.

The most common way to install JHipster is using npm:

npm install -g [email protected]

You can run the command above without the version number to get the latest version of JHipster. If it’s 6.x, this tutorial should work, but I can’t guarantee it does.

In a terminal, create a directory to hold all the projects you’re about to create. For example, jhipster.

Create an apps.jh file in this directory and put the following code into it.



application {
  config {
    baseName gateway,
    packageName com.okta.developer.gateway,
    applicationType gateway,
    authenticationType oauth2,
    prodDatabaseType postgresql,
    serviceDiscoveryType eureka,
    testFrameworks [protractor]
  }
  entities Blog, Post, Tag, Product
}

application {
  config {
    baseName blog,
    packageName com.okta.developer.blog,
    applicationType microservice,
    authenticationType oauth2,
    prodDatabaseType postgresql,
    serverPort 8081,
    serviceDiscoveryType eureka
  }
  entities Blog, Post, Tag
}

application {
  config {
    baseName store,
    packageName com.okta.developer.store,
    applicationType microservice,
    authenticationType oauth2,
    databaseType mongodb,
    devDatabaseType mongodb,
    prodDatabaseType mongodb,
    enableHibernateCache false,
    serverPort 8082,
    serviceDiscoveryType eureka
  }
  entities Product
}

entity Blog {
  name String required minlength(3),
  handle String required minlength(2)
}

entity Post {
  title String required,
  content TextBlob required,
  date Instant required
}

entity Tag {
  name String required minlength(2)
}

entity Product {
  title String required,
  price BigDecimal required min(0),
  image ImageBlob
}

relationship ManyToOne {
  Blog{user(login)} to User,
  Post{blog(name)} to Blog
}

relationship ManyToMany {
  Post{tag(name)} to Tag{post}
}

paginate Post, Tag with infinite-scroll
paginate Product with pagination

microservice Product with store
microservice Blog, Post, Tag with blog

// will be created under 'docker-compose' folder
deployment {
  deploymentType docker-compose
  appsFolders [gateway, blog, store]
  dockerRepositoryName "jmicro"
  consoleOptions [zipkin]
}


You’ll want to change the dockerRepositoryName in the JDL above to use yourDocker Hubusername if you want to publish your containers. This is not a necessary step to complete this tutorial.

This code is JDL (JHipster Domain Language) and you can use it to define your app, its entities, and even deployment settings. You can learn more about JDL in JHipster’s JDL documentation. Below is a screenshot of JDL Studio, which can be used to edit JDL and see how entities related to each other.

The JDL you just put in apps.jh defines three applications:

  • gateway: a single entry point to your microservices, that will include the UI components.
  • blog: a blog service that talks to PostgreSQL.
  • store: a store service that uses MongoDB.

Run the following command to create these projects in your jhipster folder.

jhipster import-jdl apps.jh

This will create all three projects in parallel. You can watch the console recording below to see how it looks. The time it takes to create everything will depend on how fast your computer and internet are.

Create Docker Images for Microservice Apps

When the configuration is generated for Docker Compose, a warning is spat out to the console.

WARNING! Docker Compose configuration generated, but no Jib cache found
If you forgot to generate the Docker image for this application, please run:
To generate the missing Docker image(s), please run:
  ./mvnw -Pprod verify jib:dockerBuild in /Users/mraible/java-microservices-examples/jhipster/gateway
  ./mvnw -Pprod verify jib:dockerBuild in /Users/mraible/java-microservices-examples/jhipster/blog
  ./mvnw -Pprod verify jib:dockerBuild in /Users/mraible/java-microservices-examples/jhipster/store

To make it easier to create Docker images with one command, create an aggregator pom.xml in the jhipster root directory.


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    4.0.0
    com.okta.developer
    jhipster-parent
    1.0.0-SNAPSHOT
    pom
    jhipster-parent
    
        gateway
        blog
        store
    


Then "just jib it" using Jib.

mvn -Pprod verify com.google.cloud.tools:jib-maven-plugin:dockerBuild

If you don’t have Maven installed, use brew install maven on a Mac, or see Maven’s installation docs.

[INFO] Skipping containerization because packaging is 'pom'...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] Gateway 0.0.1-SNAPSHOT ............................. SUCCESS [02:44 min]
[INFO] Blog 0.0.1-SNAPSHOT ................................ SUCCESS [ 34.391 s]
[INFO] Store 0.0.1-SNAPSHOT ............................... SUCCESS [ 28.589 s]
[INFO] jhipster-parent 1.0.0-SNAPSHOT ..................... SUCCESS [  1.096 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 03:49 min
[INFO] Finished at: 2019-05-17T07:44:39-06:00
[INFO] ------------------------------------------------------------------------
Execution time: 3 min. 50 s.

Run Your Java Microservices Stack with Docker Compose

Once everything has finished building, cd into the docker-compose directory and start all your containers.

cd docker-compose
docker-compose up -d

Remove the -d if you want to see all the logs in your current terminal window.

It will take several minutes to start all eight of your containers. You can use Kitematic to monitor their startup progress if you like.

Creating docker-compose_gateway-app_1                ... done
Creating docker-compose_gateway-postgresql_1         ... done
Creating docker-compose_blog-app_1                   ... done
Creating docker-compose_store-mongodb_1              ... done
Creating docker-compose_keycloak_1                   ... done
Creating docker-compose_blog-postgresql_1            ... done
Creating docker-compose_jhipster-registry_1          ... done
Creating docker-compose_store-app_1                  ... done
JHipster Registry for Service Discovery with Java Microservices

This microservices stack uses Eureka for service discovery, just like the bare-bones Spring Boot + Spring Cloud example. This was determined by the following line for each app in the JDL.

serviceDiscoveryType eureka

When you select eureka for service discovery, JHipster Registry is used. This application is very similar to Eureka Server, except it has an Angular UI and includes Spring Cloud Config, among other features.

JHipster also supports Hashicorp Consul for service discovery.

Because you chose OAuth 2.0/OIDC for authentication, you’ll need to create an entry in your hosts file (/etc/hosts on Linux/Mac, C:\Windows\System32\Drivers\etc\hosts on Windows) for Keycloak.

127.0.0.1 keycloak

This is because the Docker network recognizes keycloak as a registered hostname, but it also redirects you to keycloak. Your browser is not aware of that hostname without the hosts entry.

Open your browser and navigate to [http://localhost:8761](http://localhost:8761). You’ll be redirected to Keycloak to login. Enter admin/admin for credentials and you’ll be redirected back to JHipster Registry. You’ll see all your microservice instances have been registered.

Navigate to [http://localhost:8080](http://localhost:8080), click sign in, and you’ll be logged in to the gateway. You can go to Entities > Blog and add a blog.

Go to Entities > Product and you can add a product too.

Pretty slick, don’t you think?! 🤓

Configure JHipster Microservices to Use Okta for Identity

One of the problems you saw in the bare-bones Spring Boot + Spring Cloud setup is you have to configure okta.oauth2.* properties in every microservice. JHipster doesn’t use the Okta Spring Boot starter. It uses oauth2-client and oauth2-resource-server Spring Boot starters instead. The configuration for OAuth 2.0 is contained in each app’s src/main/resources/config/application.yml file.

spring:
  ...
  security:
    oauth2:
      client:
        provider:
          oidc:
            issuer-uri: http://localhost:9080/auth/realms/jhipster
        registration:
          oidc:
            client-id: internal
            client-secret: internal
Why Okta?

You might be wondering why you should use Okta instead of Keycloak? Keycloak works great for development and testing, and especially well if you’re on a plane with no wi-fi. However, in production, you want a system that’s always on. That’s where Okta comes in. To begin, you’ll need to create an Okta account and an application with it.

Create a Web Application in Okta

Log in to your Okta Developer account (or sign up if you don’t have an account).

  1. From the Applications page, choose Add Application.
  2. On the Create New Application page, select Web.
  3. Give your app a memorable name, add [http://localhost:8080/login/oauth2/code/okta](http://localhost:8080/login/oauth2/code/okta) as a Login redirect URI, select Refresh Token (in addition to Authorization Code), and click Done.
  4. To configure Logout to work in JHipster, Edit your app, add [http://localhost:8080](http://localhost:8080) as a Logout redirect URI, then click Save.
Configure Your OpenID Connect Settings with Spring Cloud Config

Rather than modifying each of your apps for Okta, you can use Spring Cloud Config in JHipster Registry to do it. Open docker-compose/central-server-config/application.yml and add your Okta settings.

The client ID and secret are available on your app settings page. You can find the issuer under API > Authorization Servers.

spring:
  security:
    oauth2:
      client:
        provider:
          oidc:
            issuer-uri: https://{yourOktaDomain}/oauth2/default
        registration:
          oidc:
            client-id: {yourClientId}
            client-secret: {yourClientSecret}

The registry, gateway, blog, and store applications are all configured to read this configuration on startup.

Restart all your containers for this configuration to take effect.

docker-compose restart

Before you can log in, you’ll need to add redirect URIs for JHipster Registry, ensure your user is in a ROLE_ADMIN group and that groups are included in the ID token.

Log in to your Okta dashboard, edit your OIDC app, and add the following Login redirect URI:

http://localhost:8761/login/oauth2/code/oidc

You’ll also need to add a Logout redirect URI:

http://localhost:8761

Then, click Save.

Create Groups and Add Them as Claims to the ID Token

JHipster is configured by default to work with two types of users: administrators and users. Keycloak is configured with users and groups automatically, but you need to do some one-time configuration for your Okta organization.

Create a ROLE_ADMIN group (Users > Groups > Add Group) and add your user to it. Navigate to API > Authorization Servers, and click on the the default server. Click the Claims tab and Add Claim. Name it groups, and include it in the ID Token. Set the value type to Groups and set the filter to be a Regex of .*. Click Create.

Now when you hit [http://localhost:8761](http://localhost:8761) or [http://localhost:8080](http://localhost:8080), you’ll be prompted to log in with Okta!

It’s pretty nifty how you can configure your service registry and all your microservices in one place with Spring Cloud Config, don’t you think?! 👌

Configuring Spring Cloud Config with Git

JHipster Registry and its Spring Cloud Config server support two kinds of configuration sources: native and git. Which one is used is determined by a spring.cloud.config.server.composite property. If you look in docker-compose/jhipster-registry.yml, you’ll see that native is enabled and git is commented out.

- SPRING_CLOUD_CONFIG_SERVER_COMPOSITE_0_TYPE=native
- SPRING_CLOUD_CONFIG_SERVER_COMPOSITE_0_SEARCH_LOCATIONS=file:./central-config
# - SPRING_CLOUD_CONFIG_SERVER_COMPOSITE_0_TYPE=git
# - SPRING_CLOUD_CONFIG_SERVER_COMPOSITE_0_URI=https://github.com/jhipster/jhipster-registry/
# - SPRING_CLOUD_CONFIG_SERVER_COMPOSITE_0_SEARCH_PATHS=central-config
# For Keycloak to work, you need to add '127.0.0.1 keycloak' to your hosts file

You can see the default configuration for Git at @jhipster/jhipster-registry/central-config/application.yml. You can learn more about application configuration with Spring Cloud Config in JHipster Registry’s documentation. It includes a section on encrypting configuration values.