Professional Documents
Culture Documents
Architecture On The Modern Stack of Java Technologies
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
1.0.4
Unit-testing framework
JUnit
5.1.1
Spring Cloud
API gateway
Included in the Finchley SR2 Release train of the Spring Cloud project
Centralized configuration
Distributed tracing
Service discovery
Circuit breaker
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.
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.
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:
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:
@Bean
@LoadBalanced
fun restTemplate() = RestTemplate()
And used in this way:
@Bean
fun webClient(loadBalancerClient: LoadBalancerClient) = WebClient.builder()
.filter(LoadBalancerExchangeFilterFunction(loadBalancerClient))
.build()
And used in this way:
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:
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.
@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:
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!