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 "";
// 过时的
@Deprecated
String 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的LogLevel
Class<?>[] configuration() default {};
// 容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑
Class<?> fallback() default void.class;
// 工厂类,用于生成fallback类实例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
Class<?> fallbackFactory() default void.class;
// 定义当前FeignClient的统一前缀,类似于controller类上的requestMapping
String path() default "";
//是否将外部代理标记为主bean
boolean 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注解对应的属性注入
@Override
public 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 unwrap
client = ((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 {
@Override
public <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 方法,说明已经有实现了,用 DefaultHandler
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
//否则就用上面的 SynchronousMethodHandler
methodToHandler.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 {
@Override
public 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 10
response.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 10
response.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 disconnected
byte[] 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 {
@Override
public 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,用于拼接整个url
fallback = UserCenterImpl.class // 熔断类,当请求超时或其他原因时,会调用熔断类里的方法,防止调用方请求过久
)
public interface UserCenter {
/**
* 获取用户信息
* @param uid
* @return
*/
@PostMapping("/getUser/{uid}")
User getUser(@PathVariable(value = "uid") Integer uid);
}
③熔断类
@Service
public class UserCenterImpl implements UserCenter {
@Override
public User getUser(Integer uid) {
// 直接返回null
return null;
}
}
5.配置自动添加token
@Configuration
public class FeignInterceptor implements RequestInterceptor {
@Override
public 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 {
@Autowired
private 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 {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
@Primary
public Encoder feignFormEncoder() {
return new FormEncoder(new SpringEncoder(this.messageConverters));
}
}
2.拦截器: 自动添加header或者token等
@Configuration
public class FeignInterceptor implements RequestInterceptor {
@Override
public 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 {
@Override
public 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;
}
}