技术分享 | 在 IDE 插件开发中接入 JCEF 框架

项目背景

当前的开发环境存在多种不同语言的 IDE,如 JetBrains 全家桶、Eclipse、Android Studio 和 VS Code 等等。由于每个 IDE 各有其特定的语言和平台要求,因此开发 IDE 插件时,需要投入大量资源才能尽可能覆盖大部分工具。同时,代码难复用、用户体验难统一等问题又会进一步加重资源负担。

在调研过程中,我们发现如今的大多数开发工具都支持集成 CEF,而 CEF 提供的跨平台解决方案正可以有效解决上述问题。

关于 CEF 和 JCEF

CEF(Chromium Embedded Framework)是一个开源项目,它基于 Google Chromium 浏览器引擎,允许开发人员将完整的浏览器功能嵌入到自己的应用程序中。

通过 CEF,开发者可以利用现代 Web 技术来创建强大的桌面应用程序,并实现与 Web 内容的无缝集成。如此一来,开发者便可以利用 CEF 的功能和灵活性,为各种开发工具提供统一的、高质量的插件体验。

JCEF(Java Chromium Embedded Framework)是基于 CEF 的一个特定版本,专门为 Java 应用程序而生。本文内容也主要围绕 JCEF 展开。

JCEF 和其他产品的对比

  • JCEF vs JxBrowser

JxBrowser 和 JCEF 都允许将 Chromium 浏览器功能嵌入到 Java 应用程序中。其中,JxBrowser 是商业产品,而 JCEF 是开源框架,且商业授权非常友好。

此外,JxBrowser 在独立的本地进程中启动 Chromium,而 JCEF 则是在 Java 进程内启动。JCEF 会快速初始化 Chromium,同时消耗 Java 进程的内存和 CPU;创建多个 Chromium 实例也会占用更多资源。

  • JCEF vs JavaFX

JavaFX 使用的内置浏览器组件是 WebView,其在不同平台上的实现有所不同。例如,在 macOS 上使用 WebKit,在 Windows 上默认为 Internet Explorer,而新版本的 JavaFX 则默认使用 JCEF。

这种不一致性会增加插件适配的难度,降低整体开发效率。

Java 进程与 JCEF 交互

如何在 IDE 插件中接入 JCEF?

下面以 LigaAI Jetbrains 插件为例,介绍集成 JCEF 的过程。

  1. 在 Java 代码里创建相应的 JcefBrowser
static JBCefBrowser createBrowser(Project project) {
    JBCefClient client = JBCefApp.getInstance().createClient();
    //CefMessageRouter 用于处理来自 Chromium 浏览器的消息和事件,
    //前端代码可以通过innerCefQuery和innerCefQueryCancel发起消息给插件进行处理
    CefMessageRouter.CefMessageRouterConfig routerConfig =
            new CefMessageRouter.CefMessageRouterConfig("innerCefQuery", "innerCefQueryCancel");
    CefMessageRouter messageRouter = CefMessageRouter.create(routerConfig, new MessageRouterHandler());
    client.getCefClient().addMessageRouter(messageRouter);
    //用于处理以http://inner/开头的请求。 用于拦截特定请求,转发请求到本地以获取本地资源
    CefApp.getInstance()
            .registerSchemeHandlerFactory("http", "inner", new DataSchemeHandlerFactory());
    return new JBCefBrowser(client, "");
}
  1. 加载对应的 URL,渲染页面。
public static void loadURL(JBCefBrowser browser, String url) {
    //如果不需要设置和浏览器显示相关的,可忽略
    browser.getJBCefClient()
            .addDisplayHandler(settingsDisplayHandler, browser.getCefBrowser());
    browser.loadURL(url);                 
}
  1. Java 进程拦截前端发起的获取静态资源的请求。如果直接访问外部资源,则不需要做拦截,这一步可忽略。
import com.intellij.liga.web.WebviewClosedConnection;
import com.intellij.liga.web.WebviewOpenedConnection;
import com.intellij.liga.web.WebviewResourceState;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import org.apache.commons.lang.StringUtils;
import org.cef.callback.CefCallback;
import org.cef.handler.CefResourceHandler;
import org.cef.misc.IntRef;
import org.cef.misc.StringRef;
import org.cef.network.CefRequest;
import org.cef.network.CefResponse;

import java.io.File;
import java.net.URL;


//继承 CefResourceHandler 接口,自定义处理 Chromium 浏览器加载的资源(如网页、图像、样式表等)。
//通过实现该接口,可以覆盖默认的资源加载行为,并提供自定义的资源加载逻辑。
public class DataResourceHandler implements CefResourceHandler {

