现代Java技术堆栈上的微服务架构

我们拥有JDK 11,Kotlin,Spring 5和Spring Boot 2,Gradle 5和可用于生产的Kotlin DSL,JUnit 5,以及许多用于服务发现,创建网关API,客户端平衡和实现Circuit Breaker的Spring Cloud堆栈库。编写声明性的HTTP客户端,分布式跟踪等等。 创建微服务架构并不需要所有这些-仅出于娱乐目的...

参赛作品


在本文中,您将看到一个使用Java世界中相关技术的微服务体系结构示例,其主要内容如下(所指示的版本在发布时已在项目中使用):
技术类型职称版本号
平台平台杰克11.0.1
程式语言科特林1.3.10
应用框架春季框架5.0.9
春季靴2.0.5
建立系统摇篮5.0
Gradle Kotlin DSL1.0.4
单元测试框架朱尼特5.1.1
春云
单一访问点(API网关)Spring Cloud Gateway包含在发行列车Finchley SR2项目Spring Cloud中
集中配置春季云配置
请求跟踪(分布式跟踪)春云侦探
声明式HTTP客户端Spring Cloud OpenFeign
服务发现Spring Cloud Netflix尤里卡
断路器Spring Cloud Netflix Hystrix
客户端负载均衡Spring Cloud Netflix功能区

该项目包含5个微服务:3个基础结构(配置服务器,服务发现服务器,UI网关)以及前端(项目UI)和后端(项目服务)的示例:


所有这些将在下面按顺序考虑。 显然,在“战斗”项目中,将实现更多实现任何业务功能的微服务。 从技术上讲,将它们添加到相似的体系结构中的方式与Items UI和Items服务相同。

免责声明


本文不考虑用于容器化和编排的工具,因为目前在项目中未使用它们。

配置服务器


Spring Cloud Config用于创建应用程序配置的集中式存储库。 可以从各种来源读取配置,例如,单独的git信息库; 在此项目中,为简单起见,它们位于应用程序资源中:


在这种情况下,配置服务器( application.yml )本身的配置如下所示:

 spring: profiles: active: native cloud: config: server: native: search-locations: classpath:/config server: port: 8888 

使用端口8888允许Config服务器客户端不要在其bootstrap.yml显式指定其端口。 在启动时,他们通过执行GET请求到HTTP API Config服务器来上传配置。

此微服务的程序代码仅由一个文件组成,该文件包含应用程序类的声明和main方法,与等效的Java代码不同,它是顶级函数:

 @SpringBootApplication @EnableConfigServer class ConfigServerApplication fun main(args: Array<String>) { runApplication<ConfigServerApplication>(*args) } 

其他微服务中的应用程序类和主要方法具有相似的外观。

服务发现服务器


服务发现是一种微服务体系结构模式,面对实例数量和网络位置可能发生变化的情况,它使您可以简化应用程序之间的交互。 这种方法的关键组成部分是服务注册中心-微服务,其实例和网络位置的数据库(更多详细信息请参见)。

在此项目中,服务发现是基于作为客户端服务发现的Netflix Eureka实现的:Eureka服务器执行Service Registry的功能,并且Eureka客户端在执行对任何微服务的请求之前,先联系Eureka服务器以获取被调用应用程序的实例列表,并独立执行平衡加载(使用Netflix Ribbon)。 像其他Netflix OSS堆栈组件(例如Hystrix和Ribbon)一样,Netflix Eureka使用Spring Cloud Netflix与Spring Boot应用程序集成。

在位于其资源( bootstrap.yml )中的服务发现服务器配置中,仅指示应用程序的名称和指示在无法连接Config服务器时将中断微服务启动的参数:

 spring: application: name: eureka-server cloud: config: fail-fast: true 

应用程序配置的其余部分位于Config服务器资源中的eureka-server.yml中:

 server: port: 8761 eureka: client: register-with-eureka: true fetch-registry: false 

