为了留住兄弟们继续看下去,还是先来看看最终效果吧
在新建一个实体类的时候把相关联的mapper、service都一并生成
当做出这个效果之后,面对一个结构复杂的DDD的项目,我再也不用新建一个实体类,新建一个mapper,新建一个service,新建一个model,再来一个转换类,再来一个。。。。。我想,这大概就是工业革命吧
难道逆向工程不香么?
什么?逆向工程能用来装逼么!谁能拒绝。。。
当然可以上来就直接开搞,毕竟用模版生成文件这种操作,用java实现起来也不是什么难事。但是如果仅仅是实现一个简单的功能,就很难提起兴致。如果在研究兴趣的同时,还能提高一些专业能力的话,岂不美哉
那我们来分析一下,针对这个点子,需要做的事情其实很少
但是事情往往没有想象中这么简单
针对这些问题,我的选择当然是去看IDEA的源码了,既做插件,又能锻炼阅读源码的能力,一菜多吃!
既然这次主要是文件生成操作,首先肯定是关注创建Class这个Action,大家都是新建文件,只是目录不同,在这个Action中肯定可以找到很多线索。
总之,最后定位到了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本身也提供了一些占位符,像类名,日期这些常用的都可以直接使用,更多模版的配置和使用也可以查看
目前还有一个问题,就是模版和文件生成目录的对应关系,应该把什么文件生成到什么目录下面。最开始的想法是写一个配置页面,但是这样就需要创造一个页面出来,前面也说过了,我其实不怎么喜欢这类创建页面的操作,另外配置天然还会关联持久化的操作,以及每次换项目其实还要重新配置,总而言之就是比较麻烦。后来发现了一个取巧的办法----如果直接用包名命名模版,其实问题就解决了,在获取到模版列表之后,通过模版的名字和生成文件的目标包来匹配就解决了这个问题,并且配置可以跟随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);
- }
- });
- }
-
- }
- 复制代码
现在我们就可以香起来了,在配置中配置好,就可以像文章开头那样生成文件了
文章的所有内容到这就结束了,都看到这了,难道不想点个赞么!