    private WebviewResourceState state;

    /**
    * 用于处理资源请求,你可以通过该方法获取请求的 URL、请求头部信息,并返回相应的响应结果。
    */
    public boolean processRequest(CefRequest cefRequest, CefCallback cefCallback) {
        String url = cefRequest.getURL();
        //判断请求是否是用于获取内部静态资源的,如果是则拦截请求,并从项目里对应配置获取对应文件返回
        //如果是请求外部资源,则跳过
        if (StringUtils.isNotBlank(url) && url.startsWith("http://inner")) {
            String pathToResource = url.replace("http://inner", "/front/inner");
            pathToResource = pathToResource.split("\\?")[0];
            URL resourceUrl = getClass().getResource(pathToResource);
            VirtualFile f = VfsUtil.findFileByURL(resourceUrl);
            resourceUrl = VfsUtil.convertToURL(f.getUrl());
            try {
                this.state = (WebviewResourceState) new WebviewOpenedConnection(resourceUrl.openConnection());
            } catch (Exception exception) {
                //log output
            }
            cefCallback.Continue();
            return true;
        }
        return false;
    }

    /**
    * 用于设置资源响应的头部信息,例如 Content-Type、Cache-Control 等。
    */
    public void getResponseHeaders(CefResponse cefResponse, IntRef responseLength, StringRef redirectUrl) {
        this.state.getResponseHeaders(cefResponse, responseLength, redirectUrl);
    }

    /**
    * 用于读取资源的内容,可以从这个方法中读取资源的数据并将其传递给浏览器
    */
    public boolean readResponse(byte[] dataOut, int designedBytesToRead, IntRef bytesRead, CefCallback callback) {
        return this.state.readResponse(dataOut, designedBytesToRead, bytesRead, callback);
    }

    /**
    * 请求取消
    */
    public void cancel() {
        this.state.close();
        this.state = (WebviewResourceState) new WebviewClosedConnection();
    }

}

//定义处理 Chromium Embedded Framework (CEF) 中的 Scheme(协议)请求
public class DataSchemeHandlerFactory implements CefSchemeHandlerFactory {
    public CefResourceHandler create(CefBrowser cefBrowser, CefFrame cefFrame, String s, CefRequest cefRequest) {
        return new DataResourceHandler();
    }
}



import org.cef.callback.CefCallback;
import org.cef.handler.CefLoadHandler;
import org.cef.misc.IntRef;
import org.cef.misc.StringRef;
import org.cef.network.CefResponse;

import java.io.InputStream;
import java.net.URLConnection;

public class WebviewOpenedConnection implements WebviewResourceState {
    private URLConnection connection;

    private InputStream inputStream;

    public WebviewOpenedConnection(URLConnection connection) {
        this.connection = connection;
        try {
            this.inputStream = connection.getInputStream();
        } catch (Exception exception) {
            System.out.println(exception);
        }
    }

    public void getResponseHeaders(CefResponse cefResponse, IntRef responseLength, StringRef redirectUrl) {
        try {
            String url = this.connection.getURL().toString();
            cefResponse.setMimeType(this.connection.getContentType());
            try {
                responseLength.set(this.inputStream.available());
                cefResponse.setStatus(200);
            } catch (Exception e) {
                cefResponse.setError(CefLoadHandler.ErrorCode.ERR_FILE_NOT_FOUND);
                cefResponse.setStatusText(e.getLocalizedMessage());
                cefResponse.setStatus(404);
            }
        } catch (Exception e) {
            cefResponse.setError(CefLoadHandler.ErrorCode.ERR_FILE_NOT_FOUND);
            cefResponse.setStatusText(e.getLocalizedMessage());
            cefResponse.setStatus(404);
        }
    }

    public boolean readResponse(byte[] dataOut, int designedBytesToRead, IntRef bytesRead, CefCallback callback) {
        try {
            int availableSize = this.inputStream.available();
            if (availableSize > 0) {
                int maxBytesToRead = Math.min(availableSize, designedBytesToRead);
                int realNumberOfReadBytes = this.inputStream.read(dataOut, 0, maxBytesToRead);
                bytesRead.set(realNumberOfReadBytes);
                return true;
            }
        } catch (Exception exception) {
            //log output
        } finally {
            this.close();
        }
        return false;
    }

    public void close() {
        try {
            if (this.inputStream != null)
                this.inputStream.close();
        } catch (Exception exception) {
            //log output
        }
    }
}
  1. 前端发送请求调用插件,Java 进程接收并处理。
