暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Feign?带大括号的参数?

天凉好个秋 好个秋 2021-03-28
1167

前言

最近在使用feign发起http请求的时候,碰到了一个问题:由于参数的值中带了大括号导致参数设置没有生效。

事情的背景是这样子的:

最近朋友推荐了一个叫做拉勾教育的APP,类似于极客时间,上面有些挺不错的技术文章,新注册用户有7天的VIP时间,可以免费看所有的技术文章。但是一旦7天之后所有的文章都需要收费。

作为一个背负着房贷车贷被生活压弯了腰的穷困潦倒的中年程序猿,本着能省就省的原则,想着趁着免费期间把这些文章扒下来转成pdf,留着慢慢学习。

过程

说干就干,整个过程思路其实很简单:

  1. 抓包,找到返回文章内容的接口。
  2. 测试接口所需要的参数。
  3. 写代码模拟调用接口,返回文章内容自行处理。

第一步和第二步很快就做出来了,并且在POSTMAN中测试了下,可以跑通。

其中x-l-req-header
这个参数是必须的,不传的话会校验不通过。

第三步,使用feign作为http client,很快代码也写出来了。

@FeignClient(name = "lagou-service",url = "拉勾教育服务器地址")
public interface LagouCall {

    String COOKIE = "cookie=抓包获取自己的cookie";
    String AUTH = "authorization=转包获取自己的authorization";
    String XLREQHEADER = "x-l-req-header={\"deviceType\":1}";

    @GetMapping(value = "拉勾教育的api", headers = {
            COOKIE, AUTH,
            XLREQHEADER
    })
    CourseCatalogResponse getCourseCatalog(@RequestParam("courseId") int courseId);

}

@RestController
@RequestMapping("/api/negols/lagou/edu")
@Slf4j
public class LagouController {

    @Resource
    private LagouCall lagouCall;

    @GetMapping("/test")
    public Object test(@RequestParam("courseId") int courseId) {
        return lagouCall.getCourseCatalog(courseId);

    }
}

调用这个接口返回报错

{
  "state"1003,
  "message""非法的访问",
  "content"null
}

通过debug发现feign发起http请求的时候实际上没有把x-l-req-header
参数传给服务器:

header

header里面只包含了authorization
cookie

代码中明明设置了x-l-req-header
,但是为什么没生效呢?

还是那句话,源码面前,了无秘密。

熟悉feign源码的同学应该都知道,feign在启动的时候会在SpringMvcContract.parseHeaders()
方法中解析feign client中配置的header数据,并生成对应的HeaderTemplate
对象保存在RequestTemplate
对象中

private void parseHeaders(MethodMetadata md, Method method,
   RequestMapping annotation)
 
{
 // TODO: only supports one header value per key
 if (annotation.headers() != null && annotation.headers().length > 0) {
  for (String header : annotation.headers()) {
   int index = header.indexOf('=');
   if (!header.contains("!=") && index >= 0) {
        // 生成对应的HeaderTemplate对象保存在RequestTemplate对象中
    md.template().header(resolve(header.substring(0, index)),
      resolve(header.substring(index + 1).trim()));
   }
  }
 }
}

解析header最终实际上是调用RequestTemplate.appendHeader()
方法

private RequestTemplate appendHeader(String name, Iterable<String> values) {
  if (!values.iterator().hasNext()) {
    /* empty value, clear the existing values */
    this.headers.remove(name);
    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.create()
调用new HeaderTemplate(template.toString(), name, values, Util.UTF_8)
生成HeaderTemplate
对象,

private HeaderTemplate(String template, String name, Iterable<String> values, Charset charset) {
  super(template, ExpansionOptions.REQUIRED, EncodingOptions.NOT_REQUIRED, false, charset);
  this.values = StreamSupport.stream(values.spliterator(), false)
      .filter(Util::isNotBlank)
      .collect(Collectors.toCollection(LinkedHashSet::new));
  this.name = name;
}

HeaderTemplate
构造方法中会先调用父类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();
}

Template()
方法中,会调用this.parseTemplate();
方法,该方法调用parseFragment()
方法:

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("{")) {
      // 如果参数值(aaa=bbb,aaa是参数名,bbb是参数值)以 { 开始,则认为这是参数值是一个表达式,
      // 则需要把参数值单独记录在templateChunks变量中
      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)));
    }
  }
}

从上面的代码中,可以看到,在形成HeaderTemplate
的时候,如果发现参数值是以左大括号{开头的,则认为改参数值是一个表达式,则会把该参数值转换成一个表达式对象Expression
保存在templateChunks
列表中。

expression

feign发起http调用是在SynchronousMethodHandler.invoke()
方法中

@Override
public Object invoke(Object[] argv) throws Throwable {
  // 构造RequestTemplate
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Options options = findOptions(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      return executeAndDecode(template, options);
    } catch (RetryableException e) {
      // 省略代码
    }
  }
}

invoke()
中首先会构造RequestTemplate.Factory
工厂类构造RequestTemplate
对象,最终会调用到RequestTemplate.resolve()
方法:

