@PropertySource 详解
本章定位:深入理解 Spring 的外部化配置加载机制。@PropertySource 是将应用配置(数据库连接、API 密钥、业务参数)从代码中剥离的核心注解,是实现"配置与代码分离"原则的基础设施。
定义与作用
@PropertySource 是 Spring 提供的属性源注解,用于在 @Configuration 类上指定额外的属性文件路径,将其加载到 Environment 中供 @Value 或 Environment API 使用。
解决的痛点:在 Java 代码中硬编码配置值,导致环境切换时必须重新编译:
// 痛点:配置硬编码在代码中
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db"); // 硬编码!
config.setUsername("root"); // 硬编码!
config.setPassword("secret"); // 硬编码!
return new HikariDataSource(config);
}
}
@PropertySource 让配置值外部化,存储在 properties 或 YAML 文件中,不同环境只需替换配置文件即可:
// 解决后:配置从外部文件加载
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig {
@Bean
public DataSource dataSource(
@Value("${db.url}") String url,
@Value("${db.username}") String username,
@Value("${db.password}") String password) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
return new HikariDataSource(config);
}
}
适用位置与常用属性
适用位置
@PropertySource 只能标注在类级别,通常与 @Configuration 配合使用。
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig {
}
常用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| value / name | String[] | {} | 指定属性文件路径。支持 classpath:、file:、http: 等前缀 |
| encoding | String | "" | 属性文件的字符编码 |
| factory | Class<? extends PropertySourceFactory> | PropertySourceFactory.class | 自定义属性源工厂,用于解析非标准格式(如 YAML) |
| ignoreResourceNotFound | boolean | false | 如果属性文件不存在,是否忽略而非抛异常 |
多属性文件加载
@Configuration
@PropertySource("classpath:application.properties")
@PropertySource("classpath:jdbc.properties")
@PropertySource("classpath:mail.properties")
public class AppConfig {
}
从 Spring 4.1 开始,可以使用 @PropertySources 聚合多个 @PropertySource(Java 8+ 可以直接重复使用 @PropertySource 注解)。
核心原理:PropertySource 加载流程
@PropertySource 触发的属性加载流程:
Environment 中的 PropertySource 优先级(从高到低):
- ServletConfig 参数(Web 环境)
- ServletContext 参数(Web 环境)
- JNDI 属性
- Java 系统属性(System.getProperties())
- 操作系统环境变量
- RandomValuePropertySource(random.*)
- @PropertySource 加载的属性文件
- 应用配置文件(application.properties / application.yml)
- Spring Boot 默认属性
优先级高的属性源会覆盖优先级低的同名属性。
完整示例:飞翔科技公司的外部化配置
场景简述
飞翔科技的学生管理系统需要连接数据库、发送邮件、调用第三方短信接口。架构师白歌要求所有可变配置(URL、账号、密码、业务参数)全部外部化到 properties 文件,代码中只保留 @Value 注入点。运维工程师李眉负责管理不同环境的配置文件。
操作前:配置硬编码在 Java 代码中
// 操作前:AppConfig.java —— 配置硬编码,环境切换需重新编译
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db");
config.setUsername("root");
config.setPassword("secret");
config.setMaximumPoolSize(10);
return new HikariDataSource(config);
}
@Bean
public EmailSender emailSender() {
return new SmtpEmailSender("smtp.learnto.cn", 587, "noreply@learnto.cn", "mailPass123");
}
@Bean
public SmsSender smsSender() {
return new AliyunSmsSender("LTAIxxxxxxxxxxxx", "secretKeyxxxxxxxx", "飞翔科技");
}
}
// 操作前:StudentService.java —— 业务参数硬编码
@Service
public class StudentService {
private static final int MAX_STUDENTS_PER_CLASS = 45; // 硬编码!
private static final int MIN_AGE = 18; // 硬编码!
private static final int MAX_AGE = 35; // 硬编码!
public void validateStudent(Student student) {
if (student.getAge() < MIN_AGE || student.getAge() > MAX_AGE) {
throw new BusinessException("年龄必须在 18-35 岁之间");
}
}
}
痛点:
- 数据库密码明文写在代码中,存在安全风险
- 生产环境切换时(如数据库地址变更),必须修改源码并重新编译打包
- 不同环境(dev/test/prod)需要维护多套代码分支
- 业务参数(如班级人数上限)调整需要发版
操作后:使用 @PropertySource 外部化配置
属性文件:
# application.properties
# 数据库配置
db.url=jdbc:mysql://localhost:3306/student_db
db.username=root
db.password=secret
db.pool.size=10
# 邮件配置
smtp.host=smtp.learnto.cn
smtp.port=587
smtp.username=noreply@learnto.cn
smtp.password=mailPass123
# 短信配置
sms.aliyun.accessKey=LTAIxxxxxxxxxxxx
sms.aliyun.secretKey=secretKeyxxxxxxxx
sms.aliyun.signature=飞翔科技
# 业务参数
student.maxPerClass=45
student.minAge=18
student.maxAge=35
student.tuition=23200.00
// 操作后:AppConfig.java —— 加载外部配置
@Configuration
@ComponentScan("com.feixiang.student")
@PropertySource("classpath:application.properties")
@EnableTransactionManagement
public class AppConfig {
@Bean
public DataSource dataSource(
@Value("${db.url}") String url,
@Value("${db.username}") String username,
@Value("${db.password}") String password,
@Value("${db.pool.size:10}") int poolSize) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(poolSize);
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
// 操作后:EmailConfig.java —— 邮件配置独立管理
@Configuration
@PropertySource("classpath:mail.properties")
public class EmailConfig {
@Bean
public EmailSender emailSender(
@Value("${smtp.host}") String host,
@Value("${smtp.port:587}") int port,
@Value("${smtp.username}") String username,
@Value("${smtp.password}") String password) {
return new SmtpEmailSender(host, port, username, password);
}
}
// 操作后:StudentService.java —— 业务参数外部化
@Service
public class StudentService {
private static final Logger logger = LoggerFactory.getLogger(StudentService.class);
private final StudentDao studentDao;
private final int maxStudentsPerClass;
private final int minAge;
private final int maxAge;
private final float tuition;
public StudentService(StudentDao studentDao,
@Value("${student.maxPerClass:45}") int maxStudentsPerClass,
@Value("${student.minAge:18}") int minAge,
@Value("${student.maxAge:35}") int maxAge,
@Value("${student.tuition:23200.00}") float tuition) {
this.studentDao = studentDao;
this.maxStudentsPerClass = maxStudentsPerClass;
this.minAge = minAge;
this.maxAge = maxAge;
this.tuition = tuition;
}
@Transactional
public void enrollStudent(Student student) {
validateStudent(student);
int currentCount = studentDao.countByClassName(student.getClassName());
if (currentCount >= maxStudentsPerClass) {
throw new BusinessException("班级已满,每班最多 " + maxStudentsPerClass + " 人");
}
student.setTuition(tuition);
studentDao.save(student);
logger.info("学生 {} 入学成功,学费:{}", student.getName(), tuition);
}
private void validateStudent(Student student) {
if (student.getAge() < minAge || student.getAge() > maxAge) {
throw new BusinessException("年龄必须在 " + minAge + "-" + maxAge + " 岁之间");
}
}
}
// 操作后:启动类
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
StudentService service = ctx.getBean(StudentService.class);
Environment env = ctx.getEnvironment();
System.out.println("数据库 URL:" + env.getProperty("db.url"));
System.out.println("班级人数上限:" + env.getProperty("student.maxPerClass"));
System.out.println("学费标准:" + env.getProperty("student.tuition"));
Student student = new Student("小崔", 22, "计算机科学与技术");
service.enrollStudent(student);
}
}
运行结果及分析
运行输出:
数据库 URL:jdbc:mysql://localhost:3306/student_db
班级人数上限:45
学费标准:23200.00
[main] INFO c.f.s.service.StudentService - 学生 小崔 入学成功,学费:23200.0
改进点总结:
| 维度 | 操作前(硬编码) | 操作后(@PropertySource 外部化) |
|---|---|---|
| 环境切换 | 修改源码 → 重新编译 → 重新打包 | 替换配置文件 → 重启应用 |
| 安全管控 | 密码明文写在代码中,所有开发者可见 | 密码只在配置文件中,可限制文件访问权限 |
| 动态调整 | 业务参数调整需发版 | 修改 properties 文件后重启即可生效 |
| 多环境管理 | 维护多套代码分支 | 一套代码 + 多套配置文件 |
| 配置集中 | 散落在各个 Java 文件中 | 统一在 properties 文件中管理 |
@Value 的高级用法
@PropertySource 加载的属性可以通过 @Value 以多种方式注入:
默认值
@Value("${db.pool.size:10}") // 如果 db.pool.size 未定义,默认使用 10
private int poolSize;
SpEL 表达式
@Value("#{${student.maxPerClass} * 2}") // SpEL 计算:45 * 2 = 90
private int maxStudentsPerGrade;
@Value("#{systemProperties['user.home']}/logs") // 引用系统属性
private String logPath;
注入列表
@Value("${student.allowedMajors}") // properties 中:student.allowedMajors=CS,SE,AI
private String[] allowedMajors; // ["CS", "SE", "AI"]
易错场景与面试考点
反例一:属性文件路径错误导致配置未加载
小崔把属性文件放在 src/main/resources 下,但路径写错了:
// ❌ 错误:路径拼写错误,属性文件未加载
@Configuration
@PropertySource("classpath:aplication.properties") // 拼写错误!少了 'p'
public class AppConfig {
}
启动时不会报错(默认 ignoreResourceNotFound = false 会抛异常,但如果文件存在只是拼写错误,会静默失败),但 @Value 注入时值为 null:
[main] WARN o.s.c.a.AnnotationConfigApplicationContext -
Exception encountered during context initialization
Caused by: java.io.FileNotFoundException:
class path resource [aplication.properties] cannot be opened because it does not exist
纠正:使用 IDE 的自动补全功能,或启用 ignoreResourceNotFound 做防御性编程:
// ✅ 正确:确保路径正确,或设置 ignoreResourceNotFound
@Configuration
@PropertySource(value = "classpath:application.properties", ignoreResourceNotFound = true)
public class AppConfig {
}
反例二:@Value 注入基本类型时类型转换失败
小崔在 properties 中定义了布尔值,但格式不标准:
# ❌ 错误:布尔值格式不标准
feature.cache.enabled=yes // Spring 只识别 true/false/1/0/ON/OFF/YES/NO
@Value("${feature.cache.enabled}")
private boolean cacheEnabled; // 可能抛 ConversionFailedException
纠正:使用 Spring 支持的标准布尔值格式:
# ✅ 正确:标准布尔值格式
feature.cache.enabled=true
反例三:@PropertySource 加载顺序导致属性被覆盖
小崔同时加载了多个属性文件,但后面的文件覆盖了前面的同名属性:
// ❌ 错误:加载顺序导致意外覆盖
@Configuration
@PropertySource("classpath:application.properties") // db.url = jdbc:mysql://localhost...
@PropertySource("classpath:override.properties") // db.url = jdbc:mysql://prod-db...
public class AppConfig {
}
问题:如果 override.properties 是用于特定环境的覆盖配置,这种写法是正确的。但如果开发者误以为 application.properties 会优先,就会产生困惑。
纠正:理解 PropertySource 的优先级规则——后加载的 @PropertySource 优先级更高,同名属性会覆盖前者。如果需要明确优先级,使用 Environment 的 API 编程式控制:
// ✅ 正确:编程式控制 PropertySource 优先级
@Configuration
public class AppConfig implements EnvironmentAware {
@Override
public void setEnvironment(Environment env) {
MutablePropertySources sources = ((ConfigurableEnvironment) env).getPropertySources();
// 将自定义属性源插入到最高优先级
sources.addFirst(new PropertiesPropertySource("custom", loadCustomProperties()));
}
private Properties loadCustomProperties() {
Properties props = new Properties();
props.setProperty("db.url", "jdbc:mysql://custom-db:3306/student_db");
return props;
}
}
反例四:在 @PropertySource 中使用 YAML 文件未配置工厂
小崔尝试用 @PropertySource 加载 YAML 文件:
// ❌ 错误:@PropertySource 默认只支持 .properties,不支持 YAML
@Configuration
@PropertySource("classpath:application.yml") // 无法正确解析!
public class AppConfig {
}
纠正:使用 YamlPropertySourceFactory(Spring Boot 已自动支持,纯 Spring Framework 需手动配置):
// ✅ 正确:指定 YAML 工厂
@Configuration
@PropertySource(value = "classpath:application.yml", factory = YamlPropertySourceFactory.class)
public class AppConfig {
}
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
Properties properties = factory.getObject();
return new PropertiesPropertySource(resource.getResource().getFilename(), properties);
}
}
面试高频题
Q1:@PropertySource 和 @Value 有什么关系?
@PropertySource 负责将外部属性文件加载到 Environment 中;@Value 负责从 Environment 中读取属性值并注入到字段或方法参数中。两者是"加载"与"消费"的关系。没有 @PropertySource,@Value 只能读取系统属性和环境变量;有了 @PropertySource,@Value 可以读取自定义属性文件中的配置。
Q2:@PropertySource 加载的属性文件优先级如何?
@PropertySource 加载的属性源默认追加到 Environment 的 propertySources 列表末尾。优先级高于 Spring Boot 默认属性,但低于 Java 系统属性和操作系统环境变量。后加载的 @PropertySource 优先级高于先加载的,同名属性会被覆盖。
Q3:如何实现配置文件的加密?
Spring 本身不提供配置加密功能。常见方案:① 使用 Jasypt 等第三方库,在 @Value 注入时自动解密;② 使用 Spring Cloud Config 的服务端加密;③ 使用操作系统级的密钥管理服务(如 AWS KMS、阿里云 KMS)。核心思路是在 PropertySource 加载后、@Value 注入前,对敏感属性进行解密转换。
Q4:@PropertySource 能否加载多个文件?
可以。Java 8+ 支持在同一个类上重复使用 @PropertySource 注解。也可以使用 @PropertySources 聚合多个 @PropertySource。加载顺序为声明顺序,后加载的文件中同名属性会覆盖先加载的。