feign使用及原理剖析
一、简介
Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。
Feign远程调用,核心就是通过一系列的封装和处理,将以JAVA注解的方式定义的远程调用API接口,最终转换成HTTP的请求形式,然后将HTTP的请求的响应结果,解码成JAVA Bean,返回给调用者。
二、http client依赖
Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。
可以通过修改 client 依赖换用底层的 client,不同的 http client 对请求的支持可能有差异。
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>2.2.2.RELEASE</version>
</dependency><!--使用Apache HttpClient-->
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId><version>11.0</version>
</dependency>
#在配置文件中启用ApacheHttpClient
feign.httpclient.enabled=true
三、注解
1、@FeignClient
public @interface FeignClient {/***具有可选协议前缀的服务的名称。无论是否提供url,都必须为所有客户端指定名称。*/@AliasFor("name")String value() default "";// 过时的@DeprecatedString serviceId() default "";/*** 当存在多个FeignClient调用同一个服务时,需要填写,否则无法启动*/String contextId() default "";// 指定FeignClient的名称@AliasFor("value")String name() default "";//返回值:外部客户端的@Qualifier值String qualifier() default "";// 全路径地址或hostname,http或https可选String url() default "";// 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException异常boolean decode404() default false;// Feign配置类,可以自定义Feign的LogLevelClass<?>[] configuration() default {};// 容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑Class<?> fallback() default void.class;// 工厂类,用于生成fallback类实例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码Class<?> fallbackFactory() default void.class;// 定义当前FeignClient的统一前缀,类似于controller类上的requestMappingString path() default "";//是否将外部代理标记为主beanboolean primary() default true;
}
2、@EnableFeignClients
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {//等价于basePackages属性,更简洁的方式String[] value() default {};//指定多个包名进行扫描String[] basePackages() default {};//指定多个类或接口的class,扫描时会在这些指定的类和接口所属的包进行扫描Class<?>[] basePackageClasses() default {};//为所有的Feign Client设置默认配置类Class<?>[] defaultConfiguration() default {};//指定用@FeignClient注释的类列表。如果该项配置不为空,则不会进行类路径扫描Class<?>[] clients() default {};
}
四、原理
- 启动时,若有@EnableFeignClients注解,则程序会进行包扫描,扫描所有包下所有@FeignClient注解的类,并将这些类注入到spring的IOC容器中。
- 当定义的@FeignClient中的接口被调用时,通过JDK的动态代理来生成RequestTemplate。RequestTemplate中包含请求的所有信息,如请求参数,请求URL等。
- RequestTemplate生成Request,然后将Request交给client处理,client默认是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
- 最后client封装成LoadBaLanceClient,结合ribbon负载均衡地发起调用。
五、流程
流程图如下:
从上图可以看到,Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的 Request 请求。通过Feign以及JAVA的动态代理机制,使得Java 开发人员,可以不用通过HTTP框架去封装HTTP请求报文的方式,完成远程服务的HTTP调用。
1、启用
启动配置上检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描被@FeignClient注解的接口。
扫描出该注解后, 通过beanDefinition注入到IOC容器中,方便后续被调用使用。
@EnableFeignClients 是关于注解扫描的配置,使用了@Import(FeignClientsRegistrar.class)。在spring context处理过程中,这个Import会在解析Configuration的时候当做提供了其他的bean definition的扩展,Spring通过调用其registerBeanDefinitions方法来获取其提供的bean definition。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {//在这个重载的方法里面做了两件事情://1.将EnableFeignClients注解对应的配置属性注入//2.将FeignClient注解对应的属性注入@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {//注入EnableFeignClients注解对应的配置属性registerDefaultConfiguration(metadata, registry);//注入FeignClient注解对应的属性registerFeignClients(metadata, registry);}}
FeignClientsRegistrar里重写了spring里ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法。也就是在启动时,处理了EnableFeignClients注解后,registry里面会多出一些关于Feign的BeanDefinition。
BeanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回的是FeignClientFactoryBean类。
FeignClientFactoryBean作为一个实现了FactoryBean的工厂类,那么每次在Spring Context 创建实体类的时候会调用它的getObject()
方法。
这里的getObject()
其实就是将@FeinClient
中设置value值进行组装起来。
public Object getObject() throws Exception {FeignContext context = applicationContext.getBean(FeignContext.class);Feign.Builder builder = feign(context);if (!StringUtils.hasText(this.url)) {String url;if (!this.name.startsWith("http")) {url = "http://" + this.name;}else {url = this.name;}url += cleanPath();return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url));}if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {this.url = "http://" + this.url;}String url = this.url + cleanPath();Client client = getOptional(context, Client.class);if (client != null) {if (client instanceof LoadBalancerFeignClient) {// not lod balancing because we have a url,// but ribbon is on the classpath, so unwrapclient = ((LoadBalancerFeignClient)client).getDelegate();}builder.client(client);}Targeter targeter = get(context, Targeter.class);return targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url));
}
2、发起请求
ReflectiveFeign内部使用了jdk的动态代理为目标接口生成了一个动态代理类,这里会生成一个InvocationHandler统一的方法拦截器,同时为接口的每个方法生成一个SynchronousMethodHandler拦截器,并解析方法上的元数据,生成一个http请求模板RequestTemplate。
查看ReflectiveFeign
类中newInstance
方法是返回一个代理对象:
这个方法大概的逻辑是:
- 根据target,解析生成MethodHandler对象;
- 对MethodHandler对象进行分类整理,整理成两类:default 方法和 SynchronousMethodHandler 方法;
- 通过jdk动态代理生成代理对象,这里是最关键的地方;
- 将DefaultMethodHandler绑定到代理对象。
public class ReflectiveFeign extends Feign {@Overridepublic <T> T newInstance(Target<T> target) {//为每个方法创建一个SynchronousMethodHandler对象,并放在 Map 里面Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();for (Method method : target.type().getMethods()) {if (method.getDeclaringClass() == Object.class) {continue;} else if (Util.isDefault(method)) {//如果是 default 方法,说明已经有实现了,用 DefaultHandlerDefaultMethodHandler handler = new DefaultMethodHandler(method);defaultMethodHandlers.add(handler);methodToHandler.put(method, handler);} else {//否则就用上面的 SynchronousMethodHandlermethodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));}}/** * 设置拦截器* 创建动态代理,factory 是 InvocationHandlerFactory.Default,创建出来的是 * ReflectiveFeign.FeignInvocationHanlder,也就是说后续对方法的调用都会进入到该对象的 inovke 方* 法*/ InvocationHandler handler = factory.create(target, methodToHandler);// 创建动态代理对象T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),new Class<?>[] {target.type()}, handler);for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {defaultMethodHandler.bindTo(proxy);}return proxy;}}
最终都是执行了SynchronousMethodHandler
拦截器中的invoke
方法:
final class SynchronousMethodHandler implements MethodHandler {@Overridepublic Object invoke(Object[] argv) throws Throwable {// 根据输入参数,构造Http请求RequestTemplate template = buildTemplateFromArgs.create(argv);// 克隆出一份重试器Retryer retryer = this.retryer.clone();// 尝试最大次数,如果中间有结果,直接返回while (true) {try {return executeAndDecode(template);} catch (RetryableException e) {try {retryer.continueOrPropagate(e);} catch (RetryableException th) {Throwable cause = th.getCause();if (propagationPolicy == UNWRAP && cause != null) {throw cause;} else {throw th;}}if (logLevel != Logger.Level.NONE) {logger.logRetry(metadata.configKey(), logLevel);}continue;}}}
}
invoke
方法方法首先生成 RequestTemplate 对象,应用 encoder,decoder 以及 retry 等配置,下面有一个死循环调用:executeAndDecode,从名字上看就是执行调用逻辑并对返回结果解析。
Object executeAndDecode(RequestTemplate template) throws Throwable {//根据 RequestTemplate生成Request对象Request request = targetRequest(template);if (logLevel != Logger.Level.NONE) {logger.logRequest(metadata.configKey(), logLevel, request);}Response response;long start = System.nanoTime();try {// 调用client对象的execute()方法执行http调用逻辑,//execute()内部可能设置request对象,也可能不设置,所以需要response.toBuilder().request(request).build();这一行代码response = client.execute(request, options);// ensure the request is set. TODO: remove in Feign 10response.toBuilder().request(request).build();} catch (IOException e) {if (logLevel != Logger.Level.NONE) {logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));}// IOException的时候,包装成 RetryableException异常,上面的while循环 catch里捕捉的就是这个异常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);// ensure the request is set. TODO: remove in Feign 10response.toBuilder().request(request).build();}//如果元数据返回类型是 Response,直接返回回去即可,不需要decode()解码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 disconnectedbyte[] bodyData = Util.toByteArray(response.body().asInputStream());return response.toBuilder().body(bodyData).build();}//主要对2xx和404等进行解码,404需要特别的开关控制。其他情况,使用errorDecoder进行解码,以异常的方式返回if (response.status() >= 200 && response.status() < 300) {if (void.class == metadata.returnType()) {return null;} else {return decode(response);}} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {return decode(response);} 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());}}
}
Feign真正发送HTTP请求是委托给feign.Client的execute
方法来做的:
public interface Client {Response execute(Request request, Options options) throws IOException;class Default implements Client {@Overridepublic Response execute(Request request, Options options) throws IOException {HttpURLConnection connection = convertAndSend(request, options);return convertResponse(connection, request);}}
}
注意:SynchronousMethodHandler并不是直接完成远程URL的请求,而是通过负载均衡机制,定位到合适的远程server服务器,然后再完成真正的远程URL请求。即:SynchronousMethodHandler实例的client成员,其实际不是feign.Client.Default类型,而是LoadBalancerFeignClient客户端负载均衡类型。
3、性能分析
Feign框架比较小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理Http请求的环节。可以从这个方面着手分析系统的性能提升点。
六、总结
1、调用接口为什么会直接发送请求?
原因就是Spring扫描了@FeignClient注解,并且根据配置的信息生成代理类,调用的接口实际上调用的是生成的代理类。
2、请求是如何被Feign接管的?
- Feign通过扫描@EnableFeignClients注解中配置包路径,扫描@FeignClient注解并将注解配置的信息注入到Spring容器中,类型为FeignClientFactoryBean;
- 然后通过FeignClientFactoryBean的getObject()方法得到不同动态代理的类并为每个方法创建一个SynchronousMethodHandler对象;
- 为每一个方法创建一个动态代理对象, 动态代理的实现是 ReflectiveFeign.FeignInvocationHanlder,代理被调用的时候,会根据当前调用的方法,转到对应的 SynchronousMethodHandler;
- 这样我们发出的请求就能够被已经配置好各种参数的Feign handler进行处理,从而被Feign托管。
七、简单入门
一、示例
1.引入依赖
<!-- spring-cloud-starter-openfeign 支持负载均衡、重试、断路器等 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>2.2.2.RELEASE</version>
</dependency>
<!--使用Apache HttpClient-->
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId><version>11.0</version>
</dependency>
2.添加配置
#配置文件启用ApacheHttpClient
feign.httpclient.enabled=true
3.开启支持
@SpringBootApplication
@EnableFeignClients
public class FeignProjectApplication {public static void main(String[] args) {SpringApplication.run(FeignProjectApplication.class, args);}}
4.编写接口
①单服务项目
@FeignClient(value = "demo", url = "http://localhost:8081/")
public interface UserCenter {/*** 获取用户信息* @param uid* @return*/@PostMapping("/user/getUser/{uid}")User getUser(@PathVariable(value = "uid") Integer uid);
}
②微服务项目
@FeignClient(name="user-center", // 微服务名称url="${feign.service.user:user-center}", // 微服务的服务名,用来定位到要调用哪个服务,可在配置文件中填写contextId = "user-center", // 当有多个FeignClient调用同一个微服务时需要填写,否则会无法启动path="/user", // 固定的一个path,用于拼接整个urlfallback = UserCenterImpl.class // 熔断类,当请求超时或其他原因时,会调用熔断类里的方法,防止调用方请求过久
)
public interface UserCenter {/*** 获取用户信息* @param uid* @return*/@PostMapping("/getUser/{uid}")User getUser(@PathVariable(value = "uid") Integer uid);
}
③熔断类
@Service
public class UserCenterImpl implements UserCenter {@Overridepublic User getUser(Integer uid) {// 直接返回nullreturn null;}
}
5.配置自动添加token
@Configuration
public class FeignInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();requestTemplate.header("Authorization", request.getHeader("Authorization"));
// requestTemplate.header("Authorization", "1kmhZwomS6LSQKXQKNjRibRORrCZnsnrTU9CcBGkQJ3DGL1soxIWegq/vF3UXdEm");}
}
6.编写controller接口
@CrossOrigin
@RequestMapping("/usercenter")
@RestController
public class UserController {@Autowiredprivate UserCenter userCenter;@PostMapping("/getUser/{uid}")public User getUser(@PathVariable(value = "uid") Integer uid){return userCenter.getUser(uid);}
}
7.接口请求结果
{"data": {"uid": 1001,"name": "张三",},"statusText": "查找成功","status": 200
}
二、一些其他配置
1.FormEncoder支持
@Configuration
public class FeignFormConfiguration {@Autowiredprivate ObjectFactory<HttpMessageConverters> messageConverters;@Bean@Primarypublic Encoder feignFormEncoder() {return new FormEncoder(new SpringEncoder(this.messageConverters));}
}
2.拦截器: 自动添加header或者token等
@Configuration
public class FeignInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();requestTemplate.header("Authorization", request.getHeader("Authorization"));// requestTemplate.header("Authorization", "1kmhZwomS6LSQKXQKNjRibRORrCZnsnrTU9CcBGkQJ3DGL1soxIWegq/vF3UXdEm");}
}
3.ErrorCode: 可以自定义错误响应码的处理
1.引入依赖
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.2</version>
</dependency>
2.配置自定义异常
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TyaleErrorException extends Exception{/*** example: "./api/{service-name}/{problem-id}"*/private String type;/*** example: {title}*/private String title;/*** example: https://api/docs/index.html#error-handling*/private String documentation;/*** example: {code}*/private String status;
}
3.配置工具类
public class GsonUtil {private static Gson filterNullGson;private static Gson nullableGson;static {nullableGson = new GsonBuilder().enableComplexMapKeySerialization().serializeNulls().setDateFormat("yyyy-MM-dd HH:mm:ss:SSS").create();filterNullGson = new GsonBuilder().enableComplexMapKeySerialization().setDateFormat("yyyy-MM-dd HH:mm:ss:SSS").create();}protected GsonUtil() {}/*** 根据对象返回json 不过滤空值字段*/public static String toJsonWtihNullField(Object obj){return nullableGson.toJson(obj);}/*** 根据对象返回json 过滤空值字段*/public static String toJsonFilterNullField(Object obj){return filterNullGson.toJson(obj);}/*** 将json转化为对应的实体对象* new TypeToken<HashMap<String, Object>>(){}.getType()*/public static <T> T fromJson(String json, Type type){return nullableGson.fromJson(json, type);}/*** 将对象值赋值给目标对象* @param source 源对象* @param <T> 目标对象类型* @return 目标对象实例*/public static <T> T convert(Object source, Class<T> clz){String json = GsonUtil.toJsonFilterNullField(source);return GsonUtil.fromJson(json, clz);}
}
4.自定义错误码
@Configuration
public class TyaleErrorDecoder implements ErrorDecoder {@Overridepublic Exception decode(String s, Response response) {TyaleErrorException errorException = null;try {if (response.body() != null) {Charset utf8 = StandardCharsets.UTF_8;var body = Util.toString(response.body().asReader(utf8));errorException = GsonUtil.fromJson(body, TyaleErrorException.class);} else {errorException = new TyaleErrorException();}} catch (IOException ignored) {}return errorException;}
}
4.自定义错误码
@Configuration
public class TyaleErrorDecoder implements ErrorDecoder {@Overridepublic Exception decode(String s, Response response) {TyaleErrorException errorException = null;try {if (response.body() != null) {Charset utf8 = StandardCharsets.UTF_8;var body = Util.toString(response.body().asReader(utf8));errorException = GsonUtil.fromJson(body, TyaleErrorException.class);} else {errorException = new TyaleErrorException();}} catch (IOException ignored) {}return errorException;}
}