引言:
随着开发年限的增长,逐渐积累了很多开发习惯,同时也积攒了开发中很多不爽的问题,如何改造IDE适应团队的开发习惯进一步提升团队开发效率以及统一代码风格变的越来越迫切 ,通过对IDE的改造,我们还可以及时排除代码安全隐患,提高程序的安全性与健壮性。
一、背景
大型项目代码开发过程中对代码风格要求非常严格,随着软件规模的增大复杂性也大大提高,为了保证代码风格的统一而制定了团队的编码规范,与此同时产生了另一个问题:每一句代码几乎都需要手动去对齐代码格式,这样非常的费力。除此之外,在平时代码编写的过程中每行的都需要结尾补齐;
或者{ }
操作,在补齐的过程中需要不断的用键盘或者鼠标去移动光标控制代码换行,长时间重复的去做这个工作非常枯燥和低效,有没有什么工具可以帮开发者自动去补齐并格式化代码从而这种重复工作中解脱出来呢?
在整理思路之后于是就有了下面这个插件,从此开发过程中只需要关心业务逻辑,每行写完直接按 Ctrl+Enter
就可以了,代码编写变的的一气呵成。
使用效果如下
二、插件功能设计
在收集需求之后,插件主要支持的功能如下:
- 行尾智能补齐和光标智能跳转
- 自动格式化当前行代码
- Block代码块结束跳出
不需要再重复输入;
和{}
,也不用手动的去移动光标和鼠标,只用关心业务逻辑,不用费力去拼代码格式,一段代码写完之后直接跳出开始下一段,编写变的非常连贯。那么如何改造我们的IDE实现我们的需求呢? 答案就是给我们IDE开发插件。
通过开发插件,我们可以进一步完善自己的IDE,像京东商城这种大型的项目,插件的开发工作还可以紧贴到具体的业务逻辑,给研发带来巨大的便利!
三、IntelliJ平台
IntelliJ框架下有很多IDE,比如IntelliJ IDEA,WebStorm,Android Studio, PhpStorm ,PyCharm等等,这些IDE都是基于IntelliJ框架定制的,国内关于IntelliJ的开发资料非常匮乏,开发过程中整理下来一些资料和经验以便分享。
IntelliJ平台简介
IntelliJ 提供了构建IDE的平台,IntelliJ是开源的可以供第三方来构建IDE, IntelliJ平台提供了一系列的基础组件,包括丰富的语言工具支持。IntelliJ一个组件驱动,基于JVM跨平台的应用,并且提供了高级别的工具包创建工具窗口,树视图和列表(支持快速搜索)以及弹出菜单和对话框IntelliJ 还包括一个图像编辑器,以及一个完整的文本编辑器,并提供抽象的语法高亮显示的实现,代码折叠、代码完成、和其他富文本编辑功能。此外,它还包括可插拔的API以构建常见的IDE功能,如项目模型和构建系统。它还提供了非常丰富的调试体验的基础设施,具有语言不可知的高级断点支持,调用堆栈,监视窗口和表达式评估。
IntelliJ真正强大的地方来自于PSI(Program Structure Index )系统,PSI是一组功能结合,可用来解析文件,构建丰富的代码语法和语义模型,并从这些数据构建索引。以此为基础提供了许多功能,比如快速导航到文件,类型和符号,到代码编辑窗口,代码引用,代码检查和代码重写,快速修复或重构,以及许多其他功能。
IntelliJ平台模块组成
IntelliJ开发资料较少,通过阅读IntelliJ源代码,我自己大致将代码分为以下几部分,此划分非出自官方难免有些纰漏,了解各部分的功能会对开发工作有很大帮助。
- Base Platform:包括基础平台支持,多线程任务,消息传递,Project Structure工程结构,library,SDK等。
- Action System:Action动作系统,开发者可以通过插件向菜单和工具栏添加新项目来自定义IntelliJ平台UI。
- PSI:构建丰富的代码语法和语义模型,代码完成代码检测都是基于此完成。
- VFS:它封装了大部分对活动文件的处理操作,在不同平台上提供了统一的文件API。
- GUI: 常用界面的封装,对话框,工具栏,文件选择器,Notification等。
- Editor Basics:获取文档,文本操作,代码定位,编辑器事件处理系统。
- Plugin:插件机制,让第三方可以扩展IDE功能。
- Others:其他功能,代码检测,代码完成等其他工具代码。
通过这些支持我们可以开发的IntelliJ功能大致分为四大类型:
- 自定义语言支持: 给IDE添加一种语言支持,比如Kotlin。
- 框架集成: 给IDE添加功能性框架,比如代码洞察。
- 工具集成: 使IDE直接操作第三方工具而无需切换上下文。
- 用户自定义界面: 对标准IDE的界面进行修改,并提供新的功能。
四、如何开发一个插件
开发前准备
开发用IDE:IntelliJ IDEA community 社区版 9.0+;
IntelliJ IDEA community源码: 因为资料太少,很多功能需要去翻阅源代码。
插件开发中的重要概念
插件的开发过程中有很多重要概念,了解这些关键点可以事半功倍,先提炼出来这些关键点,在后续的开发中会逐个讲解。
插件描述文件
在描述文件中
META-INF\plugin.xml
,开发者可以定义插件的包名版本号,插件名称及描述,插件的Action,扩展和扩展点,支持的语言,支持的IntelliJ版本,包含的Service等,和Android中的AndroidManifest.xml
功能类似。版本适配
插件可以声明自己支持的IntelliJ版本,具体的声明在
META-INF\plugin.xml
。IntelliJ经过长时间的迭代开发,在各个版本的中的API有很多修改,因此除了设定支持的版本之外,对使用到的IntelliJ API的地方也要进行兼容性测试。如出现版本差异,可以用版本号区分代码逻辑分支,也可以用反射进行处理。其中如果使用到了private级别的方法需要非常小心,因为IntelliJ旗舰版是做过混淆的,private函数很多都被混淆了,即使用反射去兼容难度也非常大,而且可行性和稳定性非常低。插件级别
插件的可以被分为
Application
,Project
,Module
三个级别。
Application 所有运行的IDE使用一个插件实例;
Project 每个打开的IDE有一份独立的插件实例;
Module 工程中的每个Module有一份独立的实例。Action系统 见下文
插件扩展与扩展点
IntelliJ平台提供了扩展和扩展点的概念,允许插件与其他插件或IDE本身进行交互。
扩展点:如果希望自己的插件允许其他插件扩展其功能,则在插件中,必须声明一个或多个扩展点。每个扩展点都定义了一个允许访问此点的类或接口。
扩展:如果希望自己的插件扩展其他插件或IntelliJ平台的功能,则必须声明一个或多个扩展。
扩展的声明:
<extensionPoints>
<extensionPoint name="MyExtensionPoint1" beanClass="MyPlugin.MyBeanClass1">
<extensionPoint name="MyExtensionPoint2" interface="MyPlugin.MyInterface">
</extensionPoints>
插件的持久化
插件可以通过PersistentStateComponent来持久化自己的状态。
Plugin Service
IntelliJ平台确保只有一个服务实例被加载,即使该服务被多次调用。服务必须具有用于服务实例化的实现类。服务也可以有一个接口类,用于获取服务实例并提供服务的API。接口和实现类在plugin.xml文件中指定。服务同样也分为三个级别,Application Project Module。
- 插件依赖
对IntelliJ之外的jar包依赖,可以通过Gradle添加jar包依赖。
对应其他插件的依赖,包括IDE捆绑插件,第三方或你自己的插件。
需要在描述文件中添加依赖声明:
<depends>org.jetbrains.xxxx</depends>
这里要提一下插件的ClassLoader,IntelliJ默认为每个插件创建了自己的ClassLoader,这样的好处是各个插件相互隔离互不影响,可以在不同的插件组使用同一jar包的不同版本。带来的影响是在你的插件中就不能直接调用其他插件中的类。<depends>
元素来指定插件依赖于一个或多个其他插件。在这种情况下,ClassLoader在当前插件中找不到的类时,会去依赖的插件的ClassLoader中查找。
- 发布插件 见下文
创建插件工程
官方提供了两种方法来创建插件工程,分别说一下优缺点。
使用DevKit 进行创建
集成度高,使用简单,只能启动单个IDE进行调试。
使用Gradle方式创建
可以自定义编译流程,非常灵活,可以启动多个IDE进行调试,修改IntelliJ支持版本比较方便。
建议小工程可以直接使用DevKit,复杂工程用Gradle方式创建。
创建Action
开发者可以通过插件向菜单和工具栏添加新项目来自定义IntelliJ平台UI。IntelliJ平台提供了AnAction类,每当点击菜单项或者工具栏按钮时,就调用其actionPerformed方法。要在IntelliJ平台中创建Action,应该执行以下步骤,在你的插件中定义一个Action或一系列Action并添加到菜单和工具栏中,注册自己Action。
定义Action
开发者想要自定义一个Action必须从AnAction类派生。在插件中创建一个从AnAction类派生的Java类。在这个类中覆盖actionPerformed方法,当点击菜单项或工具栏按钮时这个方法会被调用。
注册Action
一旦开发者定义了一个Action或一个Action系统,必须把他们注册到指定的菜单项或工具栏按钮上。开发者可以通过以下方式之一进行注册,在plugin.xml
文件的<actions>
部分中注册,或者从Java代码注册。
Devkit提供了创建Action的向导,开发者可以通过可视化的方法进行创建,最终得到的结果和我们手动创建的相同。
经过对IntelliJ源代码的研究,发现可以从EditorAction来进行继承,EditorAction继承自AnAction,在此基础上封装了很多和编辑器相关的功能。EditorAction需要借助EditorWriteActionHandler实现对应的业务逻辑处理。
public class SuperEnterAction extends EditorAction {
public SuperEnterAction() {
super(new Handler());
}
private static class Handler extends EditorWriteActionHandler {
public Handler() {
super(true);
}
@Override
public boolean isEnabledForCaret(@NotNull Editor editor, @NotNull Caret caret, DataContext dataContext) {
return getEnterHandler().isEnabled(editor, caret, dataContext);
}
@Override
public void executeWriteAction(Editor editor, Caret caret, DataContext dataContext) {
}
}
}
注册Action到系统,位置在META-INF\plugin.xml
<actions>
<!-- Add your actions here -->
<action id="super_enter" class="com.jd.tools.SuperEnterAction" text="智能助手" description="智能完成代码中的补齐和换行和格式化操作">
<add-to-group group-id="EditorActions" anchor="first" />
<keyboard-shortcut keymap="$default" first-keystroke="ctrl ENTER"/>
</action>
</actions>
业务分支逻辑解耦
考虑到代码补齐的情况非常多,后续还有可能修改或者新增需求,如果直接用if else
来进行业务区分,长串的代码会导致代码的业务逻辑混乱,后续难易读懂和扩展,考虑用职责链模式进行封装,将每种情况分交给对应的处理类,并将处理类注册到职责链中,在解耦的同时业务逻辑也更加清晰。
职责链的设计
对职责链的handler抽象如下:
public interface IEnterHandler {
/**
* 是否符合该处理类的处理范围
* @param event 当前的事件触发详情
* @return 是否符合
*/
boolean isEnabledForCase(EnterEvent event);
/**
* 执行具体的处理逻辑
* @param event 当前的事件触发详情
* @return 是否阻止系统的事件传递
*/
boolean execute(EnterEvent event);
}
对业务逻辑进行归纳后,提出三个基础类:
OnBlockEndEnterHandler
: 代码编写结束时的跳转和删除空行操作;
BaseAppendBracketHandler
: 智能补全{}
操作;
BaseAppendSemicolonHandler
: 智能补全 ;
操作。
需要补全;
的情况包括return 返回、属性声明、变量声明、方法调用,让其处理类继承 BaseAppendSemicolonHandler
。
需要补全{}
的情况包括 函数声明、if for等控制流,类声明,其处理类均继承 BaseAppendSemicolonHandler
。
在继承之后上述基础类之后,重写自己的isEnabledForCase函数即可,至此添加和修改需求只需要选择合适的BaseHandler进行继承,重写isEnabledForCase判读是否需要触发即可,连续if判断的代码被彻底解决掉了。
例如,针对return的智能补齐的处理类:
public class OnReturnEnterHandler extends BaseAppendSemicolonHandler{
private static final Pattern RETURN_PATTERN = Pattern.compile("\\s*return\\s*\\w*", Pattern.DOTALL);
@Override
public boolean isEnabledForCase(EnterEvent event) {
return RETURN_PATTERN.matcher(event.getCurrentLine()).matches();
}
}
得到的最终的UML设计图如下
职责链构建和调用
职责链的初始化:根据业务的处理单元的先后顺序构造职责链,将对应的处理单元按照业务的触发顺序添加到职责链中。
private EnterManager enterManager = new EnterManager();
private void initHandlers() {
enterManager.addHandler(new OnConditionEnterHandler());
enterManager.addHandler(new OnClassEnterHandler());
enterManager.addHandler(new OnMethodDeclareEnterHandler());
enterManager.addHandler(new OnAssignEnterHandler());
enterManager.addHandler(new OnFieldEnterHandler());
enterManager.addHandler(new OnReturnEnterHandler());
enterManager.addHandler(new OnStateMentEnterHandler());
enterManager.addHandler(new OnBlockEndEnterHandler());
enterManager.addHandler(new FinalEnterHandler());
}
职责链的调用:在事件产生的位置构建EnterEvent
并将对应的事件传递给职责链,代码非常简单。
EnterEvent enterEvent = new EnterEvent();
CharSequence documentText = DocUtils.computeDescriptionMatchingPrefix(editor.getDocument(),lineEndOffset);
enterEvent.setCurrentLine(documentText.toString().trim());
enterEvent.setEditor(editor);
consumeEvent = enterManager.onEnter(enterEvent);
工具方法
在插件中还有些关键的和IntelliJ交互的方法,在查找IntelliJ开源代码后整理如下:
- 代码插入
Editor.getDocument().insertString(int offset,String str);
- 光标移动
Editor.getCaretModel().moveToOffset(int offset);
- 编辑器事件传递
EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER).execute(editor, caret, dataContext);
- 代码格式化
/**
* 封装后的方法,格式化光标所在行代码
*/
public static void formatCurrentLine(Editor editor, DataContext dataContext){
Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project!=null){
PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
editor.getSelectionModel().selectLineAtCaret();
if (file!=null) {
ReformatCodeProcessor processor = new ReformatCodeProcessor(file,editor.getSelectionModel());
processor.runWithoutProgress();
editor.getSelectionModel().removeSelection();
}
}
}
插件的调试
在插件开发中,除了调试我们的业务代码,还经常需要调试IntelliJ的源代码,以此来查看插件在IDE中的状态已经运行情况。
使用DevKit创建的工程
可以直接用Debug按钮进行调试业务代码。
调试IntelliJ源码的时候,需要给工程所依赖的IDEA_SDK关联对应的源代码才可以调试。使用Gradle创建的工程
需要调试的Task为
intellij\runIde
或intellij\runIdea
,
在执行runIde之前Gradle会下载所依赖的IDEA对应的版本的源代码,可以在IDE中使用右键菜单中的Go To | Declaration
找到自己所继承的基类源码。也可以用通过双击Shift
调用Search Everywhere
工具直接查找源码并设置断点。
总的来说,使用Gradle创建的工程在源代码调试上更为方便,切换依赖库也更为方便。
插件的发布
插件开发完成之后需要发布对应的可执行文件才可以给其他人使用。针对不同的插件可执行包分为两类
- 插件结构
对于未使用IntelliJ之外的库依赖的插件,会生成单个jar包的执行文件:
.IntelliJIDEAx0/
plugins/
sample.jar/
com/foo/...
...
...
META-INF/
plugin.xml
对于依赖了外部jar包的插件,多个jar包会被封装成zip包的执行文件:
.IntelliJIDEAx0/
plugins/
Sample/
lib/
libfoo.jar
libbar.jar
Sample.jar/
com/foo/...
...
...
META-INF/
plugin.xml
DevKit发布
使用顶部菜单
Build | Prepare Plugin Module 'xxx' For Deployment
Gradle发布
针对插件Gradle封装了对应IntelliJ的七个任务,分别是:
buildPlugin
:构建插件包;
patchPluginXml
:修改插件包的描述文件;
prepareSandbox
:准备IDEA运行的沙箱环境;
prepareTestingSandbox
:准备IDEA运行的测试沙箱环境;
publishPlugin
:发布插件到IntelliJ插件仓库;
runIde
:在IDEA中运行插件;
runIdea
:在IDEA中运行插件。
其中开发者可以对buildPlugin
和patchPluginXml
扩展实现对打包流程的控制。
五、插件的使用效果
该插件可以使用在整个类的编写周期之中,使用方法统一为
Ctrl+Enter
!
在类声明和属性声明中的使用
在方法声明和调用的使用
在控制流中的使用
- 在赋值表达式中的使用
六、延伸阅读
关于代码情景判断,也可以用光标所在PSI语法树中的节点位置进行判断。
关于代码的插入,也可以用构造PSI语法树的方法直接往PSI中插入节点。
关于职责链,也可以用插件系统的扩展和扩展点系统进行重写。
有兴趣可以进一步研究一下。