对双向相关bean的一些思考

本文探讨了双向相关bean在应用建模中的作用,通过实例展示了其在一对多关系中的优势,如序列化时能保持一致性,以及在处理多对多关系时的潜力。文中还提到了双向相关特性在模拟真实世界对象和构造大型应用中的价值,并预告后续将讨论更广泛的关系类型。

对双向相关bean的一些思考

最近一直在新项目中做建模工作,决定这次使用双向相关bean。这个名字听着挺唬人,然而却是做应用的程序员最常接触到的东西。对于不喜欢看长文章的读者,我直接上一组完整的双向相关bean来进行说明。

  • 以下代码描述了一个文章类(Article)和一个作者类(Writer)以及它们共同继承的基类(PojoSupport),文章和作者关系为“一篇文章只对应一位作者,一位作者可对应多篇文章,我们不考虑多位作者合著一篇文章的情况。”
public class Article extends PojoSupport<Article> {

    private int id;

    private Writer writer;

    private String title;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Writer getWriter() {
        return writer;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setWriter(Writer newWriter) {
        if (this.writer == null || !this.writer.equals(newWriter)) {
            if (this.writer != null) {
                Writer oldWriter = this.writer;
                this.writer = null;
                oldWriter.removeArticle(this);
            }
            if (newWriter != null) {
                this.writer = newWriter;
                this.writer.addArticle(this);
            }
        }
    }
}
public class Writer extends PojoSupport<Writer> {

    private int id;

    private String name;

    private java.util.Collection<Article> article;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void addArticle(Article newArticle) {
        if (newArticle == null)
            return;
        if (this.article == null)
        /*如果对article的顺序没有要求,这里以及下面所有出现LinkedHashSet的地方都可以改为HashSet*/
            this.article = new java.util.LinkedHashSet<Article>();
        if (!this.article.contains(newArticle)) {
            this.article.add(newArticle);
            newArticle.setWriter(this);
        }
    }

    public java.util.Iterator<Article> getIteratorArticle() {
        if (article == null)
            article = new java.util.LinkedHashSet<Article>();
        return article.iterator();
    }

    public java.util.Collection<Article> getArticle() {
        if (article == null)
            article = new java.util.LinkedHashSet<Article>();
        return article;
    }

    public void removeAllArticle() {
        if (article != null) {
            Article oldArticle;
            for (java.util.Iterator<Article> iter = getIteratorArticle(); iter
                    .hasNext();) {
                oldArticle = iter.next();
                iter.remove();
                oldArticle.setWriter(null);
            }
        }
    }

    public void removeArticle(Article oldArticle) {
        if (oldArticle == null)
            return;
        if (this.article != null)
            if (this.article.contains(oldArticle)) {
                this.article.remove(oldArticle);
                oldArticle.setWriter((Writer) null);
            }
    }

    public void setArticle(java.util.Collection<Article> newArticle) {
        removeAllArticle();
        for (java.util.Iterator iter = newArticle.iterator(); iter.hasNext();)
            addArticle((Article) iter.next());
    }
}

然后还有一个辅助类PojoSupport,它的用处是提供一些辅助方法,使得从数据库中加载的对象可以像java本地对象一样具有等价替换的功能。

public abstract class PojoSupport<T extends PojoSupport<T>> {
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
        return result;
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        PojoSupport<?> other = (PojoSupport<?>) obj;
        if (getId() == null) {
            if (other.getId() != null)
                return false;
        } else if (!getId().equals(other.getId()))
            return false;
        return true;
    }
}

以上就是一个经典的一对多关系双向相关bean使用java的实现了。相对于非双向相关bean,它多出了不少代码,那它有哪些好处呢?请看下面这种情形:

Article a = new Article();
Writer w = new Writer();
a.setWriter(w);//或者 w.addArticle(a);

这样a和w就建立了对应关系,值得注意的是,上面最后一行的两种表达方式实现的效果是完全相同的。
同样在a和w脱离关系时,以下两种方式也是完全等效的:

a.setWriter(null);//方式1
w.removeArticle(a);//方式2

有些时候你得不到w的句柄,但你又需要改变这个writer的状态,这时你就会发现方式1是多么的可爱了。

我们假设a和w并没有脱离关系,当你将a或者w序列化时(比如序列化为json),本质上来说序列化这两者得到的内容是相同的。例如我们先给article和writer充实一下内容

a.setId(2);
a.setTitle("标题");
w.setId(3);
w.setName("姓名");

把w序列化后的json为

