OpenFeign时序图 Feign的基本调用时序图。不过这篇文章不涉及LoadBalance部分。

OpenFeign基本使用

使用SpringBoot+OpenFeign来做HTTP调用,在代码上非常简洁:

/**
 * 天气预报
 *
 * @see <a href="https://www.sojson.com/api/weather.html">天气预报接口</a>
 */
@FeignClient(name = "weatherService", url = "http://t.weather.sojson.com")
public interface WeatherService {

    /**
     * 天气预报
     *
     * @param cityCode 城市代码
     * @return 天气预报
     * @see <a href="http://cdn.sojson.com/_city.json?attname=">城市代码列表</a>
     */
    @GetMapping(path = "/api/weather/city/{cityCode}")
    WeatherDto forecast(@PathVariable(name = "cityCode") String cityCode);
}

问题

但是,公司网络上做了安全控制:线上环境中,内网服务器不能直接访问外网地址,而必须通过公司配置的代理才能访问。因此,我们需要先为上面的FeignClient配置上代理,然后才能上线。

方案一

OpenFeign本身没有代理相关的配置;要使用代理,需要结合HttpClient来配置。以SpringBoot下的OKHttp为例,我们需要在application.yml中做如下配置:

feign:
  okhttp:
    enabled: true
  httpclient:
    enabled: false

并注入以下的bean:

// Bean配置
@Bean
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties,
                                               OkHttpClientConnectionPoolFactory connectionPoolFactory) {
    Integer maxTotalConnections = httpClientProperties.getMaxConnections();
    Long timeToLive = httpClientProperties.getTimeToLive();
    TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
    return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
}

@Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool,
                                   FeignHttpClientProperties httpClientProperties) {

    Boolean followRedirects = httpClientProperties.isFollowRedirects();
    Integer connectTimeout = httpClientProperties.getConnectionTimeout();
    Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
    this.okHttpClient = httpClientFactory
            .createBuilder(disableSslValidation)
            .connectTimeout((long) connectTimeout, TimeUnit.MILLISECONDS)
            .followRedirects(followRedirects)
            .connectionPool(connectionPool)
            .proxy(new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8081)))
            .build();

    log.info("自定义OkHttpClient, proxySelector:{}", okHttpClient.proxySelector());
    return this.okHttpClient;
}

@PreDestroy
public void destroy() {
    if (this.okHttpClient != null) {
        this.okHttpClient.dispatcher().executorService().shutdown();
        this.okHttpClient.connectionPool().evictAll();
    }
}

@Bean
public Client feignClient(){
    return new OkHttpClient(okHttpClient);
}

实际上,上面这些bean都是从FeignAutoConfiguration.OkHttpFeignConfiguration中“copy”过来的。这个配置类中的代码是这样的:

@Configuration
@ConditionalOnClass({OkHttpClient.class})
@ConditionalOnMissingClass({"com.netflix.loadbalancer.ILoadBalancer"})
@ConditionalOnMissingBean({okhttp3.OkHttpClient.class})
@ConditionalOnProperty({"feign.okhttp.enabled"})
protected static class OkHttpFeignConfiguration {
    private okhttp3.OkHttpClient okHttpClient;

    protected OkHttpFeignConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean({ConnectionPool.class})
    public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
        Integer maxTotalConnections = httpClientProperties.getMaxConnections();
        Long timeToLive = httpClientProperties.getTimeToLive();
        TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
        return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
    }

    @Bean
    public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
        Boolean followRedirects = httpClientProperties.isFollowRedirects();
        Integer connectTimeout = httpClientProperties.getConnectionTimeout();
        Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
        this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation).connectTimeout((long)connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects).connectionPool(connectionPool).build();
        return this.okHttpClient;
    }

    @PreDestroy
    public void destroy() {
        if (this.okHttpClient != null) {
            this.okHttpClient.dispatcher().executorService().shutdown();
            this.okHttpClient.connectionPool().evictAll();
        }

    }

    @Bean
    @ConditionalOnMissingBean({Client.class})
    public Client feignClient(okhttp3.OkHttpClient client) {
        return new OkHttpClient(client);
    }
}

两相对比一下,会发现我们自定义的Bean其实只有这一个地方不一样:

this.okHttpClient = httpClientFactory
            .createBuilder(disableSslValidation)
            .connectTimeout((long) connectTimeout, TimeUnit.MILLISECONDS)
            .followRedirects(followRedirects)
            .connectionPool(connectionPool)
            // 这里不一样:增加了一个代理配置
            .proxy(new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8081)))
            .build();

按SpringBoot的一贯做法,应该在某个Properties类(比如FeignHttpClientProperties中)提供一个配置项,然后在application.yml中增加对应的配置就可以了。但是……我没有找到只加配置、不动代码的方式,所以就只能通过代码实现了。

补充说明

顺带我们可以看看OkHttpClient的Builder中都有什么东西,以备以后还需要添加什么功能时使用。大部分配置项都可以“顾名思义”,不过具体用法么,有空再琢磨吧:

