自定义IDEA代码补全插件

news2024/11/17 3:33:26

目标:

对于项目中的静态方法(主要是各种工具类里的静态方法),可以在输入方法名时直接提示相关的静态方法,选中后自动补全代码,并导入静态类。

设计:

初步构想,用户选择要导入的文件夹,遍历文件夹下面文件的静态方法并存储,当用户输入时使用弹窗显示候选方法,选中后补全代码。
分解步骤为:

  1. 在设置页加入视图化操作,让用户选择文件夹路径;
  2. 通过持久化数据将选择的文件夹路径保存到本地;
  3. IDE打开时遍历本地保存的文件夹路径下的所有文件,得到所有静态方法;
  4. 用户输入时弹窗显示联想方法;
  5. 选中后自动补全;

开发:

1.搭建开发环境

JetBrains已经提供了纯样板模板,我们下载提供的插件模板 ,使用Android Studio (或IntelliJ IDEA )打开后,可以在gradle.properties中修改项目的属性,gradle.gradle.properties里各属性表示的意义如下

gradle.properties配置
  • pluginGrouppluginName_pluginVersion:插件名称与版本

  • pluginSinceBuildpluginUntilBuild:插件适用的IDE版本,从since到until,各种IDE的版本号可以在这个地方查阅内部编号范围

    Android Studio对应的IntelliJ 平台版本可以查阅Android Studio

  • pluginVerifierIdeVersions:用来检查IDE版本和插件之间兼容性

  • **platformType:**插件适用的IDE类型,IC指社区版,Android Studio基于社区版修改

  • platformPlugins: 声明插件依赖项

在这里插入图片描述

更多的属性可以查阅此链接

https://github.com/JetBrains/intellij-platform-plugin-template

https://github.com/JetBrains/gradle-intellij-plugin/blob/master/README.md#intellij-platform-properties

plugin.xml

文件位于src\main\resources\META-INF下

  • id:gradle.properties里的pluginName_

  • name: gradle.properties里的pluginName_

  • vendor:开发者的名字
    在这里插入图片描述

    添加依赖

在这里插入图片描述

build.gradle.kts

在intellij节点下加入一句intellij

alternativeIdePath = "H:\Android\Android Studio"

在这里插入图片描述

路径设置为本地Android Studio位置,这样在运行时会直接使用本地的AS调试,避免重新下载Android Studio。

settings.gradle.kts

修改项目名称

rootProject.name = "Plugin Template Hint"

配置完成后,点击右边的gradle的runide即可运行插件,如果开发过程中想进行调试可以右键选择debug模式。

在这里插入图片描述

2.设置页添加视图化操作

在IDE的设置页添加新UI,需要使用applicationConfigurable Extension Points。 先在plugin.xml里注册applicationConfigurable,并且新建类继承Configurable。插件的UI模块是在java的swing组件基础上直接包装了一层,可以直接使用。

    <extensions defaultExtensionNs="com.intellij">
   		......
       <applicationConfigurable instance="com.plugin.hint.other.UtilsImportUI" />
   </extensions>
class UtilsImportUI : Configurable {
    private val persistentState: UtilsFolderSetting = ApplicationManager.getApplication().getComponent(UtilsFolderSetting::class.java)
	private var isModify = false
	
    //绘制界面,使用Swing组件
    override fun createComponent(): JComponent? {
        //......绘制代码此处省略
    }
    
    //控制按钮“Apply”是否可点击
    override fun isModified(): Boolean {
    	return isModify
    }
    
    //"Apply"按钮点击事件
    override fun apply() {
    	......
        persistentState!!.list = pathList
        persistentState.loadState(persistentState)//持久化数据
        isModify = false                
    }
    
    //配置面板左边窗口的显示名称
    override fun getDisplayName(): String {
        return "Import Utils Files"
    }

	//调用IDE的文件管理器选择文件
    private fun dir(jPanel: JPanel): String {
        if (project == null) {
            project = guessCurrentProject(jPanel)
        }
        val fcDial = FileChooserFactory.getInstance().createFileChooser(fcDesc, project, null)
        val files = fcDial.choose(project)
        return if (files.isNotEmpty()) {
            files[0].path
        } else ""
    }	
}

上面省略了部分代码,主要是绘制界面、持久化数据、保存用户选中的文件位置,并进行相关的去重。

效果如下:

