Idea插件开发(三)——插件JSL的完整开发过程

Idea插件开发(一)——插件的分类及基础认识
Idea插件开发(二)——插件的创建打包及发布
Idea插件开发(三)——插件JSL的完整开发过程

上篇主要介绍了通过两种模式创建、打包、发布插件,本篇我将记录自己写的插件 JSL 完整的开发过程,插件开发也遵从软件开发流程,同样拥有开发周期和迭代升级的维护周期,当然这完全取决于开发者。

JSL(Jpa Sql Log)

插件开源地址

插件功能描述

  • 这是我开发的一个类似mybatis log plugin的插件,说到这儿应该很多人都能猜到是什么了,我简要的说明下:我们知道ORM映射框架有很多,比较出名的有MyBatis、Hibernate、SpringDataJpa、JdbcTemplate等,这里我就是针对Jpa开发的一个控制台Sql语句还原的插件。因为Jpa开启sql日志后输出的都是带问号的sql,这个缺点和mybatis一样,所以受到mybatis log plugin插件的启发,于是开发了Jpa Sql Log这个插件。

插件截图

在这里插入图片描述

插件实现功能

例1: 将 select * from table where _id=?转换为 select * from table where _id=1

例2: 将 update table set create_date=?, update_date=?, product_id=?, status=? where id=?转换为 update table set create_date=2020-07-01 11:48:12.0, update_date=2020-07-01 11:48:12.0, product_id=1, status=0 where id=1

实现思路

因为我是针对IDEA开发的,所以我以IDEA的界面来介绍插件开发思路。
在这里插入图片描述

  • 控制台根据工具栏的选择显示不同的内容,因此我的实现思路首先就是在工具栏放一个工具,取名JSL(Jpa Sql Log),当用户点击按钮时显示一个Console窗口,当Run窗口中的日志出现sql日志时将其和参数值拼接后输出到我的JSL窗口中。看起来很简单,实现起来也不复杂,只是在实现过程中遇到些问题找不到解决办法很花时间,所以才想记录下来。
    这里有一点,我们知道Java也有自己的界面编程API,即Java Swing,它允许用户编写带有按钮、输入框、表格等元素的窗口程序,其实这里我编写Console的窗口就是用Java Swing的JComponent组件,在其中放置了一个ConsoleView组件用于输出Text形式的内容。

配置插件触发入口

首先回顾一下插件包目录的内容:

└── plugins
    └── sample.jar
        ├── com/foo/...
        │   ...
        │   ...
        └── META-INF
            ├── plugin.xml
            ├── pluginIcon.svg
            └── pluginIcon_dark.svg

以下为插件JSL的目录:在这里插入图片描述

plugin.xml作为核心文件记录了整个插件的关键配置信息,首先就是插件触发入口,被称为Action,在plugin.xml如下配置(完整的plugin.xml文末记录):

<actions>
    <!-- Add your actions here -->
    <action id="cn.feelcode.jpa.JpaSqlLogAction" class="cn.feelcode.jpa.JpaSqlLogAction" text="jpa sql log"
            description="格式化jpa生成的log,拼接为标准sql语句" icon="/icons/logo2_1616.png">
        <add-to-group group-id="ConsoleView.PopupMenu" anchor="last"/>
    </action>
</actions>

Action可以有多个,在菜单栏,工具栏都可以添加,如上我注册了插件JSL的启动方式,Action包含一些基本属性,必须配置

  • id指定Action的标识
  • class指定Action的实现类
  • text表示文本的内容
  • description为描述
  • icon为图标路径
    添加add-to-group标签,指定触发插件的形式,group-id="ConsoleView.PopupMenu"表示在控制台右键中添加该Action,anchor表示位置,last就是放在最末尾,最终的效果如图:在这里插入图片描述

右键时菜单最后显示为我设置的内容,左下角显示了description信息。
触发方式有很多种,大概分三种:工具栏、菜单栏、快捷键。具体可以查看这里,下面会讲如何变更触发方式在这里插入图片描述

Groups包含了所有的触发形式,下方的keyboard shortcuts支持快捷键触发。
输入group-id后会有自动补全代码功能弹出选择项,此时可以更改触发方式在这里插入图片描述

