IntelliJ IDE 插件开发 | (十二)自定义项目脚手架(上)

news2024/12/26 17:38:35

系列文章

本系列文章已收录到专栏,交流群号:689220994,也可点击链接加入。

前言

在开发创建一个新项目的时候,我们一般都会使用平台自带的脚手架,如下图所示:

image-20240914163053383

或者是使用网页版:

image-20240914163144526

尽管平台已经提供了灵活的配置项,甚至是可以修改原有的模板内容,例如创建插件的plugin.xml的模板:

image-20240914163318214

但是我们并没有办法增删脚手架所创建的内容,也没有办法在模板中使用未提供的参数。因此,本文将介绍如何创建自己的脚手架,不过本文所介绍的方法只适用于 IDEA,对于其它平台(WebStorm、PyCharm)将在后续文章进行介绍,本文所涉及到的完整代码也已上传到GitHub。

最终效果

动画

实现思路

  1. 继承ModuleBuilder实现自己的模块创建方式。
  2. 自定义模块类型、设置面板和模板文件。
  3. 读取自定义内容创建项目目录和文件。

注册并实现自己的模块构建类

首先创建TemplateBuilder类继承ModuleBuilder类(这里先展示全部内容,后续会针对其中的方法按点进行介绍):

class TemplateBuilder: ModuleBuilder() {
    
    // 1. 持久化配置
    private val state = TemplateState.getInstance()

    // 2. 模块类型
    override fun getModuleType() = TemplateModuleType()

    // 3. 项目创建配置
    override fun setupRootModel(rootModel: ModifiableRootModel) {
        // 3.1 设置项目路径
        val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(state.location)
        rootModel.addContentEntry(virtualFile!!)
        // 3.2 读取模板并设置变量
        val template = FileTemplateManager.getInstance(rootModel.project).getJ2eeTemplate(TemplateFileFactory.PLUGIN_XML)
        val properties = Properties()
        properties.setProperty("name", state.name)
        properties.setProperty("group", state.group)
        properties.setProperty("artifact", state.artifact)
        properties.setProperty("location", state.location)
        properties.setProperty("description", state.description)
        properties.setProperty("author", state.author)
        properties.setProperty("email", state.email)
        properties.setProperty("blogUrl", state.blogUrl)
        val renderedText = template.getText(properties)
        // 3.3 创建 resources 文件夹并写入 XML 文件
        val resFile = File("${state.location}${File.separator}src${File.separator}resources")
        resFile.mkdirs()
        val file = File(resFile.absolutePath + File.separator + TemplateFileFactory.PLUGIN_XML)
        FileUtil.writeToFile(file, renderedText)
        // 3.4 设置 source 文件夹
        rootModel.contentEntries.forEach {
            val src = LocalFileSystem.getInstance().refreshAndFindFileByPath("${state.location}${File.separator}src")!!
            it.addSourceFolder(src, false)
        }
    }

    // 4. 设置项目名称
    override fun modifyProjectTypeStep(settingsStep: SettingsStep): ModuleWizardStep? {
        settingsStep.context.projectName = state.name
        return super.modifyProjectTypeStep(settingsStep)
    }

    // 5. 创建项目
    override fun createProject(name: String?, path: String?) = super.createProject(state.name, state.location)

    // 6. 设置第二个步骤
    override fun createWizardSteps(wizardContext: WizardContext, modulesProvider: ModulesProvider) =
        arrayOf(TemplateSecondStep())

    // 7. 设置第一个步骤
    override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?) = TemplateFirstStep()

    // 8. 忽略自带的步骤
    override fun getIgnoredSteps(): MutableList<Class<out ModuleWizardStep>> = mutableListOf(ProjectSettingsStep::class.java)

}

然后在plugin.xml中进行配置:

<extensions defaultExtensionNs="com.intellij">
	<moduleBuilder builderClass="cn.butterfly.template.module.TemplateBuilder"/>
</extensions>

自定义模块类型

TemplateBuilder中的第二点的getModuleType()返回了TemplateModuleType(),其中TemplateModuleType就是我们自定义的模块类型,可以用于设置模块的图标,名称以及描述:

class TemplateModuleType(id: @NonNls String = "TEMPLATE_MODULE") : ModuleType<TemplateBuilder>(id) {

