OpenFeign / feign

Feign makes writing java http clients easier
Apache License 2.0
9.47k stars 1.92k forks source link

Encode issue with feign.template.Template#resolveExpression #2476

Open effiu opened 3 months ago

effiu commented 3 months ago

I customized an AnnotatedParameterProcessor implementation class. But I encountered a problem: when I use the following code to put the parameter into the request body.

feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>) {
if (this.bodyTemplate != null) {
      resolved.body(this.bodyTemplate.expand(variables));
    }
}

Such as the bodyTemplate‘s value is: %7B"ab":{ab}%7D, and the variable(ab) is a Array or a List.

feign.template.Template#resolveExpression feign.template.Expressions.SimpleExpression#expand

String expand(Object variable, boolean encode) {
  StringBuilder expanded = new StringBuilder();
  if (Iterable.class.isAssignableFrom(variable.getClass())) {
    expanded.append(this.expandIterable((Iterable<?>) variable));
  } else {
    expanded.append((encode) ? encode(variable) : variable);
  }

  /* return the string value of the variable */
  String result = expanded.toString();
  if (!this.matches(result)) {
    throw new IllegalArgumentException("Value " + expanded
        + " does not match the expression pattern: " + this.getPattern());
  }
  return result;
}

When the variable is of type Iterable, it will be handle and encode. But I don't want to handle my variable. How should i control it.

I customized an AnnotatedParameterProcessor to encapsulate the parameters on the feignclient method(POST) into a format like {"a":{a},"b":{b},"c":{c}}, and then put it into the bodyTemplate. Then, in feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>) replace {a} in the body with the real value, and put this string into the request body (JSON format). However, if {c} is a List, it will be encoded. {"a":1,"b":test,"c":[],"d":{}},but variable c will be processed into other formats and encoded (like in URLs).

effiu commented 3 months ago

public class PostBodyParamParameterProcessor implements AnnotatedParameterProcessor {

    private static final String JSON_TOKEN_START = "{";
    private static final String JSON_TOKEN_END = "}";
    private static final String JSON_TOKEN_START_ENCODED = "%7B";
    private static final String JSON_TOKEN_END_ENCODED = "%7D";
    private static final String QUOTA = "\"";

    private static final Class<PostBodyParam> ANNOTATION = PostBodyParam.class;

    @Override
    public Class<? extends Annotation> getAnnotationType() {
        return ANNOTATION;
    }

    @Override
    public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
        int parameterIndex = context.getParameterIndex();
        PostBodyParam bodyParam = ANNOTATION.cast(annotation);
        String name = bodyParam.value();
        checkState(emptyToNull(name) != null, "PostBodyParam.value() was empty on parameter %s", parameterIndex);
        MethodMetadata metadata = context.getMethodMetadata();
        context.setParameterName(name);
        Class<?> parameterType = method.getParameterTypes()[parameterIndex];
        Map<Integer, Param.Expander> expander = metadata.indexToExpander();
        // 这里根据不同的参数类型,拼接不同的字符串。例如数字和对象(json序列化后)不带引号,字符串带引号。
        String value = JSON_TOKEN_START + name + JSON_TOKEN_END;
        if (isNumber(parameterType)) {
            expander.put(parameterIndex, new Param.ToStringExpander());
        } else if (isString(parameterType)) {
            expander.put(parameterIndex, new Param.ToStringExpander());
            value = QUOTA + value + QUOTA;
        } else {
            // BigDecimal、集合类、数组会被认为是普通对象,json序列化.
            expander.put(parameterIndex, JsonUtil::toJson);
        }
        String appendBody = appendBody(metadata.template().bodyTemplate(), name, value);
        metadata.template().bodyTemplate(appendBody);
        metadata.template().header("content-type", MediaType.APPLICATION_JSON_VALUE);
        return true;
    }

    /**
     * 这个判断,暂不支持复杂的Java类型,例如{@code BigDecimal}等. 如何后续需要支持,加上即可。<br/>
     * 需要注意的是 {@code JsonUtil.toJson(BigDecimal)} 与 {@code BigDecimal.toString()}的区别。
     * 
     * @see java.math.BigDecimal
     * @param parameterType 参数类型
     * @return 是否是数字
     */
    private boolean isNumber(Class<?> parameterType) {
        return NumberUtils.STANDARD_NUMBER_TYPES.contains(parameterType) || parameterType.isAssignableFrom(int.class)
            || parameterType.isAssignableFrom(long.class) || parameterType.isAssignableFrom(byte.class)
            || parameterType.isAssignableFrom(double.class) || parameterType.isAssignableFrom(float.class);
    }

    /**
     * 是否是字符串,暂不考虑String的包装类。
     * 
     * @see StringBuilder#toString()
     * @see feign.Param.Expander
     * 
     * @param parameterType 参数类型
     * @return 是否是字符串
     */
    private boolean isString(Class<?> parameterType) {
        return parameterType.isAssignableFrom(String.class);
    }

    /**
     * 这里 只能手动拼接字符串。因为如果用map的话,在多参数时,序列号和反序列化过程中,body中的{key}占位符,只能用"{}"表示。 <br/>
     * 多了引号后,子对象就不再是json格式,而是字符串了。所以这里使用append拼接的方式。
     * 
     * @param body
     * @param key
     * @param value 
     */
    private String appendBody(String body, String key, String value) {
        StringBuilder builder = new StringBuilder();
        if (StringUtils.hasText(body)) {
            // 这里删除最后一个字符串,即:}/%7D
            builder.append(body).delete(body.length() - 3, body.length()).append(",");
        } else {
            builder.append(JSON_TOKEN_START_ENCODED);
        }
        builder.append(QUOTA).append(key).append(QUOTA).append(":");
        builder.append(value);
        builder.append(JSON_TOKEN_END_ENCODED);
        return builder.toString();
    }
}`
kdavisk6 commented 1 month ago

BodyTemplate is meant for at most, the simplest of use cases. Anything else should be done using an Encoder instance. What you are trying is technically possible with BodyTemplate, but as you've discovered is extremely difficult.

TLDR; use an Encoder and not @Body