You are on page 1of 9

Microservices Architecture on the Modern Stack of Java Technologies

In this post, we take a look at microservices architectures based on technologies


relevant to the Java world. Read on to get started!

We had JDK 11, Kotlin, Spring 5 and Spring Boot 2, Gradle 5 with production-ready
Kotlin DSL, JUnit 5, and another half a dozen libraries of the Spring Cloud stack
for service discovery, creating an API gateway, client-side load balancing,
implementing the Circuit breaker pattern, writing declarative HTTP clients,
distributed tracing, and all that. Not that all this was needed to create a
microservices architecture � but it's fun!

Introduction
In this article, you will see an example of a microservices architecture on the
actual technologies of the Java world, with the main ones given below (the versions
mentioned are used in the project at the time of publication):

Technology type

Name

Version

Platform

JDK

11.0.1

Programming language

Kotlin

1.3.11

Application framework

Spring Framework

5.0.9

Spring Boot

2.0.5

Build tool

Gradle

5.0

Gradle Kotlin DSL

1.0.4

Unit-testing framework

JUnit
5.1.1

Spring Cloud

API gateway

Spring Cloud Gateway

Included in the Finchley SR2 Release train of the Spring Cloud project

Centralized configuration

Spring Cloud Config

Distributed tracing

Spring Cloud Sleuth

Declarative HTTP client

Spring Cloud OpenFeign

Service discovery

Spring Cloud Netflix Eureka

Circuit breaker

Spring Cloud Netflix Hystrix

Client-side load balancing

Spring Cloud Netflix Ribbon

The project consists of 5 microservices: 3 infrastructure (Config server, Service


discovery server, UI gateway) and examples of the front-end (Items UI ) and backend
(Items service):

All of them will be sequentially considered below. In the �combat� project,


obviously, there will be many more microservices that implement any business
functionality. Their addition to a similar architecture is technically similar to
the Items UI and Items service.

Disclaimer
The article does not consider the tools for containerization and orchestration,
since, at present, they are not used in the project.

Config Server
To create a centralized repository of application configurations, Spring Cloud
Config was used. Configs can be read from various sources, for example, a separate
git repository; in this project, for simplicity and clarity, they are in the
application resources:
The config of the Config server itself (application.yml) looks like this:

spring:
profiles:
active: native
cloud:
config:
server:
native:
search-locations: classpath:/config
server:
port: 8888
Using port 8888 allows Config server clients not to explicitly specify its port in
their bootstrap.yml. At startup, they load their config by executing a GET request
to the HTTP API Config server.

The program code of this microservice consists of just one file containing the
application class declaration and the main method, which, unlike the equivalent
code in Java, is a top-level function:

@SpringBootApplication
@EnableConfigServer
class ConfigServerApplication
fun main(args: Array<String>) {
runApplication<ConfigServerApplication>(*args)
}
Application classes and main methods in other microservices have a similar form.

Service Discovery Server


Service discovery is a microservice architecture pattern that allows you to
simplify the interaction between applications under the conditions of a possible
change in the number of their instances and network location. The key component in
this approach is the service registry, a database of microservices, their
instances, and network locations (for more see here).

In this project, service discovery is implemented on the basis of Netflix Eureka,


which is a client-side service discovery: the Eureka server performs the function
of the service registry, and the Eureka client calls the Eureka server for the list
of instances of the called application and performs its own load balancing using
Netflix Ribbon before performing a request to any microservice. Netflix Eureka,
like some other components of the Netflix OSS stack (for example, Hystrix and
Ribbon), integrates with Spring Boot applications using Spring Cloud Netflix.

Service discovery server configuration, located in its resources (bootstrap.yml),


contains only the application name and the parameter indicating that the
microservice launch will be interrupted if it is impossible to connect to the
Config server:

spring:
application:
name: eureka-server
cloud:
config:
fail-fast: true
The rest of the application config is located in the eureka-server.yml file in the
Config server resources:

server:
port: 8761
eureka:
client:
register-with-eureka: true
fetch-registry: false
Eureka server uses port 8761, which allows all Eureka clients not to specify it
using the default value. The value of the register-with-eurekaparameter (specified
for clarity, since it is also used by default) indicates that the application
itself, like other microservices, will be registered with the Eureka server. fetch-
registryparameter determines whether the Eureka client will receive data from the
Service registry.

The list of registered applications and other information is available at


http://localhost:8761/:

Image titleAlternative options for implementing service discovery are Consul,


Zookeeper, and others.

Items Service
This application is an example of a backend with a REST API implemented using the
WebFlux framework that appeared in Spring 5 (the documentation here), or rather
Kotlin DSL for it:

@Bean
fun itemsRouter(handler: ItemHandler) = router {
path("/items").nest {
GET("/", handler::getAll)
POST("/", handler::add)
GET("/{id}", handler::getOne)
PUT("/{id}", handler::update)
}
}
The processing of received HTTP requests is delegated to an ItemHandler bean. For
example, the method for getting the list of objects of some entity looks like this:

fun getAll(request: ServerRequest) = ServerResponse.ok()


