- 介绍
- 服务
- 轻量级服务
- 服务定义
- 服务获取
- 实例
- 目标
- 分解
- 准备在线翻译信息
- 有道翻译
- 必应翻译
- 百度翻译
- 创建插件项目
- 创建配置界面
- 引入第三方依赖
- 定义存储的服务
- 定义配置界面
- 创建Action
- 封装抽象RestAPI
- 有道翻译
- 百度翻译
- 厂商扩展
- 编写Action后续 *** 作
- 效果
- 打包
- 最后的最后
- 总结
gitee地址:https://gitee.com/jyq_18792721831/studyplugin.git
idea插件开发入门
idea插件开发–配置
idea插件开发–服务-翻译插件
本次主要介绍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)注册账号,注册送50人民币,自己玩足够了。
然后创建文本翻译的应用
在个人信息里能看到应用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的博客-CSDN博客
首先创建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中注册
当做好这些后,就可以调试一下之前写的代码了
还是很不错的,简单明了,记得测试下存储服务是否正常。
创建Action创建Action很简单,之前就用过:idea插件开发入门_a18792721831的博客-CSDN博客
我们创建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); } 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) { Mapparams = 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 getParams(String word) { Map 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) { Mapparams = 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 getParams(String word) { Map params = new HashMap (); 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) { Mapparams = getParams(word); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); MultiValueMap map = new linkedMultiValueMap<>(); params.entrySet().forEach(ent -> { map.put(ent.getKey(), Collections.singletonList(ent.getValue())); }); RequestEntity > 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 getParams(String word) { Map 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的使用和配置。
通过各个厂商的扩展,进一步理解了抽象,以及抽象类和子类之间的关系,换句话说,这一定程度上增加了我的抽象能力。
而Action的各种逻辑,则是对idea插件平台有了进一步的理解,包括如何替换选中的字符,如何控制插件功能是否可用等等。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)