还可以通过Action类,首先创建包路径cn.feelcode.jpa,新建JpaSqlLogAction继承AnAction

package cn.feelcode.jpa;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindowManager;
import org.jetbrains.annotations.NotNull;

/**
 * @ClassName: JpaSqlLogAction
 * @Author: zhangyingqi
 * @Description: sql restore action
 * @Date: Created in 17:24 2020/7/9
 * @Modified By:
 */
public class JpaSqlLogAction extends AnAction {

    @Override
    public void update(AnActionEvent e) {
        // Using the event, evaluate the context, and enable or disable the action.
        Project project = e.getProject();
        e.getPresentation().setEnabledAndVisible(project != null);
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent event) {

        ToolWindowManager.getInstance(event.getProject()).getToolWindow("JpaSqlLog").show();
        //ToolWindow toolWindow = ToolWindowManager.getInstance(event.getProject()).getToolWindow("JpaSqlLog");
        //new JpaToolWindow(event.getProject(),toolWindow);
    }

}

需要重写update方法和actionPerformed方法,简单介绍这两个方法:

  • update()方法实现启用或禁用Action的代码
  • actionPerformed()方法实现在用户调用Action时执行的代码

清空 plugin.xmlactions标签的内容,将鼠标悬停到 class JpaSqlLogAction上然后点击Register Action,或者选中JpaSqlLogActionAlt + Shift + Enter在这里插入图片描述

操作之后将弹框提示New Action,按照图示对应填写即可,在Add to Group中直接输入可快速匹配到相似的值在这里插入图片描述

配置插件入口动作

通过上面的配置启动插件后已经生效了,在控制台右键将能看到配置的插件启动入口,只是现在点击没有用,是由于还未配置相应的动作,接下来开始配置。
因为我要实现点击后显示一个新的控制台,用于最后显示从Console筛选出来的Sql语句,所以在actionPerformed方法中增加一个对应的内容

@Override
public void actionPerformed(@NotNull AnActionEvent event) {
    ToolWindowManager.getInstance(event.getProject()).getToolWindow("JpaSqlLog").show();
}

通过上段代码即可实现显示一个名为JpaSqlLog的工具窗口,这里注意,工具窗显示的前提是需要先注册一个名为JpaSqlLog的工具窗口才行,接下来我们创建它
cn.feelcode.jpa包下新建JpaToolWindow类,实现ToolWindowFactory接口,重写createToolWindowContent方法

package cn.feelcode.jpa;

import com.intellij.execution.filters.TextConsoleBuilder;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.Content;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;

/**
 * @ClassName: JpaToolWindowFactory
 * @Author: zhangyingqi
 * @Description: TODO
 * @Date: Created in 10:25 2020/7/10
 * @Modified By:
 */
public class JpaToolWindow implements ToolWindowFactory {

    public static JComponent createConsolePanel(ConsoleView view) {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(view.getComponent(), BorderLayout.CENTER);
        return panel;
    }

    @Override
    public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
        TextConsoleBuilder consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project);
        ConsoleView console = consoleBuilder.getConsole();
        console.print("------------start to reading jpa sql------------" + "\n", ConsoleViewContentType.LOG_INFO_OUTPUT);
        JComponent consolePanel = createConsolePanel(console);
        Content content  = toolWindow.getContentManager().getFactory().createContent(consolePanel, "jpa sql plugin", false);
        toolWindow.getContentManager().addContent(content);
        new ToolWindowConsole(toolWindow, console, project);
        PropertiesCenter.init(project);
    }

}

通过createToolWindowContent创建一个工具窗口,在该窗口中创建一个Console组件,在Console中打印一行内容------------start to reading jpa sql------------,其中以下两句为初始化一些配置,可以先放一边。

new ToolWindowConsole(toolWindow, console, project);
PropertiesCenter.init(project);

至此已经完成了新控制台的创建并且输出了一行数据,但是这个Console控制台暂时不可见,还要将它注册到plugin.xml才会生效。

<extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
    <consoleFilterProvider implementation="cn.feelcode.jpa.JpaLogProvider"/>
    <toolWindow factoryClass ="cn.feelcode.jpa.JpaToolWindow" id="JpaSqlLog" anchor="bottom" icon="/icons/logo2_1616.png"/>
</extensions>

打开plugin.xml,在extensions标签中添加toolWindow,指定对应的实现类地址和id,anchor="bottom"表示在底部工具栏显示,前面已经提到过IDEA工具栏在左右下方都有。这里consoleFilterProvider是指定的一个过滤器,暂时不用注意。
关于extensions标签我之前的 Idea插件开发(一)——插件的分类及基础认识 文章 插件扩展 部分有提到,它是一组插件扩展,可以注册多个扩展件,在插件启动时会自动加载。至此已经完成了新控制台的创建和内容显示。
启动效果如下在这里插入图片描述

配置控制台日志过滤器

到这距离实现功能就剩最后一部分了,现在需要拿到控制台输出的每一行日志,并且找到JPA输出的SQL语句,将参数和问号匹配。
新增控制台日志监听类JpaLogProvider,实现ConsoleFilterProvider,重写getDefaultFilters方法

package cn.feelcode.jpa;

import com.intellij.execution.filters.ConsoleFilterProvider;
import com.intellij.execution.filters.Filter;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;

/**
 * @ClassName: JpaLogProvider
 * @Author: zhangyingqi
 * @Description: 控制台过滤器
 * @Date: Created in 9:58 2020/7/10
 * @Modified By:
 */
public class JpaLogProvider implements ConsoleFilterProvider {

    @NotNull
    @Override
    public Filter[] getDefaultFilters(@NotNull Project project) {
        Filter filter = new JpaLogFilter(project);
        return new Filter[]{filter};
    }

}

同时要创建一个JpaLogFilter类,实现Filter接口,JpaLogFilter里可以接收参数currentLine就是输出的日志

package cn.feelcode.jpa;

import com.github.hypfvieh.util.StringUtil;
import com.intellij.execution.filters.Filter;
import com.intellij.openapi.project.Project;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * @ClassName: JpaLogFilter
 * @Author: zhangyingqi
 * @Description: Jpa日志拦截
 * @Date: Created in 9:25 2020/7/10
 * @Modified By:
 */
public class JpaLogFilter implements Filter {

    private final Project project;
    private static String preparingLine = "";
    private static StringBuffer parametersLine = new StringBuffer();
    private static boolean isEnd = false;
    public static Integer endIndex = 1;


    public static final String PREPARING = "org.hibernate.SQL - ";
    public static final String PARAMETERS = "org.hibernate.type.descriptor.sql.BasicBinder - ";
    public static final String SPLIT_LINE = "-----------------------------------------------------------------------------------------------------------------------";

    public JpaLogFilter(Project myProject) {
        this.project = myProject;
    }

    @Nullable
    @Override
    public Result applyFilter(@NotNull String currentLine, int i) {
        if(this.project == null) return null;
            //System.out.println("日志:"+currentLine);

            //是否一条sql+参数结束
            //这里有个bug,如果最后一条sql参数console出来后没有其他日志console,则不会触发最后一条sql的格式化动作。
            if(endIndex!=1 && !currentLine.contains(PARAMETERS)){
                //已结束Sql
                isEnd = true;
                System.out.println("=====================结束当前sql=====================");
                endIndex =1;
                //拼接sql
                System.out.println("--------------------开始格式化sql--------------------");
                formatSql();
            }
            //是否包含sql语句
            if(currentLine.contains(PREPARING)) {
                //设置当前sql语句
                preparingLine = currentLine;
                System.out.println("监测到sql语句:" + currentLine);
                System.out.println("+++++++++++++++++++++准备开始获取sql参数,初始化序号为[1]+++++++++++++++++++++");
                endIndex=1;
                //包含则return null,继续返回读取下一行日志
                return null;
            }
            //是否包含sql参数
            if(currentLine.contains(PARAMETERS)){
                Integer paramIndex = getParamIndex(currentLine);
                String paramValue = getParamValue(currentLine);
                String paramType = getParamType(currentLine);
                System.out.println("序号为["+ endIndex +"],获取到sql参数序号为:" + paramIndex + ",参数值为:" + paramValue);
                endIndex++;
                if(paramIndex==1){
                    parametersLine.append(PARAMETERS);
                }
                if(paramIndex>1){
                    parametersLine.append(", ");
                }
                parametersLine.append(paramValue);
                parametersLine.append("(");
                parametersLine.append(paramType);
                parametersLine.append(")");
                //继续获取参数
                return null;
            }

            //sql语句为空则继续读下一行日志
            if(StringUtils.isEmpty(preparingLine)) {
                return null;
            }
        return null;
    }

