application.yml更优雅写法及遇到的问题

我在重构我的代码生成工具,之前有如下代码:

1
2
3
4
5
6

tools:
  template-directory: 'classpath:templates/'
  enum-comment-pattern: '^([A-Za-z\u4e00-\u9fa5 ]{1,})((([A-Za-z0-9-]+:[\u4e00-\u9fa5A-Za-z0-9-]{1,},?)+))$'
  number-pattern: '^[0-9]*$'

我的Properties类是这样写的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

@Data
public class ToolsProperties {
    private String templateDirectory;
    private String numberPattern;

    public Path getTemplateDirectory(){
        // 做一些处理将String转换成Path
    }

    public Pattern getNumberPattern(){
        // 做一些处理将String转换成Pattern
    }
}

我觉得这样的写法非常的不优雅,我无法接受,我想要如下的写法:

1
2
3
4
5
6
7

@Data
public class ToolsProperties {
    private Path templateDirectory;
    private Pattern numberPattern;
}

在我开始编码前,我以为我会使用Convert完成该工作,但是我发现SpringBoot支持自动将application.yml中的字符串转换成Path和Pattern,于是我就使用了该特性。

Convert的开发

我还是要写一份我开发Convert(毕竟这份代码没啥用了,我准备删除了):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public class PathConverter implements Converter<String, Path> {

    @Override
    public Path convert(String templateDirectory) {

        try {

            Path templateDirectoryPath = templateDirectory.startsWith("classpath:") ?
                    ResourceUtils.getFile(templateDirectory).toPath() :
                    Paths.get(templateDirectory);

            if (!Files.exists(templateDirectoryPath)) {
                throw new RuntimeException("TemplateDirectory不存在,请检查配置文件");
            }

            return templateDirectoryPath;

        } catch (FileNotFoundException e) {
            throw new RuntimeException("TemplateDirectory不存在,请检查配置文件");
        }
    }
}

这份代码有两个亮点:

  1. 支持在配置文件中配置classpath:
  2. 使用了ResourceUtils工具处理classpath。

可惜了,Spring默认支持对Path的转换,导致我这个代码没用到。

SpringBoot对Path的处理

很糟心的是,将ToolsProperties中templateDirectory字段换成Path类型并没有想象中的那么顺利,我一开始将template-directory配置成classpath:templates,结果提示无法找到该文件,我以为是SpringBoot默认的Path转换器不支持classpath,于是我就想替换掉这个转换器,接下来就是漫长的找这个转换器的过程(这个转换器是不存在的)

具体过程我就不写出来了,总是整个过程我收获了如下技巧:

  1. 我在ToolsProperties写了一个Setter(手写的,非Lombok),然后断点在这个setter中,最终SpringBoot调到了这个方法,我从而可以查看调用栈,从而快速找到我想观察的方法。(貌似可以直接断点到字段上,我没有试过)

  2. 我一开始不知道Idea可以断点拉姆达表达式,遇到拉姆达表达式,我就一层一层的去找这个拉姆达表达式是在哪实现的,然后在拉姆达表达式中进行断点。实际上不需要这么做,我可以直接断点在函数式接口的方法上,Idea会自动帮我找到这个方法的定义。(虽然如此,还是一层一层的找更快乐)

  3. 对Path、Pattern的处理,SpringBoot并没有采用Convert,而是一系列非常复杂的机制,有兴趣的自己去看看吧,我目前还没有完全消化。

  4. 如果我们用Path接受配置文件中的配置,Path指向一个文件夹的时候配置的值一定要一个反斜杠结尾。

分享一些快乐

  1. 我是如何知道SpringBoot支持classpath:的,如下,我在断点调试值看到了如下的代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

@Override
public void setAsText(String text) throws IllegalArgumentException {
    boolean nioPathCandidate = !text.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX);
    if (nioPathCandidate && !text.startsWith("/")) {
        try {
            URI uri = new URI(text);
            if (uri.getScheme() != null) {
                nioPathCandidate = false;
                // Let's try NIO file system providers via Paths.get(URI)
                setValue(Paths.get(uri).normalize());
                return;
            }
        }
        catch (URISyntaxException ex) {
            // Not a valid URI; potentially a Windows-style path after
            // a file prefix (let's try as Spring resource location)
            nioPathCandidate = !text.startsWith(ResourceUtils.FILE_URL_PREFIX);
        }
        catch (FileSystemNotFoundException ex) {
            // URI scheme not registered for NIO (let's try URL
            // protocol handlers via Spring's resource mechanism).
        }
    }

    this.resourceEditor.setAsText(text);
    Resource resource = (Resource) this.resourceEditor.getValue();
    if (resource == null) {
        setValue(null);
    }
    else if (nioPathCandidate && !resource.exists()) {
        setValue(Paths.get(text).normalize());
    }
    else {
        try {
            setValue(resource.getFile().toPath());
        }
        catch (IOException ex) {
            throw new IllegalArgumentException("Failed to retrieve file for " + resource, ex);
        }
    }
}

  1. 我最后是如何知道path的值要配置成classpath:templates/,没有分号则SpringBoot会提示没有该文件的?

同样是上面的源码呀,哈哈,里面有一行resource.getFile().toPath(),看到这一行时,我就开始知道是因为我少了一个反斜杠最后导致SpringBoot提示文件夹不存在。

实际上使用Java原生的API是不存在这个问题的,下面两个输出都为true:

1
2
3
4

System.out.println(Files.exists(Paths.get("C:\\Users\\wujj\\Desktop\\Tmp5")));
System.out.println(Files.exists(Paths.get("C:\\Users\\wujj\\Desktop\\Tmp5\\")));

后续:

一顿分析猛如虎,最后发现根本就不是分析的情况,只因为我的templates文件是空的,Idea没有给我拷贝到target目录下,而Spring的Resource类是判断二进制文件的目录是否存在templates文件夹。

我为什么会分析错呢???因为我第一次猜测是Path只能接受文件,所以我在template目录下创建了一个文件,然后进行测试,发现Path确实能正确的接受,所以我认为Path能接受文件。然后我觉得很奇怪,不可能说只能接受文件不能接受目录,所以我分析是因为我少了最后的分号,这个时候我没有删除这个文件就进行了测试,发现确实可以,所以我认为就是因为少了分号。此时我没有再删除这个文件进行二次测试就下结论了。擦

我找了一圈没有找到很好的方案解决这个问题(我不想再引入一些复杂的东西导致我Maven插件配置的乱七八糟),考虑到这个问题会在启动阶段暴露,所以我们知道有这个问题就好了,尽量不要在resources创建空文件夹,然后在application.yml中配置这个文件夹。