基于 Netflix Feign 实现,整合了 Spring Cloud ribbon 与 Spring Cloud Hystrix, 除了提供这两者的强大功能之外, 它还提供了一种声明式的 Web 服务客户端定义方式
我们在使用 Spring Cloud ribbon时, 通常都会利用它对 RestTemplate 的请求拦截来实现对依赖服务的接口调用, 而 RestTemplate 已经实现了对 HTTP 请求的封装处理, 形成了一套模板化的调用方法。Spring Cloud Feign 在此基础上做了进一步封装, 由它来帮助我们定义和实现依赖服务接口的定义
在 Spring Cloud Feign 的实现下, 我们只需创建一个接口并用注解的方式来配置它, 即可完成对服务提供方的接口绑定,Spring Cloud Feign 具备可插拔的注解支持,并扩展了对 Spring MVC 的注解支待
快速入门
依赖:
1 | <dependency> |
主类:
1 |
|
依赖服务的接口:
这里服务名不区分大小写, 所以使用 hello-service和HELLO-SERVICE 都是可以的
1 | "HELLO-SERVICE", fallback = HelloServiceFallback.class) (name= |
fallback类:
1 |
|
Controller:
1 |
|
配置文件:
1 | spring.application.name=feign-consumer |
服务端:
1 |
|
启动eruka sever和服务类
访问:http://localhost:9001/feign-consumer
参数绑定
这里 一 定要注意,在定义各参数绑定时,@RequestParam、@RequestHeader 等可以指定参数名称的注解, 它们的 value 千万不能少。 在 SpringMVC 程序中, 这些注解会根据参数名来作为默认值,但是在Feign 中绑定参数必须通过 value 属性来指明具体的参数名,不然会抛出口legalStateException 异常
继承特性
上面的方式,接口代码存在代码复制,下面使用继承特性实现REST接口定义的复用。
为了能够复用 DTO 与接口定义, 我们先创建一个基础的 Maven 工程, 命名为hello-service-api
pom加入依赖:
1 | <properties> |
将上一节中实现的User对象复制到hello-service-api工程中:
1 | public class User { |
创建接口:
1 | "/refactor") ( |
hello-service中加入Controller:
在 Controller 中不再包含以往会定义的请求映射注解@RequestMapping, 而参数的注解定义在重写的时候会自动带过来,只需再增加 @RestController注解使该类成为一个 REST 接口类就大功告成了
1 |
|
feign-consumer的重构:
1 | "HELLO-SERVICE") (value = |
Controller中:
1 | "/feign-consumer3", method = RequestMethod.GET) (value = |
上面方法的缺点:
接口变动就会对项目构建造成影响
Ribbon配置
由于SpringCloudFeign的客户端负载均衡是通过SpringCloudRibbon实现的,所以我们可以直接通过配置沁bbon客户端的方式来自定义各个服务客户端调用的参数:
全局配置
全局配置的方法非常简单, 我们可以直接使用
1 | ribbon.<key>=<value> |
的方式来设置ribbon的各项默认参数。 比如, 修改默认的客户端调用超时时间
指定服务配置
使用@FeignClient(value= “HELLO-SERVICE”)来创 建 Feign客户端的时候 , 同时也创建了 一 个名为HELLO-SERVICE的Ribbon客户端。 既然如此, 我们就可以使用@FeignClient注解中的name或value属性值来设置对应的Ribbon参数
1 | HELLO-SERVICE.ribbon.ConnectTimeout=SOO |
重试机制
Feign默认实现了请求的重试机制,上面客户端的配置内容就是对于请求超时以及重试机制配置的详情。Ribbon 的超时与Hystrix 的超时是两个概念。 为了让上述实现有效, 我们需要 让Hystrix的超时时间大于Ribbon的超时时间, 否则Hystrix命令超时后, 该命令直接熔断, 重试机制就没有任何意义了
Hystrix配置
默认情况下,Spring CloudFeign会为将所有Feign客户端的方法都封装到Hystrix命令中进行服务保护
全局配置
直接使用它的 默认配置前缀hys七豆x.command.defau巨 就可以进行设置, 比如设置全局的超时时间:
1 | hystrix.command.default.execution.isolation.thread.七imeoutinMilliseconds=5OOO |
另外,在对Hystrix进行配置之前,我们需要确认 feign.hystrix.enabled参数没有被设置为false
禁用Hystrix
如果只想针对某个 服务客户端关闭Hystrix支待时, 需要通过使用@Scope (“prototype”)注解为指定的客户端配置Feign.Builder实例:
该类需要加入@Configuration注解
在HelloService的 @ FeignClient注解中,通过configuration参数引入上面实现的配置
指定命令配置
配置方法也跟传统的 Hystrix 命令的参数配置相似, 采用hystrix.command.\
1 | hystrix.command.hello.execution.isolation.thread.timeoutinMilliseconds=5OOO |
在使用指定命令配置的时候, 需要注意, 由于方法名很有可能重复, 这个时候相同方法名的Hystrix配置会共用,所以在进行方法定义与配置的时候需要做好一定的规划。当然,也可以重写Feign.Builder的实现,并在应用主类中创建它的实例来覆盖自动化配置的HystrixFeign.Builder实现
服务降级配置
服务降级逻辑的实现只需要为 Feign 客户端的定义接口编写一个具体的接口实现类。比如为 HelloService 接口实现一个服务降级类 HelloServiceFallback:
1 |
|
使用:
其他配置
见书
API网关服务:Spring Cloud Zuul
在本章中, 我们将把视线聚焦在对外服务这块内容, 通常也称为边缘服务。为了保证对外服务的安全性, 我们在服务端实现的微服务接口, 往往都会有一定的权限校验机制, 比如对用户登录状态的校验等; 同时为了防止客户端在发起请求时被篡改等安全方面的考虑, 还会有一些签名校验的机制存在,因此API网关的概念应运而生。
API网关是一个更为智能的应用服务器, 它的定义类似于面向对象设计模式中的Facade模式, 它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、 负载均衡、 校验过滤等功能之外, 还需要更多能力, 比如与服务治理框架的结合、 请求转发时的熔断机制、 服务的聚合等 一系列高级功能
SpringCloud中了提供了基于NetflixZuul实现的API网关组件SpringCloudZuul。首先, 对于路由规则与服务实例的维护间题。 Spring CloudZuul通过与Spring Cloud Eureka进行整合, 将自身注册为Eureka服务治理下的应用, 同时从Eureka中获得了所有其他微服务的实例信息。 这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来, 使得将维护服务实例的工作交给了服务治理框架自动完成, 不再需要人工介入。 而对于路由规则的维护, Zuul默认会将通过以服务名作为ContextPath的方式来创建路由映射,大部分情况下, 这样的默认设置已经可以实现我们大部分的路由需求, 除了一些特殊情况(比如兼容一些老的URL)还需要做 一些特别的配置 。 但是相比于之前架构下的运维工作量, 通过引入SpringCloudZuul实现API网关后, 已经能够大大减少了
其次, 对于类似签名校验、 登录校验在微服务架构中的冗余问题,API网关服务上进行统一调用来对微服务接口做前置过滤, 以实现对微服务接口的拦截和校验
快速入门
创建api-gateway服务,依赖如下:
1 | <dependencies> |
zuul对其他组件的依赖:
主类:
1 |
|
配置文件:
1 | spring.application.name=api-gateway |
然后我们启动eruka和服务端以及消费端:
请求路由
传统路由配置
使用Spring CloudZuul实现路由功能非常简单, 只需要对 api-gateway服务增加一些关于路由规则的配置, 就能实现传统的路由转发功能, 比如:
1 | zuul.routes.api-a-url.path=/api-a-url/** |
该配置定义了发往API 网关服务的请求中, 所有符合 /api-a-url/规则的访问都将被路由转发到http://localhost:8080/地址上, 也就是 说, 当我们访问http://localhost:5555/api-a-url/hello的时候, API网关服务会将该请求路由到http://localhost: 8080/hello 提供的微服务接口上。 其中 , 配置属性zuul.routes.api-a-url.path中的api-a-url部分为路由的名字, 可以任意定义,但是 一组path和url 映射关系的路由名要相同,下面将要介绍的面向服务的映射方式也是如此
面向服务的路由:
具体的url则交给Eureka的服务发现机制去自动维护:
1 | zuul.routes.api-a.path=/api-a/** |
我们启动相关的服务:
然后可以访问下面的链接
1 | http://localhost:5555/api-a/hello |
请求过滤
Zuul 允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承 ZuulFilter 抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。下面你是一个过滤器的案例,用来检查HttpServletReque江中是否有 accessToken 参数:
1 | public class AccessFilter extends ZuulFilter { |
在实现了自定义过滤器之后, 它并不会直接生效, 我们还需要为其创建具体的Bean才能启动该过滤器,在主类中:
1 |
|
验证:直接访问http://localhost:5555/api-a/hello会失败,http://localhost:5555/api-a/hello?accessToken=token访问会成功。
网关的总结:
它作为系统的统一入口, 屏蔽了系统内部各个微服务的细节
它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发
它可以实现接口权限校验与微服务业务逻辑的解耦
通过服务网关中的过炖器, 在各生命周期中去校验请求的内容, 将原本在对外服务层做的校验前移, 保证了微服务的无状态性, 同时降低了微服务的测试难度, 让服务本身更集中关注业务逻辑的处理
路由详解
传统路由配置
所谓的传统路由配置方式就是在不依赖于服务发现机制的情况下。没有Eureka等服务治理框架的帮助,我们需要 根据服务实例的数量采用不同方式的配置来实现路由规则
单实例配置:
1 | zuul.routes.user-service.path=/user-service/** |
该配置实现了对符合/user-service/** 规则的请求路径转发到 http://localhost:8080/ 地址的路由规则
多实例配置:
serviceId是人工指定的,配合listOfServers来指定服务实例。Zuul自带了ribbon的依赖,只需要做一些配置即可,下面是一些配置的介绍
服务路由配置
Zuul通过整合Eureka,实现了对实例的自动化配置。所以在使用服务路由配置的时候,我们不需要向传统路由配置方式那样为serviceId指定具体的服务实例地址:
1 | ## user-service为服务名称 |
也可以简单写成:
zuul.routes.user-service=/user-service/**
传统路由的映射方式比较直观且容易理解, API网关直接根据请求的URL路径找到最匹配的 path表达式, 直接转发给该表达式对应的 url或对应 serviceId下配置的实例地址, 以实现外部请求的路由.
由于Zuul整合了Eureka,实际上,我们可以直接将API网关也看作Eureka服务治理下的 一个普通微服务应用。它会从注册中心获取所有服务以及它们的实例清单。所以,在Eureka的帮助下,API网关服务本身就已经维护 了系统中所有 serviceId与实例地址的映射关系
服务路由的默认规则
在实际的运用过程中会发现, 大部分的路由配置规则几乎都会采用服务名作为外部请求的前缀,就像前面配置的/user-service。Zuul默认实现了这样的贴心功能, 当我们为Zuul构建的 API 网关服务引入Eureka之后, 它为Eureka中的每个服务都自动创建一个默认路由规则, 这些默认规则的path会使用service Id配置的服务名作为请求前缀
我们可以使用zuul.ignored-services参数来设置一个服务名匹配表达式来定义不自动创建路由的规则。 设置为
zuul.ignored-services=*的时候,Zuul将对所有的服务都不自动创建路由规则。 在这种情况下,我们就要在配置文件中逐个为需要路由的服务添加映射规则
自定义路由映射规则
假设服务遵循userservice-v1命名方式,同时路径希望是/v1/userservice/**的方式,我们可以自定义服务和路由映射的关系:
第一个参数是用来匹配服务名称是否符合该自定义规则的正则表达式
第二个参数则是定义根据服务名中定义的内容转换出的路径表达式规则
路径匹配
路由匹配的路径表达式采用了Ant风格定义
有时会出现一个路径包含另一个路径的情况,比如如下场景:
但实际上上面的配置不能保证/ext能匹配到下面的路径,这取决于配置文件对路由的加载顺序。由于properties的配置内容无法保证有序,所以当出现这种情况的时候, 为了保证路由的优先顺序, 我们需要使用YAML文件来配置
忽略表达式
该参数可以用来设置不希望被 API 网关进行路由的 URL表达式
路由前缀
通过zuul.prefix 参数来进行设置,用来设置统一的前缀
本地跳转
在 Zuul 实现的 API 网关路由功能中, 还支持 forward 形式的服务端跳转配置。在 Zuul 实现的 API 网关路由功能中, 还支持 forward 形式的服务端跳转配置:
api-b请求转发到API网关中以/local 为前缀的请求上
Cookie与头信息
默认情况下, Spring Cloud Zuul在请求路由时, 会过滤掉HTTP请求头信息中的 一些敏感信息, 防止它们被传 递到下游的外部服 务器。 默认的敏感头信息通过zuul.sensitiveHeaders参数定义,包括Cookie、Set-Cookie、Authorization三个属性
如果我们要将使用了Spring Security、 Shiro等安全框架构建的Web应用通过SpringCloud Zuul构建的网关来进行路由时,由于Cookie信息无法传递, 我们的Web应用将无法实现登录和鉴权。 为了解决这个问题, 配置的方法有很多
重定向问题:
Zuul中增加了 一个参数配置,能够使得网关在进行路由转发前为请求设置Host头信息,以标识最初的服务端请求地址。 具体配置方式如下
zuul.addHostHeader=true
Hystrix和Ribbon支持
Zuul包含了hystrix和ribbon的依赖,所以 Zuul天生就拥有线程隔离和断路器的自我保护功能
但是需要注意, 当使用pa七h与url的映射关系来配置路由规则的时候, 对于路由转发的请求不会采用Hys七rixCommand来包装, 所以这类路由请求没有线程隔离和断路器的保护, 并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合来进行配置, 这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能
我们在使用Zuul搭建API网关的时候,可以通过Hystrix和Ribbon的参数来调整路由请求的各种超时时间等配置
过滤器详解
我们对于Zuul的第 一 印象通常是这样的:它包含了对请求的路由和过滤两个功能:
路由功能负责将外部请求转发到具体的微服务实例上, 是实现外部访问统一入口的基础;
而过滤器功能则负责对请求的处理过程进行干预, 是实现请求校验、 服务聚合等功能的基础。
然而实际上, 路由功能在真正运行时, 它的路由映射和请求转发都是由几个不同的过滤器完成的。
路由映射主要通过pre类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;
而请求转发的部分则是由route类型的过滤器来完成,对pre类型过滤器获得的路由地址进行转发。
所以,过滤器可以说是Zuul实现API网关功能最为核心的部件,每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端
过滤器必须包含4个基本特征: 过滤类型、 执行顺序、执行条件、 具体操作.实际上它就是ZuulFi丘er接口中定义的4个抽象方法:
请求生命周期
核心过滤器
默认启用 的过滤器中包含三种不同生命周期的过滤器, 这些过滤器都非常重要,可以帮助我们理解Zuul对外部请求处理的过程
异常处理
案例:
1 |
|
显然上面的写法不够优雅,并且并可避免其他异常抛出,因此可以自定义异常处理器:
由于在请求生命周期的pre、route、 post三个阶段中有异常抛出的时候都会进入error阶段的处理, 所以可以通过创建一个error 类型的过滤器来捕获这些异常信息,并根据这些异常信息在请求上下文中注入需要返回给客户端的错误描述
1 | /** |
异常处理的不足
异常处理的源码如下:
可以看到,error类型的过滤器处理完毕之后, 除了来自 post阶段的异常之外, 都会再被post过滤器进行处理。对于从 post 过滤器中抛出异常的清况, 在经过error过滤器处理之后, 就没有其他类型的过滤器来接手了,这就是不足之处
回想 一下之前实现的两种异常处理方法, 其中非常核心的一点是, 这两种处理方法都在异常处理时向请求上下文中添加了一系列的 error.*参数,而这些参数真正起作用的地方是在 post阶段的SendErrorFilter, 在该过滤器中会使用这些参数来组织内容返回给客户端。由于在post阶段抛出异常,error过滤器不会被SendErrorFilter消费输出。所以如果我们在自定义post过滤器的时候, 没有正确处理异常,就依然有可能出现日志中没有异常但请求响应内容为空的问题。
解决上述问题的方法有很多种,最直接的是我们可以在实现 error过滤器的时候,直接组织结果返回就能实现效果。但是这样做的缺点也很明显, 对于错误信息组织和返回的代码实现会存在多份, 这样非常不利于日后的代码维护工作。 所以为了保持对异常返回处理逻辑的 一致性,我们还是希望将 post过滤器抛出的异常交给SendErrorFilter来处理,我们可以继承该处理器,并修改其类型为error:
1 | /** |
为了扩展过滤器的处理逻辑, 为请求上下文增加 一些自定义属性, 我们需要深入了解Zuul过滤器的核心处理器: com.netflix.zuul.FilterProcessor 。 该类中定义 了下面列出的过滤器调用和处理相关的核心方法
根据之前的设计, 可以直接扩展FilterProcessor当过滤器执行抛出异常的时候, 我们捕获它,并向请求上下中记录一些信息。 比如下面的具体实现:
1 | public class DidiFilterProcessor extends FilterProcessor { |
此外需要在主类的main方法中调用setProcessor设置自定义的filter:
1 | @EnableZuulProxy |
自定义异常处理
使用 SendErrorFilter 来处理异常返回的话,我们要如何定制返回的响应结果呢?这个 时候,我们的关注点就不能放在 Zuul 的过滤器上了, 因为错误信息的生成实际上并不是由 Spring Cloud Zuul完成的。 我们在介绍
SendErrorFilter 的时候提到过, 它会根据请求上下中保存的错误信息来组织一个forward 到/error 端点的请求来获取错误响应, 所以我们的扩展目标转移到了对/error端点的实现
它的实现非常简单,通过调用 getErrorAttributes 方法来根据请求参数组织错误信息的返回结果,其实现类为DefautErrorAttributes对象,该对象仅在没有 ErrorAttributes 接口的实例时才会被创建来使用,所以我们只需要自己编写 一个自定义的 ErrorAttributes 接口实现类,并创建它的实例就能替代这个默认的实现, 从而达到自定义错误信息的效果了:
1 | public class DidiErrorAttributes extends DefaultErrorAttributes { |
为了使该类生效,还需要在主类中配置:
1 |
|
禁用过滤器
不论是核心过滤器还是自定义过滤器, 只要在API网关应用中为它们创建了实例, 那么默认情况下, 它们都是启用状态的
Zuul中特别提供了 一个参数来禁用指定的过滤器, 该参数的配置格式如下:
1 | zuul.<SimpleClassName>.<filterType>.disable=true |
动态加载
通过Zuul实现的API网关服务当然也具备了动态路由和动态过滤器的能力。我们可以在不重启 API网关服务的前提下, 为其动态修改路由规则和添加或删除过滤器
动态路由
对于如何实现Zuul的动态路由,我们很自然地会将它与SpringCloud Config的动态刷新机制联系到一起。只需将API网关服务的配置文件通过Spring Cloud Config连接的Git仓库存储和管理, 我们就能轻松实现动态刷新路由规则的功能。构建过程如下:
引入依赖:
1 | <dependencies> |
引入配置文件:bootstrap.properties
1 | spring.application.name=api-gateway |
主类:
1 |
|
创建config-repo目录,并添加配置文件:api-gateway.properties
对于 API 网关服务在 Git 仓库中的配置文件名称完全取决于网关应用配置文件,bootstrap.properties中 spring.application.name 属性的配置值
动态过滤器
Spring Cloud Config
Spring Cloud Config 是 Spring Cloud 团队创建的一个全新项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持, 它分为服务端与客户端两个部分。 其中服务端也称为分布式配置中心, 它是一个独立的微服务应用, 用来连接配置仓库并为客户端提供获取配置信息、 加密/解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施, 它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息