插件預覽:

一、開發環境配置
1、idea社區版(Community Edition)
2、IntelliJ Plateform Plugin SDK
3、安裝Plugin Devkit插件
在項目Project Structure添加Intellij IDEA SDK

二、開發插件
新建項目,選擇Intellij Platform Plugin,SDK選擇剛才添加的IDEA SDK,然后點擊next

默認項目結構如下:

src表示插件代碼目錄,resources表示插件資源目錄,plugin.xml為插件的描述文件,和一些配置信息
plugin.xml文件默認如下:
<idea-plugin>
<id>com.your.company.unique.plugin.id</id>
<name>Plugin display name here</name>
<version>1.0</version>
<vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
]]></description>
<change-notes><![CDATA[
Add change notes here.<br>
<em>most HTML tags may be used</em>
]]>
</change-notes>
<!-- please see https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html for description -->
<idea-version since-build="173.0"/>
<!-- please see https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html
on how to target different products -->
<depends>com.intellij.modules.platform</depends>
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
</extensions>
<actions>
<!-- Add your actions here -->
</actions>
</idea-plugin>
新創建會看到description和change-notes內容報紅,可不用管,修改內容之后會恢復正常,
其中id表示插件唯一的id,不可與其他插件沖突,插件不同版本之間不可修改
name表示插件的名稱,發版成功別人可以在插件市場根據名稱進行搜索
version 插件的版本號
vendor 插件的供應商 也就是作者名稱
description 插件的描述,不能使用默認值,必須修改成自己的,並且需要大於40字符
change-notes 插件的修改日志,支持html標簽
然后開始創建一個action,如果安裝了Devki插件,可以快速生成Action,在new的時候選擇Plugin Devkit Action

點擊可以進行快速創建Action,其中Name表示Action的name,這里Group選擇EditorPopupMenu表示右擊出現GenerateResource選項。下面KeyBoard Shortcuts表示觸發的快捷鍵,這里除了右擊出現GenerateResource會觸發外我們可以使用快捷鍵Ctrl S+B.

點擊OK,會自動創建一個類繼承AnAction,重寫方法actionPerforned表示觸發之后執行的操作。我們需要在這里編寫代碼

在plugin.xml會自動添加Action的配置信息

然后開始編寫actionPerformed方法,比如這里我們在執行操作之后輸出一條信息
@Override
public void actionPerformed(AnActionEvent e) {
Editor editor = e.getData(PlatformDataKeys.EDITOR);
Messages.showMessageDialog(editor.getProject(), "輸出一條提示信息", "提示", Messages.getInformationIcon());
}

三、調試、部署
編寫完成,需要進行測試,跟正常java代碼一樣。我們可以debug
點擊run或者debug來啟動插件項目

啟動完成,會重新打開idea的一個窗口,在新開的窗口可以調試自己的插件,
這里我們右擊編輯窗口,可以看到剛才添加的action

點擊可以看到輸出一條信息

開發完成,需要我們打包供自己或別人使用
點擊上方菜單build -> Prepare Plugin Module xxx For Deployment。可以在項目生成一個插件的jar包

在使用時,可以在plugins選擇從磁盤安裝剛才的插件,導入生成的jar包重啟idea可使用

當需要發布到插件市場別人可以搜索到時,我們需要注冊jetbrains賬號,點擊upload plugin
https://plugins.jetbrains.com/plugin/add#intellij

需要等待1-2個工作日等待審核通過就可以在插件市場搜索到了
四、開發插件
在許多項目中,需要將接口的地址放入resource數據庫的表中。來進行細粒度的權限控制,類似這種,需要在resource表中添加資源url,資源描述,資源名稱等字段,而這個url對應controller的@RequestMapping的value值,需要我們一個一個復制並手動書寫插入的sql語句,在實際開發中,我們無需做這種額外的費時費力的重復無用操作,可以將精力放到其他工作中,所以我們可以開發一個插件,來自動完成這些操作,來輸出數據庫的腳本。

