专栏首页CSDN技术头条IDEA 插件开发实战

IDEA 插件开发实战

一. 简介

IntelliJ IDEA 是一款开发工具,提供很多插件功能,比如阿里规范插件(Alibaba Java Coding Guidelines),但是随着日常业务展开,很多工作重复性编码,浪费很多时间,需要自定义抽象出来一些插件,自动化的方式解决问题,这也是工程师文化的体现。

二.原理

2.1 背景

IntelliJ 平台是开源的,基于 Apache 许可协议,提供很多丰富的工具,提供组件驱动,基于跨平台 JVM,可以在创建菜单栏、列表、弹出菜单、对话框等等。可以适用于多种语言,提供相关解析器和 PSI 模型,解析文件,构建语义模型。

2.2 基本原理

组件模型

负责生命周期管理以及连接组件之间的相互依赖关系。

  • Application level components,在 IDEA 启动的时候创建和初始化,可以使用 getComponent(Class) 获取它们。
  • Project level components,在 IDEA 中每个 Project 实例创建的,甚至可以为未打开的项目创建组件,可以使用 getComponent(Class)方法从 Project 实例中获取它们。
  • Module level components,它们是为 IDEA 中加载的每个项目中每个模块创建,使用 getComponent(Class)方法可以从 Module 实例获取模块级别组件。

生命周期:

  • 创建,调用构造函数
  • 初始化,initComponent 调用该方法(如果组件实现 ApplicationComponent 接口)
  • 配置,保存和加载每个组件的状态。(PersistentStateComponent 和 JDOMExternalizable,实例化配置)。
  • 注册,对于模块组件,将调用接口的 moduleAdded 方法 ModuleComponent 将模块添加到项目中,对于项目组件,调用接口的 projectOpened 方法 ProjectComponent 加载项目。
  • 保存配置,JDOMExternalizable,PersistentStateComponent 的调用。
  • 输出,disposeComponent 调用输出。

线程模型

平台相关数据结构由读/写锁覆盖,适用于 PSI,VFS 和项目模型。允许从任何线程读取数据。从 UI 线程读取数据不需要任何特殊的工作。但是,从任何其他线程执行的读取操作都需要使用 ApplicationManager.getApplication().runReadAction()或 ReadAction.run/compute。

仅允许从 UI 线程写入数据,并且写入操作始终需要用 ApplicationManager.getApplication().runWriteAction()或 WriteAction.run()/compute()。

后台流程管理

后台进度由 ProgressManager 类管理,该类有很多方法可以使用模式(对话框),非模式(在状态栏中可见)或不可见进度来执行给定代码。在所有情况下,代码都是在与 ProgressIndicator 对象关联的后台线程上执行的。

讯息传递

平台中可用的消息传递基础结构,基于 Observer 设计模式扩展实现的,通过该模式能够更好的梳理的一对多关系,实现提供了附加功能,例如在层次结构上进行广播和特殊的嵌套事件处理(此处的嵌套事件是指从另一个事件的回调中(直接或间接)触发新事件的情况)。

2.3 小结

具体相关原理研究,可查看官网(sdk 地址)。

三.API

3.1 框架结构

.IntelliJIDEA/

└──  plugins

    └── code_plugin

        └── lib

            ├── lib_foo.jar

            ├── lib_bar.jar

            │   ...

            │   ...

            └── sample.jar

                ├── com/foo/...

                │   ...

                │   ...

                └── META-INF

                    ├── plugin.xml

                    ├── pluginIcon.svg

                    └── pluginIcon_dark.svg

└──src

├──com.code

基本的框架结构,如果要导入依赖放到 lib 文件夹中,还有另一种建立框架的方式,那个是基于 Gradle 管理。

META-INF,配置文件件,管理注册的类。

3.2 常用 API 介绍

VFS

  • 提供一个处理文件的通用 API,而不关心文件的具体位置(无论文件位于磁盘上、归档文件中还是 HTTP 服务器上)。
  • 追踪文件变化,并且在检测到文件内容发生更改时能提供新旧两个版本的文件。
  • 建立文件在 VFS 和持久化存储之间的关联。

从本地 IO 文件中获取

File ioFile = new File("./io.java")
VritualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(ioFile)
virtualFile.refresh(false, true)

对 VirtualFile 进行读写操作:和 Android 一样,Intellij Platform 不允许直接在主线程进行实时的文件写入,需要通过一个异步任务进行。

