gongstring技术博客
最新文章
源码解读
软件安装
常见问题
大数据
常用工具
鸡汤文
备案号:鄂ICP备15015839号-1
鄂公网安备 42010202001692号
教你如何在SpringMVC项目中单独使用Feign组件(含源码分析)
2018-05-30 10:27:21
作者: gongstring
源码解读
/
教你如何在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
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
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
requestInterceptors; private final Logger logger; private final Logger.Level logLevel; private final boolean decode404; Factory(Client client, Retryer retryer, List
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
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
> headers = new LinkedHashMap
>(); for (Map.Entry
> 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(gongstring@foxmail.com) * @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
> getDics(ReqBean
> 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(gongstring@foxmail.com) * @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
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
findClassAnnotationMethods(String scanPackages, Class extends Annotation> annotation) { Set
clazzSet = findPackageClass(scanPackages); Set
methods = new HashSet(); Iterator var4 = clazzSet.iterator(); while(var4.hasNext()) { String clazz = (String)var4.next(); try { Set
ms = findAnnotationMethods(clazz, annotation); if (ms != null) { methods.addAll(ms); } } catch (ClassNotFoundException var7) { ; } } return methods; } public static Set
findPackageClass(String scanPackages) { if (StringUtils.isBlank(scanPackages)) { return Collections.EMPTY_SET; } else { Set
packages = checkPackage(scanPackages); ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver); Set
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
checkPackage(String scanPackages) { if (StringUtils.isBlank(scanPackages)) { return Collections.EMPTY_SET; } else { Set
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
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
findAnnotationMethods(String fullClassName, Class extends Annotation> anno) throws ClassNotFoundException { Set
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), ",")); } } ```