SpringCloudFeign之声明式服务调用



写在前面

该文参考来自 程序猿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 微服务,我将在整理好后给出地址。

1. 入门

  1. 导入依赖:

    		<dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
    
  2. 程序启动类

    @EnableFeignClients
    @EnableDiscoveryClient
    @SpringBootApplication
    public class ConsumerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ConsumerApplication.class, args);
        }
    }
    
  3. 定义服务接口:

    @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 的值,不区分大小写。

  4. 创建 Controller:

    @RestController
    public class ConsumerController {
    
        @Resource
        private HelloService helloService;
    
        @GetMapping("/feign-consumer")
        public String helloConsumer() {
            return helloService.hello();
        }
    
    }
    

    使用 @Resource 注解,注入上面的 HelloService 实例(这就是动态生成接口的实现)。使用 @Autowired 注解也可以注入,但在使用 intellij 时,会报红,dubbo 的 consumer 注入,也存在这个问题。

  5. 配置文件:

    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 好太多了。


2. 参数绑定

controller 中提供的是一个普通 GET 请求,并未使用任何参数;但实际上,我们在 HelloService 在已经定义了支持参数的服务(服务提供方必须有这些服务),使用 Spring MVC 的注解。不同的是,在 Spring MVC 中参数与方法形参名相同时,是可以省略注解的 value 值的;但在 Feign 中绑定参数必须通过 value 属性来指明具体的参数名。


3. 继承特性

当使用 Spring MVC 的注解来绑定服务接口时,我们完全可以从服务提供方的 Controller 中复制,构建出相应的服务客户端绑定接口。那么是否我们可以抽象,然后避免掉重复的代码呢?Spring Cloud feign 的继承特性就派上了用场,它能够实现 REST 接口定义的复用,避免复制操作。

创建一个 jar包,提供给客户端以及服务提供方使用(共同代码)。将文章上面的 HelloService 移动到 该jar包中,服务提供方创建 controller 类 实现该接口,添加上 RestController 注解。在客户端创建接口,继承 HelloService 接口,并添加上 FeignClient 注解即可。

继承的优点很明显,能够实现接口定义的共享,从而减少服务客户端的绑定配置。但接口在构建期间就建立起了依赖,那么接口的变动就会对项目构建造成影响了。所以我想 它的缺点是因为我们想使用它的优点带来的,我们有必要考虑到这一点。


4. 手动创建客户端

在某些情况下,使用上述方法无法实现的自定义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");
    }
}

这段代码来自官方文档,展示了如何创建具有相同接口的伪客户端,并为每一个客户端配置一个独立的请求拦截器。


5. 服务降级

有点疑惑的是 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 类型的 beanRequestMappingHandlerMapping 的作用,我想大家都知道,它是 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));
	}

可以发现,被当作 handlerbean,是指拥有类级别的 ControllerRequestMapping 注解,所以问题的原因就是 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;
}


6. @SpringQueryMap 注解

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

以上摘抄自官方文档。


7. 配置

feign 的配置是针对 ribbon 和 hystrix 的配置。

7.1 Ribbone 配置

全局的配置使用 ribbon.<key> = <value> 的方式,至于具体的 key-value ,我们一般可以去jar 包的 ...AutoConfiguration 类中找到线索。配置参数的类会被 @ConfigurationProperties注解修饰, value 值代表了其前缀。

除了全局配置以外,还可以指定服务进行配置。格式为 <服务名>.ribbon.<key>=<value>


7.2 Hystrix 配置

默认情况下,Feign 会将所有 Feign 客户端方法都封装到 Hystrix 命令中进行保护。 当然,我们可以使用 feign.hystrix.enabled 来选择是否关闭 Hystrix 功能。

hystrix 的全局配置,使用它的默认配置前缀 hystrix.command.default 即可。

指定命令配置 采用 hystrix.command.<commandKey> 作为前缀。


FeignClient 还支持指定配置类,来覆盖已包含的FeignClientsConfiguration


Feign 还支持压缩和日志的配置,这个就不再详述。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值