    override fun createModuleBuilder() = TemplateBuilder()

    // 设置模块名称
    override fun getName() = "Template"

    // 设置描述
    override fun getDescription() = "TEMPLATE_MODULE"

    // 设置模块图标
    override fun getNodeIcon(isOpened: Boolean) = PluginIcons.TEMPLATE_ICON

}

对应效果如下:

image-20240914180629017

自定义设置面板

TemplateBuilder中的第六和七点对应我们自己的设置面板,其中第七点对应下图中的效果:

image-20240914180917679

对应的代码如下(这里只展示第七点即第一个设置面板内容,第二个界面的代码类似,在GitHub上进行查看即可),需要继承并实现ModuleWizardStep作为脚手架中的一个设置步骤:

class TemplateFirstStep: ModuleWizardStep() {
    
    private val model = Model()
    
    // 持久化配置
    private val state: TemplateState = TemplateState.getInstance()
    
    // UI 界面
    private var panel = panel {
        indent {
            row("Name: ") {
                textField().bindText(model::name)
            }.topGap(TopGap.MEDIUM)
            
            row("Location: ") {
                textFieldWithBrowseButton(
                    "",
                    ProjectManager.getInstance().defaultProject,
                    FileChooserDescriptorFactory.createSingleFolderDescriptor()
                ).bindText(model::location)
            }.topGap(TopGap.MEDIUM)
            
            row("Group: ") {
                textField().bindText(model::group)
            }.topGap(TopGap.MEDIUM)
            
            row("Artifact: ") {
                textField().bindText(model::artifact)
            }.topGap(TopGap.MEDIUM)
            
            row("Description: ") {
                textField().bindText(model::description)
            }.topGap(TopGap.MEDIUM) 
        }
        
    }
    
    override fun getComponent() = panel

    // 更新数据
    override fun updateDataModel() {
        panel.apply()
        state.name = model.name
        state.group = model.group
        state.artifact = model.artifact
        state.location = model.location
        state.description = model.description
    }
    
    // 数据存储类
    data class Model(
        var name: String = "",
        var group: String = "",
        var artifact: String = "",
        var location: String = "",
        var description: String = ""
    )

}

这里使用了Kotlin UI DSL来绘制 UI,代码比较简单,不再介绍。其中持久化配置(TemplateBuilder中的第一点也使用了该持久化配置)对应的代码如下:

@Service
@State(name = "TemplateState", storages = [Storage("template-state.xml")])
class TemplateState: PersistentStateComponent<TemplateState> {
    
    var name = ""
    
    var group = ""
    
    var artifact = ""
    
    var location = ""
    
    var description = ""
    
    var email = "1945912314@qq.com"
    
    var author = "butterfly"
    
    var blogUrl = "https://juejin.cn/user/3350967174567352"
    
    override fun getState() = this

    override fun loadState(state: TemplateState) {
        XmlSerializerUtil.copyBean(state, this)
    }
    
    companion object {
        fun getInstance(): TemplateState = ApplicationManager.getApplication().getService(TemplateState::class.java)
    }

}

需要在plugin.xml中进行配置:

<extensions defaultExtensionNs="com.intellij">
    <applicationService serviceImplementation="cn.butterfly.template.state.TemplateState"/>
</extensions>

自定义模板文件

为了方便读取和后期的可定制性,本文也参考其它脚手架项目的思路将模板文件添加到了 IDEA 自带的模板管理中(这里只介绍如何添加到模板的Other模块,详细内容可参考官网),首先在 resources 文件夹下创建fileTemplates/j2ee文件层级,然后在下面创建一个template-plugin.xml.ft(在原有文件名基础上以.ft结尾)文件用于保存模板内容,之后再创建一个TemplateFileFactory类实现FileTemplateGroupDescriptorFactory接口,用于将模板文件注册到 IDEA 中:

class TemplateFileFactory: FileTemplateGroupDescriptorFactory {
    
    companion object {
        // 上述创建模板文件去掉.ft 后缀
        const val PLUGIN_XML = "template-plugin.xml"
    }
    
