Skip to content

SpringBoot 配置类详解

概述

Spring Boot 推荐使用基于 Java 的配置方式。虽然可以在 SpringApplication 中使用 XML 源,但我们通常建议将单个 @Configuration 类作为主要配置源。通常,定义 main 方法的类是作为主要 @Configuration 的好选择。

TIP

互联网上发布了许多使用 XML 配置的 Spring 配置示例。如果可能的话,请始终尝试使用等效的基于 Java 的配置。搜索 Enable* 注解可能是一个很好的起点。

为什么使用配置类?

在传统的 Spring 开发中,我们通常使用 XML 文件来配置 Bean 和依赖关系。但是 XML 配置存在以下问题:

  • 类型安全性差:XML 配置在编译时无法检查类型错误
  • 重构困难:IDE 重构工具对 XML 支持有限
  • 调试困难:运行时才能发现配置错误
  • 代码分离:配置和代码分离,不利于理解和维护

SpringBoot 的配置类解决了这些问题:

基础配置类示例

让我们通过一个实际的电商系统示例来理解配置类的使用:

kotlin
package com.example.ecommerce

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestTemplate

/**
 * 主应用程序类,同时也是主要的配置类
 * @SpringBootApplication 包含了 @Configuration 注解
 */
@SpringBootApplication
class ECommerceApplication {

    /**
     * 配置 RestTemplate Bean,用于调用外部 API
     * 比如调用支付网关、物流API等
     */
    @Bean
    fun restTemplate(): RestTemplate {
        return RestTemplate()
    }

    /**
     * 配置数据库连接池
     */
    @Bean
    fun dataSourceConfig(): DatabaseConfig {
        return DatabaseConfig().apply {
            maxPoolSize = 20
            minPoolSize = 5
            connectionTimeout = 30000
        }
    }
}

fun main(args: Array<String>) {
    runApplication<ECommerceApplication>(*args)
}
java
package com.example.ecommerce;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * 主应用程序类,同时也是主要的配置类
 */
@SpringBootApplication
public class ECommerceApplication {

    /**
     * 配置 RestTemplate Bean
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(ECommerceApplication.class, args);
    }
}

导入额外的配置类

您不需要将所有的 @Configuration 放在单个类中。可以使用 @Import 注解来导入额外的配置类。另外,您也可以使用 @ComponentScan 来自动扫描所有 Spring 组件,包括 @Configuration 类。

实际业务场景:模块化配置

在大型电商系统中,我们通常会将配置按模块分离:

kotlin
package com.example.ecommerce

import com.example.ecommerce.config.DatabaseConfig
import com.example.ecommerce.config.RedisConfig
import com.example.ecommerce.config.SecurityConfig
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Import

/**
 * 主应用配置类
 * 使用 @Import 导入其他模块的配置类
 */
@SpringBootApplication
@Import(
    DatabaseConfig::class,    // 数据库配置
    RedisConfig::class,       // Redis 缓存配置
    SecurityConfig::class     // 安全配置
)
class ECommerceApplication

fun main(args: Array<String>) {
    runApplication<ECommerceApplication>(*args)
}
java
package com.example.ecommerce;

import com.example.ecommerce.config.DatabaseConfig;
import com.example.ecommerce.config.RedisConfig;
import com.example.ecommerce.config.SecurityConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import({DatabaseConfig.class, RedisConfig.class, SecurityConfig.class})
public class ECommerceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ECommerceApplication.class, args);
    }
}

数据库配置类示例

kotlin
package com.example.ecommerce.config

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import javax.sql.DataSource

/**
 * 数据库配置类
 * 专门负责数据库相关的 Bean 配置
 */
@Configuration
class DatabaseConfig {

    @Value("\${spring.datasource.url}")
    private lateinit var dbUrl: String

    @Value("\${spring.datasource.username}")
    private lateinit var username: String

    @Value("\${spring.datasource.password}")
    private lateinit var password: String