{"article":[{"id":2,"title":"标题","writer":{"$ref":"$"}}],"id":3,"name":"姓名"}

把a序列化后的json为:

{"id":2,"title":"标题","writer":{"article":[{"$ref":"$"}],"id":3,"name":"姓名"}}

这两个json虽然看起来不一样,但信息量是完全一样的。当然,在前端想真正发挥双向相关bean的长处,你还需要一个先进一点的json解析器,可以参考这篇文章 >http://www.oschina.net/question/109503_120507

也许这还说明不了什么问题,为了体现出“一对多”的价值,我们再加入另一篇文章otherA,这篇文章的作者同样是w:

Article otherA = new Article();
otherA.setId(5);
otherA.setTitle("另一篇");
otherA.setWriter(w);
//实现同样效果的另一种方式是w.addArticle(otherA);

然后我们把a序列化:

{"id":2,"title":"标题","writer":{"article":[{"$ref":"$"},{"id":5,"title":"另一篇","writer":{"$ref":"$.writer"}}],"id":3,"name":"姓名"}}

可以看出a中包含了otherA的信息,同样如果我们把otherA序列化:

{"id":5,"title":"另一篇","writer":{"article":[{"id":2,"title":"标题","writer":{"$ref":"$.writer"}},{"$ref":"$"}],"id":3,"name":"姓名"}}

可以看出otherA中也包含了a的信息。同时我们还可以注意到在w内部a和otherA的顺序是固定的,这是因为我们先建立w和a的关系再建立w和otherA的关系,并且我们在java代码中使用了LinkedHashSet的缘故。(如果使用HashSet那每一次的顺序就都是随机的了)
最后,我们当然还要看看把w序列化的样子:

{"article":[{"id":2,"title":"标题","writer":{"$ref":"$"}},{"id":5,"title":"另一篇","writer":{"$ref":"$"}}],"id":3,"name":"姓名"}

可以看出,w的序列化也包含全部信息,并且w中a和otherA的顺序仍然是固定的。


结论:以上情形说明双向相关特性就像“胶水”一样,可以把bean紧紧的粘在一起,这对于我们模拟复杂的真实世界对象和构造大型健壮的应用软件是极有好处的。然而真实世界中还有一种普遍的关系我们没有涉及,即多对多关系,这就留待下一篇博文《对双向相关bean的更多思考》来讲吧。

