一、Feign实现远程调用的注解和配置
首先对于微服务之间的相互调用,如果微服务B可以单独启动起来,并注册到注册中心。那么我们在微服务A的启动类上加@EnableFeignClients注解,这就表明当前应用服务A中有的地方想要引用其他微服务(微服务B)中的接口。如下图所示,在启动类和接口中分别加上@EnableFeignClients注解和@FeignClient注解,就能实现远程微服务的调用:
后端接收前端传输的消息,需要使用到@RequestMapping接收映射的URI路径,使用@RequestParam和@RequestVariable和@RequestBody接收参数,对于多个参数的URI,可以使用@RequestParam接收参数,对于单个参数可以不用加注解,直接在方法中使用同名参数接收即可。对于/***/{}的URI,需要使用@RequestVariable接收参数。对于对象的传递,使用@RequestBody接收。
对于Restful风格的资源处理,GET、PUT、POST、DELETE等价于资源处理的select,update,insert,delete四种方式。restful的四种注解,@GetMapping,@PostMapping,@PutMapping,@DeleteMapping等价于@RequestMapping(method = RequestMethod.GET/POST/PUT/DELETE)
@FeignClient标签中的一些属性的作用如下所示:
value:服务名(接口提供方的服务名)
name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
url:url一般用于调试,可以手动指定@FeignClient调用的地址
decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
configuration:Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
fallback:定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
fallbackFactory:工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
path:定义当前FeignClient的统一前缀
对于注册发现的微服务架构而言,fegin接口下的方法对应的URL是通过value对应的微服务的ip+端口号的地址+URL映射路径访问对应功能的。如下图所示,通过service-product微服务的对应IP地址和端口加上/product/select的URL进而访问对应的功能。
二、JDK的动态代理
代理模式分为静态代理和动态代理两种方式。其中静态代理是指代理关系在编译期确定的代理模式,使用静态代理时,常见的作法是为每个业务类抽象一个接口,对应地创建一个代理类。动态代理是指代理关系在运行时确定的代理模式,动态代理的核心API是Proxy类和InvocationHandler接口,他的原理是利用反射机制在运行时生成代理类的字节码。动态代理的使用场景是当一个对象不能直接使用,可以在客户端和目标对象创建一个中介,这个中介就是代理。
三、Feign底层工作原理
Feign的工作过程主要分为三个步骤:
1:通过动态代理在本地实例化远程接口
2:封装Request对象并进行编程
3:发送请求并对获取结果进行编码
1、创建远程接口的本地代理实例
如果在SpringBoot的启动类上加上注解@EnableFeignCleints,这个注解就相当于Feign组件的一个入口,当使用@EnableFeignCleints后,程序启动后会进行包扫描,扫描所有被@FeignCleint注解修饰的接口,通过JDK底层的动态代理为远程接口创建代理实例,并注册到IOC容器中。
在@EnableFeignCleints注解中引入FeignClientsRegistrar类,这个类在启动时调用registerBeanDefinitions()方法,在这个方法里面加载Feign的各项配置,并注册实例化对象。方法的主要代码如下所示:
//获取扫描的包路径
Set<String> basePackages;
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
......
//遍历包路径搜索目标接口
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
registerClientConfiguration(registry, name,attributes.get("configuration"));
//为接口创建代理对象的方法
registerFeignClient(registry, annotationMetadata, attributes);
}
}
我们可以看到在registerBeanDefinitions()方法中主要是先通过扫描基础包路径,然后找到被@FeignClient注解修饰的接口,放入Set中,接着遍历集合为每个接口逐一创建实例化对象。其中registerFeignClient(registry, annotationMetadata, attributes);是创建代理对象的方法。我们继续深入registerFeignClient()的代码,最终创建实际对象的核心代码是feign.ReflectiveFeign类中的newInstance()方法。如下所示:
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
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;
上面的代码是一个JDK底层的动态代理的代码,可见Feign也是基于代理模式来创建接口的实例化对象。
2、封装Request对象并进行编码
当调用远程微服务的方法时,底层通过JDK的动态代理交给Feign的代理类FeignInvocationHandler进行处理,该代理类根据方法的对象在映射集合中找到对应的MethodHandler方法处理器。主要代码如下所示:
static class FeignInvocationHandler implements InvocationHandler {
private final Map<Method, MethodHandler> dispatch;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
......
//找到对应的方法处理器
return dispatch.get(method).invoke(args);
}
}
Feign中为MethodHandler接口提供了默认的实现类SynchronousMethodHandler,其中核心的逻辑就是根据具体的配置先创建RequestTemplate请求模板对象,然后调用Target接口中的apply(RequestTemplateinput())方法根据刚刚获取到的请求模板对象创建远程服务请求的Request对象。核心代码如下所示:
final class SynchronousMethodHandler implements MethodHandler {
......
public Object invoke(Object[] argv) throws Throwable {
//创建请求模板对象,同时使用Encode接口进行编码操作
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
//Feign默认提供的重试机制
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 {
//多个方法层层调用这里简单贴出主要逻辑
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
Request request = target.apply(new RequestTemplate(template));
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
......
}
}
经过这些操作之后,Request对象就封装好了,下面是该对象的具体结构:
//请求方法,本例中对应为GET
private final String method;
//要访问的接口地址,本例中对应为/product/getProductInfo/22
private final String url;
//请求头部信息,本例对应的键值对为token=xingren
private final Map<String, Collection<String>> headers;
//请求体,一般POST请求下将复杂对象放入请求体,会被转换成字节数组进行传输
private final byte[] body;
//字符集
private final Charset charset;
3、feign.Client发送请求并对获取结果进行解码
首先调用feign.Client接口中的excute()方法执行请求并获取响应结果。
对于Client接口,Feign提供了几个不同的实现:
(1)默认的实现Default()类,内部使用JDK中的HttpURLConnextion完成URL请求处理,没有使用连接池,http连接的复用能力弱,性能低,一般不会采用这种方式。
(2)当Feign配合Ribbon使用时提供默认实现类LoadBalanceFeignClient,内部使用Ribbon负载均衡技术完成URL请求处理的feign.Client客户端实现类。对于feign.Client发送请求并对获取结果进行解码的源码简化如下所示:
Response response;
try {
response = client.execute(request, options);
response.toBuilder().request(request).build();
} catch (IOException e) {
//一系列的日志处理
}
然后对响应对象的null值进行处理、相应种类的判断以及响应对象消息体的处理,最后会将body转化为字节数组:
//判断返回的响应对象的类型是否正确
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;
}
//转换成字节数组
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
}
最后一步是针对不同的http状态码来做不同的处理,如果http状态码为成功,再判断是否有返回值,如果有返回值就对响应对象进行解码操作,无返回值直接返回null;如果是404调用解码方法,最后Decoder底层会执行emptyValueOf()方法返回空的Response对象;如果状态码为其他类型,则返回异常。代码如下所示:
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);
}