• 关于我写的IDEA插件能一键生成service,mapper....这件事(附源码)


    最终效果

    为了留住兄弟们继续看下去,还是先来看看最终效果吧

    在新建一个实体类的时候把相关联的mapper、service都一并生成

    当做出这个效果之后,面对一个结构复杂的DDD的项目,我再也不用新建一个实体类,新建一个mapper,新建一个service,新建一个model,再来一个转换类,再来一个。。。。。我想,这大概就是工业革命吧

    难道逆向工程不香么?

    什么?逆向工程能用来装逼么!谁能拒绝。。。

    调研Api

    当然可以上来就直接开搞,毕竟用模版生成文件这种操作,用java实现起来也不是什么难事。但是如果仅仅是实现一个简单的功能,就很难提起兴致。如果在研究兴趣的同时,还能提高一些专业能力的话,岂不美哉

    那我们来分析一下,针对这个点子,需要做的事情其实很少

    1. 找到需要生成文件目录
    2. 创建一些模版
    3. 把文件生成过去

    但是事情往往没有想象中这么简单

    1. 这些目录找起来并不轻松,在一个陌生的项目环境下,直接通过文件系统找文件目录总是不那么靠谱
    2. 直接在插件中写死模版当然很简单,但是损失了不小的灵活性,总不能每次新建一个项目就去把插件发一个版本
    3. 生成文件的话其实问题不大,但是总觉得用原生的硬搞不是特别优雅,既然我们在IDEA的地盘做事,就要尽量按照他的风格来,应该也可以得到一些便利

    针对这些问题,我的选择当然是去看IDEA的源码了,既做插件,又能锻炼阅读源码的能力,一菜多吃!

    既然这次主要是文件生成操作,首先肯定是关注创建Class这个Action,大家都是新建文件,只是目录不同,在这个Action中肯定可以找到很多线索。

    总之,最后定位到了CreateClassAction这个类,源码我就不全都截出来给大家了,具体的逻辑部分有兴趣的兄弟可以自己去自己研究一下,只说我这边研究得到的线索

    创建输入类名的弹窗

    这个类中有一个比较有用的方法CreateClassAction. buildDialog(Project project, PsiDirectory directory,CreateFileFromTemplateDialog.Builder builder)

    1. protected void buildDialog(final Project project, PsiDirectory directory, CreateFileFromTemplateDialog.Builder builder) {
    2. 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");
    3. //根据版本添加一些可以创建的类型class,interface这些
    4. ...
    5. //一些验证规则
    6. builder.setValidator(new InputValidatorEx() {
    7. //当验证不通过返回的报错信息
    8. public String getErrorText(String inputString) {
    9. //if中有判断类名是否合法的api,如果有需要的话可以直接调用
    10. if (inputString.length() > 0 && !PsiNameHelper.getInstance(project).isQualifiedName(inputString)) {
    11. return JavaErrorBundle.message("create.class.action.this.not.valid.java.qualified.name", new Object[0]);
    12. }
    13. ...
    14. }
    15. //检查输入,他这个直接返回true,我们用的时候也直接返回就好了
    16. public boolean checkInput(String inputString) {
    17. return true;
    18. }
    19. //是否可以关闭对话框,这里可以写相关的验证逻辑
    20. public boolean canClose(String inputString) {
    21. return !StringUtil.isEmptyOrSpaces(inputString) && this.getErrorText(inputString) == null;
    22. }
    23. });
    24. 复制代码

    这个方法其实就是创建大家比较熟悉的这个页面,我呢作为一个后端,对创建页面这种操作自然不怎么感兴趣,那肯定是能用现成的就用现成的

    这里提一下,在getErrorText这个方法返回值中JavaErrorBundle.message的内容指向的内容是"This is not a valid Java qualified name",如果你在创建类名的时候输入了一个不合法的类名,出现的就是这句话。当时在看到了这个的时候,还是让我感受到一些阅读源码的乐趣

    创建文件

    CreateClassAction.doCreate()方法中,藏了一个生成java文件的api

    1. JavaDirectoryService.getInstance().createClass(dir, className, templateName, true)
    2. 复制代码
    • dir 当前目录
    • className 我们在弹窗中输入的类名
    • templateName 模版名称
    • 当模版中有未设置的变量名时是否提示

    获取模版

    获取所有模版的Api:

    1. FileTemplateManager.getInstance(project).getAllTemplates()
    2. 复制代码

    在创建文件时用到的模版,来源其实是配置中的File and Code Templates,研究到这里,模版不够灵活的问题就自动被解决了,用户想生成什么样的文件,完全取决于配置了什么样的模版

    当把配置模版的描述信息拉到最底部会发现,其实IDEA用到了Velocity模版语言(如果需要教程的话可以在评论区留言~),所以在IDEA本身的创建Class文件也可以有一些小技巧。比如:如果生成的文件是Controller结尾的,就把@RequestMapper这些都提前生成好,反正很多新建的文件就只有输入的名字不一样,像我这样懒的人,肯定能少写一行代码就少些一行。另外,IDEA本身也提供了一些占位符,像类名,日期这些常用的都可以直接使用,更多模版的配置和使用也可以查看

    目前还有一个问题,就是模版和文件生成目录的对应关系,应该把什么文件生成到什么目录下面。最开始的想法是写一个配置页面,但是这样就需要创造一个页面出来,前面也说过了,我其实不怎么喜欢这类创建页面的操作,另外配置天然还会关联持久化的操作,以及每次换项目其实还要重新配置,总而言之就是比较麻烦。后来发现了一个取巧的办法----如果直接用包名命名模版,其实问题就解决了,在获取到模版列表之后,通过模版的名字和生成文件的目标包来匹配就解决了这个问题,并且配置可以跟随IDEA的配置,不会因为切换项目出问题,具体的配置示例可以看文末的配置图。

    获取鼠标当前的目录和包信息

    CreateClassAction的曾祖父类CreateFromTemplateAction中,有获取当前选中目录的api

    1. DataContext dataContext = e.getDataContext();
    2. IdeView view = (IdeView)LangDataKeys.IDE_VIEW.getData(dataContext);
    3. if (view != null) {
    4. final PsiDirectory dir = Objects.requireNonNull(view).getOrChooseDirectory();
    5. 复制代码

    CreateClassAction.getActaionName中用到了通过目录获取包信息的api

    1. PsiPackage psiPackage = JavaDirectoryService.getInstance().getPackage(directory);
    2. 复制代码
    • psiPackage.getQualifiedName()可以获取到完整的包名,可以用于我们的包和模版的匹配

    • psiPackage.getDirectories()用于生成文件时把包信息再转换成目录信息

    • psiPackage.geParent()用于获取父包

    • psiPackage.getChildren()用于获取所有子包,注意只要包名相同,跨模块的子包也能获取到

    开始动手

    找出了上面这些之后,之前提到的跟Api相关的问题就都解决掉了,下面贴出实现的主要逻辑,为了逻辑尽量顺畅,删除了很多判断的代码,源码可以访问 【工众号】 ,类名是CreateClasses

    1. @Override
    2. public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
    3. //获取到project对象
    4. Project project = anActionEvent.getProject();
    5. //获取到当前action的上下文
    6. DataContext dataContext = anActionEvent.getDataContext();
    7. //用于获取当前目录
    8. IdeView view = LangDataKeys.IDE_VIEW.getData(dataContext);
    9. if (view != null) {
    10. 获取到当前目录
    11. final PsiDirectory dir = Objects.requireNonNull(view).getOrChooseDirectory();
    12. //创建弹出框
    13. CreateFileFromTemplateDialog.Builder builder = CreateFileFromTemplateDialog.createDialog(project);
    14. //填充弹出框信息
    15. this.buildDialog(project, dir, builder);
    16. //执行相关创建逻辑
    17. builder.show("Error", "class", new CreateFileFromTemplateDialog.FileCreator<>() {
    18. public PsiElement createFile(@NotNull String name, @NotNull String templateName) {
    19. //获取当前目录的包信息
    20. PsiPackage choosePackage = JavaDirectoryService.getInstance().getPackage(dir);
    21. //获取父包
    22. PsiPackage parentPackage = choosePackage.getParentPackage();
    23. List<PsiClass> result = new ArrayList<>();
    24. //获取所有子包(最深的那种)
    25. List<PsiPackage> allSubPackage = getAllSubPackage(Arrays.stream(Objects.requireNonNull(parentPackage.getParentPackage()).getSubPackages()).collect(Collectors.toList()));
    26. allSubPackage.forEach(psiPackage -> {
    27. //这边是创建文件的本体,单独处理用于最后的页面定位
    28. if (psiPackage.getQualifiedName().equals(choosePackage.getQualifiedName())) {
    29. result.add(JavaDirectoryService.getInstance().createClass(psiPackage.getDirectories()[0], name, templateName, true));
    30. return;
    31. }
    32. //生成的关联文件,从获取的模版列表和包名匹配,匹配到了就生成
    33. Arrays.stream(FileTemplateManager.getInstance(project).getAllTemplates())
    34. .forEach(item -> {
    35. if (psiPackage.getQualifiedName().endsWith(item.getName())) {
    36. for (PsiDirectory directory : psiPackage.getDirectories()) {
    37. JavaDirectoryService.getInstance().createClass(directory, name, item.getName(), true);
    38. }
    39. }
    40. });
    41. });
    42. //返回添加进去的元素,用来进行页面定位
    43. return result.get(0);
    44. }
    45. public boolean startInWriteAction() {
    46. return false;
    47. }
    48. //弹出框的名字
    49. public @NotNull String getActionName(@NotNull String name, @NotNull String templateName) {
    50. return "Create Classes";
    51. }
    52. },
    53. //最后定位的位置
    54. (createdElement) -> {
    55. if (createdElement != null) {
    56. view.selectElement(createdElement);
    57. }
    58. });
    59. }
    60. }
    61. 复制代码

    使用

    现在我们就可以香起来了,在配置中配置好,就可以像文章开头那样生成文件了

    文章的所有内容到这就结束了,都看到这了,难道不想点个赞么!

  • 相关阅读:
    ES9,ES10
    main函数之前发生什么
    屎上最全vue-pdf+Springboot与aspose-words整合,开箱即用
    2018-Adversarial Learning for Semi-Supervised Semantic Segmentation
    Mybatis 二级缓存(使用Redis作为二级缓存)
    windows频繁更新问题解决方案
    PaddleOCRv3之二:TextRecognitionDataGenerator训练集构造
    《python程序语言设计》2018版第5章第55题利用turtle黑白棋盘。可读性还是最重要的。
    lintcode 558 · 滑动窗口矩阵的最大值【二维数组前缀和 中等 vip】
    Trello的替代方案有哪些?6种国内外选择!
  • 原文地址:https://blog.csdn.net/Trouvailless/article/details/125993871