<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[竹かさ雨]]></title><description><![CDATA[竹雨入盏，光阴回甘。]]></description><link>https://blog.star-dust.link</link><image><url>https://blog.star-dust.link/icon-light.svg</url><title>竹かさ雨</title><link>https://blog.star-dust.link</link></image><generator>Cyber (https://github.com/At87668/Cyber)</generator><lastBuildDate>Mon, 24 Aug 2026 00:02:45 GMT</lastBuildDate><atom:link href="https://blog.star-dust.link/feed" rel="self" type="application/rss+xml"/><pubDate>Mon, 24 Aug 2026 00:02:45 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[Java 反射的真实性能：Method.invoke 和 MethodHandle 到底差多少？]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/posts/note/java-reflection-performance-method-invoke-vs-methodhandle">https://blog.star-dust.link/posts/note/java-reflection-performance-method-invoke-vs-methodhandle</a></blockquote><div><p><em>作者声明：创作过程中使用了AI辅助</em></p><p>平时写业务或者造框架的时候，反射肯定少不了。依赖注入、ORM、序列化这些底层全靠它撑着。但大家一直都在吐槽“反射慢”，它到底能有多慢？有救吗？</p><p>最近专门花时间跑了个微基准测试，把老熟人 <code>Method.invoke</code> 和稍微新一点的 <code>MethodHandle</code> 拉出来溜了溜。为了摸清底细，我不仅测了“直接调”和“加缓存”的性能差距，还把反射里的各个前置操作（比如查字段、查方法、读写属性）全拆开测了一遍。</p><h2 id="">测试场景</h2><p>先简单交代下测试场景：
我搞了个很普通的 <code>Target</code> 类，里面有公共字段 <code>value</code>、私有字段 <code>hidden</code> 和一个普通的 <code>add</code> 方法。</p><p>测试参照组是我们平时手写的<strong>直接调用</strong>（比如直接 <code>target.add(a,b)</code> ）。
而反射组除了测最终的执行（Call/Read/Write），还把寻找元数据的过程（比如 <code>Class.forName</code>，或者 <code>getMethod</code>）也单独拿出来算了开销。</p><hr/><h2 id="">测试数据</h2><p>咱们直接上最原始的跑分数据。这里分为四种情况，大家可以重点对比“反射平均耗时”和“直接平均耗时”这两列。</p><h3 id="1-methodinvoke">1. Method.invoke（非缓存）</h3><p>这种情况就是平时最容易踩坑的写法，每次操作都重新去拿元数据。</p><table><thead><tr><th> <strong>测试项目</strong> </th><th> <strong>反射调用次数</strong> </th><th> <strong>反射平均耗时</strong> </th><th> <strong>反射总耗时</strong> </th><th> <strong>直接调用次数</strong> </th><th> <strong>直接平均耗时</strong> </th><th> <strong>直接总耗时</strong> </th></tr></thead><tbody><tr><td> Get Type </td><td> 100000 </td><td> 153ns </td><td> 15.36ms </td><td> 100000 </td><td> 3ns </td><td> 0.302ms </td></tr><tr><td> Get Field </td><td> 100000 </td><td> 31ns </td><td> 3.13ms </td><td> 100000 </td><td> 10ns </td><td> 1.02ms </td></tr><tr><td> Get Property </td><td> 100000 </td><td> 720ns </td><td> 72.06ms </td><td> 100000 </td><td> 8ns </td><td> 0.837ms </td></tr><tr><td> Get Method </td><td> 100000 </td><td> 36ns </td><td> 3.61ms </td><td> 100000 </td><td> 7ns </td><td> 0.724ms </td></tr><tr><td> Read Field </td><td> 100000 </td><td> 10ns </td><td> 1.04ms </td><td> 100000 </td><td> 1ns </td><td> 0.181ms </td></tr><tr><td> Read Property </td><td> 100000 </td><td> 644ns </td><td> 64.47ms </td><td> 100000 </td><td> 1ns </td><td> 0.187ms </td></tr><tr><td> Write Field </td><td> 100000 </td><td> 9ns </td><td> 0.986ms </td><td> 100000 </td><td> 1ns </td><td> 0.176ms </td></tr><tr><td> Write Property </td><td> 100000 </td><td> 697ns </td><td> 69.74ms </td><td> 100000 </td><td> 1ns </td><td> 0.183ms </td></tr><tr><td> Call Method </td><td> 100000 </td><td> 34ns </td><td> 3.42ms </td><td> 100000 </td><td> 2ns </td><td> 0.202ms </td></tr></tbody></table><h3 id="2-methodinvoke">2. Method.invoke（缓存）</h3><p>只要把 Method 或者 Field 提前提出来，你看看底下读写和调用的耗时降了多少。</p><table><thead><tr><th> <strong>测试项目</strong> </th><th> <strong>反射调用次数</strong> </th><th> <strong>反射平均耗时</strong> </th><th> <strong>反射总耗时</strong> </th><th> <strong>直接调用次数</strong> </th><th> <strong>直接平均耗时</strong> </th><th> <strong>直接总耗时</strong> </th></tr></thead><tbody><tr><td> Get Type </td><td> 100000 </td><td> 155ns </td><td> 15.55ms </td><td> 100000 </td><td> 1ns </td><td> 0.173ms </td></tr><tr><td> Get Field </td><td> 100000 </td><td> 17ns </td><td> 1.76ms </td><td> 100000 </td><td> 10ns </td><td> 1.07ms </td></tr><tr><td> Get Property </td><td> 100000 </td><td> 720ns </td><td> 72.03ms </td><td> 100000 </td><td> 13ns </td><td> 1.33ms </td></tr><tr><td> Get Method </td><td> 100000 </td><td> 19ns </td><td> 1.90ms </td><td> 100000 </td><td> 10ns </td><td> 1.06ms </td></tr><tr><td> Read Field </td><td> 10000000 </td><td> 4ns </td><td> 41.53ms </td><td> 10000000 </td><td> 0ns </td><td> 0.563ms </td></tr><tr><td> Read Property </td><td> 10000000 </td><td> 5ns </td><td> 59.47ms </td><td> 10000000 </td><td> 0ns </td><td> 0.565ms </td></tr><tr><td> Write Field </td><td> 10000000 </td><td> 4ns </td><td> 44.92ms </td><td> 10000000 </td><td> 0ns </td><td> 0.285ms </td></tr><tr><td> Write Property </td><td> 10000000 </td><td> 7ns </td><td> 72.51ms </td><td> 10000000 </td><td> 0ns </td><td> 0.285ms </td></tr><tr><td> Call Method </td><td> 10000000 </td><td> 11ns </td><td> 114.66ms </td><td> 10000000 </td><td> 0ns </td><td> 2.47ms </td></tr></tbody></table><h3 id="3-methodhandle">3. MethodHandle（非缓存）</h3><p>新宠 MethodHandle 登场。如果在循环里现抓现用，不仅不快，反而更惨。</p><table><thead><tr><th> <strong>测试项目</strong> </th><th> <strong>反射调用次数</strong> </th><th> <strong>反射平均耗时</strong> </th><th> <strong>反射总耗时</strong> </th><th> <strong>直接调用次数</strong> </th><th> <strong>直接平均耗时</strong> </th><th> <strong>直接总耗时</strong> </th></tr></thead><tbody><tr><td> Get Type </td><td> 100000 </td><td> 154ns </td><td> 15.48ms </td><td> 100000 </td><td> 1ns </td><td> 0.172ms </td></tr><tr><td> Get Field </td><td> 100000 </td><td> 14ns </td><td> 1.47ms </td><td> 100000 </td><td> 2ns </td><td> 0.262ms </td></tr><tr><td> Get Property </td><td> 100000 </td><td> 645ns </td><td> 64.60ms </td><td> 100000 </td><td> 2ns </td><td> 0.264ms </td></tr><tr><td> Get Method </td><td> 100000 </td><td> 20ns </td><td> 2.07ms </td><td> 100000 </td><td> 2ns </td><td> 0.264ms </td></tr><tr><td> Read Field </td><td> 100000 </td><td> 210ns </td><td> 21.02ms </td><td> 100000 </td><td> 0ns </td><td> 0.006ms </td></tr><tr><td> Read Property </td><td> 100000 </td><td> 951ns </td><td> 95.16ms </td><td> 100000 </td><td> 0ns </td><td> 0.007ms </td></tr><tr><td> Write Field </td><td> 100000 </td><td> 215ns </td><td> 21.56ms </td><td> 100000 </td><td> 0ns </td><td> 0.004ms </td></tr><tr><td> Write Property </td><td> 100000 </td><td> 985ns </td><td> 98.54ms </td><td> 100000 </td><td> 0ns </td><td> 0.004ms </td></tr><tr><td> Call Method </td><td> 100000 </td><td> 300ns </td><td> 30.03ms </td><td> 100000 </td><td> 0ns </td><td> 0.025ms </td></tr></tbody></table><h3 id="4-methodhandle">4. MethodHandle（缓存）</h3><p>提前准备好 Handle 后，它的威力才真正发挥出来。</p><table><thead><tr><th> <strong>测试项目</strong> </th><th> <strong>反射调用次数</strong> </th><th> <strong>反射平均耗时</strong> </th><th> <strong>反射总耗时</strong> </th><th> <strong>直接调用次数</strong> </th><th> <strong>直接平均耗时</strong> </th><th> <strong>直接总耗时</strong> </th></tr></thead><tbody><tr><td> Get Type </td><td> 100000 </td><td> 154ns </td><td> 15.46ms </td><td> 100000 </td><td> 1ns </td><td> 0.175ms </td></tr><tr><td> Get Field </td><td> 100000 </td><td> 10ns </td><td> 1.04ms </td><td> 100000 </td><td> 2ns </td><td> 0.258ms </td></tr><tr><td> Get Property </td><td> 100000 </td><td> 635ns </td><td> 63.58ms </td><td> 100000 </td><td> 2ns </td><td> 0.260ms </td></tr><tr><td> Get Method </td><td> 100000 </td><td> 17ns </td><td> 1.76ms </td><td> 100000 </td><td> 2ns </td><td> 0.262ms </td></tr><tr><td> Read Field </td><td> 10000000 </td><td> 3ns </td><td> 33.39ms </td><td> 10000000 </td><td> 0ns </td><td> 0.563ms </td></tr><tr><td> Read Property </td><td> 10000000 </td><td> 4ns </td><td> 41.80ms </td><td> 10000000 </td><td> 0ns </td><td> 0.566ms </td></tr><tr><td> Write Field </td><td> 10000000 </td><td> 3ns </td><td> 32.06ms </td><td> 10000000 </td><td> 0ns </td><td> 0.285ms </td></tr><tr><td> Write Property </td><td> 10000000 </td><td> 3ns </td><td> 35.32ms </td><td> 10000000 </td><td> 0ns </td><td> 0.286ms </td></tr><tr><td> Call Method </td><td> 10000000 </td><td> 4ns </td><td> 44.29ms </td><td> 10000000 </td><td> 0ns </td><td> 2.37ms </td></tr></tbody></table><hr/><h2 id="">个人体会</h2><p>看着上面的表格，不知道你有什么感觉？我个人的最大体会是下面这几点：</p><p><strong>1. 缓不缓存，那就是天与地的差别</strong>
你看数据，只要把 <code>Method</code> 或者 <code>MethodHandle</code> 提前缓存下来，真正调用时的耗时直接掉到了 <strong>3~11ns</strong>！虽然比直接调用（0~2ns）还是慢一丢丢，但这几纳秒的差距在绝大多数业务代码里根本感受不到，基本等同于热点代码的性能。
如果不缓存，每次都要走一遍安全检查和查表，开销直接放大十几甚至上百倍。</p><p><strong>2. MethodHandle 真的比 invoke 强吗？</strong></p><ul><li><strong>缓存情况下</strong>：两者基本五五开，MethodHandle 确实略占上风（3~4ns vs 4~11ns）。</li><li><strong>非缓存情况下</strong>：MethodHandle 反而吃瘪了。因为它在 <code>unreflect</code> 动态生成 LambdaForm 的时候，第一步的代价比老反射还要高。</li><li>顺带一提，我这里测试用的是带类型转换的 <code>mh.invoke()</code>，如果你的场景极其严苛，可以用 <code>invokeExact</code>，性能还能再榨出几滴来。</li></ul><p><strong>3. “内省”这玩意儿是真的重</strong>
仔细看表格里 <code>Get Property</code> 和 <code>Read/Write Property</code> 这几项，只要沾上 <code>PropertyDescriptor</code> 去走内省解析，反射耗时直接飙到 600~900ns。底层各种机制全套跑下来，开销极大，属于性能重灾区。</p><hr/><h2 id="">几条建议</h2><p>根据这些数据，以后咱们写反射代码或者封装小框架时，可以参考下面这几套连招：</p><ul><li><strong>能缓存就绝对不现查。</strong> 拿到 <code>Method</code> 或 <code>MethodHandle</code> 后，赶紧扔进 <code>ConcurrentHashMap</code> 或者提升为静态常量。这一步做好了，性能问题就解决 90% 了。</li><li><strong>避开 PropertyDescriptor 这种重武器。</strong> 如果是在高频调用的热路径里，别用内省去推断 getter/setter。老老实实拼接方法名，用 <code>getMethod</code> 查出来存好，会快得多。</li><li><strong>热点路径优先上 MethodHandle。</strong> 配合 <code>invokeExact</code> 加上提前预绑定（<code>bindTo</code> 或者 <code>asType</code>），能让你获得极其接近原生代码的性能体验。</li></ul><p><em>(最后叠个甲：测试是在 Windows  + JDK 17 下跑的，没上 JMH，<code>System.nanoTime</code> 可能会有抖动。所以数值大家看看就好，主要是感受一下各个操作相对的量级差距。)</em></p></div><p style="text-align:right"><a href="https://blog.star-dust.link/posts/note/java-reflection-performance-method-invoke-vs-methodhandle#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/posts/note/java-reflection-performance-method-invoke-vs-methodhandle</link><guid isPermaLink="true">https://blog.star-dust.link/posts/note/java-reflection-performance-method-invoke-vs-methodhandle</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Mon, 17 Aug 2026 15:09:45 GMT</pubDate></item><item><title><![CDATA[Forge反射模组开发：用Stub绕过@Mod注解导入（补充篇）]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/posts/note/forge-reflection-mod-dev-stub-bypass-mod-annotation-import">https://blog.star-dust.link/posts/note/forge-reflection-mod-dev-stub-bypass-mod-annotation-import</a></blockquote><div><p><em>作者声明：创作过程中使用了AI辅助</em></p><p><em>上篇：<a href="https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility">Fabric 模组开发：怎么用反射搞定多版本兼容？</a></em></p><p>上篇聊了反射框架，但有个前置问题没解决：<strong>既然整个反射体系的前提是“不能引入任何MC依赖”，Forge包又和Minecraft强绑定，那主类上的<code>@Mod</code>注解怎么过编译？</strong></p><p>答案就是<strong>Compile-Time Stub</strong>。</p><h2 id="stub">为什么必须用Stub？</h2><p>在纯反射框架下，由于Forge包与Minecraft强绑定的特性，我们不能引入真的Forge包。</p><p>但Forge模组入口点是由<code>@Mod</code>注解定义的，这时候就要用到Stub。</p><p>Stub的本质是：<strong>给编译器一个假契约，运行时由Loader提供真类</strong>。</p><h2 id="">关键要点</h2><pre class="language-java lang-java"><code class="language-java lang-java">@Retention(RetentionPolicy.RUNTIME) // 必须！否则Loader扫不到
@Target(ElementType.TYPE)
public @interface Mod {
    String value(); // 只保留Loader扫描必需的最小属性
}
</code></pre>
<p>三条铁律：</p><ol start="1"><li><strong>包名精确匹配</strong> <code>net.minecraftforge.fml.common.Mod</code></li><li><strong>绝不打包进JAR</strong> —— Gradle里 <code>jar { exclude &#x27;net/minecraftforge/**&#x27; }</code></li><li><strong>不写default值、不加多余属性</strong> —— 保持最小契约，防止误用</li></ol><h2 id="">一句话总结</h2><p>Stub不是“简化版Forge API”，而是<strong>编译期占位符</strong>。它让你在不触碰任何真实Forge类的情况下，依然能写出合法的<code>@Mod</code>标注类。配合上篇的反射体系，才算真正闭环。</p></div><p style="text-align:right"><a href="https://blog.star-dust.link/posts/note/forge-reflection-mod-dev-stub-bypass-mod-annotation-import#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/posts/note/forge-reflection-mod-dev-stub-bypass-mod-annotation-import</link><guid isPermaLink="true">https://blog.star-dust.link/posts/note/forge-reflection-mod-dev-stub-bypass-mod-annotation-import</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Sun, 09 Aug 2026 16:35:56 GMT</pubDate></item><item><title><![CDATA[OpenCode Go使用感受]]></title><description><![CDATA[<link rel="preload" as="image" href="https://blogx.star-dust.link/api/v3/objects/image/lb6h6vn0xmzazo81s1.png"/><link rel="preload" as="image" href="https://blogx.star-dust.link/api/v3/objects/image/cm4fchwoxs3brqd7ig.png"/><link rel="preload" as="image" href="https://blogx.star-dust.link/api/v3/objects/image/i641a5hmxvcvnzdf32.png"/><link rel="preload" as="image" href="https://blogx.star-dust.link/api/v3/objects/image/fb56bumv1mblpylecd.png"/><div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/notes/3">https://blog.star-dust.link/notes/3</a></blockquote><div><p>最近看到OpenCode $5首月,  $10续费的 Go 订阅, 包含较多常用模型,</p><p>看了下，比我直接调API便宜多了，试了试。</p><p>目前感受一般，便宜是真便宜，
但是热门高级模型总是500/503错误，不太稳定</p><pre class=""><code class="">OpenCode Go API request failed (HTTP 500) for qwen3.8-max: {&quot;type&quot;:&quot;Router.Unavailable&quot;,&quot;modelID&quot;:&quot;qwen3.8-max&quot;}
</code></pre>
<p>而且高级模型是残血版，只有260K上下文窗口，基本不够用。</p><p><del>总体还不错，毕竟是人家贴钱给你便宜用，轻度开发或者玩玩可以，重度使用还得模型官方Coding Plan。</del></p><p>高级模型额度计算有倍乘，我5小时内只用了$4不到API就停了，后台显示额度达到100%。</p><p>用量计算公式应该是<code>(月用量上限/模型月用量上限)×模型价格</code></p><p><img src="https://blogx.star-dust.link/api/v3/objects/image/lb6h6vn0xmzazo81s1.png" alt="Snipaste_2026-08-06_18-43-54.png"/></p><p><img src="https://blogx.star-dust.link/api/v3/objects/image/cm4fchwoxs3brqd7ig.png" alt="Snipaste_2026-08-06_18-43-46.png"/></p><p>用OpenRouter试了一下，余额消耗和OpenCode使用日志差不多，回复质量也相似，确实没掺水:</p><p><img src="https://blogx.star-dust.link/api/v3/objects/image/i641a5hmxvcvnzdf32.png" alt="Snipaste_2026-08-06_19-05-15.png"/></p><p><img src="https://blogx.star-dust.link/api/v3/objects/image/fb56bumv1mblpylecd.png" alt="Snipaste_2026-08-06_18-52-25.png"/></p><p>||||另外吐槽一下OpenRouter: <em><del>总是时不时切换模型提供商导致吃不到缓存多扣钱</del></em></p></div><p style="text-align:right"><a href="https://blog.star-dust.link/notes/3#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/notes/3</link><guid isPermaLink="true">https://blog.star-dust.link/notes/3</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Tue, 04 Aug 2026 09:09:14 GMT</pubDate></item><item><title><![CDATA[如何使用邪修的方式制作Minecraft模组]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/posts/note/create-minecraft-mods-dark-cultivation">https://blog.star-dust.link/posts/note/create-minecraft-mods-dark-cultivation</a></blockquote><div><p><em>作者声明：文章创作过程中使用了AI辅助</em></p><p>本文的正经风格版本：<a href="https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility">https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility</a></p><details><summary>观前必读</summary><blockquote><p>⚠️ 重要声明：阅读本文前请仔细阅读以下条款</p></blockquote><blockquote>
</blockquote><blockquote>
<h3 id="">一、虚构性质与文学修辞声明</h3></blockquote><blockquote>
</blockquote><blockquote>
<ol start="1"><li>纯属比喻：本文所述&quot;邪修&quot;技术纯属编程技巧的文学化表达，文中所有比喻（包括但不限于&quot;修仙&quot;、&quot;炼丹&quot;、&quot;走火入魔&quot;、&quot;渡劫&quot;等）均为修辞手法，请勿当真尝试在现实中修炼。</li></ol></blockquote><blockquote>
<ol start="2"><li>非官方指导：本文内容不代表Mojang Studios、Microsoft或任何模组加载器开发团队的官方立场、建议或认可。所有观点仅代表作者个人经验总结。</li></ol></blockquote><blockquote>
<ol start="3"><li>时效性限制：技术日新月异，本文所述方法可能在发布后短时间内失效。请以实际测试为准，不要盲目照搬代码。</li></ol></blockquote><blockquote>
</blockquote><blockquote>
<h3 id="">二、技术风险警示</h3></blockquote><blockquote>
</blockquote><blockquote>
<ol start="1"><li>稳定性风险：反射兼容技术本质上是在与Minecraft的内部实现细节博弈。Mojang随时可能重构代码、混淆类名、移除API，导致你的模组在下一次更新中原地爆炸。使用此技术意味着你自愿承担维护地狱的难度。</li></ol></blockquote><blockquote>
</blockquote><blockquote>
<ol start="2"><li>性能代价：虽然文中提到缓存优化，但反射调用依然比直接调用慢。在对性能敏感的代码路径（如每tick执行的方法）中使用反射，可能导致服务器TPS下降、客户端卡顿。</li></ol></blockquote><blockquote>
</blockquote><blockquote>
<ol start="3"><li>兼容性陷阱：</li></ol></blockquote><blockquote>
</blockquote><blockquote>
<ul><li>不同加载器的内部实现差异巨大</li></ul></blockquote><blockquote>
<ul><li>不同映射表的类名和方法签名完全不同</li></ul></blockquote><blockquote>
<ul><li>某些服务端核心可能修改了原版行为</li></ul></blockquote><blockquote>
<ul><li>不要指望一套代码真正通吃所有环境，本文仅提供思路，实际落地需要大量测试</li></ul></blockquote><blockquote>
</blockquote><blockquote>
<ol start="4"><li>调试困难：反射导致的错误通常表现为：</li></ol></blockquote><blockquote>
</blockquote><blockquote>
<ul><li>NoSuchMethodException / ClassNotFoundException（还算好的）</li></ul></blockquote><blockquote>
<ul><li>ClassCastException（类型猜错了）</li></ul></blockquote><blockquote>
<ul><li>IllegalAccessException（访问权限被拦）</li></ul></blockquote><blockquote>
<ul><li>静默失败（最可怕，方法根本没被调用但你不知道）</li></ul></blockquote><blockquote>
</blockquote><blockquote>
<p>   堆栈信息往往指向反射框架内部，而非你的业务代码，排查难度指数级上升。</p></blockquote><blockquote>
</blockquote><blockquote>
<h3 id="">三、法律与合规风险</h3></blockquote><blockquote>
</blockquote><blockquote>
<ul><li>反射访问Minecraft私有API可能触及EULA灰色地带</li></ul></blockquote><blockquote>
<ul><li>虽然技术上可行，但不代表法律上完全安全</li></ul></blockquote><blockquote>
<ul><li>商业项目请谨慎评估风险，建议咨询法律顾问</li></ul></blockquote><blockquote>
</blockquote><blockquote>
<h3 id="">四、适用场景建议</h3></blockquote><blockquote>
</blockquote><blockquote>
<p>✅ 推荐使用：</p></blockquote><blockquote>
</blockquote><blockquote>
<ul><li>小型工具类模组，功能简单且稳定</li></ul></blockquote><blockquote>
<ul><li>跨加载器兼容层</li></ul></blockquote><blockquote>
<ul><li>临时应急方案</li></ul></blockquote><blockquote>
<ul><li>学习研究目的，深入理解JVM和反射机制</li></ul></blockquote><blockquote>
</blockquote><blockquote>
<p>❌ 不推荐使用：</p></blockquote><blockquote>
</blockquote><blockquote>
<ul><li>大型复杂模组，涉及大量MC内部交互</li></ul></blockquote><blockquote>
<ul><li>对性能要求极高的内容（如大规模实体处理、世界生成）</li></ul></blockquote><blockquote>
<ul><li>商业项目或长期维护的企业级应用</li></ul></blockquote><blockquote>
<ul><li>新手入门学习（请先掌握Mixin和正规API）</li></ul></blockquote><blockquote>
</blockquote><blockquote>
<h3 id="">五、最终警告</h3></blockquote><blockquote>
</blockquote><blockquote>
<p>阅读并实践本文内容即表示你已理解：</p></blockquote><blockquote>
</blockquote><blockquote>
<ul><li>这是一条充满荆棘的道路，需要极强的自学能力和问题解决能力</li></ul></blockquote><blockquote>
<ul><li>你可能需要花费数倍于正统开发的时间来调试和维护</li></ul></blockquote><blockquote>
<ul><li>社区可能不会同情你的遭遇，因为这是你自己选择的路</li></ul></blockquote><blockquote>
</blockquote><blockquote>
<h3 id="">六、免责声明</h3></blockquote><blockquote>
</blockquote><blockquote>
<p>作者郑重声明：</p></blockquote><blockquote>
</blockquote><blockquote>
<ol start="1"><li>本文内容仅供学习和参考，不构成任何形式的建议、保证或承诺</li></ol></blockquote><blockquote>
<ol start="2"><li>作者不对因使用本文所述技术导致的任何直接或间接损失承担责任，包括但不限于：</li></ol></blockquote><blockquote>
<ul><li>数据丢失或损坏</li></ul></blockquote><blockquote>
<ul><li>服务器崩溃或服务中断</li></ul></blockquote><blockquote>
<ul><li>性能下降或用户体验恶化</li></ul></blockquote><blockquote>
<ul><li>法律纠纷或合规问题</li></ul></blockquote><blockquote>
<ul><li>时间浪费和精神损失</li></ul></blockquote><blockquote>
<ul><li>任何其他形式的损害</li></ul></blockquote><blockquote>
<ol start="3"><li>用户使用本文所述技术的风险完全由用户自行承担</li></ol></blockquote><blockquote>
<ol start="4"><li>作者保留随时修改、更新或删除本文内容的权利，恕不另行通知</li></ol></blockquote><blockquote>
<ol start="5"><li>本文可能包含错误、遗漏或不准确的信息，请以实际测试为准</li></ol></blockquote><blockquote>
<ol start="6"><li>本文提及的所有商标、产品名称均属于其各自所有者</li></ol></blockquote><blockquote>
</blockquote><blockquote>
<p>---</p></blockquote><blockquote>
</blockquote><blockquote>
<p>正统开发者提示：如果你已经关闭页面，恭喜你做出了明智的选择。Mixins虽然繁琐，但至少有人维护、有文档、有社区支持。反射是最后的 resort，不是首选方案。</p></blockquote><blockquote>
</blockquote><blockquote>
<p>邪修同道中人：既然你留下来了，那就祝你好运。记住，活着就行，管他姿势——但最好还是写点单元测试。🧪</p></blockquote><blockquote>
</blockquote><blockquote>
<p>本文最后更新于2026年8月，适用于Minecraft Java Edition 1.18-26.2。技术日新月异，请以实际情况为准。</p></blockquote></details><hr/><h2 id="">第一章：为什么要当邪修？</h2><p>  因为正道太累了啊！</p><p>  你看那些名门正派的Fabric弟子，每天对着Mixin注解磕头拜佛，AccessWidener写得比情书还长，Gradle配置调得比炼丹炉还复杂。结果呢？MC一更新，全部白给！1.21写的模组到26.1直接原地飞升，连个全尸都不留。</p><p>  而我们邪修不一样。我们没有信仰，没有底线，没有import net.minecraft.*。我们只有一样东西——</p><p>  反射。</p><p>  这玩意儿就像修仙界的&quot;万能钥匙&quot;，虽然脏、虽然慢、虽然随时可能炸膛，但它能用。编译期看不到MC的类？没关系，运行时硬抢！方法名改了？没关系，挨个试！参数类型不对？没关系，动态代理糊脸！</p><p>  邪修的核心理念就八个字：活着就行，管他姿势。</p><hr/><h2 id="">第二章：邪修三件套</h2><h3 id="21-">2.1 常量层：你的&quot;通缉令&quot;</h3><p>  把所有版本里变来变去的类名、方法名、字段名，全部写进一个巨大的常量表里。这张表就是你的&quot;仇人名单&quot;，上面记满了MC每次更新欠下的债。</p><pre class="language-java lang-java"><code class="language-java lang-java">// 邪修の仇人录
static final String C_LEVEL       = &quot;net.minecraft.world.level.Level&quot;;
static final String C_COMPONENT   = &quot;net.minecraft.network.chat.Component&quot;;
static final String M_GET_UUID    = im(&quot;Entity&quot;, &quot;getUUID&quot;, &quot;()Ljava/util/UUID;&quot;, &quot;method_5667&quot;);
static final String M_LITERAL     = im(&quot;Component&quot;, &quot;literal&quot;, &quot;(Ljava/lang/String;)LComponent;&quot;, &quot;method_43470&quot;);
</code></pre>
<p>  记住：业务代码里绝对不能出现任何硬编码的MC类名。一旦出现，你就破了戒，离走火入魔不远了。</p><h3 id="22-">2.2 反射层：你的&quot;作案工具&quot;</h3><p>  这一层封装所有脏活累活。Class.forName、Method.invoke、Field.get……这些见不得光的操作全部藏在这里，对外只提供人话接口。</p><p>  关键技巧：</p><p>缓存一切：反射调用一次就够了，第二次还用就是浪费生命。用ConcurrentHashMap存起来，找不到的也要存个NOT_FOUND哨兵，不然每次都重新扫描，JVM会骂你。</p><p>描述符必须带：找方法不带描述符等于裸奔。getUUID()混淆后可能是method<em>5667也可能是method</em>5845，一个返回UUID一个返回String，猜错了当场社死。</p><p>重载方法要遍历：别傻乎乎只缓存第一个匹配到的方法。把所有同名方法都捞出来，调用时用isInstance逐个验身，谁对得上就用谁。</p><h3 id="23-">2.3 业务层：你的&quot;伪装身份&quot;</h3><p>  这一层看起来和正常模组一模一样。getBlockState()、sendMessage()、checkPermission()……全是人话，干干净净。</p><p>  但只有你自己知道，这些人话底下藏着多少反射。</p><hr/><h2 id="">第三章：邪修实战骚操作</h2><h3 id="31-">3.1 环境探测：先搞清楚自己在哪</h3><p>Fabric生产环境用intermediary名，MC 26+又改回named名。你得先知道自己身处哪个平行宇宙：</p><pre class="language-java lang-java"><code class="language-java lang-java">// 使用神识探查一下当前是不是 named 运行时
static final boolean IS_NAMED = tryLoad(&quot;net.minecraft.world.level.Level&quot;) != null;
</code></pre>
<p>然后查表回退：先试named名，不行就翻&quot;仇人录&quot;找intermediary名。intermediary名跨版本稳定，是你最后的保命符：</p><pre class="language-java lang-java"><code class="language-java lang-java">static Class&lt;?&gt; forName(String name) {
    // 1. 缓存命中直接返回（含 NOT_FOUND 哨兵）
    Object cached = CACHE.get(name);
    if (cached != null) return unwrap(cached);

    // 2. 先试 named 名，再试 intermediary 兜底
    Class&lt;?&gt; cls = tryLoad(name);
    if (cls == null) cls = tryLoad(Constants.toInter(name));

    // 3. 存结果，找不到也要存哨兵防无限重试
    CACHE.put(name, cls != null ? cls : NOT_FOUND);
    return cls;
}
</code></pre>
<h3 id="32-">3.2 事件监听：动态代理捏脸术</h3><p>Fabric的事件回调接口里全是MC原生类，编译期根本import不了。怎么办？不implements了，直接捏！</p><p>用JDK动态代理在运行时现场造一个假对象塞进去。代理对象收到回调后，把参数打包成Object[]扔给业务层慢慢拆：</p><pre class="language-java lang-java"><code class="language-java lang-java">// 运行时捏个假监听器，彻底斩断编译期类型依赖
Object proxy = Proxy.newProxyInstance(loader, new Class[]{listenerIfc}, (p, m, args) -&gt;
    m.equals(targetMethod) ? handler.handle(args) : handleObjectMethods(p, m, args)
);
registerMethod.invoke(eventObj, proxy);
</code></pre>
<p>这招的精髓在于：只要我不承认依赖存在，依赖就不存在。我们把它叫做薛定谔的类型安全。</p><h3 id="33-">3.3 多策略兜底：一条路走到黑不如多条路走到亮</h3><p>获取方块ID为例：</p><pre class="language-java lang-java"><code class="language-java lang-java">static String getBlockId(Object state) {
    Object block = call(state, &quot;getBlock&quot;);
    if (block == null) return &quot;minecraft:air&quot;;

    // 策略1：toString 硬抠（全版本通用保底）
    String s = block.toString(); // Block{minecraft:diorite}
    int b = s.indexOf(&#x27;{&#x27;), e = s.indexOf(&#x27;}&#x27;);
    if (b &gt;= 0 &amp;&amp; e &gt; b) return s.substring(b + 1, e);

    // 策略2：反射查注册表（新旧API自动兼容）
    String id = getFromRegistry(block);
    return id != null ? id : &quot;minecraft:air&quot;;
}
</code></pre>
<p>先试新API BuiltInRegistries，抛异常了？换老API Registries，又抛了？用toString()硬抠字符串（Block{minecraft:diorite} → minecraft:diorite），还不行？返回minecraft:air，假装什么都没发生。</p><p>邪修的信条：优雅地失败 &gt; 粗暴地崩溃。</p><h3 id="34-">3.4 文本组件兼容：新旧两开花</h3><p>老版本new TextComponent(&quot;hello&quot;)，新版本Component.literal(&quot;hello&quot;)。写个兼容方法：</p><pre class="language-java lang-java"><code class="language-java lang-java">// 新版 Component.literal() 失败就降级旧版 new TextComponent()
static Object text(String s) {
    try { return invokeStatic(&quot;Component&quot;, &quot;literal&quot;, s); }
    catch (Exception e) { return newInstance(&quot;TextComponent&quot;, s); }
}
</code></pre>
<p>注意：这里Component<em>literal和new</em>TextComponent都是反射拿到的，编译期依然干干净净。邪修的代码，永远不能被编译器抓到把柄。</p><hr/><h2 id="">第四章：邪修禁忌</h2><p>忌到处散落反射调用：不封装的反射就像没穿衣服上街，迟早出事。</p><p>忌信任单一API：MC的承诺比渣男的誓言还不靠谱，永远准备Plan BCD。</p><p>忌忽略性能：不做缓存的反射修士，活不过筑基期。</p><p>忌忘记哨兵：ConcurrentHashMap不能存null，不设NOT_FOUND占位符，你会陷入无限重试的地狱轮回。</p><p>忌跟正道修士争论：他们不懂你的苦，你也不必解释。默默写完兼容代码，看他们的模组在下个版本集体暴毙，然后微微一笑。</p><hr/><h2 id="">终章：邪修的归宿</h2><p>  这套体系搭完，你就能实现：</p><p>✅ 编译期零MC依赖</p><p>✅ 单JAR通吃1.18到26+</p><p>✅ 不用Mixin、不用AW、不看官方脸色</p><p>✅ 在别人模组集体阵亡时独自存活</p><p>  代价是什么？</p><p>❌ 代码可读性约等于天书</p><p>❌ 调试时堆栈信息像乱码</p><p>❌ 新人接手时会诅咒你十八代祖宗</p><p>❌ 你永远无法理直气壮地说&quot;我写的是正经模组&quot;</p><p>  但没关系。</p><p>  邪修不需要认可，只需要运行。</p><p>  当你看到自己的模组在数个大版本更新后依然顽强地加载成功，而隔壁正道修士还在Discord里哭诉Mixin冲突时——</p><p>  那一刻，你就知道，这条路，走对了。</p></div><p style="text-align:right"><a href="https://blog.star-dust.link/posts/note/create-minecraft-mods-dark-cultivation#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/posts/note/create-minecraft-mods-dark-cultivation</link><guid isPermaLink="true">https://blog.star-dust.link/posts/note/create-minecraft-mods-dark-cultivation</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Sat, 01 Aug 2026 16:07:05 GMT</pubDate></item><item><title><![CDATA[Fabric 模组开发：怎么用反射搞定多版本兼容？]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility">https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility</a></blockquote><div><p><em>作者声明：创作过程中使用了AI辅助</em></p><p>搞 Fabric 开发的小伙伴可能都遇到过一个头疼的问题：Minecraft 每个大版本更新，底层代码就像重写了一样。如果你想弄一个一套代码兼容 1.18 到 26.1+ 的模组，还不能用 Mixin，甚至编译的时候连 Minecraft 的 jar 包都引用不了，还怎么玩？</p><p>今天咱就来聊聊，在这种极端约束下，怎么纯靠反射搭起一套稳健的多版本兼容体系。</p><hr/><h2 id="">一、为什么非得用反射？</h2><p>MC 版本更新对底层类的重构可以说是毫不留情。举个例子，跨几个版本你会发现类名、包名全变了：</p><table><thead><tr><th> 逻辑含义 </th><th> 1.18–1.21 (Yarn 映射) </th><th> 26.1+ (Mojang 映射) </th></tr></thead><tbody><tr><td> 文本组件 </td><td> <code>net.minecraft.text.Text</code> </td><td> <code>net.minecraft.network.chat.Component</code> </td></tr><tr><td> 方块坐标 </td><td> <code>net.minecraft.util.math.BlockPos</code> </td><td> <code>net.minecraft.core.BlockPos</code> </td></tr><tr><td> 服务端世界 </td><td> <code>net.minecraft.server.world.ServerWorld</code> </td><td> <code>net.minecraft.server.level.ServerLevel</code> </td></tr><tr><td> 服务端玩家 </td><td> <code>net.minecraft.server.network.ServerPlayerEntity</code> </td><td> <code>net.minecraft.server.level.ServerPlayer</code> </td></tr><tr><td> 命令源 </td><td> <code>net.minecraft.server.command.ServerCommandSource</code> </td><td> <code>net.minecraft.commands.CommandSourceStack</code> </td></tr></tbody></table><p>有时候我们的开发环境非常受限，比如：</p><ol start="1"><li><strong>编译期压根没有 MC 的环境</strong>：你可能在写一个 Bukkit/Fabric 双端通用的插件，主项目是基于 Bukkit API 编译的，没法直接 <code>import net.minecraft.*</code>。</li><li><strong>没法用 Mixin</strong>：只能走 Fabric 暴露出来的公开 API 表面（比如事件、注册表、命令）。</li><li><strong>单 JAR 走天下</strong>：打出来的一个包，得在各种版本的服务器上都能跑。</li></ol><p>遇到这种“戴着镣铐跳舞”的情况，<strong>反射</strong>就成了咱们唯一的救命稻草。</p><hr/><h2 id="">二、别乱写反射，分层是关键</h2><p>几万行代码如果到处塞满 <code>Class.forName</code> 和 <code>Method.invoke</code> 绝对会让人抓狂。比较靠谱的做法是把代码拆成三层：</p><pre class="language-text lang-text"><code class="language-text lang-text">┌─────────────────────────────────────────────────────────┐
│  业务层  Platform / Listener / ...                       │
│         只管叫“人话”（如 getBlockState），不关心底层名字    │
├─────────────────────────────────────────────────────────┤
│  反射层  ReflectionUtils                                 │
│         提供 forName / call / findMethod / ...          │
│         内置缓存、父类回退、参数扫描                      │
├─────────────────────────────────────────────────────────┤
│  常量层  ReflectionConstants                             │
│         集中管理所有类名/方法名/字段名，负责不同映射间的转换  │
└─────────────────────────────────────────────────────────┘
</code></pre>
<p>这样分层最大的好处是：<strong>脏活累活全被常量层扛了</strong>。多版本的差异被死死锁在底层，业务代码看着依旧清爽。</p><hr/><h2 id="named--intermediary">三、找类：named 怎么转 intermediary？</h2><p>Fabric 生产环境把类名改成了 intermediary，但到了 MC 26+，官方又给改回了 named/mojang 名。所以第一步，你得知道现在的运行环境到底用哪套名字。</p><p>我们去找一个<strong>只在 named 运行时才存在</strong>的类：</p><pre class="language-java lang-java"><code class="language-java lang-java">private static final boolean IS_NAMED_RUNTIME;
static {
    FabricLoader fl = FabricLoader.getInstance();
    boolean namedRt = false;
    try {
        // MC 26+ 上 named 类是运行时类；1.18–1.21 只有 intermediary 的 class_NNNN
        Class.forName(&quot;net.minecraft.world.level.Level&quot;);
        namedRt = true;
    } catch (ClassNotFoundException ignored) {}
    IS_NAMED_RUNTIME = namedRt;
}
</code></pre>
<p>拿到环境状态后，咱就可以弄个回退机制了。先试 named 名，不行就查表回退到 intermediary 名：</p><pre class="language-java lang-java"><code class="language-java lang-java">static Class&lt;?&gt; forName(String namedClassName) {
    Object cached = classCache.get(namedClassName);
    if (cached != null) return unwrap(cached);

    Class&lt;?&gt; cls = tryLoad(namedClassName);          // 1) 先试 named 名
    if (cls != null) { classCache.put(namedClassName, cls); return cls; }

    // NAMED_TO_INTER 是一张硬编码的表，记录了常用类的 intermediary 映射
    String inter = Constants.toIntermediaryClass(namedClassName);
    if (inter != null &amp;&amp; !inter.equals(namedClassName)) {
        cls = tryLoad(inter);                        // 2) 不行就试 intermediary 名
        if (cls != null) { classCache.put(namedClassName, cls); return cls; }
    }
    classCache.put(namedClassName, NOT_FOUND);
    return null;
}
</code></pre>
<p>为啥要查表？因为 intermediary 名（<code>class_NNNN</code>）在所有大版本更新时是<strong>跨版本稳定</strong>的，用它做兜底最合适不过。</p><hr/><h2 id="">四、找方法：参数扫描与别名表</h2><p>类名搞定了，方法和字段更恶心。同一个方法，在这叫 <code>method_123</code>，到另一个版本可能就换地方了，比如<code>disconnect</code>。解决这个得靠两步走战略。</p><p><strong>1. 必须带描述符（descriptor）</strong>
方法名解析如果不带描述符，很容易翻车。比如找个 <code>getUUID()</code>，混淆后谁知道返回的是 UUID 还是 String？必须精确匹配：</p><pre class="language-java lang-java"><code class="language-java lang-java">static final String M_GET_UUID = im(&quot;net.minecraft.world.entity.Entity&quot;, &quot;getUUID&quot;,
    &quot;()Ljava/util/UUID;&quot;, &quot;method_5667&quot;); // 5667 -&gt; UUID, 5845 -&gt; String
</code></pre>
<p><strong>2. 搞个方法名重定向表</strong>
很多逻辑在不同版本不仅混淆名变了，连语义名都变了。我们可以在表里把它们都指到一个标准名上，这样业务层就不用操心了：</p><pre class="language-java lang-java"><code class="language-java lang-java">putM(&quot;getPlayerList&quot;,       M_GET_PLAYER_LIST);
putM(&quot;getPlayerManager&quot;,    M_GET_PLAYER_LIST);    // 1.18 的叫法
putM(&quot;getWorld&quot;,            M_GET_LEVEL);          // 1.18 的别名
putM(&quot;refreshPositionAfterTeleport&quot;, M_SET_POS);   // MC 26 的别名
</code></pre>
<hr/><h2 id="">五、事件监听怎么破？用动态代理</h2><p>碰到 Fabric 的事件回调接口（比如 <code>PlayerBlockBreakEvents$After</code>），里面的方法参数全是 MC 原生类，编译期根本没法 import。怎么办？这里有个黑科技：<strong>JDK 动态代理</strong>。</p><p>咱们不直接 <code>implements</code> 这个接口，而是在运行时动态捏一个代理对象传进去：</p><pre class="language-java lang-java"><code class="language-java lang-java">private static void registerEvent(String eventClassName, String fieldName,
                                  String listenerInterfaceName, CallbackAdapter handler) {
    Class&lt;?&gt; eventCls = forName(eventClassName);
    Object eventObj = eventCls.getField(fieldName).get(null);

    Class&lt;?&gt; listenerInterface = forName(listenerInterfaceName);
    final Method abstractMethod = findAbstractMethod(listenerInterface); // 找到那个唯一的事件回调方法

    Object proxy = Proxy.newProxyInstance(
        eventCls.getClassLoader(),
        new Class&lt;?&gt;[]{listenerInterface},
        (proxyObj, method, args) -&gt; {
            // equals, hashCode 那些不管，只管事件方法
            if (!method.equals(abstractMethod)) return handleObjectMethods(proxyObj, method, args);
            // 把原始参数 args 全打成 Object[] 丢给业务层自己用反射慢慢拆
            return handler.handle(args);   
        });

    registerMethod.invoke(eventObj, proxy);
}
</code></pre>
<p>这招直接斩断了编译期的类型依赖，完美绕过限制！当然，不同版本的参数个数可能不一样，业务层拆包的时候记得判断一下 <code>args.length</code>。</p><hr/><h2 id="">六、坑点：重载方法的匹配陷阱</h2><p>很多插件或模组的 API 都有重载方法，比如权限检查：一个接 <code>CommandSourceStack</code>，一个接 <code>Entity</code>。</p><p>如果你傻乎乎地只缓存第一个拿到的 <code>Method</code>，等运行的时候，传了个 <code>Entity</code> 进去，人家实际上要的是 <code>CommandSourceStack</code> 的那个方法，当场就会给你糊一脸 <code>IllegalArgumentException</code>。</p><p>正确的玩法是：<strong>把所有重载全找出来缓存好，每次调用前，拿 <code>param[0].isInstance(source)</code> 挨个验一下身</strong>：</p><pre class="language-java lang-java"><code class="language-java lang-java">static boolean checkPermission(Object source, String node) {
    for (Method m : findAllCheckMethods()) {          // 取出所有 check() 的重载
        Class&lt;?&gt; sourceType = m.getParameterTypes()[0];
        if (!sourceType.isInstance(source)) continue; // 匹配当前 source 的真实类型
        try {
            return (Boolean) m.invoke(null, source, node);
        } catch (Throwable t) { ... }
    }
    return false;
}
</code></pre>
<p>谁匹配得上就用谁，这样就稳如老狗了。</p><hr/><h2 id="-id-">七、多策略兜底：拿方块 ID 的玄学</h2><p>在跨版本环境中做操作，最稳妥的方式永远是“多策略兜底”。以获取方块 ID 为例：</p><pre class="language-java lang-java"><code class="language-java lang-java">private static String getBlockId(Object blockState) {
    Object block = call(blockState, &quot;getBlock&quot;);
    if (block == null) return &quot;minecraft:air&quot;;
    
    // 策略 1：老老实实用 toString() 抠字符串。
    // Block.toString() 往往会吐出 &quot;Block{minecraft:diorite}&quot;，所有版本都通用且可靠！
    String s = block.toString();
    int brace = s.indexOf(&#x27;{&#x27;);
    int close = s.indexOf(&#x27;}&#x27;);
    if (brace &gt;= 0 &amp;&amp; close &gt; brace) return s.substring(brace + 1, close);
    
    // 策略 2：反射注册表（这里还得兼容 BuiltInRegistries 和老版的 Registries）
    String id = getFromRegistry(block);
    if (id != null) return id;
    
    return &quot;minecraft:air&quot;;
}
</code></pre>
<p>创建文本组件也是同理：老版本发消息用 <code>new TextComponent()</code>，新版本这玩意直接没了，变成了 <code>Component.literal()</code>。写兼容方法的时候，<strong>先试新 API，抛异常了再试旧 API</strong>，确保能在不同版本平滑过渡。</p><hr/><h2 id="">八、榨干性能：缓存与哨兵</h2><p>都说反射慢，那是没做缓存。把找出来的 Class、Method、Field 全塞进 <code>ConcurrentHashMap</code> 里就行了。</p><p>不过要注意，<code>ConcurrentHashMap</code> 里不能存 <code>null</code>。如果某个版本确实没这个方法，每次查找都会失败，每次都会重新走一遍全量扫描。所以咱们得整个 <code>NOT_FOUND</code> 的哨兵对象当占位符：</p><pre class="language-java lang-java"><code class="language-java lang-java">private static final Object NOT_FOUND = new Object();
// 找不着就存它，下次看见它直接绕道走
</code></pre>
<hr/><h2 id="">总结</h2><p>盘点一下，这套纯反射的兼容玩法其实就几个核心套路：</p><ol start="1"><li><strong>把恶心的版本差异全关进常量层</strong>，业务代码该怎么写怎么写。</li><li><strong>多策略兜底</strong>，新 API 走不通走旧 API，旧 API 走不通走字符串硬抠。</li><li><strong>动态代理</strong>大法好，绕开编译期依赖全靠它。</li><li>遇到<strong>重载方法</strong>，用 <code>isInstance</code> 动态匹配，别死磕一个。</li><li><strong>缓存 + 占位符</strong>，把反射的性能开销降到最低。</li></ol><p>这套体系搭下来，哪怕编译期连一个 MC 的原生类都见不到，一样能把 Fabric 的多版本兼容拿捏得死死的。虽然前期写起来比直接用 Mixin 折腾点，但在特定场景下（比如单 JAR 双端通吃），这确实是最灵活的方案。</p><p>希望能帮大家少掉几根头发！</p><p><em>补充篇：<a href="https://blog.star-dust.link/posts/note/forge-reflection-mod-dev-stub-bypass-mod-annotation-import">Forge反射模组开发：用Stub绕过@Mod注解导入（补充篇）</a></em></p></div><p style="text-align:right"><a href="https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility</link><guid isPermaLink="true">https://blog.star-dust.link/posts/note/fabric-mod-reflection-multi-version-compatibility</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Sat, 01 Aug 2026 08:35:05 GMT</pubDate></item><item><title><![CDATA[警惕：新型开发者钓鱼攻击]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/posts/warn/developer-phishing-alert-renvolve-biz">https://blog.star-dust.link/posts/warn/developer-phishing-alert-renvolve-biz</a></blockquote><div><h2 id="">收到可疑邮件</h2><p>最近，我收到了来自renvolve.biz的这样一封看似专业的邮件：</p><pre class=""><code class="">Hi [我的名字],

Your work weaving GLSL and TypeScript feels both precise and creatively flexible, 
a rare combo for a developer balancing raw graphics with clean logic.

What&#x27;s the most stubborn technical hurdle you&#x27;re currently facing in that stack, 
whether it&#x27;s a missing tool or a backlog item you keep manually patching?

We are the engineers behind renvolve, and if there&#x27;s a way we could build something 
or take a load off your plate, our CEO would love to grab a quick chat.

Thanks,
Sarah
</code></pre>
<p>乍一看，这封邮件似乎很专业：</p><ul><li>提到了我在Github开源仓库使用的具体技术栈（GLSL + TypeScript）</li><li>使用了个性化的恭维</li><li>语气友好且专业</li><li>提出了一个看似合理的&quot;合作&quot;请求</li></ul><p><strong>但这一切很可能都是精心设计的陷阱。</strong></p><hr/><h2 id="">深入调查</h2><p>出于好奇和警惕，我对这个域名进行了调查，结果令人担忧：</p><h3 id="-1">危险信号 #1：极低的信任评分</h3><ul><li><strong>ScamAdviser 评分：23/100</strong></li><li>被多个安全网站标记为&quot;<strong>可疑网站</strong>&quot;</li></ul><h3 id="-2">危险信号 #2：新注册的域名</h3><ul><li>域名注册时间仅约 <strong>28 天</strong></li><li>这是诈骗网站的典型特征——使用新域名来避免负面历史记录</li></ul><h3 id="-3">危险信号 #3：匿名运营</h3><ul><li>很难确定谁真正运营该网站</li><li>缺乏透明的公司信息或团队介绍</li></ul><h3 id="-4">危险信号 #4：无实际业务痕迹</h3><ul><li>网站上没有真实的产品或服务展示</li><li>没有可验证的客户案例或合作伙伴</li></ul><p>这些都表明这很可能是一场诈骗活动。</p><h2 id="">如何防范</h2><p>收到类似邮件时，请执行以下检查：</p><pre class=""><code class="">□ 检查发件人域名的信誉评分
□ 搜索公司名称 + &quot;scam&quot; 或 &quot;fraud&quot;
□ 在 LinkedIn 上查找声称的联系人
□ 查看公司网站是否有真实业务内容
□ 检查域名注册日期（使用 whois 查询）
□ 不要点击任何链接，先独立搜索公司信息
</code></pre></div><p style="text-align:right"><a href="https://blog.star-dust.link/posts/warn/developer-phishing-alert-renvolve-biz#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/posts/warn/developer-phishing-alert-renvolve-biz</link><guid isPermaLink="true">https://blog.star-dust.link/posts/warn/developer-phishing-alert-renvolve-biz</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Mon, 27 Jul 2026 08:34:29 GMT</pubDate></item><item><title><![CDATA[多平台模组迁移：在 Fabric 命令注册中偷懒]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/posts/note/multi-platform-mod-migration-fabric-command-registration-shortcuts">https://blog.star-dust.link/posts/note/multi-platform-mod-migration-fabric-command-registration-shortcuts</a></blockquote><div><p><em>作者声明：创作过程中使用了AI辅助</em></p><p><em>以下提及的项目全部是 Minecraft 模组或插件项目。</em></p><h3 id="">引言</h3><p>把一套 Bukkit 命令系统搬到 Fabric 时，最劝退的不是 Brigadier 本身，而是要为了一个平台把所有命令逻辑全部重写一遍的痛苦。</p><p>由于之前有给一个其他项目写多平台的经历，因为那个项目代码少所以各平台的逻辑全部重复写了很多遍，造成更新时需要修改所有平台的代码，非常痛苦，因此这次给项目做多平台兼容没有使用这种方法，而是构建了 core 和 common 桥接接口。</p><p>但在编写 Fabric 平台逻辑的时候又遇到了  Brigadier 给每个命令建节点的问题，那不又回到了痛苦的重复工作吗？这事我当然不想干。</p><p>项目的 core 层早就定型了，权限校验、参数解析、补全逻辑全在那儿。如果按 Brigadier 原生写法给每个子命令建节点，等于把同样的业务用另一种范式再表达一次，以后改命令还得同步维护两套代码。这不是技术问题，是体力活。</p><p>因此我选择了让 Brigadier 当个收口，其他的事还是交给 core 管。</p><h3 id="">开发</h3><h4 id="-greedystring-">用 greedyString 当万能容器</h4><p>别为每个子命令建 LiteralArgumentBuilder。用一个 <code>greedyString()</code> 把用户输入的所有东西原样吞下来，再丢给 core 切分：</p><pre class="language-java lang-java"><code class="language-java lang-java">RequiredArgumentBuilder arg = RequiredArgumentBuilder
    .argument(&quot;args&quot;, StringArgumentType.greedyString());

arg.executes(ctx -&gt; {
    String raw = StringArgumentType.getString(ctx, &quot;args&quot;);
    return commandExecutor.onCommand(ctx.getSource(), parseArgs(raw)) ? 1 : 0;
});
</code></pre>
<p>选 <code>greedyString()</code> 而不是普通 <code>string()</code>，是因为它遇到空格不会停。<code>/&lt;command&gt; kick player reason with spaces</code> 这种带空格的参数才不会被截断。</p><h4 id="">空参数兜底</h4><p>上面那段代码有个盲区。只敲 <code>/&lt;command&gt;</code> 不带参数时，<code>greedyString</code> 节点不会被匹配，<code>arg.executes</code> 根本不会触发。</p><p>必须在根节点单独挂一个 executes：</p><pre class="language-java lang-java"><code class="language-java lang-java">literal.then(arg);
// 处理 /&lt;command&gt; 后面没跟参数的情况
literal.executes(ctx -&gt; 
    commandExecutor.onCommand(ctx.getSource(), new String[0]) ? 1 : 0
);
</code></pre>
<h4 id="">防御性解析</h4><p>既然要手动切字符串，就别信 <code>split(&quot; &quot;)</code>。在聊天框多敲两个空格是常事，直接 split 会产生空字符串元素，下游分分钟数组越界。</p><pre class="language-java lang-java"><code class="language-java lang-java">static String[] parseArgs(String input) {
    if (input == null || input.trim().isEmpty()) return new String[0];
    List&lt;String&gt; tokens = new ArrayList&lt;&gt;();
    for (String s : input.trim().split(&quot;\\s+&quot;)) {
        if (!s.isEmpty()) tokens.add(s);
    }
    return tokens.toArray(new String[0]);
}
</code></pre>
<p><code>\s+</code> 处理连续空白，<code>trim()</code> 干掉首尾。</p><h4 id="">命令补全</h4><p>Brigadier 的 suggests 是异步的，但返回值本质就是字符串列表。我项目 core 层的 <code>onTabComplete</code> 返回的也是 <code>List&lt;String&gt;</code>，两者可以直接对接：</p><pre class="language-java lang-java"><code class="language-java lang-java">arg.suggests((ctx, builder) -&gt; {
    List&lt;String&gt; completions = commandExecutor.onTabComplete(
        ctx.getSource(), 
        parseArgs(builder.getInput())
    );
    if (completions != null) completions.forEach(builder::suggest);
    return builder.buildFuture();
});
</code></pre>
<p>代价是客户端没有语法高亮，因为服务端没下发完整语法树。但补全内容是对的，对于后台管理工具来说够用了。</p><h3 id="">总结</h3><h4 id="">什么时候该这么干</h4><p>说清楚边界比假装万能更重要。</p><p>适合的场景：有现成的跨平台 Core、从 Bukkit 迁移、命令体系已经稳定且数量较多。</p><p>不适合的场景：纯 Fabric 新项目、命令结构简单、需要客户端语法高亮和自动帮助生成。</p><p>Brigadier 原生 API 在设计上确实更优雅。但工程决策不是选最优雅的，是选最省事的。当你的约束条件决定了重写成本远高于兼容成本时，“不够优雅”就是当下最合理的方案。</p></div><p style="text-align:right"><a href="https://blog.star-dust.link/posts/note/multi-platform-mod-migration-fabric-command-registration-shortcuts#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/posts/note/multi-platform-mod-migration-fabric-command-registration-shortcuts</link><guid isPermaLink="true">https://blog.star-dust.link/posts/note/multi-platform-mod-migration-fabric-command-registration-shortcuts</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Sun, 28 Jun 2026 15:50:34 GMT</pubDate></item><item><title><![CDATA[测试手记]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/notes/2">https://blog.star-dust.link/notes/2</a></blockquote><div><p>测试Cyber主题Lexical功能兼容性。</p><blockquote class="markdown-alert-note"><header>NOTE</header><p>这是Note</p></blockquote>
<pre class="language-python lang-python"><code class="language-python lang-python">print(&quot;Hello Lexical&quot;)
</code></pre>
<excalidraw>{&quot;elements&quot;:[{&quot;id&quot;:&quot;mDddXi1dZrWgyy0V6O63i&quot;,&quot;type&quot;:&quot;diamond&quot;,&quot;x&quot;:272.4652099609375,&quot;y&quot;:330.4770202636719,&quot;width&quot;:123.95062255859375,&quot;height&quot;:178.65383911132812,&quot;angle&quot;:0,&quot;strokeColor&quot;:&quot;#1e1e1e&quot;,&quot;backgroundColor&quot;:&quot;transparent&quot;,&quot;fillStyle&quot;:&quot;solid&quot;,&quot;strokeWidth&quot;:2,&quot;strokeStyle&quot;:&quot;solid&quot;,&quot;roughness&quot;:1,&quot;opacity&quot;:100,&quot;groupIds&quot;:[],&quot;frameId&quot;:null,&quot;index&quot;:&quot;a0&quot;,&quot;roundness&quot;:{&quot;type&quot;:2},&quot;seed&quot;:454625983,&quot;version&quot;:9,&quot;versionNonce&quot;:13404913,&quot;isDeleted&quot;:false,&quot;boundElements&quot;:[{&quot;id&quot;:&quot;qtDDPloXgyldghu4UFs1U&quot;,&quot;type&quot;:&quot;arrow&quot;}],&quot;updated&quot;:1785767102319,&quot;link&quot;:null,&quot;locked&quot;:false},{&quot;id&quot;:&quot;qtDDPloXgyldghu4UFs1U&quot;,&quot;type&quot;:&quot;arrow&quot;,&quot;x&quot;:400.4100341796875,&quot;y&quot;:420.3900451660156,&quot;width&quot;:138.1488037109375,&quot;height&quot;:3.429779052734375,&quot;angle&quot;:0,&quot;strokeColor&quot;:&quot;#1e1e1e&quot;,&quot;backgroundColor&quot;:&quot;transparent&quot;,&quot;fillStyle&quot;:&quot;solid&quot;,&quot;strokeWidth&quot;:2,&quot;strokeStyle&quot;:&quot;solid&quot;,&quot;roughness&quot;:1,&quot;opacity&quot;:100,&quot;groupIds&quot;:[],&quot;frameId&quot;:null,&quot;index&quot;:&quot;a2&quot;,&quot;roundness&quot;:{&quot;type&quot;:2},&quot;seed&quot;:1806378385,&quot;version&quot;:8,&quot;versionNonce&quot;:662131985,&quot;isDeleted&quot;:false,&quot;boundElements&quot;:[],&quot;updated&quot;:1785767102319,&quot;link&quot;:null,&quot;locked&quot;:false,&quot;points&quot;:[[0,0],[138.1488037109375,-3.429779052734375]],&quot;lastCommittedPoint&quot;:null,&quot;startBinding&quot;:{&quot;elementId&quot;:&quot;mDddXi1dZrWgyy0V6O63i&quot;,&quot;focus&quot;:0.024896310486690384,&quot;gap&quot;:7.869389678038024},&quot;endBinding&quot;:null,&quot;startArrowhead&quot;:null,&quot;endArrowhead&quot;:&quot;arrow&quot;,&quot;elbowed&quot;:false},{&quot;id&quot;:&quot;EfNDTBurwzd5WNNpOt82W&quot;,&quot;type&quot;:&quot;rectangle&quot;,&quot;x&quot;:554.16356912001,&quot;y&quot;:273.2788991869879,&quot;width&quot;:214.16763305664062,&quot;height&quot;:257.0424499511719,&quot;angle&quot;:0,&quot;strokeColor&quot;:&quot;#1e1e1e&quot;,&quot;backgroundColor&quot;:&quot;transparent&quot;,&quot;fillStyle&quot;:&quot;solid&quot;,&quot;strokeWidth&quot;:2,&quot;strokeStyle&quot;:&quot;solid&quot;,&quot;roughness&quot;:1,&quot;opacity&quot;:100,&quot;groupIds&quot;:[],&quot;frameId&quot;:null,&quot;index&quot;:&quot;a3&quot;,&quot;roundness&quot;:{&quot;type&quot;:3},&quot;seed&quot;:902573247,&quot;version&quot;:25,&quot;versionNonce&quot;:1759992095,&quot;isDeleted&quot;:false,&quot;boundElements&quot;:[],&quot;updated&quot;:1785767162791,&quot;link&quot;:null,&quot;locked&quot;:false},{&quot;id&quot;:&quot;1Fwkq0dFZ<em>H</em>0xKwu1mQ5&quot;,&quot;type&quot;:&quot;text&quot;,&quot;x&quot;:296.4015081150176,&quot;y&quot;:413.7774725410162,&quot;width&quot;:49.159942626953125,&quot;height&quot;:25,&quot;angle&quot;:0,&quot;strokeColor&quot;:&quot;#1e1e1e&quot;,&quot;backgroundColor&quot;:&quot;transparent&quot;,&quot;fillStyle&quot;:&quot;solid&quot;,&quot;strokeWidth&quot;:2,&quot;strokeStyle&quot;:&quot;solid&quot;,&quot;roughness&quot;:1,&quot;opacity&quot;:100,&quot;groupIds&quot;:[],&quot;frameId&quot;:null,&quot;index&quot;:&quot;aA&quot;,&quot;roundness&quot;:null,&quot;seed&quot;:1501642737,&quot;version&quot;:6,&quot;versionNonce&quot;:1034299729,&quot;isDeleted&quot;:false,&quot;boundElements&quot;:null,&quot;updated&quot;:1785767180612,&quot;link&quot;:null,&quot;locked&quot;:false,&quot;text&quot;:&quot;Text&quot;,&quot;fontSize&quot;:20,&quot;fontFamily&quot;:5,&quot;textAlign&quot;:&quot;left&quot;,&quot;verticalAlign&quot;:&quot;top&quot;,&quot;containerId&quot;:null,&quot;originalText&quot;:&quot;Text&quot;,&quot;autoResize&quot;:true,&quot;lineHeight&quot;:1.25},{&quot;id&quot;:&quot;XFdfwM2U8GB1gGWxc5Y2T&quot;,&quot;type&quot;:&quot;text&quot;,&quot;x&quot;:605.0865626413354,&quot;y&quot;:371.73230843191175,&quot;width&quot;:134.57986450195312,&quot;height&quot;:25,&quot;angle&quot;:0,&quot;strokeColor&quot;:&quot;#1e1e1e&quot;,&quot;backgroundColor&quot;:&quot;transparent&quot;,&quot;fillStyle&quot;:&quot;solid&quot;,&quot;strokeWidth&quot;:2,&quot;strokeStyle&quot;:&quot;solid&quot;,&quot;roughness&quot;:1,&quot;opacity&quot;:100,&quot;groupIds&quot;:[],&quot;frameId&quot;:null,&quot;index&quot;:&quot;aB&quot;,&quot;roundness&quot;:null,&quot;seed&quot;:205510609,&quot;version&quot;:14,&quot;versionNonce&quot;:1640813617,&quot;isDeleted&quot;:false,&quot;boundElements&quot;:null,&quot;updated&quot;:1785767199881,&quot;link&quot;:null,&quot;locked&quot;:false,&quot;text&quot;:&quot;Another Text&quot;,&quot;fontSize&quot;:20,&quot;fontFamily&quot;:5,&quot;textAlign&quot;:&quot;left&quot;,&quot;verticalAlign&quot;:&quot;top&quot;,&quot;containerId&quot;:null,&quot;originalText&quot;:&quot;Another Text&quot;,&quot;autoResize&quot;:true,&quot;lineHeight&quot;:1.25}],&quot;appState&quot;:{&quot;viewBackgroundColor&quot;:&quot;#ffffff&quot;,&quot;gridSize&quot;:20},&quot;files&quot;:{}}
</excalidraw><tag>tag</tag><p>Tag 1</p><p>::: note</p><p>:::</p></div><p style="text-align:right"><a href="https://blog.star-dust.link/notes/2#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/notes/2</link><guid isPermaLink="true">https://blog.star-dust.link/notes/2</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Sat, 27 Jun 2026 06:40:18 GMT</pubDate></item><item><title><![CDATA[解决api/v3/aggregate返回NOT FOUND问题]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/notes/1">https://blog.star-dust.link/notes/1</a></blockquote><span>需发表一篇笔记</span><p style="text-align:right"><a href="https://blog.star-dust.link/notes/1#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/notes/1</link><guid isPermaLink="true">https://blog.star-dust.link/notes/1</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Wed, 17 Jun 2026 11:18:25 GMT</pubDate></item><item><title><![CDATA[Mx Space v13部署：踩坑实录]]></title><description><![CDATA[<div><blockquote>该渲染由 Cyber API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.star-dust.link/posts/note/mxspace-deploy-trapped-log">https://blog.star-dust.link/posts/note/mxspace-deploy-trapped-log</a></blockquote><div><p>最近发现了一个很喜欢的博客框架Mx Space，于是准备把之前的框架换掉，由于官方文档实际上过时，中间踩了很多坑。</p><p><strong>坑1：Docker Ccompose中健康检查调用的API版本不对</strong>
在Mx Space Core v13中，API已由<code>v2</code>升级为<code>v3</code>，但官方文档给出的命令拉到的<code>docker-compose.yml</code>中仍为<code>v2</code>，导致容器状态一直为<code>starting</code>。</p><p>解决方法：在docker-compose.yml中将<code>http://127.0.0.1:2333/api/v2/ping</code>中的<code>v2</code>改为<code>v3</code></p><p><strong>坑2：配置与云函数已改名</strong>
在老版本的Mx Space的「配置与云函数」已改名为「代码片段」，导致我一开始一直没找到添加前端所需云函数的页面。</p><p><strong>坑3: 需要发表一篇笔记<code>api/v3/aggregate</code>才能正常工作</strong>
如果不发布笔记, <code>api/v3/aggregate</code>的返回为<code>NOT_FOUND</code>。</p></div><p style="text-align:right"><a href="https://blog.star-dust.link/posts/note/mxspace-deploy-trapped-log#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.star-dust.link/posts/note/mxspace-deploy-trapped-log</link><guid isPermaLink="true">https://blog.star-dust.link/posts/note/mxspace-deploy-trapped-log</guid><dc:creator><![CDATA[竹かさ]]></dc:creator><pubDate>Fri, 12 Jun 2026 01:30:02 GMT</pubDate></item></channel></rss>