当前位置: 首页 > news >正文

技术演进中的开发沉思-174 java-EJB:分布式通信

当年做跨服务器的系统时,第一次面临 “接口该选本地还是远程” 的抉择:商品服务部署在 A 服务器,订单服务部署在 B 服务器,订单要调用商品接口查价格,这时候必须用远程接口;而订单服务内部的库存模块和日志模块在同一服务器,用本地接口就行。那时候才明白,EJB 的本地 / 远程接口设计,本质是为了解决 “不同部署场景下的通信效率与可用性” 问题 —— 用对了接口,能少走很多性能弯路;用错了,可能让系统响应慢到用户投诉。

一、差异

EJB 的接口分两类:本地接口(Local Interface)和远程接口(Remote Interface),它们的核心差异在于 “通信范围” 和 “性能开销”,选择的关键是 “调用方与 EJB 是否在同一 JVM(或同一服务器)”。

1.1、核心差异:

先直白点说:本地接口是 “同一屋檐下的对话”,远程接口是 “跨城市的长途电话”—— 两者的通信成本天差地别:

对比维度

本地接口(Local)

远程接口(Remote)

通信范围

同一 JVM(如同一 Web 应用、同一服务器内的多个应用)

跨 JVM、跨服务器、甚至跨网络(如 A 机房调用 B 机房的 EJB)

通信协议

直接内存调用(无网络开销)

RMI-IIOP(远程方法调用协议,有网络传输开销)

数据传输

直接传对象引用(不用序列化)

必须序列化 / 反序列化对象(网络传输需二进制数据)

性能开销

极低(接近普通 Java 方法调用)

较高(网络延迟 + 序列化耗时)

适用场景

同一部署单元内的组件调用(如订单服务调用本地库存模块)

跨服务器、跨应用的分布式调用(如订单服务调用远程商品服务)

当年踩过一个经典的坑:把同一服务器内的 “订单 - 库存” 调用用了远程接口,结果原本 10ms 的本地调用,变成了 500ms 的远程调用(含序列化和网络延迟),整个下单流程变慢,最后换成本地接口才恢复正常。记住:能不用远程接口就不用,优先用本地接口 —— 远程接口是 “万不得已的分布式选择”,不是默认选项。

1.2、接口定义的 “小细节”

EJB 3.0 之后,接口定义很简单,核心是两个注解:@Local和@Remote,要么加在接口上,要么直接加在 EJB 类上(省略单独接口类)。

1.2.1、JVM 调用

比如订单服务内部的库存模块,用本地接口:

import javax.ejb.Local;import javax.ejb.Stateless;// 方式1:单独定义本地接口@Localpublic interface LocalInventoryService {// 扣库存方法(本地调用)boolean deduct(Long productId, int quantity);}// EJB实现类@Statelesspublic class InventoryService implements LocalInventoryService {@Overridepublic boolean deduct(Long productId, int quantity) {// 本地扣库存逻辑(直接操作数据库,无网络开销)System.out.println("本地扣库存:商品" + productId + ",数量" + quantity);return true;}}// 调用方(同一JVM内的OrderService)@Statelesspublic class OrderService {// 注入本地接口(用@EJB,容器直接返回本地实例引用)@EJBprivate LocalInventoryService inventoryService;public void createOrder(Order order) {// 本地调用:无序列化、无网络延迟,快!boolean deductSuccess = inventoryService.deduct(order.getProductId(), order.getQuantity());if (deductSuccess) {// 创建订单}}}

1.2.2、 远程接口示例(跨 JVM 调用)

比如订单服务调用远程商品服务查价格,用远程接口:


