Spring HATEOAS ControllerLinkBuilder 方法显着增加响应时间

Spring HATEOAS ControllerLinkBuilder methodOn increasing response times significantly

设置: 所以我有一个 RESTfull API 写在 java,使用 spring-bootspring-hates 用于向资源添加 links(超媒体驱动 RESTful Web 服务)。我拥有的一切都是标准的,没有进行额外的设置或更改

问题

  1. 案例:没有 links 资源 - Chrome TTFB 平均。 (10 次运行)1000 项 400 毫秒
  2. 案例:1 资源自参考 link - Chrome TTFB 平均。 (10 次运行)1000 项 1500 毫秒

我正在使用this official guide

问题

为什么只向我的资源添加 1 link 会额外增加 1 秒来处理请求。我将需要大约 5 - 7 links 在每个资源上并且每个资源都有额外的嵌入式资源?

对于 9000 个项目,每个项目只有 1 个 link(包括嵌套项目),我必须等待 30 秒才能得到响应,并且没有 links ~ 400 毫秒。

P.S。额外的代码无关紧要,因为我只是添加了教程中的代码,这会显着影响性能。

编辑 1

按照建议,我从 TextItem 构造函数

添加示例代码
add(linkTo(methodOn(TestController.class).getTestItems()).withRel("testLink"));

编辑 2

所以@Mathias Dpunkt 提出的以下示例绝对完美

private Method method = ReflectionUtils.findMethod(TestController.class, "getOne", Integer.class);

@Override
public Resource<Item> process(Resource<Item> resource) {
  resource.add(linkTo(method, resource.getContent().getId()).withSelfRel());
  return resource;
}

新问题

控制器:

@RestController
@RequestMapping("items")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {

    private final ItemResourceProcessor resourceProcessor;

    @RequestMapping(method = GET)
    public ResponseEntity<List<Resource<Item>>> getAll() {
        List<Resource<Item>> items = new ArrayList<>(100);
        for (int i = 0; i < 100; i++) {
            items.add(resourceProcessor.process(
                    new Resource<>(new Item(i, UUID.randomUUID().toString()))));
        }

        return ResponseEntity.ok(items);
    }

    @RequestMapping(method = GET, path = "/{id}")
    public ResponseEntity<Resource<Item>> getOne(@PathVariable Integer id, @RequestParam boolean test1, @RequestParam boolean test2) {
        return null;
    }
}

如果控制器方法采用 @RequestParam,则发布的解决方案不会将其附加到 link。当我打电话给

private Method method = ReflectionUtils.findMethod(TestController.class, "getOne", Integer.class);    

@Override
public Resource<Item> process(Resource<Item> resource) {
     resource.add(linkTo(method, resource.getContent().getId(), true, true).withSelfRel());
     return resource;
}

编辑:

已更新 post 以使用改进的版本 - Spring HATEOAS 0.22 and Spring Framework 4.3.5 / 5.0 M4

答案:

这很有趣。我查看了 ControllerLinkBuilder 方法 linkTomethodOn 的源代码,对于一个简单的 link:

有很多事情要做
  • 为控制器构建一个 aop propxy,记录交互并获取方法和参数,为
  • 构建 link
  • 它发现此方法的映射以构造 link

ControllerLinkBuilder 非常方便,因为它避免了重复映射中已包含的逻辑。

我想出了一个简单的示例应用程序和一个非常基本的基准来衡量和比较 link 构建器性能

它基于一个简单的控制器 - 它只返回 100 个简单的对象 - 每个对象都携带一个自 link。

@RestController
@RequestMapping("items")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {

    private final ItemResourceProcessor resourceProcessor;

    @RequestMapping(method = GET)
    public ResponseEntity<List<Resource<Item>>> getAll() {
        List<Resource<Item>> items = new ArrayList<>(100);
        for (int i = 0; i < 100; i++) {
            items.add(resourceProcessor.process(
                    new Resource<>(new Item(i, UUID.randomUUID().toString()))));
        }

        return ResponseEntity.ok(items);
    }

    @RequestMapping(method = GET, path = "/{id}")
    public ResponseEntity<Resource<Item>> getOne(@PathVariable Integer id) {
        return null;
    }
}

ItemResourceProcessor 添加了一个简单的 self-link 我尝试并测量了三种不同的选择:

1. ControllerLinkBuilder with linkTo(methodOn)

此处控制器LinkBuilder 用于检查控制器和方法上的映射 - 每个 link 生成都需要一个 aop 代理。

@Component
public class ItemResourceProcessor implements ResourceProcessor<Resource<Item>> {

    @Override
    public Resource<Item> process(Resource<Item> resource) {
        resource.add(linkTo(methodOn(TestController.class).getOne(resource.getContent().getId())).withSelfRel());
        return resource;
    }
}

此变体的结果如下:

    wrk -t2 -c5 -d30s http://localhost:8080/items

    Running 30s test @ http://localhost:8080/items
  2 threads and 5 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.77ms    0.93ms  25.57ms   83.97%
    Req/Sec   420.87     48.63   500.00     71.33%
  25180 requests in 30.06s, 305.70MB read
Requests/sec:    837.63

2。没有 methodOn() 的控制器Link构建器

这里避免了对 methodOn() 的调用,方法引用在创建资源处理器时确定一次,并重新用于生成 link。此版本避免了 methodOn 的开销,但仍会发现方法上的映射以生成 link.

@Component
public class ItemResourceProcessor implements ResourceProcessor<Resource<Item>> {

    private Method method = ReflectionUtils.findMethod(TestController.class, "getOne", Integer.class);

    @Override
    public Resource<Item> process(Resource<Item> resource) {
    resource.add(linkTo(method, resource.getContent().getId()).withSelfRel());
    return resource;
    }
}

结果比第一个版本略好。优化只给我们带来了很小的好处。

wrk -t2 -c5 -d30s http://localhost:8080/items

Running 30s test @ http://localhost:8080/items
  2 threads and 5 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.02ms  477.64us  13.80ms   84.01%
    Req/Sec   499.42     18.24   540.00     65.50%
  29871 requests in 30.05s, 365.50MB read
Requests/sec:    994.03

3。 Link 使用 BasicLinkBuilder

生成

这里我们从 ControllerLinkBuilder 移开并使用 BasicLinkBuilder。此实现不执行控制器映射的任何内省,因此是参考基准的一个很好的候选者。

@Component
public class ItemResourceProcessor implements ResourceProcessor<Resource<Item>> {

    private ControllerLinkBuilder baseLink;

    @Override
    public Resource<Item> process(Resource<Item> resource) {
      resource.add(BasicLinkBuilder.linkToCurrentMapping()
            .slash("items")
            .slash(resource.getContent().getId()).withSelfRel());
      return resource;
    }
}

结果再次优于上次

wrk -t2 -c5 -d30s http://localhost:8080/items

Running 30s test @ http://localhost:8080/items
  2 threads and 5 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.05ms  683.71us  12.84ms   72.12%
    Req/Sec   658.31     87.79   828.00     66.67%
  39349 requests in 30.03s, 458.91MB read
Requests/sec:   1310.14

总结

当然还有methodOn()的开销。测试表明,与 BasicLinkBuilder.

相比,100 links 平均耗时不到 2ms

因此,当渲染的 link 数量不是很大时,ControllerLinkBuilder 的便利性使其成为 link 一代的不错选择。

免责声明:我知道我的 wrk 测试不是正确的基准测试 - 但结果可以重复并显示相同的结果比较替代方案 - 所以他们至少可以提供提示关于性能差异的维度)