在日常开发中,你是否遇到过这样的需求:用户输入一段文本,系统自动转成语音文件供下载?比如智能客服的语音通知、教育平台的有声讲解、政务系统的语音播报等。—— 基于 Jacob 框架调用 Windows 系统语音接口,结合 Spring Boot 快速开发文本转语音(TTS)+ 文件下载接口。
一、先搞懂:为什么这么选技术?
做开发先选对工具,这个需求的核心是 “文本转语音” 和 “文件下载”,我们的技术栈选型都是围绕这两个点来的,理由很实在:

  1. 核心技术栈及作用
    •Spring Boot:不用多说,快速搭 Web 接口的神器,处理 HTTP 请求、参数校验、依赖注入一步到位,省去传统 SSM 的繁琐配置;
    •Jacob:Java COM Bridge 的缩写,能让 Java 代码调用 Windows 系统的 COM 组件 —— 这里主要用它调用 Windows 自带的 SAPI(Speech API),不用额外部署语音服务,轻量又免费;
    •Java IO 流:生成的语音文件要通过流写入 HTTP 响应,实现浏览器下载,这部分是 Java 基础但关键的操作。
  2. 环境准备(避坑第一步)
    这个方案有个前提:只能在 Windows 系统运行(因为 Jacob 依赖 Windows COM 组件),如果是 Linux/macOS,后面会给替代方案。
首先加 Jacob 依赖(pom.xml):

<dependency>
    <groupId>com.jacob</groupId>
    <artifactId>jacob</artifactId>
    <version>1.19</version> <!-- 稳定版本,建议不随意升级 -->
</dependency>

重点避坑:光加依赖不够,还要下载对应系统位数的jacob-1.19-x64.dll(64 位系统)或jacob-1.19-x86.dll(32 位),放到 JRE 的bin目录下(比如C:\Program Files\Java\jre1.8.0_301\bin),否则会报 “找不到 dll” 的错误!
二、核心功能实现:两步搞定需求
整个流程很清晰:第一步,写个工具类把文本转成 MP3 文件;第二步,写个接口让用户调用,生成文件后直接下载。咱们一步步来,代码都标了详细注释,放心抄!
第一步:文本转语音工具类(SpeakUtils)
这个工具类是核心,负责调用 Windows 语音接口生成 MP3 文件。关键点有三个:初始化 COM 组件、设置语音参数(音量 / 语速)、释放资源避免内存泄漏。

