前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >IDEA 插件开发实战

IDEA 插件开发实战

作者头像
CSDN技术头条
发布2020-02-19 11:36:27
2.5K0
发布2020-02-19 11:36:27
举报
文章被收录于专栏:CSDN技术头条

一. 简介

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 框架结构

代码语言:javascript
复制
.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 文件中获取

代码语言:javascript
复制
File ioFile = new File("./io.java")
VritualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(ioFile)
virtualFile.refresh(false, true)

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

代码语言:javascript
复制
WriteCommandAction.runWriteCommandAction(project, new Runnable() {
     @Override
    public void run() {
    //   virtualFile.getInputStream() / virtualFile.getOutputStream()         
    }
 });

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

代码语言:javascript
复制
ApplicationManager.getApplication().invokeLater(new Runnable(){ 
 ...
})

PSI

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

常用子类:PsiDirectory、PsiJavaFile 和 XmlFile。

创建目录和文件:

代码语言:javascript
复制
//创建目录
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 文件类:

代码语言:javascript
复制
PsiClass clazz =JavaDirectoryService.getInstance().createClass(subDir, className)
//还有通过 freemarker 模板建立 class 类。

psiClass 类中添加接口:

代码语言:javascript
复制
PsiClass view = myFactory.createInterface("View");
psiClass.add(view);

设置包名:

代码语言:javascript
复制
PsiJavaFile javaFile = (PsiJavaFile) psiClass.getContainingFile();
PsiPackage psiPackage = myDirectoryService.getPackage(directory);
javaFile.setPackageName(psiPackage.getQualifiedName());

设置类权限:

代码语言:javascript
复制
psiClass.getModifierList().setModifierProperty(PsiModifier.PUBLIC,true);

四.实例架构

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

五.准备工作

创建插件项目:

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

六.编码

6.1 组成

总共有几个部分组成。

BaseAnAction

AnActionEvent 一些基本信息。

代码语言:javascript
复制
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

代码语言:javascript
复制
应用管理。
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

代码语言:javascript
复制
#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 接口和实现类。

代码语言:javascript
复制
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 类名称。

代码语言:javascript
复制
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 插件开发介绍完毕,这个可以基于模板快速拓展,有兴趣的朋友可以尝试下,毕竟授人以鱼不如授人以渔,自动化是工程师文化的一个重要体现。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-02-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GitChat精品课 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一. 简介
  • 二.原理
    • 2.1 背景
      • 2.2 基本原理
        • 2.3 小结
        • 三.API
          • 3.1 框架结构
            • 3.2 常用 API 介绍
            • 四.实例架构
            • 五.准备工作
            • 六.编码
              • 6.1 组成
                • 6.2 GitHub项目地址
                • 七.部署
                • 八.总结
                相关产品与服务
                云数据库 MySQL
                腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档