Eureka服务器使用端口8761,该端口允许所有Eureka客户端不使用默认值指定它。 register-with-eureka (为了清楚起见, register-with-eureka它也是默认使用的, register-with-eureka表示出来)意味着应用程序本身,像其他微服务一样,将在Eureka服务器中注册。 fetch-registry参数确定Eureka客户端是否将从服务注册表中接收数据。

可在http://localhost:8761/找到已注册的应用程序和其他信息的列表:


实施服务发现的替代方法是领事,Zookeeper等。

物品服务


此应用程序是使用REST API的后端的示例,该REST API使用出现在Spring 5中的WebFlux框架(文档在此处 )或更确切地说是Kotlin DSL来实现:

 @Bean fun itemsRouter(handler: ItemHandler) = router { path("/items").nest { GET("/", handler::getAll) POST("/", handler::add) GET("/{id}", handler::getOne) PUT("/{id}", handler::update) } } 

处理收到的HTTP请求将委托给ItemHandlerItemHandler 。 例如,一种用于获取某个实体的对象列表的方法如下所示:

 fun getAll(request: ServerRequest) = ServerResponse.ok() .contentType(APPLICATION_JSON_UTF8) .body(fromObject(itemRepository.findAll())) 

由于存在spring-cloud-starter-netflix-eureka-client项,该应用程序成为Eureka服务器客户端,即,它注册并从Service注册表接收数据。 注册后,应用程序以一定的频率将哈比特比特发送到Eureka服务器,并且如果在一定时间段内Eureka服务器接收的哈比特比特相对于最大可能值的百分比降至某个阈值以下,则该应用将从服务注册表中删除。

考虑将其他元数据发送到Eureka服务器的方法之一:

 @PostConstruct private fun addMetadata() = aim.registerAppMetadata(mapOf("description" to "Some description")) 

通过http://localhost:8761/eureka/apps/items-service通过邮递员,确保Eureka服务器接收到该数据:



物品界面


除了演示与UI网关的交互(将在下一节中显示)之外,此微服务还为Items服务执行前端功能,该功能可以通过几种方式与REST API进行交互:

  1. 使用OpenFeign编写的REST API客户端:

     @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 Bean
    在Java配置中创建一个bin:

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

    并以此方式使用:

     fun requestWithRestTemplate(id: Long): String = restTemplate.getForEntity("http://items-service/items/$id", String::class.java).body ?: "No result" 
  3. WebClientWebClient (特定于WebFlux框架的方法)
    在Java配置中创建一个bin:

     @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来验证所有三个方法返回相同结果的事实:


我更喜欢使用OpenFeign的选项,因为它使开发与被调用的微服务交互的契约成为可能,该契约的实现由Spring承担。 实现该合同的对象像常规bean一样被注入和使用:

 itemsServiceFeignClient.getItem(1) 

如果请求由于某种原因而失败,则将调用实现FallbackFactory接口的类的相应方法,在该方法中,您需要处理错误并返回默认响应(或进一步引发异常)。 如果一定数量的连续呼叫失败,保险丝将断开电路(更多有关此处此处的断路器的信息 ),从而有时间恢复掉落的微服务。

要使用Feign客户端,您需要注释@EnableFeignClients应用程序@EnableFeignClients

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

要在Feign客户端中使用Hystrix后备,您需要在应用程序配置中添加以下内容:

 feign: hystrix: enabled: true 

要在Feign客户端中测试Hystrix fallback的操作,只需转到http://localhost:8081/hystrix-fallback 。 Feign客户端将尝试在Items服务中不存在的路径上执行请求,这将导致返回响应:

 {"error" : "Some error"} 

UI网关


使用API​​网关模式,您可以为其他微服务提供的API创建单个入口点( 此处有更多详细信息)。 实现此模式的应用程序将请求路由(路由)到微服务,还可以执行其他功能,例如身份验证。