WriteCommandAction.runWriteCommandAction(project, new Runnable() {
     @Override
    public void run() {
    //   virtualFile.getInputStream() / virtualFile.getOutputStream()         
    }
 });

在异步任务结束后,切回 UI 线程进行 UI 更新:

ApplicationManager.getApplication().invokeLater(new Runnable(){ 
 ...
})

PSI

PSI(Program Structure Interface)是 Intellij Platform 中一个非常重要的概念,在 IDE 所管理的 Project 中,每个目录,Package,源代码和资源文件都会被抽象成相应的 PSI 对象。

常用子类:PsiDirectory、PsiJavaFile 和 XmlFile。

创建目录和文件:

//创建目录
PsiDirectory baseDir =PsiDirectoryFactory.getInstance(project).createDirectory(project.getBaseDir());
//创建 Java 文件
PsiJavaFile psiFile = (PsiJavaFile) PsiFileFactory.getInstance(project).createFileFromText("",StdFileTypes.JAVA, "");
//创建 Xml 文件
XmlFile psiFile = (XmlFile) PsiFileFactory.getInstance(project).createFileFromText("",StdFileTypes.XML, "");

读写文件:和写入 VirtualFile 一样,读写操作都需要在 WriteCommandAction 异步线程中进行。

创建 Class 文件类:

PsiClass clazz =JavaDirectoryService.getInstance().createClass(subDir, className)
//还有通过 freemarker 模板建立 class 类。

psiClass 类中添加接口:

PsiClass view = myFactory.createInterface("View");
psiClass.add(view);

设置包名:

PsiJavaFile javaFile = (PsiJavaFile) psiClass.getContainingFile();
PsiPackage psiPackage = myDirectoryService.getPackage(directory);
javaFile.setPackageName(psiPackage.getQualifiedName());

设置类权限:

psiClass.getModifierList().setModifierProperty(PsiModifier.PUBLIC,true);

四.实例架构

平时开发过程中,代码结构会分层,类似 MVC 思想,这里面有很多可以抽象出来的公共类,比如 JavaBean,DTO,Service 等等,我这个实例结合类似场景,实现自动化插件。

五.准备工作

创建插件项目:

还可以用 Gradle 方式创建项目,我用的 idea 版本 2019.2.4,上述内容中提到框架结构,现在可以在 src 目录中编码。

六.编码

6.1 组成

总共有几个部分组成。

BaseAnAction

AnActionEvent 一些基本信息。

public abstract class BaseAnAction extends AnAction {
    private AnActionEvent anActionEvent;
    private DataContext dataContext;
    private Presentation presentation;
    private Module module;
    private IdeView view;
    private ModuleType moduleType;
    private Project project;
    private PsiDirectory psiDirectory;
    private DialogBuilder builder;
    private PsiFile file;
    private JavaDirectoryService javaDirectoryService;
    private MysqlJdbc mysqlJdbc = MysqlJdbc.getMysqlJdbc();
    private PropertiesUtil properties = PropertiesUtil.getConfigProperties();

    public void init(AnActionEvent anActionEvent) {
        this.javaDirectoryService = new JavaDirectoryServiceImpl();
        this.anActionEvent = anActionEvent;
        IdeView ideView = (IdeView)anActionEvent.getRequiredData(LangDataKeys.IDE_VIEW);
        this.psiDirectory = ideView.getOrChooseDirectory();
        this.project = this.psiDirectory.getProject();
    }

    public PropertiesUtil getProperties() {
        return this.properties;
    }

    public MysqlJdbc getMysqlJdbc() {
        return this.mysqlJdbc;
    }

    public PsiDirectory getPsiDirectory() {
        return this.psiDirectory;
    }

    public JavaDirectoryService getJavaDirectoryService() {
        return this.javaDirectoryService;
    }

    public AnActionEvent getAnActionEvent() {
        return this.anActionEvent;
    }

    public void setAnActionEvent(AnActionEvent anActionEvent) {
        this.anActionEvent = anActionEvent;
    }

    public DataContext getDataContext() {
        return this.dataContext;
    }

    public void setDataContext(DataContext dataContext) {
        this.dataContext = dataContext;
    }

    public Module getModule() {
        return this.module;
    }

    public void setModule(Module module) {
        this.module = module;
    }

    public IdeView getView() {
        return this.view;
    }

    public void setView(IdeView view) {
        this.view = view;
    }

    public ModuleType getModuleType() {
        return this.moduleType;
    }

    public void setModuleType(ModuleType moduleType) {
        this.moduleType = moduleType;
    }

