首页 > 其他分享 >OpenFeign不支持{}特殊字符的header解决

OpenFeign不支持{}特殊字符的header解决

时间:2022-12-06 13:02:05浏览次数:34  
标签:resolved String OpenFeign header RequestTemplate values 特殊字符 name

一、环境

<properties>
<spring.version>5.3.22</spring.version>
<spring-boot.version>2.7.3</spring-boot.version>
<spring-cloud.version>3.1.3</spring-cloud.version>
<spring-cloud-dependencies.version>2021.0.3</spring-cloud-dependencies.version>
<spring-cloud-starter-alibaba.version>2021.0.1.0</spring-cloud-starter-alibaba.version>
</properties>

其中​​feign​​包的版本号

<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-bom</artifactId>
<version>11.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>

二、场景描述

在​​feign​​​需要传递一些​​json​​格式的数据,代码如下

@Slf4j
public class AuthFeignInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate requestTemplate) {
UserDetail userDetail = UserContext.getUserDetail();

if (Objects.nonNull(appDetail)) {
String userJson = JsonUtils.toJsonString(userDetail);
requestTemplate.header(Constant.User.HEADER_NAME, userJson);
}
}
}

三、 问题定位

​userJson​​​携带了特殊符号​​{}​​​、​​:​​​,断点调试的时候参数都是正常设置,尝试定位是生产者还是消费者的问题,使用​​postman​​模拟消费者调用,生产者可以正常收到信息并解析成功,那么问题就在消费者

尝试源码断点

public final class RequestTemplate implements Serializable {
public RequestTemplate header(String name, String... values) {
return header(name, Arrays.asList(values));
}

public RequestTemplate header(String name, Iterable<String> values) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name is required.");
}
if (values == null) {
values = Collections.emptyList();
}

return appendHeader(name, values);
}

private RequestTemplate appendHeader(String name, Iterable<String> values) {
if (!values.iterator().hasNext()) {
/* empty value, clear the existing values */
this.headers.remove(name);
return this;
}
if (name.equals("Content-Type")) {
// a client can only produce content of one single type, so always override Content-Type and
// only add a single type
this.headers.remove(name);
this.headers.put(name,
HeaderTemplate.create(name, Collections.singletonList(values.iterator().next())));
return this;
}
this.headers.compute(name, (headerName, headerTemplate) -> {
if (headerTemplate == null) {
return HeaderTemplate.create(headerName, values);
} else {
return HeaderTemplate.append(headerTemplate, values);
}
});
return this;
}
}

可以看到最终是调用​​HeaderTemplate​​​来实现​​header​​​设置,继续查看​​HeaderTemplate​​源码

public final class HeaderTemplate {

public static HeaderTemplate create(String name, Iterable<String> values) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name is required.");
}

if (values == null) {
throw new IllegalArgumentException("values are required");
}

return new HeaderTemplate(name, values, Util.UTF_8);
}

private HeaderTemplate(String name, Iterable<String> values, Charset charset) {
this.name = name;

for (String value : values) {
if (value == null || value.isEmpty()) {
/* skip */
continue;
}

this.values.add(
new Template(
value,
ExpansionOptions.REQUIRED,
EncodingOptions.NOT_REQUIRED,
false,
charset));
}
}
}

可以看到​​HeaderTemplate​​​只是对​​Template​​进行封装

public class Template {

Template(
String value, ExpansionOptions allowUnresolved, EncodingOptions encode, boolean encodeSlash,
Charset charset) {
if (value == null) {
throw new IllegalArgumentException("template is required.");
}
this.template = value;
this.allowUnresolved = ExpansionOptions.ALLOW_UNRESOLVED == allowUnresolved;
this.encode = encode;
this.encodeSlash = encodeSlash;
this.charset = charset;
// 解析${}占位符
this.parseTemplate();
}

private void parseTemplate() {

// 解析{}占位符
this.parseFragment(this.template);
}

private void parseFragment(String fragment) {
// 解析每个{}占位符
ChunkTokenizer tokenizer = new ChunkTokenizer(fragment);

while (tokenizer.hasNext()) {
/* check to see if we have an expression or a literal */
String chunk = tokenizer.next();
// 如果占位符以{起始,则默认使用模板解析
if (chunk.startsWith("{")) {
Expression expression = Expressions.create(chunk);
if (expression == null) {
this.templateChunks.add(Literal.create(this.encodeLiteral(chunk)));
} else {
this.templateChunks.add(expression);
}
} else {
this.templateChunks.add(Literal.create(this.encodeLiteral(chunk)));
}
}
}
}

