生成ORM的插件

Code Generate ORM

使用方法

首先在应用市场下载插件:

idea插件开发-Code Generate ORM_ide

然后使用方式:

idea插件开发-Code Generate ORM_java_02

选中项目名称,然后右键:

这里详细说下一些选项的配置:

idea插件开发-Code Generate ORM_ide_03

  1. 最开始的那一排,只有最后一个框里面的​​wcx​​需要注意一项,这里的话是会帮你生成对应java文件里面的作者名称:

idea插件开发-Code Generate ORM_ide_04

  1. 接下来的三排就是对应的文件生成目录,这里有一点点需要注意的下面有个选项与这里有点关联,对应的是​​options​​里面的是否生成前置包;这里如果我们勾选了这个选项,那么会在上面对应的目录下,创建一些对应的包,这里举个例子:

    idea插件开发-Code Generate ORM_intellij-idea_05

  2. 这里假设我们选择的上面的​​controller​​目录,又勾选了生成前置包,那么这里是会在​​controller​​目录下在次创建一个​​controller​​目录,如下:

idea插件开发-Code Generate ORM_ide_06

所以当我们自己已经创建了`controller`目录的时候,上面的文件夹又选择了`controller`之后,那么我们就**没必要再次勾选改选项**;
  1. 接下来就是sql连接的一些配置,不过目前只支持mysql连接,之后可以考虑在扩展,这里的话就是一些mysql的基本连接信息,没什么好说的!这里目前的话是选择的是​​mysql 8.0.20​​的驱动版本;低版本的应该也是可以的;
  2. ​options​​选项的作用解释一下:
  • mybatisPlus:这里是选配,是否生成plus的模版,考虑到有一大波用户是没有使用plus的,所以加了这个选项;同样,勾选上的话就是为我们生成plus模版;
  • 是否生成前置包:这个在前面也结合文件目录说明了,这里就是为了方便那些只需要新建基础包路径的用户,会自动帮其生成对应层的报名;
  • 其他的就是生成对应的文件配置,swagger的配置文件也会帮忙生成,然后controller里面的返回格式也是会帮忙生成的,也就是返回对应的json格式的数据;这里也可以更具自己调整;swagger的配置文件默认是会在controller层级下面新建一个config目录,然后在里面生成对应的配置;
  • 这里暂时没有想到还有什么其他的选项可插拔,也欢迎使用的人及时反馈;我个人想法是在将自己之前写的分页插件和分库分表插件也设置成可插拔,之后在说;
  1. 然后就是上面的​​Settings​​和​​Options​​都是有记忆功能的,会帮你记录你上次的行为,下次就是直接将这批数据读取出来,这样更方便一点,当然,这个也可以设置成可插拔,之后考虑给个默认值,然后在加个可插拔;
  2. 然后点击就可以生成了,对应的目录格式就是下图:

idea插件开发-Code Generate ORM_数据_07

插件的基本使用就是这些,接下来分享一下源码大致流程,也比较简单;

代码调试

其实这里我不是很想写的,因为Debug谁都会,这里随便说说;

idea插件开发-Code Generate ORM_ide_08

然后我们需要定位那个问题,就在哪断点就行,然后这里说点不同的地方

当我们想在本地构建不同版本的idea去调试的时候,我们只需要修改:​​build.gradle​

idea插件开发-Code Generate ORM_数据_09

这样就能唤起对应版本的idea了;

调试就是这些,没什么好讲的;

源码分析

接下来就是根据下面这些步骤来分析:

  • UI的绘制
  • 入口主流程讲解
  • 主核心业务详解

UI绘制

这个之前的文章也写了,我们完全是基于idea提供的可视化拖拽的形式去生成我们对应的UI界面:

idea插件开发-Code Generate ORM_中间件_10

整个UI界面就是上面的图,这个完全是根据自己的风格去定义,具体的绘制过程可以参考这篇文章:

​https://www.wolai.com/qoUnMJr5sVxwWWpDNEo89F​

不过这里有一点需要提示的是,关于那些标题的定义,如下图:

idea插件开发-Code Generate ORM_数据_11

这里通过​​JPanel​​的title方式画出来的UI可以将不同功能区域更加明显的划分出来;

UI绘制大概就是这些,哈哈哈,个人审美就是这样,有什么好建议也可以提出来;

这里UI绘制出来之后,会生成对应的字段信息,这个不需要我们自己在Java文件中去定义,UI界面上修改即可:

idea插件开发-Code Generate ORM_中间件_12

这样在java文件总,也是这个名字,所以修改成比较有识别度的名称还是有必要的;

入口主流程

plugin.xml文件分析

关于入口,看过之前文章的这里应该会比较清晰,不过这里还是一步一步的分析一下,主入口还是在​​plugin.xml​​文件中:

