【问题标题】:Stream API not working for lazy loaded collections in EclipseLink / Glassfish?Stream API 不适用于 EclipseLink / Glassfish 中的延迟加载集合?
【发布时间】:2016-02-12 12:22:56
【问题描述】:

在我的一项网络服务中检测到缺陷后,我将错误追踪到以下单行:

return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));

当我确定域列表包含一个名称等于提供的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);
    }
}

我已经包含了getVersionsgetDomains 方法,因为我有类似的操作在版本上完美运行。我能找到的唯一显着区别是versions 被急切地获取,而domains 被懒惰地获取。但据我所知,代码正在事务中执行,并且正在加载域列表。如果不是,我会得到一个惰性初始化异常,不是吗?

更新:按照@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!
});

因此,即使集合已加载,您似乎仍然有这种奇怪的行为。这似乎是用于管理惰性集合的代理中缺少或空的拆分器实现。你怎么看?

顺便说一句,这是部署在 Glassfish / EclipseLink 上

【问题讨论】:

  • 首先出现的是延迟加载中的一个错误,实现该方法的api可能不支持延迟加载列表上的splititerator或正常迭代,你能检查一下返回吗splitIterator()iterator() 的结果,看看它们是否有元素?
  • 你能发布导入吗?
  • domains.spliterator().estimateSize() 返回 0,并且不能使用 forEachRemaining 遍历元素(消费者内部的代码永远不会被执行)。 iterator 确实可以正常工作,让我可以遍历元素。不知道这是否相关,但 spliterator 返回一个 java.util.Vector$VectorSpliterator 对象,而 iterator 返回一个 java.util.Collections$UnmodifiableCollection$1(我猜是匿名内部类)
  • @hahn 这些类中有很多导入,你在想什么,所以我可以只发布相关的吗?
  • 好奇,“收藏家”是否来自第三方库

标签: java jpa jakarta-ee jax-rs jboss-arquillian


【解决方案1】:

这里的问题来自于其他人在几个地方的错误的组合。所有这些错误的总和引发了这种错误的行为。

第一个错误:可疑的继承。 EclipseLink 似乎创建了一个代理来管理org.eclipse.persistence.indirection.IndirectList 类型的惰性集合。此类扩展 java.util.Vector,尽管它覆盖了除removeRange 之外的所有内容。亲爱的 Eclipse 开发人员,到底为什么要扩展一个类来覆盖父级中的几乎所有内容,而不是声明该类来实现合适的接口(@98​​7654326@、Collection&lt;E&gt;List&lt;E&gt;)?

第二个错误:嘿,我继承了你,但没有给出关于你的内部结构的 $#|T。所以IndirectList 使用 delegate 实现了延迟加载事物的魔力。但是,天哪!如何计算大小?我是否使用(并保持更新)父母的elementCount 属性?不,当然,我只是将该任务委托给我的委托...所以如果父类需要做任何与大小相关的事情,那么,运气不好。无论如何,我已经覆盖了所有内容......而且他们不会在该类中添加任何新内容,是吗?

第三个错误:封装破损。输入Vector。在 Java 1.8 中,这个类得到了增强,现在提供了一个 spliterator 方法来支持新的流功能。他们创建了一个静态内部类 (VectorSpliterator),允许客户端使用闪亮的新 API 遍历向量。一切正常,直到您注意到为了知道何时完成遍历,他们使用 受保护的实例变量 elementCount 而不是使用公共 API 方法 size()。因为谁会扩展非最终类并返回不基于elementCount 的大小?你看到灾难来临了吗?

所以我们在这里,IndirectList 不知不觉地从 Vector 继承了新功能(请记住,它可能一开始不应该从它继承),并且由于这些错误组合而破坏了事物。

总结起来,似乎在使用 EclipseLink(Glassfish 中的默认 JPA 提供程序)时,即使对于已加载的集合,延迟集合的流遍历也不起作用。请记住,这些产品来自同一供应商。万岁!

解决方法:如果您遇到此问题,但仍想利用stream() 提供的函数式编程风格,您可以复制集合,以便构建适当的迭代器。就我而言,我能够将域的所有类似用途保留为修改getDomains 方法的单行。在这种情况下,我更喜欢代码可读性(具有函数式风格)而不是性能:

public List<Domain> getDomains() {
    return Collections.unmodifiableList(new ArrayList<>(domains));
}

读者注意:很抱歉,我很抱歉,但我不想在这些事情上浪费我宝贵的开发时间。

感谢@Ferrybig 提供初步线索

更新:已报告错误。如果这对您有所影响,您可以在https://bugs.eclipse.org/bugs/show_bug.cgi?id=487799

关注它的进展

【讨论】:

  • 这个问题不仅仅影响流处理。 This older answersort 描述了这个问题。
【解决方案2】:

我在单元测试中遇到了与此代码非常相似的问题:

Optional&lt;ChildTable&gt; ct = st.getChildren().stream().filter(i -&gt; 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 流 API https://bugs.eclipse.org/bugs/show_bug.cgi?id=467470 未解决的错误。

【讨论】:

  • 是的,您说得对,这是由于旧版本的 EclipseLink 造成的。我对这些错误感到非常惊讶(和欺骗)。我可能是错的,但是这个和其他类似的事情让我觉得他们甚至没有一个测试电池来涵盖所有这些问题。好难过。
猜你喜欢
  • 2013-09-15
  • 1970-01-01
  • 1970-01-01
  • 2013-09-17
  • 2012-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多