SpringBoot使用Chromium开发桌面客户端
架构设计
- 将B/S前后端的概念利用在C/S端
- 使用纯前端展示UI界面
- 使用SpringBoot REST API 作为后端接口
- 前端使用 ajax/fetch 获取后端数据或者触发后端行为
- 后端通过发布订阅方式主动推送数据给前端
- 后端利用JCEF调用Chromium作为前端运行环境
搭建开发环境
- IDE使用的是IDEA 2021.2.2
- SpringBoot使用的是 2.3.7.RELEASE 版本
- Chromium使用的是 110.0.25.1 版本
后端依赖
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>me.friwi</groupId>
<artifactId>jcefmaven</artifactId>
<version>110.0.25.1</version>
</dependency>
</dependencies>
前端依赖
<script src="https://cdn.bootcdn.net/ajax/libs/pubsub-js/1.9.4/pubsub.min.js"></script>
下载chromium
- 进入 https://developer.aliyun.com/mvn/search
- 仓库选择
central
,关键字选择jcef-natives
,点击搜索
- ctrl+f 搜索
110.0.25
找到 artifact-id 为jcef-natives-windows-amd64
,packaging为jar
的项 - 点击右边的下载题标,获取jar
- 解压jar,里面有个tar包,再解压tar包
- 将解压的文件放在一个文件夹里,文件夹拖到springboot工程下
代码
- SpringBoot启动类
package com.example.jcefdemo;
import lombok.extern.slf4j.Slf4j;
import me.friwi.jcefmaven.CefAppBuilder;
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.browser.CefBrowser;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
@SpringBootApplication
@Slf4j
public class App {
private static CefBrowser browser;
private static int port = 8080;
private static String chromiumDir = "chromium";
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(App.class)
.headless(false).run(args);
createUI(context,args);
}
private static void createUI(ConfigurableApplicationContext context, String[] args) {
try {
CefAppBuilder builder = new CefAppBuilder();
builder.getCefSettings().windowless_rendering_enabled = false;
builder.setAppHandler(new MavenCefAppHandlerAdapter() {
@Override
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
// 关闭应用时退出jvm运行
if (state == CefApp.CefAppState.TERMINATED) exit(context);
}
});
// 设置cef运行参数,这里为空
builder.addJcefArgs(args);
// 设置 cef chrome实例的目录,关键,若不设置他会默认从网络中下载,国外网络可能下载不稳定导致失败
builder.setInstallDir(new File(System.getProperty("user.dir") + File.separator + chromiumDir));
// 由于是手动设置cef的chrome,我们要跳过ins检查,防止版本不一致导致从镜像站下载
builder.setSkipInstallation(true);
// 全局的 CefApp 每个程序只能有一个,线程安全
CefApp build = builder.build();
// 显示一些版本信息
CefApp.CefVersion cefVersion = build.getVersion();
log.info(cefVersion.toString());
// 创建一个浏览器客户端实例
CefClient client = build.createClient();
// 创建一个浏览器实例,设置访问 url
browser = client.createBrowser("http://localhost:"+port, false, true);
// 获取UI组件
java.awt.Component uiComponent = browser.getUIComponent();
// 创建 JFrame UI 用于放入chrome
JFrame main = new JFrame("jcef demo");
main.getContentPane().add(uiComponent, BorderLayout.CENTER);
main.setSize(1280, 720);//大小
main.setLocation(0, 0);//位置
main.setVisible(true);//显示
main.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// 关闭应用时要释放资源
CefApp.getInstance().dispose();
main.dispose();
exit(context);
}
});
}catch (Exception e) {
e.printStackTrace();
exit(context);
}
}
private static void exit(ConfigurableApplicationContext context) {
if (context.isActive()) {
SpringApplication.exit(context);
System.exit(0);
}
}
public static void publish(String name, String data) {
String js = "PubSub.publish('"+name+"','"+data+"')";
browser.executeJavaScript(js,"",0);
}
}
- 测试用的Controller
package com.example.jcefdemo.controller;
import com.example.jcefdemo.App;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.swing.*;
import java.io.File;
@RestController
public class TestController {
@Autowired
private ObjectMapper objectMapper;
@GetMapping("/hello")
public ObjectNode hello() {
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put("name","hello");
return objectNode;
}
@GetMapping("/selectFile")
public void selectFile() {
SwingUtilities.invokeLater(() -> {
JFileChooser fileChooser = new JFileChooser();
int result = fileChooser.showOpenDialog(null);
// 处理用户的选择
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = fileChooser.getSelectedFile();
String path = selectedFile.getAbsolutePath().replace("\\","/");
App.publish("fileChooser",path);
}
});
}
}
- 在resources/static目录下新建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/pubsub-js/1.9.4/pubsub.min.js"></script>
</head>
<body>
<p>hello world</p>
<p id="p1"></p>
<p id="p2"></p>
<button onclick="fetch('/selectFile')">打开</button>
<script>
fetch('/hello').then(r => r.json()).then(d => {
document.getElementById('p1').innerText = d.name;
})
PubSub.subscribe('fileChooser', function (message, data) {
document.getElementById('p2').innerText = data;
});
</script>
</body>
</html>
不积跬步无以至千里