it编程 > 编程语言 > Java

SpringBoot集成OnlyOffice实现Word/Excel在线编辑功能

12人参与 2026-08-03 Java

01 引言

工作中我们经常会遇到多人同时编辑一个文件,尤其企业微信中经常使用。但是在项目中如果出现需要多人编辑文件的场景,那该如何操作呢?

这就是今天要分享的项目onlyoffice,可以帮我们实现office文件在线编辑的功能。本节使用springboot4.x集成作为案例。

02 简介

onlyoffice 文档(文档服务器)是一款开源办公套件,包含处理文档、电子表格、演示文稿、pdf 及 pdf 表单所需的全部工具。该套件支持所有主流办公文件格式(docx、odt、xlsx、ods、csv、pptx、odp 等),并支持实时协同编辑。

onlyoffice(文档服务)是一个独立的 web 服务,负责:

github上提供社区版和企业版的docker服务,我们下面讲义社区版为例。

github地址:https://github.com/onlyoffice/docker-documentserver

官网地址:https://www.onlyoffice.com/zh/office-suite

03 服务搭建

onlyoffice服务搭建直接使用docker部署即可。

sudo docker run -i -t -d -p 9090:80 \
    -v /app/onlyoffice/documentserver/logs:/var/log/onlyoffice  \
    -v /app/onlyoffice/documentserver/data:/var/www/onlyoffice/data  \
    -v /app/onlyoffice/documentserver/lib:/var/lib/onlyoffice \
    onlyoffice/documentserver

注意这里默认的包含jwt认证的。

可以通过命令找到jwt的秘钥。进入首页有很多命令提示。

04 springboot集成

4.1 maven依赖

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-webmvc</artifactid>
</dependency>
<!-- nimbus jose+jwt (onlyoffice 官方推荐的 jwt 库) -->
<dependency>
    <groupid>com.nimbusds</groupid>
    <artifactid>nimbus-jose-jwt</artifactid>
    <version>10.2</version>
</dependency>

如果onlyoffice不启用jwt认证,则不需要引入nimbus-jose-jwt.

4.2 核心常量

/**
 * onlyoffice 常量配置
 */
public final class onlyofficeproperties {
    private onlyofficeproperties() {}
    /** onlyoffice 文档服务地址 */
    public static final string doc_service_url = "http://10.100.xx.xx:9090";
    /** 本项目对外可访问地址(必须是 onlyoffice 能访问到的 ip) */
    public static final string server_url = "http://10.50.xx.xx:8080";
    /** 文件存储目录 */
    public static final string storage_path = "./storage";
    /** jwt 密钥(为空则不启用签名) */
    public static final string jwt_secret = "inyyl*******xtddugbfyqi";
}

4.3 核心代码

上传和列表展示

@getmapping("/files")
public responseentity<list<string>> listfiles() throws ioexception {
    try (stream<path> stream = files.list(storagedir)) {
        return responseentity.ok(stream
                .filter(files::isregularfile)
                .map(p -> p.getfilename().tostring())
                .tolist());
    }
}
@postmapping("/files")
public responseentity<string> upload(@requestparam("file") multipartfile file) throws ioexception {
    files.copy(file.getinputstream(),
            storagedir.resolve(file.getoriginalfilename()),
            standardcopyoption.replace_existing);
    return responseentity.ok("uploaded: " + file.getoriginalfilename());
}

onlyoffice 通过下载令牌加载文档

@getmapping("/files/download/{token}")
public responseentity<resource> downloadbytoken(@pathvariable string token) throws ioexception {
    string name = downloadtokens.getordefault(token, token);
    path file = storagedir.resolve(name).normalize();
    resource resource = new urlresource(file.touri());
    if (!resource.exists() || !resource.isreadable())
        return responseentity.notfound().build();
    return responseentity.ok()
            .contenttype(mediatype.parsemediatype(contenttype(name)))
            .body(resource);
}

编辑