    private Integer getParamIndex(String sqlParam){
        try {
            if(!sqlParam.contains("binding parameter")){
                return -1;
            }
            String prefixIndex = "binding parameter [";
            String suffixIndex = "] as ";
            String index = sqlParam.substring(sqlParam.lastIndexOf(prefixIndex) + prefixIndex.length(),sqlParam.indexOf(suffixIndex));
            if(StringUtil.isEmpty(index)){
                return -1;
            }
            return Integer.parseInt(index);
        } catch (NumberFormatException e) {
            e.printStackTrace();
            return -1;
        }
    }

    private String getParamValue(String sqlParam){
        String prefixParam = "[";
        String suffixParam = "]";
        String param = sqlParam.substring(sqlParam.lastIndexOf(prefixParam)+1,sqlParam.lastIndexOf(suffixParam));
        if(StringUtil.isEmpty(param)){
            return "";
        }
        return param;
    }

    private String getParamType(String sqlParam){
        String prefixType = "as [";
        String suffixType = "] - ";
        String type = sqlParam.substring(sqlParam.lastIndexOf(prefixType) + prefixType.length(),sqlParam.lastIndexOf(suffixType));
        if(StringUtil.isEmpty(type)){
            return "";
        }
        return type;
    }

    private void formatSql(){
        if(StringUtils.isNotEmpty(preparingLine) && StringUtils.isNotEmpty(parametersLine.toString()) && isEnd) {
            int indexNum = PropertiesCenter.getIndex(project);
            String preStr = "------- " + indexNum + " -------" + parametersLine.toString().split(PARAMETERS)[0].trim();//序号前缀字符串
            PropertiesCenter.setIndex(project, ++indexNum);
            String restoreSql = FormatJpaSql.restoreSql(preparingLine, parametersLine.toString());
            ToolWindowConsole.logJpa(preStr);
            ToolWindowConsole.logJpa(restoreSql);
            ToolWindowConsole.logJpa(SPLIT_LINE);
            preparingLine = "";
            parametersLine = new StringBuffer();
            isEnd = false;
        }
    }

}
  • 控制台输出的JPA SQL 参数:PREPARING = "org.hibernate.SQL - ";
  • 控制台输出的JPA SQL 语句:PARAMETERS = "org.hibernate.type.descriptor.sql.BasicBinder - ";
    通过这两个字符串就可以判断输出的日志是否为数据库执行语句,由此再经过一些格式化转换就能匹配出完整的SQL语句了。
    注意 JpaLogProvider类同样要注册到 plugin.xml中,指定实现类即可生效
<extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
    <consoleFilterProvider implementation="cn.feelcode.jpa.JpaLogProvider"/>
    <toolWindow factoryClass ="cn.feelcode.jpa.JpaToolWindow" id="JpaSqlLog" anchor="bottom" icon="/icons/logo2_1616.png"/>
</extensions>

到此完成了所有的组件开发,下面是我的plugin.xml配置文件