.contentType(APPLICATION_JSON_UTF8)
.body(fromObject(itemRepository.findAll()))
The application becomes an Eureka server client, i.e. it registers and receives
data from the service registry, due to the spring-cloud-starter-netflix-eureka-
client dependency. After registration, the application sends heartbeats to the
Eureka server with a certain periodicity, and if within a certain period of time
the percentage of the heartbeats received by the Eureka server relative to the
maximum possible value is below a certain threshold, the application will be
deleted from the service registry.

Let's consider one of the ways to send additional metadata to the Eureka server:

@PostConstruct
private fun addMetadata() = aim.registerAppMetadata(mapOf("description" to "Some
description"))
Make sure that the Eureka server receives this data by going to
http://localhost:8761/eureka/apps/items-service via Postman:

Items UI
This microservice, besides showing interaction with the UI gateway (will be shown
in the next section), performs the front-end function for the Items service, with
which REST APIs can interact in several ways:

Client to the REST API, written using OpenFeign:


@FeignClient("items-service", fallbackFactory =
ItemsServiceFeignClient.ItemsServiceFeignClientFallbackFactory::class)
interface ItemsServiceFeignClient {
@GetMapping("/items/{id}")
fun getItem(@PathVariable("id") id: Long): String
@GetMapping("/not-existing-path")
fun testHystrixFallback(): String
@Component
class ItemsServiceFeignClientFallbackFactory :
FallbackFactory<ItemsServiceFeignClient> {
private val log = LoggerFactory.getLogger(this::class.java)
override fun create(cause: Throwable) = object : ItemsServiceFeignClient {
override fun getItem(id: Long): String {
log.error("Cannot get item with id=$id")
throw ItemsUiException(cause)
}
override fun testHystrixFallback(): String {
log.error("This is expected error")
return "{\"error\" : \"Some error\"}"
}
}
}
}
RestTemplate bean
In the java-config, a bean is created:

@Bean
@LoadBalanced
fun restTemplate() = RestTemplate()
And used in this way:

fun requestWithRestTemplate(id: Long): String =


restTemplate.getForEntity("http://items-service/items/$id",
String::class.java).body ?: "No result"
WebClient bean (method is specific for WebFlux framework)
In the java-config, a bean is created:

@Bean
fun webClient(loadBalancerClient: LoadBalancerClient) = WebClient.builder()
.filter(LoadBalancerExchangeFilterFunction(loadBalancerClient))
.build()
And used in this way:

fun requestWithWebClient(id: Long): Mono<String> =


webClient.get().uri("http://items-service/items/
$id").retrieve().bodyToMono(String::class.java)
You can make sure that all three methods return the same result by going to
http://localhost:8081/example :

I prefer to use an OpenFeign because it gives the opportunity to develop a contract


for interaction with the called microservice, which Spring takes on implementation.
The object implementing this contract is injected and used as a normal bean:

itemsServiceFeignClient.getItem(1)
If the request for any reason fails, the corresponding method of the class
implementing FallbackFactory interface will be called in which the error should be
processed and the default response returned (or forward an exception further). In
the event that some number of consecutive calls fail, the Circuit breaker will open
the circuit (for more on Circuit breaker here and here), giving time to restore the
fallen microservice.

For the Feign client to work, you need to annotate the application class with
@EnableFeignClients :

@SpringBootApplication
@EnableFeignClients(clients = [ItemsServiceFeignClient::class])
class ItemsUiApplication
To use the Hystrix fallback in the Feign client, you need to add the following to
the application config:

feign:
hystrix:
enabled: true
To test the Hystrix fallback in the Feign client, just go to
http://localhost:8081/hystrix-fallback. Feign client will try to make a request for
a path that does not exist in the Items service, which will lead to the following
fallback response:

{"error" : "Some error"}


UI Gateway
The API gateway pattern allows you to create a single entry point for the API
provided by other microservices (more here). The application implementing this
pattern performs routing of requests to underlying microservices, and can also
perform additional functions, such as authentication.

In this project, for greater clarity, a UI gateway has been implemented, that is, a
single entry point for different UIs; obviously, the API gateway is implemented in
a similar way. Microservice is implemented on the basis of the Spring Cloud Gateway
framework. An alternative option is Netflix Zuul, which is included in Netflix OSS
and integrated with Spring Boot using Spring Cloud Netflix.

The UI gateway works on port 443 using the generated SSL certificate (located in
the project). SSL and HTTPS are configured as follows:

server:
port: 443
ssl:
key-store: classpath:keystore.p12
key-store-password: qwerty
key-alias: test_key
key-store-type: PKCS12
User logins and passwords are stored in the Map-based implementation of the
WebFlux-specific interface ReactiveUserDetailsService:

@Bean
fun reactiveUserDetailsService(): ReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("john_doe").password("qwerty").roles("USER")
.build()
val admin = User.withDefaultPasswordEncoder()
.username("admin").password("admin").roles("ADMIN")
.build()
return MapReactiveUserDetailsService(user, admin)
}
The security settings are configured as follows:

@Bean
fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain = http
.formLogin().loginPage("/login")
.and()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.pathMatchers("/static/**").permitAll()
.pathMatchers("/favicon.ico").permitAll()
.pathMatchers("/webjars/**").permitAll()
.pathMatchers("/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.csrf().disable()
.build()
The given config defines that part of the web resources (for example, static
resources) is available to all users, including those who have not been
authenticated, and everything else (.anyExchange ()) is available only to
authenticated users. When attempting to login to a URL that requires
authentication, it will be redirected to the login page (https://localhost/login):

This page uses the Bootstrap framework connected to the project using Webjars,
which allows you to manage client-side libraries as normal dependencies. Thymeleaf
is used to form HTML pages. Access to the login page is configured using WebFlux:

@Bean
fun routes() = router {
GET("/login")
{ ServerResponse.ok().contentType(MediaType.TEXT_HTML).render("login") }
}
Routing using Spring Cloud Gateway can be configured in a YAML or Java config.
Routes to microservices are either set manually or are created automatically based
on data received from the Service registry. With a sufficiently large number of UIs
that need to be routed, it will be more convenient to use the integration with the
Service registry:

spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
include-expression: serviceId.endsWith('-UI')
url-expression: "'lb:http://'+serviceId"
The value of the include-expression parameter indicates that routes will be created
only for microservices whose names end with "-UI," and the value of url-expression
parameter indicates they are accessible via HTTP protocol, in contrast to the UI
gateway, which uses HTTPS, and when accessing them, client load balancing
(implemented using Netflix Ribbon) will be used.

Let's consider an example of creating routes in the Java config manually (without
integration with the service registry):

@Bean
fun routeLocator(builder: RouteLocatorBuilder) = builder.routes {
route("eureka-gui") {
path("/eureka")
filters {
rewritePath("/eureka", "/")
}
uri("lb:http://eureka-server")
}
route("eureka-internals") {
path("/eureka/**")
uri("lb:http://eureka-server")
}
}
The first route leads to the previously shown home page of the Eureka server
(http://localhost:8761), the second is needed to load the resources of this page.

All routes created by the application are accessible via


https://localhost/actuator/gateway/routes.

In the underlying microservices, it may be necessary to get access to the login


and/or roles of a user authenticated in the UI gateway. To do this, I created a
filter that adds the appropriate headers to the request:

@Component
class AddCredentialsGlobalFilter : GlobalFilter {
private val loggedInUserHeader = "logged-in-user"
private val loggedInUserRolesHeader = "logged-in-user-roles"
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain) =
exchange.getPrincipal<Principal>()
.flatMap {
val request = exchange.request.mutate()
.header(loggedInUserHeader, it.name)
.header(loggedInUserRolesHeader, (it as
Authentication).authorities?.joinToString(";") ?: "")
.build()
chain.filter(exchange.mutate().request(request).build())
}
}
Now let's access Items UI using the UI gateway � https://localhost/items-
ui/greeting � rightly suggesting that the Items UI has already implemented the
processing of these headers:

Spring Cloud Sleuth is a solution for tracing requests in a distributed system.


Trace Id (pass-through identifier) and Span Id (identifier of a unit of work) are
added to the headers of the request passing through several microservices (for
easier perception, I simplified the scheme; here is a more detailed explanation):

This functionality becomes available by simply adding the spring-cloud-starter-


sleuth dependency.

Specifying the appropriate logging settings, you can see something like the
following in the console of the respective microservices (Trace Id and Span Id are
displayed after the name of the microservice):

DEBUG [ui-gateway,009b085bfab5d0f2,009b085bfab5d0f2,false]
o.s.c.g.h.RoutePredicateHandlerMapping : Route matched:
CompositeDiscoveryClient_ITEMS-UI
DEBUG [items-ui,009b085bfab5d0f2,947bff0ce8d184f4,false]
o.s.w.r.function.server.RouterFunctions : Predicate "(GET && /example)" matches
against "GET /example"
DEBUG [items-service,009b085bfab5d0f2,dd3fa674cd994b01,false]
o.s.w.r.function.server.RouterFunctions : Predicate "(GET && /{id})" matches
against "GET /1"
For a graphical representation of a distributed routing Zipkin, for example, can be
used, which will execute server function, aggregating information about HTTP-
requests from other microservices (more here).

Build
Depending on the OS, perform a gradlew clean build or ./gradlew clean build.

Given the possibility of using the Gradle wrapper, there is no need for a locally
installed Gradle.

Build and subsequent launch successfully pass on JDK 11.0.1. Prior to this, the
project worked on JDK 10, so I admit that there will be no problems with build and
launch on this version. I have no data for earlier versions of the JDK. In
addition, you need to take into account that the Gradle 5 used requires at least
JDK 8.

Launch
I recommend to start the applications in the order described in this article. If
you use Intellij IDEA with Run Dashboard enabled, you should get something like the
following:

Conclusion
In the article we examined an example of the microservices architecture
implementing with the modern technology stack suggested by the Java world, its main
components, and some features. I hope the material will be useful. Thanks!

You might also like