Stream API 不适用于 EclipseLink / Glassfish 中的延迟加载集合?

Stream API not working for lazy loaded collections in EclipseLink / Glassfish?

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

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

当我肯定地知道域列表包含一个名称等于所提供的 name 的域时,此行 return 是错误的。因此,在摸索了一会儿之后,我最终拆分了整条线以查看发生了什么。我在调试会话中得到以下信息:

请注意以下行:

List<Domain> domains2 = domains.stream().collect(Collectors.toList());

根据调试器,domains 是一个包含两个元素的列表。但是在应用 .stream().collect(Collectors.toList()) 之后,我得到了一个完全空的列表。如果我错了,请纠正我,但据我了解,这应该是身份操作和 return 相同的列表(或者如果我们严格的话,它的副本)。那么这里发生了什么???

在你问之前:不,我根本没有操纵那个截图。

为了将此放在上下文中,此代码在有状态请求范围内的 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 上

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

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

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

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

所以我们到了,IndirectList 不知不觉地从 Vector 继承了新功能(记住它可能首先不应该从它继承),并用这种错误组合破坏了东西.

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

变通方法:如果您遇到此问题但仍想利用 stream() 提供的函数式编程风格,您可以制作该集合的副本,这样一个合适的迭代器被构建。在我的例子中,我能够将域的所有类似用法保留为 one-liners 修改 getDomains 方法。在这种情况下,我更喜欢代码可读性(具有函数式风格)而不是性能:

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

READER注意事项:抱歉讽刺,但我不想在这些事情上浪费我宝贵的开发时间。

感谢@Ferrybig 提供的初步线索

更新:错误报告。如果这对您有影响,您可以在 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 流 API https://bugs.eclipse.org/bugs/show_bug.cgi?id=467470.

未解决的错误