--- title: "03-优雅配置管理方案" created: 2025-12-02 tags: - 项目 aliases: - 优雅配置管理方案 --- # 优雅配置管理方案 我觉得这种方式挺不错的 使用环境占位符 然后编写一个.env 部署时指定.env、再指定prod启动 在代码中统一使用AppProperties管理 其他地方注入AppProperties 实现优雅的管理 修改仅需在.env集中修改 ### **核心思路** ![[优雅配置管理-0beb31a5.jpg]] ### **.env 文件模板** #### `.env.example` ```properties # ===================================================== # .env.example - 环境变量模板(提交到 Git) # ===================================================== # 使用方式: # 1. 复制此文件:cp .env.example .env.prod # 2. 填写实际配置值 # 3. 启动:source .env.prod && java -jar app.jar --spring.profiles.active=prod # ---------- 环境标识 ---------- SPRING_PROFILES_ACTIVE=prod # ---------- 数据库配置 ---------- DB_HOST=localhost DB_PORT=3306 DB_NAME=your_database DB_USERNAME=root DB_PASSWORD=your_password # ---------- Redis 配置 ---------- REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= REDIS_DATABASE=0 # ---------- 应用安全配置 ---------- JWT_SECRET=your-jwt-secret-key-at-least-32-characters-long # ---------- CORS 配置 ---------- CORS_ORIGINS=httpswww.example.com,httpsadmin.example.com # ---------- 可选配置 ---------- # SERVER_PORT=8080 # LOG_LEVEL=INFO ``` #### `.env.dev` ```properties # ===================================================== # .env.dev - 开发环境 # ===================================================== SPRING_PROFILES_ACTIVE=dev APP_NAME=ZwwwSpringBootBaseTemplate APP_VERSION=1.0.0 APP_DEBUG=true SERVER_PORT=8080 DB_HOST=localhost DB_PORT=3306 DB_NAME=zwwwspringbootbasetemplate DB_USERNAME=root DB_PASSWORD=zw200495 REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= REDIS_DATABASE=0 APP_JWT_SECRET=dev-jwt-secret-for-local-development-only-simple-key APP_JWT_EXPIRATION=86400 APP_FILE_MAX_SIZE=10485760 APP_FILE_ALLOWED_FORMATS=jpg,jpeg,png,gif,webp,pdf APP_FILE_UPLOAD_PATH=D:/uploads/dev APP_USER_MAX_PASSWORD_RETRY=5 APP_USER_MAX_LOGIN_DEVICE=5 APP_USER_LOCK_MINUTES=15 APP_CORS_ORIGINS=http://localhost:5173,http://localhost:3000 KNIFE4J_ENABLE=true ``` #### `.env.prod` ```properties # ===================================================== # .env.prod - 生产环境配置 # ===================================================== # ============ Spring Profile ============ SPRING_PROFILES_ACTIVE=prod # ============ 应用信息 ============ APP_NAME=ZwwwSpringBootBaseTemplate-Prod APP_VERSION=1.0.0 SERVER_PORT=8080 # ============ 数据库配置 ============ DB_HOST=prod-mysql.example.com DB_PORT=3306 DB_NAME=prod_database DB_USERNAME=prod_db_user DB_PASSWORD=super_secure_database_password_here_min_16_chars # ============ Redis 配置 ============ REDIS_HOST=prod-redis.example.com REDIS_PORT=6379 REDIS_PASSWORD=redis_secure_password_here_min_16_chars REDIS_DATABASE=0 # ============ JWT 配置 ============ APP_JWT_SECRET=production_jwt_secret_must_be_very_long_and_secure_at_least_256_bits APP_JWT_EXPIRATION=604800 # ============ 文件上传配置 ============ APP_FILE_MAX_SIZE=10485760 APP_FILE_ALLOWED_FORMATS=jpg,jpeg,png,gif,webp,pdf APP_FILE_UPLOAD_PATH=/var/data/uploads # ============ 用户配置 ============ APP_USER_MAX_PASSWORD_RETRY=3 APP_USER_MAX_LOGIN_DEVICE=3 APP_USER_LOCK_MINUTES=60 # ============ CORS 配置 ============ APP_CORS_ORIGINS=https://www.example.com,https://admin.example.com # ============ 文档配置 ============ KNIFE4J_ENABLE=false ``` #### `.env.prod.local` ```properties # ===================================================== # .env.prod.local - 本地模拟生产环境 # ===================================================== SPRING_PROFILES_ACTIVE=prod APP_NAME=ZwwwSpringBootBaseTemplate-Prod APP_VERSION=1.0.0 APP_DEBUG=false SERVER_PORT=8080 DB_HOST=localhost DB_PORT=3306 DB_NAME=zwwwspringbootbasetemplate DB_USERNAME=root DB_PASSWORD=zw200495 REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= REDIS_DATABASE=1 APP_JWT_SECRET=prod_test_jwt_secret_must_be_very_long_and_secure_at_least_256_bits APP_JWT_EXPIRATION=604800 APP_FILE_MAX_SIZE=10485760 APP_FILE_ALLOWED_FORMATS=jpg,jpeg,png,gif,webp,pdf APP_FILE_UPLOAD_PATH=D:/uploads/prod APP_USER_MAX_PASSWORD_RETRY=3 APP_USER_MAX_LOGIN_DEVICE=3 APP_USER_LOCK_MINUTES=60 APP_CORS_ORIGINS=http://localhost:5173,http://localhost:3000 KNIFE4J_ENABLE=true ``` ### **YAML 配置** #### `application.yml` ```yaml # ===================================================== # application.yml - 主配置文件 # ===================================================== server: port: ${SERVER_PORT:8080} servlet: context-path: /api tomcat: max-http-form-post-size: 100MB max-swallow-size: -1 spring: application: name: ${APP_NAME:ZwwwSpringBootBaseTemplate} profiles: active: ${SPRING_PROFILES_ACTIVE:dev} datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:zwwwspringbootbasetemplate}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: ${DB_USERNAME:root} password: ${DB_PASSWORD:} servlet: multipart: enabled: true max-file-size: 10MB max-request-size: 50MB redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD:} database: ${REDIS_DATABASE:0} timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms cache: type: redis redis: time-to-live: 300s cache-null-values: true session: store-type: redis redis: namespace: ${spring.application.name}:session flush-mode: on_save timeout: 86400s mybatis-plus: configuration: map-underscore-to-camel-case: true default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler global-config: db-config: logic-delete-field: isDeleted logic-delete-value: 1 logic-not-delete-value: 0 knife4j: enable: ${KNIFE4J_ENABLE:true} # 自定义应用配置 app: name: ${APP_NAME:ZwwwSpringBootBaseTemplate} version: ${APP_VERSION:1.0.0} debug: ${APP_DEBUG:false} jwt: secret: ${APP_JWT_SECRET:default-dev-secret-please-change-in-production} expiration: ${APP_JWT_EXPIRATION:604800} token-prefix: "Bearer " header-name: Authorization file: max-size: ${APP_FILE_MAX_SIZE:10485760} allowed-formats: ${APP_FILE_ALLOWED_FORMATS:jpg,jpeg,png,gif,webp,pdf} upload-path: ${APP_FILE_UPLOAD_PATH:/uploads} user: max-password-retry: ${APP_USER_MAX_PASSWORD_RETRY:5} max-login-device: ${APP_USER_MAX_LOGIN_DEVICE:3} lock-minutes: ${APP_USER_LOCK_MINUTES:30} cors: allowed-origins: ${APP_CORS_ORIGINS:http://localhost:5173,http://localhost:3000} logging: level: root: INFO com.zwnsyw.zwwwspringbootbasetemplate: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" ``` #### `application-dev.yml` ```yaml # ===================================================== # application-dev.yml - 开发环境配置 # ===================================================== mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl knife4j: enable: true logging: level: com.zwnsyw: DEBUG ``` #### `application-prod.yml` ```yaml # ===================================================== # application-prod.yml - 生产环境配置 # ===================================================== spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl knife4j: enable: false logging: level: root: WARN com.zwnsyw: INFO ``` --- ### **完善的 AppProperties** ```java package com.zwnsyw.zwwwspringbootbasetemplate.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; import javax.validation.constraints.Min; import javax.validation.constraints.NotBlank; /** * 应用配置属性 - 统一配置入口 * * ==================== 使用说明 ==================== * * 本类采用三层配置管理机制: * 1. application.yml - 主配置文件(所有环境通用的默认值) * 2. application-{profile}.yml - 环境特定配置(dev/test/prod) * 3. .env.{profile} - 环境变量文件(敏感信息,不提交 Git) * * ==================== 加载顺序 ==================== * * 1. application.yml 中的默认值被加载 * 2. application-{spring.profiles.active}.yml 覆盖默认值 * 3. 系统环境变量或 .env 文件中的值最终覆盖以上配置 * * 示例: * - 在 application.yml 中定义: app.name: default-name * - 在 application-dev.yml 中覆盖: app.name: dev-name * - 在 .env.dev 中注入: APP_NAME=local-dev-name * * ==================== 注入方式 ==================== * * 1. 字段注入(简单场景): * @Autowired * private AppProperties appProperties; * * 2. 构造器注入(推荐,便于测试): * public MyService(AppProperties appProperties) { * this.appProperties = appProperties; * } * * 3. 方法参数注入: * public void myMethod(@Qualifier("appProperties") AppProperties config) { } * * ==================== 环境变量映射 ==================== * * YAML 配置 -> 环境变量(Spring 会自动转换): * - app.name -> APP_NAME * - app.jwt.secret -> APP_JWT_SECRET * - app.jwt.expiration -> APP_JWT_EXPIRATION * - app.file.max-size -> APP_FILE_MAX_SIZE * * 注:在 .env 文件中使用大写 + 下划线格式 * * ==================== 配置文件检查列表 ==================== * * ✓ application.yml - 主配置,包含所有默认值 * ✓ application-dev.yml - 开发环境覆盖 * ✓ application-test.yml - 测试环境覆盖 * ✓ application-prod.yml - 生产环境覆盖(关键值从环境变量读取) * ✓ .env.dev - 开发本地环境变量(.gitignore) * ✓ .env.test - 测试环境变量(.gitignore) * ✓ .env.prod - 生产环境变量(不提交,服务器手动创建) * * ==================== 使用示例 ==================== * * @RestController * public class UserController { * private final AppProperties appProperties; * * public UserController(AppProperties appProperties) { * this.appProperties = appProperties; * } * * @PostMapping("/login") * public ResponseEntity> login(@RequestBody LoginRequest request) { * // 获取 JWT 配置并生成 Token * String jwtSecret = appProperties.getJwt().getSecret(); * long expirationMs = appProperties.getJwt().getExpirationMs(); * return ResponseEntity.ok(generateToken(request.getUsername(), jwtSecret, expirationMs)); * } * * @PostMapping("/upload") * public ResponseEntity> uploadFile(@RequestParam("file") MultipartFile file) { * // 获取文件配置 * long maxSize = appProperties.getFile().getMaxSize(); * String[] formats = appProperties.getFile().getAllowedFormatArray(); * * if (file.getSize() > maxSize) { * return ResponseEntity.badRequest() * .body("文件大小超过限制: " + appProperties.getFile().getMaxSizeMB() + "MB"); * } * return ResponseEntity.ok("上传成功"); * } * } * * @Service * public class AuthService { * private final AppProperties appProperties; * * public AuthService(AppProperties appProperties) { * this.appProperties = appProperties; * } * * // 判断环境 * public void initializeSystem() { * if (appProperties.isProduction()) { * logger.info("生产环境启动"); * } else if (appProperties.isDebug()) { * logger.info("开发环境启动,调试模式已启用"); * } * } * * // 密码重试锁定 * public void validatePasswordRetry(String userId) { * int maxRetry = appProperties.getUser().getMaxPasswordRetry(); * // 业务逻辑... * } * } * * @Configuration * public class CorsConfig implements WebMvcConfigurer { * private final AppProperties appProperties; * * public CorsConfig(AppProperties appProperties) { * this.appProperties = appProperties; * } * * @Override * public void addCorsMappings(CorsRegistry registry) { * // 从配置中读取允许的源 * String[] allowedOrigins = appProperties.getCors().getAllowedOriginsArray(); * registry.addMapping("/**") * .allowedOrigins(allowedOrigins) * .allowedMethods("*") * .allowedHeaders("*"); * } * } */ @Data @Component @Validated @ConfigurationProperties(prefix = "app") public class AppProperties { /** 应用名称 */ @NotBlank(message = "应用名称不能为空") private String name; /** 应用版本 */ private String version = "1.0.0"; /** 是否开启调试模式(仅用于标识,实际由 spring.profiles.active 决定) */ private boolean debug = false; /** JWT 配置 */ private JwtConfig jwt = new JwtConfig(); /** 文件上传配置 */ private FileConfig file = new FileConfig(); /** 用户相关配置 */ private UserConfig user = new UserConfig(); /** CORS 配置 */ private CorsConfig cors = new CorsConfig(); // ==================== 内部配置类 ==================== /** * JWT 配置 * 来源: application.yml + application-{profile}.yml + .env.{profile} */ @Data public static class JwtConfig { /** JWT 密钥 - 生产环境必须从 .env.prod 注入,不要使用默认值 */ @NotBlank(message = "JWT密钥不能为空") private String secret; /** 过期时间(秒),默认7天 */ @Min(value = 60, message = "JWT过期时间不能少于60秒") private long expiration = 604800; /** Token 前缀,默认 Bearer */ private String tokenPrefix = "Bearer "; /** Authorization Header 名称,默认 Authorization */ private String headerName = "Authorization"; /** 获取过期时间(毫秒)- 方便在代码中使用 */ public long getExpirationMs() { return expiration * 1000; } } /** * 文件上传配置 * 来源: application.yml + application-{profile}.yml */ @Data public static class FileConfig { /** 最大文件大小(字节),默认10MB */ private long maxSize = 10485760; /** 允许的文件格式(逗号分隔) */ private String allowedFormats = "jpg,jpeg,png,gif,webp,pdf"; /** 上传路径 */ private String uploadPath = "/uploads"; /** 获取允许的格式列表(数组形式,便于校验) */ public String[] getAllowedFormatArray() { return allowedFormats.split(","); } /** 获取最大文件大小(MB)- 方便在错误提示中使用 */ public long getMaxSizeMB() { return maxSize / (1024 * 1024); } } /** * 用户相关配置 * 来源: application.yml + application-{profile}.yml */ @Data public static class UserConfig { /** 密码最大重试次数,超过后锁定账户 */ private int maxPasswordRetry = 5; /** 最大同时登录设备数 */ private int maxLoginDevice = 3; /** 账户锁定时间(分钟) */ private int lockMinutes = 30; } /** * CORS 跨域配置 * 来源: cors.allowed-origins(在 application.yml 中定义) * 注:虽然放在 app 下,但实际配置在 cors 节点,需要手动读取 */ @Data public static class CorsConfig { /** 允许的源(逗号分隔) */ private String allowedOrigins = "http://localhost:5173,http://localhost:3000"; /** 获取允许的源列表(数组形式) */ public String[] getAllowedOriginsArray() { if (allowedOrigins == null || allowedOrigins.isEmpty()) { return new String[]{"*"}; } return allowedOrigins.split(","); } } // ==================== 便捷方法 ==================== /** 是否为生产环境 */ public boolean isProduction() { return !debug; } /** 是否为开发环境 */ public boolean isDevelopment() { return debug; } } ``` ### **Contoller测试输出配置** ```java package com.zwnsyw.zwwwspringbootbasetemplate.controller; import com.zwnsyw.zwwwspringbootbasetemplate.config.AppProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; /** * 配置信息展示控制器 *
* 用于查看当前运行环境和配置信息 * 注意:生产环境应考虑禁用或添加权限控制 *
*/ @Profile("!prod") @RestController @RequestMapping("/config") @Slf4j public class ConfigController { @Autowired private AppProperties appProperties; @Autowired private Environment environment; @Value("${spring.profiles.active:default}") private String activeProfile; @Value("${server.port:8080}") private String serverPort; @Value("${spring.datasource.url:未配置}") private String datasourceUrl; /** * 显示完整配置信息 * GET /api/config/show */ @GetMapping("/show") public ResponseEntity