设置:所以我有一个用java编写的RESTful API,使用spring-boot
和spring-hates
来添加到资源的链接(Hypermedia-Driven RESTful Web Service)。我所拥有的一切都是标准的,没有进行额外的设置或更改
问题
- 案例:资源上没有链接-Chrome TTFB平均值(10次运行)400毫秒,1000个项目
- 案例:资源上的1个自参考链接-Chrome TTFB平均值(10次运行)1500毫秒,1000个项目
我正在使用这个官方指南
问题
为什么只向我的资源添加1个链接会为处理请求增加额外的1秒。我需要每个资源上大约5-7个链接,每个资源都有额外的嵌入链接?
对于总共9000个项目,每个项目只有1个链接(包括嵌套的),我必须等待30秒的响应,而没有链接~400毫秒。
附言:额外的代码无关紧要,因为我只是添加了教程中的一个代码,它会显著影响性能。
编辑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
,则发布的解决方案不会将其附加到链接。当我呼叫时
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;
}
编辑:
更新后使用改进的版本-Spring HATEOAS 0.22和Spring Framework 4.3.5/5.0 M4。
答案:
这很有趣。我看了ControllerLinkBuilder
方法linkTo
和methodOn
的源代码,对于一个简单的链接有很多内容:
- 为控制器构建一个aop-propxy,用于记录交互并获取用于构建链接的方法和参数
- 它发现该方法的映射以构建链接
ControllerLinkBuilder
非常方便,因为它避免了重复映射中已经包含的逻辑。
我提出了一个简单的示例应用程序和一个非常基本的基准来衡量和比较链接生成器性能
它基于一个简单的控制器——它只返回100个简单的对象——每个对象都携带一个自链接。
@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
添加了一个简单的自链接,我尝试并测量了三种不同的替代方案:
1.带linkTo(methodOn)的ControllerLinkBuilder
这里,ControllerLinkBuilder用于检查控制器和方法上的映射,该方法需要为生成的每个链接提供一个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()的ControllerLinkBuilder
这里避免了对methodOn()
的调用,并且方法引用在创建资源处理器时确定一次,并被重用以生成链接。此版本避免了methodOn的开销,但仍然可以发现方法上的映射以生成链接。
@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.使用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个链路的平均开销不到2ms。
因此,当呈现的链接数量不多时,ControllerLinkBuilder
的便利性使其成为链接生成的良好选择。
免责声明:我知道我的wrk测试不是合适的基准测试,但结果可以重复,并显示出与备选方案相比相同的结果,因此它们至少可以提供性能差异的提示)