教你如何在SpringMVC項目中單獨使用Feign組件(含源碼分析) 頂 原

需求

在項目中,經常有基於Restful格式的接口需要調用,特別是遠程調用。做法有多種,例如:自己手寫http請求接口、使用Spring的RestTemplate進行遠程調用等。得益於SpringCloud組件的Feign組件,有了一種易於上手,忽略請求細節的選擇方案。

我們當前有很多應用是基於SpringMVC開發的,而目前線上大部分集成Feign的解決方案都是基於SpringCloud,至少基於SpringBoot的場景。(參考網上的案例,自己整理了可行方案)

源碼分析

【源碼】SynchronousMethodHandler、Client類(含解釋)

每次通過業務代碼調用接口的時候,都會調用:

/*
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package feign;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;

import feign.InvocationHandlerFactory.MethodHandler;
import feign.Request.Options;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;

import static feign.FeignException.errorExecuting;
import static feign.FeignException.errorReading;
import static feign.Util.checkNotNull;
import static feign.Util.ensureClosed;

final class SynchronousMethodHandler implements MethodHandler {

  private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L;

  private final MethodMetadata metadata;
  private final Target<?> target;
  private final Client client;
  private final Retryer retryer;
  private final List<RequestInterceptor> requestInterceptors;
  private final Logger logger;
  private final Logger.Level logLevel;
  private final RequestTemplate.Factory buildTemplateFromArgs;
  private final Options options;
  private final Decoder decoder;
  private final ErrorDecoder errorDecoder;
  private final boolean decode404;

//創建實例,同時根據容器配置的參數進行加載,此處可以在Register(後面有代碼)中個性化配置
  private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer,
                                   List<RequestInterceptor> requestInterceptors, Logger logger,
                                   Logger.Level logLevel, MethodMetadata metadata,
                                   RequestTemplate.Factory buildTemplateFromArgs, Options options,
                                   Decoder decoder, ErrorDecoder errorDecoder, boolean decode404) {
    this.target = checkNotNull(target, "target");
    this.client = checkNotNull(client, "client for %s", target);
    this.retryer = checkNotNull(retryer, "retryer for %s", target);
    this.requestInterceptors =
        checkNotNull(requestInterceptors, "requestInterceptors for %s", target);
    this.logger = checkNotNull(logger, "logger for %s", target);
    this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
    this.metadata = checkNotNull(metadata, "metadata for %s", target);
    this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target);
    this.options = checkNotNull(options, "options for %s", target);
    this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target);
    this.decoder = checkNotNull(decoder, "decoder for %s", target);
    this.decode404 = decode404;
  }

//方法調用時執行的代碼,其中executeAndDecode是最重要的,就是基於http的請求以及數據格式轉碼
  @Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

//源碼請求發送和接收處理類
  Object executeAndDecode(RequestTemplate template) throws Throwable {
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      response = client.execute(request, options);
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    boolean shouldClose = true;
    try {
      if (logLevel != Logger.Level.NONE) {
        response =
            logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
      }
      if (Response.class == metadata.returnType()) {
        if (response.body() == null) {
          return response;
        }
        if (response.body().length() == null ||
                response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          return response;
        }
        // Ensure the response body is disconnected
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        return Response.create(response.status(), response.reason(), response.headers(), bodyData);
      }
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          return decode(response);
        }
      } else if (decode404 && response.status() == 404) {
        return decoder.decode(response, metadata.returnType());
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }

  long elapsedTime(long start) {
    return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
  }

  Request targetRequest(RequestTemplate template) {
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
    return target.apply(new RequestTemplate(template));
  }

  Object decode(Response response) throws Throwable {
    try {
      return decoder.decode(response, metadata.returnType());
    } catch (FeignException e) {
      throw e;
    } catch (RuntimeException e) {
      throw new DecodeException(e.getMessage(), e);
    }
  }

  static class Factory {

    private final Client client;
    private final Retryer retryer;
    private final List<RequestInterceptor> requestInterceptors;
    private final Logger logger;
    private final Logger.Level logLevel;
    private final boolean decode404;

    Factory(Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors,
            Logger logger, Logger.Level logLevel, boolean decode404) {
      this.client = checkNotNull(client, "client");
      this.retryer = checkNotNull(retryer, "retryer");
      this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
      this.logger = checkNotNull(logger, "logger");
      this.logLevel = checkNotNull(logLevel, "logLevel");
      this.decode404 = decode404;
    }

    public MethodHandler create(Target<?> target, MethodMetadata md,
                                RequestTemplate.Factory buildTemplateFromArgs,
                                Options options, Decoder decoder, ErrorDecoder errorDecoder) {
      return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
                                          logLevel, md, buildTemplateFromArgs, options, decoder,
                                          errorDecoder, decode404);
    }
  }
}

【源碼】Client客戶端請求類

/*
 * Copyright 2013 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package feign;

import static java.lang.String.format;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;

import feign.Request.Options;

import static feign.Util.CONTENT_ENCODING;
import static feign.Util.CONTENT_LENGTH;
import static feign.Util.ENCODING_DEFLATE;
import static feign.Util.ENCODING_GZIP;

/**
 * Submits HTTP {@link Request requests}. Implementations are expected to be thread-safe.
 */
