OpenFeign不支持{}特殊字符的header解决
一、环境
<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 {@Overridepublic 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 typethis.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 {@Overridepublic 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