到这里基本已经定位到问题是feign实现了高级特性,占位符和模板解析造成的问题,具体还是​​:​​的特殊处理,暂时不展开

四、解决

定位到了问题,尝试找解决方案

4.1 占位符设置参数进行替换

查看​​RestTemplate​​​源码发现有一个​​resolve​​​方法可以对​​uriTemplate​​​、​​queries​​​、​​headers​​进行参数替换

public RequestTemplate resolve(Map<String, ?> variables) {

StringBuilder uri = new StringBuilder();

/* create a new template form this one, but explicitly */
RequestTemplate resolved = RequestTemplate.from(this);

if (this.uriTemplate == null) {
/* create a new uri template using the default root */
this.uriTemplate = UriTemplate.create("", !this.decodeSlash, this.charset);
}

String expanded = this.uriTemplate.expand(variables);
if (expanded != null) {
uri.append(expanded);
}

/*
* for simplicity, combine the queries into the uri and use the resulting uri to seed the
* resolved template.
*/
if (!this.queries.isEmpty()) {
/*
* since we only want to keep resolved query values, reset any queries on the resolved copy
*/
resolved.queries(Collections.emptyMap());
StringBuilder query = new StringBuilder();
Iterator<QueryTemplate> queryTemplates = this.queries.values().iterator();

while (queryTemplates.hasNext()) {
QueryTemplate queryTemplate = queryTemplates.next();
String queryExpanded = queryTemplate.expand(variables);
if (Util.isNotBlank(queryExpanded)) {
query.append(queryExpanded);
if (queryTemplates.hasNext()) {
query.append("&");
}
}
}

String queryString = query.toString();
if (!queryString.isEmpty()) {
Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri);
if (queryMatcher.find()) {
/* the uri already has a query, so any additional queries should be appended */
uri.append("&");
} else {
uri.append("?");
}
uri.append(queryString);
}
}

/* add the uri to result */
resolved.uri(uri.toString());

/* headers */
if (!this.headers.isEmpty()) {
/*
* same as the query string, we only want to keep resolved values, so clear the header map on
* the resolved instance
*/
resolved.headers(Collections.emptyMap());
for (HeaderTemplate headerTemplate : this.headers.values()) {
/* resolve the header */
String header = headerTemplate.expand(variables);
if (!header.isEmpty()) {
/* append the header as a new literal as the value has already been expanded. */
resolved.header(headerTemplate.getName(), header);
}
}
}

if (this.bodyTemplate != null) {
resolved.body(this.bodyTemplate.expand(variables));
}

/* mark the new template resolved */
resolved.resolved = true;
return resolved;
}

但是​​RequestTemplate​​对象是新生成的,无法进行传递

4.2 替换RequestTemplate的HeaderTemplate

查看源码发现​​RequestTemplate​​​并没有开发对​​HeaderTemplate​​​直接注入方法,所有​​header​​​都是使用​​header()​​​方法进行处理,而对​​RequestTemplate​​​的修改都是新生成一个​​RequestTemplate​​,放弃这个方案

4.3 参数编码

所有签名都是基于​​Base64​​​进行字节数组编码,那么该方案的适应性是最好的,对于​​http​​协议支持最好,修改源码

@Slf4j
public class AuthFeignInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate requestTemplate) {
UserDetail userDetail = UserContext.getUserDetail();

if (Objects.nonNull(appDetail)) {
String userJson = JsonUtils.toJsonString(userDetail);
String encodeAppJson = Base64.encode(appJson);
requestTemplate.header(Constant.User.HEADER_NAME, encodeAppJson);
}
}
}

参数解析可以​​Base64.decode​​​完成解析,还可以兼容中文,不要再进行​​UTF-8​​​编码,如果考虑历史兼容性,可以先判断​​header​​​是否以​​{​​​起始,如果不是则使用​​Base64​​​解析,如果考虑协议层的可变更性,可以在​​header​​​中接入类似​​content-type​​的编码类型,来实现对变化的支持

五、参考

​​issue​​

标签:resolved,String,OpenFeign,header,RequestTemplate,values,特殊字符,name
From: https://blog.51cto.com/u_15814313/5915644

相关文章