<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<projectService serviceImplementation="com.simple.idea.plugin.mybatis.infrastructure.data.DataSetting"/>
</extensions>

<actions>
<!-- Add your actions here -->
<action id="CodeGenerateAction" class="com.simple.idea.plugin.mybatis.action.CodeGenerateAction"
text="ORMCodeGenerate" description="Code Generate ORM" icon="/icons/logo.png">
<!-- 这里是我门触发操作的位置,这里是光标选中选中,然后右键点击,就能触发 -->
<add-to-group group-id="ProjectViewPopupMenu" anchor="last"/>
</action>

</actions>

我这里只把一些关键的信息复制出来,然后讲解一下:

  • ​projectService​​:这个标签的话就是我们用来做持久话存储的实现类权限定名;
@State(name = "DataSetting", storages = @Storage("plugin.xml"))
public class DataSetting implements PersistentStateComponent<DataState> {
private DataState state = new DataState();

public static DataSetting getInstance(Project project) {
return ServiceManager.getService(project, DataSetting.class);
}

@Nullable
@Override
public DataState getState() {
return this.state;
}

@Override
public void loadState(@NotNull DataState state) {
this.state = state;
}

public ORMConfigVO getORMConfig() {
return state.getOrmConfigVO();
}

public GenerateOptions getOptions() {
return state.getOptions();
}
}

这里的话是将我们需要存储的数据封装到​​DataState​​​类中,然后通过​​DataSetting​​​存入到​​plugin.xml​​中,这里也是我们实现用户行为记录的地方;然后这里具体用法在之后的详细流程中在提及;

然后就是​​actions​​​标签里面的​​action​​操作,这里就是我们插件的触发点,可以看看:

  • id跟类都是我们随便自定义的;
  • text这个会在我们使用插件的时候展示,也就是触发操作的名称,icon就是那个操作对应的图标
  • ​group-id​​​:这个是我们触发按钮所在的位置组,这个在代码里面可以点击进去的,是idea提供的一系列操作位置,比如File、Edit、View等类似的操作,也是对应的不同组里面的;然后这里的​​ProjectViewPopupMenu​​就是注册在:光标选中项目右键;​​anchor="last"​​这就是在最下面的意思,不过这些操作都可以自己修改,根据自己的习惯,因为很多这种生成ORM层的框架都是选中表然后进行生成的,我们这个插件是进行表连接的,所以前置会麻烦一点;

核心类分析

分析这么多,核心就在这个动作类上面:​​CodeGenerateAction​​:

public class CodeGenerateAction extends AnAction {
private IProjectGenerator projectGenerator = new ProjectGeneratorImpl();

@Override
public void actionPerformed(AnActionEvent e) {
// 这里是所有操作的入口,这里是注册到plugin.xml文件中,来触发全局操作
Project project = e.getRequiredData(CommonDataKeys.PROJECT);
ShowSettingsUtil.getInstance().editConfigurable(project, new ORMSettingsUI(project, projectGenerator));
}
}

可以看出,我们这里是将所有的操作都放到UI里面去操作,所以这个动作入口类很简单,就是执行操作;

接下来就是涉及到​​ORMSettingsUI​​​ → ​​IProjectGenerator​​​ → ​​ftl文件​​等一系列过程,这里就先到这,然后下面结合业务来进行分析;

核心业务分析

UI相关初始化逻辑

首先是我们这些数据存储对应的类:​​ORMConfigVO​​具体的可以看gitee上面的源码,这里就不贴出来了;

然后根据上面的分析,也知道是在​​ORMSettingsUI​​里面进行初始化展示的,部分代码:

