Stream API не работает для лениво загруженных коллекций в EclipseLink / Glassfish?
После обнаружения дефекта в одном из моих веб-сервисов я отследил ошибку до следующего однострочного:
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
Эта строка возвращала false, когда я положительно знал, что список доменов содержит домен, имя которого было равно указанному name. Поэтому, почесав немного в затылке, я в конце концов разделил всю линию, чтобы посмотреть, что происходит. Я получил следующее В моей сессии отладки:
Пожалуйста, обратите внимание на следующее строка:
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
Согласно отладчику, domains - это список из двух элементов. Но после применения .stream().collect(Collectors.toList()) я получаю совершенно пустой список. Поправьте меня, если я ошибаюсь, но из того, что я понимаю, это должна быть операция идентификации и возврат того же списка (или его копии, если мы строги). Так что же здесь происходит???
Прежде чем вы спросите: Нет, я вообще не манипулировал этим скриншотом.
Чтобы поместить это в контекст, этот код выполняется в запросе с сохранением состояния область применения EJB с использованием управляемых сущностей JPA с доступом к полю в расширенном контексте сохраняемости. Вот некоторые части кода, относящиеся к рассматриваемой проблеме:
@Stateful
@RequestScoped
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class DomainResources {
@PersistenceContext(type = PersistenceContextType.EXTENDED) @RequestScoped
private EntityManager entityManager;
public boolean templateContainsDomainWithName(String name) { // Extra code included to diagnose the problem
MetadataTemplate template = this.getTemplate();
List<Domain> domains = template.getDomains();
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
List<String> names = domains.stream().map(Domain::getName).collect(Collectors.toList());
boolean exists1 = names.contains(name);
boolean exists2 = this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
}
@POST
@RolesAllowed({"root"})
public Response createDomain(@Valid @EmptyID DomainDTO domainDTO, @Context UriInfo uriInfo) {
if (this.getTemplate().getLastVersionState() != State.DRAFT) {
throw new UnmodifiableTemplateException();
} else if (templateContainsDomainWithName(domainDTO.name)) {
throw new DuplicatedKeyException("name", domainDTO.name);
} else {
Domain domain = this.getTemplate().createNewDomain(domainDTO.name);
this.entityManager.flush();
return Response.created(uriInfo.getAbsolutePathBuilder().path(domain.getId()).build()).entity(new DomainDTO(domain)).type(MediaType.APPLICATION_JSON).build();
}
}
}
@Entity
public class MetadataTemplate extends IdentifiedObject {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "metadataTemplate", orphanRemoval = true) @OrderBy(value = "creationDate")
private List<Version> versions = new LinkedList<>();
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) @OrderBy(value = "name")
private List<Domain> domains = new LinkedList<>();
public List<Version> getVersions() {
return Collections.unmodifiableList(versions);
}
public List<Domain> getDomains() {
return Collections.unmodifiableList(domains);
}
}
Я включил оба метода getVersions и getDomains, потому что у меня есть аналогичные операции, выполняемые безупречно на версиях. Единственное существенное различие, которое я могу найти, заключается в том, что versions охотно выбираются, а domains лениво выбираются. Но насколько мне известно код выполняется внутри транзакции и списка доменов идет погрузка. Если нет, я получу исключение ленивой инициализации, не так ли?
UPDATE : следуя предложению @Ferrybig, я исследовал проблему немного дальше, и там, кажется, нет ничего общего с неправильной ленивой загрузкой. Если я пройдусь по коллекции классическим способом, я все равно не смогу получить правильные результаты, используя потоки:
boolean found = false;
for (Domain domain: this.getTemplate().getDomains()) {
if (domain.getName().equals(name)) {
found = true;
}
}
List<Domain> domains = this.getTemplate().getDomains();
long estimatedSize = domains.spliterator().estimateSize(); // This returns 0!
domains.spliterator().forEachRemaining(domain -> {
// Execution flow never reaches this point!
});
Таким образом, кажется, что даже когда коллекция была загружена, у вас все еще есть это странное поведение. Это, кажется, пропавший или пустая реализация spliterator не в прокси-сервер, используемый для управления ленивые коллекции. А ты как думаешь?
Кстати, это развернуто на Glassfish / EclipseLink
2 ответов:
Проблема здесь возникает из-за комбинации чужих ошибок в нескольких местах. Сумма всех этих ошибок провоцирует такое поведение багги.
Первая ошибка: сомнительное наследство . EclipseLink, по-видимому, создает прокси-сервер для управления ленивыми коллекциями типа
org.eclipse.persistence.indirection.IndirectList. Этот класс расширяетсяjava.util.Vectorхотя он перекрывает все, кромеremoveRange. Почему, дорогие разработчики Eclipse, вы расширяете класс, чтобы переопределить почти все в Родительском, а не объявление этого класса для реализации подходящего интерфейса (Iterable<E>,Collection<E>илиList<E>)?Вторая ошибка: Эй, я унаследовал от вас, но не даю $#|T о ваших внутренних органах . Так
IndirectListделает свою магию ленивой загрузки вещей с помощью делегата . Но, боже мой! Как вычислить размер? Я использую (и постоянно обновляет) родителяelementCountсобственность? Нет, конечно, я просто делегирую эту задачу своемуделегату ... так что если родительский класс должен делать что-то, связанное с размером, ну, не повезло. Во всяком случае, я все преодолел... и они не добавят ничего нового к этому классу, не так ли?Третья ошибка: разрыв инкапсуляции . Входит
Vector. В Java 1.8 этот класс расширен и теперь предоставляет методspliteratorдля поддержки новых функций потока. Они создают статический внутренний класс (VectorSpliterator), который позволяет клиентам пересекать вектор с помощью блестящего нового API. Все в порядке, пока вы не заметите, что для того, чтобы знать, когда закончить обход они используют переменная защищенного экземпляраelementCountвместо использования метода public APIsize(). Потому что кто будет расширять не конечный класс и возвращать размер, не основанный наelementCount? Вы видите приближение катастрофы?Итак, мы здесь,
IndirectListнеосознанно наследует новую функциональность отVector(помните, что она, вероятно, не должна наследовать от него в первую очередь), и ломает вещи с этой комбинацией ошибок.Подводя итог, кажется, что обход потока ленивых коллекции не будут работать даже для уже загруженных коллекций при использовании EclipseLink (поставщик JPA по умолчанию в Glassfish) . Помните, что эти продукты поступают от одного и того же поставщика. Ура!
Обходной путь: Если вы столкнулись с этой проблемой и все еще хотите использовать функциональный стиль программирования, предоставляемый
stream(), вы можете сделать копию коллекции, чтобы построить правильный итератор. В моем случае я смог сохранить все подобные использования доменов в качестве однострочных, модифицирующихgetDomainsметод. В этом случае я предпочитаю читаемость кода (с функциональным стилем) производительности:public List<Domain> getDomains() { return Collections.unmodifiableList(new ArrayList<>(domains)); }примечание для читателя : извините за сарказм, но я ненавижу терять свое драгоценное время разработки с этими вещами.
Спасибо @Ferrybig за первоначальный ключ
UPDATE : сообщение об ошибке. Если это поразило вас, вы можете следить за его прогрессом в https://bugs.eclipse.org/bugs/show_bug.cgi?id=487799
Я столкнулся с очень похожей проблемой с этим кодом в модульном тесте:
Optional<ChildTable> ct = st.getChildren().stream().filter(i -> i.getId().equals(20001000l)).findFirst();Ct.get () не удалось с NoSuchElementException.
Обновление от EclipseLink 2.5.2 2.6.2, чтобы решить эту проблему. Вы не упомянули версию EclipseLink.
Я думаю, что ваш отчет об ошибке является дубликатом https://bugs.eclipse.org/bugs/show_bug.cgi?id=433075 .
Смотрите также неразрешенную ошибку с EclipseLink и Java 8 stream API https://bugs.eclipse.org/bugs/show_bug.cgi?id=467470 .

Comments