创建一个公用的httpClient肯定是很多码农都遇到过的任务,Feign
提供了一个简单优雅的方式。首先要区别一点,@FeignClient
是spring封装的,和原始的Feign在用法上有一定的区别。Sping提供的封装固然用起来简单,但是其中的依赖或许会给我们的工程带来很多麻烦。这里我们把重点放在原始的Feign
,看看他是如何为我们创建httpClient的。
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner,
@Param("repo") String repo);
}
public class MyApp {
public static void main(String... args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
// 发送请求
List<Contributor> contributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
上述事例中展示了一种Feign的用法,这种指定baseUrl的形式最终会生成一个HardCodedTarget
,如果baseUrl不固定,Feign还提供了一种EmptyTarget
的实现
interface GitHubSimple {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(URI baseUri,
@Param("owner") String owner,
@Param("repo") String repo);
}
public class MyApp {
public static void main(String... args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(Target.EmptyTarget.create(GitHubSimple.class));
// 发送请求
URI baseURI = new URI("https://api.github.com");
List<Contributor> contributors = github.contributors(baseURI,"OpenFeign", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
Feign
的创建和使用就是这么简单,但是在这简单的背后,Feign.Builder
到底做了哪些事情,让我们一步步来看。
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
public Feign build() {
Client client = Capability.enrich(this.client, capabilities);
Retryer retryer = Capability.enrich(this.retryer, capabilities);
List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
.map(ri -> Capability.enrich(ri, capabilities))
.collect(Collectors.toList());
Logger logger = Capability.enrich(this.logger, capabilities);
Contract contract = Capability.enrich(this.contract, capabilities);
Options options = Capability.enrich(this.options, capabilities);
Encoder encoder = Capability.enrich(this.encoder, capabilities);
Decoder decoder = Capability.enrich(this.decoder, capabilities);
InvocationHandlerFactory invocationHandlerFactory =
Capability.enrich(this.invocationHandlerFactory, capabilities);
QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
从上面的事例中我们可以看到,最终是通过Feign.Builder.target()
方法来生成的实例,在当前的版本中,返回了一个ReflectiveFeign
,最终通过ReflectiveFeign.newInstance
生成了代理对象。在构建ReflectiveFeign
时需要三个参数
我们来看看newInstance里都做了什么
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
...
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
return proxy;
}
首先来看第一行, Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
,这里创建了一个map,key是方法签名,value则是对应的handler,这里的变量targetToHandlersByName
就是ParseHandlersByName
,我们来看看它具体是怎么做的
public Map<String, MethodHandler> apply(Target target) {
List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
for (MethodMetadata md : metadata) {
BuildTemplateByResolvingArgs buildTemplate;
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
buildTemplate =
new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
} else if (md.bodyIndex() != null || md.alwaysEncodeBody()) {
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
} else {
buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
}
if (md.isIgnored()) {
result.put(md.configKey(), args -> {
throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
});
} else {
result.put(md.configKey(),
factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
}
}
return result;
}
这里有一个非常重要的类MethodMetadata
,顾名思义它记录了方法的各类元数据,比方说方法签名,url参数的位置,requestBody参数的位置,方法的返回值等等,这里的内容比较多我们暂时不展开。
在生成了所有方法的MethodMetadata
之后,还会根据MethodMetadata
里的信息构建不同的buildTemplate(实际上是一个工厂RequestTemplate.Factory
),最终根据方法签名,生成对应的代理(SynchronousMethodHandler
)。这里的代理就是最终进行http调用的地方。
看到这里其实最重要的工作都已经结束了,在最后还需要一个代理,将方法的调用dispatch到正确的SynchronousMethodHandler上。
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
这里的factory
是一个InvocationHandlerFactory.Default()
实例
static final class Default implements InvocationHandlerFactory {
@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
}
}
ReflectiveFeign.FeignInvocationHandler
里就非常简单了,
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
简单来说就是做了一个dispatch的工作。
这里限于篇幅的原因其实很多细节没有展开,比如Feign的各类配置,如何生成MethodMetadata
,buildTemplate
是如何工作的。但是整体的流程已经大致梳理清楚了,fi对于我们自己在工作中构建各种公共类库也是非常有参考意义的。后续我们来看看Spring是如何处理的。
因篇幅问题不能全部显示,请点此查看更多更全内容