import javax.ejb.Remote;import javax.ejb.Stateless;import java.io.Serializable;// 方式1:单独定义远程接口(注意:远程传输的对象必须实现Serializable)@Remotepublic interface RemoteProductService {// 查商品价格(远程调用,返回的Product需序列化)Product getProductPrice(Long productId);}// 远程传输的实体类:必须实现Serializable,否则序列化失败public class Product implements Serializable {private static final long serialVersionUID = 1L; // 序列化版本号,当年忘加导致远程调用报错private Long id;private String name;private BigDecimal price;// getter、setter省略}// 远程EJB实现(部署在另一台服务器)@Statelesspublic class ProductService implements RemoteProductService {@Overridepublic Product getProductPrice(Long productId) {// 远程服务器查询商品价格Product product = new Product();product.setId(productId);product.setName("手机");product.setPrice(new BigDecimal("3999"));return product;}}// 调用方(订单服务,部署在本地服务器)@Statelesspublic class OrderService {// 注入远程接口(容器会通过RMI-IIOP连接远程服务器)@EJB(lookup = "java:global/RemoteApp/ProductService!com.example.RemoteProductService")// lookup值是远程EJB的JNDI地址,格式:java:global/应用名/EJB类名!接口全限定名private RemoteProductService productService;public void createOrder(Order order) {// 远程调用:会序列化请求→网络传输→远程执行→序列化响应→网络返回,慢!Product product = productService.getProductPrice(order.getProductId());BigDecimal totalPrice = product.getPrice().multiply(new BigDecimal(order.getQuantity()));// 创建订单}}

1.3、选择原则:3 个 “优先”

当年总结的选择经验,现在看依然实用:

  • 优先用本地接口:只要调用方和 EJB 在同一服务器(同一 JVM),哪怕是不同应用,都用本地接口 —— 性能远优于远程;
  • 必须跨服务器才用远程:比如多机房部署、微服务拆分到不同节点,才考虑远程接口;
  • 远程调用的对象要 “轻”:别传大对象(比如带图片字节数组的商品),当年传了大对象导致远程调用耗时超 2 秒,后来改成传商品 ID + 图片 URL 才解决。

二、远程调用

远程接口的核心是 “跨 JVM 通信”,EJB 用的是RMI-IIOP 协议(Remote Method Invocation over Internet Inter-ORB Protocol)—— 这是 Java 的远程调用标准,能让一个 JVM 的方法调用另一个 JVM 的方法,就像调用本地方法一样(但性能差很多)。

2.1、远程调用原理

当年为了排查远程调用超时问题,研究过 RMI-IIOP 的原理,其实就是 “序列化 - 传输 - 执行 - 反序列化” 四步:

  • 客户端序列化请求:调用方调用远程接口方法时,容器会把 “方法名 + 参数” 序列化成二进制数据(必须实现 Serializable);
  • 网络传输请求:通过 RMI-IIOP 协议把二进制数据传到远程服务器;
  • 远程服务器执行:远程容器反序列化请求→找到对应的 EJB 实例→执行方法→序列化执行结果(返回值或异常);
  • 网络返回响应:远程容器把序列化后的结果传回客户端→客户端反序列化→得到执行结果。

这个过程中,序列化 / 反序列化网络延迟是两大性能杀手 —— 当年做压测时,本地调用 QPS 能到 1000,远程调用 QPS 直接降到 100,差距主要在这两步。

2.2、性能考量

远程调用的性能问题,很多是 “开发者没注意细节” 导致的,当年踩过的坑值得记牢:

坑 1:没实现 Serializable,远程调用直接崩

远程传输的对象(参数、返回值)必须实现 Serializable 接口,当年有个同事写 Product 类时忘了加,结果远程调用报 “NotSerializableException”,查了半天才发现是少了个接口。记住:只要是远程传输的对象,不管是自定义 POJO 还是集合,都得实现 Serializable(基本类型如 Long、String 不用,因为 Java 自带序列化)。

坑 2:传大对象或频繁调用,性能雪崩

当年做订单详情查询,远程调用时传了 “订单 + 商品 + 用户 + 物流” 的完整大对象,每个对象有几十字段,序列化后数据量达 100KB,网络传输一次要 500ms;更糟的是,一次订单查询要调用 3 次远程接口(查商品、查用户、查物流),总耗时直接到 1.5 秒,用户投诉 “点详情半天加载不出来”。

