概述
一、环境
<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
最后
以上就是孤独盼望为你收集整理的OpenFeign不支持{}特殊字符的header解决的全部内容,希望文章能够帮你解决OpenFeign不支持{}特殊字符的header解决所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复