阿里妹导读:本文从IDEA插件的基本概念讲起,通过一个简单的开发实例,介绍IDEA插件开发的过程,并总结了8条实战经验。更详细的IDEA插件开发介绍,可参见官方说明或到官方论坛讨论。
文末福利:轻量应用服务器优惠,新用户专享。
自定义语言支持,例如Go语言插件。这种插件包括文件类型识别、格式化、语言保留字支持、编译、运行等语言开发必备功能。属于比较重量级的插件。
开发框架支持,例如Spring框架插件。这种插件包括框架特殊代码识别、框架功能支持等。不同开发框架开发量、难度不同。
工具集成,例如我司内容的云雀,就是这种插件,也是最常用的插件,后续的开发实例也属于这种类型。这种插件一般包括额外的功能、功能相关的UI以及访问外部资源。
UI附加,主要限于UI的修改。
配置文件,配置文件就是插件对IDE的自我介绍,IDEA中是META-INF/plugin.xml,详细的配置信息请参见官方文档。
ClassLoader,每个插件对应一个ClassLoader,彼此之间隔离(类似于Pandora的插件机制)。
Component(组件),插件内部可以有三个级别的组件:Applciation、Project、Module,分别需要在plugin.xml文件配置,并实现不同的接口。
扩展和扩展点(Extesions and Extension Points),扩展用于扩展IDEA自身或者其他组件扩展点的功能,例如添加一个自定义的JavaDoc中的Tag。扩展点是插件提供给其他插件使用的。
动作(Action),动作在配置文件中配置,由某个菜单项触发。
图标(Icon),插件使用到的图标。
服务(Service),用于后端运行的某些服务实现。
依赖(Dependencies),插件可能依赖的其他插件、三方包等。
<actions>
<!-- Add your actions here -->
<group id="分组id" text="显示文本1" description="鼠标驻留时的显示">
<add-to-group group-id="MainMenu(这个id指的是IDEA的顶部菜单)" anchor="位置(last等)"/>
<action class="动作类全路径" id="动作类id" text="显示文本2" description="鼠标驻留时的显示"/>
</group>
</actions>
<extensions defaultExtensionNs="com.intellij">
<!-- 添加自定义扩展标签,这里的customJavadocTagProvider是IDEA自身申明的 -->
<customJavadocTagProvider implementation="扩展点实现类"/>
</extensions>
<!-- 自定义LiveTemplate -->
<defaultLiveTemplatesProvider
implementation="DefaultLiveTemplatesProvider接口的实现类"/>
<!-- 自定义LiveTemplate上下文,以及上下文可以使用的配置 -->
<liveTemplateContext
implementation="TemplateContextType类的子类"/>
public String[] getDefaultLiveTemplateFiles() {
//文件名不需要有后缀,例如a.xml,这里只需要输入a
return new String[]{"liveTemplates/文件1", "liveTemplates/文件2"};
}
public XXXJavaInlineCommentContextType() {
super("上下文id", "名称", 上下文基础类型);
}
public boolean isInContext(@NotNull final PsiFile file, final int offset) {
if (PsiUtilCore.getLanguageAtOffset(file, offset).isKindOf(JavaLanguage.INSTANCE)) {
PsiElement element = file.findElementAt(offset);
if (element instanceof PsiWhiteSpace && offset > 0) {
element = file.findElementAt(offset-1);
}
if (null == element) {
return false;
}
return (element.getParent() instanceof PsiInlineDocTag && element.getParent().getParent() instanceof PsiDocTag)
|| (element.getParent() instanceof PsiInlineDocTag && PsiTreeUtil.getParentOfType(element, PsiField.class, false) != null);
}
return false;
}
<templateSet group="分组名">
<template name="模板名" value="模板值,可以使用$VAR1$来指代变量位置" description="描述信息" toReformat="false" toShortenFQNames="true">
<variable name="变量名" expression="" defaultValue="" alwaysStopAt="true" />
<context>
<option name="自定义的或者预定义的template上下文id" value="true" />
</context>
</template>
</templateSet>
compile 'org.apache.dubbo:dubbo:2.7.7'
compile 'org.apache.dubbo:dubbo-dependencies-zookeeper:2.7.7'
//将当前线程的classloader备份,并设置当前线程的classloader为当前类的classloader
//当前线程的classloader是IDEA的,当前类的classloader是当前插件的
//不进行如此设置会造成Dubbo扩展点实现无法在ClassLoader中找到
ClassLoader backCl = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
// 以下内容参考Dubbo的泛化调用
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("应用名");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181");
// 引用远程服务
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
reference.setInterface("服务全类名");
reference.setVersion("服务版本号");
reference.setGeneric(true);
// 和本地bean一样使用xxxService
GenericService genericService = reference.get();
//泛化调用
Object result = genericService.$invoke("方法名", new String[]{"参数类型"}, new Object[]{"参数值"});
System.out.println(result);
//恢复classloader设置
Thread.currentThread().setContextClassLoader(backCl);
publishPlugin {
host = 'https://xxxx.com' //仓库地址
username 'onepublish' //仓库指定用户名
password 'onepublish' //仓库密码
token 'onepublish' //仓库验证token
}
The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.
package idea;
import retrofit.RestAdapter;
import retrofit.client.Request;
import retrofit.client.Response;
import retrofit.client.UrlConnectionClient;
import retrofit.converter.SimpleXMLConverter;
import retrofit.mime.TypedFile;
import retrofit.mime.TypedString;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
/**
* @author lijie
* @date 2019/1/17
*/
public class PublishPluginTest {
public static void main(String[] args) {
PluginRepositoryService service = new RestAdapter.Builder().setEndpoint("https://插件仓库链接").setClient(new UrlConnectionClient() {
protected HttpURLConnection openConnection(Request request) throws IOException {
HttpURLConnection connection = super.openConnection(request);
connection.setReadTimeout(10 * 60 * 1000);
return connection;
}
}).setLogLevel(RestAdapter.LogLevel.BASIC)
.setConverter(new SimpleXMLConverter())
.build()
.create(PluginRepositoryService.class);
Response response = service.uploadByXmlId(new TypedString(""), new TypedString(""),
new TypedString(pluginId), new TypedString("default"),
new TypedFile("application/octet-stream",
new File(plugin压缩文件路径)));
System.out.println(response.getBody());
}
}
package idea;
import retrofit.client.Response;
import retrofit.http.*;
import retrofit.mime.TypedFile;
import retrofit.mime.TypedString;
/**
* @author lijie
* @date 2019/1/17
*/
public interface PluginRepositoryService {
public Response uploadByXmlId( TypedString username, TypedString password,
TypedString pluginId, TypedString channel,
TypedFile file);
}
compile fileTree(dir:'src/main/resources/lib',includes:['*jar'])
阿里云开发者成长计划面向全年龄段开发者,依托免费资源、免费体验、免费学习、免费实践 4 大场景,全面助力开发者轻松掌握云上技能。新用户专享轻量应用服务器,内置WordPress等8种主流环境,5M峰值带宽,40GBSSD云盘,1000G流量包,轻松满足学习、搭建应用等场景!
识别下方二维码,或点击 “阅读原文” ,快去优惠购买吧~