优雅配置管理方案

我觉得这种方式挺不错的 使用环境占位符 然后编写一个.env 部署时指定.env、再指定prod启动 在代码中统一使用AppProperties管理 其他地方注入AppProperties 实现优雅的管理 修改仅需在.env集中修改

核心思路

优雅配置管理-0beb31a5

.env 文件模板

.env.example

# =====================================================
# .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

# =====================================================
# .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

# =====================================================
# .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

# =====================================================
# .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

# =====================================================
# 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

# =====================================================
# 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

# =====================================================
# 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

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测试输出配置

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;

/**
 * 配置信息展示控制器
 * <p>
 * 用于查看当前运行环境和配置信息
 * 注意:生产环境应考虑禁用或添加权限控制
 * </p>
 */
@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<Map<String, Object>> getConfig() {
        Map<String, Object> result = new LinkedHashMap<>();

        // ==================== 环境信息 ====================
        Map<String, Object> envInfo = new LinkedHashMap<>();
        envInfo.put("activeProfile", activeProfile);
        envInfo.put("activeProfiles", Arrays.asList(environment.getActiveProfiles()));
        envInfo.put("isDebugMode", appProperties.isDebug());
        envInfo.put("isProduction", appProperties.isProduction());
        envInfo.put("isDevelopment", appProperties.isDevelopment());
        envInfo.put("serverPort", serverPort);
        envInfo.put("javaVersion", System.getProperty("java.version"));
        envInfo.put("osName", System.getProperty("os.name"));
        envInfo.put("queryTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        result.put("environment", envInfo);

        // ==================== 应用基本信息 ====================
        Map<String, Object> appInfo = new LinkedHashMap<>();
        appInfo.put("name", appProperties.getName());
        appInfo.put("version", appProperties.getVersion());
        result.put("application", appInfo);

        // ==================== JWT 配置 ====================
        Map<String, Object> jwtConfig = new LinkedHashMap<>();
        jwtConfig.put("secret", maskSecret(appProperties.getJwt().getSecret()));
        jwtConfig.put("expirationSeconds", appProperties.getJwt().getExpiration());
        jwtConfig.put("expirationMs", appProperties.getJwt().getExpirationMs());
        jwtConfig.put("expirationHuman", formatDuration(appProperties.getJwt().getExpiration()));
        jwtConfig.put("tokenPrefix", appProperties.getJwt().getTokenPrefix());
        jwtConfig.put("headerName", appProperties.getJwt().getHeaderName());
        result.put("jwt", jwtConfig);

        // ==================== 文件上传配置 ====================
        Map<String, Object> fileConfig = new LinkedHashMap<>();
        fileConfig.put("maxSizeBytes", appProperties.getFile().getMaxSize());
        fileConfig.put("maxSizeMB", appProperties.getFile().getMaxSizeMB() + " MB");
        fileConfig.put("allowedFormats", appProperties.getFile().getAllowedFormats());
        fileConfig.put("allowedFormatList", Arrays.asList(appProperties.getFile().getAllowedFormatArray()));
        fileConfig.put("uploadPath", appProperties.getFile().getUploadPath());
        result.put("file", fileConfig);

        // ==================== 用户配置 ====================
        Map<String, Object> userConfig = new LinkedHashMap<>();
        userConfig.put("maxPasswordRetry", appProperties.getUser().getMaxPasswordRetry());
        userConfig.put("maxLoginDevice", appProperties.getUser().getMaxLoginDevice());
        userConfig.put("lockMinutes", appProperties.getUser().getLockMinutes());
        result.put("user", userConfig);

        // ==================== CORS 配置 ====================
        Map<String, Object> corsConfig = new LinkedHashMap<>();
        corsConfig.put("allowedOrigins", appProperties.getCors().getAllowedOrigins());
        corsConfig.put("allowedOriginList", Arrays.asList(appProperties.getCors().getAllowedOriginsArray()));
        result.put("cors", corsConfig);

        // ==================== 数据源信息(脱敏) ====================
        Map<String, Object> dbInfo = new LinkedHashMap<>();
        dbInfo.put("url", maskDatabaseUrl(datasourceUrl));
        result.put("datasource", dbInfo);

        return ResponseEntity.ok(result);
    }

    /**
     * 简要信息(适合健康检查)
     * GET /api/config/info
     */
    @GetMapping("/info")
    public ResponseEntity<Map<String, Object>> getBasicInfo() {
        Map<String, Object> info = new LinkedHashMap<>();
        info.put("app", appProperties.getName());
        info.put("version", appProperties.getVersion());
        info.put("profile", activeProfile);
        info.put("debug", appProperties.isDebug());
        info.put("time", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        return ResponseEntity.ok(info);
    }

    /**
     * 环境判断接口
     * GET /api/config/env
     */
    @GetMapping("/env")
    public ResponseEntity<Map<String, Object>> getEnvironment() {
        Map<String, Object> env = new LinkedHashMap<>();
        env.put("profile", activeProfile);
        env.put("isDev", "dev".equalsIgnoreCase(activeProfile));
        env.put("isTest", "test".equalsIgnoreCase(activeProfile));
        env.put("isProd", "prod".equalsIgnoreCase(activeProfile));
        env.put("isDebugMode", appProperties.isDebug());
        return ResponseEntity.ok(env);
    }

    // ==================== 工具方法 ====================

    /**
     * 脱敏密钥(只显示前4位和后4位)
     */
    private String maskSecret(String secret) {
        if (secret == null || secret.length() < 10) {
            return "***";
        }
        return secret.substring(0, 4) + "****" + secret.substring(secret.length() - 4);
    }

    /**
     * 脱敏数据库URL(隐藏密码部分)
     */
    private String maskDatabaseUrl(String url) {
        if (url == null) {
            return "未配置";
        }
        // 移除可能包含的密码参数
        return url.replaceAll("password=[^&]*", "password=***")
                .replaceAll("://[^:]+:[^@]+@", "://***:***@");
    }

    /**
     * 格式化时间为可读格式
     */
    private String formatDuration(long seconds) {
        if (seconds < 60) {
            return seconds + " 秒";
        } else if (seconds < 3600) {
            return (seconds / 60) + " 分钟";
        } else if (seconds < 86400) {
            return (seconds / 3600) + " 小时";
        } else {
            return (seconds / 86400) + " 天";
        }
    }
}

在代码中统一使用

// ==================== Service 中使用 ====================
@Service
public class AuthService {

    private final AppProperties appProperties;

    // 构造器注入(推荐)
    public AuthService(AppProperties appProperties) {
        this.appProperties = appProperties;
    }

    public String generateToken(Long userId) {
        AppProperties.JwtConfig jwt = appProperties.getJwt();

        return Jwts.builder()
                .setSubject(String.valueOf(userId))
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + jwt.getExpirationMs()))
                .signWith(Keys.hmacShaKeyFor(jwt.getSecret().getBytes()))
                .compact();
    }
}

