Integrate OnlyOffice with Spring Boot in 5 Minutes for Online Word/Excel Editing
This guide shows how enterprise developers can quickly add full‑featured online Word, Excel and PPT editing to Spring Boot 4.x applications by deploying OnlyOffice via Docker, configuring JWT authentication, and implementing backend endpoints for file storage, editor configuration, and save callbacks.
Why Online Document Editing Is Needed
Enterprise developers often need to let users preview and edit Office documents directly in OA systems, knowledge bases, project management tools, or HR portals. Word, Excel and PPT online preview/editing has become a standard capability for mid‑to‑back‑office applications.
How OnlyOffice and Spring Boot Work Together
Core Roles
OnlyOffice Document Server – the core engine that parses, renders, calculates edits and synchronises collaborative changes.
Spring Boot Business Backend – provides file storage, permission checks, business logic and receives callbacks. It does not parse documents itself.
Frontend Page – loads the OnlyOffice SDK and renders the editor UI for users.
Interaction Flow
User clicks “Open Document” on the front end, which requests editor configuration from the backend.
Spring Boot generates a configuration containing the document URL, save‑callback URL, permissions and a JWT signature, then returns it to the front end.
The front end initialises the OnlyOffice editor with the configuration and asks the document server to load the file.
OnlyOffice Document Server downloads the file from the Spring Boot backend, parses and renders it, and streams the result back to the front end.
During editing, every operation is synchronised in real time to the document server.
When the user saves or closes the document, the document server pushes the final file to the backend via the callback URL.
The backend overwrites the original file, completing the full cycle.
Core design idea: Let professionals handle the complex document processing. All heavy‑lifting is delegated to OnlyOffice, while the business system only concerns itself with storage and logic.
Environment Setup: Deploy OnlyOffice with Docker
One‑Click Command
docker run -d \
--name onlyoffice-documentserver \
-p 8089:80 \
-e JWT_ENABLED=true \
-e JWT_SECRET=onlyoffice_secret_2026 \
-e JWT_HEADER=Authorization \
-v /data/onlyoffice/data:/var/www/onlyoffice/Data \
-v /data/onlyoffice/logs:/var/log/onlyoffice \
-v /data/onlyoffice/fonts:/usr/share/fonts/trltetype/custom \
--restart=always \
onlyoffice/documentserver:latestKey Parameters Explained
-p 8089:80– maps host port 8089 to container port 80; can be customised. JWT_ENABLED=true – enables JWT authentication to prevent illegal calls. JWT_SECRET=onlyoffice_secret_2026 – secret key; must match the backend configuration. /var/www/onlyoffice/Data – persistent data directory to avoid data loss on container restart. /var/log/onlyoffice – log directory for troubleshooting. /usr/share/fonts/trltetype/custom – mount custom Chinese fonts to fix garbled characters.
Spring Boot 4.x Backend Implementation
Maven Core Dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>springboot4-onlyoffice</artifactId>
<version>1.0.0</version>
<properties>
<java.version>21</java.version>
<jjwt.version>0.12.6</jjwt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JWT generation and verification -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>Configuration Properties (type‑safe binding)
server:
port: 8080
spring:
application:
name: springboot4-onlyoffice
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
# OnlyOffice core settings
onlyoffice:
document-server-url: http://192.168.1.100:8089/
web-sdk-path: web-apps/apps/api/documents/api.js
jwt-secret: onlyoffice_secret_2026
callback-host: http://192.168.1.100:8080
file-storage-path: ./data/onlyoffice-files/ @Data
@Configuration
@ConfigurationProperties(prefix = "onlyoffice")
public class OnlyOfficeProperties {
private String documentServerUrl;
private String webSdkPath;
private String jwtSecret;
private String callbackHost;
private String fileStoragePath;
}File Storage Service (decouples business from document handling)
@Service
@RequiredArgsConstructor
public class DocumentStorageService {
private final OnlyOfficeProperties properties;
/** Initialise storage directory */
@PostConstruct
public void init() throws IOException {
Path path = Paths.get(properties.getFileStoragePath());
if (!Files.exists(path)) {
Files.createDirectories(path);
}
}
/** Get absolute path for a given file key */
public Path getFilePath(String fileKey) {
return Paths.get(properties.getFileStoragePath(), fileKey);
}
/** Save uploaded file stream */
public void saveFile(String fileKey, InputStream inputStream) throws IOException {
Path filePath = getFilePath(fileKey);
Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
}
/** Check existence */
public boolean exists(String fileKey) {
return Files.exists(getFilePath(fileKey));
}
/** URL for front‑end to download the file */
public String getFileDownloadUrl(String fileKey) {
return properties.getCallbackHost() + "/document/download?fileKey=" + fileKey;
}
/** Callback URL for OnlyOffice to push saved document */
public String getCallbackUrl(String fileKey) {
return properties.getCallbackHost() + "/document/callback?fileKey=" + fileKey;
}
}JWT Utility (signs editor config and validates callbacks)
@Component
@RequiredArgsConstructor
public class OnlyOfficeJwtUtil {
private final OnlyOfficeProperties properties;
/** Create JWT token from any payload object */
public String createToken(Object payload) {
try {
byte[] keyBytes = properties.getJwtSecret().getBytes(StandardCharsets.UTF_8);
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
return Jwts.builder()
.claim("payload", payload)
.signWith(key)
.compact();
} catch (Exception e) {
throw new RuntimeException("JWT signature generation failed", e);
}
}
/** Validate JWT token from callback request */
public boolean validateToken(String token) {
try {
byte[] keyBytes = properties.getJwtSecret().getBytes(StandardCharsets.UTF_8);
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
Jwts.parser().verifyWith(key).build().parseSignedClaims(token);
return true;
} catch (Exception e) {
return false;
}
}
}Editor Configuration API (core backend endpoint)
@RestController
@RequestMapping("/document")
@RequiredArgsConstructor
public class DocumentController {
private final DocumentStorageService storageService;
private final OnlyOfficeJwtUtil jwtUtil;
private final OnlyOfficeProperties properties;
/** Get editor configuration for a given file */
@GetMapping("/config")
public Result<EditorConfigVO> getEditorConfig(@RequestParam String fileKey,
@RequestParam(defaultValue = "edit") String mode,
@RequestParam(defaultValue = "1") String userId,
@RequestParam(defaultValue = "User") String userName) {
if (!storageService.exists(fileKey)) {
return Result.fail("File does not exist");
}
String suffix = FilenameUtils.getExtension(fileKey).toLowerCase();
String docType = convertDocumentType(suffix);
EditorConfigVO config = new EditorConfigVO();
config.setDocumentKey(UUID.randomUUID().toString().replace("-", ""));
config.setDocumentTitle(fileKey);
config.setDocumentType(docType);
config.setDocumentUrl(storageService.getFileDownloadUrl(fileKey));
config.setMode(mode);
config.setCallbackUrl(storageService.getCallbackUrl(fileKey));
EditorConfigVO.UserInfo user = new EditorConfigVO.UserInfo();
user.setId(userId);
user.setName(userName);
config.setUser(user);
String token = jwtUtil.createToken(config);
config.setToken(token);
return Result.success(config);
}
private String convertDocumentType(String suffix) {
return switch (suffix) {
case "doc", "docx", "txt", "pdf" -> "word";
case "xls", "xlsx", "csv" -> "cell";
case "ppt", "pptx" -> "slide";
default -> "word";
};
}
/** Save callback – OnlyOffice pushes the final document */
@PostMapping("/callback")
public Map<String, Integer> callback(@RequestParam String fileKey,
@RequestBody Map<String, Object> callbackBody,
HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (!jwtUtil.validateToken(token)) {
return Map.of("error", 403);
}
}
int status = (int) callbackBody.get("status");
if (status == 2) { // ready to save
String downloadUrl = (String) callbackBody.get("url");
try {
URL url = new URL(downloadUrl);
try (InputStream in = url.openStream()) {
storageService.saveFile(fileKey, in);
}
return Map.of("error", 0);
} catch (Exception e) {
log.error("Document save failed", e);
return Map.of("error", 1);
}
}
return Map.of("error", 0);
}
/** File download for OnlyOffice */
@GetMapping("/download")
public void download(@RequestParam String fileKey, HttpServletResponse response) throws IOException {
Path filePath = storageService.getFilePath(fileKey);
if (!Files.exists(filePath)) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileKey, StandardCharsets.UTF_8));
Files.copy(filePath, response.getOutputStream());
}
/** Simple file upload */
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
storageService.saveFile(fileName, file.getInputStream());
return Result.success(fileName);
}
}
/** DTO for editor configuration */
@Data
public class EditorConfigVO {
private String documentKey; // unique identifier
private String documentTitle; // file name
private String documentType; // word / cell / slide
private String documentUrl; // download URL for OnlyOffice
private String mode; // edit or view
private String callbackUrl; // where OnlyOffice pushes saved file
private UserInfo user; // user info displayed in editor
private String token; // JWT signature
@Data
public static class UserInfo {
private String id;
private String name;
}
}Frontend Quick Integration (Vue 3 Example)
<template>
<div class="document-editor">
<div id="docxEditor"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { getDocumentConfig } from '@/api/document'
const props = defineProps({
fileKey: { type: String, required: true },
mode: { type: String, default: 'edit' } // edit or view
})
const initEditor = async () => {
// 1. fetch config from backend
const res = await getDocumentConfig(props.fileKey, props.mode)
const config = res.data
// 2. build full editor config
const editorConfig = {
document: {
fileType: config.documentType,
key: config.documentKey,
title: config.documentTitle,
url: config.documentUrl,
permissions: { edit: config.mode === 'edit', download: true, print: true }
},
editorConfig: {
mode: config.mode,
lang: 'zh-CN',
user: { id: config.user.id, name: config.user.name },
callbackUrl: config.callbackUrl
},
token: config.token,
height: '100%',
width: '100%'
}
// 3. initialise OnlyOffice editor
new DocsAPI.DocEditor('docxEditor', editorConfig)
}
onMounted(() => {
// dynamically load OnlyOffice SDK
const script = document.createElement('script')
script.src = 'http://192.168.1.100:8089/web-apps/apps/api/documents/api.js'
script.onload = initEditor
document.head.appendChild(script)
})
</script>
<style scoped>
.document-editor { width: 100%; height: 85vh; }
</style>Key Configuration Items Explained
document.key– unique identifier; changing it forces the editor to reload a new version. editorConfig.mode – edit for editing, view for read‑only preview. permissions – fine‑grained control over edit, download, print, comment, etc. callbackUrl – endpoint that OnlyOffice calls to push the saved document. token – JWT signature that the backend validates.
Advanced Production‑Level Features
Real‑time Collaborative Editing
OnlyOffice natively supports multiple users opening the same document simultaneously; each cursor and edit is synchronised in real time without extra backend code, ideal for contracts, project docs and reports.
Fine‑grained Permission Control
Read‑only preview: edit: false.
Comment‑only mode: users can add comments but cannot modify the main content.
Disable download/print to prevent data leakage for sensitive contracts.
Form‑fill mode: restrict editing to designated form fields.
Document Format Conversion
OnlyOffice provides APIs to convert Word to PDF, Excel to PDF, or legacy doc / xls / ppt formats to modern OOXML before editing, enabling automatic conversion after upload.
Watermark and Security
Editors can add text watermarks showing the current user’s name/ID and can disable right‑click copy or text selection to enhance document security.
Version History
Each save creates a new version; the backend can archive the previous file and record a version number, allowing users to view change logs or roll back.
Custom Plugin Extensions
OnlyOffice’s plugin mechanism lets developers add features such as corporate seals, internal asset libraries, or one‑click data insertion, fully adapting the editor to enterprise needs.
Common Pitfalls and Troubleshooting
Network Connectivity Issues (Three Major Traps)
Never use loopback addresses ( localhost / 127.0.0.1) for the document server; use a real IP or domain because OnlyOffice must reach the business backend.
Bidirectional communication is required: the front end must reach OnlyOffice, and OnlyOffice must reach the backend’s download and callback URLs.
If front end and OnlyOffice are on different domains, ensure CORS is allowed; the official Docker image enables it by default.
JWT Authentication Failures
Make sure the Docker JWT_SECRET matches the backend’s secret exactly.
Verify that the JWT payload structure matches the configuration object sent to the editor.
OnlyOffice uses HS256 by default; do not switch to another algorithm without adjusting both sides.
Chinese Font / Garbled Characters
Mount Chinese fonts (e.g., simhei.ttf, simsun.ttc) into /usr/share/fonts/trltetype/custom and run documentserver-generate-allfonts.sh to refresh the font cache.
Large File Performance
Allocate sufficient CPU and memory to the document server; insufficient resources cause noticeable lag.
Enable document caching; identical document.key values reuse parsed content.
For very large files, consider converting to PDF for preview instead of full‑edit mode.
HTTPS Deployment
When the overall system uses HTTPS, the OnlyOffice service must also serve HTTPS; otherwise browsers block mixed‑content loading. Using Nginx as a reverse proxy to terminate SSL is a common solution.
Compatibility Notes
OnlyOffice works best with modern OOXML formats ( docx, xlsx, pptx). Older doc / xls / ppt files should be converted first. Complex macros, VBA scripts or advanced formulas may exhibit differences; validate critical documents before production.
Solution Comparison: Is OnlyOffice Right for Your Project?
The table below summarises typical online‑document solutions:
OnlyOffice – private‑deployment, free open‑source, native editing, real‑time collaboration, fully private‑cloud suitable for intranet OA, knowledge bases, project management with limited budget.
Commercial Document Service – SaaS or private‑cloud, high yearly licensing cost, supports editing, collaboration, but may have partial feature gaps and higher expense.
PDF/Image Preview – simple backend conversion, low cost, view‑only, no editing, suitable for read‑only scenarios.
Online Document Platform (SaaS) – pay‑per‑use, supports editing and collaboration, but lacks private‑deployment and is unsuitable for highly confidential data.
Recommended use cases for OnlyOffice include internal enterprise OA, knowledge bases, and any scenario requiring deep integration, custom workflows, and strict data control. It is not recommended for consumer‑facing C‑end products with massive user bases, for 100 % Office‑format compatibility requirements, or when the team lacks ops capability to maintain a dedicated document service.
Full‑Cycle Summary
Combining OnlyOffice with Spring Boot provides a cost‑effective, production‑grade solution for private‑cloud online document editing. By offloading the heavy document‑processing logic to the OnlyOffice service, developers can focus on business concerns such as storage, permissions and workflow integration. The architecture scales from simple preview to real‑time multi‑user collaboration, fine‑grained security, versioning, format conversion and custom plug‑ins, covering the majority of enterprise scenarios.
Choosing a solution is not about the newest or most expensive product; it’s about fitting the business context, controlling costs, and ensuring maintainability. This guide equips you with the complete end‑to‑end implementation, from Docker deployment and Spring Boot configuration to front‑end integration and advanced features, ready to be extended with electronic signatures, contract management, reporting engines, workflow integration and a full‑stack file‑management system.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