public interface Client {

  /**
   * Executes a request against its {@link Request#url() url} and returns a response.
   *
   * @param request safe to replay.
   * @param options options to apply to this request.
   * @return connected response, {@link Response.Body} is absent or unread.
   * @throws IOException on a network error connecting to {@link Request#url()}.
   */
  Response execute(Request request, Options options) throws IOException;

  public static class Default implements Client {

    private final SSLSocketFactory sslContextFactory;
    private final HostnameVerifier hostnameVerifier;

    /**
     * Null parameters imply platform defaults.
     */
    public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
      this.sslContextFactory = sslContextFactory;
      this.hostnameVerifier = hostnameVerifier;
    }

    @Override
    public Response execute(Request request, Options options) throws IOException {
      HttpURLConnection connection = convertAndSend(request, options);
      return convertResponse(connection);
    }

//此處封裝與請求有關的參數信息
    HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
      final HttpURLConnection
          connection =
          (HttpURLConnection) new URL(request.url()).openConnection();
      if (connection instanceof HttpsURLConnection) {
        HttpsURLConnection sslCon = (HttpsURLConnection) connection;
        if (sslContextFactory != null) {
          sslCon.setSSLSocketFactory(sslContextFactory);
        }
        if (hostnameVerifier != null) {
          sslCon.setHostnameVerifier(hostnameVerifier);
        }
      }
      connection.setConnectTimeout(options.connectTimeoutMillis());
      connection.setReadTimeout(options.readTimeoutMillis());
      connection.setAllowUserInteraction(false);
      connection.setInstanceFollowRedirects(true);
      connection.setRequestMethod(request.method());

      Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
      boolean
          gzipEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
      boolean
          deflateEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);

      boolean hasAcceptHeader = false;
      Integer contentLength = null;
      for (String field : request.headers().keySet()) {
        if (field.equalsIgnoreCase("Accept")) {
          hasAcceptHeader = true;
        }
        for (String value : request.headers().get(field)) {
          if (field.equals(CONTENT_LENGTH)) {
            if (!gzipEncodedRequest && !deflateEncodedRequest) {
              contentLength = Integer.valueOf(value);
              connection.addRequestProperty(field, value);
            }
          } else {
            connection.addRequestProperty(field, value);
          }
        }
      }
      // Some servers choke on the default accept string.
      if (!hasAcceptHeader) {
        connection.addRequestProperty("Accept", "*/*");
      }

      if (request.body() != null) {
        if (contentLength != null) {
          connection.setFixedLengthStreamingMode(contentLength);
        } else {
          connection.setChunkedStreamingMode(8196);
        }
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
        if (gzipEncodedRequest) {
          out = new GZIPOutputStream(out);
        } else if (deflateEncodedRequest) {
          out = new DeflaterOutputStream(out);
        }
        try {
          out.write(request.body());
        } finally {
          try {
            out.close();
          } catch (IOException suppressed) { // NOPMD
          }
        }
      }
      return connection;
    }

    Response convertResponse(HttpURLConnection connection) throws IOException {
      int status = connection.getResponseCode();
      String reason = connection.getResponseMessage();

      if (status < 0) {
        throw new IOException(format("Invalid status(%s) executing %s %s", status,
            connection.getRequestMethod(), connection.getURL()));
      }

      Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>();
      for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
        // response message
        if (field.getKey() != null) {
          headers.put(field.getKey(), field.getValue());
        }
      }

      Integer length = connection.getContentLength();
      if (length == -1) {
        length = null;
      }
      InputStream stream;
      if (status >= 400) {
        stream = connection.getErrorStream();
      } else {
        stream = connection.getInputStream();
      }
      return Response.create(status, reason, headers, stream, length);
    }
  }
}

如何使用