//前端示例代码
<button onclick="callBrowser()">调用浏览器代码</button>

<script>
function callBrowser() {
  var parameter = "example parameter";
  window.location.href = "innerCefQuery://" + parameter;
}
</script>


//插件示例代码
import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.callback.CefQueryCallback;
import org.cef.handler.CefMessageRouterHandlerAdapter;

public class MessageRouterHandler extends CefMessageRouterHandlerAdapter {
    @Override
    public boolean onQuery(CefBrowser browser, CefFrame frame, long query_id, String request,
                           boolean persistent, CefQueryCallback callback) {
        try {
            System.out.println(request);
            callback.success("");
            return true;
        } catch (Exception e) {
            //log output
        }
        return false;
    }
}
  1. 插件前端代码。
// java进程调用前端代码
String script = "window.postMessage('" + JSONObject.toJSONString(scriptObj) + "');";
browser.executeJavaScript(script, "", 0);

// 前端代码
function postMessage(data) {
  // 处理从后端传递过来的数据
  console.log('Received message from backend:', data);
  // 在这里进行你希望执行的其他操作
}

实现效果

通过使用 LigaAI IDE 插件,开发者们无需跳转或登录外部系统,在 IDE 内就能查看任务详情、完成工作、更新和同步任务状态、记录并提报完成信息;在享受沉浸式工作的同时,零负担地实现个人目标管理。

此外,JCEF 为插件开发者提供了一个强大的工具,可以利用 Chromium 浏览器的各种功能和扩展性,以更丰富、更高级的方式提供信息和功能,使编码过程变得容易。

因此,利用 LigaAI IDE 插件提供的可视化图表,研发团队还可以了解整体编码情况、不同任务类型的耗时分布等,更有针对性地制定优化方案,或调整规划排期。

常见问题及避坑指南

1:集成 JCEF,如何使 Web 样式与 IDE 插件整体样式保持统一?

通过下述方法获取 IDE 的主题模式;

public static String getGlobalStyle() {
    if (EditorColorsManager.getInstance().isDarkEditor())
        return "dark";
    return "light";
}

获取 IDE 内的样式。

//主要可以查看com.intellij.util.ui.UIUtil和com.intellij.ui.JBColor这两个类
//获取字体大小
Font font = UIUtil.getLabelFont();
//获取背景颜色
Color bg = JBColor.background();
//获取字体颜色
Color labelFontColor = UIUtil.getLabelFontColor(UIUtil.FontColor.NORMAL);
//获取按钮的背景颜色
JBColor buttonBg = JBColor.namedColor("Button.default.startBackground",JBUI.CurrentTheme.Focus.defaultButtonColor());
//获取边框的颜色
Color border = JBColor.border();

2:Java 和浏览器之间的交互路由名称不能设置为 cefQuerycefQueryCancel

这两个为 JCEF 的内置路由,同名会干扰甚至覆盖 JCEF 的内部处理逻辑,有一定概率会导致系统白屏等意外行为和异常情况。

CefMessageRouter.CefMessageRouterConfig routerConfig =
            new CefMessageRouter.CefMessageRouterConfig("innerCefQuery", "innerCefQueryCancel");

3:于 JetBrains 插件而言,如果浏览器加载的静态页面数据是打包在插件包内的本地数据,加载过程中获取目标 URL 需要先把目标文件转化为 JetBrains 的虚拟文件,再获取虚拟文件的 URL 作为结果,不然会加载不到目标文件。

public boolean processRequest(CefRequest cefRequest, CefCallback cefCallback) {
    String url = cefRequest.getURL();
    if (StringUtils.isNotBlank(url) && url.startsWith("http://inner")) {
        String pathToResource = url.replace("http://inner", "/front/inner");
        pathToResource = pathToResource.split("\\?")[0];
        // 这里先获取目标文件,转成虚拟文件,再获取对应URL
        URL resourceUrl = getClass().getResource(pathToResource);
        VirtualFile f = VfsUtil.findFileByURL(resourceUrl);
        resourceUrl = VfsUtil.convertToURL(f.getUrl());
        //
        try {
            this.state = (WebviewResourceState) new WebviewOpenedConnection(resourceUrl.openConnection());
        } catch (Exception exception) {
        }
        cefCallback.Continue();
        return true;
    }
    return false;
}

4:插件初始化时,如果浏览器请求 java 的接口较多,或接口速度较慢时,可能会出现白屏。这是因为 onQuery 里复杂的逻辑需要异步处理,不然多个请求会阻塞导致浏览器白屏。

