如何在 jBilling scheduled plugin 中创建 payment?
结论
创建不关联 invoice 的 payment 时,不要使用:
applyPayment(payment, null, executorUserId)
applyPayment 用于创建并应用付款,通常会进入 invoice 分配逻辑。如果只需保存一笔未分配付款,应调用当前 jBilling 版本中用于“创建 payment”的 API,例如内部接口的 create(...),或 Web Service API 的 createPayment(...)。
LazyInitializationException 与认证失败无关。userbl.webServicesAuthenticate(user, pass) 只负责身份验证,不会为 scheduled plugin 打开 Hibernate Session,因此不能解决该异常。
推荐实现
如果当前版本的 IPaymentSessionBean 提供 create 方法,可以在 scheduled plugin 中直接调用:
IPaymentSessionBean paymentSession =
Context.getBean(Context.Name.PAYMENT_SESSION);
PaymentDTOEx payment = new PaymentDTOEx(paymentWS);
Integer paymentId = paymentSession.create(payment, executorUserId);
注意区分这里的两个用户标识:
- payment 中的
userId:付款所属的客户。 executorUserId:执行本次操作的 jBilling 用户,主要用于权限检查和审计。
不同 jBilling 分支中的 create 方法签名可能略有不同,请以当前版本的 IPaymentSessionBean 接口为准。如果内部接口没有对应方法,可以改用公开 API:
JbillingAPI api = JbillingAPIFactory.getAPI(
url,
username,
password
);
Integer paymentId = api.createPayment(paymentWS);
scheduled plugin 已经运行在 jBilling 服务端时,通常应优先调用内部 session bean,无需再次通过 Web Service 登录。
为什么会出现 LazyInitializationException
scheduled plugin 通常由调度线程执行,不会经过 Web 请求中的 Open Session in View 流程。以下情况都可能导致代码在 Hibernate Session 关闭后访问代理对象:
PaymentDTOEx中保存了 Hibernate entity 或尚未初始化的关联对象。- payment、用户、币种或 payment instrument 是在之前的事务中加载的。
- scheduled plugin 缓存了某个 entity,并在后续任务中继续使用。
- 代码在事务外访问了 entity 的关联属性。
- 实际取得的业务对象没有经过 Spring 或 EJB 的事务代理。
不要通过手动打开 Hibernate Session 来掩盖这类问题。任务之间只传递 ID 或普通 DTO,并在事务内重新加载所需数据,会更稳妥。
事务边界的处理
如果创建付款前还要读取用户、币种或支付方式,应将完整操作放进由 Spring 或 EJB 管理的事务服务:
@Service
public class ScheduledPaymentService {
private final IPaymentSessionBean paymentSession;
public ScheduledPaymentService(IPaymentSessionBean paymentSession) {
this.paymentSession = paymentSession;
}
@Transactional
public Integer createUnallocatedPayment(
PaymentWS paymentWS,
Integer executorUserId) {
PaymentDTOEx payment = new PaymentDTOEx(paymentWS);
return paymentSession.create(payment, executorUserId);
}
}
scheduled plugin 应通过容器取得该服务:
ScheduledPaymentService service =
Context.getBean(ScheduledPaymentService.class);
Integer paymentId =
service.createUnallocatedPayment(paymentWS, executorUserId);
不要在 plugin 中通过 new ScheduledPaymentService(...) 创建服务对象,否则 @Transactional 不会生效。具体的 Bean 注册方式和 Context.getBean 重载形式取决于所使用的 jBilling 版本。
DTO 的构造要求
传给 payment service 的对象应是完整、独立的 DTO,不能携带从旧 Hibernate Session 中取得的实体代理。至少需要确认以下字段已经设置:
PaymentWS payment = new PaymentWS();
payment.setUserId(customerId);
payment.setAmount(amount);
payment.setCurrencyId(currencyId);
payment.setPaymentDate(new Date());
payment.setIsRefund(0);
还需要根据当前 jBilling 版本设置所需的 payment method 或 payment instrument 信息。字段要求会随版本和支付插件配置变化,请参考当前代码中的 PaymentWS、PaymentDTOEx 和 IPaymentSessionBean.create(...) 实现。
尤其不要将以下对象长期保存在 scheduled plugin 的实例字段中:
UserDTO user;
PaymentDTO payment;
InvoiceDTO invoice;
应保存对应的 ID,并在每次执行任务时,在事务内重新查询对象。
注意事项
invoiceId == null不代表“只创建未分配付款”。它仍可能使applyPayment进入不适合该场景的逻辑。webServicesAuthenticate(...)不负责创建 Hibernate Session,也不会建立事务。- 不要把手动调用
Hibernate.initialize(...)当作长期解决方案,因为调用时仍然必须存在有效的 Session。 - payment 能否保持未分配状态,还可能受自动分配、账龄处理或付款处理插件配置影响。
- 如果改用
create(...)后仍然出现同样的异常,应结合异常堆栈确认是哪个属性触发了代理初始化。问题通常出现在 DTO 转换过程,或事务外读取实体关联的位置,而不是invoiceId本身。
备注:内容仅供参考。