后来优化方案:

  • 拆分大对象:远程调用只传必要字段(比如查价格只传商品 ID 和价格,不传商品描述、图片等);
  • 减少调用次数:把 3 次远程调用合并成 1 次(比如远程服务提供 “查订单关联数据” 的聚合接口);
  • 加缓存:本地缓存常用商品价格,避免频繁远程调用。优化后耗时降到 300ms,用户才满意。
坑 3:没处理远程调用超时,线程堆积

远程调用依赖网络,万一远程服务器卡了或网络断了,客户端会一直等,导致线程堆积。当年没设超时,远程商品服务故障时,订单服务的线程全卡在远程调用上,最后整个订单服务不可用。

后来在 EJB 客户端配置超时(以 JBoss 为例,在jboss-ejb-client.properties中):

# 远程调用超时时间:5秒,超过就报错,避免线程一直等jboss.ejb.client.timeout=5000

这样即使远程服务故障,客户端 5 秒后会抛超时异常,线程能释放,不会导致整个服务崩溃。

2.3、远程调用代码优化

EJB 3.1 之后支持 “无接口视图”,不用单独定义 Local/Remote 接口,直接在 EJB 类上加@LocalBean或@Remote注解,代码更简洁。比如远程 EJB 可以这样写:

import javax.ejb.Remote;import javax.ejb.Stateless;// 无单独接口,直接在类上加@Remote,指定远程接口的功能(本质是隐式接口)@Stateless@Remote // 标记为远程EJB,无单独接口public class ProductService {// 远程方法,返回的Product需序列化public Product getProductPrice(Long productId) {// 逻辑同上}}// 调用方注入时,lookup值格式:java:global/RemoteApp/ProductService!com.example.ProductService@EJB(lookup = "java:global/RemoteApp/ProductService!com.example.ProductService")private ProductService productService;

这种方式更简单,但缺点是 “接口不清晰”,团队协作时容易不知道远程方法有哪些参数 —— 当年小项目用这种方式很方便,大项目还是建议单独定义接口,可读性更高。

三、Web Service 集成

早期分布式系统中,跨语言、跨平台的通信常靠 Web Service(SOAP 协议)——EJB 支持用@WebService注解把无状态会话 Bean 直接暴露成 SOAP 服务,不用额外写 Web 层代码,当年做与.NET 系统的对接时,这个功能帮了大忙。

3.1、SOAP 与 WSDL

先简单说:Web Service 是 “跨语言的远程调用”,比如 Java 的 EJB 能被.NET 的程序调用,核心靠两个东西:

  • SOAP 协议:基于 XML 的通信协议,用来传输请求和响应(比如调用 “查价格” 方法,会把方法名、参数包装成 XML 格式传输);
  • WSDL 文档:Web Service 的 “说明书”,自动生成,包含服务地址、方法名、参数类型等信息,调用方(不管什么语言)都能通过 WSDL 生成调用代码。

EJB 的 Web Service 集成,本质是 “容器自动把 EJB 方法转换成 SOAP 接口,并生成 WSDL”,开发者不用手动处理 XML 和 SOAP 协议。

3.2、把无状态 Bean 暴露成 Web Service

当年做与第三方.NET 系统的商品查询对接,用 EJB 暴露 Web Service,步骤很简单:

3.2.1、 定义 Web Service EJB
import javax.ejb.Stateless;import javax.jws.WebMethod;import javax.jws.WebParam;import javax.jws.WebService;import javax.jws.soap.SOAPBinding;// @WebService:标记这是Web Service服务,name是服务名,serviceName是对外暴露的服务名称@WebService(name = "ProductWebService", serviceName = "ProductService", targetNamespace = "http://example.com/")// @SOAPBinding:指定SOAP绑定风格,RPC风格适合简单参数,Document风格适合复杂对象@SOAPBinding(style = SOAPBinding.Style.RPC)@Stateless // 必须是无状态Bean,Web Service不支持有状态Bean(因为有状态依赖会话,跨语言调用无法维护会话)public class ProductWebServiceEJB {// @WebMethod:标记这是对外暴露的Web Service方法,exclude=true表示不暴露@WebMethod(operationName = "getProductPrice") // operationName是方法在SOAP中的名称// @WebParam:标记方法参数,name是参数在SOAP中的名称,避免默认名称(arg0、arg1)public Product getProductPrice(@WebParam(name = "productId") Long productId) {// 业务逻辑:查询商品价格(和远程EJB逻辑类似)Product product = new Product();product.setId(productId);product.setName("笔记本电脑");product.setPrice(new BigDecimal("5999"));return product;}// 不暴露的方法:加exclude=true@WebMethod(exclude = true)public void internalMethod() {// 内部方法,不对外提供Web Service调用}}

