前几天有人问我,Java 做桌面应用是不是已经过时了。这个问题我不太想直接回答,因为很多人一提到 Java 桌面开发,脑子里就自动跳出十几年前的 Swing 界面。
最近我在做一些内部工具时,反而试出一个挺有意思的组合:Java 管业务逻辑,Shadcn UI 管界面体验。
以前 Java 桌面程序最大的问题不是功能不够,而是界面太像工具软件。按钮、表格、弹窗,一眼看去就是企业内部系统的味道。现在前端生态已经成熟,没必要再用 Java 硬造一套 UI。
Shadcn UI 本身不是一个完整组件库,它更像是一套基于 React 和 Tailwind CSS 的可复制组件方案。所以我这里的思路不是让 Java 去渲染 Shadcn,而是让 Java 做后端能力,通过 WebView 嵌入现代 UI。
这个架构我更喜欢:
Java 程序 → Spring Boot / 本地服务 → 提供 REST API → React + Shadcn UI → WebView 显示桌面界面
这样做最大的好处是,Java 负责它擅长的数据库、文件处理、线程任务,UI 层交给前端。
很多人一听“桌面应用”,第一反应就是 Swing。比如以前写个文件导入工具,大概是这种:
public class ImportWindow extends JFrame {
private JButton button;
public ImportWindow() {
button = new JButton("开始导入");
button.addActionListener(e -> {
importData();
});
add(button);
setSize(400, 200);
}
private void importData() {
System.out.println("开始处理文件");
}
}
功能没问题。但需求一变,比如按钮换个样式、增加进度条、表格支持筛选,Swing 代码很快开始膨胀。
所以现在我更倾向把 UI 独立出来,Java 这边只提供能力。比如一个本地文件处理工具,Java 可以只暴露一个接口:
@RestController
@RequestMapping("/task")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@PostMapping("/start")
public Map<String, Object> start(@RequestParam String file) {
String taskId = taskService.createTask(file);
return Map.of(
"taskId", taskId,
"status", "running"
);
}
}
前端点击按钮以后,只需要调用这个接口。Shadcn UI 的优势就在这里体现出来了。比如一个简单按钮:
<Button onClick={startTask}>
开始处理
</Button>
然后配合 Card、Table、Dialog 这些组件,很快可以拼出一个接近现在 Web 产品体验的桌面工具。
我自己做这类工具时有个习惯:Java 层尽量不要知道页面长什么样。不要出现这种代码:
if(buttonName.equals("导出")) {
exportExcel();
}
这种写法短期很爽,后面改需求特别痛苦。Java 应该只关心业务状态。例如:
@Service
public class ExportService {
public ExportResult export(Long jobId) {
Job job = jobRepository.findById(jobId)
.orElseThrow();
if(job.isFinished()){
return ExportResult.done();
}
generateFile(job);
return ExportResult.success();
}
}
页面怎么展示,是 React 的事。
另一个比较容易踩坑的地方是桌面应用的数据通信。很多人直接让前端调用 Java 方法,这个耦合会越来越重。我一般会加一层事件状态。比如任务执行:
@Component
public class TaskRunner {
private final ConcurrentHashMap<String, String> status =
new ConcurrentHashMap<>();
public void run(String id) {
status.put(id, "processing");
try {
Thread.sleep(2000);
status.put(id, "success");
} catch (Exception e) {
status.put(id, "failed");
}
}
public String getStatus(String id) {
return status.get(id);
}
}
前端轮询:
const timer = setInterval(async () => {
const res = await fetch("/task/status?id=" + taskId);
const data = await res.json();
setStatus(data.status);
}, 1000);
这种模式虽然简单,但对内部工具非常实用。比如:
- Excel 批量处理工具
- 日志分析工具
- 数据同步工具
- 自动化运维客户端
这些场景不需要上云,也不需要复杂部署。一个 jar 包,加一个前端资源,就能运行。
当然,Shadcn UI 不是万能的。它解决的是界面问题,不解决桌面应用打包问题。Java 桌面应用最后还是需要考虑:
- 怎么启动?
- 怎么升级?
- 怎么访问本地文件?
- 怎么调用系统能力?
比如读取用户目录:
public Path getWorkspace() {
String home = System.getProperty("user.home");
return Paths.get(home, "my-tool", "workspace");
}
或者监听本地文件变化:
WatchService watcher = FileSystems.getDefault().newWatchService();
Path path = Paths.get("./data");
path.register(
watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY
);
这些才是桌面程序真正麻烦的地方。UI 漂亮只是第一步。
我现在看 Java 桌面应用,已经不是以前那个思路了。不是 Swing 和 JavaFX 二选一,而是重新拆分职责。Java 管稳定的东西:数据库连接、文件处理、线程任务、系统调用,这些 Java 很成熟。UI 交给 Shadcn UI,前端生态发展这么多年,没必要重复造轮子。
对于企业内部工具、小型客户端、开发辅助工具,这套组合其实挺舒服。代码还是 Java,部署还是一个客户端,但界面已经不是十年前那个味道了。