    /**
     * 配置主数据源
     * 用于用户、订单等核心业务数据
     */
    @Bean
    @Primary
    fun primaryDataSource(): DataSource {
        val config = HikariConfig().apply {
            jdbcUrl = dbUrl
            username = this@DatabaseConfig.username
            password = this@DatabaseConfig.password
            maximumPoolSize = 20  // 最大连接池大小
            minimumIdle = 5       // 最小空闲连接数
            connectionTimeout = 30000  // 连接超时时间
            idleTimeout = 600000      // 空闲超时时间
            maxLifetime = 1800000     // 连接最大生命周期
        }
        return HikariDataSource(config)
    }

    /**
     * 配置只读数据源
     * 用于报表查询等只读操作
     */
    @Bean("readOnlyDataSource")
    fun readOnlyDataSource(): DataSource {
        val config = HikariConfig().apply {
            jdbcUrl = "$dbUrl?readOnly=true"
            username = this@DatabaseConfig.username
            password = this@DatabaseConfig.password
            maximumPoolSize = 10
            minimumIdle = 2
            isReadOnly = true  // 设置为只读
        }
        return HikariDataSource(config)
    }
}
java
package com.example.ecommerce.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DatabaseConfig {

    @Value("${spring.datasource.url}")
    private String dbUrl;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Bean
    @Primary
    public DataSource primaryDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(dbUrl);
        config.setUsername(username);
        config.setPassword(password);
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        return new HikariDataSource(config);
    }
}

Redis 配置类示例

kotlin
package com.example.ecommerce.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer

/**
 * Redis 配置类
 * 负责缓存相关的配置
 */
@Configuration
class RedisConfig {

    @Value("\${spring.redis.host:localhost}")
    private lateinit var redisHost: String

    @Value("\${spring.redis.port:6379}")
    private var redisPort: Int = 6379

    /**
     * Redis 连接工厂配置
     */
    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        val factory = JedisConnectionFactory()
        factory.hostName = redisHost
        factory.port = redisPort
        factory.timeout = 2000  // 连接超时时间
        return factory
    }

    /**
     * Redis 模板配置
     * 用于购物车、会话存储等
     */
    @Bean
    fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Any> {
        val template = RedisTemplate<String, Any>()
        template.connectionFactory = connectionFactory

        // 设置键的序列化方式
        template.keySerializer = StringRedisSerializer()
        template.hashKeySerializer = StringRedisSerializer()

        // 设置值的序列化方式
        template.valueSerializer = GenericJackson2JsonRedisSerializer()
        template.hashValueSerializer = GenericJackson2JsonRedisSerializer()

        template.afterPropertiesSet()
        return template
    }
}
java
package com.example.ecommerce.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Value("${spring.redis.host:localhost}")
    private String redisHost;

    @Value("${spring.redis.port:6379}")
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setHostName(redisHost);
        factory.setPort(redisPort);
        return factory;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

使用组件扫描

除了使用 @Import,我们还可以使用 @ComponentScan 来自动扫描和加载配置类:

kotlin
package com.example.ecommerce

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan

/**
 * 使用组件扫描自动发现配置类
 * 扫描指定包下的所有 @Configuration 类
 */
@SpringBootApplication
@ComponentScan(
    basePackages = [
        "com.example.ecommerce.config",    // 扫描配置包
        "com.example.ecommerce.service",   // 扫描服务包
        "com.example.ecommerce.controller" // 扫描控制器包
    ]
)
class ECommerceApplication

fun main(args: Array<String>) {
    runApplication<ECommerceApplication>(*args)
}
java
package com.example.ecommerce;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {
    "com.example.ecommerce.config",
    "com.example.ecommerce.service",
    "com.example.ecommerce.controller"
})
public class ECommerceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ECommerceApplication.class, args);
    }
}

> `@SpringBootApplication` 注解本身就包含了 `@ComponentScan`,默认会扫描主类所在包及其子包。只有当您需要扫描其他包时,才需要显式使用 `@ComponentScan`。

导入 XML 配置

如果您绝对必须使用基于 XML 的配置,我们建议您仍然从 @Configuration 类开始。然后可以使用 @ImportResource 注解来加载 XML 配置文件。

实际场景:整合遗留系统

在实际项目中,可能需要整合一些使用 XML 配置的遗留系统:

kotlin
package com.example.ecommerce

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ImportResource

/**
 * 导入 XML 配置文件
 * 适用于需要兼容遗留系统的场景
 */
