OpenFeign 组件的前身是 Netflix Feign 项目。后来 Feign 项目被贡献给了开源组织,才有了今天使用的 Spring Cloud OpenFeign 组件。
OpenFeign 提供了一种声明式的远程调用接口,它可以大幅简化远程调用的编程体验。用一个代码片段看一下,由 OpenFeign 发起的远程服务调用的代码风格是什么样的。
String response = helloWorldService.hello("Spring Cloud");
可以发现,使用 OpenFeign 组件来实现远程调用非常简单,就像使用本地方法一样,只要一行代码就能实现 WebClient 组件好几行代码干的事情。而且这段代码不包含任何业务无关的信息,完美实现了调用逻辑和业务逻辑之间的职责分离。
OpenFeign 使用了一种动态代理技术来封装远程服务调用的过程,在上面的例子中看到的 helloWorldService 其实是一个特殊的接口,它是由 OpenFeign 组件中的 FeignClient 注解所声明的接口。
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(value = "hello-world-service")
public interface HelloWorldService {
@PostMapping("/sayHello")
String hello(String guestName);
}
远程服务调用的信息被写在了 FeignClient 接口中。在上面的代码里,可以看到,服务的名称、接口类型、访问路径已经通过注解做了声明。OpenFeign 通过解析这些注解标签生成一个动态代理类,这个代理类会将接口调用转化为一个远程服务调用的 Request,并发送给目标服务。
在项目初始化阶段,OpenFeign 会生成一个代理类,对所有通过FeignClient 接口发起的远程调用进行动态代理。如图:
上图中的步骤中,在项目启动阶段加载完成的是 1 ~ 3步 ,只有第 4 步(调用远程服务)是发生在项目的运行阶段。
关键步骤描述:
OpenFeign 组件加载过程的重要阶段,如图:
OpenFeign 动态代理类的创建过程:
@SpringBootApplication
@EnableFeignClients
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication .class, args);
}
}
// 解析FeignClient接口方法级别上的RequestMapping注解
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
// 如果方法上没有使用RequestMapping注解,则不进行解析
// 其实GetMapping、PostMapping等注解都属于RequestMapping注解
if (!RequestMapping.class.isInstance(methodAnnotation)
&& !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
return;
}
// 获取RequestMapping注解实例
RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
// 解析Http Method定义,即注解中的GET、POST、PUT、DELETE方法类型
RequestMethod[] methods = methodMapping.method();
// 如果没有定义methods属性则默认当前方法是个GET方法
if (methods.length == 0) {
methods = new RequestMethod[] { RequestMethod.GET };
}
checkOne(method, methods, "method");
data.template().method(Request.HttpMethod.valueOf(methods[0].name()));
// 解析Path属性,即方法上写明的请求路径
checkAtMostOne(method, methodMapping.value(), "value");
if (methodMapping.value().length > 0) {
String pathValue = emptyToNull(methodMapping.value()[0]);
if (pathValue != null) {
pathValue = resolve(pathValue);
// 如果path没有以斜杠开头,则补上/
if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
pathValue = "/" + pathValue;
}
data.template().uri(pathValue, true);
if (data.template().decodeSlash() != decodeSlash) {
data.template().decodeSlash(decodeSlash);
}
}
}
// 解析RequestMapping中定义的produces属性
parseProduces(data, method, methodMapping);
// 解析RequestMapping中定义的consumer属性
parseConsumes(data, method, methodMapping);
// 解析RequestMapping中定义的headers属性
parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<>());
}
通过上面的方法可以看到,OpenFeign 对 RequestMappings 注解的各个属性都做了解析。
如果项目中使用的是 GetMapping、PostMapping 之类的注解,没有使用 RequestMapping,那么 OpenFeign 也可以解析。以 GetMapping 为例,它对 RequestMapping 注解做了一层封装。如下代码片段,这个注解头上也挂了一个 RequestMapping 注解。因此 OpenFeign 可以正确识别 GetMapping 并完成加载。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
// ...省略部分代码
}
OpenFeign 其实底层调用的是 Feign 的方法,生成了代理类,使用的是 JDK 的动态代理,然后 bean 注入。
调用过程,就是代理类作为客户端向被调用方发送请求,接收相应的过程。其中,feign 自行封装了 JDK java.net 相关的网络请求方法,请求过程中还有 Loadbalancer 进行负载均衡;收到响应后,还会对响应类进行解析,取出正确的响应信息。