@getmapping("/settings")
public responseentity<map<string, string>> settings() {
    return responseentity.ok(map.of("docserviceurl", onlyofficeproperties.doc_service_url));
}
@getmapping("/config")
public responseentity<map<string, object>> config(
        @requestparam("name") string name,
        @requestparam(value = "user", defaultvalue = "user") string user) throws exception {
    string token = uuid.randomuuid().tostring().replace("-", "");
    downloadtokens.put(token, name);
    map<string, object> document = new linkedhashmap<>();
    document.put("filetype", filetype(name));
    document.put("key", token);
    document.put("title", name);
    document.put("url", onlyofficeproperties.server_url + "/api/files/download/" + token);
    map<string, object> editorconfig = new linkedhashmap<>();
    editorconfig.put("callbackurl", onlyofficeproperties.server_url + "/api/callback");
    editorconfig.put("lang", "zh-cn");
    editorconfig.put("mode", "edit");
    editorconfig.put("user", map.of("id", user, "name", user));
    map<string, object> cfg = new linkedhashmap<>();
    cfg.put("document", document);
    cfg.put("documenttype", doctype(name));
    cfg.put("editorconfig", editorconfig);
    cfg.put("height", "100%");
    cfg.put("width", "100%");
    if (isjwtenabled()) {
        cfg.put("token", sign(objectmapper.writevalueasstring(cfg)));
    }
    return responseentity.ok(cfg);
}

回调

@postmapping("/callback")
public responseentity<map<string, integer>> callback(@requestbody map<string, object> body) {
    try {
        handlecallback(body);
    } catch (exception e) {
        e.printstacktrace();
    }
    return responseentity.ok(map.of("error", 0));
}
@suppresswarnings("unchecked")
private void handlecallback(map<string, object> body) throws ioexception, interruptedexception {
    int status = ((number) body.get("status")).intvalue();
    if (status != 2 && status != 6) return;
    string downloadurl = (string) body.get("url");
    if (downloadurl == null || downloadurl.isempty()) return;
    string filename = downloadtokens.getordefault(
            (string) body.get("key"), (string) body.get("key"));
    httprequest request = httprequest.newbuilder()
            .uri(uri.create(downloadurl)).get().build();
    httpresponse<path> response = httpclient.send(request,
            httpresponse.bodyhandlers.offile(storagedir.resolve(filename + ".tmp")));
    if (response.statuscode() == 200) {
        files.move(storagedir.resolve(filename + ".tmp"),
                storagedir.resolve(filename), standardcopyoption.replace_existing);
    }
}

其他辅助方法

private string contenttype(string name) {
    return switch (ext(name)) {
        case "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
        case "xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        case "pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation";
        case "pdf" -> "application/pdf";
        default -> "application/octet-stream";
    };
}
private string filetype(string name) {
    return switch (ext(name)) {
        case "xlsx", "xls" -> "xlsx";
        case "pptx", "ppt" -> "pptx";
        case "pdf" -> "pdf";
        case "txt" -> "txt";
        case "csv" -> "csv";
        default -> "docx";
    };
}
private string doctype(string name) {
    return switch (ext(name)) {
        case "xlsx", "xls", "csv" -> "cell";
        case "pptx", "ppt" -> "slide";
        default -> "word";
    };
}
private string ext(string name) {
    int dot = name.lastindexof('.');
    return dot > 0 ? name.substring(dot + 1).tolowercase() : "docx";
}
private boolean isjwtenabled() {
    return onlyofficeproperties.jwt_secret != null && !onlyofficeproperties.jwt_secret.isblank();
}
private string sign(string configjson) throws exception {
    jwtclaimsset claims = jwtclaimsset.parse(configjson);
    signedjwt signed = new signedjwt(
            new jwsheader.builder(jwsalgorithm.hs256).type(joseobjecttype.jwt).build(), claims);
    signed.sign(new macsigner(onlyofficeproperties.jwt_secret.getbytes(standardcharsets.utf_8)));
    return signed.serialize();
}

4.4 页面

文件管理页 index.html