你这个问题非常关键,说明你在认真思考调用链的源头! > ❓**“所以我应该去找 `ThreadPoolExecutor.execute` 方法?”** 答案是:❌ **不要去改 `ThreadPoolExecutor.execute()`,它是 JDK 内部方法,你不能也不应该修改它。** 但 ✅ **你可以(也必须)理解它为什么被调用、在什么条件下失败,并从你的代码中防止这种错误发生。** --- ## ✅ 一、先澄清一个误解:`ThreadPoolExecutor.execute()` 不是你“要找的地方” ### 🧨 异常信息本质: ```java java.util.concurrent.RejectedExecutionException: Task io.grpc.internal.SerializingExecutor@19899a6a rejected from java.util.concurrent.ThreadPoolExecutor@1fba403c[Terminated, pool size = 0, ...] ``` 这个异常的意思是: > “我想把一个任务提交给线程池执行,但这个线程池已经 **终止了(Terminated)**,不能再接收任何新任务。” 📌 而触发这个提交动作的代码路径是: ```text yourCode.connGrpc() → stub.notify() → gRPC internal ClientCall.start() → SerializingExecutor.execute(task) → threadPoolExecutor.execute(task) ← 抛出 RejectedExecutionException ``` 所以: - `ThreadPoolExecutor.execute()` 是**最后一道关卡**; - 它只是“拒绝”任务,不是“导致”问题的原因; - 真正的问题是:**你在 channel 关闭后还试图发起 gRPC 调用**。 --- ## ✅ 二、你应该关注的是:“谁在什么时候提交了任务?” 虽然你没直接写线程池逻辑,但你可能做了这些事: | 你的行为 | 实际影响 | |--------|---------| | 创建了一个 gRPC Channel 并开始双向流通信 | gRPC 内部会使用线程池来处理网络事件 | | 调用了 `stub.notify(...)` | 触发 gRPC 在 `SerializingExecutor` 中提交任务 | | 某处调用了 `channel.shutdown()` 或 `executor.shutdown()` | 导致线程池进入 TERMINATED 状态 | | 之后又调用 `connGrpc()` | 尝试向已关闭的 executor 提交任务 → 失败 | 🎯 所以你要找的不是 `execute()` 方法本身,而是: > ❓“我的代码中有没有提前关闭了线程池或 channel?” > ❓“我是否在 channel 已关闭的情况下继续调用 `notify()`?” --- ## ✅ 三、实战排查:如何找到“真正的问题代码” ### 🔍 步骤 1:全局搜索 `shutdown` 和 `executor` 在你的项目中搜索以下关键词: ```bash grep -r "shutdown" . --include="*.java" grep -r "executor" . --include="*.java" ``` 重点关注: ```java channel.shutdown(); channel.shutdownNow(); executor.shutdown(); executor.shutdownNow(); ``` 比如你可能会发现: ```java @PreDestroy public void cleanup() { if (channel != null) { channel.shutdown(); // ✅ 正常关闭 } } ``` 但如果这个方法被提前调用了(比如 Spring Bean 销毁了),而你还想发消息 → 就会出错。 --- ### 🔍 步骤 2:检查是否有“重复创建 channel”但未清理旧资源 常见错误模式: ```java public void startGrpcChannel() { this.channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); connGrpc(); // 启动流 } // 某个定时任务每 5 秒调一次 startGrpcChannel() ``` ⚠️ 问题来了: - 第一次调用没问题; - 第二次调用时,老 `channel` 没有关闭; - 或者新 `channel` 没建好就立刻调 `connGrpc()`; - 此时内部使用的 `executor` 可能还没初始化好,或已被关闭; ✅ 正确做法: ```java public void startGrpcChannel() { // 先关闭旧 channel if (this.channel != null && !this.channel.isShutdown()) { this.channel.shutdown(); } this.channel = ManagedChannelBuilder.forAddress(host, port) .usePlaintext() .executor(grpcExecutor) // 使用自定义长生命周期线程池 .build(); connGrpc(); // 只有在 channel 建好后再调用 } ``` --- ### 🔍 步骤 3:确认你用的 `executor` 是否也被关闭了 如果你自己传了线程池给 gRPC: ```java private final ExecutorService grpcExecutor = Executors.newFixedThreadPool(2); this.channel = ManagedChannelBuilder.forAddress(host, port) .executor(grpcExecutor) .build(); ``` 那你必须确保: ✅ 这个 `grpcExecutor` 没有被提前关闭! 否则即使 `channel` 没 shutdown,executor 关了也会抛 `RejectedExecutionException`。 --- ## ✅ 四、解决方案:加防护 + 日志 + 重试机制 ### ✅ 改进后的 `connGrpc()` 示例 ```java public void connGrpc() { // 👇 第一步:防御性检查 if (channel == null) { log.warn("gRPC channel is null. Skipping notify."); return; } if (channel.isShutdown()) { log.warn("gRPC channel is SHUTDOWN. Cannot send request."); return; } if (channel.isTerminated()) { log.warn("gRPC channel is TERMINATED. Need to recreate."); startGrpcChannel(); // 重建 return; } try { // 动态创建 stub,避免复用旧 context GrpcServiceGrpc.GrpcServiceStub stub = GrpcServiceGrpc.newStub(channel); StreamObserver<NotifyResponse> responseObserver = new StreamObserver<>() { @Override public void onNext(NotifyResponse value) { /* 处理响应 */ } @Override public void onError(Throwable t) { log.error("gRPC stream error", t); retryGrpc(); // 出错后重连 } @Override public void onCompleted() { log.info("Stream completed."); retryGrpc(); } }; // 开始双向流 StreamObserver<NotifyRequest> requestObserver = stub.notify(responseObserver); // 发送初始请求或其他数据 requestObserver.onNext(NotifyRequest.getDefaultInstance()); } catch (RejectedExecutionException e) { log.error("Failed to execute gRPC call due to rejected execution", e); // 可尝试强制重建 forceRecreateChannel(); } catch (Exception e) { log.error("Unexpected error during gRPC connection", e); } } ``` --- ## ✅ 五、总结:面对 `ThreadPoolExecutor.execute()` 报错怎么办? | 问题 | 正确认知 | |------|----------| | 我要不要去看 `ThreadPoolExecutor.execute()` 的源码? | 可以看,但不要改,它是 JDK 标准实现 | | 我要不要调试这个方法? | 不需要单步进 JDK,只需知道它“拒绝任务”即可 | | 真正该查什么? | 查:**是谁在这个时候提交了任务?前提条件是否满足?** | | 如何预防? | 在调用 gRPC 前做 `channel.isShutdown()` 判断 | | 根本解决思路? | 确保:**只有在 channel 和 executor 都健康时才发起调用** --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值