在这里插入图片描述

3.持久化数据

为了保存用户选择的文件夹路径,我们需要对数据进行持久化。
在plugin.xml里注册implementation-class,并且新建类继承PersistentStateComponent,其中,name为XML中根标记的名称,storages 为保存的文件的名称,默认位置是配置文件地址的options目录下(默认位置可以点击File -> Mange IDE Settings -> Export Settings 查看)。

我们将路径通过list保存,读取时

    <application-components>
        <component>
            <implementation-class>com.plugin.hint.other.UtilsFolderSetting</implementation-class>
        </component>
    </application-components>
@State(name = "searchUtilsPath", storages = [Storage(value = "searchUtilsPath.xml")])
class UtilsFolderSetting : PersistentStateComponent<UtilsFolderSetting?> {
    var list: MutableList<String> = ArrayList()

    override fun getState(): UtilsFolderSetting {
        return this
    }

    override fun loadState(state: UtilsFolderSetting) {
        XmlSerializerUtil.copyBean(state, this)
    }
}
4.启动时遍历文件,保存静态方法

工程模板service下有两个类MyApplicationServiceMyProjectService,分别是 application 级别的service和 project 级别的service,其实还有一个module 级别的service,但是并不推荐(性能原因)。其中MyApplicationService为全局单例,而MyProjectService会在对应范围的每个实例创建一个单独的服务实例。这里我们在MyProjectService里遍历文件夹路径,对所有文件进行解析,并保存静态方法。

 class MyProjectService(project: Project) {

    init {
        if (project.workspaceFile != null) {
            val persistentState = ApplicationManager.getApplication().getComponent(UtilsFolderSetting::class.java)
            val pathList = persistentState.list//得到持久化数据
            for (s in pathList) {//遍历文件夹路径
                UtilMethodsHandle.addPsiMethodByPath(s, project)
            }
        }
    }
}

persistentState 为得到的持久化数据,然后再对文件路径进行解析。
addPsiMethodByPath方法如下,逻辑可以看注释

    var globalPsiMethods = HashMap<String, List<PsiMethod>>()

    //遍历文件夹,解析文件,存储方法
    fun addPsiMethodByPath(path: String, project: Project) {
        val virtualFile = project.workspaceFile!!.fileSystem.findFileByPath(path) ?: return
        if (virtualFile.isDirectory) {//如果是文件夹,递归遍历
            val virtualFiles = virtualFile.children
            for (file in virtualFiles) {
                addPsiMethodByPath(file.path, project)
            }
        } else {//如果是文件,解析
            val psiFile = PsiManager.getInstance(project).findFile(virtualFile)
            //判断是否是java文件,后面看是否支持kotlin文件
            if (psiFile is PsiJavaFile) {
                val classes = psiFile.classes 
                //遍历文件里的类,因为可能会有内部类
                for (aClass in classes) {
                    val tempMethods = aClass.methods
                    val list: MutableList<PsiMethod> = ArrayList()
                    //遍历类里面的方法
                    for (method in tempMethods) {
                        //判断是静态并且不是私有的方法
                        if (method.hasModifierProperty(PsiModifier.STATIC)
                                && !method.hasModifierProperty(PsiModifier.PRIVATE)) {
                            list.add(method)
                        }
                    }
                    globalPsiMethods[path] = list
                }
            }
        }
    }

解释上面的代码,需要先了解IntelliJ平台的一些名称概念。

PSI 程序结构接口(Program Structure Interface),是IntelliJ平台中的一个层,负责解析文件并创建支持平台许多功能的语法和语义代码模型。

PSI File ,IDEA将文件结构抽象为接口,叫程序结构接口文件(PSI File),不同类型的文件解析后生成不同的PsiFile接口的实现类实例,这也是IDEA能够扩展支持多语言的基础。PsiFile类是所有PSI文件的公共基类,而在特定的语言文件通常是由它的子类来表示。例如,PsiJavaFile类表示Java文件,XmlFile类表示XML文件。

VirtualFileSystem 虚拟文件系统(VFS)是IntelliJ平台的组件,该组件封装了用于处理以Virtual File表示的文件的大部分活动。
它具有以下主要目的:
提供一个通用API来处理文件,而不管文件的实际位置如何(在磁盘上,在归档中,在HTTP服务器等上)
当检测到更改时,跟踪文件修改并提供文件内容的旧版本和新版本。
提供了将其他持久性数据与VFS中的文件相关联的可能性。
Virtual File System

