Skip to main content

OpenFeign拦截器 :RequestInterceptor流程与源码解析

RequestInterceptor是Feign的请求拦截器,在请求之前,对RequestTemplate进行处理的拦截器,拦截器执行完成以后,Feign才会真正的发起一个http请求

我们来看一下,Feign的RequestInterceptor 定义

public interface RequestInterceptor {

/**
* Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}

使用起来也很简单,实现RequestInterceptor 然后重写apply(RequestTemplate template);

在Feign core包2.2.0下默认有五个实现类,上类图

先从BasicAuthRequestInterceptor开始,用于添加使用HTTP基本身份验证所需的请求标头

public class BasicAuthRequestInterceptor implements RequestInterceptor {

private final String headerValue;

/**
* Creates an interceptor that authenticates all requests with the specified username and password
* encoded using ISO-8859-1.
*
* @param username the username to use for authentication
* @param password the password to use for authentication
*/
public BasicAuthRequestInterceptor(String username, String password) {
//设置字符编码ISO_8859_1
this(username, password, ISO_8859_1);
}

/**
* Creates an interceptor that authenticates all requests with the specified username and password
* encoded using the specified charset.
*
* @param username the username to use for authentication
* @param password the password to use for authentication
* @param charset the charset to use when encoding the credentials
*/
public BasicAuthRequestInterceptor(String username, String password, Charset charset) {
checkNotNull(username, "username");
checkNotNull(password, "password");
//设置headerValue值,进行base64编码,然后前缀是Basic
this.headerValue = "Basic " + base64Encode((username + ":" + password).getBytes(charset));
}

/*
* This uses a Sun internal method; if we ever encounter a case where this method is not
* available, the appropriate response would be to pull the necessary portions of Guava's
* BaseEncoding class into Util.
*/
private static String base64Encode(byte[] bytes) {
return Base64.encode(bytes);
}

@Override
public void apply(RequestTemplate template) {
//放在请求头里
template.header("Authorization", headerValue);
}
}

用法也很简单,只需要这样,注册到spring中即可。当然,如果需要,你也可以编写一个更复杂的权限拦截器,请注意,本文只是做了一个简单的介绍。

    @Bean
public RequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("your_username", "your_password");
}

然后就是BaseRequestInterceptor

public abstract class BaseRequestInterceptor implements RequestInterceptor {

/**
* The encoding properties.
*/
//FeignClientEncodingProperties 是 Spring Cloud OpenFeign 中的一个类,
//用于配置 Feign 客户端的请求和响应编码相关的属性。它通常用于指定请求和响应的字符编码、压缩、解压缩等属性。
private final FeignClientEncodingProperties properties;

/**
* Creates new instance of {@link BaseRequestInterceptor}.
* @param properties the encoding properties
*/
protected BaseRequestInterceptor(FeignClientEncodingProperties properties) {
Assert.notNull(properties, "Properties can not be null");
this.properties = properties;
}

/**
* Adds the header if it wasn't yet specified.
* @param requestTemplate the request
* @param name the header name
* @param values the header values
*/
//提供了addHeader的方法,目的也是为了让子类能够进行复用
protected void addHeader(RequestTemplate requestTemplate, String name,
String... values) {

if (!requestTemplate.headers().containsKey(name)) {
requestTemplate.header(name, values);
}
}

protected FeignClientEncodingProperties getProperties() {
return this.properties;
}

}

我在代码上加了注释,阅读完注释后应该能够理解,然后来看FeignAcceptGzipEncodingInterceptor,它是用来启用HTTP响应负载压缩,实际上内部就是在请求头上加入标识。

public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {

/**
* Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}.
* @param properties the encoding properties
*/
protected FeignAcceptGzipEncodingInterceptor(
FeignClientEncodingProperties properties) {
super(properties);
}

/**
* {@inheritDoc}
*/
@Override
public void apply(RequestTemplate template) {
//加上gzip的请求头信息
addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER,
HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
}

}

FeignContentGzipEncodingInterceptor 来启用HTTP请求负载压缩,与FeignAcceptGzipEncodingInterceptor类似,最终效果都是加header标识

