
作者 | 冀澤
來源 | 阿裡技術公衆号
IDEA作為我們(後端Java開發者)必不可少的IDE,以其智能的代碼提示、多樣的架構支援、簡潔的開發界面等特性,被業界公認為最好的Java開發工具之一。而一款IDE是否強大,最簡單的衡量标準就是檢視其插件生态環境的發展情況,多種多樣的插件既豐富了IDE自身的功能,同時大大提高了開發人員的工作效率。
一 概念簡介
插件類型
IDEA的插件根據功能分為以下4種類型:
- 自定義語言支援,例如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),插件可能依賴的其他插件、三方包等。
二 開發過程簡述
此部分以一個簡單插件開發執行個體進行說明。
1 建立項目
IDEA插件項目開發時,有兩種建立方式,一種是IntelliJ Platform Plugin,另一種是Gradle下的IntelliJ Platform Plugin(在Gradle插件安裝的情況下)。推薦使用第二種方式,使用Gradle的方式可以友善的添加第三方依賴庫,同時也是官方推薦的方式。
選擇好建立方式後,根據需要填寫資訊即可完成建立。
2 設定建立入口
由于執行個體插件是一個工具內建類型的插件,我們需要在IDEA的UI添加插件的入口,這部分在配置檔案plugin.xml中添加如下内容:
<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>
我們可以發現,入口就是一個Action。需要申明Action的位置和處理類。以上聲明的UI效果:
3 編寫處理邏輯
在配置檔案中指明的動作處理類中添加處理邏輯。具體邏輯根據實際需要。
三 使用總結
1 彈出對話框
使用:
Messages.showErrorDialog(myTabbedPane.getComponent()," 彈出文本内容");
2 提示資訊
使用 new Notification(groupId 自定義, 标題, 内容, 類型(info、warning、error)).notify(項目對象執行個體);
3 擴充點使用
在配置檔案中,添加擴充點配置,其他擴充點類型:
<extensions defaultExtensionNs="com.intellij">
<!-- 添加自定義擴充标簽,這裡的customJavadocTagProvider是IDEA自身申明的 -->
<customJavadocTagProvider implementation="擴充點實作類"/>
</extensions>
自定義JavaDoc的擴充效果:
4 自定義LiveTemplate
(1)在plugin.xml中配置liveTemplate擴充點的相關實作:
<!-- 自定義LiveTemplate -->
<defaultLiveTemplatesProvider
implementation="DefaultLiveTemplatesProvider接口的實作類"/>
<!-- 自定義LiveTemplate上下文,以及上下文可以使用的配置 -->
<liveTemplateContext
implementation="TemplateContextType類的子類"/>
(2)在 DefaultLiveTemplatesProvider 接口的實作類的 getDefaultLiveTemplateFiles 方法中注冊LiveTemplate定義檔案:
@Override
public String[] getDefaultLiveTemplateFiles() {
//檔案名不需要有字尾,例如a.xml,這裡隻需要輸入a
return new String[]{"liveTemplates/檔案1", "liveTemplates/檔案2"};
}
(3)在 TemplateContextType 類的子類的構造方法中定義上下文名稱,以及 isInContext 方法中定義上下文可以使用的位置。例如:
public XXXJavaInlineCommentContextType() {
super("上下文id", "名稱", 上下文基礎類型);
}
@Override
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;
}
(4)編寫LiveTemplate定義xml檔案,例如:
<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>
5 插件中調用Dubbo
(1)在Gradle的建構檔案build.gradle中的dependencies内添加如下配置:
compile 'org.apache.dubbo:dubbo:2.7.7'
compile 'org.apache.dubbo:dubbo-dependencies-zookeeper:2.7.7'
(2)在接口調用處進行如下編碼:
//将目前線程的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);
6 釋出插件的指定IDEA倉庫
(1)在build.gradle檔案中進行如下配置
publishPlugin {
host = 'https://xxxx.com' //倉庫位址
username 'onepublish' //倉庫指定使用者名
password 'onepublish' //倉庫密碼
token 'onepublish' //倉庫驗證token
}
(2)執行gradle中publishPlugin任務。
7 釋出到指定IDEA插件失敗
當我們開發完成後,通過publishPlugin任務釋出時,可能會出現以下報錯資訊:
The resource identified by this request is only capable of generating responses with characteristics not acceptable according to the request "accept" headers.
這個問題的原因是我們使用的org.jetbrains.intellij版本較高,請使用2.x的版本。或者參照插件的源碼自己寫一個沒有accept的上傳方法即可。
相關代碼:
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() {
@Override
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 {
@Multipart
@POST("/plugin/uploadPlugin")
public Response uploadByXmlId(@Part("userName")TypedString username, @Part("password")TypedString password,
@Part("pluginId")TypedString pluginId, @Part("channel")TypedString channel,
@Part("file")TypedFile file);
}
8 如何在插件中引入本地jar包
在build.gradle的dependencies裡邊添加如下内容:
compile fileTree(dir:'src/main/resources/lib',includes:['*jar'])
然後,将本地jar包放到指定目錄即可。