@SpringBootApplication
@ImportResource(
    "classpath:legacy-beans.xml",      // 遗留系统的Bean配置
    "classpath:third-party-config.xml" // 第三方库的XML配置
)
class ECommerceApplication

fun main(args: Array<String>) {
    runApplication<ECommerceApplication>(*args)
}
java
package com.example.ecommerce;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

@SpringBootApplication
@ImportResource({
    "classpath:legacy-beans.xml",
    "classpath:third-party-config.xml"
})
public class ECommerceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ECommerceApplication.class, args);
    }
}

XML 配置文件示例

xml
<!-- legacy-beans.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 遗留系统的订单处理器 -->
    <bean id="legacyOrderProcessor"
          class="com.legacy.system.OrderProcessor">
        <property name="timeout" value="30000"/>
        <property name="retryCount" value="3"/>
    </bean>

    <!-- 第三方支付网关配置 -->
    <bean id="paymentGateway"
          class="com.thirdparty.payment.Gateway">
        <constructor-arg value="${payment.gateway.url}"/>
        <constructor-arg value="${payment.gateway.key}"/>
    </bean>
</beans>

配置类的最佳实践

1. 模块化配置

将不同功能模块的配置分离到不同的配置类中:

2. 条件化配置

使用条件注解来控制配置的生效条件:

kotlin
package com.example.ecommerce.config

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile

/**
 * 条件化配置示例
 */
@Configuration
class ConditionalConfig {

    /**
     * 只在开发环境生效的配置
     */
    @Bean
    @Profile("dev")
    fun devMockService(): MockService {
        return MockService("开发环境模拟服务")
    }

    /**
     * 只在生产环境生效的配置
     */
    @Bean
    @Profile("prod")
    fun prodRealService(): RealService {
        return RealService("生产环境真实服务")
    }

    /**
     * 根据配置属性决定是否创建Bean
     */
    @Bean
    @ConditionalOnProperty(
        name = ["feature.email.enabled"],
        havingValue = "true",
        matchIfMissing = false
    )
    fun emailService(): EmailService {
        return EmailService()
    }
}
java
package com.example.ecommerce.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class ConditionalConfig {

    @Bean
    @Profile("dev")
    public MockService devMockService() {
        return new MockService("开发环境模拟服务");
    }

    @Bean
    @Profile("prod")
    public RealService prodRealService() {
        return new RealService("生产环境真实服务");
    }

    @Bean
    @ConditionalOnProperty(
        name = "feature.email.enabled",
        havingValue = "true",
        matchIfMissing = false
    )
    public EmailService emailService() {
        return new EmailService();
    }
}

3. 配置属性绑定

使用 @ConfigurationProperties 来绑定外部配置:

kotlin
package com.example.ecommerce.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration

/**
 * 应用配置属性
 * 绑定 application.yml 中的配置
 */
@Configuration
@ConfigurationProperties(prefix = "app")
data class AppConfig(
    var name: String = "电商系统",
    var version: String = "1.0.0",
    var database: DatabaseProperties = DatabaseProperties(),
    var redis: RedisProperties = RedisProperties(),
    var email: EmailProperties = EmailProperties()
) {

    data class DatabaseProperties(
        var maxPoolSize: Int = 20,
        var minPoolSize: Int = 5,
        var connectionTimeout: Long = 30000
    )

    data class RedisProperties(
        var timeout: Int = 2000,
        var maxIdle: Int = 10,
        var minIdle: Int = 2
    )

    data class EmailProperties(
        var enabled: Boolean = false,
        var host: String = "smtp.example.com",
        var port: Int = 587,
        var username: String = "",
        var password: String = ""
    )
}
java
package com.example.ecommerce.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "app")
public class AppConfig {
    private String name = "电商系统";
    private String version = "1.0.0";
    private DatabaseProperties database = new DatabaseProperties();
    private RedisProperties redis = new RedisProperties();

    // getter 和 setter 方法
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public static class DatabaseProperties {
        private int maxPoolSize = 20;
        private int minPoolSize = 5;
        private long connectionTimeout = 30000;

        // getter 和 setter 方法
    }