<idea-plugin require-restart="true">
    <id>cn.feelcode.jpa-sql-log</id>
    <name>jpa sql log</name>
    <vendor email="824247231@qq.com" url="http://www.feelcode.cn">Julier</vendor>
    <!-- 插件版本 -->
    <version>0.9</version>
    <description><![CDATA[
    <h2>jpa sql log</h2>
    <p>This a plugin from <a href="http://www.feelcode.cn">www.feelcode.cn</a> which made by Julier.</p>
    <p>You can use it to restore your jpa sql.</p>
    <hr style="border-bottom:3px double;">
    <h4>eg1:</h4>
    <p> from </p>
    <xmp>    select * from table where _id=?</xmp>
    <p> to </p>
    <xmp>    select * from table where _id=1</xmp>
    <hr>
    <h4>eg2:</h4>
    <p> from </p>
    <xmp>    update table set create_date=?, update_date=?, product_id=?, status=? where id=?</xmp>
    <p> to </p>
    <xmp>    update table set create_date=2020-07-01 11:48:12.0, update_date=2020-07-01 11:48:12.0, product_id=1, status=0 where id=1</xmp>
    <hr>
    <p>First at all,you must create a file named logback.xml or other,remeber that you must make the console log display jpa sql and parameter through configuration,maybe is application.yml or other config file, or you can add this in logback.xml</p>
    <xmp>
    <logger name="org.hibernate.SQL" level="DEBUG"/>
    <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
    </xmp>
    <p>enjoy it!</p>
    <hr style="border-bottom:3px double;">
    <p>Hello,这是基于使用Jpa持久层ORM框架的一个格式化sql语句的插件,你无需做任何操作即可将带 ? 的控制台sql转化为带真实参数的sql语句,当然这是临时的,仅仅为了方便调试。
    插件执行的前提是控制台有原生sql输出,这要求您打开Jpa的sql日志输出,并且打开参数输出,可能因此您的控制台将输出更多内容,但是这一切仅仅发生在本地debug环境。您可以在生产配置中去除此日志。</p>
    <p>关于日志输出的配置可以参考以下:</p>
    <xmp>
    <logger name="org.hibernate.SQL" level="DEBUG"/>
    <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
    </xmp>
    <p>enjoy it!</p>
    <p>call me :</p>
    <p>QQ:824247231</p>
    <p>Email:824247231@qq.com</p>
    ]]></description>

    <change-notes><![CDATA[
    <label>2020.7.12</label>
    <ul>
        <li>Initial version</li>
    </ul>
    <label>2020.7.28</label>
    <ul>
        <li>Add Icon</li>
        <li>Optimized Code</li>
    </ul>
    ]]></change-notes>

    <!-- please see https://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
         on how to target different products -->
    <depends>com.intellij.modules.platform</depends>

    <extensions defaultExtensionNs="com.intellij">
        <!-- Add your extensions here -->
        <consoleFilterProvider implementation="cn.feelcode.jpa.JpaLogProvider"/>
        <toolWindow factoryClass ="cn.feelcode.jpa.JpaToolWindow" id="JpaSqlLog" anchor="bottom" icon="/icons/logo2_1616.png"/>
    </extensions>

    <actions>
        <!-- Add your actions here -->
        <action id="cn.feelcode.jpa.JpaSqlLogAction" class="cn.feelcode.jpa.JpaSqlLogAction" text="jpa sql log"
                description="格式化jpa生成的log,拼接为标准sql语句" icon="/icons/logo2_1616.png">
            <add-to-group group-id="ConsoleView.PopupMenu" anchor="last"/>
        </action>
    </actions>

</idea-plugin>

PS:JSL插件的使用请参照具体使用说明,要注意你的项目必须打印源SQL,可以直接在你的项目配置中设置控制台输出SQL语句,如果是用logback.xml日志配置则只要在<configuration>标签中添加如下配置:

<logger name="org.hibernate.SQL" level="DEBUG"/>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" />

结束语

最后再放一下我开发的Jpa Sql Log插件开源地址:Jpa Sql Log,起初在寻找创建控制台工具窗口的时候我走了不少弯路,最后通过官方的论坛搜索才实现了插件的功能,过程花费了近半个月,现在总结的时候发现其实都很简单,不过我刚开始做插件从零开始,由于在国内的网站查找不到对应的资料,官方文档看起来很详细但是对于特定的插件几乎没什么资料可循,在此也感谢开源旧版本的插件mybatis-log-plugin,虽然现在收费了但是我还是受到了很大的启发和帮助,如果有开发插件的问题可以去官方论坛提问或搜索。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页