// ==================== Controller 中使用 ====================
@RestController
@RequestMapping("/api")
public class InfoController {

    @Autowired
    private AppProperties appProperties;

    @GetMapping("/info")
    public Map<String, Object> getAppInfo() {
        return Map.of(
            "name", appProperties.getName(),
            "version", appProperties.getVersion(),
            "debug", appProperties.isDebug()
        );
    }
}

// ==================== Filter 中使用 ====================
@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired
    private AppProperties appProperties;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) {
        String header = request.getHeader(appProperties.getJwt().getHeaderName());
        String prefix = appProperties.getJwt().getTokenPrefix();

        if (header != null && header.startsWith(prefix)) {
            String token = header.substring(prefix.length());
            // 验证 token...
        }
        chain.doFilter(request, response);
    }
}

// ==================== 工具类中使用 ====================
@Component
public class FileUtils {

    private final AppProperties appProperties;

    public FileUtils(AppProperties appProperties) {
        this.appProperties = appProperties;
    }

    public void validateFile(MultipartFile file) {
        AppProperties.FileConfig config = appProperties.getFile();

        if (file.getSize() > config.getMaxSize()) {
            throw new BusinessException("文件大小不能超过 " + config.getMaxSizeMB() + "MB");
        }

        // 检查格式...
    }
}

部署脚本

start-dev.bat

@echo off
chcp 65001 >nul
echo ========================================
echo   加载 .env.dev 环境变量
echo ========================================

REM 检查 .env.dev 文件是否存在
if not exist .env.dev (
    echo [错误] .env.dev 文件不存在!
    pause
    exit /b 1
)

