Spring动态注入外部文件到classpath

原理参考**ImportBeanDefinitionRegistrar+SPI简化Spring开发**

一个比较典型的场景是配置文件的动态注入,配置文件与代码是分离的,常用的解决办法是配置中心,如Apollo、disconf、Eureka等。如果不满足配置中心的应用条件我们该如何解决呢?

下面是简单的实现
首先定义注入外部文件的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@EnableAutoRegistrar
@Repeatable(EnableFileInjects.class)
public @interface EnableFileInject {

    /**
     * 注入文件的路径
     */
    String[] basePath() default {};

    /**
     * 包含文件的过滤器
     */
    EnableFileInject.Filter[] includeFilters() default {};

    /**
     * 排除文件的过滤器
     */
    EnableFileInject.Filter[] excludeFilters() default {};

    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public @interface Filter {

        String[] pattern() default {};
    }
}

允许注入多个的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@EnableAutoRegistrar
public @interface EnableFileInjects {

    EnableFileInject[] value();

}

参数basePath指定外部文件位置,同时允许配置一定的过滤规则,例如只允许.properties类型或.yml类型。
最重要的是实现对应注解处理的Handler

@Slf4j
public class EnableFileInjectRegisterHandler implements ConfigurationRegisterHandler {

    private static final String TMP_PATH_DIR = IOUtils.joinPath(System.getProperty("java.io.tmpdir"),
            UUID.randomUUID().toString().replaceAll("-", ""));

    @Override
    public void registerBeanDefinitions(RegisterBeanDefinitionContext context) {
        Set<AnnotationAttributes> enableExtFileInjects = SpringAnnotationConfigUtils.attributesForRepeatable(
                context.getImportingClassMetadata(), EnableFileInjects.class, EnableFileInject.class);
        if (CollectionUtils.isEmpty(enableExtFileInjects)) {
            return;
        }
        for (AnnotationAttributes extFileInject : enableExtFileInjects) {
            final String[] basePaths = extFileInject.getStringArray("basePath");
            PathFileScanner scanner = new PathFileScanner(basePaths);
            for (AnnotationAttributes filter : extFileInject.getAnnotationArray("includeFilters")) {
                parseFilters(filter).forEach(x -> scanner.addIncludeScanFilter(x));
            }
            for (AnnotationAttributes filter : extFileInject.getAnnotationArray("excludeFilters")) {
                parseFilters(filter).forEach(x -> scanner.addExcludeScanFilter(x));
            }
            try {
                final List<File> files = scanner.scan();

                if (log.isDebugEnabled()) {
                    log.debug("Scan files: {}",
                            files.stream().map(this::getFilePath).reduce((x, y) -> x + "," + y).orElse(""));
                }
                injectFilesToClasspath(files);
            } catch (IOException e) {
                log.error("Scan file and inject fail for {}", StringUtils.join(basePaths, ","), e);
            }
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }

    private List<Tuple<ScanFilter, Object>> parseFilters(AnnotationAttributes filterAttributes) {
        List<Tuple<ScanFilter, Object>> tuples = new ArrayList<>();

        for (String expression : filterAttributes.getStringArray("pattern")) {
            tuples.add(new Tuple<>(new RegexPatternScanFilter(f -> ((File) f).getName(), String::valueOf),
                    expression));
        }

        return tuples;
    }

    private synchronized void injectFilesToClasspath(List<File> files) throws IOException {
        File file = new File(TMP_PATH_DIR);
        if (!file.exists()) {
            file.mkdirs();
            if (log.isDebugEnabled()) {
                log.debug("config file dir is {}", TMP_PATH_DIR);
            }
        }
        for (File f : files) {
            File destFile = new File(TMP_PATH_DIR + File.separator + f.getName());
            if (destFile.exists()) {
                destFile.delete();
            }
            FileUtils.copyFileToDirectory(f, file);
        }
        ClassLoader currentThreadClassLoader = Thread.currentThread().getContextClassLoader();
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {file.toURI().toURL()}, currentThreadClassLoader);
        Thread.currentThread().setContextClassLoader(urlClassLoader);
    }

    private String getFilePath(File file) {
        try {
            return file.getCanonicalPath();
        } catch (IOException e) {
            return file.getAbsolutePath();
        }
    }

}

利用PathFileScanner实现扫描路径获取并过滤文件,得到文件后复制到java.io.tmpdir目录下并更新到classpath.这里是简单的举例,至于其他问题如不同目录下重名文件、tmp目录可能会被系统清理等,自行决定如何处理。甚至不放在tmp目录下也行。
使用时引入注解到@configuration的class上

@EnableFileInjects({
        @EnableFileInject(basePath = "${file1.path}/a",
                includeFilters = {@EnableFileInject.Filter(pattern = {"*.properties"})}),
        @EnableFileInject(basePath = "${file2.path}/b",
                includeFilters = {@EnableFileInject.Filter(pattern = {"*.yml"})})
})
@Configuration
public class EnableFileInjectRegisterConfiguration {
}

file1.path这样的参数可以通过启动参数配置在System.properties中,如${file1.path}/a/目录下有个a.properties文件,那么可以直接利用ClasspathResource或PathMatchingResourcePatternResolver等查找到该资源。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值