public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor {

/**
* Creates new instance of {@link FeignContentGzipEncodingInterceptor}.
* @param properties the encoding properties
*/
protected FeignContentGzipEncodingInterceptor(
FeignClientEncodingProperties properties) {
super(properties);
}

/**
* {@inheritDoc}
*/
@Override
public void apply(RequestTemplate template) {
//检验请求
if (requiresCompression(template)) {
//添加请求头 Content-Encoding:gzip,deflate

addHeader(template, HttpEncoding.CONTENT_ENCODING_HEADER,
HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
}
}

/**
* Returns whether the request requires GZIP compression.
* @param template the request template
* @return true if request requires compression, false otherwise
*/
private boolean requiresCompression(RequestTemplate template) {

final Map<String, Collection<String>> headers = template.headers();
return matchesMimeType(headers.get(HttpEncoding.CONTENT_TYPE))
&& contentLengthExceedThreshold(headers.get(HttpEncoding.CONTENT_LENGTH));
}

/**
* Returns whether the request content length exceed configured minimum size.
* @param contentLength the content length header value
* @return true if length is grater than minimum size, false otherwise
*/
private boolean contentLengthExceedThreshold(Collection<String> contentLength) {

try {
if (contentLength == null || contentLength.size() != 1) {
return false;
}

final String strLen = contentLength.iterator().next();
final long length = Long.parseLong(strLen);
//是否满足最小请求长度
return length > getProperties().getMinRequestSize();
}
catch (NumberFormatException ex) {
return false;
}
}

/**
* Returns whether the content mime types matches the configures mime types.
* @param contentTypes the content types
* @return true if any specified content type matches the request content types
*/
private boolean matchesMimeType(Collection<String> contentTypes) {
if (contentTypes == null || contentTypes.size() == 0) {
return false;
}

if (getProperties().getMimeTypes() == null
|| getProperties().getMimeTypes().length == 0) {
// no specific mime types has been set - matching everything
return true;
}
//是否为指定的几个格式
// private String[] mimeTypes = new String[] { "text/xml", "application/xml",
// "application/json" };
for (String mimeType : getProperties().getMimeTypes()) {
if (contentTypes.contains(mimeType)) {
return true;
}
}

return false;
}

}

到这里,就已经梳理完成,Feign提供的RequestInterceptor,然后我们再来看看,RequestInterceptor是什么时候加载,又什么时候进行初始化使用的,此前文章其实已经有涉及到,就是在FeignClientFactoryBeangetTarget()

    Feign.Builder builder = feign(context);

在通过容器获取Feing.Builder的时候

  protected Feign.Builder feign(FeignContext context) {
...
//配置feign,通过名字就能看出来了,feign的配置项都在这里,请求拦截器也作为配置项的一员,也逃不掉
configureFeign(context, builder);

return builder;
}

这里有点多,我们重点看configureUsingConfiguration(context, builder);


protected void configureFeign(FeignContext context, Feign.Builder builder) {
FeignClientProperties properties = this.applicationContext
.getBean(FeignClientProperties.class);
if (properties != null) {
if (properties.isDefaultToProperties()) {
configureUsingConfiguration(context, builder);
configureUsingProperties(
properties.getConfig().get(properties.getDefaultConfig()),
builder);
configureUsingProperties(properties.getConfig().get(this.contextId),
builder);
}
else {
configureUsingProperties(
properties.getConfig().get(properties.getDefaultConfig()),
builder);
configureUsingProperties(properties.getConfig().get(this.contextId),
builder);
configureUsingConfiguration(context, builder);
}
}
else {
configureUsingConfiguration(context, builder);
}
}

在他内部也就是通过contextId去拿到对应的RequestInterceptor ,之所以通过contextId去拿就是为了区分环境隔离,如果父容器存在的话,拿到父容器的,如果子容器,也就是当前容器存在,也仅仅是获取当前容器自定义的配置项,然后设置到builder构造器中

    Map<String, RequestInterceptor> requestInterceptors = context
.getInstances(this.contextId, RequestInterceptor.class);
if (requestInterceptors != null) {
builder.requestInterceptors(requestInterceptors.values());
}

创建好了builder以后,通过HystrixTargeter.target进行实例化创建

    public <T> T target(Target<T> target) {
return build().newInstance(target);
}

这里返回到哪里呢? 实际上也就是FactoryBean的getObject返回的,所以他最后会被spring拿到进行实例化的创建!最后我们看一看这些拦截器什么时候会被使用

我们之前已经知道了,在进行FeignClient的类的调用时,实际上执行的就是代理中的invoke方法



public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
//这里就是执行的方法,进去这里
return executeAndDecode(template, options);
} 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;
}
}
}

进入executeAndDecode(template, options);

  Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
//这一行就是真正去执行拦截器的地方了
Request request = targetRequest(template);
...
}
  Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
//遍历执行所有拦截器
interceptor.apply(template);
}
return target.apply(template);
}

到了这里,整个拦截器的初始化获取到使用时机,我们已经全部了解清晰了