PersistentEntityResource низкая производительность сериализации

У меня очень низкая производительность сериализации для пользовательского метода @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

Mikhail Kadan 16.04.2018 11:45
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
1
122
0

Другие вопросы по теме