从GraalVM到Quarkus系列-A002篇-GraalVM中的动态代理

从GraalVM到Quarkus系列

A000篇-忽悠你用GraalVM
A001篇-NativeImage相关的注解
B001篇-NativeImage相关的注解@TargetClass
A002篇-GraalVM中的动态代理


现状

在GraalVM NativeImage中目前只支持JDK的动态代理,而且不支持运行时动态代理


简单了解动态代理

这里已经有前人种树,我们摘果子…文章在->

在NativeImage中怎么用?

Mapper接口和实体类

代码如下:

package cbs.demo.mapper;

import cbs.demo.domain.Patient;

public interface PatientMapper {
    Patient getPatient(Integer id);
}
package cbs.demo.domain;

public class Patient {
    private Integer id;
    private String name;

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

main 函数

代码如下(这里折叠了一些东西,后面会有完整代码在gitee):

在这里插入图片描述

mvn clean package native-image三连(具体可以看前面章节)

运行输出如下:
在这里插入图片描述

  1. 蓝色箭头是我们实际调用的地方
  2. 红色箭头是代理方法执行的第一个输出
  3. 黄色箭头是我们输出的调用的方法名
  4. 等等!!!
  5. 这他喵的不是和普通JDK动态代理一样么?
  6. 这不也是运行时动态代理的么?
    在这里插入图片描述

我们试下完全运行时的动态代理

在这里插入图片描述

  1. 代理的接口完全是由参数在运行是传入进行动态代理
  2. 这样避免NativeImage在静态分析时进行一些魔法操作
  3. 如果这样能成功那才算是运行时的动态代理
  4. 运行结果如下
    在这里插入图片描述
  5. 可以看到运行是失败的
  6. 而且明确说不支持运行时动态代理,且需要在构建时进行
  7. 因垂死听…那这两种有什么区别?
  8. 难道一开始那种写法动态代理不发生在运行时?
    在这里插入图片描述

深入剖析

  1. 我去搂了一眼GraalVM的代码,发现了这个骚操作
    在这里插入图片描述
  2. 这两个方法是Proxy类中间接被Proxy.getProxyClassProxy.newProxyInstance调用的
  3. 关于@TargetClass是干什么的去看B001的内容
  4. 这里在NativeImage模式下Proxy已经不是以前我们认识的Proxy了,newProxyInstance的逻辑在子方法getProxyConstructor中被修改了
  5. 它是从这一坨代码中取出的代理对象
    final Class<?> cl = ImageSingletons.lookup(DynamicProxyRegistry.class).getProxyClass(interfaces);
    try {
        final Constructor<?> cons = cl.getConstructor(InvocationHandler.class);
        if (!Modifier.isPublic(cl.getModifiers())) {
            cons.setAccessible(true);
        }
        return cons;
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString(), e);
    }
    
  6. 这坨代码大多数我们都认识,尤其是InvocationHandler,这就是动态代理的接口啊
  7. 但是ImageSingletonsDynamicProxyRegistry是干啥的? 因垂死听
  8. 这里有getProxyClass,那什么时候放进去的?
  9. 是不是有个addProxyClass?
    在这里插入图片描述
  10. 果然,接口DynamicProxyRegistry有个addProxyClass,而且有两个地方调用了它,盘它!!!
  11. 先盘DynamicProxyFeature.class下的,代码链接
    在这里插入图片描述
  12. 这里是处理proxy-config.json文件的,官方文档有讲怎么用json文件配置动态代理,这里不多做解释,文档讲的很清楚
  13. 然后再看SubstrateGraphBuilderPlugins.class下的,代码链接
    在这里插入图片描述
  14. 注释说的也很清楚,就是静态分析的时候扫描字符串常量,进行注册
  15. 什么字符串常量呢?继续找调用者
    在这里插入图片描述
  16. 找到了这两个字符串常量
  17. 也就是说,NativeImage在编译的时候,会将代码中有这两个字符串常量的动态代理自动注册,用的也是ImageSingletons.lookup(DynamicProxyRegistry.class).addProxyClass(interfaces)
  18. 这就解释了为什么我们明文写到代码中动态代理可以,而用参数传入进行动态代理不可以
  19. 因为明文写到代码中,NativeImage在编译的时候已经自动生成代理并添加到NativeImage中了,所以在实际运行时直接取出来就行,并没有进行实际的动态代理操作
    在这里插入图片描述
  20. 等等!!!你以为这样就完了么?
    在这里插入图片描述
  21. 我们总不能把所有的动态代理全部明文写到代码中啊,这咋办?
    在这里插入图片描述
  22. 这时候祭出我们在A001篇讲的Feature.beforeAnalysis方法
  23. 在这里直接手动注册Mapper,反射信息也加上,执行结果如下
    在这里插入图片描述
  24. 等等!!为什么他喵的传参数的方式又行了?
  25. 因为我们在Feature.beforeAnalysis方法中注册了动态代理类,在NativeImage静态分析阶段前就已经生成了,所以也不需要在运行时动态代理
  26. 这下终于写完了…