通過對Feign底層源碼的分析,大致關鍵點如下:

  1. 容器啓動的時候動態掃描class文件,然後動態創建bean,並且注入到Spring容器中;
  2. 容器啓動的同時,初始化Feign.Builder中的參數配置,例如是否在每次請求添加前置過濾等(後面有代碼示例);
  3. 每次發送請求的的時候,通過Builder對象找到接口配置信息,並使用invoke動態調用接口數據,Feign會先構建Request對象,其中包括URL組裝,前置處理代碼如何集成等;
  4. Feign底層實際上是使用的HttpConnection的方式進行遠程調用;

添加包依賴

    compile 'com.netflix.feign:feign-core:8.18.0'
    compile 'com.netflix.feign:feign-jackson:8.18.0'
    compile 'io.github.lukehutch:fast-classpath-scanner:2.18.1'

添加註解類

package com.chz.apps.web.bean;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Feign接口標記註解
 * @Author gongstring([email protected])
 * @website http://www.gongstring.com
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeignApi {

    /**
     * 調用的服務地址
     * @return
     */
    String serviceUrl();
}

添加接口類(示例)

package com.cxkh.apps.userinfo.client.apis;

import com.chz.apps.common.vo.DicVo;
import com.chz.apps.web.bean.FeignApi;
import com.chz.apps.web.bean.ReqBean;
import com.chz.apps.web.bean.RespBean;
import com.cxkh.apps.userinfo.client.vo.CorpBean;
import feign.Headers;
import feign.RequestLine;

import java.util.List;

@FeignApi(serviceUrl = "http://dev.xxxx.com/xxxx/remote")
public interface UserInfoApi {

    @Headers({"Content-Type: application/json","Accept: application/json"})
    @RequestLine("POST /dictinary/all")
    RespBean<List<DicVo>> getDics(ReqBean<List<String>> reqBean);
}


添加Spring Bean加載類

package com.chz.apps.web.plugin;

import com.chz.apps.common.cache.LocalCache;
import com.chz.apps.common.redisson.RedissionTools;
import com.chz.apps.common.tools.HttpConstant;
import com.chz.apps.web.bean.FeignApi;
import com.chz.apps.web.bean.FeignConstant;
import com.chz.component.basic.tools.PackageTools;
import feign.Feign;
import feign.Request;
import feign.Retryer;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

import java.util.Set;

/**
 * Feign容器初始化類,用於動態加載Bean
 * @Author gongstring([email protected])
 * @website http://www.gongstring.com
 */
@Component
public class FeignClientRegister implements BeanFactoryPostProcessor{

    //掃描的接口路徑
    private String  scanPath="com.chz.apps.**.apis,com.cxkh.apps.**.apis";

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        Set<String> classes = PackageTools.findPackageClass(scanPath);
        if(classes==null){
            return ;
        }
        Feign.Builder builder = getFeignBuilder();
        //動態創建bean,並注入到Spring容器中
        if(classes.size()>0){
            for (String claz : classes) {
                Class<?> targetClass = null;
                try {
                    targetClass = Class.forName(claz);
                    String url=targetClass.getAnnotation(FeignApi.class).serviceUrl();
                    if(url.indexOf("http://")!=0){
                        url="http://"+url;
                    }
                    Object target = builder.target(targetClass, url);
                    beanFactory.registerSingleton(targetClass.getName(), target);
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage());
                }
            }
        }
    }

    public Feign.Builder getFeignBuilder(){
        //設置請求超時時間(從配置文件中讀取,否則使用默認配置)
        Object feignConnectTimeoutMillis = LocalCache.getInstance().getValue(FeignConstant.OPTIONS_CONNECTTIMEOUTMILLIS,1000);
        Object feignReadTimeoutMillis = LocalCache.getInstance().getValue(FeignConstant.OPTIONS_READTIMEOUTMILLIS,3500);

        Feign.Builder builder = Feign.builder()
                //使用Jackson進行參數處理,如果有必要可以自行定義
                .encoder(new JacksonEncoder())
                .decoder(new JacksonDecoder())
                //超時處理
                .options(new Request.Options(Integer.parseInt(feignConnectTimeoutMillis.toString()), Integer.parseInt(feignReadTimeoutMillis.toString())))
                .retryer(new Retryer.Default(5000, 5000, 3))
                //每次請求時,自定義內部請求頭部信息,例如:權限相關的信息
                .requestInterceptor(template -> {
                    template.header(HttpConstant.REQUEST_HEADER_REMOTE_TOKEN_KEY, RedissionTools.getSingleTokenKey());
                  template.header(HttpConstant.REQUEST_HEADER_REMOTE_FLAG,HttpConstant.REQUEST_HEADER_REMOTE_VALUE);
                });
        return builder;
    }

}

