插件开发
项目中 Entity对象增加字段后 DTO,VO等都需要增加相应的字段, 这些重复性的代码太讨厌了, 遂写个插件解决一劳永逸, 自己动手, 丰衣足食!
前置条件
Use the following checklist to ensure that you are ready to develop your custom plugins.
- Plugin DevKit plugin must be enabled in IntelliJ IDEA.
- IntelliJ Platform SDK must be configured for your IDEA project.
1. Plugin DevKit
Plugin DevKit is a bundled IntelliJ IDEA plugin for developing plugins for the IntelliJ Platform using IntelliJ IDEA’s build system. It provides its custom SDK type and a set of actions for building plugins within the IDE.
需安装启用IntelliJ IDEA 自带的Plugin DevKit插件
2. IntelliJ Platform SDK
启用 Plugin DevKit 后, 还需要配置 IntelliJ Platform SDK, 可以在 File -< Project Structure > SDKs + 指定到idea的安装目录即可
-
关于 SandBox 选项 IntelliJ IDEA 插件以 Debug/Run 模式运行时是在 SandBox 中进行的, 不会影响当前的 IntelliJ IDEA; 但是同一台机器同时开发多个插件时默认使用的同一个 sandbox
-
关于源码调试 需要下载对应分支的社区版源码active, 附加到 Sourcepath github idea 社区版源码
插件的主要类型 Main Types of Plugins
Products based on the IntelliJ Platform can be modified and adjusted for custom purposes by adding plugins. All downloadable plugins are available from the JetBrains Marketplace.
The most common types of plugins include:
- UI Themes
- Custom language support
- Framework integration
- Tool integration
- User interface add-ons
插件的配置文件 Plugin Content
Plugin Content plugin-configuration-file
Plugin distribution will be built using Gradle or Plugin DevKit. The plugin jar file must contain:
- the configuration file (META-INF/plugin.xml) (Plugin Configuration File)
- the classes that implement the plugin functionality
- recommended: plugin logo file(s) (META-INF/pluginIcon*.svg) (Plugin Logo)
META-INF/plugin.xml插件的核心配置文件, 指定插件名称, 描述, 版本号, 支持的 IntelliJ IDEA 版本, 插件的 components 和 actions 以及软件商等信息
<idea-plugin>
<id>org.yangfh.idea.iframetools</id>
<name>iFrameTools</name>
<version>1.0</version>
<vendor email="718556228@qq.com" url="">yangfh</vendor>
<description><![CDATA[
深农院-框架工具插件:
1. 实体新增字段自动补充其 DTO,ListDTO, Mapper代码(目前仅支持Java基本类型属性);
]]></description>
<change-notes><![CDATA[
build on 2022-03-21 ver 1.0: 第一个版本
]]>
</change-notes>
<!-- please see https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html for description -->
<idea-version since-build="203.0"/>
<!-- please see https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html
on how to target different products -->
<depends>com.intellij.modules.platform</depends>
<!-- 同时需要引入,否则2020版本找不到 com.intellij.psi -->
<depends>com.intellij.modules.lang</depends>
<depends>com.intellij.modules.java</depends>
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
</extensions>
<actions>
<!-- Add your actions here -->
<action id="iframetools.main" class="org.yangfh.idea.iframetools.actions.MainAction" text="iFrame-实体字段代码补充">
<add-to-group group-id="CodeMenu" relative-to-action="Generate" anchor="before"/>
</action>
</actions>
</idea-plugin>依赖可以选项 depends
Module or Plugin for <depends> Element | Functionality | Product Compatibility |
|---|---|---|
com.intellij.modules.java or com.intellij.javaSee Java below. | Java language PSI Model, Inspections, Intentions, Completion, Refactoring, Test Framework | IntelliJ IDEA, Android Studio |
com.intellij.modules.androidstudio | Android SDK Platform, Build Tools, Platform Tools, SDK Tools | Android Studio |
com.intellij.modules.cidr.lang | C, C++, Objective-C/C++ language PSI Model, Swift/Objective-C Interaction, Inspections, Intentions, Completion, Refactoring, Test Framework | AppCode, CLion |
com.intellij.modules.cidr.debugger | Debugger Watches, Evaluations, Breakpoints, Inline Debugging | AppCode, CLion, RubyMine |
com.intellij.modules.appcode or com.intellij.appcodeSee AppCode/CLion below. | Xcode Project Model, CocoaPods, Core Data Objects, Device & Simulator Support | AppCode |
com.intellij.modules.clion or com.intellij.clionSee AppCode/CLion below. | CMake, Profiler, Embedded Development, Remote Development, Remote Debug, Disassembly | CLion |
com.intellij.cidr.base | Native Debugger Integration, Utility Classes, C/C++ Project Model/Workspace Support (OCWorkspace, CidrWorkspace, etc.), C/C++ Build and Run Support | AppCode, CLion |
com.intellij.database | Database Tools and SQL language PSI Model, Inspections, Completion, Refactoring, Queries | DataGrip, IntelliJ IDEA Ultimate, AppCode, PhpStorm, PyCharm Professional, RubyMine, CLion, GoLand, Rider, and WebStorm if the Database Tools and SQL plugin is installed. |
org.jetbrains.plugins.go | Go language PSI Model, Inspections, Intentions, Completion, Refactoring, Test Framework | GoLand |
com.intellij.modules.python | Python language PSI Model, Inspections, Intentions, Completion, Refactoring, Test Framework | PyCharm, and other products if the Python plugin is installed. |
com.intellij.modules.rider | Connection to ReSharper Process in Background | Rider |
com.intellij.modules.ruby | Ruby language PSI Model, Inspections, Intentions, Completion, Refactoring, Test Framework | RubyMine, and IntelliJ IDEA Ultimate if the Ruby plugin is installed. |
com.intellij.modules.ultimate | Licensing | All commercial IDEs (IntelliJ IDEA Ultimate, PhpStorm, DataGrip, …) |
com.intellij.swift | Swift language PSI Model, Inspections, Intentions, Completion, Refactoring, Test Framework | AppCode, CLion |
com.jetbrains.php | PHP language PSI Model, Inspections, Intentions, Completion, Refactoring, Test Framework | PhpStorm, and other products if the PHP plugin is installed. |
JavaScript | JavaScript language PSI Model, Inspections, Intentions, Completion, Refactoring, Test Framework | WebStorm, and other products if the JavaScript plugin is installed. |
项目结构
├─.idea
├─out 编译输出
│ └─production
│ └─iFrameTools
├─resources
│ └─META-INF
└─plugin.xml 插件配置文件
└─src java 源码
└─org
└─yangfh
└─idea迁移至 2023 版本
新版本是默认是用 grdadle 构建, Kotlin 语言开发.
- 删除Kotlin 相关
- 删除
./src/main/kotlin文件夹 - 修改
build.gradle.kts
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.8.21" //删除
id("org.jetbrains.intellij") version "1.13.3" //删除
}
group = "org.yangfh"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
intellij {
version.set("2022.2.5")
type.set("IC") // Target IDE Platform
// 另外插件依赖 在这里需要添加, 同时 plugin.xml 也照旧定义
//例如
// plugins.set(listOf("com.intellij.java"))
plugins.set(listOf(/* Plugin Dependencies */))
}
tasks {
withType<JavaCompile> {
sourceCompatibility = "17"
targetCompatibility = "17"
}
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { //删除
kotlinOptions.jvmTarget = "17"
}
patchPluginXml {
sinceBuild.set("222")
untilBuild.set("232.*")
}
signPlugin {
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
privateKey.set(System.getenv("PRIVATE_KEY"))
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
}
publishPlugin {
token.set(System.getenv("PUBLISH_TOKEN"))
}
}代码归档
Transclude of iFrameTool.zip
PSI
IDEA中所有文件以及文件中的内容都是用 PSI 树来表示的, 比如类表示为PsiClass, 方法表示为PsiMethod, 字段表示为PsiField; 我们可以通过更改PSI来做到动态的添加字段, 方法等;
com.intellij.psi.PsiFileFactory: 文件相关操作, e.g.创建文件等;
com.intellij.psi.PsiElementFactory: 元素相关操作, e.g.创建java方法, 注解, 字段, 构造方法等;
com.intellij.psi.PsiManager: 项目访问PSI服务的主要入口点, e.g.查找文件, 查找文件夹等;
com.intellij.psi.PsiClass: 在java类查找元素, e.g.查找方法, 字段, 注解;
com.intellij.psi.JavaPsiFacade: java元素查找等操作, e.g.查找类等;
PSI操作 简单的遍历
Project project = e.getProject();//当前项目
PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);//当前PSI文件 (类似一个.java文件)
for (PsiElement psiElement: file.getChildren()) {//变量所有子 (.java 文件, 包括: 包名, 类名, 字段, 方法...)
if(psiElement instanceof PsiClass) {
PsiClass clazz = (PsiClass) psiElement;//找到类 PSI
//
// clazz.findFieldByName()
// clazz.findMethodsByName();
return clazz;
}
}
PSI操作 创建字段, 方法, 注释..
PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
psiElementFactory.createCommentFromText("这是个注释" ,psiDTOClass);//创建注释
psiElementFactory.createConstructor//构造函数
psiElementFactory.createField()//字段
psiElementFactory.createCodeBlock()//代码块PSI操作 给某个方法插入代码块
JvmMethod [] toDtoMethods = clazz.findMethodsByName("toDto");
PsiElement toDtosourceElement = toDtoMethods[0].getSourceElement();//Method psi
PsiElement sourceBlock= toDtosourceElement.getLastChild();//相当于 中括号psi
PSI操作 数组字段追加
//selectField = {"code", "xxx"}
PsiElement[] children = selectField.getChildren();
PsiElement value= children[children.length-2];
value.add(
psiElementFactory.createExpressionFromText("\"name\"",selectField)//字符串字面量 "name"
);
PSI操作 创建表达式
psiElementFactory.createExpressionFromText(" int i = 5; ",selectField.getLastChild())
PSI操作 空行? 代码格式
Whitespaces and Imports
When working with PSI modification functions,you should never create individual whitespace nodes (spaces or line breaks) from the text.
Instead, all whitespace modifications are performed by the formatter, which follows the code style settings selected by the user.
Formatting is automatically performed at the end of every command, andif you need, you can also perform it manually using the reformat(PsiElement) method in the CodeStyleManager class.
实测, 毛用也没有, 格式化基本废了
Actions
Actions
The IntelliJ Platform provides the concept of actions. An action is a class derived from AnAction, whose actionPerformed() method is called when its menu item or toolbar button is selected.
com.intellij.openapi.actionSystem.AnAction 是所有 Action的基类;
update 函数
有时候我们定义的插件只在某些场景中才可以使用, 比如说我们编写自动生成代码的插件时, 只有当文件打开且是相应的类型时才能正常执行; 如果不符合条件, 就应该将插件按钮置为不能点击;
//实例 如果当前编辑器是java文件则Action可用, 否则不可用
@Override
public void update(@NotNull AnActionEvent e) {
Project project = e.getProject();
Editor editor = e.getData(CommonDataKeys.EDITOR);//当前 PSI编辑器
if (editor == null){
e.getPresentation().setEnabled(false);// 设置当前 action 菜单的可用性
//e.getPresentation().setVisible(false);// 设置当前 action 菜单的可见性
//e.getPresentation().setEnabledAndVisible(false);// 同时设置当前 action 菜单的 可见性和可用性
return ;
}
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());//当前编辑 PSI文件
if("JAVA".equals(psiFile.getFileType().getName())){
e.getPresentation().setEnabled(true);
}else{
e.getPresentation().setEnabled(false);
}
}actionPerformed 函数
在菜单栏中点击我们定义的action时, 就会执行具体Action类中的actionPerformed函数; 当回调actionPerformed()方法时, 就相当于当前的Action被点击了一次;
实例 如果当前编辑器, 以当前为Entity类, 查找其DTO类添加相同的字段
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getProject();
Editor editor = e.getData(CommonDataKeys.EDITOR);//当前编辑PSI
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());//当前编辑文件
String fileName = psiFile.getName();
String className = fileName.substring(0, fileName.length() - 5);
//当做实体类
PsiClass entityPsi = getPsiClassByFile(psiFile);
//查找其DTO
PsiClass entityDTOPsi = getPsiClassByName(className+"DTO", project);
if(entityDTOPsi==null)return;
final PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
final PsiField[] enityFields = entityPsi.getAllFields();
WriteCommandAction.runWriteCommandAction(project, () -> {
for (int i = 0; i < enityFields.length; i++) {
PsiField enityField = enityFields[i];
PsiField enityFieldCopy = psiElementFactory.createField(enityField.getName(), enityField.getType());
//给DTO 添加字段
entityDTOPsi.add( enityFieldCopy);
System.out.println(" entityDTOPsi add "+ enityFieldCopy);
}
});
}
public static final PsiClass getPsiClassByName(String name, Project project){
PsiShortNamesCache psiShortNamesCache = PsiShortNamesCache.getInstance(project);//缓存查找实例
PsiClass[] psiClasss = psiShortNamesCache.getClassesByName(name, GlobalSearchScope.projectScope(project));
if(psiClasss.length > 0){
return psiClasss[0];
}else{
return null;
}
}
public static final PsiClass getPsiClassByFile(PsiFile file){
for (PsiElement psiElement: file.getChildren()) {
if(psiElement instanceof PsiClass) {
PsiClass clazz = (PsiClass) psiElement;
return clazz;
}
}
log.warn(" PsiFile "+file.getName() +", not found PsiClass!");
return null;
}Determining the Action Context
The AnActionEvent object passed to update() carries information about the current context for the action. Context information is available from the methods of AnActionEvent,
providing information such as the Presentation and whether the action is triggered by a Toolbar.
Additional context information is available using the method AnActionEvent.getData().
Keys defined in CommonDataKeys arepassed to the getData() method to retrieve objects such as Project, Editor, PsiFile, and other information.
Accessing this information is relatively light-weight and is suited for AnAction.update().
附加上下文的信息可以通过 AnActionEvent.getData() 获取到
//例如 获取 PsiFile
PsiFile data = e.getData(CommonDataKeys.PSI_FILE);
插件入口: 扩展idea 自带的 Generate…
扩展 idea 自带的 Code → Generate… 选项 Action;
看代码, 整体是分开 Action 和 Handler 两个对象来处理
Action
Action 的核心代码类 com.intellij.codeInsight.generation.actions.GenerateGetterSetterBaseAction
对话框显示, 在 GenerateMembersHandlerBase::invoke 函数里面, 调用 chooseOriginalMembers 显示选择对话框并返回选择的成员字段
final ClassMember[] members = chooseOriginalMembers(aClass, project, editor);Handler
Handler 的核心代码类 com.intellij.codeInsight.generation.GenerateGetterSetterHandlerBase
对话框显示的字段会根据Handler的 GenerateMembersHandlerBase::generateMemberPrototypes 方法返回的信息过滤显示显示;
实例代码
so 根据其套路:
创建 Action 继承com.intellij.codeInsight.generation.actions.GenerateGetterSetterBaseAction
public class MainAction extends GenerateGetterSetterBaseAction {
private static final Logger log = Logger.getInstance(AnAction.class);
public MainAction() {
super(new MainActionHandle("标题!!!"));
}
...
}
创建一个 Handler 继承 com.intellij.codeInsight.generation.GenerateGetterSetterHandlerBase
复制 GenerateGetterHandler 的 generateMemberPrototypes 函数代码
public class MainActionHandle extends GenerateGetterSetterHandlerBase {
public MainActionHandle(@NlsContexts.DialogTitle String chooserTitle) {
super(chooserTitle);
}
@Override
protected @NlsContexts.HintText String getNothingFoundMessage() {
return " NothingFound ?";
}
@Override
protected GenerationInfo[] generateMemberPrototypes(PsiClass aClass, ClassMember original) throws IncorrectOperationException {
if (original instanceof PropertyClassMember) {
final PropertyClassMember propertyClassMember = (PropertyClassMember)original;
final GenerationInfo[] getters = propertyClassMember.generateGetters(aClass);
if (getters != null) {
return getters;
}
} else if (original instanceof EncapsulatableClassMember) {
final EncapsulatableClassMember encapsulatableClassMember = (EncapsulatableClassMember)original;
final GenerationInfo getter = encapsulatableClassMember.generateGetter();
if (getter != null) {
return new GenerationInfo[]{getter};
}
}
return GenerationInfo.EMPTY_ARRAY;
}
@Override
protected String getHelpId() {
return null;//去掉 help 图标
}
....插件开发 实例
实体添加新字段后, 查找DTO 添加字段, 查找ListDTO插入字段和, 查找Mapper toDto方法插入Get, Set代码
- 选择对话框
public void showDialog(@NotNull final Project project, @NotNull final Editor editor, @NotNull PsiFile file){
if (!EditorModificationUtil.checkModificationAllowed(editor)) return;
if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), project)) {
return;
}
final PsiClass aClass = OverrideImplementUtil.getContextClass(project, editor, file, false);
if (aClass == null || aClass.isInterface()) return;
log.assertTrue(aClass.isValid());
log.assertTrue(aClass.getContainingFile() != null);
try {
//弹窗&返回选择的字段
final ClassMember[] members = chooseOriginalMembers(aClass, project, editor);
if (members == null) return;
List<PsiFieldMember> chooses = Arrays.asList(members).stream().map(e -> (PsiFieldMember) e)
.collect(Collectors.toList());
process(project, aClass, chooses);
}catch (Exception e){
e.printStackTrace();
}
}
protected void process(final Project project,
final PsiClass entity, final List<PsiFieldMember> members){
WriteCommandAction.runWriteCommandAction(project, () -> {
LocalDateTime localTime = LocalDateTime.now();
processDTO(project, entity, members, localTime, "");//处理 DTO
processListDTO(project, entity, members, localTime, "");//处理 ListDTO
processMapper(project, entity, members, localTime, "");//处理 Mapper
});
}- 处理 DTO
String className = entityPsi.getName();
PsiClass entityDTOPsi = PsiElementUtils.getPsiClassByName(className+"DTO", project);
if(entityDTOPsi==null)return;
PsiElement clsElement = entityDTOPsi.getSourceElement();
final PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
for (Iterator<PsiFieldMember> iterator = members.iterator(); iterator.hasNext(); ) {
PsiFieldMember next = iterator.next();
PsiField element = next.getElement();
PsiField enityFieldCopy = psiElementFactory.createField(element.getName(), element.getType());
//字段注释
PsiComment commentForFidld = psiElementFactory.createCommentFromText("//追加于: "+lTime,entityDTOPsi);
enityFieldCopy.add( commentForFidld);
PsiElement anchor= PsiElementUtils.getLastPsiField(clsElement);
//添加字段
clsElement.addAfter(enityFieldCopy, anchor);
}- 处理 ListDTO
String className = entityPsi.getName();
PsiClass entityListDTOPsi = PsiElementUtils.getPsiClassByName(className+"ListDTO", project);
PsiField selectField = entityListDTOPsi.findFieldByName("SELECT", false);//String[] SELECT 字段
if(entityListDTOPsi==null)return;
if(selectField==null)return;
PsiElement clsElement = entityListDTOPsi.getSourceElement();
final PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
//PsiType stringType = psiElementFactory.createTypeFromText("java.lang.String", null);
for (Iterator<PsiFieldMember> iterator = members.iterator(); iterator.hasNext(); ) {
PsiFieldMember next = iterator.next();
PsiField element = next.getElement();
String fieldName = element.getName();
PsiField enityFieldCopy = psiElementFactory.createField(fieldName, element.getType());
//字段注释
PsiComment commentFromText = psiElementFactory.createCommentFromText("//追加于: "+lTime,
enityFieldCopy);
enityFieldCopy.add(commentFromText);
//添加字段
PsiElement anchor= PsiElementUtils.getLastPsiField(clsElement);
entityListDTOPsi.addAfter(enityFieldCopy, anchor);
//给select 添加
PsiElement[] children = selectField.getChildren();
PsiElement value= children[children.length-2];
value.add(
psiElementFactory.createExpressionFromText("\""+fieldName+"\"",selectField)//字符串字面量
);
}- 处理 Mapper
String className = entityPsi.getName();
PsiClass entityMappPsi = PsiElementUtils.getPsiClassByName(className+"Mapper", project);
JvmMethod[] toDtoPropsFound = entityMappPsi.findMethodsByName("toDtoProps");
JvmMethod[] toEntityPropsFound = entityMappPsi.findMethodsByName("toEntityProps");
JvmMethod toDtoProps = toDtoPropsFound[0];
JvmMethod toEntityProps = toEntityPropsFound[0];
final PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
PsiElement toDtosourceElement = toDtoProps.getSourceElement();
PsiElement toEntitysourceElement = toEntityProps.getSourceElement();
//分号
PsiStatement statementSemicolon = psiElementFactory.createStatementFromText(";", null);
String lTime = localTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
boolean isAddComment = false;
//注释
PsiComment comment = psiElementFactory.createCommentFromText("//追加于: "+lTime,null);
for (Iterator<PsiFieldMember> iterator = members.iterator(); iterator.hasNext(); ) {
PsiFieldMember next = iterator.next();
PsiField element = next.getElement();
String fieldName = element.getName();
String capFieldName = PsiElementUtils.captureName(fieldName);
// to dto
PsiElement sourceBlock= toDtosourceElement.getLastChild();//中括号
PsiElement anchor = PsiElementUtils.getReturnPsiElement(toDtoProps);// RETURN
String expToDtoString = String.format("giveDto.set%s( giveEntity.get%s() )", capFieldName, capFieldName);
PsiExpression expToDto = psiElementFactory.createExpressionFromText(expToDtoString, toDtosourceElement);
expToDto.add(statementSemicolon);// ;
if (!isAddComment) {
sourceBlock.addBefore( comment, anchor);
}
sourceBlock.addBefore( expToDto, anchor);
// to ent
...
}代码归档
踩坑指南
Run Plugin 错误 accessible: module java.desktop does not “opens javax.swing.text.html”
又是JDK9模块化后的反射问题, 添加--illegal-access, --add-opens 之类的参数也没用..
正解是使用在 project structure 添加并使用IDEA自带的那个JDK: ./IntelliJ IDEA Community Edition 2021.2.3/jbr
idea 插件的各种类定义错误 ClassDefFoundError
这个一般是由于idea版本不兼容出现的, 在高版本的idea中需要手动加载依赖; 一般解决的方法是直接在 plugin.xml 中添加 <depends>
<!-- 依赖模块 -->
<depends>com.intellij.modules.platform</depends>
<!-- 同时需要引入,否则2020版本找不到 com.intellij.psi -->
<depends>com.intellij.modules.lang</depends>
<depends>com.intellij.modules.java</depends>
版本更新 破坏API 更改列表
https://plugins.jetbrains.com/docs/intellij/api-changes-list.html
插件开发的模板项目
官方还提供 插件的项目模版
IntelliJ Platform Plugin Template SDK 的代示例 IntelliJ Platform Plugin Template is a repository that provides a pure boilerplate template to make it easier to create a new plugin project using the recommended Gradle setup.