<!doctype html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>onlyoffice 在线编辑</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, sans-serif; background: #f5f5f5; padding: 24px; }
        h1 { font-size: 20px; margin-bottom: 20px; color: #333; }
        .upload-box { display: flex; gap: 10px; margin-bottom: 20px; }
        input[type="file"] { padding: 8px; border: 2px dashed #ccc; border-radius: 6px; flex: 1; }
        button { padding: 8px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
        .btn-upload { background: #667eea; color: #fff; }
        .btn-edit { background: #52c41a; color: #fff; padding: 4px 14px; font-size: 13px; }
        .btn-del { background: #ff4d4f; color: #fff; padding: 4px 14px; font-size: 13px; }
        .file-list { background: #fff; border-radius: 8px; padding: 16px; }
        .file-item { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
        .file-item:last-child { border-bottom: none; }
        .file-name { flex: 1; font-size: 14px; }
        .file-actions { display: flex; gap: 6px; }
        .empty { text-align: center; color: #999; padding: 40px; font-size: 14px; }
    </style>
</head>
<body>
<h1>onlyoffice 在线编辑</h1>
<div class="upload-box">
    <input type="file" id="fileinput" accept=".docx,.xlsx,.pptx,.pdf,.txt,.csv">
    <button class="btn-upload" onclick="upload()">上传</button>
</div>
<div class="file-list" id="filelist"></div>
<script>
    const api = '/api/files';
    async function loadfiles() {
        const res = await fetch(api).then(r => r.json());
        const box = document.getelementbyid('filelist');
        if (!res.length) { box.innerhtml = '<div class="empty">暂无文档,请先上传</div>'; return; }
        box.innerhtml = res.map(f => `
            <div class="file-item">
                <span class="file-name">${f}</span>
                <div class="file-actions">
                    <button class="btn-edit" onclick="openeditor('${f}')">编辑</button>
                    <button class="btn-del" onclick="deletefile('${f}')">删除</button>
                </div>
            </div>`).join('');
    }
    function openeditor(name) {
        window.open('/editor.html?name=' + encodeuricomponent(name), '_blank');
    }
    async function upload() {
        const file = document.getelementbyid('fileinput').files[0];
        if (!file) return;
        const fd = new formdata(); fd.append('file', file);
        await fetch(api, { method: 'post', body: fd });
        document.getelementbyid('fileinput').value = '';
        loadfiles();
    }
    async function deletefile(name) {
        if (!confirm('确定删除 ' + name + ' ?')) return;
        await fetch(api + '/' + encodeuricomponent(name), { method: 'delete' });
        loadfiles();
    }
    loadfiles();
</script>
</body>
</html>

编辑页面

<!doctype html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文档编辑 - onlyoffice</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        html, body { height: 100%; overflow: hidden; }
        #editor { width: 100%; height: 100%; }
        #error { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 16px; color: #ff4d4f; font-family: -apple-system, sans-serif; }
    </style>
</head>
<body>
<div id="editor"></div>
<script>
    const name = new urlsearchparams(location.search).get("name");
    if (!name) {
        document.getelementbyid('editor').outerhtml = '<div id="error">缺少文档名称参数</div>';
        throw new error('missing name');
    }
    document.title = name;
    (async function () {
        const { docserviceurl } = await fetch('/api/settings').then(r => r.json());
        const script = document.createelement('script');
        script.src = docserviceurl + '/web-apps/apps/api/documents/api.js';
        script.onload = async () => {
            const config = await fetch('/api/config?name=' + encodeuricomponent(name)).then(r => r.json());
            new docsapi.doceditor("editor", config);
        };
        document.head.appendchild(script);
    })();
</script>
</body>
</html>

05 测试

进入首页,页面管理。我们需要上传文件。

我们是分别上传excel和word文档,文件上传之后就会展示在下面的列表里,点击编辑就可以实现编辑了。

编辑完成之后,再次打开,保留上次添加的内容。

以上就是springboot集成onlyoffice实现word/excel在线编辑功能的详细内容,更多关于springboot word/excel在线编辑的资料请关注代码网其它相关文章!

(0)

您想发表意见!!点此发布评论

推荐阅读

Spring定时任务@Scheduled cron原理与实现详解

08-03

Spring Boot中拦截器与过滤器的使用场景和区别详解

08-03

SpringBoot拦截器怎么用?从入门到多拦截器链式调用详解

08-03

Java集合框架常用API有哪些?List、Set、Map方法大全

08-03

SpringBoot接口参数接收指南:从URL到请求体,4种方式一次掌握

08-03

java运行排错,新码旧jar问题及解决

08-03

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论