在自定义注解与拦截器实现不规范sql拦截(拦截器实现篇)中提到过,写了一个idea插件来辅助对Mapper接口中的方法添加自定义注解,这边记录一下插件的实现。
需求简介
在上一篇中,定义了一个自定义注解对需要经过where判断的Mapper sql方法进行修饰。那么,现在想使用一个idea插件来辅助进行自定义注解的增加,需要做到以下几点:
- 支持在接口名带Mapper的编辑页面中,右键菜单,显示增加注解信息的选项
- 鼠标移动到该选项,支持显示可选的需要新增的注解名称
- 点击增加,对当前Mapper中的所有方法增加对应注解;同时,没有import的文件中需要增加对应的包导入。
具体实现
插件开发所需前置
第一点就是需要gradle进行打包,所以需要配置gradle项目和对应的配置文件;第二点就是在Project Structure中,将SDK设置为IDEA的sdk,从而导入支持对idea界面和编辑内容进行处理的api。idea大多数版本本身就会提供plugin开发专用的project,对应的配置文件会在project模板中初始化,直接用就行。
插件配置文件
plugin.xml,放在reources的META-INF元数据文件夹下,自动进行插件基本信息的读取:
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin><!-- Unique identifier of the plugin. It should be FQN. It cannot be changed between the plugin versions. --><id>com.huiluczp.checkAnnocationPlugin</id><!-- Public plugin name should be written in Title Case.Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-name --><name>CheckAnnocationPlugin</name><!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. --><vendor email="970921331@qq.com" url="https://www.huiluczp.com">huiluczP</vendor><!-- Description of the plugin displayed on the Plugin Page and IDE Plugin Manager.Simple HTML elements (text formatting, paragraphs, and lists) can be added inside of <![CDATA[ ]]> tag.Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description --><description>Simple annotation complete plugin used for mybatis mapping interface.</description><!-- Product and plugin compatibility requirements.Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html --><depends>com.intellij.modules.platform</depends><depends>com.intellij.modules.lang</depends><depends>com.intellij.modules.java</depends><!-- Extension points defined by the plugin.Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html --><extensions defaultExtensionNs="com.intellij"></extensions><actions><group id="add_annotation_group" text="Add Self Annotation" popup="true"><!-- EditorPopupMenu是文件中右键会显示的菜单 --><add-to-group group-id="EditorPopupMenu" anchor="last"/><action id="plugin.demoAction" class="com.huiluczp.checkannotationplugin.AnnotationAdditionAction" text="@WhereConditionCheck"description="com.huiluczP.annotation.WhereConditionCheck"></action></group></actions>
</idea-plugin>
对插件功能实现来说,主要需要关注的是actions部分,其中,设置了一个名为add_annotation_group的菜单组,在这个标签中,使用add-to-group标签将其插入EditorPopupMenu
中,也就是右键展开菜单。最后,在我们定义的菜单组中,增加一个action,也就是点击后会进行对应功能处理的单元,在class中设置具体的实现类,并用text设置需要显示的信息。
功能类实现
将所有功能都塞到了AnnotationAdditionAction类中。
public class AnnotationAdditionAction extends AnAction {private Project project;private Editor editor;private String annotationStr;private AnActionEvent event;private String fullAnnotationStr;@Override// 主方法,增加对应的注解信息public void actionPerformed(AnActionEvent event) {project = event.getData(PlatformDataKeys.PROJECT);editor = event.getRequiredData(CommonDataKeys.EDITOR);// 获取注解名称annotationStr = event.getPresentation().getText();fullAnnotationStr = event.getPresentation().getDescription();// 获取// 获取所有类PsiClass[] psiClasses = getAllClasses(event);// 对类中所有满足条件的类增加Annotationfor(PsiClass psiClass:psiClasses){// 满足条件List<String> methodNames = new ArrayList<>();if(checkMapperInterface(psiClass)) {PsiMethod[] psiMethods = psiClass.getMethods();for (PsiMethod psiMethod : psiMethods) {PsiAnnotation[] psiAnnotations = psiMethod.getAnnotations();boolean isExist = false;System.out.println(psiMethod.getName());for (PsiAnnotation psiAnnotation : psiAnnotations) {// 注解已存在if (psiAnnotation.getText().equals(annotationStr)){isExist = true;break;}}// 不存在,增加信息if(!isExist){System.out.println("add annotation "+annotationStr + ", method:" + psiMethod.getName());methodNames.add(psiMethod.getName());}}}// 创建线程进行编辑器内容的修改// todo 考虑同名,还需要考虑对方法的参数判断,有空再说吧WriteCommandAction.runWriteCommandAction(project, new TextChangeRunnable(methodNames, event));}}
实现类需要继承AnAction
抽象类,并通过actionPerformed
方法来执行具体的操作逻辑。通过event
对象,可以获取idea定义的project项目信息和editor当前编辑窗口的信息。通过获取当前窗口的类信息,并编辑对应文本,最终实现对所有满足条件的方法增加自定义注解的功能。
// 获取对应的method 并插入字符串class TextChangeRunnable implements Runnable{private final List<String> methodNames;private final AnActionEvent event;public TextChangeRunnable(List<String> methodNames, AnActionEvent event) {this.methodNames = methodNames;this.event = event;}@Overridepublic void run() {String textNow = editor.getDocument().getText();StringBuilder result = new StringBuilder();// 考虑import,不存在则增加import信息PsiImportList psiImportList = getImportList(event);if(!psiImportList.getText().contains(fullAnnotationStr)){result.append("import ").append(fullAnnotationStr).append(";\n");}// 对所有的方法进行定位,增加注解// 粗暴一点,直接找到public的位置,前面增加注解+\nString[] strList = textNow.split("\n");for(String s:strList){boolean has = false;for(String methodName:methodNames) {if (s.contains(methodName)){has = true;break;}}if(has){// 获取当前行的缩进int offSet = calculateBlank(s);result.append(" ".repeat(Math.max(0, offSet)));result.append(annotationStr).append("\n");}result.append(s).append("\n");}editor.getDocument().setText(result);}// 找到字符串第一个非空字符前空格数量private int calculateBlank(String str){int length = str.length();int index = 0;while(index < length && str.charAt(index) == ' '){index ++;}if(index >= length)return -1;return index;}}
需要注意的是,在插件中对文本进行编辑,需要新建线程进行处理。TextChangeRunnable
线程类对当前编辑的每一行进行分析,保留对应的缩进信息并增加public方法的自定义注解修饰。同时,判断import包信息,增加对应注解的import。
@Override// 当文件为接口,且名称中包含Mapper信息时,才显示对应的右键菜单public void update(@NotNull AnActionEvent event) {super.update(event);Presentation presentation = event.getPresentation();PsiFile psiFile = event.getData(PlatformDataKeys.PSI_FILE);presentation.setEnabledAndVisible(false); // 默认不可用if(psiFile != null){VirtualFile virtualFile = psiFile.getVirtualFile();FileType fileType = virtualFile.getFileType();// 首先满足为JAVA文件if(fileType.getName().equals("JAVA")){// 获取当前文件中的所有类信息PsiClass[] psiClasses = getAllClasses(event);// 只允许存在一个接口类if(psiClasses.length!=1)return;for(PsiClass psiClass:psiClasses){// 其中包含Mapper接口即可boolean isOk = checkMapperInterface(psiClass);if(isOk){presentation.setEnabledAndVisible(true);break;}}}}}
重写update方法,当前右键菜单显示时,判断是否为接口名带Mapper的情况,若不是则进行自定义注解增加功能的隐藏。
// 获取当前文件中所有类private PsiClass[] getAllClasses(AnActionEvent event){PsiFile psiFile = event.getData(PlatformDataKeys.PSI_FILE);assert psiFile != null;FileASTNode node = psiFile.getNode();PsiElement psi = node.getPsi();PsiJavaFile pp = (PsiJavaFile) psi;return pp.getClasses();}// 获取所有import信息private PsiImportList getImportList(AnActionEvent event){PsiFile psiFile = event.getData(PlatformDataKeys.PSI_FILE);assert psiFile != null;FileASTNode node = psiFile.getNode();PsiElement psi = node.getPsi();PsiJavaFile pp = (PsiJavaFile) psi;return pp.getImportList();}// 判断是否为名称Mapper结尾的接口private boolean checkMapperInterface(PsiClass psiClass){if(psiClass == null)return false;if(!psiClass.isInterface())return false;String name = psiClass.getName();if(name == null)return false;return name.endsWith("Mapper");}
最后是几个工具方法,通过psiFile来获取对应的psiJavaFile,从而得到对应的类信息。
插件打包
因为使用了gradle,直接使用gradle命令进行打包。
gradlew build
之后会自动执行完整的编译和打包流程,最终会在/build/distributions文件夹下生成对应的jar文件。
之后,在idea的settings中搜索plugins,点击配置中的本地install选项,即可选择并加载对应的插件jar。
效果展示
创建一个简单的UserMapper类。
public interface UserMapper {public String queryG();public String queryKKP();
}
在编辑页面上右键显示菜单,点击我们之前设置的新按钮增加自定义注解信息,增加成功。
总结
这次主要是记录了下简单的idea插件开发过程,idea的sdk以编辑页面为基础提供了PSI api来对当前页面与整体项目的展示进行修改,还是挺方便的。配置文件对action展示的位置进行编辑,感觉和传统的gui开发差不多。
对现在这个插件,感觉还可以拓展一下编辑界面,输进其他想增加的注解类型和展示逻辑,有空再拓展吧。