需求
在項目中,經常有基於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底層源碼的分析,大致關鍵點如下:
- 容器啓動的時候動態掃描class文件,然後動態創建bean,並且注入到Spring容器中;
- 容器啓動的同時,初始化Feign.Builder中的參數配置,例如是否在每次請求添加前置過濾等(後面有代碼示例);
- 每次發送請求的的時候,通過Builder對象找到接口配置信息,並使用invoke動態調用接口數據,Feign會先構建Request對象,其中包括URL組裝,前置處理代碼如何集成等;
- 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), ","));
}
}