在本项目中,为了更加清晰起见,实现了一个UI网关,即,用于不同UI的单个入口点; 显然,网关API的实现方式与此类似。 微服务是在Spring Cloud Gateway框架的基础上实现的。 替代方案是Netflix Zuul,它是Netflix OSS的一部分,并使用Spring Cloud Netflix与Spring Boot集成。
UI网关使用生成的SSL证书(位于项目中)在端口443上运行。 SSL和HTTPS的配置如下:

 server: port: 443 ssl: key-store: classpath:keystore.p12 key-store-password: qwerty key-alias: test_key key-store-type: PKCS12 

用户登录名和密码存储在特定于WebFlux的ReactiveUserDetailsService接口的基于Map的实现中:

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

给定的配置确定部分Web资源(例如,静态资源)可用于所有用户,包括尚未通过身份验证的用户,而其他所有内容( .anyExchange() )都仅通过身份验证。 如果您尝试输入需要身份验证的URL,它将被重定向到登录页面( https://localhost/login ):


该页面使用Bootstrap框架的工具,该工具使用Webjars连接到项目,这使得可以将客户端库作为常规依赖项进行管理。 Thymeleaf用于形成HTML页面。 使用WebFlux配置对登录页面的访问:

 @Bean fun routes() = router { GET("/login") { ServerResponse.ok().contentType(MediaType.TEXT_HTML).render("login") } } 

可以在YAML或Java配置中配置Spring Cloud Gateway路由。 到微服务的路由可以手动分配,也可以基于从服务注册表中接收到的数据自动创建。 在需要路由到足够多的UI的情况下,与Service Registry集成使用将更加方便:

 spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true include-expression: serviceId.endsWith('-UI') url-expression: "'lb:http://'+serviceId" 

include-expression参数的值表示将仅为名称以-UI结尾的微服务创建路由,并且url-expression参数的值是它们可通过HTTP协议访问,这与通过HTTPS起作用的UI网关不同,并且在访问时他们将使用客户端负载平衡(使用Netflix Ribbon实现)。

考虑在Java配置中手动创建路由的示例(不与Service注册表集成):

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

第一条路由路由到先前显示的Eureka服务器主页( http://localhost:8761 ),第二条路由是加载该页面上的资源所必需的。

该应用程序创建的所有路由均位于https://localhost/actuator/gateway/routes

在基础微服务中,可能有必要访问在UI网关中经过身份验证的用户的登录名和/或角色。 为此,我创建了一个过滤器,将适当的标头添加到请求中:

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

现在,让我们使用UI网关https://localhost/items-ui/greeting转到Items UI,正确地假设已经在Items UI中实现了这些标头的处理:


Spring Cloud Sleuth是用于分布式系统中查询跟踪的解决方案。 跟踪ID(传递标识符)和跨度ID(工作标识符)被添加到通过多个微服务的请求的标头中(为了更容易理解,我简化了该方案; 这是更详细的说明):


只需添加spring-cloud-starter-sleuth即可连接此功能。

在指定了适当的日志记录设置之后,在相应的微服务的控制台中,您会看到类似以下的内容(在微服务名称之后显示跟踪ID和跨度ID):

 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" 

对于分布式跟踪的图形表示,您可以使用例如Zipkin,它可以充当服务器来聚合有关来自其他微服务的HTTP请求的信息( 此处有更多详细信息)。

组装方式


根据操作系统,执行gradlew clean build./gradlew clean build

如果可以使用Gradle包装器 ,则无需本地安装Gradle。

构建和随后的启动成功传递给JDK 11.0.1。 在此之前,该项目在JDK 10上运行,因此我认为在此版本上,组装和启动不会有问题。 我没有有关JDK早期版本的数据。 另外,请记住,使用的Gradle 5至少需要JDK 8。

发射


我建议按照本文中描述的顺序启动应用程序。 如果使用启用了运行仪表板的Intellij IDEA,则应获得以下内容:


结论


本文研究了Java世界中当前技术堆栈上的微服务架构示例,其主要组件和某些功能。 我希望对某人有用。 感谢您的关注!

参考文献


Source: https://habr.com/ru/post/zh-CN431474/


All Articles