public static final class Builder {
  Dispatcher dispatcher;
  Proxy proxy;
  List<Protocol> protocols;
  List<ConnectionSpec> connectionSpecs;
  final List<Interceptor> interceptors = new ArrayList<>();
  final List<Interceptor> networkInterceptors = new ArrayList<>();
  ProxySelector proxySelector;
  CookieJar cookieJar;
  Cache cache;
  InternalCache internalCache;
  SocketFactory socketFactory;
  SSLSocketFactory sslSocketFactory;
  CertificateChainCleaner certificateChainCleaner;
  HostnameVerifier hostnameVerifier;
  CertificatePinner certificatePinner;
  Authenticator proxyAuthenticator;
  Authenticator authenticator;
  ConnectionPool connectionPool;
  Dns dns;
  boolean followSslRedirects;
  boolean followRedirects;
  boolean retryOnConnectionFailure;
  int connectTimeout;
  int readTimeout;
  int writeTimeout;
  int pingInterval;
 
  // 略去后面的代码 
}

方案二

本来到这里,OpenFeign+OkHttp配置代理的工作就完成了。不过,方案一的配置会导致由Feign发出的所有Http请求都使用代理。虽然访问外网时有必要这样做,但是在访问内网时,这就有点多此一举了。

所以,我换了个方案。

在OkHttpClient的Builder中,有一个ProxySelector proxySelector。这是一个抽象类,其作用可以参见它的javadoc:

/**
 * Selects the proxy server to use, if any, when connecting to the
 * network resource referenced by a URL. A proxy selector is a
 * concrete sub-class of this class and is registered by invoking the
 * {@link java.net.ProxySelector#setDefault setDefault} method. The
 * currently registered proxy selector can be retrieved by calling
 * {@link java.net.ProxySelector#getDefault getDefault} method.
 *
 * <p> When a proxy selector is registered, for instance, a subclass
 * of URLConnection class should call the {@link #select select}
 * method for each URL request so that the proxy selector can decide
 * if a direct, or proxied connection should be used. The {@link
 * #select select} method returns an iterator over a collection with
 * the preferred connection approach.
 *
 * <p> If a connection cannot be established to a proxy (PROXY or
 * SOCKS) servers then the caller should call the proxy selector's
 * {@link #connectFailed connectFailed} method to notify the proxy
 * selector that the proxy server is unavailable. </p>
 *
 * <P>The default proxy selector does enforce a
 * <a href="doc-files/net-properties.html#Proxies">set of System Properties</a>
 * related to proxy settings.</P>
 *
 * @author Yingxian Wang
 * @author Jean-Christophe Collet
 * @since 1.5
 */
public abstract class ProxySelector {
    // 略
}

简单说,它的作用就如它的类名一样:为一个请求选择使用什么代理。这个功能正适合我们的需求,因此我新增了这样一个类:

public class ProxySelectorImpl extends ProxySelector {

    private Proxy proxy = new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8081));

    @Override
    public List<Proxy> select(URI uri) {

        List<Proxy> proxies;
        // 这里的写法值得商榷,只做参考
        if (uri.toString().startsWith("http://t.weather.sojson.com/")) {
            proxies = null;
        } else {
            proxies = Arrays.asList(proxy);
        }
        log.info("使用自定义的代理选择器,uri:{}, proxies:{}", uri, proxies);
        return proxies;
    }

    @Override
    public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
        log.error("uri:{}, sa:{}, ", uri, sa, ioe);
    }

}

继承ProxySelector类只需要重写两个方法。select(URI uri)方法就是用来选择代理的;另一个方法connectFailed(URI uri, SocketAddress sa, IOException ioe)则用来在连接失败之后做一些善后处理。

定义好这个类之后,把自定义配置中的proxy换成proxySelector就可以了:

this.okHttpClient = httpClientFactory
        .createBuilder(disableSslValidation)
        .connectTimeout((long) connectTimeout, TimeUnit.MILLISECONDS)
        .followRedirects(followRedirects)
        .connectionPool(connectionPool)
        // 这里不一样:不直接使用代理,而是通过代理选择器来选择
        // .proxy(new Proxy(Proxy.Type.HTTP,new InetSocketAddress("127.0.0.1",8081)))
        .proxySelector(proxySelector)
        .build();

这样,就可以做到只有访问htp://t.weather.sojson.com/才使用代理、访问其它地址时则直接连接了。

hou'ji

OpenFeign的设计上有很多值得借鉴之处。例如分层——数据解析、负载、熔断、连接等,都由不同层次上的类进行管理。又如策略——OpenFeign只提供了feign.Client这一个接口,底层却能通过DefaultClient、ApacheHTTPClient、OkHttpClient和LoadBalancerFeignClient或来提供不同的服务,这一点和Slf4j日志框架很像。还有装饰者——LoadBalancerFeignClient就是通过装饰者模式来在基础的连接管理功能上提供负载均衡能力的。还有……

这些设计思想可以用在我们的业务系统中吗?我们的人脸识别服务接入了不止一家供应商,这不是天然的策略模式应用场景吗?我们在每一次查询到订单后都要检查它是否过期、是否需要修复上次上线遗留的历史数据,这不是装饰者可以发挥作用的地方吗?更不要说Controller-Service-Dao这样的层次结构了。

但是,大家的业务系统使用了这些设计思想吗?为什么呢?