Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
effiu opened this issue Jul 18, 2024 · 2 comments
Open

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

effiu opened this issue Jul 18, 2024 · 2 comments
Labels
feedback provided Feedback has been provided to the author

Comments

@effiu
Copy link

effiu commented Jul 18, 2024

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
Copy link
Author

effiu commented Jul 18, 2024

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
Copy link
Member

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

@kdavisk6 kdavisk6 added the feedback provided Feedback has been provided to the author label Sep 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feedback provided Feedback has been provided to the author
Projects
None yet
Development

No branches or pull requests

2 participants