    override fun getFileTemplatesDescriptor(): FileTemplateGroupDescriptor {
        // 设置模板分组
        val templateGroup = FileTemplateGroupDescriptor("Template", PluginIcons.TEMPLATE_ICON)
        // 添加模板文件
        templateGroup.addTemplate(FileTemplateDescriptor(PLUGIN_XML, PluginIcons.TEMPLATE_ICON))
        return templateGroup
    }

}

然后在plugin.xml中进行配置:

<extensions defaultExtensionNs="com.intellij">
    <fileTemplateGroup implementation="cn.butterfly.template.template.TemplateFileFactory"/>
</extensions>

然后就可以在 IDEA 中进行管理了:

image-20240914184421883

创建项目结构和文件

TemplateBuilder中的第三点即用于读取上述中的模板和配置然后用于生成项目文件,其中 3.1 用于读取第一步骤中的 location 值然后作为项目路径,3.2 即是读取上文中模板然后再根据读取到的配置完善文件内容,3.3 用于创建目录层级并将生成的文件写入到 resources 文件夹中,3.4 步骤则是手动将文件夹标记为源代码文件夹

其它细节

TemplateBuilder中的第四和五点用于手动设置项目的名称和项目路径。

TemplateBuilder中的第八点忽略步骤用于忽略掉平台自带的创建项目步骤,如果不进行忽略,就会在原有的步骤基础上多一个步骤:

image-20240914182851963

总结

本文简单介绍了如何在 IDEA 中创建属于自己的脚手架,关于自动识别模块(可在创建完项目后手动导入解决)和其它平台的脚手架创建方式将在后续文章中进行介绍,大家也可以加入开头提到的交流群一起交流讨论。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GoogleDrive中上传文件,Java整合操作

GoogleDrive使用ServiceAccount的授权方式&#xff1a;&#xff08;科学上网&#xff09; 1.在Google Cloud中查看自己的项目&#xff1a;Dashboard – My First Project – Google Cloud console&#xff0c;没有的话新建项目。默认名称&#xff1a;My First Project 2. 创建…

基于 WeChatFerry 的 Python 机器人框架WeChatRobot

WeChatRobot 一个基于 WeChatFerry 的 Python 机器人框架。 微信机器人&#xff0c;接入Gemini、ChatGPT、ChatGLM、讯飞星火、Tigerbot&#xff1b;成语接龙、天气预报、新闻摘要、定时任务 克隆项目&#xff1a; git clone https://github.com/lich0821/WeChatRobot.git …

计算机毕业设计 沉浸式戏曲文化体验系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

百年病态集论的症结:3000年不识伪≌直线段

黄小宁 公元前1100年中国人商高同周公的一段对话谈到了勾股定理说明人类认识几何学的直线段起码已有3000多年。 直角三角形⊿的斜边c&#xff5e;水平直角边a&#xff0c;即c经旋转和均匀压缩变换可变为a&#xff5e;c。3000年不识伪≌直线段使数学认定a经刚体运动变为附着在c…

rk3399 的 HDMI 热插拔的问题

问题&#xff1a; 客户的3399 的板子上&#xff0c;烧写ubuntu 发现&#xff0c; 没有热插拔。 测试情况&#xff1a; 系统在第一次烧写完成之后&#xff0c;是有热插拔的&#xff0c;但是第二次启动就没有了。 还有一个情况&#xff0c;就是 &#xff0c;如果我一开始 上电的…

[论文精读]Polarized message-passing in graph neural networks

论文网址&#xff1a;Polarized message-passing in graph neural networks - ScienceDirect 论文代码&#xff1a;he-tiantian/PMP-GNNs&#xff1a;极化消息传递图神经网络的 Pytorch 实现&#xff0c;发表在 Artificial Intelligence&#xff0c;2024 年。 (github.com) 英…

红日靶场通关

初始准备 首先是网络配置&#xff0c;看教程来的&#xff0c;我配置完的效果如下 windows7&#xff1a;(内&#xff1a;192.168.52.143 / 外&#xff1a;192.168.154.136) windows2003&#xff1a;(内&#xff1a;192.168.52.141)windows2008:&#xff08;内&#xff1a;192.…

运算放大器选型的关键参数

上图中的顺序是从左上到右下进行选型&#xff0c;小信号看带宽&#xff0c;大信号看压摆率。一般选用电压反馈型的运放&#xff0c;但是涉及到高频特性的时候也会选择电流反馈型的运放。精密运放选用失调电压比较小的运放&#xff0c;一般失调电压在1mv左右。低功耗的情况下需要…

