gitee位址:https://gitee.com/jyq_18792721831/studyplugin.git
idea插件開發入門
idea插件開發–配置
idea插件開發–服務-翻譯插件
idea插件開發–元件–程式設計久坐提醒
idea插件開發--服務-翻譯插件
- 介紹
- 服務
- 輕量級服務
- 服務定義
- 服務擷取
- 執行個體
- 目标
- 分解
- 準備線上翻譯資訊
- 有道翻譯
- 必應翻譯
- 百度翻譯
- 建立插件項目
- 建立配置界面
- 引入第三方依賴
- 定義存儲的服務
- 定義配置界面
- 建立Action
- 封裝抽象RestAPI
- 有道翻譯
- 百度翻譯
- 廠商擴充
- 編寫Action後續操作
- 效果
- 打包
- 最後的最後
- 總結
介紹
本次主要介紹idea中服務的相關内容,包括服務的種類,服務的定義,服務的擷取,以及服務的使用。
之後綜合idea插件的Action和簡單配置,實作一個較為實用的翻譯小插件,以此複習和鞏固idea插件的Action和簡單配置。
服務
在spring中,服務一般是單例的,使用起來也比較友善,自動注入。
idea插件平台也提供了類似的解決方案,允許我們建立單例的服務,然後在使用的時候擷取。
在idea插件項目中,通過
com.intellij.openapi.components.ComponentManager
接口擷取,服務會在第一次調用的時候建立一個執行個體,而且在作用域範圍内,保證隻有一個執行個體。服務應該實作
Disposable
接口,用于登出服務。
ComponentManager
接口有這幾個實作類
最長用的也就是Application了
idea提供三種類型的服務:application,prject和module級别的服務。其作用域分别是全局,項目和子產品。子產品級别的服務需要慎用,因為當項目中子產品比較多的時候,會占用較多的記憶體和資源。
對于project和module級别的服務,可以注入project和module的對象,注入方式為在構造函數中增加Project和Module參數。因為這個注入的構造函數主要是用于參數注入,是以在使用的時候,盡可能避免在自己的代碼中調用。
輕量級服務
在2019.3版本之後,增加了另一種輕量級的服務,輕量級服務不需要在
plugin.xml
中定義,隻需要增加
@Service
注解即可。
- 輕量級服務必須是
修飾final
- 輕量級服務不推薦使用構造函數注入(根據文檔給出的示例,project對象還是能夠使用構造函數注入)
- 如果服務用于存儲(
,那麼需要增加參數PersistentStateComponent
)roamingType=RoamingtType.DISABLED
服務定義
如果不是輕量級服務,那麼需要在
plugin.xml
中定義,定義需要在
extensions
節點下定義。定義不同作用域的服務,使用不同的标簽:
applicationService,projectService,moduleService
定義服務,接口不是必須的,如果沒有接口,把接口和執行個體屬性設定成實作類就行。
服務擷取
可以使用
ComponentManager
接口的執行個體擷取接口,常使用
ApplicationManager
擷取。
執行個體
目标
實作一個翻譯插件,說實話,本人英語水準是在有限,是以在開發編碼的時候,有時候給變量起名字,就需要翻譯好,在拷貝過來。
當然,現在在插件市場上也有許許多多的翻譯插件,做的功能齊全,使用友善。
我們這主要是學習插件開發,翻譯插件邏輯也不複雜,正好作為一個練手的項目。
需求:在編輯視窗,選中需要翻譯的中文,按下快捷鍵,翻譯為英文,并轉為駝峰形式,替換選中的中文。
分解
- 我們需要增加編輯視窗的Action,而且需要有快捷鍵
- 需要擷取選中的中文
- 需要翻譯接口
- 配置線上翻譯接口的參數
- 得到翻譯的英文,處理為駝峰形式
- 替換編輯視窗選中的中文
準備線上翻譯資訊
有道翻譯
在有道智雲AI開放平台 (youdao.com)新增賬號,自己玩足夠了。
然後建立文本翻譯的應用
在個人資訊裡能看到應用id和秘鑰
必應翻譯
在Bing for Partners helps businesses and developers succeed新增賬號,必應線上文本翻譯每月有免費的數量,個人使用完全足夠
有了賬号後,根據快速入門:Translator 入門 - Azure Cognitive Services | Microsoft Docs選擇文本翻譯即可
注冊需要visa卡等進行驗證,如果沒有就跳過(我就沒有)
百度翻譯
在百度翻譯開放平台 (baidu.com)新增賬號,選擇通用翻譯
這裡需要進行實名認證,并注冊為個人開發者,然後在控制台就能看到自己選擇的服務了
在最下面有應用id和秘鑰
建立插件項目
建立如下項目
在
plugin.xml
中定義好插件的資訊
建立配置界面
在ui包下建立配置的資訊
然後通過拖動的方式增加控件
需要注意,使用密碼輸入框,而不是文本輸入框
記得選擇生成源代碼
在源代碼中,我們增加方法,用于擷取資料,這樣就不把控件進行暴露了
編譯才會生成源代碼
然後生成測試ui的main方法(需要給最外層的JPanel設定屬性名字)
運作main方法就可以看看我們的界面效果了
需要注意,我們需要将最外層的JPannel暴露到外面,雖然自己生成了一個暴露最外層JPannel的方法,但是不介意使用。
如果使用者是修改配置,我們還需要增加方法,用于設定控件的值
引入第三方依賴
我們使用lombok注解進行暴露,要是用lombok就需要在項目中加入lombok的依賴。
還記得我們的項目結構中,有個lib的檔案夾。
lib檔案夾就是放第三方依賴的jar包的。
首選需要在Maven Central Repository Search搜尋lombok插件,然後下載下傳jar包,并将jar拷貝到lib目錄下。
然後将增加的jar包加入項目
當然,其他第三方jar包也是這樣增加的。
定義存儲的服務
我們使用之前說的最簡單的存儲方式,然後對這種方式進行封裝。服務使用輕量級的服務,直接使用注解,也不需要實作登出的方法。
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.components.Service;
@Service
public final class TranslateAppInfoService {
private final PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
public void save(String key, String value) {
propertiesComponent.setValue(key, value);
}
public String get(String key, String defaultValue) {
return propertiesComponent.getValue(key, defaultValue);
}
public String get(String key) {
return get(key, "");
}
}
我們封裝三個方法,一個是存儲,一個是擷取,一個是帶有預設值的擷取。
很簡單,這裡定義的存儲服務,會在
SearchableConfiguable
的實作類中使用。
定義配置界面
我們建立好了配置界面的UI後,還需要配置到setting下,idea插件開發–配置_a18792721831的部落格
首先建立
SearchableConfigurable
接口的實作類,傳輸配置id,配置名字。
在定義配置界面的時候,首先從存儲服務中擷取已經儲存的配置,然後把配置放入控件中,因為使用者可能隻想修改一部分,如果不設定,就會被空值覆寫,而且不設定,使用者也不知道哪些已經配置過了。是以需要在建立好控件from後,擷取已有配置,設定到控件。
如果使用者根本無修改,此時給
isModified
方法傳回false,表示應用按鈕不可用,無修改,無需儲存,無需調用apply方法。
在apply方法中,則是将控件中輸入的值,調用存儲服務,存儲起來。
完整代碼如下
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.text.StringUtil;
import com.study.plugin.translate.service.TranslateAppInfoService;
import com.study.plugin.translate.ui.TranslateConfigUI;
import com.study.plugin.translate.utils.PluginAppKeys;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.JComponent;
public class TranslateAppInfoConfig implements SearchableConfigurable, PluginAppKeys {
private TranslateConfigUI ui = new TranslateConfigUI();
private TranslateAppInfoService appInfoService = ApplicationManager.getApplication().getService(TranslateAppInfoService.class);
@Override
public @NotNull
@NonNls
String getId() {
return PLUGIN_CONFIG_ID;
}
@Override
public @NlsContexts.ConfigurableName String getDisplayName() {
return PLUGIN_CONFIG_NAME;
}
@Override
public @Nullable
JComponent createComponent() {
ui.setYoudaoAppId(appInfoService.get(YOUDAO_APP_ID_SAVE_KEY, ""));
return ui.getRootJPanel();
}
@Override
public boolean isModified() {
if (!appInfoService.get(YOUDAO_APP_ID_SAVE_KEY).equals(ui.getYoudaoAppId()) ||
!appInfoService.get(YOUDAO_APP_SECRET_SAVE_KEY).equals(ui.getYoudaoAppSecret()) ||
!appInfoService.get(BIYING_APP_ID_SAVE_KEY).equals(ui.getBiyingAppId()) ||
!appInfoService.get(BIYING_APP_SECRET_SAVE_KEY).equals(ui.getBiyingAppSecret()) ||
!appInfoService.get(BAIDU_APP_ID_SAVE_KEY).equals(ui.getBaiduAppId()) ||
!appInfoService.get(BAIDU_APP_SECRET_SAVE_KEY).equals(ui.getBaiduAppSecret())) {
return true;
}
return false;
}
@Override
public void apply() throws ConfigurationException {
String youdaoAppId = ui.getYoudaoAppId();
String youdaoAppSecret = ui.getYoudaoAppSecret();
if (StringUtil.isNotEmpty(youdaoAppId) && StringUtil.isNotEmpty(youdaoAppSecret)) {
appInfoService.save(YOUDAO_APP_ID_SAVE_KEY, youdaoAppId);
appInfoService.save(YOUDAO_APP_SECRET_SAVE_KEY, youdaoAppSecret);
}
String biyingAppId = ui.getBiyingAppId();
String biyingAppSecret = ui.getBiyingAppSecret();
if (StringUtil.isNotEmpty(biyingAppId) && StringUtil.isNotEmpty(biyingAppSecret)) {
appInfoService.save(BIYING_APP_ID_SAVE_KEY, biyingAppId);
appInfoService.save(BIYING_APP_SECRET_SAVE_KEY, biyingAppSecret);
}
String baiduAppId = ui.getBaiduAppId();
String baiduAppSecret = ui.getBaiduAppSecret();
if (StringUtil.isNotEmpty(baiduAppId) && StringUtil.isNotEmpty(baiduAppSecret)) {
appInfoService.save(BAIDU_APP_ID_SAVE_KEY, baiduAppId);
appInfoService.save(BAIDU_APP_SECRET_SAVE_KEY, baiduAppSecret);
}
}
}
最後别忘記在
plugin.xml
中注冊
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<notificationGroup displayType="BALLOON" id="simpleconfig.notification.balloon" isLogByDefault="false"/>
<applicationConfigurable parentId="tools" instance="com.study.plugin.translate.config.TranslateAppInfoConfig"
id="com.study.plugin.translate。setting.config.id" displayName="線上翻譯資訊"/>
</extensions>
當做好這些後,就可以調試一下之前寫的代碼了
還是很不錯的,簡單明了,記得測試下存儲服務是否正常。
建立Action
建立Action很簡單,之前就用過:idea插件開發入門_a18792721831的部落格
我們建立Action,快捷鍵還是使用
ctrl+alt+;
在觸發後,首先擷取選中的文本,然後調用翻譯的服務(假設我們已經寫好了一個翻譯的RestAPI)
以此來觸發翻譯,等翻譯至少有一個可用時,在回頭基礎開發這裡的操作。
封裝抽象RestAPI
因為我們使用的都是線上API的方式請求的,是以需要使用URL請求。
為了使用更加友善,我們使用spring的restTemplate接口進行請求。
首先從maven倉庫下載下傳spring-beans,spring-context,spring-web,spring-core四個依賴,并加入項目。
然後封裝Rest請求的抽象類。
抽象類中主要是restTemplate的對象和存儲服務的對象,因為對所有的各個廠商的線上翻譯平台來說,我們的restTemplate和存儲服務使用同一個就可以了,而且我們定義子類必須實作翻譯方法,翻譯方法傳入待翻譯的中文,傳回翻譯後的英文或者空串。
一些公共的工具方法,也可以放在抽象類中,比如加密
import com.intellij.openapi.application.ApplicationManager;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
public abstract class TranslateRestService {
protected RestTemplate restTemplate;
protected volatile AtomicBoolean isInit = new AtomicBoolean(Boolean.FALSE);
protected TranslateAppInfoService appInfoService = ApplicationManager.getApplication().getService(TranslateAppInfoService.class);
protected synchronized void init() {
// 如果已經初始化了,直接結束
if (isInit.get()) {
return;
}
// 連接配接池
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
poolingHttpClientConnectionManager.setMaxTotal(4);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(2);
// 我們目前隻有2個線上翻譯可用,每個翻譯2個線程用于Rest請求,是以設定最大連接配接4,每個翻譯api是2個并發
// 用戶端構造器
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
// 建立restTemplate
HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
httpRequestFactory.setHttpClient(httpClientBuilder.build());
httpRequestFactory.setConnectTimeout(6000);
httpRequestFactory.setConnectTimeout(6000);
httpRequestFactory.setReadTimeout(12000);
RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
this.restTemplate = restTemplate;
isInit.compareAndSet(Boolean.FALSE, Boolean.TRUE);
}
/**
* 加密
*
* @param string
* @return
*/
protected static String getDigest(String string, String key) {
if (string == null) {
return null;
}
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
byte[] btInput = string.getBytes(StandardCharsets.UTF_8);
try {
MessageDigest mdInst = MessageDigest.getInstance(key);
mdInst.update(btInput);
byte[] md = mdInst.digest();
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (byte byte0 : md) {
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
public abstract String translate(String word);
}
當抽象方法完成後,就需要針對各個廠商實作翻譯的子類。
有道翻譯
根據有道翻譯的api産品文檔-自然語言翻譯服務 (youdao.com),根據裡面的示例程式,拷貝相關的請求參數封裝的代碼到子類中,然後調用父類的存儲服務,進行請求app_id,app_secret的讀取,并使用父類的restTemplate進行請求,并傳回。
import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.YoudaoTranslateResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.util.HashMap;
import java.util.Map;
@Service
public final class YoudaoTranslateRestService extends TranslateRestService implements PluginAppKeys {
private String HOST = "https://openapi.youdao.com/api";
private String APP_ID = appInfoService.get(YOUDAO_APP_ID_SAVE_KEY);
private String APP_SECRET = appInfoService.get(YOUDAO_APP_SECRET_SAVE_KEY);
private String DIGEST_KEY = "SHA-256";
public YoudaoTranslateRestService() {
super();
if (!isInit.get()) {
super.init();
}
}
@Override
public String translate(String word) {
Map<String, String> params = getParams(word);
StringBuilder builder = new StringBuilder(HOST + "?");
params.entrySet().forEach(ent -> {
builder.append(ent.getKey() + "=" + ent.getValue() + "&");
});
String requestUrl = builder.toString();
requestUrl = requestUrl.substring(0, requestUrl.length() - 1);
YoudaoTranslateResult result = restTemplate.getForObject(requestUrl, YoudaoTranslateResult.class);
if (result.getErrorCode().equals("0")) {
return result.getTranslation().get(0);
}
return null;
}
private Map<String, String> getParams(String word) {
Map<String, String> params = new HashMap<>();
String salt = String.valueOf(System.currentTimeMillis());
params.put("from", "auto");
params.put("to", "en");
params.put("signType", "v3");
String curtime = String.valueOf(System.currentTimeMillis() / 1000);
params.put("curtime", curtime);
String signStr = APP_ID + truncate(word) + salt + curtime + APP_SECRET;
String sign = getDigest(signStr, DIGEST_KEY);
params.put("appKey", APP_ID);
params.put("q", word);
params.put("salt", salt);
params.put("sign", sign);
return params;
}
public static String truncate(String q) {
if (q == null) {
return null;
}
int len = q.length();
return len <= 20 ? q : (q.substring(0, 10) + len + q.substring(len - 10, len));
}
}
不要忘記把子類定義為輕量級的服務。這裡我們還沒有做英文單詞的駝峰化。
這裡需要将傳回值封裝為對象,根據文檔中給出的傳回資訊,我們隻需要處理一定傳回的項目即可。
是以,增加有道傳回的對象:
調用這些接口,可能出現各種問題,需要找廠商的客服進行調試。
百度翻譯
百度翻譯也差不多,根據百度翻譯開放平台 (baidu.com)文檔,找到示例程式,拷貝到子類,進行調用。
import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.BaiduTranslateResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.util.CollectionUtils;
@Service
public final class BaiduTranslateRestService extends TranslateRestService implements PluginAppKeys {
private String HOST = "http://api.fanyi.baidu.com/api/trans/vip/translate";
private String APP_ID = appInfoService.get(BAIDU_APP_ID_SAVE_KEY);
private String APP_SECRET = appInfoService.get(BAIDU_APP_SECRET_SAVE_KEY);
private String DIGEST_KEY = "MD5";
public BaiduTranslateRestService() {
super();
if (!isInit.get()) {
super.init();
}
}
@Override
public String translate(String word) {
Map<String, String> params = getParams(word);
StringBuilder builder = new StringBuilder(HOST + "?");
params.entrySet().forEach(ent -> {
builder.append(ent.getKey() + "=" + ent.getValue() + "&");
});
String requestUrl = builder.toString();
requestUrl = requestUrl.substring(0, requestUrl.length() - 1);
BaiduTranslateResult result = restTemplate.getForObject(requestUrl, BaiduTranslateResult.class);
if (Objects.isNull(result.getError_code()) && !CollectionUtils.isEmpty(result.getTrans_result())) {
return result.getTrans_result().get(0).getDst();
}
return null;
}
private Map<String, String> getParams(String word) {
Map<String, String> params = new HashMap<String, String>();
params.put("q", word);
params.put("from", "zh");
params.put("to", "en");
params.put("appid", APP_ID);
// 随機數
String salt = String.valueOf(System.currentTimeMillis());
params.put("salt", salt);
// 簽名
String src = APP_ID + word + salt + APP_SECRET; // 加密前的原文
params.put("sign", getDigest(src, DIGEST_KEY).toLowerCase());
return params;
}
}
百度翻譯的傳回對象定義
廠商擴充
上面兩個廠商的免費額度有限,或者說因為各種原因,無法使用,那麼可以選擇另外其他的廠商。
是以廠商的擴充就很有必要。
以DeepL翻譯為例,這是一個提供機器翻譯的網站,根據介紹是使用機器學習,實作的線上翻譯。
當然也需要新增賬號資訊,擷取app_id和app_secret。DeepL翻譯API|機器翻譯技術
其技術文檔在這裡:DeepL API
每增加一個廠商,就需要同步增加配置資訊。
是以我們根據廠商要求,增加相應的配置界面。
然後在界面中增加資料設定和讀取的方法
接着和其他配置相同的處理,在初始化界面時,将已有的值放入,判斷是否修改,然後進行儲存
然後實作抽象的RestAPI類,定義deepl的子類翻譯
import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.DeeplResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.groovy.util.Maps;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@Service
public final class DeeplTranslateRestService extends TranslateRestService implements PluginAppKeys {
private String HOST = "https://api-free.deepl.com/v2/translate";
private String APP_SECRET = appInfoService.get(DEEPL_APP_SECRET_SAVE_KEY);
public DeeplTranslateRestService() {
super();
if (!isInit.get()) {
init();
}
}
@Override
public String translate(String word) {
Map<String, String> params = getParams(word);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-Type", "application/x-www-form-urlencoded");
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
params.entrySet().forEach(ent -> {
map.put(ent.getKey(), Collections.singletonList(ent.getValue()));
});
RequestEntity<MultiValueMap<String, String>> request = new RequestEntity<>(map, httpHeaders, HttpMethod.POST, URI.create(HOST));
DeeplResult result = restTemplate.postForObject(HOST, request, DeeplResult.class, Maps.of("auth_key", APP_SECRET));
if (Objects.nonNull(result) && !CollectionUtils.isEmpty(result.getTranslations())) {
return result.getTranslations().get(0).getText();
}
return null;
}
private Map<String, String> getParams(String word) {
Map<String, String> params = new HashMap<>();
params.put("text", word);
// 非必填
params.put("source_lang", "ZH");
params.put("target_lang", "EN-US");
params.put("auth_key", APP_SECRET);
return params;
}
}
需要注意的是我們使用的
Service
注解是idea-platfrom的,而不是spring的。
然後在Action中調用即可。
暫時我們隻是将翻譯的結果使用通知輸出,實際在Action中還應該對英文單詞結果做駝峰化,以及多個廠商之間的排程操作,還有就是需要替換選中的中文。現在還剩下這些未完成,當然這些前提是你至少有一個廠商能進行翻譯。
編寫Action後續操作
首先我們有多個用于翻譯的RestApi,是以我們建立一個排程工具,排程工具也非常簡單,就是輪訓。
當我們得到了翻譯後的英文語句後,需要轉為駝峰形式
因為我們翻譯可能翻譯的是一個詞組,當翻譯的是詞組的時候,傳回的就不是單詞,而是短語,短語是通過空格分割的,是以我們需要将傳回的英文字元串根據空格拆分,然後第一個單詞轉為小寫,取餘單詞的第一個首字母大寫,然後拼接起來就行了
接着我們需要控制什麼時候可用翻譯功能,當使用者沒有選中字元的時候,是不能使用字元的
最後一步,我們需要使用翻譯後的英文字元串,并且是轉為駝峰形式的字元串替換掉選中的中文字元
效果
打包
打包直接使用ide的打包功能即可
打包後的zip包就可以釋出給其他人使用了
最後的最後
配置的地方增加點說明,告訴使用者該去哪裡注冊。
增加的文本區不可編輯。
總結
通過這個小插件,學習了idea插件中服務的定義,服務的擷取和使用。
通過調用線上翻譯API,學習了restTemplate的使用和配置。