Spring Method HATEROAS ControllerLinkBuilder. Increased response time significantly

Setup: So, I have a RESTfull API written in java using spring-boot and spring-hates to add links to a resource (Hypermedia-Driven RESTful Web Service). All that I have is standard and no additional settings or changes have been made

Problem

  • Case: without links to the resource - Chrome TTFB avg. (10 runs) 400 ms for 1000 items.
  • Case: 1 link for an independent link on a resource - Chrome TTFB avg. (10 runs) 1500 ms for 1000 items

I use this official guide

Question

Why adding only 1 link to my resource adds an additional 1 second to process the request. I need about 5-7 links to each resource, and each resource has additional built-in functions?

For 9000 common elements with 1 link to an element (including nested ones), I have to wait 30 seconds for a response and without links ~ 400 ms.

PS Extra code doesn't matter because I'm just adding code from a tutorial that dramatically affects performance.

Change 1

As I said, I'm adding sample code from my TextItem constructor

 add(linkTo(methodOn(TestController.class).getTestItems()).withRel("testLink")); 

Edit 2

So the following example suggested by @Mathias Dpunkt works absolutely flawlessly

 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; } 

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 accepts @RequestParam , the published solution does not add 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; } 
+2
source share
1 answer

EDIT:

Updated message to use improved versions - Spring HATEOAS 0.22 and Spring Framework 4.3.5 / 5.0 M4 .

Answer:

It is very interesting. I looked at the source code for the ControllerLinkBuilder linkTo and methodOn , and there is a lot of methodOn for a simple link:

  • creates a proxy server for the controller, which records the interactions and receives the method and parameters for creating the link for
  • it discovers mappings of this method to build the link

ControllerLinkBuilder very convenient as it avoids duplicating the logic that is already contained in your mapping.

I came up with a simple sample application and a very simple test for measuring and comparing link linker performance

It is based on a simple controller - it simply returns 100 simple objects - each of them carries one link of its own.

 @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 adds simple self-tightening, and I tried and measured three different alternatives:

1. ControllerLinkBuilder with linkTo (methodOn)

Here, ControllerLinkBuilder is used to verify that the controller and the method need aop proxy for each link created.

 @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; } } 

Results for this option:

  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 the On () method

The methodOn() call is called methodOn() , and the method reference is determined once when the resource processor is created and reused to generate the link. This version avoids the overhead of the On method, but still detects a method mapping for creating 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. Optimization gives us only small advantages.

 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 does not perform any introspection of controller mappings and is therefore a good candidate for a 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 better again.

 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 the On () method. Tests show that 100 links cost us less than 2 ms on average compared to BasicLinkBuilder .

Therefore, when the number of displayed links is not massive, the convenience of ControllerLinkBuilder makes it a good choice for generating links.

DISCLAIMER . I know that my wrk tests are not proper benchmarks, but the results can be repeated and show the same results comparing alternatives - so they can at least give an idea of ​​the size of the differences in performance)

+7
source

Source: https://habr.com/ru/post/1207388/


All Articles