Spring Cloud 之 OpenFeign 的使用

在spring cloud 体系中我们一般使用openfeign来实现服务间的访问,比如用户服务访问日志服务记录用户的一些操作。
我们选择openfeign的需要原因是openfeign作为spring cloud的亲儿子可以和spring cloud 完美集成,然后openfeign通过简单的注解就可以实现接口的定义和访问。

比如如下的一个接口定义

@FeignClient("service-auth/api")
public interface AddressServiceClient {

    @GetMapping(value = "/address/findByIds")
    List findByIds(@RequestParam("ids") String[] ids);
}

@FeignClient注解声明这个接口是一个REST客户端,里面的方法定义和@RestController中定义接口的方式一致
@FeignClient中有好几个属性可以定义,其实默认的name属性是必填的,表示Feign Client的名称,如果项目使用了 Ribbon,name属性会作为微服务的名称,用于服务发现,另外还有一个url属性,启用这个属性后,name属性还是必须要填写但已无用,url的值表示为具体的服务地址,可能是一个ip地址也可能是一个域名。
比如@FeignClient(name="baidu",url="www.baidu.com").在内部处理逻辑中,url和name的值如果不是http开头的,会自动加上http,如果是https的则需要主动加上去了。
具体的源码在FeignClientFactoryBean.class

 T getTarget() {
        FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class);
        Builder builder = this.feign(context);
        //this.url 指的是url属性的值 
        if (!StringUtils.hasText(this.url)) {
            // url的值将会变成name属性的值
            if (!this.name.startsWith("http")) {
                  // 判断url是不是http开头的 不是的话 补上协议头
                this.url = "http://" + this.name;
            } else {
                this.url = this.name;
            }
            this.url += cleanPath();
            // 如果url是空的 则通过name获取一个负载均衡后的客户端实例
            return (T) loadBalance(builder, context,
					new HardCodedTarget<>(this.type, this.name, this.url));
        } else {
            if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
                this.url = "http://" + this.url;
            }

            String url = this.url + this.cleanPath();
            Client client = (Client)this.getOptional(context, Client.class);
            if (client != null) {
                if (client instanceof LoadBalancerFeignClient) {
		    // not load balancing because we have a url,
		    // but ribbon is on the classpath, so unwrap
                    client = ((LoadBalancerFeignClient)client).getDelegate();
                }

                builder.client(client);
            }
            
            Targeter targeter = (Targeter)this.get(context, Targeter.class);
            return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
        }
    }

另外比较有用的属性比如@decode404,表示遇到了404问题是直接抛出FeignException还是调用feign的decode去处理异常。@path用于定义所有方法映射的统一前缀。


另外使用openfeign,需要在入口函数所在类加上@EnableFeignClients注解启用openfeign.
这个注解开启后,会自动扫描使用了@FeignClient的接口。


在实际使用的时候,可能会遇到一个问题,访问其他服务的时候,目标服务会返回401错误,调试的时候你会发现,openfeign调用的时候,request中并不含浏览器给出的认证信息,也就是request中的header信息丢了。
此时,你需要重新构造一个RequestInterceptor接口的实现。这是openfeign中给出的request的拦截器接口。接口定义了一个 void apply(RequestTemplate template);.
我们需要对这个template执行一些操作,让他带上原本的header。

    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            // 从spring bean容器中拿到当前请求的request
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
            // 给requestTemplate重新赋值
            requestTemplate.header("Authorization", request.getHeader("Authorization"));
            requestTemplate.header("Cookie", request.getHeader("Cookie"));
        };
    }

最后,说一下feign的实现原理,简单的将就是java动态代理的应用。我们都知道接口是不能实例化的,声明的接口在运行时会被动态代理。我们常用的maybatis也是如此。

Spring Cloud 之 OpenFeign 的使用-我的技术分享
被代理的每一个方法都指代了一个访问地址,这些方法都会被转化为具体的RequestTemplate实现,默认会使用httpclient实现具体的请求,当然你也可以引入feign-okhttp这个包实现底层通过okhttp访问。
最后,如果开启了负债均衡,则还会通过负载均衡选取目标服务。