简介JxBrowser 7.19 是面向 Java 桌面应用开发者的嵌入式浏览器组件资源适合在 Swing、SWT、JavaFX 等界面中集成网页渲染、HTML 交互、登录授权与跨平台浏览能力的项目使用也可为旧版本升级、兼容性验证和多端部署提供参考。压缩包共 1359 个文件以 1345 个 HTML 文档为主辅以 10 个 JAR 运行库及少量脚本、样式与索引文件整体约 437.21MB内容覆盖 Windows、Linux、macOS 等桌面与 ARM 平台依赖并附带完整 Javadoc 与 Browser.java 示例方便按容器类型选择依赖并完成接入。已有 2599 人学习下载。读者可获得全平台运行库、较完整的接口文档和最小接入示例用于核对版本差异、搭建开发环境、验证容器嵌入方式并对照平台分包排查常见集成问题文档目录也便于按类名与接口快速检索减少反复试错对需要维护多端一致体验的团队较为实用并降低环境配置成本。1. 用 jxbrowser java 把 Chromium 塞进 Swing它真正解决的是哪一类问题Java 桌面端要嵌一个现代内核的浏览器这事儿的坑比想象中深。JavaFX 自带的 WebView 引擎版本老H5 报表里的 ES6 语法一上来就白屏JCEF 能跑但编译链得自己维护换个 JDK 就得重来一遍。所以我一般会把 jxbrowser java 这条路线重新捡起来——JxBrowser 7.19 属于 7.x 线上比较稳的一个点本质是把整个 Chromium 内核封装成Engine/Browser两层 JVM 对象Swing、JavaFX、SWT 都能挂上去。它值钱的地方不是“能显示网页”而是网页里的 JS 能回调 Java、Java 能拦请求改响应、离屏模式下还能截图和打印。适合谁做桌面客户端、界面里要嵌 H5 模块、又不想碰 CEF 编译链的 Java 团队。它是商业库这点要有心理准备。2. 依赖坐标与内核目录jxbrowser 7.19 的包结构拆开看把 7.19 的构件下下来第一反应往往是“怎么这么多 jar”。它不是一个单体包而是按宿主 UI 框架切的核心 API 一个 jarSwing / JavaFX / SWT 各自的视图实现各一个 jar再叠一个装 Chromium 原生库的 classifier 包。搞不清这个分层最容易出的问题就是“代码编译过了一跑就NoClassDefFoundError”或者打出来的安装包莫名其妙 800 MB。2.1 四个构件各管什么别一股脑全引进来先看分工这张表建议存下来排查依赖冲突时对着看构件里面是什么什么时候引jxbrowserEngine、Browser、Frame、Network等核心 API必引jxbrowser-swingBrowserView的 Swing 实现界面是 Swing / AWTjxbrowser-javafxBrowserView的 JavaFX 实现界面是 JavaFXjxbrowser-swtSWT 宿主适配老 RCP 项目核心包只提供“引擎 浏览器实例 DOM 操作 网络事件”它不知道你要把画面贴到哪个组件上。视图包才是把渲染结果落到JComponent或Node上的那一层。所以只引jxbrowser不引jxbrowser-swingBrowserView.newInstance(browser)会直接找不到类。真正的体积大头在 classifier 上。7.x 的坐标长这样!-- 核心 API必引 -- dependency groupIdcom.teamdev.jxbrowser/groupId artifactIdjxbrowser/artifactId version7.19/version !-- cross-desktop 把 win64/linux64/mac64/mac64arm 的原生库全塞进一个 jar 开发期省事发布期建议换成单平台 classifier -- classifiercross-desktop/classifier /dependency !-- Swing 宿主视图 -- dependency groupIdcom.teamdev.jxbrowser/groupId artifactIdjxbrowser-swing/artifactId version7.19/version /dependencyGradle 写法等价dependencies { implementation(com.teamdev.jxbrowser:jxbrowser:7.19:cross-desktop) implementation(com.teamdev.jxbrowser:jxbrowser-swing:7.19) }这里的关键参数是classifier。cross-desktop是“全平台 fat jar”好处是同一份依赖在 Windows 开发机、Linux 构建机、Mac 上都能直接跑坏处是每个平台的chrome可执行文件和.dll/.so/.dylib都在里面包体直接翻几倍。发布阶段我一般会改成单平台 classifier——win64、linux64、mac64、mac64arm这几个按目标平台分别构建安装包能瘦一大圈。JDK 版本上7.x 的编译目标是 Java 8所以 8、11、17 都能跑我这边的 11 和 17 环境都验证过。至于 21 及以上得看构件是否带了对应的模块化描述动手前先写个Engine.newInstance的 ten 行 demo 试一下比看文档快。2.2 渲染模式选型HARDWARE_ACCELERATED 和 OFF_SCREEN 差在哪这一步选错后面所有界面问题都是它引起的。7.x 的RenderingMode实际上就两条路模式渲染路径优点代价HARDWARE_ACCELERATED内核开原生窗口直接叠在宿主窗口上性能接近独立浏览器视频、Canvas 都流畅z-order 由操作系统管Swing 的弹窗、JPopupMenu会被浏览器视图盖住OFF_SCREEN渲染到离屏缓冲再合成到 Swing 组件上能被 Swing 正常遮挡、能截图、能透明叠加必须手动resize滚动和输入事件得自己转发滚动帧率明显低一档判断标准很朴素如果你的界面里浏览器区域是“一整块不跟别的组件重叠的矩形”选HARDWARE_ACCELERATED如果需要在这个区域上浮一层自定义的 Swing 控件、或者要对着页面做截图和录像、或者要嵌进带透明背景的面板里选OFF_SCREEN。OFF_SCREEN有一个几乎人人都会踩的点不调resize就是一片白。它不是自动跟随容器尺寸的必须显式告诉内核画布多大// 离屏模式下必须先给出画布尺寸否则内核不知道渲染多大表现为纯白 browser.resize(1280, 800); BrowserView view BrowserView.newInstance(browser); // 组件尺寸变化时要同步回去否则拉伸窗口后页面还是旧尺寸 view.addComponentListener(new ComponentAdapter() { Override public void componentResized(ComponentEvent e) { browser.resize(Math.max(1, view.getWidth()), Math.max(1, view.getHeight())); } });resize的宽高必须大于 0最小化窗口时如果传 0内核那边会抛参数异常所以上面加了Math.max(1, ...)兜底。这一行是血泪经验不加的话用户一最小化再还原就崩。2.3 内核落地目录别让首次启动去解压临时目录Engine.newInstance第一次执行时会把 Chromium 内核解压出来。默认落到系统临时目录下的一个带版本号的子目录里。开发机上无所谓生产环境就有两个隐患一是临时目录可能被安全策略限制写入二是某些清理工具会把临时目录当垃圾扫掉用户第二天打开软件就报内核加载失败。所以我一般会显式指定内核目录Path base Paths.get(System.getProperty(user.home), .myapp); Path chromiumDir base.resolve(chromium); EngineOptions options EngineOptions.newBuilder(RenderingMode.HARDWARE_ACCELERATED) // license 单独封装别硬编码在源码里也别提交到仓库 .licenseKey(LicenseStore.load()) // 内核落地目录要求可写预留至少 500MB 空间 .chromiumDir(chromiumDir) // 虚拟机和远程桌面环境下关掉着色器缓存能少一类花屏问题 .addSwitch(--disable-gpu-shader-disk-cache) .build(); Engine engine Engine.newInstance(options);三个参数逐个说。licenseKey是必填的7.x 没有合法 license 会在newInstance阶段直接抛异常不是运行到一半才拦。chromiumDir指定解压目标一旦指定后续启动会复用这份内核不再重复解压冷启动能从几十秒降到几百毫秒。addSwitch是往 Chromium 命令行透传开关等价于你在启动 chrome 时加参数排障时特别有用——花屏加--disable-gpu字体渲染异常试--disable-font-subpixel-positioning。如果chromiumDir这个构建器方法报“找不到符号”先别怀疑自己写错了多半是拿到的构件不是标准 7.19 的官方构建被重新打包过。版本对不上的包后面还会在内核版本、license 校验上出各种玄学问题。指定完目录要验证进到~/.myapp/chromium下应该能看到一个chromeWindows 上是chrome.exe加一堆动态库总大小几百 MB。如果目录是空的说明参数没生效或者写入被拦了去看日志里有没有被吞掉的IOException。3. 最小闭环Engine 单例、Browser 多开、BrowserView 挂载依赖理顺了接下来是把一个能跑的窗口搭出来。这三层对象的生命周期差别很大混着用是后面卡顿和进程泄漏的根源Engine是重对象一个进程只能有一个Browser是轻对象可以开很多个BrowserView只是把某个Browser的画面贴到某个组件上不持有内核状态。3.1 一个线程安全的 Engine 持有类Engine.newInstance是阻塞的而且内部会拉起一整套 Chromium 子进程。放在 EDT 上执行界面会直接冻住十几秒——这不是性能问题是“用户以为程序挂了”的问题。所以我把引擎初始化单独抽出来public final class BrowserEngineHolder { private static volatile Engine instance; public static Engine get() { Engine local instance; if (local null) { synchronized (BrowserEngineHolder.class) { local instance; if (local null) { EngineOptions options EngineOptions.newBuilder(RenderingMode.HARDWARE_ACCELERATED) .licenseKey(LicenseStore.load()) .chromiumDir(Paths.get(System.getProperty(user.home), .myapp, chromium)) .build(); local Engine.newInstance(options); instance local; // 兜底关闭防止用户直接关进程留下僵尸子进程 Runtime.getRuntime().addShutdownHook(new Thread(local::close, jxbrowser-shutdown)); } } } return local; } private BrowserEngineHolder() { } }双检锁是为了保证并发调用时只创建一个引擎。很多项目启动时会并发初始化多个业务面板如果每个面板都newInstance你会看到任务管理器里瞬间冒出几组 Chromium 进程每个都占几百 MB还互相抢 GPU 进程。addShutdownHook那一行是后悔药。用户在任务管理器里结束进程时JVM 的关闭钩子仍会执行能把这批 Chromium 子进程一起收掉没有它异常退出后下次启动可能出现端口/锁文件冲突。这个get()必须在启动阶段、非 EDT 线程上预热一次。我一般放在 splash 界面加载时用SwingWorker里跑界面先显示“正在初始化浏览器内核…”。3.2 Browser 的创建与 URL 加载时机Browser很便宜engine.newBrowser()通常在几十毫秒内返回。真正耗时的是页面导航。所以创建和加载要分开看Engine engine BrowserEngineHolder.get(); Browser browser engine.newBrowser(); BrowserView view BrowserView.newInstance(browser); panel.setLayout(new BorderLayout()); panel.add(view, BorderLayout.CENTER); // 导航是异步的这里返回不代表页面已经加载完 browser.navigation().loadUrl(https://report.internal/dashboard); // 加载完成事件里再做 DOM 注入、JS 通信这类事 browser.navigation().on(FinishLoading.class, event - { LOG.info(loaded: {}, event.url()); });loadUrl是异步的。紧接着在下一行去executeJavaScript经常拿到的是null或者空对象因为 DOM 还没建完。所有依赖页面结构的操作都得挂在FinishLoading或DocumentLoaded回调里。同一个Browser可以重复用切换页面用loadUrl或loadHtml不需要关掉重建。但换业务模块时建议browser.close()再新建一个——不是必须只是把可能泄漏的 JS 定时器和事件监听一起清掉。BrowserView.newInstance(browser)这个方法名在HARDWARE_ACCELERATED和OFF_SCREEN下是同一个差异在Engine那边就定了。所以渲染模式必须在建引擎时就定死中途改不了。3.3 关闭顺序先 browser 后 engine关闭顺序错了控制台会刷一堆IllegalStateException严重的时候进程退不干净。正确顺序是自下而上// 关闭单个浏览器视图 browser.close(); // 应用退出时再关引擎顺序不能反 engine.close();browser.close()会释放这个页面的渲染进程和所有 JS 上下文engine.close()会终止整个 Chromium 进程组。如果先engine.close()再browser.close()后者会对一个已经失效的引擎发指令抛异常如果只关窗口不close浏览器渲染进程会一直挂着用户反复打开关闭同一个模块进程数会一路涨上去。一个常见的错误写法是在windowClosing里先调engine.close()再dispose()界面。改成先关所有Browser最后统一关Engine这类问题就没了。4. 双向通信JS 调 Java、Java 注 JS、请求拦截页面能显示只是第一步。真正让嵌入方案有价值的是双向通信网页里的按钮要触发 Java 侧的下载、Java 侧要把登录态塞给页面、某些请求要在网络层被改写。JxBrowser 7.x 给了三层机制从轻到重依次是注入执行、对象桥接、网络拦截。4.1 JsAccessible 加 JsObject把 Java 对象挂到 window 上最省事的做法是把一个普通 Java 对象挂到window上页面里直接当 JS 对象调用。前提是方法必须标JsAccessible不标的一律不可见——这既是安全边界也避免把整个对象图暴露出去public final class HostBridge { JsAccessible public String readToken() { return TokenStore.current(); } JsAccessible public void log(String message) { LOG.info([web] {}, message); } }挂载的时机必须等页面加载完browser.navigation().on(FinishLoading.class, event - { browser.mainFrame().ifPresent(frame - { // executeJavaScript 返回 JsObject 时是页面里的 window 本身 JsObject window frame.executeJavaScript(window); // 挂上去之后页面里可以直接 hostBridge.readToken() window.putProperty(hostBridge, new HostBridge()); }); });frame.executeJavaScript(window)这里看起来有点绕但它返回的就是页面window的JsObject引用putProperty相当于在页面里执行window.hostBridge {...}。参数名这里是hostBridge就是页面侧访问用的全局变量名别跟业务里已有的变量撞。注意两点。第一JsAccessible方法是在哪个线程被调用的取决于内核回调线程不是 EDT方法体里如果要改 Swing 组件必须SwingUtilities.invokeLater包一层否则就是经典的随机崩溃。第二参数和返回值只支持基础类型和字符串传自定义对象会静默变成undefined这个坑很隐蔽。4.2 executeJavaScript 的返回值边界Promise 不是值executeJavaScript看着像个万能入口实际上它只能同步拿到表达式的即时结果。页面里fetch(...)、async函数返回的是一个 Promise 对象你拿到的是JsObject不是数据// 错误示范拿到的不是数据是 Promise 的包装对象 Object wrong frame.executeJavaScript(fetch(/api/profile).then(r r.json())); // 可行做法让页面同步返回序列化好的字符串 String json frame.executeJavaScript(JSON.stringify(window.__profile || null));所以我的习惯是不指望从executeJavaScript直接取异步结果而是在页面侧挂一个回调函数Java 侧先注册桥接对象页面拿到数据后主动调hostBridge.onProfileLoaded(json)。这样数据流向是单向的、可追踪的出问题也容易定位。JSON.stringify那一步别省。JxBrowser 对 JS 对象的自动转换只覆盖基础类型直接返回Object或者数组经常得到一个看起来像数组、实际取不到值的怪东西也不好断点。序列化成字符串Java 侧用任意 JSON 库解析行为最确定。4.3 网络拦截BeforeUrlRequest 拦请求、BeforeSendHeaders 改头网络层是这套方案里最容易被低估的能力。engine.network()上可以订阅一系列事件用来做请求阻断、请求头注入、mock 响应// 阻断某个路径下的所有请求常用于屏蔽页面里的第三方统计脚本 engine.network().on(BeforeUrlRequest.class, event - { if (event.url().contains(/legacy/)) { event.preventDefault(); // 拦掉页面侧表现为请求失败 } }); // 给所有出站请求补一个客户端标识后端按这个标识下发不同版本 engine.network().on(BeforeSendHeaders.class, event - { HttpHeaders headers event.headers().toBuilder() .add(X-Client, desktop/1.0) .build(); event.setHeaders(headers); });BeforeUrlRequest触发在请求发出之前preventDefault()就是阻断页面里对应的fetch会走 reject 分支所以页面侧要有兜底处理否则用户看到的是一个一直转圈的加载态。BeforeSendHeaders触发在请求头组装完成之后headers().toBuilder()拷一份再改不要直接改原对象。这两个接口在不同小版本之间签名略有出入写的时候靠 IDE 补全确认参数类型别照抄网上的老代码。另外网络事件回调不保证在 EDT 上回调体里做重活比如写数据库、读大文件会拖慢整个网络层建议只做判断和转发。5. 避坑与排查许可证、首次启动、EDT、进程残留、打包五个翻车点前面四章都是“怎么跑起来”这一章讲“为什么跑不起来”。下面五个问题是我在不同项目里反复遇到的按出现频率排。每个都按现象、原因、解决三段写遇到了直接对号入座。5.1 许可证与内核来源两个最容易被忽略的前置条件现象一本地开发一切正常换到测试机或者打包后运行启动即抛异常提示 license 缺失或无效。原因7.x 的 license 校验发生在Engine.newInstance而且它是跟签发时的应用标识绑定的不是随便一个字符串都能过。另外把 license 硬编码进源码、或者放在没被打进安装包的资源目录里都会导致运行期读不到。解决把 license 的读取抽成单独一层优先读外部配置文件其次读环境变量最后才回落到打包内的资源。启动时把 key 的前 8 位打进日志不要打全量一眼就能看出读到的是不是预期那一份。生产环境更稳的做法是把 license 放在安装目录的conf/下随配置一起下发方便替换。现象二chromiumDir指定了目录却始终是空的或者目录里有文件但程序报内核版本不匹配。原因拿到的构件被重新打包过classifier 里的原生库和jxbrowser核心包的版本对不上。这种情况在来源不明的整合包里特别常见。解决核对 jar 的清单文件里的版本号和依赖声明里的7.19是否一致。对不上就换回标准构件。生产项目不要用来源不明的整合包出了问题没有任何线索可查纯属给自己埋雷。5.2 线程与卡死在 EDT 上碰 Engine 的代价现象双击图标后窗口白屏十几秒到一分钟进度条不动Windows 直接标“未响应”。原因Engine.newInstance在 EDT 上执行。首次运行要解压几百 MB 的内核文件这段时间 EDT 被完全占住Swing 连重绘都做不了。解决把引擎初始化放到启动线程或者SwingWorker#doInBackground里界面先显示一个 splash 或者“初始化中”的占位面板初始化完成后再切到主界面。第二次及以后的启动因为内核已解压通常几百毫秒就够但代码不能假设“一定是第二次”。现象点击页面按钮后整个界面卡住几秒然后恢复日志里能看到 JS 调用记录。原因在 EDT 上同步等待executeJavaScript的结果或者JsAccessible方法体里做了阻塞 IO。解决桥接方法里只做轻量转换把耗时逻辑丢到线程池需要的返回值通过回调再送回页面。判断标准很简单——桥接方法体超过 10 行就该拆了。5.3 进程与打包任务管理器里的进程数是最好的体检指标现象反复打开关闭同一个业务模块任务管理器里 Chromium 相关进程数一路往上涨内存也跟着涨最后 OOM。原因只把BrowserView从容器里remove了没有browser.close()。视图没了但渲染进程和 JS 上下文还活着。解决把browser.close()和界面销毁绑在一起写在一个统一的dispose()方法里模块卸载时必调。上线前做一次“打开关闭 20 次”的压测看进程数是否回到初始值这是最直接的验证手段。现象开发机能跑打成安装包后安装到干净机器上界面一片空白日志里没有任何异常。原因内核二进制没进安装包或者进了包但被安全软件拦在临时目录外或者目标机器的用户目录没有写权限。解决显式指定chromiumDir到安装目录下的子目录并在安装包里预置好这份内核跳过首次解压。安装完成后加一步校验检查目录下chrome可执行文件是否存在、动态库数量是否与预期一致。这一步能挡掉大部分“换台机器就不行”的问题。6. 上线前的自检把内核版本、线程归属和关闭行为都打出来调到后期问题往往不是“跑不起来”而是“跑起来但行为不对”。我习惯在动手写业务代码之前先跑一段自检把三个不确定因素量化出来内核到底是什么版本、回调落在哪个线程、关闭之后进程有没有真的退干净。public final class BrowserSelfCheck { public static void main(String[] args) throws Exception { Engine engine BrowserEngineHolder.get(); System.out.println(engine created on: Thread.currentThread().getName()); Browser browser engine.newBrowser(); browser.navigation().loadUrl(about:blank); browser.navigation().on(FinishLoading.class, event - { // UA 里 Chrome/xx 那段就是内核真实版本比看文档靠谱 System.out.println(userAgent browser.userAgent()); // 回调线程名能直接告诉你这里能不能碰 Swing System.out.println(callback thread Thread.currentThread().getName()); // 离屏模式下这一行必须 0否则就是白屏 System.out.println(viewport browser.mainFrame().isPresent()); }); Thread.sleep(3000); browser.close(); engine.close(); // 正常情况这行打印后 1~2 秒内进程列表里的 Chromium 子进程应全部消失 System.out.println(closed, check process list now); } }三个输出各有用途。userAgent里的Chrome/后面那段数字是内核真实版本比你翻构件元数据可靠——如果它和你预期的代际差得远说明内核目录里躺着一份旧的解压结果删掉chromiumDir重来。callback thread决定了你的回调体能不能直接操作界面名字不是AWT-EventQueue-0的一律得包invokeLater。最后那句closed打印完之后去任务管理器看一眼子进程如果还在就是某个Browser没关。这段代码我一般会做成一个隐藏的“诊断”菜单项让现场实施的人也能点一下把三行输出贴回来。比让用户描述“有时候会卡”有用得多。从那以后我每次把 jxbrowser 塞进新项目都强制先跑一遍这段自检再谈业务逻辑——省下来的排查时间比写这段代码的时间多得多。手上这份 7.19 的包建议也先按这个顺序跑通再加功能。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?