原理参考**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等查找到该资源。