У меня очень низкая производительность сериализации для пользовательского метода @RepositoryRestController
, возвращающего PagedResources<PersistentEntityResource>
, например Я получаю 15 секунд сериализации для данных JSON размером 2,5 МБ вместо 0,5 секунды после того, как я сделал обходной путь (подробнее об этом позже).
Учти это:
@Entity
public class Content {
@OneToMany
private Set<ContentMapping> contentMappings = new HashSet<>();
// ...
}
@RepositoryRestController
public class MyController {
private final ContentService contentService;
private final PagedResourcesAssembler pagedResourcesAssembler;
public ContentRestController(
ContentService contentService,
PagedResourcesAssembler pagedResourcesAssembler) {
this.contentService = contentService;
this.pagedResourcesAssembler = pagedResourcesAssembler;
}
@RequestMapping(value = "/findContent", method = RequestMethod.GET)
@ResponseBody
public PagedResources<PersistentEntityResource> findContent(PersistentEntityResourceAssembler resourceAssembler) {
Page<Content> page = contentService.getContent();
@SuppressWarnings("unchecked")
PagedResources<PersistentEntityResource> pagedResources = pagedResourcesAssembler.toResource(page, resourceAssembler);
return pagedResources;
}
}
Для полного ответа на вызов /findContent
требуется 15 секунд (в то время как потоковая передача данных начинается сразу после того, как это сделано, это похоже на время сериализации 15 секунд).
После профилирования я обнаружил, что причиной проблемы являются постоянные свойства коллекции на Content
. Во время сериализации Content
новая транзакция открывается для каждой попытки доступа к коллекции contentMappings
, даже если contentMappings
был правильно выбран перед сериализацией внутри вызова contentService.getContent()
.
Открытие явной транзакции в методе контроллера не помогло (потому что оно закрывается после выхода из метода и до того, как произойдет сериализация), но я смог обойти это поведение, используя HttpServletResponse
и вручную сериализуя ответ:
@RepositoryRestController
public class MyController {
private final ContentService contentService;
private final PagedResourcesAssembler pagedResourcesAssembler;
private final List<HttpMessageConverter> messageConverters;
public ContentRestController(
ContentService contentService,
PagedResourcesAssembler pagedResourcesAssembler,
List<HttpMessageConverter> messageConverters) {
this.contentService = contentService;
this.pagedResourcesAssembler = pagedResourcesAssembler;
this.messageConverters = messageConverters;
}
@RequestMapping(value = "/findContent", method = RequestMethod.GET)
@ResponseBody
@Transactional(readOnly = true)
public void findContent(PersistentEntityResourceAssembler resourceAssembler, HttpServletResponse response) throws IOException {
Page<Content> page = contentService.getContent();
@SuppressWarnings("unchecked")
PagedResources<PersistentEntityResource> pagedResources = pagedResourcesAssembler.toResource(page, resourceAssembler);
// manual response serialization
MediaType mediaType = MediaType.valueOf("application/hal+json");
ResponseEntity<String> responseEntity = messageConverters.stream()
.filter(messageConverter -> messageConverter.canWrite(pagedResources.getClass(), mediaType))
.findFirst()
.map(messageConverter -> {
HttpOutputMessage outputMessage = new HttpOutputMessage() {
private final OutputStream outputStream = new ByteArrayOutputStream();
private final HttpHeaders httpHeaders = new HttpHeaders();
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return httpHeaders;
}
};
try {
messageConverter.write(pagedResources, mediaType, outputMessage);
return ResponseEntity.ok()
.headers(outputMessage.getHeaders())
.body(new String(((ByteArrayOutputStream) outputMessage.getBody()).toByteArray(), StandardCharsets.UTF_8));
} catch (IOException e) {
throw new IllegalStateException("Failed to convert output to " + mediaType.toString());
}
})
.orElseThrow(() -> new IllegalStateException("Failed to convert output to " + mediaType.toString()));
response.setContentType(mediaType.toString());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().write(responseEntity.getBody());
responseEntity.getHeaders().entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(value -> Tuples.of(entry.getKey(), value)))
.forEach(t -> response.addHeader(t.getT1(), t.getT2()));
response.flushBuffer();
}
}
Таким образом, ответ будет получен через 0,5 секунды вместо 15 секунд.
Проблемы, которые я вижу с этим обходным решением, например, полностью игнорируя обработку RequestBodyAdvice
/ ResponseBodyAdvice
и необходимость вручную работать с HttpServletResponse
и HttpMessageConverter
, эффективно дублируя код Spring.
Итак, мой вопрос в том, что я делаю не так, потому что, если все в порядке, я открою отчет об ошибке в Spring Jira.
Открыт тикет JIRA: jira.spring.io/browse/DATAREST-1233