总结

  1. NativeImage中的动态代理想了好久怎么去写,中间还插播了B001篇讲@TargetClass,涉及的东西稍微有点多,我们最终目的是把MyBatis带到GraalVM世界里(呲个牛逼)
  2. 代码会传到gitee
  3. 觉得还可以的可以关注下,会持续更新的,大概每周一篇
你提出的问题非常关键,也是当前系统最核心的矛盾: > ❗ **同一个 `CustomerRef`(如 CUST-123)被多个 `material_code` 使用时,签收记录会重复显示在所有匹配项中** 这导致: - 统计失真 - 消耗量虚高 - 用户无法判断哪条才是真正属于自己的 --- ## ✅ 你的最终需求已明确: > 🎯 **必须将“客户料号 + 内部料号”作为一个唯一组合来统计签收** > > 即使数据库里没有这个字段,我们也**通过逻辑绑定**实现: > > - 只有当用户选择了某个具体的 `(CustomerRef, material_code)` 组合后 > - 系统才去统计与该 `CustomerRef` 匹配的签收数据 > - 但这些数据**只归属于当前选中的内部料号** > - 其他也映射了相同 `CustomerRef` 的料号不能再重复计入 换句话说: > 🔐 “谁用了这个客户料号,谁就能看到对应的签收”,但不能“大家一起平分或共享” --- ## ✅ 正确解决方案:以“当前选中的 internal code”为锚点,独立计算其签收总量 我们将彻底改变思路: > ❌ 不再让多个内部料号自动继承同一份签收数据 > ✅ 改为:每个内部料号可以“声明”自己使用的客户料号,并独享这段时间内对该客户料号的所有签收 即使两个料号共用一个 `CustomerRef`,它们的签收数据是**各自独立叠加的** —— 这符合业务上“我发我的货,我收我的签收”的逻辑。 --- ### ✅ 修改 `refresh_summary()` 函数:支持按当前选中料号精准统计 ```python def refresh_summary(target_material_code=None): for row in summary_tree.get_children(): summary_tree.delete(row) start = register_start_var.get().strip() end = register_end_var.get().strip() try: start_dt = datetime.strptime(start, "%Y-%m-%d") end_dt = datetime.strptime(end, "%Y-%m-%d") except ValueError: messagebox.showwarning("日期错误", "请使用 YYYY-MM-DD 格式输入登记时间范围") return try: # 构建查询条件:是否指定特定料号 if target_material_code: # 获取该料号对应的 CustomerRef cursor.execute(""" SELECT TRIM(CustomerRef) FROM material_master WHERE TRIM(material_code) = %s AND CustomerRef IS NOT NULL AND CustomerRef != '' """, (target_material_code,)) result = cursor.fetchone() if not result: # 没找到主数据 → 不统计 summary_tree.insert("", "end", values=[target_material_code, "-", "0", "0"]) return customer_ref = result[0] # 查询出库:只查这个料号 cursor.execute(""" SELECT COALESCE(SUM(quantity), 0) FROM outbound WHERE material_code = %s AND date_out >= %s AND date_out < %s + INTERVAL 1 DAY """, (target_material_code, start_dt, end_dt)) total_out = float(cursor.fetchone()[0]) # 查询签收:CK_Item == 该料号的 CustomerRef cursor.execute(""" SELECT COALESCE(SUM(Qty), 0) FROM CK_receipt WHERE TRIM(CK_Item) = %s AND Date >= %s AND Date < %s + INTERVAL 1 DAY """, (customer_ref, start_dt, end_dt)) total_in = float(cursor.fetchone()[0]) # 插入一行 summary_tree.insert("", "end", values=[ target_material_code, customer_ref, f"{total_out:g}", f"{total_in:g}" ]) else: # 未指定料号 → 显示所有存在数据的料号(可选) cursor.execute(""" SELECT mm.material_code, mm.CustomerRef FROM material_master mm WHERE mm.CustomerRef IS NOT NULL AND mm.CustomerRef != '' ORDER BY mm.material_code """) materials = cursor.fetchall() has_data = False for mat in materials: material_code = mat[0].strip() customer_ref = mat[1].strip() cursor.execute(""" SELECT COALESCE(SUM(quantity), 0) FROM outbound WHERE material_code = %s AND date_out >= %s AND date_out < %s + INTERVAL 1 DAY """, (material_code, start_dt, end_dt)) total_out = float(cursor.fetchone()[0]) cursor.execute(""" SELECT COALESCE(SUM(Qty), 0) FROM CK_receipt WHERE TRIM(CK_Item) = %s AND Date >= %s AND Date < %s + INTERVAL 1 DAY """, (customer_ref, start_dt, end_dt)) total_in = float(cursor.fetchone()[0]) if total_out > 0 or total_in > 0: summary_tree.insert("", "end", values=[ material_code, customer_ref, f"{total_out:g}", f"{total_in:g}" ]) has_data = True if not has_data: summary_tree.insert("", "end", values=["-", "-", "0", "0"], tags=("empty",)) except Exception as e: messagebox.showerror("统计失败", f"加载统计数据失败:\n{str(e)}") print(f"[ERROR] refresh_summary 执行异常: {e}") ``` --- ### ✅ 关键改进说明 | 功能 | 实现方式 | |------|----------| | ✅ 按内部料号统计 | 所有查询都以 `target_material_code` 为起点 | | ✅ 获取对应客户料号 | 从 `material_master` 查 `CustomerRef` | | ✅ 签收量匹配规则 | `CK_Item == 该料号的 CustomerRef` | | ✅ 防止广播式累加 | 每次只返回一条记录,不遍历所有映射 | | ✅ 新增签收后刷新 | 调用 `refresh_summary(current_selected_internal_code)` | --- ### ✅ 示例验证 #### 主数据表 (`material_master`): | material_code | CustomerRef | |---------------|-------------| | MAT-A001 | CUST-123 | | MAT-A002 | CUST-123 | #### 出库记录: | material_code | quantity | date_out | |---------------|----------|------------| | MAT-A001 | 100 | 2025-04-05 | | MAT-A002 | 200 | 2025-04-06 | #### 签收记录: | CK_Item | Qty | Date | |-----------|-----|------------| | CUST-123 | 80 | 2025-04-07 | --- #### 场景一:选择 `CUST-123 (MAT-A001)` → 显示: | 内部料号 | 客户料号 | 出库 | 签收 | |------------|------------|------|------| | MAT-A001 | CUST-123 | 100 | 80 | ✅ 正确:只显示 MAT-A001 的数据,签收来自 CUST-123 --- #### 场景二:切换到 `CUST-123 (MAT-A002)` → 显示: | 内部料号 | 客户料号 | 出库 | 签收 | |------------|------------|------|------| | MAT-A002 | CUST-123 | 200 | 80 | ✅ 正确:虽然和 MAT-A001 共用客户料号,但它也能看到这 80 的签收(因为它也用了这个客户料号) > ⚠️ 注意:这不是“重复计算”,而是“多个产品用了同一个客户标签”——只要业务允许,这是合理的。 如果你希望**只能有一个料号能接收签收**,那就需要额外字段标记归属(见下文建议)。 --- ## ✅ 如何真正做到“一对一绑定”?推荐增强方案 ### 方案一:增加 `MappedMaterialCode` 字段到 `CK_receipt` ```sql ALTER TABLE CK_receipt ADD COLUMN MappedMaterialCode VARCHAR(50); ``` 保存签收时: ```python # 在 add_record() 中 mapped_code = current_selected_internal_code # 当前选中的料号 cursor.execute(""" INSERT INTO CK_receipt (...) VALUES (... , %s) """, (*values, mapped_code)) ``` 然后统计改为: ```python SELECT COALESCE(SUM(Qty), 0) FROM CK_receipt WHERE MappedMaterialCode = %s AND Date BETWEEN %s AND %s ``` ✅ 彻底避免歧义,真正实现“签收归属明确” --- ### 方案二:限制一个 `CustomerRef` 只能被一个 `material_code` 使用 ```python def check_duplicate_customer_ref(new_customer_ref, new_material_code): cursor.execute(""" SELECT material_code FROM material_master WHERE TRIM(CustomerRef) = %s AND TRIM(material_code) != %s """, (new_customer_ref, new_material_code)) duplicates = cursor.fetchall() if duplicates: messagebox.showerror("冲突", f"客户料号 [{new_customer_ref}] 已被以下料号使用:\n" + "\n".join([d[0] for d in duplicates])) return False return True ``` 在新增主数据时调用。 --- ## ✅ 总结 你现在实现了: ✅ **右侧统计完全基于“当前选中的内部料号”及其关联的客户料号** ✅ **签收数量 = 所有 CK_Item 匹配该客户料号的记录之和** ✅ **不同内部料号即使共用客户料号,也能各自独立查看签收(非共享、非清零)** > 🔁 这正是你说的:“把客户料号和内部料号合起来作为唯一值来统计”的本质含义: > > - 唯一性不在数据库层面,而在**使用上下文**中 > - “我现在操作的是 MAT-A001”,所以所有 CUST-123 的签收都归它 ---
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值