實現思路:獲取@RequestMapping(@GetMapping、@PostMapping、@PutMapping、@DeleteMapping)注解的value值,也就是訪問時的url,資源名稱(RES_XXX)我們可以將url進行大小寫轉換,並縮短至數據庫規定大小來進行改造。資源描述:在一般開發中,我們應該按照規范在每個接口上填寫注釋,所以我們可以獲取到每個方法上的注釋,並進行簡單的匹配以及分割就可以得到這個方法的描述,也就是資源描述的信息。代碼如下:
package com.liufuqiang.packages;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.ui.Messages;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtilBase;
import org.apache.commons.lang3.StringUtils;
import javax.swing.tree.DefaultMutableTreeNode;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @date 2021/10/21
* @author liufuqiang
*/
public class GenerateResourceAction extends AnAction {
private static final String PREFIX = "/";
@Override
public void actionPerformed(AnActionEvent event) {
Editor editor = event.getData(PlatformDataKeys.EDITOR);
PsiFile psiFile = PsiUtilBase.getPsiFileInEditor(editor, editor.getProject());
//只讀文件直接返回
if( psiFile.getFileType().isReadOnly()){
return;
}
String fileName = psiFile.getVirtualFile().getName();
// 判斷文件后綴是不是Controller
String fileSuffix = "Controller.java";
if (!fileName.endsWith(fileSuffix)) {
return;
}
String baseUrl = "";
Document document = PsiDocumentManager.getInstance(event.getProject()).getDocument(psiFile);
DefaultMutableTreeNode fileNode = new DefaultMutableTreeNode(fileName);
for (PsiElement psiElement : psiFile.getChildren()) {
if (psiElement instanceof PsiClass){
// 獲取類上面的RequestMapping注解信息
PsiClass psiClass = (PsiClass) psiElement;
for (PsiAnnotation annotation : psiClass.getAnnotations()) {
if (StringUtils.equals(annotation.getQualifiedName(), "org.springframework.web.bind.annotation.RequestMapping")) {
baseUrl = annotation.findAttributeValue("value").getText().replaceAll("\"", "").trim();
}
}
if (StringUtils.isNotBlank(baseUrl) && !baseUrl.startsWith("/")) {
baseUrl = PREFIX.concat(baseUrl);
}
// 方法列表
List<Map<String, String>> resourceList = new ArrayList<>(20);
PsiMethod[] methods = psiClass.getMethods();
for (PsiMethod method : methods) {
PsiAnnotation[] annotations = method.getAnnotations();
for (PsiAnnotation annotation : annotations) {
String qualifiedName = annotation.getQualifiedName();
if (!StringUtils.equals(qualifiedName, "org.springframework.web.bind.annotation.RequestMapping")
&& !StringUtils.equals(qualifiedName, "org.springframework.web.bind.annotation.GetMapping")
&& !StringUtils.equals(qualifiedName, "org.springframework.web.bind.annotation.PostMapping")
&& !StringUtils.equals(qualifiedName, "org.springframework.web.bind.annotation.PutMapping")
&& !StringUtils.equals(qualifiedName, "org.springframework.web.bind.annotation.DeleteMapping")) {
continue;
}
Map<String, String> params = new HashMap<>(3);
PsiAnnotationMemberValue annotationMemberValue = annotation.findAttributeValue("value");
String memberValue = annotationMemberValue.getText().replaceAll("\"", "").trim();
if (StringUtils.isNotBlank(memberValue) && !memberValue.startsWith("/")) {
memberValue = PREFIX.concat(memberValue);
}
String resourceUrl = baseUrl.concat(memberValue);
// resource_url
params.put("resource_url", resourceUrl);
// resource_name
String resourceName = humpToUnderline(resourceUrl);
if (resourceName.length() > 50) {
resourceName = resourceName.substring(0, 50);
}
params.put("resource_name", resourceName);
// resource_desc
String resourceDesc = checkMethodComment(document, method);
params.put("resource_des", resourceDesc);
resourceList.add(params);
continue;
}
}
if (resourceList.size() == 0) {
return;
}
outputSqlInfo(editor, resourceList);
}
}
}
/**
* 輸出sql語句
* @param editor
* @param resourceList
*/
public void outputSqlInfo(Editor editor, List<Map<String, String>> resourceList) {
StringBuilder sb = new StringBuilder();
sb.append("-- sa_resource");
sb.append("SET @parent_id = \"0\";\n");
for (Map<String, String> param : resourceList) {
String resourceSql = "INSERT INTO `sa_resource` (`id`, `p_id`, `resource_name`, `resource_des`, `resource_type`, `resource_url`, `curr_status`, `relation`, `company_id`, `create_user`, `create_time`,`update_user`, `update_time`, `status`) \n" +
"VALUES (CONCAT(UUID_SHORT(),''), @parent_id, '%s', '%s', NULL, '%s', NULL, NULL, '', NULL, NOW(), NULL, NOW(), NULL);\n";
sb.append(String.format(resourceSql, param.get("resource_name"), param.get("resource_des"), param.get("resource_url")));
sb.append("\n");
}
Messages.showMessageDialog(editor.getProject(), sb.toString(), "總共有方法" + resourceList.size() + "個", Messages.getInformationIcon());
}
/**
* 小寫轉大寫
* @param var1
* @return
*/
public static String humpToUnderline(String var1) {
StringBuilder result = new StringBuilder();
if (var1 != null || var1.length() > 0) {
result.append("RES_");
result.append(var1.substring(0, 1).toUpperCase());
for (int i = 1; i < var1.length(); i++) {
String var2 = var1.substring(i, i + 1);
// 在大寫字母前添加下划線
if (var2.equals(var2.toUpperCase()) && !Character.isDigit(var2.charAt(0))) {
result.append("_");
}
result.append(var2.toUpperCase());
}
}
return result.toString().replaceAll("/", "");
}
/**
* 獲取注釋
* @param document
* @param psiMethod
* @return
*/
private String checkMethodComment(Document document, PsiMethod psiMethod){
String comment = "";
PsiComment classComment = null;
for (PsiElement tmpEle : psiMethod.getChildren()) {
if (tmpEle instanceof PsiComment){
classComment = (PsiComment) tmpEle;
// 注釋的內容
String tmpText = classComment.getText();
String pattern = "[\\u4E00-\\u9FA5A-Za-z0-9]+";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(tmpText);
while (m.find()) {
comment = m.group(0);
break;
}
}
}
return comment;
}
}
開發完成,我們可以進行sql語句的模板替換並進行輸出,最后輸出結果如下:

比如這個方法

我們可以看到最后生成的sql語句為
INSERT INTO `sa_resource` (`id`, `p_id`, `resource_name`, `resource_des`, `resource_type`, `resource_url`, `curr_status`, `relation`, `company_id`, `create_user`, `create_time`,`update_user`, `update_time`, `status`) VALUES (CONCAT(UUID_SHORT(),''), @parent_id, 'RES_COMPANY_POLICY_REPORT_INIT', '主頁面', NULL, '/companyPolicyReport/init', NULL, NULL, '', NULL, NOW(), NULL, NOW(), NULL);

滿足我們當時的要求,自此可以進行一鍵生成所需要的sql語句,所以此插件名Generate Resource SQL,

可以在idea插件市場搜索Generate Resource SQL,重啟idea。在controller類里右擊鼠標,點擊Generate Resource SQL進行使用

項目已上傳至Github: https://github.com/LiuFqiang/GeneratePlugin
插件主頁:https://plugins.jetbrains.com/plugin/17843-generate-resource-sql