3.2.2、 部署后获取 WSDL 文档

EJB 部署到服务器(如 JBoss)后,容器会自动生成 WSDL 文档,访问地址通常是:

http://服务器IP:端口/EJB项目名/ProductService?wsdl

比如:http://192.168.1.100:8080/ProductEJB/ProductService?wsdl

访问这个地址能看到 XML 格式的 WSDL 文档,里面包含服务的所有信息 —— 第三方(比如.NET 团队)可以用这个 WSDL 生成调用代码。

3.2.3\ 客户端调用(以 Java 客户端为例)

Java 客户端可以用 JDK 自带的wsimport工具,根据 WSDL 生成调用代码:

  • 执行命令生成代码:wsimport -s src/main/java http://192.168.1.100:8080/ProductEJB/ProductService?wsdl
  • 生成的代码中,会有ProductService(服务类)和ProductWebService(接口类),直接调用:
public class WebServiceClient {public static void main(String[] args) {// 创建服务实例ProductService service = new ProductService();// 获取Web Service接口代理ProductWebService webService = service.getProductWebServicePort();// 调用Web Service方法(和调用本地方法一样,底层是SOAP协议)Product product = webService.getProductPrice(1L);System.out.println("商品价格:" + product.getPrice());}}

当年.NET 团队就是用这个 WSDL 生成了 C# 调用代码,轻松实现了跨语言调用 —— 不用我们写任何 SOAP 协议代码,全靠 EJB 容器自动处理,省心不少。

实战注意:Web Service 的 “局限性”

虽然 Web Service 跨语言方便,但当年也发现它的缺点:

  • 性能差:SOAP 用 XML 传输,数据量大,序列化 / 反序列化耗时,比 RMI-IIOP 还慢;
  • 只支持无状态 Bean:Web Service 是无状态的,有状态 Bean 依赖会话,无法暴露成 Web Service;
  • 现代替代多:现在跨语言通信更多用 RESTful API(JSON 格式,轻量快)或 gRPC(二进制协议,更高效),Web Service 只在老系统对接时用。

当年新项目已经很少用 Web Service 了,但维护老系统时,还是会遇到 EJB 暴露的 SOAP 服务 —— 理解它的原理,能更快排查对接问题。

最后 小结

总结下来就这么几点实在话:​

第一,本地与远程接口的选择,核心是 “看部署范围,优先保性能”。本地接口是 “同一 JVM 内的高效对话”,无网络开销、不用序列化,适合同一服务器内的组件调用(比如订单服务调用本地库存模块);远程接口是 “跨 JVM 的长途传输”,靠 RMI-IIOP 协议,有网络延迟和序列化开销,只在必须跨服务器时用(比如 A 机房订单服务调用 B 机房商品服务)。当年把本地调用错用成远程,10ms 的操作变成 500ms,整个下单流程变慢,这教训太深刻了。记住三个 “优先” 原则:优先用本地接口、跨服务器才用远程、远程传参要 “轻”(别传大对象),基本能避开 80% 的选择坑。​

第二,远程调用的核心是 “防坑 + 优化”,别让分布式变成 “性能杀手”。远程调用的底层是 “序列化 - 传输 - 执行 - 反序列化” 四步,坑也多在这四步里:没给传输对象加 Serializable,调用直接崩;传大对象、频繁调用,性能雪崩;没设超时,远程服务故障导致线程堆积。当年查远程调用超时问题,最后发现是没加序列化版本号;优化订单详情查询,把 3 次远程调用合并成 1 次、本地缓存常用数据,耗时从 1.5 秒降到 300ms—— 这些经验告诉我们:远程调用要 “精简数据、减少次数、控制超时”,才能既保证分布式能力,又不牺牲性能。另外,EJB 3.1 后的 “无接口视图” 虽然简洁,但大项目还是建议单独定义远程接口,可读性和协作性更强。​

第三,Web Service 是 “老系统跨语言的选择”,但要认清它的局限性。EJB 用 @WebService 注解能轻松把无状态 Bean 暴露成 SOAP 服务,自动生成 WSDL,当年对接.NET 系统时帮了大忙 —— 不用写 SOAP 协议代码,跨语言调用很方便。但 Web Service 性能差(XML 传输比 JSON 慢)、只支持无状态 Bean,现在新项目更多用 RESTful API(轻量快)或 gRPC(二进制协议),Web Service 只剩老系统维护时才会碰到。理解它的原理,主要是为了能快速排查老系统的跨语言对接问题,比如 WSDL 地址找不到、SOAP 消息格式错误,这些当年踩过的坑,现在维护时还能用上。​

最后说句实在的:EJB 的分布式通信设计,在当年确实解决了企业级应用的跨服务器、跨语言问题,但也带着时代的局限性 —— 性能开销大、配置相对繁琐。现在微服务架构下,更多用 Spring Cloud、Dubbo 这些轻量级框架,但 EJB 的 “按部署场景选接口”“远程调用优化” 思路,依然适用于现代分布式系统。理解 EJB 的分布式通信,不仅是看懂老项目,更是掌握 “分布式系统性能优化” 的底层逻辑,这才是最有价值的地方。未完待续..........

http://www.dtcms.com/a/576837.html

相关文章:

  • HarmonyOS实战项目:AI健康助手(影像识别与健康分析)
  • 利用 AWS Lambda 与 EventBridge 优化低频 Java 作业的云计算成本
  • 工业和信息化部网站备案管理系统公司网站维护怎么维护
  • 深入理解 Spring Boot 中的 Redis 缓存集成:从基础配置到高可用实践
  • 辽宁网站建站优化公司怎么在网上做装修网站
  • 界面控件Telerik UI for WPF 2025 Q3亮点 - 集成AI编码助手
  • 拦截adb install/uninstall安装 - 安装流程分析
  • 【小技巧】PyCharm建立项目,VScode+CodeX+WindowsPowerShell开发Python pyQT6
  • DevExpress WPF中文教程:Data Grid - 如何使用虚拟源?(五)
  • AI SQL助手本地搭建(附源码)
  • Zabbix企业级分布式监控系统(下)
  • 『Linux升级路』解析环境变量
  • 浏览器能正常访问URL获取JSON,但是pycharm里调不通
  • AI代码开发宝库系列:PDF文档解析MinerU
  • 校园招聘seo行业网
  • 开发网站的技术路线博达高校网站群建设教程
  • 物联网运维中基于联邦学习的跨设备隐私保护与协同优化技术
  • 物联网AI模组:连接与智能的融合
  • 【底层机制】ART虚拟机深度解析:Android运行时的架构革命
  • 嵌入式硬件:如何理解高频电子线路,从入门开始
  • 物联网赋能校园共享站:打造24小时一站式服务新体验!
  • 萤石开放平台申请物联网卡指南
  • 矩阵在密码学的应用——希尔密码详解
  • 20251106给荣品RD-RK3588-MID开发板跑Rockchip的原厂Android13系统时适配AP6275P模块的WIFI【使用荣品的DTS】
  • 学校网站代码模板成都的网站建设开发公司哪家好
  • 怎么制作自己的小网站低代码前端开发平台
  • Elastic Stack 或 ELK —— 日志管理与数据分析方案
  • 手机、平板、电脑如何投屏画面到电视?ToDesk远程控制TV版教程分享
  • UEC++UNiagaraFunctionLibrary源代码
  • 小杰-大模型(twelve)——大模型部署与应用——gradipo-实现UI界面