大家好,我是编程小6,很高兴遇见你,有问题可以及时留言哦。
为了留住兄弟们继续看下去,还是先来看看最终效果吧
在新建一个实体类的时候把相关联的mapper、service都一并生成
当做出这个效果之后,面对一个结构复杂的DDD的项目,我再也不用新建一个实体类,新建一个mapper,新建一个service,新建一个model,再来一个转换类,再来一个。。。。。我想,这大概就是工业革命吧
难道逆向工程不香么?
什么?逆向工程能用来装逼么!谁能拒绝。。。
当然可以上来就直接开搞,毕竟用模版生成文件这种操作,用java实现起来也不是什么难事。但是如果仅仅是实现一个简单的功能,就很难提起兴致。如果在研究兴趣的同时,还能提高一些专业能力的话,岂不美哉
那我们来分析一下,针对这个点子,需要做的事情其实很少
但是事情往往没有想象中这么简单
针对这些问题,我的选择当然是去看IDEA的源码了,既做插件,又能锻炼阅读源码的能力,一菜多吃!
既然这次主要是文件生成操作,首先肯定是关注创建Class这个Action,大家都是新建文件,只是目录不同,在这个Action中肯定可以找到很多线索。
关于找源码的具体方法大家可以参考上篇文章中的介绍。
你可能不知道的IDEA Plugin开发技巧
总之,最后定位到了CreateClassAction
这个类,源码我就不全都截出来给大家了,具体的逻辑部分有兴趣的兄弟可以自己去自己研究一下,只说我这边研究得到的线索
这个类中有一个比较有用的方法CreateClassAction. buildDialog(Project project, PsiDirectory directory,CreateFileFromTemplateDialog.Builder builder)
protected void buildDialog(final Project project, PsiDirectory directory, CreateFileFromTemplateDialog.Builder builder) {
builder.setTitle(JavaBundle.message("action.create.new.class", new Object[0])).addKind(JavaPsiBundle.message("node.class.tooltip", new Object[0]), PlatformIcons.CLASS_ICON, "Class").addKind(JavaPsiBundle.message("node.interface.tooltip", new Object[0]), PlatformIcons.INTERFACE_ICON, "Interface");
//根据版本添加一些可以创建的类型class,interface这些
...
//一些验证规则
builder.setValidator(new InputValidatorEx() {
//当验证不通过返回的报错信息
public String getErrorText(String inputString) {
//if中有判断类名是否合法的api,如果有需要的话可以直接调用
if (inputString.length() > 0 && !PsiNameHelper.getInstance(project).isQualifiedName(inputString)) {
return JavaErrorBundle.message("create.class.action.this.not.valid.java.qualified.name", new Object[0]);
}
...
}
//检查输入,他这个直接返回true,我们用的时候也直接返回就好了
public boolean checkInput(String inputString) {
return true;
}
//是否可以关闭对话框,这里可以写相关的验证逻辑
public boolean canClose(String inputString) {
return !StringUtil.isEmptyOrSpaces(inputString) && this.getErrorText(inputString) == null;
}
});
这个方法其实就是创建大家比较熟悉的这个页面,我呢作为一个后端,对创建页面这种操作自然不怎么感兴趣,那肯定是能用现成的就用现成的
这里提一下,在getErrorText这个方法返回值中JavaErrorBundle.message的内容指向的内容是"This is not a valid Java qualified name",如果你在创建类名的时候输入了一个不合法的类名,出现的就是这句话。当时在看到了这个的时候,还是让我感受到一些阅读源码的乐趣
在CreateClassAction.doCreate()
方法中,藏了一个生成java文件的api
JavaDirectoryService.getInstance().createClass(dir, className, templateName, true)
获取所有模版的Api:
FileTemplateManager.getInstance(project).getAllTemplates()
在创建文件时用到的模版,来源其实是配置中的File and Code Templates,研究到这里,模版不够灵活的问题就自动被解决了,用户想生成什么样的文件,完全取决于配置了什么样的模版
当把配置模版的描述信息拉到最底部会发现,其实IDEA用到了Velocity模版语言(如果需要教程的话可以在评论区留言~),所以在IDEA本身的创建Class文件也可以有一些小技巧。比如:如果生成的文件是Controller结尾的,就把@RequestMapper这些都提前生成好,反正很多新建的文件就只有输入的名字不一样,像我这样懒的人,肯定能少写一行代码就少些一行。另外,IDEA本身也提供了一些占位符,像类名,日期这些常用的都可以直接使用,更多模版的配置和使用也可以查看官方文档
www.jetbrains.com/help/idea/u…
目前还有一个问题,就是模版和文件生成目录的对应关系,应该把什么文件生成到什么目录下面。最开始的想法是写一个配置页面,但是这样就需要创造一个页面出来,前面也说过了,我其实不怎么喜欢这类创建页面的操作,另外配置天然还会关联持久化的操作,以及每次换项目其实还要重新配置,总而言之就是比较麻烦。后来发现了一个取巧的办法----如果直接用包名命名模版,其实问题就解决了,在获取到模版列表之后,通过模版的名字和生成文件的目标包来匹配就解决了这个问题,并且配置可以跟随IDEA的配置,不会因为切换项目出问题,具体的配置示例可以看文末的配置图。
在CreateClassAction
的曾祖父类CreateFromTemplateAction
中,有获取当前选中目录的api
DataContext dataContext = e.getDataContext();
IdeView view = (IdeView)LangDataKeys.IDE_VIEW.getData(dataContext);
if (view != null) {
final PsiDirectory dir = Objects.requireNonNull(view).getOrChooseDirectory();
在CreateClassAction.getActaionName
中用到了通过目录获取包信息的api
PsiPackage psiPackage = JavaDirectoryService.getInstance().getPackage(directory);
psiPackage.getQualifiedName()
可以获取到完整的包名,可以用于我们的包和模版的匹配
psiPackage.getDirectories()
用于生成文件时把包信息再转换成目录信息
psiPackage.geParent()
用于获取父包
psiPackage.getChildren()
用于获取所有子包,注意只要包名相同,跨模块的子包也能获取到
找出了上面这些之后,之前提到的跟Api相关的问题就都解决掉了,下面贴出实现的主要逻辑,为了逻辑尽量顺畅,删除了很多判断的代码,源码可以访问 源码地址 ,类名是CreateClasses
@Override
public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
//获取到project对象
Project project = anActionEvent.getProject();
//获取到当前action的上下文
DataContext dataContext = anActionEvent.getDataContext();
//用于获取当前目录
IdeView view = LangDataKeys.IDE_VIEW.getData(dataContext);
if (view != null) {
获取到当前目录
final PsiDirectory dir = Objects.requireNonNull(view).getOrChooseDirectory();
//创建弹出框
CreateFileFromTemplateDialog.Builder builder = CreateFileFromTemplateDialog.createDialog(project);
//填充弹出框信息
this.buildDialog(project, dir, builder);
//执行相关创建逻辑
builder.show("Error", "class", new CreateFileFromTemplateDialog.FileCreator<>() {
public PsiElement createFile(@NotNull String name, @NotNull String templateName) {
//获取当前目录的包信息
PsiPackage choosePackage = JavaDirectoryService.getInstance().getPackage(dir);
//获取父包
PsiPackage parentPackage = choosePackage.getParentPackage();
List<PsiClass> result = new ArrayList<>();
//获取所有子包(最深的那种)
List<PsiPackage> allSubPackage = getAllSubPackage(Arrays.stream(Objects.requireNonNull(parentPackage.getParentPackage()).getSubPackages()).collect(Collectors.toList()));
allSubPackage.forEach(psiPackage -> {
//这边是创建文件的本体,单独处理用于最后的页面定位
if (psiPackage.getQualifiedName().equals(choosePackage.getQualifiedName())) {
result.add(JavaDirectoryService.getInstance().createClass(psiPackage.getDirectories()[0], name, templateName, true));
return;
}
//生成的关联文件,从获取的模版列表和包名匹配,匹配到了就生成
Arrays.stream(FileTemplateManager.getInstance(project).getAllTemplates())
.forEach(item -> {
if (psiPackage.getQualifiedName().endsWith(item.getName())) {
for (PsiDirectory directory : psiPackage.getDirectories()) {
JavaDirectoryService.getInstance().createClass(directory, name, item.getName(), true);
}
}
});
});
//返回添加进去的元素,用来进行页面定位
return result.get(0);
}
public boolean startInWriteAction() {
return false;
}
//弹出框的名字
public @NotNull String getActionName(@NotNull String name, @NotNull String templateName) {
return "Create Classes";
}
},
//最后定位的位置
(createdElement) -> {
if (createdElement != null) {
view.selectElement(createdElement);
}
});
}
}
现在我们就可以香起来了,在配置中配置好,就可以像文章开头那样生成文件了
文章的所有内容到这就结束了,都看到这了,难道不想点个赞么!
源码地址:笨天才/SlowToolkit
本文的功能可以通过官方插件市场搜索SlowToolkit下载体验,如果有什么问题或者经验, 欢迎联系我交流
wx:yslowgenius