    public Project getProject() {
        return this.project;
    }

    public void setProject(Project project) {
        this.project = project;
    }

    public DialogBuilder getBuilder() {
        return this.builder;
    }

    public void setBuilder(DialogBuilder builder) {
        this.builder = builder;
    }

    public PsiFile getFile() {
        return this.file;
    }

    public void setFile(PsiFile file) {
        this.file = file;
    }

    public Presentation getPresentation() {
        return this.presentation;
    }

    public void setPresentation(Presentation presentation) {
        this.presentation = presentation;
    }

    @Override
    public void update(AnActionEvent e) {
        try {
            this.presentation = e.getPresentation();
            this.onMenuUpade(e, (PsiFile)e.getData(DataKeys.PSI_FILE), ((IdeView)LangDataKeys.IDE_VIEW.getData(e.getDataContext())).getOrChooseDirectory());
        }
        catch (Exception exception) {
            // empty catch block
        }
    }

    public void show() {
        this.presentation.setEnabled(true);
        this.presentation.setVisible(true);
    }

    public void hide() {
        this.presentation.setEnabled(false);
        this.presentation.setVisible(false);
    }

    public void onMenuUpade(AnActionEvent e, PsiFile file, PsiDirectory dir) {
    }
}

CodeComponent

应用管理。
public class CodeComponent implements ApplicationComponent {
    @Override
    public void initComponent() {

        // TODO: insert component initialization logic here

    }



    @Override
    public void disposeComponent() {

        // TODO: insert component disposal logic here

    }



    @Override
    @NotNull
    public String getComponentName() {
        if ("CreateMicroServiceProjectComponent" == null) {
            CodeComponent.reportNull(0);
        }
        return "CreateMicroServiceProjectComponent";
    }

    private static  void reportNull(int n) {
        throw new IllegalStateException(String.format("@NotNull method %s.%s must not return null", "com/code/action/CodeComponent", "getComponentName"));}}

还有一些工具类,比如操作 MySQL 数据库,操作字符串等等。一些 freemarker 模板,Action 动作。

MMS_DO.java.ft

#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
import lombok.Data;
import java.util.*;
import java.math.BigDecimal;

/**
 * @author ${USER} E-mail:${E_MAIL}
 * @version 创建时间:${DATE} ${TIME}
 *     ${doCalssName}DO 对象
 */
@Data
public class ${doCalssName}DO {
}

CreateServiceAction

创建 Service 接口和实现类。

public class CreateServiceAction extends BaseAnAction {
    @Override
    public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
        this.init(anActionEvent);
        String serviceName = Messages.showInputDialog((String)"Service name", (String)"Create service", (Icon)Messages.getInformationIcon());
        Map<String, String> param = new HashMap<String, String>();
        param.put("doCalssName", BaseUtils.firstLetterUpperCase(BaseUtils.markToHump(serviceName, "_", null)));
        param.put("tableName", serviceName);
        PsiDirectory implDir = this.getPsiDirectory().findSubdirectory("impl");
        if (implDir == null) {
            implDir = this.getPsiDirectory().createSubdirectory("impl");
        }
        PsiClass servicePsiClass = this.getJavaDirectoryService().createClass(this.getPsiDirectory(), "", "MMS_Service", false, param);
        String packagePath = servicePsiClass.getQualifiedName();
        String implT = "impl";
        param.put(implT, packagePath + ";");
        param.put("serviceName", serviceName);
        this.getJavaDirectoryService().createClass(implDir, "", "MMS_ServiceImpl", false, param);
    }
}

6.2 GitHub项目地址

项目内容放到 GitHub 中,地址:code_plugin

项目中缺失 jar,有些依赖得自行下载,

七.部署

在 code_plugin 项目鼠标右击,或者 build 点击 Prepare Plugin Module '插件名称(codeplugin)' For Deployment 生成插件包(zip/jar)。

在 IDEA 文件夹,File->Settings->Plugins->Install Plugin from Disk,安装打出插件,查看目录,重启。

导入插件

在这里插入图片描述

效果展示

插件位置

项目,鼠标右击,新建 New,有 CreateDO、CreateDTO、CreateService 三个功能窗口。

创建 DO

这个实体是跟 MySQL 业务表像映射的,窗口填的是数据库表名称。

创建 DTO

DTO 是跟 DO 相映射的,符合阿里的编程规范,用于处理 Service 层业务处理,这个代码中写上包名称(com.lm.model),DO 得在特定包名下,DTO 才能映射,DTO 创建窗口,填 DO 类名称。