public ORMSettingsUI(Project project, IProjectGenerator projectGenerator) {
this.project = project;
// 相当于注入文件生成的bean
this.projectGenerator = projectGenerator;
// 这里是拿到配置信息(这里是通过在idea的缓存中拿到的,第一次初始化插件是没有的)
config = DataSetting.getInstance(null != project ? project : ProjectManager.getInstance().getDefaultProject()).getORMConfig();
options = DataSetting.getInstance(null != project ? project : ProjectManager.getInstance().getDefaultProject()).getOptions();
// 下面开始就是拿到上次初始化过后的一些值重新赋值
assert project != null;
this.projectName.setText(project.getName());
...
this.database.setText(config.getDatabase());
...
this.daoPath.setText(config.getDaoPath());

这里的核心操作就是将之前我们之前的操作记录存入的值取出来,然后进行​​setXXX​​,最后展示在页面上;

当然我们也可以操作吗,对吧:

public void chooseFiles() {
// 选择PO生成目录
this.poButton.addActionListener(e -> {
FileChooserComponent component = FileChooserComponent.getInstance(project);
VirtualFile baseDir = project.getBaseDir();
VirtualFile virtualFile = component.showFolderSelectionDialog("选择PO生成目录", baseDir, baseDir);
if (null != virtualFile) {
ORMSettingsUI.this.poPath.setText(virtualFile.getPath());
}
});

这里的话是去监听按钮点击事件,进行文件的选取,这里用到了​​FileChooserComponent​​来做一些文件相关的操作,可以自行看下,比较简单;

上面就是简单的UI页面展示操作,包括数据回显和重新选择数据;

数据获取操作

因为这里会提前涉及到读取表的操作,所以这里讲讲数据连接、数据获取等操作;

同样我们是在​​ORMSettingsUI​​类中进行,操作,也是在构造器中:

public ORMSettingsUI(Project project, IProjectGenerator projectGenerator) {
// 查询数据库表列表
this.selectButton.addActionListener(e -> {
try {
DBHelper dbHelper = new DBHelper(this.host.getText(), Integer.parseInt(this.port.getText()), this.user.getText(), this.password.getText(), this.database.getText());
List<String> tableList = dbHelper.getAllTableName(this.database.getText());

String[] title = {"", "表名"};
Object[][] data = new Object[tableList.size()][2];
for (int i = 0; i < tableList.size(); i++) {
// 将表格的第二列进行数据赋值
data[i][1] = tableList.get(i);
}
// 第一列是多选框供用户选择需要生成的表
table1.setModel(new DefaultTableModel(data, title));
TableColumn tc = table1.getColumnModel().getColumn(0);
tc.setCellEditor(new DefaultCellEditor(new JCheckBox()));
tc.setCellEditor(table1.getDefaultEditor(Boolean.class));
tc.setCellRenderer(table1.getDefaultRenderer(Boolean.class));
tc.setMaxWidth(100);
} catch (Exception exception) {
Messages.showWarningDialog(project, "数据库连接错误,请检查配置.", "Warning");
}
});
  • 首先是监听按钮的点击事件触发
  • 通过​​DBHelper​​​来完成一些表操作,可以看下这个类,相当于是个工具类:
    我们这个就拿​​​getAllTableName​​​来分析下,其实都是​​JDBC​​相关的操作
public List<String> getAllTableName(String database) {
this.database = database;
Connection conn = getConnection(this.database);
try {
DatabaseMetaData metaData = conn.getMetaData();
ResultSet rs = metaData.getTables(null, null, "%", new String[]{"TABLE"});
List<String> ls = new ArrayList<>();
while (rs.next()) {
String s = rs.getString("TABLE_NAME");
ls.add(s);
}
return ls;
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
} finally {
closeConnection(conn);
}
}

这里很简单,就是先获取一个数据库的连接​​getConnection(this.database)​​​,然后就是获取数据库对应的元数据​​DatabaseMetaData metaData = conn.getMetaData();​​​,然后根据关键字“​​TABLE​​”获取对应库里面所有的表,其实这里是有个问题的:

TIPS:如果表过多的话,全部展示出来我自个也没试过,但是肯定不美观,这里有个需要优化的地方是支持对应表明的搜索查询,或者直接根据用户自己输入的表名直接生成也可以;待优化;

然后就是将这些表明全部放到list中,然后在UI上进行展示即可;

  • 接下来就是监听表格的选中事件,将选中的表放入到对应的数据结构中:
// 给表添加事件
this.table1.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (1 == e.getClickCount()) {
int rowIdx = table1.rowAtPoint(e.getPoint());
Boolean flag = (Boolean) table1.getValueAt(rowIdx, 0);
Set<String> tableNames = ORMSettingsUI.this.config.getTableNames();
if (null != flag && flag) {
tableNames.add(table1.getValueAt(rowIdx, 1).toString());
} else {
tableNames.remove(table1.getValueAt(rowIdx, 1).toString());
}
}
}
});

这里就是会把值放入到​​ORMConfigVO​​供后续使用;

UI里面一写相关的业务也就是这些了,都是比较简单的,接下来分析一下生成业务,这里面会稍微有点复杂;

核心生成业务分析

上面那些操作都是前置操作,当数据都准备好了之后,就是点击应用了;这里的出发点是让这个UI类继承:

public class ORMSettingsUI implements Configurable

然后重写里面的方法:

@Override
public void apply() {
// 获取配置
config.setUser(this.user.getText());
config.setPort(this.port.getText() != null ? this.port.getText() : "3306");
...
// 设置文件路径
config.setPoPath(this.poPath.getText());
...
config.setAuthor(this.authorField.getText());

// 链接DB
DBHelper dbHelper = new DBHelper(config.getHost(), Integer.parseInt(config.getPort()), config.getUser(), config.getPassword(), config.getDatabase());

// 全局配置项
options.setIsPlus(getIsPlus());
...

// 组装代码生产上下文
CodeGenContextVO codeGenContext = new CodeGenContextVO();

// 这里是去判断是否需要生成前置目录(先将所有的目录生成上下文组装好,留下是否需要生成service等选项之后判断)
codeGenContext.setModelPackage((Constants.YES.equals(getIsCreateDir())) ? config.getPoPath() + "/domain/" : config.getPoPath() + "/");
...
codeGenContext.setAuthor(config.getAuthor());
codeGenContext.setProjectName(config.getProjectName());

List<Table> tables = new ArrayList<>();
Set<String> tableNames = config.getTableNames();
for (String tableName : tableNames) {
tables.add(dbHelper.getTable(tableName));
}
codeGenContext.setTables(tables);

// 生成代码
projectGenerator.generation(project, codeGenContext, options);
}

这里就是重写了​​apply​​方法,也就是当我们点击完成的时候,会调用这个方法,然后仔细看看:

  • 获取目前UI界面里,例如text框里面最新的内容,然后存入到​​ORMConfigVO​​结构中,然后存入到对应的plugin.xml中;
  • 同样的,除了这些text,那些多选框的值也会存入,都是为了之后的回显;
  • 然后就是构建生成代码的上下文,其实就是一批组装好的数据,用于之后的文件之间传递;

所以这里的核心方法是​​projectGenerator.generation(project, codeGenContext, options);​

我们跟进去看看:​​IProjectGenerator​​​ → ​​AbstractProjectGenerator​​​ → ​​ProjectGeneratorImpl​

先看看​​AbstractProjectGenerator​​:

public void writeFile(Project project, String packageName, String name, String ftl, Object dataModel) {
VirtualFile virtualFile = null;
try {
virtualFile = createPackageDir(packageName).createChildData(project, name);
StringWriter stringWriter = new StringWriter();
Template template = super.getTemplate(ftl);
template.process(dataModel, stringWriter);
virtualFile.setBinaryContent(stringWriter.toString().getBytes(StandardCharsets.UTF_8));
} catch (IOException | TemplateException e) {
e.printStackTrace();
}
}

private static VirtualFile createPackageDir(String packageName) {
String path = FileUtil.toSystemIndependentName(StringUtil.replace(packageName, ".", "/"));
new File(path).mkdirs();
return LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
}

这里主要就是生成对应文件的操作,大部分都是idea提供的Api操作,这里稍微需要注意的是,最后的​​dataModel​​是之后要传递给ftl文件的参数,所以这里需要思考清楚这么设计;

然后就是核心实现类,其实就是数据处理类,这里也是比较核心的,都在子类中:​​ProjectGeneratorImpl​

// 生成PO
Model model = new Model(table.getComment(), codeGenContext.getModelPackage() + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, table.getName()), table.getName(), fields);
model.setAuthor(codeGenContext.getAuthor());
model.setProjectName(codeGenContext.getProjectName());
String fileModelName = Constants.YES.equals(options.getIsCreateSwagger()) ? "domain/orm/SwaggerModel.ftl" : "domain/orm/model.ftl";
writeFile(project, codeGenContext.getModelPackage(), model.getSimpleName() + ".java", fileModelName, model);

这里就拿生成实体类来讲讲:

  • 首先是构建Model,可以看看
public class Model extends Base {
private String tableName;
private List<Field> fields;

public Model(String comment, String name, String tableName, List<Field> fields) {
super(comment, name);
this.tableName = tableName;
this.fields = fields;
}

public List<Field> getFields() {
return fields;
}

public String getTableName() {
return tableName;
}

@Override
public Set<String> getImports() {
Set<String> imports = new HashSet<>();
List<Field> fields = getFields();
for (Field field : fields) {
if (field.isImport()) {
imports.add(field.getTypeName());
}
}
return imports;
}
}
  • 这里是存储表名和所有的表字段信息
  • ​getImports​​:这个对应之后ftl文件中的包导入操作

idea插件开发-Code Generate ORM_intellij-idea_13

  • Base里面的话就是一些公共的基本定义,有兴趣自己看看源码
  • 然后就是设置一些公共字段,比如作者名、项目名称等用于后续java的doc上面展示
  • 接下来就是根据我们的选项配置,来判断是否需要生成对应的​​Swagger​​,其他的选项配置也是如此,这里有一点就是,我是创建了两个ftl文件,应该是可以把这些参数传入到对应的ftl文件里面去做判断,这里我的想法是,我本身对ftl不熟悉,所以还是放在业务里面去写;
  • 最后就是文件生成;

整个大致的逻辑就是这些,不是很难;


谢谢大家阅读!!!

公众号: 扫码关注,爱搞技术的吴同学 ,公众号上会经常写实用性的文章,谢谢关注!!

idea插件开发-Code Generate ORM_ide_14

大量源码: 欢迎star,可能会分享微服务实战,分页插件等;​​gitee​