public class MessageRouterHandler extends CefMessageRouterHandlerAdapter {
    @Override
    public boolean onQuery(CefBrowser browser, CefFrame frame, long query_id, String request,
                           boolean persistent, CefQueryCallback callback) {
        try {
            ApplicationManager.getApplication().invokeLater(() -> {
                //进行复杂的逻辑
            });
            callback.success("");
            return true;
        } catch (Exception e) {
            //log output
        }
        return false;
    }
}

参考资料

[1] CEF 相关文档:https://github.com/chromiumembedded/cef

[2] JCEF 源码位置: https://github.com/chromiumembedded/java-cef

[3] Jetbrains 插件开发文档:https://plugins.jetbrains.com/docs/intellij/welcome.html

[4] JxBrowser 和 JCEF 的对比:https://dzone.com/articles/jxbrowser-and-jcef


了解更多技术干货、研发管理实践等分享,请关注 LigaAI。

欢迎试用 LigaAI-智能研发协作平台,体验智能研发协作,一起变大变强!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/186506.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【精选】框架初探篇之——MyBatis的CRUD及配置文件

MyBatis增删改查 MyBatis新增 新增用户 持久层接口添加方法 void add(User user);映射文件添加标签 <insert id"add" parameterType"com.mybatis.pojo.User">insert into user(username,sex,address) values(# {username},# {sex},# {address}) <…

SOLIDWORKS 2024新功能之CAM篇

SOLIDWORKS 2024 新功能 CAM篇目录概述 • 附加探测周期参数 • 反转切割的固定循环螺纹加工 • 包含装配体的零件的正确进给/速度数据 • Heidenhain 探测类型 • 2.5 轴特征向导中岛屿的终止条件 • 链接轮廓铣削操作的切入引导和切出引导参数 • 螺纹铣削操作的最小孔…

从0开始学习JavaScript--深入了解JavaScript框架

JavaScript框架在现代Web开发中扮演着关键角色&#xff0c;为开发者提供了丰富的工具和抽象层&#xff0c;使得构建复杂的、高性能的Web应用变得更加容易。本文将深入探讨JavaScript框架的核心概念、常见框架的特点以及它们在实际应用中的使用。 JavaScript框架的作用 JavaSc…

NX二次开发UF_CSYS_edit_matrix_of_object 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CSYS_edit_matrix_of_object Defined in: uf_csys.h int UF_CSYS_edit_matrix_of_object(tag_t object_id, tag_t matrix_id ) overview 概述 Updates the specified coordinat…

Matplotlib自定义坐标刻度_Python数据分析与可视化

自定义坐标刻度 主次要刻度隐藏刻度与标签花哨的刻度格式格式生成器与定位器 虽然matplotlib默认的坐标轴定位器与格式生成器可以满足大部分需求&#xff0c;但是并非对每一幅图都合适。 主次要刻度 学习前最好有对matplotlib图形的对象层级较为了解&#xff0c;例如查看前面…

leetcode刷题详解三

2. 两数相加 思路&#xff1a;直接加&#xff0c;注意进位条件不要用if&#xff0c;核心代码在于sum l1->val l2->val carry; ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {ListNode* dummy new ListNode();ListNode* dummy_head dummy;int carry 0;int …

Ubuntu 22.04.3编译AOSP13刷机

文章目录 设备信息下载AOSP并切换分支获取设备驱动编译系统编译遇到的问题Cannot allocate memoryUbuntu设置USB调试刷机参考链接 设备信息 手机&#xff1a;Pixel 4XL 下载AOSP并切换分支 在清华大学开源软件镜像站下载初始化包aosp-latest.tar。 解压缩&#xff0c;切换到…

【汉诺塔 —— (经典分治递归)】

汉诺塔 —— &#xff08;经典分治递归&#xff09; 一.汉诺塔介绍二.分治算法解决汉诺塔问题三.汉诺塔问题的代码实现四.主函数测试展示 一.汉诺塔介绍 汉诺塔问题源自印度一个古老的传说&#xff0c;印度教的“创造之神”梵天创造世界时做了 3 根金刚石柱&#xff0c;其中的一…

机器视觉尺寸测量仪 助力打造智能工厂!

摘要&#xff1a;机器视觉系统基本的特点就是提高生产的灵活性和自动化程度&#xff0c;目前机器视觉技术在蓬勃发展中&#xff0c;机器视觉尺寸测量仪是基于机器视觉原理制造而成的在线几何尺寸精密仪器。本文系统介绍一下该类测量设备。 机器视觉是什么&#xff1f; 简单来讲…

从0开始学习JavaScript--JavaScript数据类型与数据结构

JavaScript作为一门动态、弱类型的脚本语言&#xff0c;拥有丰富的数据类型和数据结构&#xff0c;这些构建了语言的基础&#xff0c;为开发者提供了灵活性和表达力。本文将深入探讨JavaScript中的各种数据类型&#xff0c;包括基本数据类型和复杂数据类型&#xff0c;并介绍常…

2023.11.24制作一个常用的登录注册模板(包含密码验证、输出格式验证、验证码等功能)

2023.11.24制作一个常用的登录注册模板&#xff08;包含密码验证、输出格式验证、验证码等功能&#xff09; 1. 简介2. 功能3. 页面效果3.1 登录页面3.2 忘记密码页3.3 注册页面 1. 简介 比较喜欢简洁风&#xff0c;只是用bootstrap进行简单装饰 制作一个模板&#xff0c;日常…

Leetcode---372周赛

题目列表 2937. 使三个字符串相等 2938. 区分黑球与白球 2939. 最大异或乘积 2940. 找到 Alice 和 Bob 可以相遇的建筑 一、使三个字符串相等 这题把题目意思读懂&#xff0c;正常模拟就行&#xff0c;简单来说就是看三个字符串的最长公共前缀有多长&#xff0c; 代码如下…

学习Pandas 二(Pandas缺失值处理、数据离散化、合并、交叉表与透视表、分组与聚合)

文章目录 六、高级处理-缺失值处理6.1 检查是否有缺失值6.2 缺失值处理6.3 不是缺失值NaN&#xff0c;有默认标记的 七、高级处理-数据离散化7.1 什么是数据的离散化7.2 为什么要离散化7.3 如何实现数据的离散化 八、高级处理-合并8.1 pc.concat实现合并&#xff0c;按方向进行…

x-www-form-urlencoded的含义解释,getReader()和getParameter()的区别

1、x-www-form-urlencoded x-www-form-urlencoded是一种编码格式&#xff0c;它是一种常见的编码方式&#xff0c;用于在HTTP请求中 传输表单数据 。在这种编码方式下&#xff0c;表单数据被编码为URL格式&#xff0c;然后作为请求体&#xff08;payload&#xff09;发送。 需要…

前端大厂(腾讯、字节跳动、阿里......)校招面试真题解析,让你面试轻松无压力!

前言 校招很重要&#xff0c;应届生的身份很珍贵&#xff01;在校招的时候与我们竞争的大部分都是没有工作经验的学生&#xff0c;而且校招企业对学生的包容度高&#xff0c;一般对企业来说&#xff0c;社招更看重实际工作经验&#xff0c;而校招更愿意“培养人”&#xff0c;校…

FindMy技术用于旅行箱

旅行箱&#xff0c;那是出门在外的我们不可或缺的伙伴。无论是商务出差&#xff0c;还是短途旅行&#xff0c;亦或是长途度假&#xff0c;旅行箱都以其便捷的方式&#xff0c;陪伴着我们的整个行程。 然而&#xff0c;在旅途中&#xff0c;丢失旅行箱是一件非常棘手的问题&…

【Web】PhpBypassTrick相关例题wp

目录 ①[NSSCTF 2022 Spring Recruit]babyphp ②[鹤城杯 2021]Middle magic ③[WUSTCTF 2020]朴实无华 ④[SWPUCTF 2022 新生赛]funny_php 明天中期考&#xff0c;先整理些小知识点冷静一下 ①[NSSCTF 2022 Spring Recruit]babyphp payload: a[]1&b1[]1&b2[]2&…

【计算机网络笔记】数据链路层——差错编码

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

RK3588平台 USB框架与USB识别流程

一.USB的基本概念 在最初的标准里&#xff0c;USB接头有4条线&#xff1a;电源&#xff0c;D-,D,地线。我们暂且把这样的叫做标准的USB接头吧。后来OTG出现了&#xff0c;又增加了miniUSB接头。而miniUSB接头则有5条线&#xff0c;多了一条ID线,用来标识身份用的。 热插拔&am…

VR全景展示,“超前点播”打开娱乐行业线上营销门户

如今&#xff0c;人们的生活水平正在逐步提高&#xff0c;这种提高不仅仅是体现在衣食住行上&#xff0c;更多方面是体现在大众的娱乐活动上。我们可以看到&#xff0c;相比于过去娱乐种类的匮乏&#xff0c;现如今&#xff0c;各种娱乐活动可谓是百家争鸣&#xff0c;例如温泉…