做呼叫中心、电话通知或语音机器人时,经常会遇到这些需求:
- 电话接通、挂断后,通知业务系统更新状态。
- 在 Java 中查询通话、发起呼叫、结束通话。
- 接听来电、播放提示音,再根据用户按键执行不同操作。
esl-client 就是 Java 程序与 FreeSWITCH 之间的连接工具。2.0.0 已发布到 Maven 仓库,普通 Java 项目和 Spring Boot 项目都可以接入。
下面直接看怎么用。
一、先选对接入方式
| 你的需求 | 使用方式 |
|---|---|
| 监听接通、挂断事件,查询状态、发送命令 | Inbound:Java 主动连接 FreeSWITCH |
| 把某通电话交给 Java,执行接听、放音、收号等流程 | Outbound:FreeSWITCH 将呼叫连接到 Java 服务 |
这里的 Inbound、Outbound 指的是 ESL 连接方向,不是电话的呼入、呼出方向。
运行环境:Java 17+ 。本文的 Spring Boot Starter 示例面向 Spring Boot 4.x。
二、Maven 项目怎么引入
普通 Java 项目,在 pom.xml 的 <dependencies> 中添加:
<dependency>
<groupId>io.github.xxd763795151</groupId>
<artifactId>esl-client</artifactId>
<version>2.0.0</version>
</dependency>
Spring Boot 4.x 项目添加 Starter 即可,它已经包含核心库:
<dependency>
<groupId>io.github.xxd763795151</groupId>
<artifactId>esl-client-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
2.0.0 与 1.x 的 API 不兼容,老项目升级时需要调整调用代码。
三、连接前,先准备 FreeSWITCH
使用 Inbound 时,需要启用 FreeSWITCH 的 mod_event_socket,并确认三个信息:
- FreeSWITCH 的 IP。
- ESL 端口,通常为
8021。 - ESL 密码。
下面是 Java 与 FreeSWITCH 在同一台机器上测试时的配置示例,文件为 FreeSWITCH 配置目录下的 autoload_configs/event_socket.conf.xml:
<configuration name="event_socket.conf" description="Socket Client">
<settings>
<param name="listen-ip" value="127.0.0.1"/>
<param name="listen-port" value="8021"/>
<param name="password" value="your-esl-password"/>
</settings>
</configuration>
如果部署在不同机器上,需要把监听地址改成 FreeSWITCH 可访问的内网地址,并让 ACL 和防火墙允许 Java 服务访问。相关配置可参考 FreeSWITCH 官方文档。
下面示例中的地址、密码,都要替换成自己的实际配置。
四、非 Spring Boot 项目:连接、查状态、监听通话
适合普通 Java 程序、后台服务,以及自己管理客户端生命周期的项目。
新建 EslDemo.java:
import io.github.xxd763795151.esl.event.EventSelector;
import io.github.xxd763795151.esl.inbound.InboundClient;
public class EslDemo {
public static void main(String[] args) throws Exception {
try (InboundClient client = InboundClient.builder()
.address("127.0.0.1", 8021)
.password("your-esl-password")
.events("CHANNEL_ANSWER", "CHANNEL_HANGUP_COMPLETE")
.build()) {
client.addEventListener(
EventSelector.named(
"CHANNEL_ANSWER",
"CHANNEL_HANGUP_COMPLETE"),
event -> System.out.printf(
"事件=%s,通话ID=%s,主叫=%s%n",
event.name(),
event.uniqueId(),
event.header("Caller-Caller-ID-Number"))
);
client.connect().join();
String status = client.api("status").join().body();
System.out.println(status);
// 演示监听 60 秒,期间可以拨打一通测试电话。
Thread.sleep(60_000);
}
}
}
运行后,先看到 FreeSWITCH 状态;测试电话接通、挂断时,会打印对应事件。
这里的 .join() 表示等待操作完成,方便按顺序编写代码。
实际服务中,创建并复用一个长连接客户端,在应用退出时关闭,不用每发一条命令就重新连接。上面的 60 秒等待仅用于演示。
连接成功后,还能做什么?
以下代码放在客户端连接成功之后,根据业务需要调用:
// 查询当前通话数量
String count = client.api("show", "calls count").join().body();
// 呼叫已配置并注册的分机 1000,接听后保持通话
String result = client.bgapi(
"originate", "user/1000 &park").join().body();
System.out.println(result);
已经拿到某通电话的 UUID,也可以操作这通电话:
// uuid 来自通话事件中的 event.uniqueId()
client.channel(uuid).playback("/opt/voice/welcome.wav").join();
// 结束这通电话
client.channel(uuid).hangup().join();
语音文件路径指向 FreeSWITCH 所在机器上的文件。示例中的分机、文件路径,都需要提前准备。
五、Spring Boot 项目:写配置、注入使用
适合已有 Spring Boot 业务系统,希望将通话事件接入订单、坐席、客户管理等业务的场景。
引入 Starter 后,只需要两步。
第一步:配置 application.yml
esl:
inbound:
enabled: true
addresses:
- 127.0.0.1:8021
password: your-esl-password
events:
- CHANNEL_ANSWER
- CHANNEL_HANGUP_COMPLETE
connect-timeout: 10s
command-timeout: 30s
reconnect:
strategy: exponential
initial-delay: 1s
max-delay: 30s
第二步:编写业务组件
把下面的类放在 Spring Boot 启动类所在包或其子包中:
import io.github.xxd763795151.esl.event.EslEvent;
import io.github.xxd763795151.esl.inbound.InboundClient;
import io.github.xxd763795151.esl.spring.EslEventListener;
import org.springframework.stereotype.Service;
@Service
public class CallService {
private final InboundClient client;
public CallService(InboundClient client) {
this.client = client;
}
public String queryStatus() {
return client.api("status").join().body();
}
@EslEventListener("CHANNEL_ANSWER")
public void onAnswer(EslEvent event) {
System.out.println("电话已接通:" + event.uniqueId());
// 在这里更新业务状态
}
@EslEventListener("CHANNEL_HANGUP_COMPLETE")
public void onHangup(EslEvent event) {
System.out.printf("电话已挂断:%s,原因:%s%n",
event.uniqueId(),
event.header("Hangup-Cause"));
// 在这里记录通话结果
}
}
启动应用后,Starter 会管理连接和关闭流程;其他业务组件注入 CallService,即可调用 queryStatus()。
注意:监听注解与事件订阅要配套。 写了 @EslEventListener,还要在 YAML 的 events 中订阅对应事件。
六、想用 Java 编写电话流程?使用 Outbound
例如:用户拨打 9999,由 Java 接听、播放欢迎语,然后挂断。
在 Spring Boot 项目中,添加配置:
esl:
outbound:
port: 8084
shutdown-timeout: 30s
spring:
lifecycle:
timeout-per-shutdown-phase: 40s
如果已有 esl.inbound 配置,把 outbound 合并到同一个 esl 节点下。只做 Outbound 时,无须开启 Inbound。
然后添加呼叫处理器:
import io.github.xxd763795151.esl.outbound.OutboundSessionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class IvrConfig {
@Bean
public OutboundSessionHandler ivrHandler() {
return session -> {
var call = session.channel();
call.answer().join();
call.playback("/opt/voice/welcome.wav").join();
call.hangup().join();
};
}
}
存在这个 Bean 时,Starter 会启动 Outbound 服务。
再在 FreeSWITCH 对应的拨号方案中加入:
<extension name="java-ivr">
<condition field="destination_number" expression="^9999$">
<action application="socket"
data="127.0.0.1:8084 async full"/>
</condition>
</extension>
这里的 127.0.0.1:8084 是 Java 服务的地址。如果两者不在同一台机器上,要改为 FreeSWITCH 能访问到的 Java 服务 IP。
加载拨号方案后,从匹配该拨号方案的分机拨打 9999,即可测试。
需要收集按键时,可以在接听后加入:
// 需要导入 io.github.xxd763795151.esl.app.PlayAndGetDigits
String digit = call.playAndGetDigits(
PlayAndGetDigits.builder("/opt/voice/menu.wav")
.digits(1, 1)
.regex("[1-2]")
.build()
).join();
if ("1".equals(digit)) {
call.playback("/opt/voice/business-hours.wav").join();
} else if ("2".equals(digit)) {
call.bridge("user/1000").join();
}
提前准备提示音和分机,就可以扩展成“按 1 查询营业时间,按 2 转人工”的语音菜单。
非 Spring Boot 项目也能使用 Outbound,手动启动服务即可:
import io.github.xxd763795151.esl.outbound.OutboundServer;
import java.time.Duration;
public class IvrDemo {
public static void main(String[] args) throws Exception {
OutboundServer server = OutboundServer.builder()
.port(8084)
.handler(session -> {
var call = session.channel();
call.answer().join();
call.playback("/opt/voice/welcome.wav").join();
call.hangup().join();
})
.build();
Runtime.getRuntime().addShutdownHook(new Thread(
() -> server.shutdownGracefully(
Duration.ofSeconds(30)).join()));
server.start().join();
Thread.currentThread().join();
}
}
FreeSWITCH 侧使用同样的拨号方案配置。
七、几个常用配置,按场景选择
以下属性位于 esl.inbound 下:
| 配置 | 用途 |
|---|---|
startup-wait: 10s | 默认启动等待连接的时间;超时后允许应用继续启动 |
fail-fast: true | 首次连接失败时,让应用启动失败 |
command-timeout: 30s | 普通命令的回复超时时间 |
job-timeout: 5m | bgapi 后台任务的等待超时时间 |
disconnected-command-policy: fail-fast | 断线时,新命令直接失败,默认行为 |
disconnected-command-policy: queue-until-ready | 断线时暂存新命令,等待恢复;仍受队列容量和超时限制 |
如果业务要求 FreeSWITCH 可用后才能提供服务,可以设置 fail-fast: true。
如果还有其他独立业务,希望 FreeSWITCH 暂时不可用时应用仍能启动,可以保留默认值,并在调用处处理连接不可用的情况。
需要同时连接多台 FreeSWITCH?
使用具名配置,每台一个客户端:
esl:
inbounds:
fs1:
addresses: ["10.0.1.10:8021"]
password: password-for-fs1
events: [CHANNEL_ANSWER, CHANNEL_HANGUP_COMPLETE]
fs2:
addresses: ["10.0.2.10:8021"]
password: password-for-fs2
events: [CHANNEL_ANSWER, CHANNEL_HANGUP_COMPLETE]
业务类通过构造器指定要使用的实例:
public CallService(
@Qualifier("fs1") InboundClient client) {
this.client = client;
}
@Qualifier 的包名为 org.springframework.beans.factory.annotation.Qualifier。
只监听某台服务器时:
@EslEventListener(value = "CHANNEL_ANSWER", client = "fs1")
public void onAnswer(EslEvent event) {
System.out.println("fs1 电话接通:" + event.uniqueId());
}
同一个 addresses 中填写多个地址,用于重连时切换;同时连接多台服务器,要用 esl.inbounds。