上面的代码通过project得到VirtualFile,判断如果是文件夹,递归调用方法,否则返回相对应的PsiFile,接着判断如果是PsiJavaFile(因为项目有可能包含kotlin文件),则遍历PsiClass(有可能包含内部类)得到所有PsiMethod,最后判断method是否是静态的(method.hasModifierProperty(PsiModifier.STATIC))并且不是私有的(!method.hasModifierProperty(PsiModifier.PRIVATE)),最后加入列表。

5.用户输入时自动弹窗显示联想方法

这里的两种方案,其实最开始使用的是第一种方法,在IDE自带的代码补全弹窗里插入我们保存的方法,但是这种方案没有解决方法显示排序的问题,提供的 order="first"属性并没有生效,最后使用了第二种方案。这里记录一下,可能以后在写其他插件时会用到。

第一种方案:

我们在plugin.xml里注册CompletionContributor languageJAVA

    <extensions defaultExtensionNs="com.intellij">
		......
        <completion.contributor
            implementationClass="com.plugin.hint.other.UtilsCompletionContributor" language="JAVA"
            order="first" />
    </extensions>

CompletionContributor,实现extend函数,有三个参数

  1. CompletionType:代码完成的类型,基本完成(BASIC)、智能类型(SMART)匹配完成,Settings/Preferences | Editor | General | Code Completion里可选.
    在这里插入图片描述
  2. ElementPattern:匹配类型,可以对返回的元素进行过滤
  3. CompletionProvider:内容提供者,我们在这里返回待选择的
class UtilsCompletionContributor : CompletionContributor() {

    //查找可以自动补全的代码
    init {
       extend(CompletionType.BASIC, PlatformPatterns.psiElement(), UtilsCompletionProvider())
    }
}

UtilsCompletionProvider类,继承CompletionProvider,重写addCompletions方法,将元素加入到CompletionResultSet

class UtilsCompletionProvider : CompletionProvider<CompletionParameters>() {