    public static class RedisProperties {
        private int timeout = 2000;
        private int maxIdle = 10;
        private int minIdle = 2;

        // getter 和 setter 方法
    }
}

对应的 application.yml 配置:

yaml
app:
  name: 电商管理系统
  version: 2.0.0
  database:
    max-pool-size: 50
    min-pool-size: 10
    connection-timeout: 60000
  redis:
    timeout: 5000
    max-idle: 20
    min-idle: 5
  email:
    enabled: true
    host: smtp.company.com
    port: 587
    username: [email protected]
    password: ${EMAIL_PASSWORD}

常见问题和解决方案

1. 循环依赖问题

WARNING

配置类之间可能会出现循环依赖问题,需要注意避免。

kotlin
// 错误示例 - 会导致循环依赖
@Configuration
class ConfigA {
    @Autowired
    private lateinit var serviceB: ServiceB

    @Bean
    fun serviceA(): ServiceA {
        return ServiceA(serviceB) // 依赖 ServiceB
    }
}

@Configuration
class ConfigB {
    @Autowired
    private lateinit var serviceA: ServiceA

    @Bean
    fun serviceB(): ServiceB {
        return ServiceB(serviceA) // 依赖 ServiceA,形成循环
    }
}

解决方案:

kotlin
@Configuration
class BetterConfig {

    @Bean
    fun serviceA(): ServiceA {
        return ServiceA()
    }

    @Bean
    fun serviceB(): ServiceB {
        return ServiceB()
    }

    /**
     * 通过方法参数注入,避免循环依赖
     */
    @Bean
    fun serviceIntegrator(serviceA: ServiceA, serviceB: ServiceB): ServiceIntegrator {
        serviceA.setServiceB(serviceB)
        serviceB.setServiceA(serviceA)
        return ServiceIntegrator(serviceA, serviceB)
    }
}
java
@Configuration
public class BetterConfig {

    @Bean
    public ServiceA serviceA() {
        return new ServiceA();
    }

    @Bean
    public ServiceB serviceB() {
        return new ServiceB();
    }

    @Bean
    public ServiceIntegrator serviceIntegrator(ServiceA serviceA, ServiceB serviceB) {
        serviceA.setServiceB(serviceB);
        serviceB.setServiceA(serviceA);
        return new ServiceIntegrator(serviceA, serviceB);
    }
}

2. Bean 重复定义

CAUTION

多个配置类定义相同名称的 Bean 可能会导致冲突。

解决方案:

kotlin
@Configuration
class DatabaseConfig {

    @Bean("primaryDataSource") // 明确指定Bean名称
    @Primary // 标记为主要Bean
    fun primaryDataSource(): DataSource {
        return createDataSource("primary")
    }

    @Bean("secondaryDataSource") // 明确指定不同的Bean名称
    fun secondaryDataSource(): DataSource {
        return createDataSource("secondary")
    }

    private fun createDataSource(name: String): DataSource {
        // 创建数据源的逻辑
        return HikariDataSource()
    }
}
java
@Configuration
public class DatabaseConfig {

    @Bean("primaryDataSource")
    @Primary
    public DataSource primaryDataSource() {
        return createDataSource("primary");
    }

    @Bean("secondaryDataSource")
    public DataSource secondaryDataSource() {
        return createDataSource("secondary");
    }

    private DataSource createDataSource(String name) {
        return new HikariDataSource();
    }
}

总结

配置类是 SpringBoot 应用的重要组成部分,它提供了类型安全、易于维护的配置方式。通过合理的模块划分和最佳实践,可以构建出清晰、可维护的应用架构。

核心要点

  1. 优先使用 Java 配置:相比 XML 配置,Java 配置提供更好的类型安全性和 IDE 支持
  2. 模块化设计:将不同功能的配置分离到不同的配置类中
  3. 合理使用注解@Import@ComponentScan@Conditional 等注解的合理使用
  4. 避免常见陷阱:循环依赖、Bean 重复定义等问题的预防和解决

TIP

在实际项目中,建议按照功能模块来组织配置类,这样既便于维护,也有利于团队协作开发。

通过掌握这些配置类的使用技巧,您可以更好地构建和维护 SpringBoot 应用程序。