Spring feign client 中 @QueryParam 名称中的特殊字符

Special characters in @QueryParam name in Spring feign client

我们的一个外部 API 使用带有特殊字符的查询参数名称(不要问我为什么,我不知道)。我的伪装客户为此 API 声明的方法是这样的:

@GetMapping("/v1/user/{userId}/orders")
List<Order> getOrders(
    @PathVariable("userId") String userId,
    @RequestParam("page[number]") Integer pageNumber,
    @RequestParam("page[size]") Integer pageSize);

正如我提到的,请求参数包含特殊字符 []。 我正在使用 Spock 进行测试,我想像这样设置 Wiremock 存根:

wiremock.stubFor(
  get(urlPathMatching('/v1/users/' + userId + '/orders'))
    .withQueryParam("page[number]", new EqualToPattern("1"))
    .withQueryParam("page[size]", new AnythingPattern())
    .willReturn(
      status(200)
        .withHeader("Content-type", "application/json")
        .withBody("""[]""")
    ))

但我得到:

--------------------------------------------------------------------------------------------------
| Closest stub                        | Request                                                  |
--------------------------------------------------------------------------------------------------
                                      |
GET                                   | GET
/v1/users/123/orders                  | /v1/users/123/orders?page%5Bnumber%5D=%7Bpage%5Bnumber%5
                                      | D%7D&page%5Bsize%5D=%7Bpage%5Bsize%5D%7D
                                      |
Query: page[number] = 1               |                                 <<<<< Query is not present
Query: page[size] [anything] (always) |                                 <<<<< Query is not present
                                      |
--------------------------------------------------------------------------------------------------

经过多次试验和错误,我想出了一个解决方案,在 feign 客户端方法中使用 @PathVariables 而不是 @RequestParams:

@GetMapping("/v1/users/{userId}/orders?page[number]={pageNumber}&page[size]={pageSize}")
List<Order> getOrders(
    @PathVariable("userId") String userId,
    @PathVariable("pageNumber") Integer pageNumber,
    @PathVariable("pageSize") Integer pageSize);

并在 Wiremock 中对所有查询参数进行编码

wiremock.stubFor(
    get(urlPathMatching('/v1/users/' + userId + '/orders'))
    .withQueryParam("page%5Bnumber%5D", new EqualToPattern("1"))
    .withQueryParam("page%5Bsize%5D", new AnythingPattern())
    .willReturn(
        status(200)
        .withHeader("Content-type", "application/json")
        .withBody("""[]""")
    ))

然后就可以了。但这看起来像是一种黑客攻击。使用可选的查询参数也是有问题的。

有没有办法在 @RequestParam 中使用特殊字符? 它看起来像是 Spring?

中的错误

同时我会尝试调试它以了解问题所在。

首先,这不是 Spring 中的错误。这些类似于 spring 的注释(@RequestParam@RequestMapping 等来自 spring-web)的主要目标是在服务器上的 Spring MVC 控制器中使用它们-侧而不是为请求创建 urls。

对于 Feign 客户端,还有另一个库 spring-cloud-openfeign,它允许您将它们放在 Feign 客户端中用于请求映射,而不是原始的 Feign 注释(如 @RequestLine@Param 等等)。 SpringMvcContract负责,it is used by default in Spring applications。但这只是为了方便使用 Feign with Spring 的这群特定开发人员,因此不能成为更改这些注释的签名或添加一些新设置的理由。

关于所描述的问题 - 似乎没有任何 "official" 方法来禁用 url 编码,即使你正在使用原始的 Feign 注释进行请求映射 - 目前(version 10.7.4),你只能对斜杠进行此操作(例如检查 this issue)。

我进行了进一步调查,得出以下几点:

  1. 在构造请求查询模板时,您不能手动设置编码选项,因为它只是硬编码的(查看UriTemplate and QueryTemplate源代码)。
  2. 您实际上可以稍后通过编写自己的 RequestInterceptor 来修复构造的 url(通过反思,f.e),但这实际上毫无意义,因为 after applying all interceptors the request is built and, at the moment, it implicitly encodes the brackets again (check what's happened under the hood, basically it uses queryTemplate.toString() which, in turn, encodes brackets while using methods of UriUtils class)
  3. 请求创建后,由Client开始执行,我实际上在这一步修复了url。我从标准 Client.Default 继承了一个 class,注册了它而不是原来的自动配置的,然后我在实际调用客户端之前完成了我的工作(当然也可以对不同的客户端执行相同的操作客户,f.e., LoadBalancerFeignClient):
import feign.Client;
import feign.Request;
import feign.Response;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import java.io.IOException;
import java.lang.reflect.Field;

@Component
public class MyClient implements Client {

    private Client delegate;

    public MyClient() {
        this.delegate = new Client.Default(null, null);
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        fixRequestUrlIfPossible(request);
        return delegate.execute(request, options);
    }

    private void fixRequestUrlIfPossible(Request request) {
        try {
            Field urlField = ReflectionUtils.findField(Request.class, "url");
            urlField.setAccessible(true);

            String encodedUrl = (String) urlField.get(request);
            String url = encodedUrl.replace("%5B", "[").replace("%5D", "]");
            urlField.set(request, url);
        } catch (Exception e) {
            /*NOP*/
        }
    }
}

但是因为反射很容易出错(可以在以后的Feign版本中停止工作)。

所以,这取决于您 - 如果您不害怕做这种事情,请使用这种方法。我其实更喜欢你的版本。主要问题是:您的真实服务器是否能够处理带有编码括号的请求?在我看来,Wiremock 的问题不太重要。