REM 逐行读取并设置环境变量(跳过注释和空行)
for /f "usebackq eol=# tokens=1,* delims==" %%a in (".env.dev") do (
    if not "%%a"=="" (
        if not "%%b"=="" (
            echo 设置: %%a=%%b
            set "%%a=%%b"
        )
    )
)

echo.
echo ========================================
echo   启动 Spring Boot 应用
echo ========================================
echo.
echo 提示: 按 Ctrl+C 可停止应用
echo.

REM 启动应用(移除 call,直接运行保持窗口)
mvn spring-boot:run -Dspring-boot.run.profiles=dev

REM 无论成功失败都暂停
echo.
echo ========================================
if errorlevel 1 (
    echo [错误] 应用异常退出!
) else (
    echo [信息] 应用已停止
)
echo ========================================
pause

start-prod.bat

@echo off
chcp 65001 >nul
echo ========================================
echo   加载 .env.prod.local 环境变量
echo   【本地模拟生产环境】
echo ========================================

REM 检查文件是否存在
if not exist .env.prod.local (
    echo [错误] .env.prod.local 文件不存在!
    echo 请复制 .env.prod 并修改为本地配置
    pause
    exit /b 1
)

REM 加载环境变量
for /f "usebackq eol=# tokens=1,* delims==" %%a in (".env.prod.local") do (
    if not "%%a"=="" (
        if not "%%b"=="" (
            echo 设置: %%a=%%b
            set "%%a=%%b"
        )
    )
)

echo.
echo ========================================
echo   启动 Spring Boot 应用 [PROD 模式]
echo ========================================
echo.
echo [警告] 当前为生产模式配置!
echo.

mvn spring-boot:run -Dspring-boot.run.profiles=prod

echo.
echo ========================================
if errorlevel 1 (
    echo [错误] 应用异常退出!
) else (
    echo [信息] 应用已停止
)
echo ========================================
pause

.gitignore 配置

# 编译输出文件
*.class
*.jar
*.war
*.ear

# Maven 和 Gradle 的构建目录
/build/
/target/

# 忽略 IntelliJ IDEA 文件
/.idea/
/*.iml

# 忽略 VS Code 工作区文件
.vscode/

# 忽略 Eclipse 的项目文件
.project
.classpath
.settings/

# 忽略 MacOS 系统生成的文件
.DS_Store

# 忽略 Windows 系统生成的文件
Thumbs.db
ehthumbs.db
desktop.ini

# 忽略日志文件
*.log

# 忽略临时文件
*.tmp
*.swp

# 忽略 Java 的缓存目录
*.class
*.jar

# 忽略环境配置文件
*.env

# 忽略 Gradle 的缓存文件
.gradle/
/build/

# 忽略 Maven 的输出文件
/target/

# 忽略 IntelliJ IDEA 文件
.idea/
*.iws
*.iml

# 忽略 NetBeans 文件
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

# 忽略 JRebel 重载缓存
.rebel/

# 忽略 JetBrains IDE 编译生成文件
/out/

# 忽略构建脚本生成的二进制文件
*.exe
*.dll
*.so
*.out

# 忽略其他不需要提交的临时文件
*.pid
*.pid.lock


# 环境配置文件(包含敏感信息,不提交)
.env
.env.local
.env.dev
.env.prod
.env.prod.local
.env*.local

# 日志
logs/
*.log

总结

┌─────────────────────────────────────────────────────────────────┐
│                     方案优势                                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ✓ 敏感信息安全     .env 不提交 Git,各环境独立管理              │
│  ✓ 配置集中管理     修改只需改 .env,无需改代码                  │
│  ✓ 代码整洁         统一通过 AppProperties 访问配置              │
│  ✓ 类型安全         配置有类型、有默认值、有校验                  │
│  ✓ IDE 友好         有代码提示,重构方便                         │
│  ✓ 部署简单         source .env && java -jar app.jar            │
│  ✓ 环境隔离         dev/test/prod 配置完全独立                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

这套方案非常适合中小型项目,简单实用又不失规范!


项目分区导航:⬅️ 02-SpringBoot MVC 分层最佳实践 | 03-优雅配置管理方案 | ➡️ 04-多环境配置详解