    //添加自动补全代码
    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext
                                , result: CompletionResultSet) {
        val prefix = result.prefixMatcher.prefix
        if (prefix.isEmpty()) {
            return
        }
        for (methodList in UtilMethodsHandle.globalPsiMethods.values) {
            for (method in methodList) {
                var s: String? = ""
                if (method.containingClass != null) {
                    s = method.containingClass!!.qualifiedName//类名称
                }
                val element: LookupElement = LookupElementBuilder.create(method)
                        .withTypeText(s)//右边文字
                        //.withIcon(MethodIcon)
                        .withIcon(AllIcons.Nodes.MethodReference)//左边图标
                        .withBoldness(true)//是否加粗
                		//选中后的处理事件
                        .withInsertHandler { context1: InsertionContext, lookupElement: LookupElement? ->
                            context1.document.insertString(context1.startOffset, ".")
                            context1.document.insertString(context1.tailOffset, "();")
                            //导入所引用的类
                            JavaCompletionUtil.insertClassReference(method.containingClass!!, context1.file, context1.startOffset)
                            //移动光标到代码尾部               
                            context1.editor.caretModel.moveToOffset(context1.tailOffset - 2)
                        }
                //添加element到代码补全弹窗
                result.addElement(PrioritizedLookupElement.withPriority(element, Int.MAX_VALUE.toDouble()))
            }
        }

上面代码,先检测是否有匹配的,否则返回。然后循环创建LookupElement。InsertHandler为选中后的操作,在这里补全代码,引入当前方法所在类。

如上图,在自带的代码补全弹窗里添加了2条我们的方法。

第二种方案:
在用户输入后使用快捷键呼出代码补全弹窗,使用Action完成。IntelliJ 平台中的Action需要代码实现并且必须注册。注册决定了Action在 IDE UI 中出现的位置。实现并注册后,Action会接收来自 IntelliJ 平台的回调以响应用户。
1.创建UtilsAction类,继承 Action类。当使用键盘快捷键或从菜单、工具栏操作时,就会回调 Action 类的 actionPerformed 方法。
先在plugin.xml里注册Action,这里默认的快捷键是"control shift X"

    <actions>
        <action class="com.plugin.hint.other.UtilsAction" description="方法提示" id="plugin.hint" text="hint">
            <add-to-group anchor="first" group-id="CodeCompletionGroup" />
            <keyboard-shortcut first-keystroke="control shift X" keymap="$default" />
        </action>
    </actions>

效果如图,Code Completion组下添加了我们新建的Action,在这里也可以更改快捷键。
在这里插入图片描述
UtilsAction类里,我们在actionPerformed 方法里弹出代码补全弹窗。searchText为用户输入的需要补全的代码。LookupImpl为为代码补全的弹窗。选中逻辑与第一种方案一样。

class UtilsAction : AnAction() {

    override fun actionPerformed(e: AnActionEvent) {
		......
		
        //需要查找的字符
        val searchText = StringBuilder()
        //selectedText表示光标选中的文本,如果不为空,则查找选中的,没有就从光标位置向前拼接字符,一直到空格为止
        if (editor.selectionModel.selectedText == null
                || editor.selectionModel.selectedText == "") {
            var indexText = document.text.subSequence(startOffset - 1, startOffset).toString()
            while (startOffset > 0 && nameMatch(indexText)) {
                searchText.insert(0, indexText)
                startOffset--
                indexText = document.text.subSequence(startOffset - 1, startOffset).toString()
            }
        } else {
            searchText.append(editor.selectionModel.selectedText)
        }

        if (project != null) {
            val lookup = obtainLookup(editor, project)
            for (methodList in UtilMethodsHandle.globalPsiMethods.values) {
                for (method in methodList) {
                    var qualifiedName: String? = ""
                    if (method.containingClass != null) {
                        qualifiedName = method.containingClass!!.qualifiedName
                    }
                    LOG.info("actionPerformed: $method+$qualifiedName")
                    if (!method.isValid) continue//检查元素是否有效,比如切换分支后就会失效
                    //创建一个element,与第一种方案一样
                    val element: LookupElement = LookupElementBuilder.create(method)
                            .withTypeText(qualifiedName)
                            .withIcon(MethodIcon)
                            //.withIcon(AllIcons.Nodes.MethodReference)
                            .withBoldness(true)
                    val item = CompletionResult.wrap(element, PlainPrefixMatcher(searchText.toString()), CompletionSorter.emptySorter())
                    if (item != null) {
                        //将element添加进去
                        lookup.addItem(item.lookupElement, item.prefixMatcher)
                    }
                }
            }
            lookup.addLookupListener(object : LookupListener {
                override fun itemSelected(event: LookupEvent) {//item选中事件,与
                    val lookupElement = event.item as LookupElement
                    if (lookupElement.psiElement is PsiMethod) {//如果选中的element是方法
                        val psiMethod = lookupElement.psiElement as PsiMethod
                        //得到上下文InsertionContext
                        val insertionContext = InsertionContext(OffsetMap(document), Lookup.AUTO_INSERT_SELECT_CHAR, arrayOf(lookupElement), psiFile!!, editor, false)
                        //val tailOffset = OffsetMap(document).getOffset(InsertionContext.TAIL_OFFSET)
                        //如果是选中状态,计算开始位置需要减去字符长度
                        if (startOffset == start) startOffset -= searchText.length
                        document.insertString(startOffset, ".")
                        document.insertString(insertionContext.tailOffset, "();")
                        //导入所引用的类
                        JavaCompletionUtil.insertClassReference(psiMethod.containingClass!!, psiFile, startOffset)
                        //移动光标到代码尾部
                        editor.caretModel.moveToOffset(insertionContext.tailOffset - 2)
                    }
                }
            })
            lookup.showLookup()//显示弹窗
        }
        
	    private fun obtainLookup(editor: Editor, project: Project): LookupImpl {
	        val lookup = LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "",
	                DefaultArranger()) as LookupImpl
	/*        if (editor.isOneLineMode) {
	            lookup.setCancelOnClickOutside(true)
	            lookup.setCancelOnOtherWindowOpen(true)
	        }*/
	        //lookup.lookupFocusDegree = if (autopopup) LookupFocusDegree.UNFOCUSED else LookupFocusDegree.FOCUSED
	        return lookup
	    }
    }

这里使用的代码补全弹窗是系统自带的弹窗,在这里说一下怎么找到各种UI相对应的类。
我们需要启用内部模式。在idea.properties里添加idea.is.internal=true,保存并重启IDE。会看到Tool中多了一个选项Internal Actions,然后选择 UI -> UI Inspector,打开 UI 检查器,启用之后就可以以交互方式测试UI元素。查看时,将光标居中于UI元素上,使用Ctrl+Alt+鼠标左键即可显示UI元素的内部描述.。
效果如图,可以看到相关的类,然后就可以再去找到具体的实现方法。
在这里插入图片描述

最终效果如下:
在这里插入图片描述

相关资料

IntelliJ Platform SDK

使用PSI分析Java代码

Intellij IDEA 插件开发秘籍

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

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

相关文章

得物-Golang-记一次线上服务的内存泄露排查

1.出现内存泄漏 1.1 事发现场 在风和日丽的一天&#xff0c;本人正看着需求、敲着代码&#xff0c;展望美好的未来。突然收到一条内存使用率过高的告警。 1.2 证人证词 告警的这个项目&#xff0c;老代码是python的&#xff0c;最近一直在go化。随着go化率不断上升&#xff…

nodejs+vue+微信小程序+python+PHP协同过滤算法的电影推荐系统-计算机毕业设计推荐python

信息数据的处理完全依赖人工进行操作&#xff0c;会耗费大量的人工成本&#xff0c;特别是面对大量的数据信息时&#xff0c;传统人工操作不仅不能对数据的出错率进行保证&#xff0c; 所以电子化信息管理的出现就能缓解以及改变传统人工方式面临的处境&#xff0c;一方面可以确…

管理类联考——数学——真题篇——按题型分类——充分性判断题——蒙猜D

先看目录&#xff0c;除了2018年比较怪&#xff0c;其他最多2个D&#xff08;数学只有两个弟弟&#xff0c;一个大弟&#xff0c;一个小弟&#xff09; 文章目录 2023真题&#xff08;2023-16&#xff09;-D 2022真题&#xff08;2022-21&#xff09;-D-分析选项⇒是否等价⇒是…

pycharm中如何去除波浪线的设置

pycharm中&#xff0c;碰到恼人的红绿波浪线&#xff0c;打开’file-settings’&#xff0c;然后&#xff0c;参照如图设置&#xff0c;去除’effects’选项&#xff1a;

AWS 知识二:AWS同一个VPC下的ubuntu实例通过ldapsearch命令查询目录用户信息

前言&#xff1a; 前提&#xff1a;需要完成我的AWS 知识一创建一个成功运行的目录。 主要两个重要&#xff1a;1.本地windows如何通过SSH的方式连接到Ubuntu实例 2.ldapsearch命令的构成 一 &#xff0c;启动一个新的Ubuntu实例 1.创建一个ubuntu实例 具体创建实例步骤我就不…

深入了解常见的应用层网络协议

目录 1. HTTP协议 1.1. 工作原理 1.2. 应用场景 1.3. 安全性考虑 2. SMTP协议 2.1. 工作原理 2.2. 应用场景 2.3. 安全性考虑 3. FTP协议 3.1. 工作原理 3.2. 应用场景 3.3. 安全性考虑 4. DNS协议 4.1. 工作原理 4.2. 应用场景 4.3. 安全性考虑 5. 安全性考虑…

用23种设计模式打造一个cocos creator的游戏框架----(二十一)组合模式

1、模式标准 模式名称&#xff1a;组合模式 模式分类&#xff1a;结构型 模式意图&#xff1a;将对象组合成树型结构以表示“部分-整体”的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。 结构图&#xff1a; 适用于&#xff1a; 1、想表示对象的部分…

『OPEN3D』1.5.4 动手实现点云八叉树(OctoTree)最近邻

本专栏地址: https://blog.csdn.net/qq_41366026/category_12186023.html?spm=1001.2014.3001.5482 在二维和三维空间中,我们可以采用四叉树(Quad tree)和八叉树(Octree)这两种特定的数据结构来处理空间分割。这些树形结构可以看作是K-d树在不同维度下的扩展。…

C7练习题答案

一、单项选择题(本大题共20小题,每小题2分,共40分。在每小题给出的四个备选项中,选出一个正确的答案,并将所选项前的字母填写在答题纸的相应位置上。) 以下不是 C 语言的特点的是BA. C 简洁,紧凑 B.不能够编制出功能复杂的程序 C. C语言可以直接对硬件进行操作 D. 语言 C 语言…

云原生之深入解析Kubernetes集群发生网络异常时如何排查

一、Pod 网络异常 网络不可达&#xff0c;主要现象为 ping 不通&#xff0c;其可能原因为&#xff1a; 源端和目的端防火墙&#xff08;iptables, selinux&#xff09;限制&#xff1b; 网络路由配置不正确&#xff1b; 源端和目的端的系统负载过高&#xff0c;网络连接数满…

没有数据线,在手机上查看电脑备忘录怎么操作

在工作中&#xff0c;电脑和手机是我最常用的工具。我经常需要在电脑上记录一些重要的工作事项&#xff0c;然后又需要在手机上查看这些记录&#xff0c;以便随时了解工作进展。但是&#xff0c;每次都需要通过数据线来传输数据&#xff0c;实在是太麻烦了。 有一次&#xff0…

腾讯云微服务11月产品月报 | TSE 云原生 API 网关支持 WAF 对象接入

2023年 11月动态 TSE 云原生 API 网关 1、支持使用私有 DNS 解析 服务来源支持私有 DNS 解析器&#xff0c;用户可以添加自己的 DNS 解析器地址进行私有域名解析&#xff0c;适用于服务配置了私有域名的用户。 2、支持 WAF 对象接入 云原生 API 网关对接 Web 安全防火墙&…

解决:Android 报错 Failed to transform exifinterface-1.2.0.jar

一、问题说明 Failed to transform exifinterface-1.2.0.jar (androidx.exifinterface:exifinterface:1.2.0) to match attributes {artifactTypeandroid-classes-jar, org.gradle.categorylibrary, org.gradle.libraryelementsjar, org.gradle.statusrelease, org.gradle.usa…

学习k8s

学习k8s 我为什么要用k8s 和其他部署方式的区别是什么? 传统部署方式 java --> package --> 放到服务器上 --> Tomcat 如果是同时进行写操作,会存在并发问题. 用户 --网络带宽–> 服务器 -->服务 同一个服务器上,多个服务: 网络资源的占用 内存的占用 cpu的占…

Vue-图片懒加载

实现图片懒加载可以使用vue-lazyload插件 npm 链接&#xff1a;vue-lazyload - npm (npmjs.com) 使用方法&#xff1a; 1. 安装vue-lazyload npm i vue-lazyload npm i vue-lazyload1.3.3 // 如果是vue2就需要安装1.3.3版本 2. 引入vue-lazyload并使用 可以在使用该插…

设计模式——0前言目录

1 设计模式介绍 应当站在产品经理的角度来学习设计模式 是软件设计中常见问题的典型解决方案&#xff0c;可用于解决代码中反复出现的设计问题 学习效果一般的原因在于自己没有站在产品经理的角度学习&#xff0c;仅仅是为了学习怎么实现&#xff0c;用什么算法实现。 分类&…

STM32 RTC总结

RTC入侵检测Tamper RTC Tamper功能就是&#xff0c;MCU在Tamper管脚检测到一个指定边缘信号&#xff08;可配置&#xff09;时&#xff0c;就主动清除所有备份寄存器数据的功能。如果需要&#xff0c;可以使能Tamper中断&#xff0c;在每次检测到Tamper信号后执行指定代码。 在…

无人机在融合通信系统中的应用

无人驾驶飞机简称“无人机”&#xff0c;是利用无线电遥控设备和自备的程序控制装置操纵的不载人飞行器&#xff0c;现今无人机在航拍、农业、快递运输、测绘、新闻报道多个领域中都有深度的应用。 在通信行业中&#xff0c;无人机广泛应用于交通&#xff0c;救援&#xff0c;消…

Postman使用总结--生成测试报告

1.执行生成的命令格式 newman run 用例集文件 .json -e 环境文件 .json -d 数据文件 .json/.csv -r htmlextra --reporter- htmlextra-export 测试报告名 .html -e 和 -d 是 非必须的。 如果没有使用 环境&#xff0c;不需要指定 -e 如果没有使用 数据…

【设计模式--行为型--备忘录模式】

设计模式--行为型--备忘录模式 备忘录模式定义结构案例实现白箱备忘录模式黑箱备忘录模式 优缺点使用场景 备忘录模式 定义 又叫快照模式&#xff0c;在不破坏封装性的前提下&#xff0c;捕获一个对象的对象的内部状态&#xff0c;并在该对象之外保存这个状态&#xff0c;以便…