乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@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 / nameString[]{}指定属性文件路径。支持 classpath:、file:、http: 等前缀
encodingString""属性文件的字符编码
factoryClass<? extends PropertySourceFactory>PropertySourceFactory.class自定义属性源工厂,用于解析非标准格式(如 YAML)
ignoreResourceNotFoundbooleanfalse如果属性文件不存在,是否忽略而非抛异常

多属性文件加载

@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 优先级(从高到低):

  1. ServletConfig 参数(Web 环境)
  2. ServletContext 参数(Web 环境)
  3. JNDI 属性
  4. Java 系统属性(System.getProperties())
  5. 操作系统环境变量
  6. RandomValuePropertySource(random.*)
  7. @PropertySource 加载的属性文件
  8. 应用配置文件(application.properties / application.yml)
  9. 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 岁之间");
        }
    }
}

痛点:

  1. 数据库密码明文写在代码中,存在安全风险
  2. 生产环境切换时(如数据库地址变更),必须修改源码并重新编译打包
  3. 不同环境(dev/test/prod)需要维护多套代码分支
  4. 业务参数(如班级人数上限)调整需要发版

操作后:使用 @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。加载顺序为声明顺序,后加载的文件中同名属性会覆盖先加载的。

上一页
@Profile 详解
下一页
@Service 详解