📜 ⬆️ ⬇️

Microservice architecture on a 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 Spring Cloud stack libraries for Service discovery, creating API gateway, client balancing, implementing the Circuit breaker pattern , writing declarative HTTP clients, distributed tracing and all that. Not that all this was needed to create a microservice architecture - only just for fun ...

Introduction


In this article, you will see an example of a microservice architecture on technologies that are relevant in the Java world, the main of which are given below (these versions are used in the project at the time of publication):
Technology typeTitleVersion
PlatformJDK11.0.1
Programming languageKotlin1.3.10
Application frameworkSpring framework5.0.9
Spring boot2.0.5
Assembly systemGradle5.0
Gradle kotlin dsl1.0.4
Unit testing frameworkJunit5.1.1
Spring cloud
Single access point (API gateway)Spring cloud gatewayIncluded in the Spring Cloud Release train Finchley SR2
Centralized configurationSpring cloud config
Request Tracing (Distributed tracing)Spring cloud sleuth
Declarative HTTP clientSpring cloud openfeign
Service discoverySpring cloud netflix eureka
Circuit breakerSpring cloud netflix hystrix
Client load balancingSpring 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 back-end (Services service):


All of them will be discussed below. In the “combat” project, obviously, there will be much more microservices implementing 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


Spring Cloud Config was used to create a centralized repository for application configurations. 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 configuration 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, which contains the declaration of the application class and the main method, which, unlike the equivalent Java code, 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 appearance.

Service discovery server


Service discovery is a microservice architecture pattern that allows you to simplify the interaction between applications in terms 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 (more here ).

In this project, Service discovery is implemented on the basis of Netflix Eureka, which is a Client-side service discovery : Eureka server performs the function of the Service registry, and Eureka client calls the Eureka server for the list of instances of the called application and independently balances before performing a request to any microservice. load (using Netflix Ribbon). 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 .

In the Service discovery server configuration, located in its resources ( bootstrap.yml ), only the application name and the parameter determining that the microservice launch will be interrupted is indicated 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 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 (indicated for clarity, register-with-eureka it is also used by default) indicates that the application itself, like other microservices, will be registered with the Eureka server. The 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 back-end with the REST API implemented using the WebFlux framework that appeared in Spring 5 (the documentation is here ), or rather the 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) } } 

Processing received HTTP requests is delegated to the ItemHandler class ItemHandler . For example, the method for getting a 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 a client of the Eureka server, i.e., it registers and receives data from the Service registry, due to the spring-cloud-starter-netflix-eureka-client . After registration, the application with a certain periodicity sends hartbits to the Eureka server, and if, for a certain period of time, the percentage of the Hearbits 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.

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 function of the front-end for the Items service, with which REST API can interact in several ways:

  1. Client to 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\"}" } } } } 
  2. RestTemplate class RestTemplate
    In the java-config, a bin is created:

     @Bean @LoadBalanced fun restTemplate() = RestTemplate() 

    And is used in this way:

     fun requestWithRestTemplate(id: Long): String = restTemplate.getForEntity("http://items-service/items/$id", String::class.java).body ?: "No result" 
  3. Bean of the WebClient class (the method is specific for the WebFlux framework)
    In the java-config, a bin is created:

     @Bean fun webClient(loadBalancerClient: LoadBalancerClient) = WebClient.builder() .filter(LoadBalancerExchangeFilterFunction(loadBalancerClient)) .build() 

    And is used in this way:

     fun requestWithWebClient(id: Long): Mono<String> = webClient.get().uri("http://items-service/items/$id").retrieve().bodyToMono(String::class.java) 

The fact that all three methods return the same result can be seen by going to http://localhost:8081/example :


I prefer the option using OpenFeign, because it gives the opportunity to develop a contract for interaction with the called microservice, the implementation of which Spring assumes. The object implementing this contract is injected and used as an ordinary bin:

 itemsServiceFeignClient.getItem(1) 

If the request for any reason fails, the corresponding class method that implements the FallbackFactory interface will be called, in which you need to handle the error and return the default response (or forward an exception further). In the event that some number of consecutive calls fail, the Fuse 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 @EnableFeignClients :

 @SpringBootApplication @EnableFeignClients(clients = [ItemsServiceFeignClient::class]) class ItemsUiApplication 

To use Hystrix fallback in the Feign client, you need to enter the configuration file:

 feign: hystrix: enabled: true 

To test the work of Hystrix fallback in the Feign client, just go to http://localhost:8081/hystrix-fallback . The feign client will attempt to execute the query on the path that does not exist in the Items service, which will lead to the return of respons:

 {"error" : "Some error"} 

Ui gateway


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

In this project, for greater clarity, a UI gateway is 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 part of 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 ReactiveUserDetailsService interface:

 @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) } 

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 determines that a part of web resources (for example, static) is available to all users, including those who are not authenticated, and everything else ( .anyExchange() ) is available only to authenticated users. When you try to login to a URL that requires authentication, you will be redirected to the login page ( https://localhost/login ):


This page uses the tools of 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") } } 

Spring Cloud Gateway routing can be configured in a YAML or java-config. Routes to microservices are either set manually or created automatically based on data received from the Service registry. With a sufficiently large number of UIs that require routing, 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 the routes will be created only for microservices whose names end with -UI , and the value of the url-expression parameter means that they are accessible via the HTTP protocol, unlike the UI gateway working via HTTPS, and when accessing they will use client load balancing (implemented using the Netflix Ribbon).

Consider the 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 routes 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 gain access to the login and / or user roles 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 turn to Items UI using the UI gateway - https://localhost/items-ui/greeting , rightly assuming 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 (unit of work) identifier 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 is connected by simply adding the spring-cloud-starter-sleuth .

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

 DEBUG [ui-gateway,009b085bfab5d0f2,009b085bfab5d0f2,false] oscghRoutePredicateHandlerMapping : Route matched: CompositeDiscoveryClient_ITEMS-UI DEBUG [items-ui,009b085bfab5d0f2,947bff0ce8d184f4,false] oswrfunction.server.RouterFunctions : Predicate "(GET && /example)" matches against "GET /example" DEBUG [items-service,009b085bfab5d0f2,dd3fa674cd994b01,false] oswrfunction.server.RouterFunctions : Predicate "(GET && /{id})" matches against "GET /1" 

For a graphical representation of distributed tracing, you can use, for example, Zipkin, which will act as a server that aggregates information about HTTP requests from other microservices (more here ).

Assembly


Depending on the OS, gradlew clean build or ./gradlew clean build is performed.

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 that, 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 starting the applications in the order of their description in this article. If you use Intellij IDEA with Run Dashboard enabled, you should get something like the following:


Conclusion


The article reviewed an example of a microservice architecture on the current technology stack in the Java world, its main components and some features. I hope for someone the material will be useful. Thank you for attention!

Links


Source: https://habr.com/ru/post/431474/


All Articles