public class CreateDTOAction extends BaseAnAction  {
    @Override
    public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
        this.init(anActionEvent);
        String doName = Messages.showInputDialog((String)"DO name", (String)"Create DTO", (Icon)Messages.getInformationIcon());
        final PsiElementFactory factory = JavaPsiFacade.getInstance((Project)this.getProject()).getElementFactory();
        HashMap<String, String> param = new HashMap<String, String>();
        String doClassName = BaseUtils.firstLetterUpperCase(BaseUtils.markToHump(doName.substring(0, doName.length() - 2), "_", null));
        param.put("doCalssName", doClassName);
        GlobalSearchScope searchScope = GlobalSearchScope.allScope((Project)this.getProject());
        PsiPackage psiPackage = JavaPsiFacade.getInstance((Project)this.getProject()).findPackage("com.lm.model");
        PsiClass[] doPsiClasss = psiPackage.findClassByShortName(doName, searchScope);
        PsiClass doPsiClass = doPsiClasss[0];
        PsiClass dtoPsiClass = JavaPsiFacade.getInstance((Project)this.getProject()).findClass(doPsiClass.getQualifiedName(), searchScope);
        final PsiField[] psiFields = dtoPsiClass.getFields();
        final PsiClass psiClass = this.getJavaDirectoryService().createClass(this.getPsiDirectory(), "", "MMS_DTO", false, param);
        WriteCommandAction.runWriteCommandAction((Project)this.getProject(), (Runnable)new Runnable(){

            @Override
            public void run() {
                for (PsiField psiField : psiFields) {
                    String comment = psiField.getDocComment().getText().replaceAll("\\*", "").replaceAll("/", "").replaceAll(" ", "").replaceAll("\n", "");
                    StringBuffer fieldStrBuf = new StringBuffer(psiField.getDocComment().getText()).append("\nprivate ").append(psiField.getType().getPresentableText()).append(" ").append(psiField.getName()).append(";");
                    psiClass.add((PsiElement)factory.createFieldFromText(fieldStrBuf.toString(), (PsiElement)psiClass));
                }
            }
        });
    }}

八.总结

总体的 IDEA 插件开发介绍完毕,这个可以基于模板快速拓展,有兴趣的朋友可以尝试下,毕竟授人以鱼不如授人以渔,自动化是工程师文化的一个重要体现。

本文分享自微信公众号 - GitChat精品课(CSDN_Tech),作者:李孟

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 动手编写你的第一个 Flutter 应用

    我将带领大家尝试编写一个 Flutter 应用,感受一下 Flutter 开发的语法特点和运行效率。

    CSDN技术头条
  • TIOBE 2015年12月编程语言排行榜:Java正处巅峰

    TIOBE 2015年12月编程语言发布了,毫无疑问,Java将成为2015年的年度语言。 在Top10榜单中,另一个引入注目的是则属Python,其份额在持续...

    CSDN技术头条
  • 程序员等电梯时竟然想这事儿

    今天就为大家科普一下电梯调度算法,为在等电梯之余,打发时间做出一点贡献。(电梯调度算法可以参考各种硬盘换道算法,下面内容整理自网络)

    CSDN技术头条
  • java学习:调用 java web service

    先写一个java的class:AwbModel(相当于要在web service中传输的实体对象) package webservicesample; pub...

    菩提树下的杨过
  • 用netty 3的channelbuffer来重写序列化类

    我们都知道用java来序列化一个对象,需要用到ObjectOutputSteam来把对象写进一个字节流ByteOutputStream,然后把字节流转成字节数组...

    算法之名
  • 阅读开源框架,总结Java类的定义

    即使我们明白Java的类,也未必清楚该如何正确地定义一个Java类。阅读一些开源框架的源代码,会启发我们灵感,并给出好代码的规范,提炼设计原则与模式。

    张逸
  • 建造者模式浅析

    建造者模式是一种创建型的模式,其意图是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

    孟君
  • 宠物商店

    葆宁
  • JAVA设计模式-策略模式

    策略模式:定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户

    DH镔
  • java中复制对象通过反射或序列化

    在使用缓存读取数据后修改发现缓存被修改。于是找了下复制对象的方法。 关于对象克隆 ---- 按我的理解,对象是包含引用+数据。通常变量复制都是将引用传递过去。比...

    Ryan-Miao

扫码关注云+社区

领取腾讯云代金券