import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.ComThread;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
@Slf4j
@Component // 交给Spring管理,方便注入
public class SpeakUtils {
    // 语音文件存储根路径,建议配置在application.yml里,方便修改
    @Value("${file.upload.path:C:/temp/speak-files}") 
    private String uploadPath;
    /**
     * 文本转语音,生成MP3文件
     * @param speakText 要转换的文本(比如“你好,这是测试语音”)
     * @return 生成的MP3文件路径(如C:/temp/speak-files/20240520/123456.mp3)
     */
    public String textToSpeech1(String speakText) {
        // 1. 初始化COM线程(Jacob必须要,STA模式适配Windows COM组件)
        ComThread.InitSTA();
        String filePath = "";
        // 2. 按日期分目录存储,避免单目录文件太多(比如20240520这天的文件放一个文件夹)
        String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date());
        File dir = new File(uploadPath + File.separator + "speak" + File.separator + dateDir);
        if (!dir.exists()) {
            dir.mkdirs(); // 不存在就创建目录
        }
        // 3. 生成唯一文件名(用UUID避免重复,比如a1b2c3d4.mp3)
        String fileName = UUID.randomUUID().toString().replace("-", "") + ".mp3";
        filePath = dir.getPath() + File.separator + fileName;
        File targetFile = new File(filePath);
        // 4. 如果文件已经存在,直接返回路径(避免重复生成,优化性能)
        if (targetFile.exists()) {
            log.info("语音文件已存在,直接返回:{}", filePath);
            return filePath;
        }
        // 5. 声明COM组件对象(要记得最后释放!)
        ActiveXComponent axVoice = null;    // 语音引擎组件
        Dispatch spVoice = null;            // 语音操作对象
        ActiveXComponent axFileStream = null;// 文件流组件
        Dispatch spFileStream = null;        // 文件流操作对象
        ActiveXComponent axAudioFormat = null;// 音频格式组件
        Dispatch spAudioFormat = null;        // 音频格式操作对象
        try {
            // 初始化语音引擎:Sapi.SpVoice是Windows自带的语音组件
            axVoice = new ActiveXComponent("Sapi.SpVoice");
            spVoice = axVoice.getObject();
            // 设置音量(0-100,100最大)
            axVoice.setProperty("Volume", new Variant(100));
            // 设置语速(-10到10,负数变慢,正数变快,-2是比较自然的语速)
            axVoice.setProperty("Rate", new Variant(-2));
            // 初始化文件流:用于把语音写入MP3文件
            axFileStream = new ActiveXComponent("Sapi.SpFileStream");
            spFileStream = axFileStream.getObject();
            // 初始化音频格式:设置为MP3格式(Type=22是MP3,1是WAV,别改错了!)
            axAudioFormat = new ActiveXComponent("Sapi.SpAudioFormat");
            spAudioFormat = axAudioFormat.getObject();
            Dispatch.put(spAudioFormat, "Type", new Variant(22));
            // 把音频格式绑定到文件流
            Dispatch.putRef(spFileStream, "Format", spAudioFormat);
            // 打开文件流:3表示“可写模式”,true表示“如果文件不存在就创建”
            Dispatch.call(spFileStream, "Open", new Variant(filePath), new Variant(3), new Variant(true));
            // 把语音输出方向绑定到文件流(原本是输出到扬声器,现在改到文件)
            Dispatch.putRef(spVoice, "AudioOutputStream", spFileStream);
            // 核心操作:执行文本朗读(其实是把语音写入文件)
            Dispatch.call(spVoice, "Speak", new Variant(speakText));
            // 收尾:关闭文件流,解绑输出
            Dispatch.call(spFileStream, "Close");
            Dispatch.putRef(spVoice, "AudioOutputStream", null);
            log.info("语音文件生成成功:{}", filePath);
        } catch (Exception e) {
            log.error("生成语音失败!文本:{}", speakText, e);
            // 失败时删除空文件,避免垃圾文件
            if (targetFile.exists() && targetFile.length() == 0) {
                targetFile.delete();
            }
        } finally {
            // 关键:按创建顺序反向释放资源,避免内存泄漏!
            if (spAudioFormat != null) spAudioFormat.safeRelease();
            if (axAudioFormat != null) axAudioFormat.safeRelease();
            if (spFileStream != null) spFileStream.safeRelease();
            if (axFileStream != null) axFileStream.safeRelease();
            if (spVoice != null) spVoice.safeRelease();
            if (axVoice != null) axVoice.safeRelease();
            // 清理COM线程(必须加,否则会有资源残留)
            try {
                ComThread.Release();
                ComThread.InitSTA(false); // 清理旧上下文
            } catch (Exception e) {
                log.error("清理COM线程失败", e);
            }
        }
        return filePath;
    }
}

