@PropertySource在批处理Jar应用中无法加载指定配置文件问题
问题
我有一个供多应用共用的邮件发送共享库,需要通过服务器文件路径下的安全文件获取SMTP授权凭证,而非应用类路径。该库提供可导入的共享配置来自动配置凭证,在Spring Boot及Spring Web应用中运行正常,但在一个非Spring Batch的批处理Jar应用中,导入共享配置后无法找到该保密文件,导致发送邮件时凭证为空。
若让该批处理Jar自行加载该安全文件则可正常运行,说明应用有权限访问该文件路径。移除@PropertySource的ignoreResourceNotFound标记后,应用未报错但MyLibProperties的属性仍为空。
相关代码示例
共享库配置类
@Configuration @PropertySource(value = { "file:/scr/my-lib/${platform.env}-sec.properties" }, ignoreResourceNotFound = true) public class MyLibraryConfig { @Bean public MyLibProperties myLibProperties() { return new MyLibProperties(); } @Bean public JavaMailSender mailSender(final MyLibProperties properties) { final JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setUsername(properties.getAccount()); sender.setPassword(properties.getPassword()); } } public class MyLibProperties { @Value("${scr.mylib.account:#{null}}") private String account; @Value("${scr.mylib.password:#{null}}") private String password; }
注:
scr.mylib属性定义在保密文件中,加载成功则会被填充,否则为null。
出现问题的Spring批处理Jar应用
/** * 这不是Spring Web应用,而是通过定时脚本启动的批处理Jar */ public class MyAppMain { public static void main(String[] args) { final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.registerShutdownHook(); context.register(ApplicationConfig.class); context.refresh(); context.getBean(AppRunner.class).run(); } } @Configuration @Import(MyLibConfig.class) @ComponentScan(basePackages = {"..."}) @ImportResource({"classpath:spring.xml"}) public class ApplicationConfig { private static final String ENV = System.getProperty("platform.env"); @Bean public static PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() { final PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer(); configurer.setLocations( new ClassPathResource("props/env.properties"), // 注:若在此处添加库配置文件,凭证可正常加载,但仅通过@Import共享配置则失败 // new FileSystemResource("/scr/my-lib/" + ENV + "-sec.properties") ); configurer.setIgnoreResourceNotFound(true); return configurer; } }
可正常运行的旧Spring应用示例
@Configuration @Import(SnetEmailConfig.class) @ComponentScan(basePackages = "...") public class MySpringAppInitializer extends HttpServlet { ... }
可正常运行的Spring Boot应用示例
@Import(SnetEmailConfig.class) @SpringBootApplication public class MyBootApp extends SpringBootServletInitializer { ... }
请问这是否是配置加载顺序导致的问题,该如何解决?
原因分析与解决方案
核心原因:配置加载顺序与占位符解析优先级冲突
确实是配置加载顺序导致的问题:
ApplicationConfig中定义的static类型PropertyPlaceholderConfigurerBean会被Spring优先加载(静态Bean在上下文初始化早期就会被处理)。- 共享库
@PropertySource中的${platform.env}占位符,需要依赖系统属性或已加载的配置解析,但此时PropertyPlaceholderConfigurer还未完成占位符解析逻辑,导致路径无法正确替换为实际文件路径,最终加载失败。 - 而Spring Boot和传统Spring Web应用的默认属性处理机制(比如Spring Boot的
Environment体系)会先解析系统属性、环境变量,再处理@PropertySource,因此不会出现该问题。
解决方法
方法一:替换PropertyPlaceholderConfigurer为PropertySourcesPlaceholderConfigurer
Spring 3.1+推荐使用PropertySourcesPlaceholderConfigurer替代旧的PropertyPlaceholderConfigurer,它能更好地整合Spring的Environment体系,支持@PropertySource的占位符解析:
修改ApplicationConfig中的Bean定义:
@Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { final PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); configurer.setLocations( new ClassPathResource("props/env.properties") ); configurer.setIgnoreResourceNotFound(true); return configurer; }
修改后@PropertySource中的${platform.env}会被正确解析,共享库的配置文件能正常加载。
方法二:手动解析占位符并导入配置
如果必须保留PropertyPlaceholderConfigurer,可以在ApplicationConfig中手动解析系统属性,直接拼接共享库配置文件路径并导入:
@Configuration @Import(MyLibConfig.class) @ComponentScan(basePackages = {"..."}) @ImportResource({"classpath:spring.xml"}) // 手动拼接路径,提前解析占位符 @PropertySource(value = "file:/scr/my-lib/" + System.getProperty("platform.env") + "-sec.properties", ignoreResourceNotFound = true) public class ApplicationConfig { private static final String ENV = System.getProperty("platform.env"); @Bean public static PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() { final PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer(); configurer.setLocations( new ClassPathResource("props/env.properties") ); configurer.setIgnoreResourceNotFound(true); return configurer; } }
这种方式绕过Spring占位符解析的顺序问题,直接使用已解析的系统属性拼接路径。
方法三:让共享库依赖Environment获取属性
修改共享库的MyLibProperties,直接从Environment中获取属性,避免依赖@Value和@PropertySource的顺序问题:
public class MyLibProperties { private String account; private String password; // 通过构造函数注入Environment public MyLibProperties(Environment env) { this.account = env.getProperty("scr.mylib.account"); this.password = env.getProperty("scr.mylib.password"); } // getter方法 public String getAccount() { return account; } public String getPassword() { return password; } }
然后修改共享库的配置类:
@Configuration @PropertySource(value = { "file:/scr/my-lib/${platform.env}-sec.properties" }, ignoreResourceNotFound = true) public class MyLibraryConfig { @Bean public MyLibProperties myLibProperties(Environment env) { return new MyLibProperties(env); } @Bean public JavaMailSender mailSender(final MyLibProperties properties) { final JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setUsername(properties.getAccount()); sender.setPassword(properties.getPassword()); return sender; } }
这种方式利用Environment的属性优先级,确保能获取到已加载的配置。
内容的提问来源于stack exchange,提问作者LoganBlack

