系列源码地址
写在前面
该文参考来自 程序猿DD 的Spring Cloud 微服务实战一书,该文是作为阅读了 spring cloud hystrix 一章的读书笔记。书中版本比较老,我选择了最新稳定版的 spring cloud Greenwich.SR2 版本,该版本较书中版本有些变动。非常感谢作者提供了这么好的学习思路,谢谢!文章也参考了 Spring-cloud-netflix 的官方文档。
Feign 可以看作是对 Ribbon 和 Hystrix 两种基础工具更高层次的封装,它整合了 Ribbon 和 Hystrix。
声明性 REST 客户端:Feign 使用 JAX-RS 或者 Spring MVC 注解创建一个接口的动态实现。在 feign 的帮助下,我们只需要创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。
不知道从哪个版本开始,Spring cloud feign
已变为 Spring cloud openfeign
。
源码对应 eureka-service-feign-consumer 微服务,我将在整理好后给出地址。
导入依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
程序启动类
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
定义服务接口:
@FeignClient("hello-service")
@RequestMapping("/provider")
public interface HelloService {
@RequestMapping("/hello")
String hello();
@GetMapping("/hello1")
String hello(@RequestParam("name") String name);
@GetMapping("/hello2")
User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age);
@PostMapping("/hello3")
String hello(@RequestBody User user);
}
我的服务提供类上下文路径为 “/provider” ,所以我需要在接口上加上带有 ‘/provider’ 的 @RequestMapping
注解。@FeignClient
注解的 value 值指的是服务名,也就是提供服务的 spring.application.name
的值,不区分大小写。
创建 Controller:
@RestController
public class ConsumerController {
@Resource
private HelloService helloService;
@GetMapping("/feign-consumer")
public String helloConsumer() {
return helloService.hello();
}
}
使用 @Resource
注解,注入上面的 HelloService
实例(这就是动态生成接口的实现)。使用 @Autowired
注解也可以注入,但在使用 intellij 时,会报红,dubbo 的 consumer 注入,也存在这个问题。
配置文件:
server:
port: 8082
spring:
application:
name: feign-consumer
cloud:
loadbalancer:
retry:
enabled: false
eureka:
client:
service-url:
defaultZone: http://localhost:1112/eureka/
启动程序,访问 controller 提供的 web 接口。你会发现如此的调用服务,可是比之前使用 RestTemplate
好太多了。
controller 中提供的是一个普通 GET 请求,并未使用任何参数;但实际上,我们在 HelloService
在已经定义了支持参数的服务(服务提供方必须有这些服务),使用 Spring MVC 的注解。不同的是,在 Spring MVC 中参数与方法形参名相同时,是可以省略注解的 value 值的;但在 Feign 中绑定参数必须通过 value 属性来指明具体的参数名。
当使用 Spring MVC 的注解来绑定服务接口时,我们完全可以从服务提供方的 Controller 中复制,构建出相应的服务客户端绑定接口。那么是否我们可以抽象,然后避免掉重复的代码呢?Spring Cloud feign 的继承特性就派上了用场,它能够实现 REST 接口定义的复用,避免复制操作。
创建一个 jar包,提供给客户端以及服务提供方使用(共同代码)。将文章上面的 HelloService
移动到 该jar包中,服务提供方创建 controller 类 实现该接口,添加上 RestController
注解。在客户端创建接口,继承 HelloService
接口,并添加上 FeignClient
注解即可。
继承的优点很明显,能够实现接口定义的共享,从而减少服务客户端的绑定配置。但接口在构建期间就建立起了依赖,那么接口的变动就会对项目构建造成影响了。所以我想 它的缺点是因为我们想使用它的优点带来的,我们有必要考虑到这一点。
在某些情况下,使用上述方法无法实现的自定义Feign客户,在这种情况下,可以使用 Feign Builder Api 创建客户端。
@Import(FeignClientsConfiguration.class)
class FooController {
private FooClient fooClient;
private FooClient adminClient;
@Autowired
public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "http://PROD-SVC");
this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "http://PROD-SVC");
}
}
这段代码来自官方文档,展示了如何创建具有相同接口的伪客户端,并为每一个客户端配置一个独立的请求拦截器。
有点疑惑的是 hystrix 提供的服务降级怎么使用呢?很简单,创建一个实体类实现 HelloService 接口,该实体类的方法实现会作为 服务的降级逻辑,该实体类需要注入到 Spring 中。我们还需要在 FeignClient
中指定使用的服务降级逻辑类,通过 fallback
指定即可。
使用这种方式有可能造成 同一类型的 bean 在ApplicationContext 中有多个,这将导致 @Autowired
不能正常工作,因为没有一个bean 被标记为 主 bean(@Primary),但为了解决这个问题,Spring Cloud Netflix将所有佯装实例标记为@Primary,因此Spring框架将知道注入哪个bean。在某些情况下,这可能是不可取的。要关闭此行为,请将@FeignClient 的 primary 设置为false。
2020-5-9 补:
官网有着这样的 demo
,简单明了,可我在使用中仍然得到一个错误,参见如下:
package com.duofei.deal.service.remote;
import com.duofei.deal.bean.GoodsInfo;
import com.duofei.deal.service.remote.fallback.GoodsServiceImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 商品服务
* @author duofei
* @date 2020/5/8
*/
@FeignClient(value = "Q-GOODS", fallback = GoodsServiceImpl.class)
@RequestMapping("/goods")
public interface GoodsService {
@GetMapping("/reduce/{goodsId}")
void goodsReduce(@PathVariable("goodsId") String goodsId, @RequestParam("num") Integer num) throws Exception;
@GetMapping("/query/{id}")
GoodsInfo queryGoodsInfo(@PathVariable("id") String id) throws Exception;
}
备用方法实现类如下:
package com.duofei.deal.service.remote.fallback;
import com.duofei.deal.bean.GoodsInfo;
import com.duofei.deal.service.remote.GoodsService;
import org.springframework.stereotype.Component;
/**
* 商品服务备用方法实现
* @author duofei
* @date 2020/5/8
*/
@Component
public class GoodsServiceImpl implements GoodsService {
@Override
public void goodsReduce(String goodsId, Integer num) throws Exception{
throw new Exception("商品服务忙!稍后重试");
}
@Override
public GoodsInfo queryGoodsInfo(String id) throws Exception{
throw new Exception("商品服务忙!稍后重试");
}
}
这里在
fallback
方法中直接抛出异常,这是一种错误的用法,特在此说明。认真思考以后,我认为应该使用响应实体(code,message,data)来封装,但仍然要面临其他的问题,例如,调用者需要通过响应实体中的信息来判断此次执行成功与否,这或许对你的分布式事务框架也有影响。
不过似乎还有另外一种想法,对于每个远程接口调用,必须有返回值,这样调用者便能根据返回值做相应的处理,但这会带来枯燥且繁琐的工作。
启动时,得到一个无法创建 requestMappingHandlerMapping
bean 的异常,异常信息:
Ambiguous mapping. Cannot map 'com.duofei.deal.service.remote.GoodsService' method
com.duofei.deal.service.remote.GoodsService#goodsReduce(String, Integer)
to {GET /goods/reduce/{goodsId}}: There is already 'goodsServiceImpl' bean method
com.duofei.deal.service.remote.fallback.GoodsServiceImpl#goodsReduce(String, Integer) mapped
大意是在初始化 RequestMappingHandlerMapping
时,检测到重复的映射。首先需要明白的是以上代码会创建两个 GoodsService
类型的 bean
。RequestMappingHandlerMapping
的作用,我想大家都知道,它是 spring MVC
在接收到客户端请求后,寻求处理方法的,那么它为何会将 GoodsService
类型的两个 bean
当作 handler
来处理呢?
RequestMappingHandlerMapping.java
@Override
@SuppressWarnings("deprecation")
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(useSuffixPatternMatch());
this.config.setTrailingSlashMatch(useTrailingSlashMatch());
this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
this.config.setContentNegotiationManager(getContentNegotiationManager());
super.afterPropertiesSet();
}
继续追踪,最后定位到 AbstractHandlerMethodMapping
类的 processCandidateBean
方法:
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
该方法是用来处理候选的 bean
,如果是 Handler
,就会从中检测 HandlerMethod
,那么,我们只要分析其中的 isHandler
方法,
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
可以发现,被当作 handler
的 bean
,是指拥有类级别的 Controller
或 RequestMapping
注解,所以问题的原因就是 GoodsService
添加了类级别的 RequestMapping
注解,被错当作了 Handler
来处理。我本意是想统一公共的路径的,看来不能这样使用,只能将公共路径重复写在每个方法上的 RequestMapping
注解里,修改为以下,就能够正常启动了。
package com.duofei.deal.service.remote;
import com.duofei.deal.bean.GoodsInfo;
import com.duofei.deal.service.remote.fallback.GoodsServiceImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 商品服务
* @author duofei
* @date 2020/5/8
*/
@FeignClient(value = "Q-GOODS", fallback = GoodsServiceImpl.class)
public interface GoodsService {
@GetMapping("/goods/reduce/{goodsId}")
void goodsReduce(@PathVariable("goodsId") String goodsId, @RequestParam("num") Integer num) throws Exception;
@GetMapping("/goods/query/{id}")
GoodsInfo queryGoodsInfo(@PathVariable("id") String id) throws Exception;
}
OpenFeign 的 @QueryMap注释支持将pojo用作GET参数映射。不幸的是,默认的OpenFeign QueryMap注释与Spring不兼容,因为它缺少值属性。
Spring Cloud OpenFeign提供了一个等价的@SpringQueryMap注释,用于将POJO或Map参数注释为查询参数映射。
例如,Params类定义了参数param1和param2:
// Params.java
public class Params {
private String param1;
private String param2;
// [Getters and setters omitted for brevity]
}
下面的feign客户端通过使用@SpringQueryMap注释来使用Params类:
@FeignClient("demo")
public class DemoTemplate {
@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);
}
以上摘抄自官方文档。
feign 的配置是针对 ribbon 和 hystrix 的配置。
全局的配置使用 ribbon.
的方式,至于具体的 key-value ,我们一般可以去jar 包的 ...AutoConfiguration
类中找到线索。配置参数的类会被 @ConfigurationProperties
注解修饰, value 值代表了其前缀。
除了全局配置以外,还可以指定服务进行配置。格式为 <服务名>.ribbon.
。
默认情况下,Feign 会将所有 Feign 客户端方法都封装到 Hystrix 命令中进行保护。 当然,我们可以使用 feign.hystrix.enabled
来选择是否关闭 Hystrix 功能。
hystrix 的全局配置,使用它的默认配置前缀 hystrix.command.default
即可。
指定命令配置 采用 hystrix.command.
作为前缀。
FeignClient
还支持指定配置类,来覆盖已包含的FeignClientsConfiguration
。
Feign 还支持压缩和日志的配置,这个就不再详述。