第二步:写下载接口(SpeakDownloadController)
接口很简单:接收用户传的文本参数,调用上面的工具类生成 MP3,然后通过 IO 流把文件写给浏览器,触发下载。还要注意文本预处理(过滤特殊字符)和中文文件名乱码问题。

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
@Slf4j
@RestController
public class SpeakDownloadController {
    // 注入语音工具类
    @Autowired
    private SpeakUtils speakUtils;
    // 要过滤的特殊字符(比如标点、空格,避免影响语音生成),建议配置在application.yml
    private String filterChars = ",。!?;:\"'()【】、, ";
    /**
     * 文本转语音并下载接口
     * 调用示例:http://localhost:8080/downloadSpeak?speakText=你好,这是测试语音
     * @param speakText 要转换的文本(必填)
     * @param response 用于设置下载响应头
     */
    @GetMapping("/downloadSpeak")
    public void downloadSpeak(@RequestParam(name = "speakText", required = true) String speakText,
                              HttpServletResponse response) {
        // 1. 文本预处理:过滤特殊字符
        if (StringUtils.isNotBlank(speakText)) {
            String[] charsToFilter = filterChars.split(",");
            for (String c : charsToFilter) {
                speakText = speakText.replace(c, "");
            }
            speakText = speakText.replace(" ", ""); // 额外过滤空格
            log.info("预处理后文本:{}", speakText);
        } else {
            log.error("文本为空,无法生成语音");
            return;
        }
        // 2. 生成语音文件
        String filePath = speakUtils.textToSpeech1(speakText);
        File audioFile = new File(filePath);
        if (!audioFile.exists() || audioFile.length() == 0) {
            log.error("语音文件不存在或为空:{}", filePath);
            return;
        }
        // 3. 用IO流把文件写给浏览器,实现下载
        BufferedInputStream bis = null;
        OutputStream os = null;
        try {
            // 设置响应头:强制下载,避免浏览器预览
            response.setContentType("application/force-download");
            // 处理中文文件名乱码(URLEncoder编码)
            String fileName = URLEncoder.encode(speakText + ".mp3", "UTF-8");
            response.addHeader("Content-Disposition", "attachment;fileName=" + fileName);
            // 设置文件大小(可选,但能让浏览器显示下载进度)
            response.setContentLength((int) audioFile.length());
            // 读取文件并写入响应流
            bis = new BufferedInputStream(new FileInputStream(audioFile));
            os = response.getOutputStream();
            byte[] buffer = new byte[1024]; // 1KB缓冲
            int len;
            while ((len = bis.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            // 刷新流,确保数据全部写出
            response.flushBuffer();
            log.info("语音文件下载成功:{}", fileName);
        } catch (Exception e) {
            log.error("下载语音文件失败", e);
        } finally {
            // 关闭流(必须加,避免资源泄漏)
            try {
                if (bis != null) bis.close();
                if (os != null) os.close();
            } catch (Exception e) {
                log.error("关闭流失败", e);
            }
        }
    }
}

三、这些坑我替你踩过了
写代码时遇到几个坑,这里总结出来,你照着避就行:

  1. Jacob 组件的 3 个关键注意点
    •COM 线程必须初始化和释放:ComThread.InitSTA()和ComThread.Release()缺一不可,否则会导致内存泄漏,Windows 任务管理器里的javaw.exe进程会越来越多;
    •资源释放顺序要对:按 “音频格式→文件流→语音引擎” 的反向顺序释放,就像代码里那样,不然可能释放不干净;
    •音频格式 Type 别乱改:要生成 MP3 就设Type=22,生成 WAV 设Type=1,其他值可能导致文件无法播放。
  2. 中文乱码怎么解决?
    •文件名乱码:用URLEncoder.encode(fileName, “UTF-8”)编码,兼容所有浏览器;
    •文本内容乱码:确保接口接收参数时用 UTF-8 编码(Spring Boot 默认是 UTF-8,不用额外配置)。
  3. 文件下载的响应头怎么设?
    •Content-Type: application/force-download:告诉浏览器这是要下载的文件,不是预览的;
    •Content-Disposition: attachment;fileName=xxx.mp3:指定下载后的文件名;
    •Content-Length:可选,但加上能让浏览器显示下载进度,体验更好。
    四、常见问题解决方案(收藏这部分!)
    遇到问题别慌,先看这里,90% 的问题都能解决:

问题现象 可能原因 解决方案
启动报错 “UnsatisfiedLinkError: no jacob in java.library.path” 没放 jacob.dll,或放的路径不对 把对应位数的 dll 放到 JRE 的 bin 目录,或用 System.setProperty (“jacob.dll.path”, “dll 路径”) 指定
生成的 MP3 文件为空 1. 文本为空;2. COM 组件没释放;3. 音频格式 Type 错了 1. 加文本非空校验;2. 检查资源释放顺序;3. 确认 Type=22
下载的文件名是乱码 没对文件名编码 用 URLEncoder.encode (fileName, “UTF-8”) 处理
语音语速太快 / 太慢 语速参数设错了 调整 axVoice.setProperty (“Rate”, new Variant (-2)),范围 - 10 到 10
五、扩展优化:让功能更实用
如果要用到生产环境,这些优化点可以加上:

  1. 跨平台适配(Linux/macOS 用户看这里)
    Jacob 只支持 Windows,要跨平台的话,换成第三方云服务的语音合成 API,比如:
    •百度智能云语音合成:免费额度足够测试,支持多语种、多音色;
    •阿里云语音合成:稳定性好,适合生产环境;
    •腾讯云语音合成:接口简单,文档清晰。
  2. 性能优化
    •加缓存:相同的文本不用重复生成,用 Redis 缓存 “文本→文件路径” 的映射,过期时间设 1 天;
    •异步生成:高并发场景下,用 Spring 的 @Async 注解异步生成语音文件,避免阻塞 HTTP 请求;
    •文件清理:用定时任务(比如 Quartz)清理 3 天前的旧文件,避免磁盘占满。
  3. 功能扩展
    •自定义语速 / 音量:接口加rate和volume参数,让用户自己调;
    •支持多格式:除了 MP3,还支持生成 WAV(改音频格式 Type=1);
    •文本长度限制:防止用户传超长文本,加个限制(比如最多 500 字),超长就分段生成。
    六、总结
    今天这个方案,适合 Windows 环境下的轻量级文本转语音需求,不用部署额外服务,成本低、上手快。核心是掌握 Jacob 调用 COM 组件的方法,以及 IO 流下载文件的逻辑。
    如果你的项目要跨平台,或者需要更丰富的语音效果(比如情感语音、方言),建议用云服务 API;如果只是内部系统用,这个方案完全够用。
    最后,代码已经给全了,你只要替换一下文件存储路径,放好 jacob.dll,就能跑起来
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