极越造车2.0:01销量回暖,07杀出血路,ASD抢跑FSD

‍‍‍作者 |张马也 编辑 |德新 9月13日&#xff0c;极越公布其第二款车型极越07上市48小时内&#xff0c;订单超过5000台。 对这家造车4年多的车企来说&#xff0c;这意味着新车取得了初步的成功。 懂车帝的数据显示&#xff0c;7月极越01销量1143台&#xff0c;8月销量则翻…

Linux 入门:简单的基础操作

“批判他人总是想的太简单 剖析自己总是想的太困难” 文章目录 前言Linux 入门&#xff1a;从基础操作到 WSL2 安装文章有误敬请斧正 不胜感恩&#xff01;1. 什么是 Linux&#xff1f;2. Linux 和其他系统有啥不同&#xff1f;3. Linux 的主要组成4. 常见 Linux 发行版5. 基本…

openstack之cinder介绍

概念 cinder 为虚拟机提供管理块存储服务。支持的文件系统&#xff1a;lvm、iscsi、nfs、san、RBD 组件构成及功能介绍 cinder api&#xff1a;在控制节点运行&#xff0c;管理服务的接口&#xff0c;被命令行、其他组件调用&#xff1b; cinder scheduler&#xff1a;类似n…

大数据新视界 --大数据大厂之Kafka消息队列实战:实现高吞吐量数据传输

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

美国税收制度及SAP实施

1. 税制综述 美国是以直接税为主的国家,实行联邦、州和地方&#xff08;市、县&#xff09;三级征税制度&#xff0c;属于彻底的分税制国家。美国联邦税以个人所得税和企业所得税为其主要收入来源&#xff0c;州税以销售与使用税为其主要收入来源&#xff0c;地方税以财产税为…

UART 16550的使用

前言 本文从操作系统使用 16550 的角度来学习 16550。主要解析通用的串口寄存器的作用。 16550 串口由一系列寄存器控制串口行为。不同的具体设备寄存器的偏移不同&#xff0c;寄存器的长度可能不同。 例如&#xff0c;在 AXI UART 16550中&#xff0c;各寄存器长度都为 32 b…

overleaf如何下载论文的pdf

用overleaf写完英文论文后&#xff0c;要将论文保存为PDF格式 点击图片中的下载按钮 然后选择一个路径保存论文的PDF格式即可。

测试通用面试题大全

24年软件测试的发展如何&#xff1f; 1、IT行业还会继续升温&#xff0c;高质量人才需求相对还是短缺。 2、要求变高之后&#xff0c;很难再下降了&#xff0c;学历和经验。 3、功能测试之外的东西&#xff0c;接口、性能和自动化要掌握一点。 4、长远来看&#xff0c;软件…

Android Framework(五)WMS-窗口显示流程——窗口布局与计算

文章目录 relayoutWindow流程概览应用端处理——ViewRootImpl::setView -> relayoutWindowViewRootImpl::setViewViewRootImpl::performTraversalsViewRootImpl::relayoutWindow Surface的创建WindowManagerService::relayoutWindow了解容器类型和Buff类型的SurfaceBuff类型…

并发编程 - GCD的栅栏(dispatch_barrier_async)

引言 Grand Central Dispath&#xff08;GCD&#xff09;是苹果提供的强大工具&#xff0c;它几乎涵盖了多线程编程的所有方面。通过GCD&#xff0c;我们可以轻松地创建队列、管理线程&#xff0c;并以更优雅的方式处理并发任务。在前面的博客中&#xff0c;我们已经深入探讨了…

基于SpringBoot+Vue+MySQL的校园健康驿站管理系统

系统展示 用户前台界面 管理员后台界面 系统背景 本文设计并实现了一个基于SpringBoot后端、Vue前端与MySQL数据库的校园健康驿站管理系统。该系统旨在通过数字化手段&#xff0c;全面管理学生的健康信息&#xff0c;包括体温监测、疫苗接种记录、健康状况申报等&#xff0c;为…

【Canvas与表盘】绘制黄蓝两色简约表盘

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>黄蓝卡通手表</title><style type"text/css">…