Microservices Architecture on the Modern Stack of Java Technologies
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-eureka
parameter (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-registry
parameter 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/
:
Alternative 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!
References
Source code of the project on GitHub.
Microservices articles by Chris Richardson.
Microservices article by Martin Fowler.
Microservices resources guide by Martin Fowler.