问题
The setup: So I have a RESTfull API written in java, using spring-boot
and spring-hates
for adding links to the resource (Hypermedia-Driven RESTful Web Service). Everything I have is standard and no additional settings or changes are made
The problem
- Case: no links on resource - Chrome TTFB avg. (10 runs) 400ms for 1000 items
- Case: 1 self reference link on resource - Chrome TTFB avg. (10 runs) 1500ms for 1000 items
I am using this official guide
The question
Why adding only 1 link to my resource adds additional 1 second for the processing the request. I will need around 5 - 7 links on each resource and every resource has additional embedded ones?
For 9000 total items with only 1 link per item (included the nested ones), i have to wait 30 sec for the response and without links ~ 400 ms.
P.S. The additional code is irrelevant because I am just adding a code from the tutorial which effects the performance dramatically.
Edit 1
As suggested I am adding example code from my TextItem
constructor
add(linkTo(methodOn(TestController.class).getTestItems()).withRel("testLink"));
Edit 2
So the following example proposed from @Mathias Dpunkt works absolutely perfect
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;
}
The new problem
Controller:
@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;
}
}
If the controller method takes @RequestParam
the posted solution does not append it to the link. When I call
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;
}
回答1:
EDIT:
Updated post to use improved versions - Spring HATEOAS 0.22 and Spring Framework 4.3.5 / 5.0 M4.
Answer:
This is very interesting. I had a look at the source code for the ControllerLinkBuilder
methods linkTo
and methodOn
and there is a lot going on for a simple link:
- builds an aop propxy for the controller that records the interactions and gets hold of the method and the parameters to build the link for
- it discovers the mappings of this method to construct the link
The ControllerLinkBuilder
is very convenient because it avoids duplicating logic that is already contained in your mapping.
I came up with a simple sample application and a very basic benchmark to measure and compare link builder performance
It is based on a simple controller - it is just returning 100 simple objects - each is carrying one self-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;
}
}
The ItemResourceProcessor
adds a simple self-link and I tried and measured three different alternatives:
1. ControllerLinkBuilder with linkTo(methodOn)
Here ControllerLinkBuilder is used to inspect the mapping on controller and method - which needs an aop proxy for every link generated.
@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;
}
}
The results for this variant are these:
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. ControllerLinkBuilder without methodOn()
Here the call the to methodOn()
is avoided and the method reference is determined once at the creation of the resource processor and reused to generate the link. This version avoids the overhead of methodOn but still discovers the mapping on the method to generate the 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;
}
}
The results are slightly better than for the first version. The optimization is giving us only small benefits.
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 generation using BasicLinkBuilder
Here we move away from ControllerLinkBuilder
and use BasicLinkBuilder
. This implementation is not performing any introspections of controller mappings and is thus a good candidate for reference benchmark.
@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;
}
}
The results are again better than the previous
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
Summary
Of course, there is an overhead of methodOn(). The tests show that 100 links cost us less than 2ms on average compared to BasicLinkBuilder
.
So when the amounts of rendered links are not massive the convenience of ControllerLinkBuilder
is making it a good choice for link generation.
DISCLAIMER: I know my wrk tests are not proper benchmarks - but the results could be repeated and showed the same results comparing the alternatives - so they at least can provide a hint on the dimensions of differences in performance)
来源:https://stackoverflow.com/questions/36303642/spring-hateoas-controllerlinkbuilder-methodon-increasing-response-times-signific