/**
  * Resolve all expressions using the variable value substitutions provided. Variable values will
  * be pct-encoded, if they are not already.
  *
  * @param variables containing the variable values to use when resolving expressions.
  * @return a new Request Template with all of the variables resolved.
  */

public RequestTemplate resolve(Map<String, ?> variables) {

  // ... 省略代码
 /* headers */
 if (!this.headers.isEmpty()) {
   // 先清空目标RequestTemplate对象的header映射
   resolved.headers(Collections.emptyMap());

   // 依次遍历所有的headers,调用HeaderTemplate.expand()方法
   // 如果header被认为是表达式,则会执行具体的表达式
   for (HeaderTemplate headerTemplate : this.headers.values()) {
     /* resolve the header */
     // 解析header,如果是表达式对象,且表达式的值存在的话,则会替换表达式对象
     // variables是定义FeignClient通过RequestParam注解注释的变量
     String header = headerTemplate.expand(variables);
     if (!header.isEmpty()) {
       // 如果最终数据不为空,则会把该header放到目标RequestTemplate对象的headers映射表中
       String headerValues = header.substring(header.indexOf(" ") + 1);
       if (!headerValues.isEmpty()) {
         resolved.header(headerTemplate.getName(), headerValues);
       }
     }
   }
 }

 if (this.bodyTemplate != null) {
   resolved.body(this.bodyTemplate.expand(variables));
 }

 /* mark the new template resolved */
 resolved.resolved = true;
 return resolved;
}

public String expand(Map<String, ?> variables) {
  if (variables == null) {
    throw new IllegalArgumentException("variable map is required.");
  }

  /* resolve all expressions within the template */
  StringBuilder resolved = null;
  for (TemplateChunk chunk : this.templateChunks) {
    String expanded;
    if (chunk instanceof Expression) {
      // 如果是Expression对象,则调用resolveExpression()方法解析
      expanded = this.resolveExpression((Expression) chunk, variables);
    }
    // ... 其他省略代码

  return resolved.toString();
}

protected String resolveExpression(
                                     Expression expression,
                                     Map<String, ?> variables)
 
{
  String resolved = null;
  Object value = variables.get(expression.getName());
  // 从variables中key为找到参数值中大括号包围的值对应的value
  if (value != null) {
    String expanded = expression.expand(
        value, this.encode.isEncodingRequired());
    if (expanded != null) {
      if (!this.encodeSlash) {
        logger.fine("Explicit slash decoding specified, decoding all slashes in uri");
        expanded = expanded.replaceAll("%2F""/");
      }
      resolved = expanded;
    }
  } else {
    if (this.allowUnresolved) {
      /* unresolved variables are treated as literals */
      resolved = encodeLiteral(expression.toString());
    }
  }
  return resolved;
}

从注释中可以看出,resolve()
方法主要就是用具体的数据来替换表达式占位符。该方法中,会依次遍历当前的header对象,调用HeaderTemplate.expand()
方法来完成表达式的替换(使用定义FeignClient时使用@RequestParam
等注解注释的变量,本例中是courseId),如果替换之后数据不为空的话,则会把对应的header设置到目标RequestTemplate
对象的headers映射上去。

总结一下:feign client定义的参数值(aaa=bbb,aaa是参数名,bbb是参数值)中如果包含了大括号,则feign认为参数名对应的值为表达值,在运行期间会找到以参数值为名字的变量的值来替换,如果没有找到的话则忽略该参数。

所以在本例中,参数x-l-req-header={"deviceType":1}
中的参数值{"deviceType":1}
包含了大括号,所以参数x-l-req-header
具体的值被认为是表达值,在运行期间需要动态替换,但是本例中只设置了courseId
这个动态参数,没有设置名为"deviceType":1
的动态参数,所以直接忽略了x-l-req-header
这个参数,导致发送到拉勾教育服务器的请求中没有了x-l-req-header
这个header。

解法

根据上面的分析,可以把x-l-req-header
的值设置为动态参数,具体做法如下:

@FeignClient(name = "lagou-service",url = "拉勾教育服务器地址")
public interface LagouCall {

    String COOKIE = "cookie=抓包获取自己的cookie";
    String AUTH = "authorization=转包获取自己的authorization";
    String XLREQHEADER = "x-l-req-header={xlReqHeader}";

    @GetMapping(value = "拉勾教育的api", headers = {
            COOKIE, AUTH,
            XLREQHEADER
    })
    CourseCatalogResponse getCourseCatalog(@RequestParam("courseId") int courseId, @RequestParam("xlReqHeader") String xlReqHeader);

}

@RestController
@RequestMapping("/api/negols/lagou/edu")
@Slf4j
public class LagouController {

    @Resource
    private LagouCall lagouCall;

    @GetMapping("/test")
    public Object test(@RequestParam("courseId") int courseId) {
        return lagouCall.getCourseCatalog(courseId, "{\"deviceType\":1}");
    }
}


文章转载自天凉好个秋 好个秋,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论