Technology type | Title | Version |
---|---|---|
Platform | JDK | 11.0.1 |
Programming language | Kotlin | 1.3.10 |
Application framework | Spring framework | 5.0.9 |
Spring boot | 2.0.5 | |
Assembly system | Gradle | 5.0 |
Gradle kotlin dsl | 1.0.4 | |
Unit testing framework | Junit | 5.1.1 |
Spring cloud | ||
Single access point (API gateway) | Spring cloud gateway | Included in the Spring Cloud Release train Finchley SR2 |
Centralized configuration | Spring cloud config | |
Request Tracing (Distributed tracing) | Spring cloud sleuth | |
Declarative HTTP client | Spring cloud openfeign | |
Service discovery | Spring cloud netflix eureka | |
Circuit breaker | Spring cloud netflix hystrix | |
Client load balancing | Spring Cloud Netflix Ribbon |
application.yml
) looks like this: spring: profiles: active: native cloud: config: server: native: search-locations: classpath:/config server: port: 8888
bootstrap.yml
. At startup, they load their config by executing a GET request to the HTTP API Config server. @SpringBootApplication @EnableConfigServer class ConfigServerApplication fun main(args: Array<String>) { runApplication<ConfigServerApplication>(*args) }
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
eureka-server.yml
in the Config server resources: server: port: 8761 eureka: client: register-with-eureka: true fetch-registry: false
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.http://localhost:8761/
: @Bean fun itemsRouter(handler: ItemHandler) = router { path("/items").nest { GET("/", handler::getAll) POST("/", handler::add) GET("/{id}", handler::getOne) PUT("/{id}", handler::update) } }
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()))
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. @PostConstruct private fun addMetadata() = aim.registerAppMetadata(mapOf("description" to "Some description"))
http://localhost:8761/eureka/apps/items-service
via Postman: @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
class RestTemplate
@Bean @LoadBalanced fun restTemplate() = RestTemplate()
fun requestWithRestTemplate(id: Long): String = restTemplate.getForEntity("http://items-service/items/$id", String::class.java).body ?: "No result"
WebClient
class (the method is specific for the WebFlux framework) @Bean fun webClient(loadBalancerClient: LoadBalancerClient) = WebClient.builder() .filter(LoadBalancerExchangeFilterFunction(loadBalancerClient)) .build()
fun requestWithWebClient(id: Long): Mono<String> = webClient.get().uri("http://items-service/items/$id").retrieve().bodyToMono(String::class.java)
http://localhost:8081/example
: itemsServiceFeignClient.getItem(1)
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.@EnableFeignClients
: @SpringBootApplication @EnableFeignClients(clients = [ItemsServiceFeignClient::class]) class ItemsUiApplication
feign: hystrix: enabled: true
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"}
server: port: 443 ssl: key-store: classpath:keystore.p12 key-store-password: qwerty key-alias: test_key key-store-type: PKCS12
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) }
@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()
.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
): @Bean fun routes() = router { GET("/login") { ServerResponse.ok().contentType(MediaType.TEXT_HTML).render("login") } }
spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true include-expression: serviceId.endsWith('-UI') url-expression: "'lb:http://'+serviceId"
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). @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") } }
http://localhost:8761
), the second is needed to load the resources of this page.https://localhost/actuator/gateway/routes
. @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()) } }
https://localhost/items-ui/greeting
, rightly assuming that the Items UI has already implemented the processing of these headers:spring-cloud-starter-sleuth
. 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"
gradlew clean build
or ./gradlew clean build
is performed.Source: https://habr.com/ru/post/431474/
All Articles