在代碼中調用Api

@Autowired
private UserInfoApi userInfoApi;

基於Spring底層源碼的包掃描工具類

package com.chz.component.basic.tools;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.SystemPropertyUtils;

public class PackageTools {
    private static final Log log = LogFactory.getLog(PackageTools.class);
    protected static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    public PackageTools() {
    }

    public static Set<Method> findClassAnnotationMethods(String scanPackages, Class<? extends Annotation> annotation) {
        Set<String> clazzSet = findPackageClass(scanPackages);
        Set<Method> methods = new HashSet();
        Iterator var4 = clazzSet.iterator();

        while(var4.hasNext()) {
            String clazz = (String)var4.next();

            try {
                Set<Method> ms = findAnnotationMethods(clazz, annotation);
                if (ms != null) {
                    methods.addAll(ms);
                }
            } catch (ClassNotFoundException var7) {
                ;
            }
        }

        return methods;
    }

    public static Set<String> findPackageClass(String scanPackages) {
        if (StringUtils.isBlank(scanPackages)) {
            return Collections.EMPTY_SET;
        } else {
            Set<String> packages = checkPackage(scanPackages);
            ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
            MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
            Set<String> clazzSet = new HashSet();
            Iterator var5 = packages.iterator();

            while(true) {
                String basePackage;
                do {
                    if (!var5.hasNext()) {
                        return clazzSet;
                    }

                    basePackage = (String)var5.next();
                } while(StringUtils.isBlank(basePackage));

                String packageSearchPath = "classpath*:" + ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage)) + "/" + "**/*.class";

                try {
                    Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
                    Resource[] var9 = resources;
                    int var10 = resources.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        Resource resource = var9[var11];
                        String clazz = loadClassName(metadataReaderFactory, resource);
                        clazzSet.add(clazz);
                    }
                } catch (Exception var14) {
                    log.error("獲取包下面的類信息失敗,package:" + basePackage, var14);
                }
            }
        }
    }

    private static Set<String> checkPackage(String scanPackages) {
        if (StringUtils.isBlank(scanPackages)) {
            return Collections.EMPTY_SET;
        } else {
            Set<String> packages = new HashSet();
            Collections.addAll(packages, scanPackages.split(","));
            String[] var2 = (String[])packages.toArray(new String[packages.size()]);
            int var3 = var2.length;

            for(int var4 = 0; var4 < var3; ++var4) {
                String pInArr = var2[var4];
                if (!StringUtils.isBlank(pInArr) && !pInArr.equals(".") && !pInArr.startsWith(".")) {
                    if (pInArr.endsWith(".")) {
                        pInArr = pInArr.substring(0, pInArr.length() - 1);
                    }

                    Iterator<String> packageIte = packages.iterator();
                    boolean needAdd = true;

                    while(packageIte.hasNext()) {
                        String pack = (String)packageIte.next();
                        if (pInArr.startsWith(pack + ".")) {
                            needAdd = false;
                        } else if (pack.startsWith(pInArr + ".")) {
                            packageIte.remove();
                        }
                    }

                    if (needAdd) {
                        packages.add(pInArr);
                    }
                }
            }

            return packages;
        }
    }

    private static String loadClassName(MetadataReaderFactory metadataReaderFactory, Resource resource) throws IOException {
        try {
            if (resource.isReadable()) {
                MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
                if (metadataReader != null) {
                    return metadataReader.getClassMetadata().getClassName();
                }
            }
        } catch (Exception var3) {
            log.error("根據resource獲取類名稱失敗", var3);
        }

        return null;
    }

    public static Set<Method> findAnnotationMethods(String fullClassName, Class<? extends Annotation> anno) throws ClassNotFoundException {
        Set<Method> methodSet = new HashSet();
        Class<?> clz = Class.forName(fullClassName);
        Method[] methods = clz.getDeclaredMethods();
        Method[] var5 = methods;
        int var6 = methods.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method method = var5[var7];
            if (method.getModifiers() == 1) {
                Annotation annotation = method.getAnnotation(anno);
                if (annotation != null) {
                    methodSet.add(method);
                }
            }
        }

        return methodSet;
    }

    public static void main(String[] args) {
        String packages = "com.a,com.ab,com.c,com.as.t,com.as,com.as.ta,com.at.ja,com.at.jc,com.at.";
        System.out.println("檢測前的package: " + packages);
        System.out.println("檢測後的package: " + StringUtils.join(checkPackage(packages), ","));
    }
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章