李锋镝的博客

  • 首页
  • 时间轴
  • 评论区显眼包🔥
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
    • 赞助
  • 留言
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 后端
  3. 正文

解锁 Spring Boot 10 个高频 "神仙功能"

2025年10月15日 241点热度 0人点赞 0条评论

作为Java生态中最流行的开发框架,Spring Boot凭借"约定优于配置"的理念,让开发者摆脱了繁琐的XML配置,快速搭建生产级应用。但多数人日常仅使用其20%的基础功能(如自动配置、嵌入式容器),却忽略了那些能让开发效率翻倍、系统稳定性飙升的"隐藏神器"。本文将围绕Spring Boot的10个核心进阶功能,从原理剖析、实战代码到生产级最佳实践,带你全面掌握Spring Boot的"全栈能力",真正实现"写得少、跑得稳、排障快"。

一、@Conditional:精准控制Bean的加载逻辑

在多环境部署(开发/测试/生产)、多组件适配(MySQL/PostgreSQL、Redis/Caffeine)场景中,我们常需要根据特定条件加载不同Bean。@Profile虽能实现环境区分,但@Conditional通过自定义条件判断,提供了更灵活的Bean加载控制,是Spring Boot自动配置的核心基石。

1.1 内置条件注解:开箱即用的常见场景

Spring Boot内置了10+个@Conditional衍生注解,覆盖90%的常见场景,无需手动实现Condition接口:

注解 功能 适用场景
@ConditionalOnProperty 根据配置文件属性值判断 开关控制(如my.service.enabled=true才加载Bean)
@ConditionalOnBean 当容器中存在指定Bean时 依赖其他Bean的场景(如存在DataSource才加载JdbcTemplate)
@ConditionalOnMissingBean 当容器中不存在指定Bean时 自动配置的"兜底"逻辑(用户未自定义时用默认实现)
@ConditionalOnClass 当类路径中存在指定类时 依赖特定jar包的场景(如存在RedisTemplate才加载Redis缓存)
@ConditionalOnWebApplication 当应用是Web应用时 加载Web相关Bean(如DispatcherServlet、WebMvcConfigurer)
@ConditionalOnNotWebApplication 当应用是非Web应用时 加载非Web相关Bean(如定时任务、消息消费者)

实战示例:基于配置开关加载Bean
通过@ConditionalOnProperty实现"配置开关控制Bean加载",例如仅当app.redis.enabled=true时加载Redis缓存管理器:

@Configuration
public class CacheAutoConfig {

    // 仅当app.redis.enabled=true时加载Redis缓存管理器
    @Bean
    @ConditionalOnProperty(
        prefix = "app.redis", 
        name = "enabled", 
        havingValue = "true", 
        matchIfMissing = false // 配置缺失时不加载
    )
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)) // 默认过期时间30分钟
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }

    // 仅当app.redis.enabled=false(或配置缺失)时加载Caffeine本地缓存
    @Bean
    @ConditionalOnProperty(
        prefix = "app.redis", 
        name = "enabled", 
        havingValue = "false", 
        matchIfMissing = true // 配置缺失时默认加载
    )
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofMinutes(10)) // 本地缓存10分钟过期
                .maximumSize(10000) // 最大缓存条目数
        );
        return cacheManager;
    }
}

配置文件(application.yml):

app:
  redis:
    enabled: true # 开发环境可设为false,使用本地缓存;生产环境设为true,使用Redis

1.2 自定义条件注解:应对复杂业务场景

当内置注解无法满足需求(如基于数据库类型、第三方服务状态判断)时,可通过"自定义条件注解+Condition接口"实现灵活控制。

实战示例:基于数据库类型加载数据源
需求:根据配置的app.db.type(mysql/postgresql)加载对应的数据源Bean:

  1. 定义自定义条件注解@ConditionalOnDatabaseType:

    import org.springframework.context.annotation.Conditional;
    import java.lang.annotation.*;
    // 自定义条件注解:指定数据库类型才加载Bean
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(OnDatabaseTypeCondition.class) // 关联条件判断类
    public @interface ConditionalOnDatabaseType {
    String value(); // 接收数据库类型(如"mysql"、"postgresql")
    }
  2. 实现Condition接口,编写条件判断逻辑:

    import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.type.AnnotatedTypeMetadata;
    import java.util.Map;
    public class OnDatabaseTypeCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 1. 获取自定义注解的属性值(即@ConditionalOnDatabaseType指定的value)
        Map<String, Object> attributes = metadata.getAnnotationAttributes(
                ConditionalOnDatabaseType.class.getName()
        );
        String expectedDbType = (String) attributes.get("value");
    
        // 2. 从配置文件中获取实际的数据库类型
        String actualDbType = context.getEnvironment().getProperty("app.db.type");
    
        // 3. 匹配判断(忽略大小写,提高容错性)
        return expectedDbType.equalsIgnoreCase(actualDbType);
    }
    }
  3. 使用自定义注解加载不同数据源:

    @Configuration
    public class DataSourceAutoConfig {
    
    // 当数据库类型为mysql时加载MySQL数据源
    @Bean
    @ConditionalOnDatabaseType("mysql")
    @ConditionalOnMissingBean // 允许用户自定义数据源覆盖默认实现
    public DataSource mysqlDataSource(DataSourceProperties properties) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(properties.getUrl());
        dataSource.setUsername(properties.getUsername());
        dataSource.setPassword(properties.getPassword());
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }
    
    // 当数据库类型为postgresql时加载PostgreSQL数据源
    @Bean
    @ConditionalOnDatabaseType("postgresql")
    @ConditionalOnMissingBean
    public DataSource postgresDataSource(DataSourceProperties properties) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(properties.getUrl());
        dataSource.setUsername(properties.getUsername());
        dataSource.setPassword(properties.getPassword());
        dataSource.setDriverClassName("org.postgresql.Driver");
        return dataSource;
    }
    }

配置文件(application.yml):

app:
  db:
    type: mysql # 生产环境切换为postgresql即可加载对应数据源
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/testdb?serverTimezone=UTC
    username: root
    password: 123456

1.3 原理剖析:@Conditional如何工作?

Spring容器启动时,会对每个标注@Conditional(或其衍生注解)的Bean执行以下流程:

  1. 解析注解关联的Condition实现类;
  2. 调用Condition.matches()方法,传入ConditionContext(提供环境变量、Bean工厂、资源加载器等上下文)和AnnotatedTypeMetadata(提供注解属性信息);
  3. 若matches()返回true,则Bean会被注册到容器;返回false则跳过注册。

这一机制是Spring Boot"自动配置"的核心——例如RedisAutoConfiguration会通过@ConditionalOnClass(RedisOperations.class)判断类路径中是否有Redis依赖,再通过@ConditionalOnMissingBean提供默认的RedisTemplate。

二、@ConfigurationProperties:类型安全的配置管理

多数开发者初期会用@Value("${app.datasource.url}")逐个注入配置,但当配置项增多(如数据源、缓存、第三方API)时,@Value会导致代码冗余、类型转换繁琐、缺乏校验。@ConfigurationProperties通过"批量绑定+类型安全+数据校验",完美解决这些问题,是Spring Boot配置管理的最佳实践。

2.1 核心特性:不止于"绑定配置"

@ConfigurationProperties的强大之处在于其丰富的特性,覆盖配置管理的全场景:

  • 宽松绑定:支持kebab-case(配置文件)、camelCase(Java类)、snake_case、UPPER_SNAKE_CASE自动转换(如app.datasource.max-pool-size可绑定到maxPoolSize字段);
  • 类型安全:自动将配置值转换为Java类型(如Duration、Integer、List),无需手动转换;
  • 数据校验:结合@Validated和JSR-380注解(如@NotBlank、@Min、@Email),在绑定阶段校验配置合法性;
  • 嵌套配置:支持嵌套类绑定复杂配置(如app.datasource.pool.max-size绑定到pool.maxSize);
  • 集合绑定:支持List、Map类型配置(如app.allowed-ips: [192.168.1.1, 192.168.1.2])。

2.2 实战:完整的配置绑定流程

以"第三方支付接口配置"为例,展示@ConfigurationProperties的完整用法:

步骤1:定义配置属性类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.*;
import java.time.Duration;
import java.util.List;

// 1. prefix指定配置前缀(与application.yml中的层级对应)
// 2. @Validated启用数据校验
// 3. @Component将类注册为Bean(或在配置类中用@EnableConfigurationProperties启用)
@Component
@ConfigurationProperties(prefix = "app.payment")
@Validated
@Data
public class PaymentProperties {

    // 支付接口基础URL(非空校验)
    @NotBlank(message = "支付接口URL不能为空")
    private String baseUrl;

    // 超时时间(默认30秒,最小值5秒)
    @Min(value = 5, message = "超时时间不能小于5秒")
    private Duration timeout = Duration.ofSeconds(30);

    // 应用ID(非空,长度10-20)
    @NotBlank(message = "应用ID不能为空")
    @Size(min = 10, max = 20, message = "应用ID长度必须为10-20位")
    private String appId;

    // 应用密钥(非空,必须包含大小写字母和数字)
    @NotBlank(message = "应用密钥不能为空")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$", message = "密钥必须包含大小写字母和数字")
    private String appSecret;

    // 支持的支付方式(非空,至少包含一种)
    @NotEmpty(message = "至少需要支持一种支付方式")
    private List<String> supportedMethods;

    // 嵌套配置:退款相关配置
    @Valid // 启用嵌套对象的校验
    private RefundConfig refund = new RefundConfig();

    // 嵌套配置类
    @Data
    public static class RefundConfig {
        // 退款申请有效期(默认7天,最大值30天)
        @Max(value = 30, message = "退款有效期不能超过30天")
        private int validDays = 7;

        // 是否允许部分退款(默认true)
        private boolean allowPartial = true;
    }
}

步骤2:编写配置文件(application.yml)

app:
  payment:
    base-url: https://api.payment.com/v1 # kebab-case对应Java的baseUrl
    timeout: 60s # 自动转换为Duration类型
    app-id: "TEST_APP_1234567890" # 满足10-20位要求
    app-secret: "Payment@123" # 包含大小写和数字
    supported-methods: [ "alipay", "wechat", "unionpay" ] # List类型绑定
    refund:
      valid-days: 15 # 嵌套配置,对应refund.validDays
      allow-partial: false # 覆盖默认值

步骤3:启用配置绑定(二选一)

  • 方式1:在配置属性类上添加@Component(如上例),直接注册为Bean;
  • 方式2:在配置类中用@EnableConfigurationProperties启用(更灵活,适合第三方组件):
    @Configuration
    // 启用PaymentProperties的配置绑定(无需在PaymentProperties上加@Component)
    @EnableConfigurationProperties(PaymentProperties.class)
    public class PaymentAutoConfig {
    // ... 后续可注入PaymentProperties使用
    }

步骤4:使用配置属性

在服务类中注入PaymentProperties,直接使用绑定后的配置:

@Service
public class PaymentService {

    private final PaymentProperties properties;
    private final RestTemplate restTemplate;

    // 构造器注入(推荐,避免字段注入的循环依赖风险)
    @Autowired
    public PaymentService(PaymentProperties properties, RestTemplate restTemplate) {
        this.properties = properties;
        this.restTemplate = restTemplate;
    }

    // 调用支付接口
    public PaymentResponse createOrder(PaymentRequest request) {
        // 1. 构建请求URL(使用绑定的baseUrl)
        String url = properties.getBaseUrl() + "/orders";

        // 2. 配置请求超时(使用绑定的timeout)
        RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
        RestTemplate template = restTemplateBuilder
                .setConnectTimeout(properties.getTimeout())
                .setReadTimeout(properties.getTimeout())
                .build();

        // 3. 设置请求头(使用绑定的appId和appSecret)
        HttpHeaders headers = new HttpHeaders();
        headers.set("X-App-Id", properties.getAppId());
        headers.set("X-App-Secret", properties.getAppSecret());
        HttpEntity<PaymentRequest> requestEntity = new HttpEntity<>(request, headers);

        // 4. 发送请求
        return template.postForObject(url, requestEntity, PaymentResponse.class);
    }
}

2.3 @ConfigurationProperties vs @Value:怎么选?

很多开发者纠结两者的区别,其实核心是"场景匹配":

对比维度 @ConfigurationProperties @Value
配置批量绑定 支持(适合多配置项场景) 不支持(需逐个注入)
类型转换 自动支持(Duration、List等) 需手动转换(如@Value("${app.timeout:30}") int timeout)
数据校验 支持(结合@Validated) 不支持(需手动校验)
宽松绑定 支持 不支持(需严格匹配名称)
适用场景 多配置项(数据源、第三方API) 单个简单配置(如app.name)

结论:

  • 当配置项超过3个时,优先用@ConfigurationProperties;
  • 仅需注入1-2个简单配置(如server.port)时,可用@Value。

三、Spring Boot Actuator:生产级应用可观测性

生产环境中,"应用是否存活"、"接口响应时间"、"JVM内存使用"等监控需求是系统稳定性的核心。Spring Boot Actuator提供了开箱即用的监控端点,无需手动开发,即可快速构建应用的可观测性体系。

3.1 核心端点:监控能力全覆盖

Actuator默认提供13个端点,涵盖健康检查、指标收集、配置查看等核心场景,常用端点如下:

端点路径 功能 安全建议
/actuator/health 应用健康状态(UP/DOWN/UNKNOWN) 公开(用于运维监控存活)
/actuator/metrics 应用指标(JVM、CPU、接口耗时等) 内部访问(避免敏感指标泄露)
/actuator/prometheus Prometheus格式指标(用于Grafana展示) 内部访问
/actuator/info 应用自定义信息(版本、构建时间等) 公开
/actuator/env 环境变量与配置属性 严格保密(含数据库密码等敏感信息)
/actuator/loggers 动态调整日志级别 内部访问

基础配置:暴露端点

Actuator默认仅暴露/actuator/health和/actuator/info,需在application.yml中配置暴露更多端点:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus # 暴露指定端点
        exclude: env,loggers # 排除敏感端点
  endpoint:
    health:
      show-details: always # 显示健康检查详情(如数据库、Redis的状态)
      show-components: always # 显示组件健康状态
    metrics:
      enabled: true # 启用指标收集
  metrics:
    tags:
      application: ${spring.application.name} # 为指标添加应用标签(方便多应用监控)

3.2 实战1:自定义健康检查

默认的/actuator/health仅返回应用存活状态,实际生产中需监控依赖组件(如数据库、Redis、消息队列)的健康。通过实现HealthIndicator接口,可自定义健康检查逻辑。

示例:数据库健康检查(扩展默认健康检查)

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;

@Component
public class DatabaseHealthIndicator implements HealthIndicator {

    private final DataSource dataSource;

    @Autowired
    public DatabaseHealthIndicator(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Health health() {
        try (Connection connection = dataSource.getConnection()) {
            // 1. 校验数据库连接是否有效(超时1秒)
            if (connection.isValid(1000)) {
                // 2. 收集额外详情(如数据库版本、驱动版本)
                String dbProduct = connection.getMetaData().getDatabaseProductName();
                String dbVersion = connection.getMetaData().getDatabaseProductVersion();
                String driverVersion = connection.getMetaData().getDriverVersion();

                return Health.up() // 健康状态为UP
                        .withDetail("database", dbProduct)
                        .withDetail("version", dbVersion)
                        .withDetail("driver-version", driverVersion)
                        .withDetail("validation-result", "连接有效")
                        .build();
            } else {
                return Health.down() // 健康状态为DOWN
                        .withDetail("database", "未知")
                        .withDetail("validation-result", "连接无效")
                        .build();
            }
        } catch (Exception e) {
            // 3. 捕获异常,返回DOWN状态并携带错误信息
            return Health.down(e)
                    .withDetail("database", "连接失败")
                    .withDetail("error-message", e.getMessage())
                    .build();
        }
    }
}

访问http://localhost:8080/actuator/health,返回结果如下(包含自定义详情):

{
  "status": "UP",
  "components": {
    "database": {
      "status": "UP",
      "details": {
        "database": "MySQL",
        "version": "8.0.33",
        "driver-version": "8.0.33",
        "validation-result": "连接有效"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 107374182400,
        "free": 53687091200,
        "threshold": 10485760
      }
    }
  }
}

3.3 实战2:自定义业务指标

除了JVM、CPU等默认指标,Actuator支持通过MeterRegistry自定义业务指标(如订单量、支付成功率),方便后续通过Prometheus+Grafana展示。

示例:订单业务指标监控

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;

@Component
public class OrderMetrics {

    // 1. 订单总数量计数器(记录订单创建次数)
    private final Counter orderCreateCounter;
    // 2. 订单金额分布统计(记录订单金额的平均值、最大值、分位数)
    private final DistributionSummary orderAmountSummary;
    // 3. 订单支付成功率计数器(区分成功/失败状态)
    private final Counter orderPaySuccessCounter;
    private final Counter orderPayFailCounter;

    // 构造器注入MeterRegistry(Actuator自动配置)
    public OrderMetrics(MeterRegistry registry) {
        // 初始化订单创建计数器
        this.orderCreateCounter = Counter.builder("order.create.count")
                .description("订单创建总次数")
                .tag("app", "order-service") // 添加标签,方便多维度筛选
                .register(registry);

        // 初始化订单金额统计(单位:元)
        this.orderAmountSummary = DistributionSummary.builder("order.amount.summary")
                .description("订单金额分布统计")
                .baseUnit("元")
                .tag("app", "order-service")
                .register(registry);

        // 初始化支付成功率计数器
        this.orderPaySuccessCounter = Counter.builder("order.pay.success.count")
                .description("订单支付成功次数")
                .tag("app", "order-service")
                .register(registry);

        this.orderPayFailCounter = Counter.builder("order.pay.fail.count")
                .description("订单支付失败次数")
                .tag("app", "order-service")
                .register(registry);
    }

    // 记录订单创建(调用此方法即可更新指标)
    public void recordOrderCreate(BigDecimal amount) {
        orderCreateCounter.increment(); // 计数器+1
        orderAmountSummary.record(amount.doubleValue()); // 记录订单金额
    }

    // 记录支付结果
    public void recordPayResult(boolean success) {
        if (success) {
            orderPaySuccessCounter.increment();
        } else {
            orderPayFailCounter.increment();
        }
    }
}

在订单服务中调用指标记录方法:

@Service
public class OrderService {

    private final OrderMetrics orderMetrics;
    private final OrderRepository orderRepository;

    @Autowired
    public OrderService(OrderMetrics orderMetrics, OrderRepository orderRepository) {
        this.orderMetrics = orderMetrics;
        this.orderRepository = orderRepository;
    }

    public Order createOrder(OrderCreateDTO dto) {
        // 1. 业务逻辑:创建订单
        Order order = new Order();
        order.setUserId(dto.getUserId());
        order.setAmount(dto.getAmount());
        order.setStatus(OrderStatus.CREATED);
        Order savedOrder = orderRepository.save(order);

        // 2. 记录指标
        orderMetrics.recordOrderCreate(dto.getAmount());

        return savedOrder;
    }

    public void payOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new RuntimeException("订单不存在"));

        try {
            // 调用支付接口(省略逻辑)
            boolean paySuccess = true; // 实际根据支付结果判断
            if (paySuccess) {
                order.setStatus(OrderStatus.PAID);
                orderMetrics.recordPayResult(true);
            } else {
                order.setStatus(OrderStatus.PAY_FAILED);
                orderMetrics.recordPayResult(false);
            }
            orderRepository.save(order);
        } catch (Exception e) {
            order.setStatus(OrderStatus.PAY_FAILED);
            orderRepository.save(order);
            orderMetrics.recordPayResult(false);
            throw e;
        }
    }
}

访问http://localhost:8080/actuator/metrics/order.create.count,可查看订单创建次数:

{
  "name": "order.create.count",
  "description": "订单创建总次数",
  "baseUnit": null,
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 100.0 // 已创建100个订单
    }
  ],
  "availableTags": [
    {
      "tag": "app",
      "values": [
        "order-service"
      ]
    }
  ]
}

3.4 安全加固:保护敏感端点

Actuator的/actuator/env、/actuator/configprops等端点包含敏感信息(如数据库密码、API密钥),必须通过Spring Security进行访问控制。

示例:Actuator端点安全配置

  1. 引入Spring Security依赖(pom.xml):

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  2. 编写安全配置类:

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    import org.springframework.security.web.SecurityFilterChain;
    @Configuration
    @EnableWebSecurity
    public class ActuatorSecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 1. 配置Actuator端点的访问权限
                .authorizeHttpRequests(auth -> auth
                        // 公开健康检查和info端点(无需登录)
                        .requestMatchers("/actuator/health/**", "/actuator/info/**").permitAll()
                        // 其他Actuator端点需ADMIN角色
                        .requestMatchers("/actuator/**").hasRole("ADMIN")
                        // 其他业务接口按实际需求配置
                        .anyRequest().authenticated()
                )
                // 2. 使用HTTP Basic认证(简单,适合内部监控)
                .httpBasic();
    
        // 关闭CSRF(监控端点通常通过脚本访问,无需CSRF保护)
        http.csrf(csrf -> csrf.disable());
    
        return http.build();
    }
    
    // 3. 配置内存用户(生产环境建议用数据库存储用户)
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails adminUser = User.withUsername("actuator-admin")
                .password("{noop}admin@123") // {noop}表示不加密(生产环境需用BCrypt加密)
                .roles("ADMIN")
                .build();
    
        return new InMemoryUserDetailsManager(adminUser);
    }
    }

生产环境优化:

  • 密码加密:使用BCryptPasswordEncoder加密密码,而非noop;
  • 用户存储:将用户信息存入数据库(结合JdbcUserDetailsManager);
  • IP限制:仅允许运维网段访问Actuator端点(通过Nginx或Spring Security的hasIpAddress配置)。

四、Spring Boot DevTools:极致的开发体验

日常开发中,"修改代码→重启应用→等待生效"的流程会浪费大量时间。Spring Boot DevTools通过"热重启"和"LiveReload"功能,将代码变更生效时间从分钟级缩短到秒级,大幅提升开发效率。

4.1 核心功能:开发效率加速器

DevTools的核心功能围绕"减少等待时间"设计:

  • 自动重启:监测类路径变化(如Java文件、配置文件修改),自动重启Spring容器(通过两个类加载器实现:基础类加载器加载第三方jar,重启类加载器加载项目代码,重启时仅重建后者,速度更快);
  • LiveReload:集成LiveReload服务器,修改静态资源(HTML、CSS、JS)后,自动刷新浏览器(需安装浏览器插件);
  • 配置覆盖:开发环境自动覆盖生产配置(如禁用缓存、启用调试日志);
  • 全局配置:~/.spring-boot-devtools.properties可配置所有项目的DevTools参数(如统一的重启排除路径)。

4.2 实战配置:打造高效开发环境

步骤1:引入DevTools依赖(pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope> <!-- 仅运行时生效,生产环境不打包 -->
    <optional>true</optional> <!-- 避免传递依赖,其他模块需显式引入 -->
</dependency>

步骤2:配置DevTools(application-dev.yml)

spring:
  profiles:
    active: dev # 激活开发环境
  devtools:
    restart:
      enabled: true # 启用自动重启
      exclude: static/**,public/**,templates/** # 排除静态资源(修改后无需重启)
      additional-paths: src/main/java,src/main/resources/application-dev.yml # 额外监测路径
      poll-interval: 1000ms # 监测文件变化的间隔(默认1秒)
      quiet-period: 400ms # 文件变化后等待多久重启(避免频繁重启)
    livereload:
      enabled: true # 启用LiveReload(默认端口35729)
    remote:
      enabled: true # 启用远程调试(可选,适合开发服务器调试)
      secret: devtools-secret # 远程调试密钥(防止未授权访问)
  # 禁用模板引擎缓存(Thymeleaf/Freemarker)
  thymeleaf:
    cache: false
  freemarker:
    cache: false
  # 禁用Spring MVC静态资源缓存
  web:
    resources:
      cache:
        period: 0

步骤3:IDE配置(以IntelliJ IDEA为例)

DevTools默认监测"编译后的class文件"变化,需确保IDEA自动编译:

  1. 打开File → Settings → Build, Execution, Deployment → Compiler,勾选Build project automatically;
  2. 按Ctrl+Shift+Alt+/,选择Registry,勾选compiler.automake.allow.when.app.running(允许应用运行时自动编译);
  3. 重启IDEA生效。

4.3 进阶:自定义重启触发器与排除规则

场景1:自定义重启触发文件

默认情况下,修改Java文件会触发重启,但有时需要"手动控制重启"(如修改多个文件后统一重启)。可通过"触发文件"实现:

spring:
  devtools:
    restart:
      trigger-file: .reload-trigger # 仅当此文件修改时才触发重启

开发时,修改完所有文件后,触摸(修改).reload-trigger文件即可触发重启。

场景2:排除依赖包的重启

若项目依赖的第三方jar包频繁更新(如自定义Starter),会导致不必要的重启,可排除这些jar:

spring:
  devtools:
    restart:
      additional-exclude: libs/my-starter-*.jar # 排除自定义Starter的jar

4.4 注意事项:避免生产环境泄露

DevTools仅用于开发环境,需确保生产环境不打包此依赖:

  1. Maven配置(pom.xml):

    <profiles>
    <profile>
        <id>prod</id>
        <activation>
            <activeByDefault>false</activeByDefault>
        </activation>
        <dependencies>
            <!-- 生产环境排除DevTools -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </profile>
    </profiles>
  2. Gradle配置(build.gradle):

    configurations {
    developmentOnly
    runtimeClasspath {
        extendsFrom developmentOnly
    }
    }
    dependencies {
    developmentOnly "org.springframework.boot:spring-boot-devtools"
    }
    // 生产环境排除DevTools
    bootJar {
    exclude "META-INF/spring-boot-devtools.properties"
    }

五、Spring Retry:分布式系统的容错利器

分布式系统中,网络抖动、服务短暂不可用、数据库连接超时等"暂时性故障"频繁发生。Spring Retry提供声明式的重试机制,无需手动编写for循环重试逻辑,即可优雅处理这些故障,提升系统容错能力。

5.1 核心概念:重试的三大要素

Spring Retry的核心是"重试策略+退避策略+恢复逻辑":

  • 重试策略:决定哪些异常、哪些返回值需要重试(如仅重试NetworkException,不重试BusinessException);
  • 退避策略:重试之间的等待时间(如固定等待1秒、指数级等待1秒→2秒→4秒);
  • 恢复逻辑:重试多次失败后执行的兜底逻辑(如返回默认值、触发告警)。

5.2 实战1:基础重试配置(注解方式)

步骤1:引入依赖(pom.xml)

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId> <!-- 注解方式依赖AOP -->
</dependency>

步骤2:启用重试(配置类)

@Configuration
@EnableRetry // 启用Spring Retry(基于AOP实现)
public class RetryConfig {
}

步骤3:声明式重试(服务类)

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class PaymentGatewayService {

    // 1. 重试配置:仅重试NetworkException和TimeoutException,最多重试3次
    @Retryable(
        value = {NetworkException.class, TimeoutException.class}, // 需要重试的异常类型
        maxAttempts = 3, // 最大重试次数(包含首次调用,共3次:1次原始+2次重试)
        backoff = @Backoff(
            delay = 1000, // 初始延迟时间(毫秒)
            multiplier = 2, // 延迟倍数(1秒→2秒→4秒)
            maxDelay = 10000 // 最大延迟时间(避免无限增长)
        ),
        listeners = {"retryLoggingListener"} // 重试监听器(记录日志)
    )
    public PaymentResponse callPaymentGateway(PaymentRequest request) {
        // 调用第三方支付网关(可能抛出NetworkException或TimeoutException)
        System.out.println("调用支付网关,当前时间:" + System.currentTimeMillis());
        if (Math.random() > 0.3) { // 模拟70%概率失败
            throw new NetworkException("支付网关网络抖动");
        }
        return new PaymentResponse(true, "支付成功", "PAY" + System.currentTimeMillis());
    }

    // 2. 恢复逻辑:仅当@Retryable指定的异常发生且重试耗尽时执行
    @Recover
    public PaymentResponse recover(NetworkException e, PaymentRequest request) {
        System.out.println("支付网关调用失败,进入恢复逻辑:" + e.getMessage());
        // 兜底逻辑:返回失败结果,并触发告警
        sendAlarm("支付网关调用失败:" + e.getMessage());
        return new PaymentResponse(false, "支付暂时不可用,请稍后重试", null);
    }

    // 3. 恢复逻辑:针对TimeoutException的单独处理(可选,若未定义则使用上面的通用恢复)
    @Recover
    public PaymentResponse recover(TimeoutException e, PaymentRequest request) {
        System.out.println("支付网关超时,进入恢复逻辑:" + e.getMessage());
        sendAlarm("支付网关超时:" + e.getMessage());
        return new PaymentResponse(false, "支付超时,请稍后重试", null);
    }

    // 模拟发送告警
    private void sendAlarm(String message) {
        System.out.println("发送告警:" + message);
    }
}

关键说明:

  • maxAttempts=3表示"首次调用+2次重试",共3次尝试;
  • @Backoff(multiplier=2)表示退避时间按指数增长(1秒→2秒→4秒),避免频繁重试给第三方服务造成压力;
  • @Recover方法的参数必须与@Retryable方法一致,且首个参数为重试的异常类型;
  • 若未定义@Recover方法,重试耗尽后会直接抛出原始异常。

5.3 实战2:高级重试策略(编程式+熔断器)

对于复杂场景(如结合熔断器、动态调整重试次数),可通过RetryTemplate编程式配置,并集成Resilience4j实现熔断功能(Spring Retry本身不支持熔断,需结合第三方组件)。

步骤1:引入Resilience4j依赖(pom.xml)

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId> <!-- Spring Boot 3.x -->
</dependency>

步骤2:编程式重试+熔断器配置

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;

@Service
public class AdvancedPaymentService {

    private final RetryTemplate retryTemplate;
    private final CircuitBreaker circuitBreaker;

    // 构造器注入RetryTemplate和CircuitBreaker
    public AdvancedPaymentService(CircuitBreakerRegistry circuitBreakerRegistry) {
        // 1. 配置重试模板
        this.retryTemplate = RetryTemplate.builder()
                .maxAttempts(3) // 最大重试3次
                .exponentialBackoff(1000, 2, 10000) // 指数退避
                .retryOn(NetworkException.class, TimeoutException.class) // 重试异常
                .traversingCauses() // 重试嵌套异常(如Exception包含NetworkException)
                .withListener(new RetryLoggingListener()) // 重试监听
                .build();

        // 2. 配置熔断器(Resilience4j)
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(50) // 失败率超过50%时打开熔断器
                .slidingWindowSize(10) // 滑动窗口大小(10个请求)
                .minimumNumberOfCalls(5) // 至少5个请求才计算失败率
                .waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断器打开30秒后进入半开状态
                .permittedNumberOfCallsInHalfOpenState(3) // 半开状态允许3个请求测试
                .build();
        this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentService", circuitBreakerConfig);
    }

    // 编程式调用:重试+熔断器
    public PaymentResponse processPayment(PaymentRequest request) {
        // 1. 检查熔断器状态:若打开,直接返回失败
        if (circuitBreaker.getState() == CircuitBreaker.State.OPEN) {
            System.out.println("熔断器已打开,拒绝调用支付网关");
            sendAlarm("支付网关熔断器已打开,暂时不可用");
            return new PaymentResponse(false, "系统繁忙,请稍后重试", null);
        }

        // 2. 执行重试逻辑,并记录熔断器状态
        try {
            return circuitBreaker.executeSupplier(() -> 
                retryTemplate.execute((RetryCallback<PaymentResponse, Exception>) context -> {
                    // 重试回调:调用支付网关
                    PaymentResponse response = callPaymentGateway(request);
                    // 调用成功,记录熔断器成功事件
                    circuitBreaker.onSuccess();
                    return response;
                }, context -> {
                    // 恢复回调:重试失败后执行
                    Exception e = (Exception) context.getLastThrowable();
                    System.out.println("重试失败,进入恢复逻辑:" + e.getMessage());
                    // 调用失败,记录熔断器失败事件
                    circuitBreaker.onError(0, e);
                    sendAlarm("支付网关调用失败:" + e.getMessage());
                    return new PaymentResponse(false, "支付暂时不可用,请稍后重试", null);
                })
            );
        } catch (Exception e) {
            circuitBreaker.onError(0, e);
            sendAlarm("支付处理异常:" + e.getMessage());
            return new PaymentResponse(false, "系统异常,请稍后重试", null);
        }
    }

    // 模拟调用支付网关
    private PaymentResponse callPaymentGateway(PaymentRequest request) {
        System.out.println("调用支付网关,当前时间:" + System.currentTimeMillis());
        if (Math.random() > 0.3) {
            throw new NetworkException("支付网关网络抖动");
        }
        return new PaymentResponse(true, "支付成功", "PAY" + System.currentTimeMillis());
    }

    private void sendAlarm(String message) {
        System.out.println("发送告警:" + message);
    }

    // 重试监听器:记录重试日志
    private static class RetryLoggingListener implements RetryListener {
        @Override
        public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
            System.out.println("重试开始,当前重试上下文:" + context);
            return true; // 返回true表示允许重试
        }

        @Override
        public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
            System.out.println("重试失败,次数:" + context.getRetryCount() + ",异常:" + throwable.getMessage());
        }

        @Override
        public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
            if (throwable == null) {
                System.out.println("重试成功,总次数:" + context.getRetryCount());
            } else {
                System.out.println("重试耗尽,总次数:" + context.getRetryCount() + ",最终异常:" + throwable.getMessage());
            }
        }
    }
}

5.4 Spring Retry vs Resilience4j:怎么选?

Resilience4j是新一代容错框架,支持熔断、限流、重试、舱壁等更多功能,且轻量级(无依赖),逐渐替代Spring Retry成为主流。两者对比:

功能 Spring Retry Resilience4j
重试 支持(注解+编程式) 支持(更灵活的配置)
熔断 不支持(需集成第三方) 原生支持
限流 不支持 原生支持
舱壁 不支持 原生支持
依赖 依赖Spring AOP、AspectJ 无依赖(轻量级)
适用场景 简单重试需求 复杂容错需求(熔断+限流+重试)

结论:

  • 简单项目(仅需重试):用Spring Retry(注解方式简单);
  • 分布式系统(需熔断、限流):用Resilience4j(功能更全面)。

六、Spring Cache:统一缓存抽象

缓存是提升系统性能的关键手段,但手动管理缓存(如Redis)会导致代码冗余、耦合度高。Spring Cache提供了统一的缓存抽象层,支持Redis、Caffeine、Ehcache等多种缓存实现,通过注解即可完成缓存的增删改查,无需关注具体缓存技术细节。

6.1 核心注解:缓存操作全覆盖

Spring Cache通过5个核心注解实现缓存管理,覆盖90%的缓存场景:

注解 功能 常用属性
@Cacheable 查询缓存:存在则返回缓存,不存在则执行方法并缓存结果 value(缓存名称)、key(缓存键)、condition(缓存条件)、unless(不缓存条件)
@CachePut 更新缓存:执行方法后,将结果更新到缓存(不查询缓存) value、key(与查询缓存的key一致)
@CacheEvict 删除缓存:执行方法后,删除指定缓存 value、key、allEntries(删除所有缓存条目)
@Caching 组合缓存操作:同时执行多个缓存操作(如删除多个缓存) cacheable、put、evict(数组形式)
@CacheConfig 类级缓存配置:统一指定value(缓存名称)、cacheManager等 cacheNames(默认缓存名称)、cacheManager(默认缓存管理器)

6.2 实战1:多缓存管理器配置(Redis+Caffeine)

生产环境中,常结合"本地缓存(Caffeine)+分布式缓存(Redis)":本地缓存提升热点数据访问速度,分布式缓存保证多实例数据一致性。

步骤1:引入依赖(pom.xml)

<!-- Spring Cache核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis缓存依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Caffeine本地缓存依赖 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

步骤2:配置多缓存管理器

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching // 启用Spring Cache
public class CacheConfig {

    // 1. 配置Redis缓存管理器(默认缓存管理器,用于分布式缓存)
    @Bean
    @Primary
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 全局默认缓存配置(过期时间30分钟)
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                // 键序列化:StringRedisSerializer(避免键乱码)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 值序列化:GenericJackson2JsonRedisSerializer(支持对象序列化,保留类型信息)
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues(); // 不缓存null值(避免缓存穿透)

        // 自定义缓存配置(针对不同缓存名称设置不同过期时间)
        Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
        // "users"缓存:过期时间1小时
        cacheConfigs.put("users", defaultConfig.entryTtl(Duration.ofHours(1)));
        // "orders"缓存:过期时间15分钟
        cacheConfigs.put("orders", defaultConfig.entryTtl(Duration.ofMinutes(15)));

        // 构建Redis缓存管理器
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultConfig)
                .withInitialCacheConfigurations(cacheConfigs)
                .transactionAware() // 支持事务(事务提交后才更新缓存)
                .build();
    }

    // 2. 配置Caffeine缓存管理器(用于本地缓存,如热点数据)
    @Bean
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // 配置Caffeine:10分钟过期,最大缓存10000条数据
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofMinutes(10))
                .maximumSize(10000)
                .recordStats() // 记录缓存统计信息(命中率、加载时间等)
        );
        // 指定需要本地缓存的名称(如"hot-products")
        cacheManager.setCacheNames(java.util.Arrays.asList("hot-products"));
        return cacheManager;
    }
}

步骤3:使用缓存注解(服务类)

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import java.util.Optional;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    /**
     * 1. 查询缓存:根据ID查询用户
     * - 缓存名称:users(使用Redis缓存,过期1小时)
     * - 缓存键:#id(方法参数id)
     * - unless:结果为null时不缓存(避免缓存穿透)
     */
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        System.out.println("从数据库查询用户:" + id); // 仅首次调用或缓存过期时打印
        Optional<User> user = userRepository.findById(id);
        return user.orElse(null);
    }

    /**
     * 2. 查询缓存:根据用户名查询用户(使用本地缓存Caffeine)
     * - cacheManager:指定使用caffeineCacheManager
     * - sync:开启同步(多线程并发查询时,仅一个线程查询数据库,其他线程等待缓存)
     */
    @Cacheable(value = "hot-products", key = "#username", cacheManager = "caffeineCacheManager", sync = true)
    public User getUserByUsername(String username) {
        System.out.println("从数据库查询用户(用户名):" + username);
        return userRepository.findByUsername(username);
    }

    /**
     * 3. 更新缓存:更新用户信息后,删除对应缓存(避免缓存脏数据)
     * - @CacheEvict:删除缓存键为#user.id的缓存条目
     */
    @CacheEvict(value = "users", key = "#user.id")
    public User updateUser(User user) {
        User updatedUser = userRepository.save(user);
        return updatedUser;
    }

    /**
     * 4. 组合缓存操作:删除用户时,同时删除ID和用户名对应的缓存
     * - @Caching:组合多个@CacheEvict操作
     */
    @Caching(evict = {
            @CacheEvict(value = "users", key = "#user.id"), // 删除ID对应的缓存
            @CacheEvict(value = "hot-products", key = "#user.username") // 删除用户名对应的本地缓存
    })
    public void deleteUser(User user) {
        userRepository.delete(user);
    }
}

6.3 实战2:解决缓存三大问题(穿透、击穿、雪崩)

缓存使用中,常面临三大问题:缓存穿透、缓存击穿、缓存雪崩,需通过Spring Cache的高级配置解决。

(1)缓存穿透:查询不存在的数据,导致请求直达数据库

原因:如查询id=-1的用户(不存在),缓存不命中,每次请求都查询数据库,可能导致数据库压力过大。
解决方案:

  • 方案1:@Cacheable(disableCachingNullValues = true)(不缓存null值,默认已配置);
  • 方案2:缓存空值(适用于数据可能后续存在的场景),结合短期过期时间:
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
    User user = userRepository.findById(id).orElse(null);
    // 若用户不存在,缓存一个空对象(避免缓存穿透)
    if (user == null) {
        return new User(); // 空对象(需在业务层判断是否为有效用户)
    }
    return user;
    }

(2)缓存击穿:热点数据缓存过期,大量请求直达数据库

原因:如热门商品缓存过期,瞬间大量请求查询该商品,导致数据库压力骤增。
解决方案:

  • 方案1:@Cacheable(sync = true)(并发查询时,仅一个线程查询数据库,其他线程等待缓存);
  • 方案2:热点数据永不过期(定期后台更新缓存)。
// 开启同步,避免缓存击穿
@Cacheable(value = "hot-products", key = "#productId", sync = true)
public Product getHotProduct(Long productId) {
    return productRepository.findById(productId).orElse(null);
}

(3)缓存雪崩:大量缓存同时过期,导致请求直达数据库

原因:如缓存设置统一过期时间(如凌晨3点),到期后大量缓存失效,请求全部打向数据库。
解决方案:

  • 方案1:设置随机过期时间(避免缓存同时过期);
  • 方案2:分层缓存(本地缓存+分布式缓存),本地缓存过期时间比分布式缓存长。
// Redis缓存配置:为不同缓存名称设置不同过期时间(避免雪崩)
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("users", defaultConfig.entryTtl(Duration.ofHours(1) + Duration.ofMinutes((long) (Math.random() * 10)))); // 1小时±10分钟
cacheConfigs.put("orders", defaultConfig.entryTtl(Duration.ofMinutes(15) + Duration.ofMinutes((long) (Math.random() * 5)))); // 15分钟±5分钟

七、Spring Boot Test:全方位测试支持

测试是保证代码质量的关键,但手动测试效率低、覆盖度差。Spring Boot Test提供了分层测试方案,从单元测试到集成测试,再到端到端测试,全方位保障代码正确性。

7.1 分层测试策略:效率与覆盖度的平衡

Spring Boot Test将测试分为三层,每层对应不同的测试目标和范围:

测试层级 目标 核心注解 启动速度 适用场景
单元测试 测试单个类/方法,隔离外部依赖 @ExtendWith(MockitoExtension.class) 毫秒级 Service、Util类测试
切片测试 测试某一层(如DAO、Controller),仅启动部分Spring容器 @DataJpaTest、@WebMvcTest 秒级 Repository、Controller测试
集成测试 测试整个应用流程,启动完整Spring容器 @SpringBootTest 分钟级 端到端业务流程测试

7.2 实战1:单元测试(Service层)

单元测试专注于测试单个Service方法,通过Mockito模拟依赖(如Repository),无需启动Spring容器,速度最快。

示例:UserService单元测试

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

// 启用Mockito扩展(用于模拟依赖)
@ExtendWith(MockitoExtension.class)
class UserServiceUnitTest {

    // 1. 模拟依赖:UserRepository(无需真实实现)
    @Mock
    private UserRepository userRepository;

    // 2. 注入被测试对象:UserService(将@Mock模拟的Repository注入)
    @InjectMocks
    private UserService userService;

    /**
     * 测试场景:用户存在时,getUserById应返回正确用户
     */
    @Test
    void getUserById_WhenUserExists_ReturnsUser() {
        // Given:准备测试数据
        Long userId = 1L;
        User expectedUser = new User(userId, "john", "john@example.com");
        // 模拟Repository的findById方法,当传入userId=1L时返回expectedUser
        when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));

        // When:执行被测试方法
        User actualUser = userService.getUserById(userId);

        // Then:验证结果
        assertNotNull(actualUser); // 结果不为null
        assertEquals(expectedUser.getId(), actualUser.getId()); // ID匹配
        assertEquals(expectedUser.getUsername(), actualUser.getUsername()); // 用户名匹配
        // 验证Repository的findById方法被调用了一次,且参数为userId=1L
        verify(userRepository, times(1)).findById(userId);
    }

    /**
     * 测试场景:用户不存在时,getUserById应返回null
     */
    @Test
    void getUserById_WhenUserDoesNotExist_ReturnsNull() {
        // Given:准备测试数据
        Long userId = 99L;
        // 模拟Repository的findById方法,当传入userId=99L时返回空
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // When:执行被测试方法
        User actualUser = userService.getUserById(userId);

        // Then:验证结果
        assertNull(actualUser); // 结果为null
        verify(userRepository, times(1)).findById(userId); // 验证方法调用
    }
}

关键注解说明:

  • @Mock:模拟依赖对象(如Repository),无需真实实现;
  • @InjectMocks:将@Mock模拟的对象注入到被测试对象中;
  • when(...).thenReturn(...):定义模拟方法的返回值;
  • verify(...):验证模拟方法的调用次数和参数。

7.3 实战2:切片测试(Repository层+Controller层)

切片测试仅启动Spring容器的一部分(如DAO层、Web层),既保证测试的真实性,又比集成测试更快。

(1)Repository层测试(@DataJpaTest)

@DataJpaTest仅启动JPA相关的Bean(如EntityManager、Repository),并默认使用内存数据库(H2),适合测试Repository方法。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import static org.junit.jupiter.api.Assertions.*;

// 启用JPA切片测试
@DataJpaTest
// 禁用默认的内存数据库替换(使用配置的测试数据库,如MySQL测试库)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {

    // 注入真实的UserRepository(由Spring容器管理)
    @Autowired
    private UserRepository userRepository;

    // 注入TestEntityManager(用于测试JPA操作)
    @Autowired
    private TestEntityManager entityManager;

    /**
     * 测试场景:findByUsername应返回正确用户
     */
    @Test
    void findByUsername_WhenUserExists_ReturnsUser() {
        // Given:向数据库插入测试数据(TestEntityManager用于测试数据管理)
        User user = new User(null, "jane", "jane@example.com");
        entityManager.persistAndFlush(user); // 持久化并刷新到数据库

        // When:执行Repository方法
        User foundUser = userRepository.findByUsername("jane");

        // Then:验证结果
        assertNotNull(foundUser);
        assertEquals("jane@example.com", foundUser.getEmail());
    }
}

(2)Controller层测试(@WebMvcTest)

@WebMvcTest仅启动Web层相关的Bean(如DispatcherServlet、Controller),通过MockMvc模拟HTTP请求,无需启动完整应用。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

// 仅测试UserController(指定Controller类)
@WebMvcTest(UserController.class)
class UserControllerTest {

    // 模拟HTTP请求(由Spring提供)
    @Autowired
    private MockMvc mockMvc;

    // 模拟依赖的UserService(Controller依赖Service,需用@MockBean注入)
    @MockBean
    private UserService userService;

    /**
     * 测试场景:GET /users/{id} 应返回200和用户信息
     */
    @Test
    void getUserById_WhenUserExists_ReturnsOk() throws Exception {
        // Given:准备测试数据
        Long userId = 1L;
        User user = new User(userId, "john", "john@example.com");
        when(userService.getUserById(userId)).thenReturn(user);

        // When & Then:模拟GET请求并验证结果
        mockMvc.perform(get("/users/{id}", userId) // 模拟GET /users/1
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()) // 响应状态码200
                .andExpect(jsonPath("$.id").value(userId)) // 响应JSON的id字段为1
                .andExpect(jsonPath("$.username").value("john")) // username为john
                .andExpect(jsonPath("$.email").value("john@example.com")); // email匹配

        // 验证Service方法被调用
        verify(userService, times(1)).getUserById(userId);
    }

    /**
     * 测试场景:GET /users/{id} 当用户不存在时返回404
     */
    @Test
    void getUserById_WhenUserDoesNotExist_ReturnsNotFound() throws Exception {
        // Given:模拟Service返回null
        Long userId = 99L;
        when(userService.getUserById(userId)).thenReturn(null);

        // When & Then:模拟GET请求并验证结果
        mockMvc.perform(get("/users/{id}", userId)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isNotFound()); // 响应状态码404

        verify(userService, times(1)).getUserById(userId);
    }
}

7.4 实战3:集成测试(@SpringBootTest)

集成测试启动完整的Spring Boot应用,覆盖从Controller到Repository的全链路,适合验证端到端业务流程的正确性。以下以“用户CRUD流程”为例,展示集成测试的实战写法:

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.*;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.*;

// 启动完整Spring Boot应用,使用随机端口避免端口冲突
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserIntegrationTest {

    // 注入随机端口(由Spring Boot自动分配)
    @LocalServerPort
    private int port;

    // 注入TestRestTemplate(用于发送HTTP请求)
    @Autowired
    private TestRestTemplate restTemplate;

    // 注入Repository(用于测试前后数据清理)
    @Autowired
    private UserRepository userRepository;

    // 基础URL(拼接随机端口)
    private String baseUrl;

    // 测试用户数据
    private User testUser;

    // 请求头(指定JSON格式)
    private HttpHeaders headers;

    // 测试前初始化:设置基础URL、请求头,清理历史数据
    @BeforeEach
    void setUp() {
        baseUrl = "http://localhost:" + port + "/api/users";
        headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // 清理测试数据,避免影响测试结果
        userRepository.deleteAll();

        // 初始化测试用户
        testUser = new User(null, "integration_test", "test@example.com", "123456");
    }

    // 测试后清理:删除测试数据
    @AfterEach
    void tearDown() {
        userRepository.deleteAll();
    }

    /**
     * 端到端测试:创建用户 → 查询用户 → 更新用户 → 删除用户
     */
    @Test
    void testUserCrudFlow() {
        // 1. 第一步:创建用户(POST /api/users)
        HttpEntity<User> createRequest = new HttpEntity<>(testUser, headers);
        ResponseEntity<User> createResponse = restTemplate.exchange(
                baseUrl,
                HttpMethod.POST,
                createRequest,
                User.class
        );

        // 验证创建结果
        assertEquals(HttpStatus.CREATED, createResponse.getStatusCode());
        User createdUser = createResponse.getBody();
        assertNotNull(createdUser.getId()); // 数据库自增ID不为空
        assertEquals(testUser.getUsername(), createdUser.getUsername());

        // 2. 第二步:查询用户(GET /api/users/{id})
        Long userId = Objects.requireNonNull(createdUser).getId();
        ResponseEntity<User> getResponse = restTemplate.getForEntity(
                baseUrl + "/" + userId,
                User.class
        );

        // 验证查询结果
        assertEquals(HttpStatus.OK, getResponse.getStatusCode());
        User fetchedUser = getResponse.getBody();
        assertEquals(userId, fetchedUser.getId());
        assertEquals(testUser.getEmail(), fetchedUser.getEmail());

        // 3. 第三步:更新用户(PUT /api/users/{id})
        fetchedUser.setEmail("updated_test@example.com"); // 修改邮箱
        HttpEntity<User> updateRequest = new HttpEntity<>(fetchedUser, headers);
        ResponseEntity<User> updateResponse = restTemplate.exchange(
                baseUrl + "/" + userId,
                HttpMethod.PUT,
                updateRequest,
                User.class
        );

        // 验证更新结果
        assertEquals(HttpStatus.OK, updateResponse.getStatusCode());
        User updatedUser = updateResponse.getBody();
        assertEquals("updated_test@example.com", updatedUser.getEmail());

        // 4. 第四步:删除用户(DELETE /api/users/{id})
        ResponseEntity<Void> deleteResponse = restTemplate.exchange(
                baseUrl + "/" + userId,
                HttpMethod.DELETE,
                null,
                Void.class
        );

        // 验证删除结果
        assertEquals(HttpStatus.NO_CONTENT, deleteResponse.getStatusCode());

        // 再次查询已删除的用户,应返回404
        ResponseEntity<User> deleteCheckResponse = restTemplate.getForEntity(
                baseUrl + "/" + userId,
                User.class
        );
        assertEquals(HttpStatus.NOT_FOUND, deleteCheckResponse.getStatusCode());
    }
}

集成测试关键注意事项:

  1. 环境隔离:通过@BeforeEach和@AfterEach清理测试数据,避免不同测试用例相互影响;
  2. 随机端口:使用@LocalServerPort获取随机端口,避免本地端口占用导致测试失败;
  3. 真实依赖:集成测试会启动完整Spring容器,依赖真实的数据库(建议使用测试环境数据库,而非生产库);
  4. 测试粒度:集成测试侧重“流程正确性”,不建议覆盖过多细节(细节验证交给单元测试)。

7.5 测试覆盖率统计:确保测试完整性

为了保证测试覆盖足够多的代码逻辑,可通过JaCoCo插件统计测试覆盖率(行覆盖率、分支覆盖率),并设置最低覆盖率阈值。

步骤1:配置JaCoCo插件(pom.xml)

<build>
    <plugins>
        <!-- JaCoCo测试覆盖率插件 -->
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.8.11</version>
            <executions>
                <!-- 1. 准备测试环境,记录代码执行轨迹 -->
                <execution>
                    <id>prepare-agent</id>
                    <goals>
                        <goal>prepare-agent</goal>
                    </goals>
                </execution>
                <!-- 2. 执行测试后生成覆盖率报告 -->
                <execution>
                    <id>report</id>
                    <phase>test</phase>
                    <goals>
                        <goal>report</goal>
                    </goals>
                    <configuration>
                        <!-- 报告输出目录(HTML格式,便于查看) -->
                        <outputDirectory>${project.build.directory}/jacoco-report</outputDirectory>
                    </configuration>
                </execution>
                <!-- 3. 强制设置最低覆盖率阈值,未达标则构建失败 -->
                <execution>
                    <id>check</id>
                    <phase>test</phase>
                    <goals>
                        <goal>check</goal>
                    </goals>
                    <configuration>
                        <rules>
                            <rule>
                                <element>BUNDLE</element>
                                <limits>
                                    <!-- 行覆盖率最低80% -->
                                    <limit>
                                        <counter>LINE</counter>
                                        <value>COVEREDRATIO</value>
                                        <minimum>0.80</minimum>
                                    </limit>
                                    <!-- 分支覆盖率最低70%(覆盖if-else、switch等分支) -->
                                    <limit>
                                        <counter>BRANCH</counter>
                                        <value>COVEREDRATIO</value>
                                        <minimum>0.70</minimum>
                                    </limit>
                                </limits>
                            </rule>
                        </rules>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

步骤2:生成并查看覆盖率报告

  1. 执行mvn clean test,JaCoCo会自动统计测试覆盖率;
  2. 打开target/jacoco-report/index.html,查看HTML格式报告:
    • 绿色:已覆盖的代码行/分支;
    • 黄色:部分覆盖的分支(如if (a>0)只测试了a>0,未测试a<=0);
    • 红色:未覆盖的代码行/分支。

通过覆盖率报告,可快速定位“未覆盖的代码”,补充对应的测试用例,提升测试完整性。

八、自定义Starter:封装复用通用能力

在多项目开发中,经常会遇到“重复配置相同组件”的问题(如统一的日志配置、脱敏工具、第三方API客户端)。Spring Boot的“自定义Starter”机制,可将通用能力封装为独立组件,实现“一次开发,多项目复用”,大幅减少重复工作量。

8.1 自定义Starter的核心组成

一个标准的Spring Boot Starter包含3个核心部分:

  1. 自动配置类:通过@Configuration和@Conditional注解,实现组件的自动注册;
  2. 配置属性类:通过@ConfigurationProperties绑定外部配置,提升灵活性;
  3. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:Spring Boot 2.7+后,通过该文件指定自动配置类(替代旧版的spring.factories)。

8.2 实战:开发“脱敏工具Starter”

以“通用数据脱敏工具”为例,开发自定义Starter,支持对手机号、邮箱、身份证号等敏感信息进行脱敏,且可通过配置自定义脱敏规则。

步骤1:创建Starter项目结构

脱敏工具Starter项目结构:
com.example
├── desensitize-spring-boot-starter/  # Starter模块名(遵循xxx-spring-boot-starter命名规范)
│   ├── src/main/java/com/example/desensitize/
│   │   ├── config/                  # 自动配置类
│   │   │   └── DesensitizeAutoConfig.java
│   │   ├── properties/              # 配置属性类
│   │   │   └── DesensitizeProperties.java
│   │   ├── core/                    # 核心功能类
│   │   │   ├── Desensitizer.java    # 脱敏器接口
│   │   │   ├── impl/                # 脱敏器实现
│   │   │   │   ├── PhoneDesensitizer.java
│   │   │   │   ├── EmailDesensitizer.java
│   │   │   │   └── IdCardDesensitizer.java
│   │   │   └── DesensitizeUtils.java # 工具类(对外提供API)
│   │   └── annotation/              # 自定义注解(可选)
│   │       └── Desensitize.java
│   └── src/main/resources/
│       └── META-INF/
│           └── spring/
│               └── org.springframework.boot.autoconfigure.AutoConfiguration.imports # 自动配置导入文件
└── pom.xml  # Starter依赖配置

步骤2:编写配置属性类(DesensitizeProperties.java)

通过配置属性类,允许用户在application.yml中自定义脱敏规则(如手机号保留前3后4位):

import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;

// 配置前缀:spring.desensitize
@ConfigurationProperties(prefix = "spring.desensitize")
@Data
public class DesensitizeProperties {

    // 是否启用脱敏功能(默认true)
    private boolean enabled = true;

    // 手机号脱敏规则:保留前n位和后m位(默认前3后4)
    private PhoneRule phone = new PhoneRule();

    // 邮箱脱敏规则:保留前n位用户名(默认保留前2位)
    private EmailRule email = new EmailRule();

    // 身份证号脱敏规则:保留前n位和后m位(默认前6后4)
    private IdCardRule idCard = new IdCardRule();

    // 嵌套类:手机号脱敏规则
    @Data
    public static class PhoneRule {
        private int keepPrefix = 3;  // 保留前3位
        private int keepSuffix = 4;  // 保留后4位
    }

    // 嵌套类:邮箱脱敏规则
    @Data
    public static class EmailRule {
        private int keepUsername = 2; // 保留用户名前2位
    }

    // 嵌套类:身份证号脱敏规则
    @Data
    public static class IdCardRule {
        private int keepPrefix = 6;  // 保留前6位(出生地编码)
        private int keepSuffix = 4;  // 保留后4位(校验位)
    }
}

步骤3:编写核心脱敏功能类

首先定义脱敏器接口,再实现不同类型的脱敏逻辑:

  1. 脱敏器接口(Desensitizer.java):

    /**
    * 脱敏器接口:定义脱敏方法
    * @param <T> 待脱敏数据类型(如String)
    */
    public interface Desensitizer<T> {
    T desensitize(T data);
    }
  2. 手机号脱敏实现(PhoneDesensitizer.java):

    import com.example.desensitize.properties.DesensitizeProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    public class PhoneDesensitizer implements Desensitizer<String> {
    
    private final DesensitizeProperties.PhoneRule phoneRule;
    
    // 注入配置属性中的手机号规则
    @Autowired
    public PhoneDesensitizer(DesensitizeProperties properties) {
        this.phoneRule = properties.getPhone();
    }
    
    @Override
    public String desensitize(String phone) {
        if (phone == null || phone.length() < phoneRule.getKeepPrefix() + phoneRule.getKeepSuffix()) {
            return phone; // 数据长度不足,不脱敏
        }
        // 脱敏逻辑:保留前n位 + **** + 保留后m位
        String prefix = phone.substring(0, phoneRule.getKeepPrefix());
        String suffix = phone.substring(phone.length() - phoneRule.getKeepSuffix());
        return prefix + "****" + suffix;
    }
    }
  3. 工具类(DesensitizeUtils.java):对外提供统一的脱敏API,简化使用:

    import com.example.desensitize.core.Desensitizer;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    /**
    * 脱敏工具类:对外提供统一的脱敏方法
    */
    @Component
    public class DesensitizeUtils {
    
    // 注入不同类型的脱敏器(自动配置类中注册为Bean)
    @Autowired
    private Desensitizer<String> phoneDesensitizer;
    
    @Autowired
    private Desensitizer<String> emailDesensitizer;
    
    @Autowired
    private Desensitizer<String> idCardDesensitizer;
    
    // 手机号脱敏
    public String desensitizePhone(String phone) {
        return phoneDesensitizer.desensitize(phone);
    }
    
    // 邮箱脱敏
    public String desensitizeEmail(String email) {
        return emailDesensitizer.desensitize(email);
    }
    
    // 身份证号脱敏
    public String desensitizeIdCard(String idCard) {
        return idCardDesensitizer.desensitize(idCard);
    }
    }

步骤4:编写自动配置类(DesensitizeAutoConfig.java)

通过自动配置类,实现脱敏器、工具类的自动注册,且支持通过配置开关启用/禁用:

import com.example.desensitize.core.Desensitizer;
import com.example.desensitize.core.DesensitizeUtils;
import com.example.desensitize.core.impl.EmailDesensitizer;
import com.example.desensitize.core.impl.IdCardDesensitizer;
import com.example.desensitize.core.impl.PhoneDesensitizer;
import com.example.desensitize.properties.DesensitizeProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 脱敏工具自动配置类:当满足条件时,自动注册Bean
 */
@Configuration
// 1. 当类路径中存在DesensitizeUtils时才生效(避免依赖缺失)
@ConditionalOnClass(DesensitizeUtils.class)
// 2. 启用配置属性绑定(将application.yml中的配置注入DesensitizeProperties)
@EnableConfigurationProperties(DesensitizeProperties.class)
// 3. 当spring.desensitize.enabled=true时才生效(默认true)
@ConditionalOnProperty(
        prefix = "spring.desensitize",
        name = "enabled",
        havingValue = "true",
        matchIfMissing = true
)
public class DesensitizeAutoConfig {

    // 1. 注册手机号脱敏器Bean(用户未自定义时使用默认实现)
    @Bean
    @ConditionalOnMissingBean(name = "phoneDesensitizer")
    public Desensitizer<String> phoneDesensitizer(DesensitizeProperties properties) {
        return new PhoneDesensitizer(properties);
    }

    // 2. 注册邮箱脱敏器Bean
    @Bean
    @ConditionalOnMissingBean(name = "emailDesensitizer")
    public Desensitizer<String> emailDesensitizer(DesensitizeProperties properties) {
        return new EmailDesensitizer(properties);
    }

    // 3. 注册身份证号脱敏器Bean
    @Bean
    @ConditionalOnMissingBean(name = "idCardDesensitizer")
    public Desensitizer<String> idCardDesensitizer(DesensitizeProperties properties) {
        return new IdCardDesensitizer(properties);
    }

    // 4. 注册脱敏工具类Bean(对外提供API)
    @Bean
    @ConditionalOnMissingBean
    public DesensitizeUtils desensitizeUtils() {
        return new DesensitizeUtils();
    }
}

步骤5:配置自动配置类导入(AutoConfiguration.imports)

Spring Boot 2.7+不再支持META-INF/spring.factories,需在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中指定自动配置类:

# 内容为自动配置类的全限定名
com.example.desensitize.config.DesensitizeAutoConfig

步骤6:打包并发布Starter

执行mvn clean install,将Starter打包到本地Maven仓库(或发布到公司私有仓库),其他项目即可通过依赖引入。

8.3 其他项目使用自定义Starter

步骤1:引入依赖(pom.xml)

<dependency>
    <groupId>com.example</groupId>
    <artifactId>desensitize-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

步骤2:配置脱敏规则(application.yml)

spring:
  desensitize:
    enabled: true # 启用脱敏(默认true,可省略)
    phone:
      keep-prefix: 4 # 手机号保留前4位(覆盖默认的3位)
      keep-suffix: 3 # 手机号保留后3位(覆盖默认的4位)
    email:
      keep-username: 3 # 邮箱保留用户名前3位(覆盖默认的2位)

步骤3:使用脱敏工具

在Service或Controller中注入DesensitizeUtils,直接调用脱敏方法:

import com.example.desensitize.core.DesensitizeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private DesensitizeUtils desensitizeUtils;

    @GetMapping("/api/users/desensitize")
    public UserDesensitizeVO desensitizeUserInfo(
            @RequestParam String phone,
            @RequestParam String email,
            @RequestParam String idCard) {

        // 调用脱敏工具
        String desensitizedPhone = desensitizeUtils.desensitizePhone(phone);
        String desensitizedEmail = desensitizeUtils.desensitizeEmail(email);
        String desensitizedIdCard = desensitizeUtils.desensitizeIdCard(idCard);

        // 构建返回结果
        UserDesensitizeVO vo = new UserDesensitizeVO();
        vo.setPhone(desensitizedPhone);
        vo.setEmail(desensitizedEmail);
        vo.setIdCard(desensitizedIdCard);
        return vo;
    }

    // 响应VO
    public static class UserDesensitizeVO {
        private String phone;
        private String email;
        private String idCard;
        // getter/setter
    }
}

测试结果:

请求http://localhost:8080/api/users/desensitize?phone=13812345678&email=testuser123@example.com&idCard=110101199001011234,返回脱敏后的数据:

{
  "phone": "1381****567",    // 保留前4位(1381)+ **** + 后3位(567)
  "email": "tes****@example.com", // 保留用户名前3位(tes)+ **** + 域名
  "idCard": "110101********1234" // 保留前6位(110101)+ **** + 后4位(1234)
}

8.4 自定义Starter最佳实践

  1. 命名规范:Starter模块名遵循xxx-spring-boot-starter(如desensitize-spring-boot-starter),避免与官方Starter(spring-boot-starter-xxx)冲突;
  2. 条件注解:使用@ConditionalOnClass、@ConditionalOnProperty等注解,确保Starter在满足依赖和配置时才生效;
  3. 允许覆盖:通过@ConditionalOnMissingBean,允许用户自定义Bean覆盖默认实现(如自定义手机号脱敏器);
  4. 文档完善:提供README.md,说明Starter的功能、配置参数、使用示例,降低其他开发者的使用成本;
  5. 依赖控制:Starter的依赖尽量精简,避免引入不必要的依赖(如仅依赖spring-boot-starter,而非spring-boot-starter-web)。

九、Spring Boot 异步处理:提升系统并发能力

在处理耗时操作(如发送短信、生成报表、调用第三方API)时,同步执行会导致接口响应缓慢、线程阻塞,降低系统并发能力。Spring Boot的异步处理机制,通过@Async注解将耗时操作提交到线程池异步执行,实现“主线程快速响应,异步线程处理耗时任务”,大幅提升系统吞吐量。

9.1 异步处理核心原理

Spring Boot异步处理基于“线程池+AOP”实现:

  1. 启用异步:通过@EnableAsync注解开启异步功能;
  2. 标记异步方法:在耗时方法上添加@Async,Spring会通过AOP拦截该方法的调用;
  3. 提交异步任务:AOP拦截后,将方法执行逻辑封装为Runnable或Callable,提交到指定线程池;
  4. 主线程返回:提交任务后,主线程立即返回,无需等待异步任务执行完成;
  5. 异步任务执行:线程池中的空闲线程执行异步任务,执行结果可通过Future或CompletableFuture获取。

9.2 实战1:基础异步配置与使用

步骤1:启用异步功能(配置类)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync // 启用Spring异步处理
public class AsyncConfig {

    /**
     * 自定义异步线程池:避免使用默认线程池(默认线程池无界,可能导致OOM)
     * @return 线程池实例
     */
    @Bean(name = "asyncTaskExecutor") // 线程池名称,@Async可指定使用
    public Executor asyncTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 1. 核心线程数:线程池常驻的线程数量(根据CPU核心数配置,一般为CPU核心数*2)
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);

        // 2. 最大线程数:线程池可创建的最大线程数量
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 4);

        // 3. 队列容量:核心线程满后,任务排队等待的队列大小(避免无界队列)
        executor.setQueueCapacity(100);

        // 4. 线程空闲时间:非核心线程空闲超过该时间后会被销毁(单位:秒)
        executor.setKeepAliveSeconds(60);

        // 5. 线程名称前缀:便于日志排查
        executor.setThreadNamePrefix("Async-Task-");

        // 6. 拒绝策略:队列满且最大线程数达到时,如何处理新任务(此处为“丢弃任务并抛出异常”)
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

        // 7. 初始化线程池
        executor.initialize();

        return executor;
    }
}

步骤2:编写异步服务类

在耗时方法上添加@Async,指定使用自定义线程池:

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;

@Service
public class AsyncTaskService {

    /**
     * 异步发送短信(无返回值)
     * @param phone 手机号
     * @param content 短信内容
     */
    @Async("asyncTaskExecutor") // 指定使用自定义线程池
    public void sendSmsAsync(String phone, String content) {
        try {
            // 模拟耗时操作(如调用短信API,耗时2秒)
            Thread.sleep(2000);
            System.out.println("短信发送完成:手机号=" + phone + ",内容=" + content + ",线程名=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("短信发送失败", e);
        }
    }

    /**
     * 异步生成报表(有返回值,使用CompletableFuture接收结果)
     * @param reportId 报表ID
     * @return 报表生成结果(CompletableFuture支持链式调用)
     */
    @Async("asyncTaskExecutor")
    public CompletableFuture<String> generateReportAsync(String reportId) {
        try {
            // 模拟耗时操作(如查询数据库、生成Excel,耗时3秒)
            Thread.sleep(3000);
            String result = "报表生成完成:reportId=" + reportId + ",文件路径=/reports/" + reportId + ".xlsx";
            System.out.println(result + ",线程名=" + Thread.currentThread().getName());
            return CompletableFuture.completedFuture(result); // 返回成功结果
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return CompletableFuture.failedFuture(new RuntimeException("报表生成失败", e)); // 返回失败结果
        }
    }
}

步骤3:调用异步方法

在Controller或Service中调用异步方法,主线程无需等待异步任务完成:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;

@RestController
public class AsyncTaskController {

    @Autowired
    private AsyncTaskService asyncTaskService;

    /**
     * 测试异步发送短信(无返回值)
     * 接口响应时间:约10ms(仅处理请求参数,不等待短信发送完成)
     */
    @GetMapping("/async/send-sms")
    public String sendSms(@RequestParam String phone, @RequestParam String content) {
        long start = System.currentTimeMillis();

        // 调用异步方法(主线程立即返回)
        asyncTaskService.sendSmsAsync(phone, content);

        long end = System.currentTimeMillis();
        System.out.println("接口响应时间:" + (end - start) + "ms,线程名=" + Thread.currentThread().getName());

        return "短信发送请求已接收,正在处理中";
    }

    /**
     * 测试异步生成报表(有返回值,通过CompletableFuture获取结果)
     * 接口响应时间:约3秒(等待报表生成完成,适合需要返回结果的场景)
     */
    @GetMapping("/async/generate-report")
    public CompletableFuture<String> generateReport(@RequestParam String reportId) {
        long start = System.currentTimeMillis();

        // 调用异步方法,返回CompletableFuture(支持链式调用)
        CompletableFuture<String> reportFuture = asyncTaskService.generateReportAsync(reportId)
                // 异步任务成功后的回调(可选)
                .thenApply(result -> {
                    System.out.println("报表生成成功回调:" + result);
                    return result;
                })
                // 异步任务失败后的回调(可选)
                .exceptionally(ex -> {
                    System.out.println("报表生成失败回调:" + ex.getMessage());
                    return "报表生成失败:" + ex.getMessage();
                });

        long end = System.currentTimeMillis();
        System.out.println("接口请求处理时间:" + (end - start) + "ms,线程名=" + Thread.currentThread().getName());

        return reportFuture; // 返回CompletableFuture,Spring会自动处理结果
    }
}

测试结果:

  1. 调用/async/send-sms:

    • 接口响应时间约10ms(主线程快速返回);
    • 2秒后控制台打印“短信发送完成”(异步线程执行)。
  2. 调用/async/generate-report:

    • 接口响应时间约3秒(等待异步任务完成);
    • 3秒后返回报表生成结果,控制台打印“报表生成完成”。

9.3 实战2:异步处理的异常捕获

异步方法中若发生异常,默认不会被主线程的异常处理器捕获,需通过以下两种方式处理:

方式1:实现AsyncUncaughtExceptionHandler(处理无返回值的异步方法异常)

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;

@Configuration
public class AsyncExceptionConfig implements AsyncConfigurer {

    // 自定义异步线程池(与之前的配置一致)
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 4);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("Async-Task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }

    // 自定义异步异常处理器(处理无返回值的异步方法异常)
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncUncaughtExceptionHandler();
    }

    // 自定义异常处理器实现
    public static class CustomAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
        @Override
        public void handleUncaughtException(Throwable ex, Method method, Object... params) {
            // 异常处理逻辑:如记录日志、发送告警
            System.err.println("异步方法异常捕获:");
            System.err.println("方法名:" + method.getName());
            System.err.println("参数:" + params);
            System.err.println("异常信息:" + ex.getMessage());
            ex.printStackTrace();

            // 可选:发送告警(如短信、邮件)
            // sendAlarm("异步方法" + method.getName() + "执行失败:" + ex.getMessage());
        }
    }
}

方式2:使用CompletableFuture的exceptionally方法(处理有返回值的异步方法异常)

在generateReportAsync方法中,已通过exceptionally处理异常(见9.2步骤2),也可在调用方统一处理:

@GetMapping("/async/generate-report")
public CompletableFuture<String> generateReport(@RequestParam String reportId) {
    return asyncTaskService.generateReportAsync(reportId)
            .exceptionally(ex -> {
                // 统一异常处理:记录日志+返回友好提示
                System.err.println("报表生成异常:" + ex.getMessage());
                return "报表生成失败,请稍后重试";
            });
}

9.4 异步处理注意事项

  1. 线程池配置:必须自定义线程池,避免使用默认线程池(SimpleAsyncTaskExecutor)——默认线程池每次调用都会创建新线程,无上限,高并发下可能导致OOM;
  2. 方法调用限制:@Async方法不能在同一个类中调用(AOP无法拦截同类调用),需通过@Autowired注入后调用;
  3. 返回值类型:异步方法的返回值只能是void或Future(如CompletableFuture、ListenableFuture),返回其他类型会导致异步失效;
  4. 事务管理:@Async方法与@Transactional同时使用时,事务只作用于异步方法内部,主线程的事务不会包含异步方法(需注意数据一致性);
  5. 线程安全:异步方法中若使用非线程安全的变量(如成员变量),需通过锁或ThreadLocal保证线程安全。

十、Spring Boot 事件驱动:解耦业务逻辑

在复杂业务系统中,模块间直接依赖会导致代码耦合度高、难以维护(如“用户注册成功后,需要发送短信、创建会员、记录日志”,若在注册方法中直接调用这三个服务,会导致注册模块与其他模块强耦合)。Spring Boot的事件驱动机制,通过“发布-订阅”模式解耦模块间依赖,实现“事件触发后,多个订阅者自动执行,发布者无需关心订阅者”。

10.1 事件驱动核心组件

Spring事件驱动包含4个核心组件:

  1. 事件(Event):继承ApplicationEvent,封装事件相关数据(如用户注册事件包含用户ID、手机号);
  2. 事件发布者(Publisher):通过ApplicationEventPublisher发布事件;
  3. 事件订阅者(Listener):实现ApplicationListener或使用@EventListener注解,监听指定事件并执行逻辑;
  4. 事件多播器(Multicaster):默认由SimpleApplicationEventMulticaster实现,负责将事件广播给所有订阅者(支持同步/异步广播)。

10.2 实战:用户注册事件驱动示例

以“用户注册成功后,自动发送短信、创建会员、记录日志”为例,展示事件驱动的实现:

步骤1:定义事件(UserRegisteredEvent.java)

继承ApplicationEvent,封装用户注册相关数据:

import org.springframework.context.ApplicationEvent;
import lombok.Getter;

/**
 * 用户注册事件:用户注册成功后发布该事件
 */
@Getter // 提供getter方法,方便订阅者获取事件数据
public class UserRegisteredEvent extends ApplicationEvent {

    // 事件数据:用户ID
    private final Long userId;

    // 事件数据:用户手机号
    private final String phone;

    // 事件数据:注册时间(毫秒时间戳)
    private final long registerTime;

    /**
     * 构造事件
     * @param source 事件源(通常是发布事件的对象,如UserService)
     * @param userId 用户ID
     * @param phone 用户手机号
     */
    public UserRegisteredEvent(Object source, Long userId, String phone) {
        super(source);
        this.userId = userId;
        this.phone = phone;
        this.registerTime = System.currentTimeMillis();
    }
}

步骤2:实现事件发布者(UserService.java)

在用户注册成功后,通过ApplicationEventPublisher发布事件:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // 注入事件发布器
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private UserRepository userRepository;

    /**
     * 用户注册方法:注册成功后发布UserRegisteredEvent
     */
    public User register(UserRegisterDTO dto) {
        // 1. 业务逻辑:保存用户到数据库
        User user = new User();
        user.setUsername(dto.getUsername());
        user.setPhone(dto.getPhone());
        user.setPassword(dto.getPassword()); // 实际项目中需加密存储
        User savedUser = userRepository.save(user);

        // 2. 发布事件:用户注册成功,触发后续操作(发送短信、创建会员等)
        // 事件源:this(当前UserService对象)
        // 事件数据:用户ID、手机号
        eventPublisher.publishEvent(new UserRegisteredEvent(this, savedUser.getId(), savedUser.getPhone()));

        return savedUser;
    }
}

步骤3:实现事件订阅者(多个订阅者)

通过@EventListener注解实现订阅者,每个订阅者处理独立的业务逻辑(解耦):

  1. 短信发送订阅者(SmsListener.java):

    import org.springframework.context.event.EventListener;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Component;
    /**
    * 短信发送订阅者:监听UserRegisteredEvent,发送注册成功短信
    */
    @Component
    public class SmsListener {
    
    // @Async:异步处理(避免阻塞事件发布者,可选)
    @Async("asyncTaskExecutor")
    @EventListener(UserRegisteredEvent.class) // 监听UserRegisteredEvent
    public void sendRegisterSms(UserRegisteredEvent event) {
        // 获取事件数据
        Long userId = event.getUserId();
        String phone = event.getPhone();
        long registerTime = event.getRegisterTime();
    
        // 业务逻辑:发送短信(模拟耗时操作,耗时1秒)
        try {
            Thread.sleep(1000);
            System.out.println("用户注册短信发送完成:userId=" + userId + ",phone=" + phone + ",registerTime=" + registerTime + ",线程名=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("短信发送失败", e);
        }
    }
    }
  2. 会员创建订阅者(MemberListener.java):

    import org.springframework.context.event.EventListener;
    import org.springframework.stereotype.Component;
    /**
    * 会员创建订阅者:监听UserRegisteredEvent,为新用户创建会员账号
    */
    @Component
    public class MemberListener {
    
    @Autowired
    private MemberService memberService;
    
    @EventListener(UserRegisteredEvent.class)
    public void createMember(UserRegisteredEvent event) {
        Long userId = event.getUserId();
        // 业务逻辑:创建会员(默认注册用户为普通会员)
        memberService.createMember(userId, MemberLevel.NORMAL);
        System.out.println("用户会员创建完成:userId=" + userId + ",会员等级=" + MemberLevel.NORMAL);
    }
    }
  3. 日志记录订阅者(LogListener.java):

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
 * 日志记录订阅者:监听UserRegisteredEvent,记录用户注册日志
 */
@Component
public class LogListener {

    @Autowired
    private OperateLogService operateLogService;

    @EventListener(UserRegisteredEvent.class)
    public void recordRegisterLog(UserRegisteredEvent event) {
        // 构建日志信息
        OperateLog log = new OperateLog();
        log.setUserId(event.getUserId());
        log.setOperateType("USER_REGISTER");
        log.setOperateTime(event.getRegisterTime());
        log.setOperateDesc("用户注册成功,手机号:" + event.getPhone());

        // 业务逻辑:保存日志到数据库
        operateLogService.saveLog(log);
        System.out.println("用户注册日志记录完成:userId=" + event.getUserId() + ",日志ID=" + log.getId());
    }
}

步骤4:配置事件异步广播(可选)

默认情况下,事件广播是同步的(发布者会等待所有订阅者执行完成)。若订阅者包含耗时操作,可配置事件多播器为异步模式:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.TaskExecutor;

@Configuration
public class EventAsyncConfig {

    /**
     * 配置异步事件多播器:所有事件订阅者都会异步执行
     */
    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster(TaskExecutor asyncTaskExecutor) {
        SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
        // 设置异步执行的线程池(使用之前定义的asyncTaskExecutor)
        multicaster.setTaskExecutor(asyncTaskExecutor);
        return multicaster;
    }
}

测试结果:

调用userService.register()注册用户后,控制台输出:

用户会员创建完成:userId=1,会员等级=NORMAL
用户注册日志记录完成:userId=1,日志ID=1001
用户注册短信发送完成:userId=1,phone=13812345678,registerTime=1699999999999,线程名=Async-Task-1
  • 若未配置异步多播器:三个订阅者同步执行(日志→会员→短信,总耗时约1秒);
  • 若配置异步多播器:三个订阅者异步执行(总耗时约1秒,线程名不同)。

10.3 事件驱动最佳实践

  1. 事件命名规范:事件类名遵循XXXEvent(如UserRegisteredEvent),清晰表达事件含义;
  2. 事件数据最小化:事件中只包含订阅者必需的数据(如用户ID、手机号),避免传递过大对象;
  3. 异步优先:若订阅者包含耗时操作(如发送短信、调用第三方API),优先使用异步事件(@Async或异步多播器),避免阻塞发布者;
  4. 事务一致性:若事件发布在事务内(如用户注册事务未提交时发布事件),订阅者可能无法读取到未提交的数据,需通过以下方式解决:
    • 方式1:在事务提交后发布事件(使用TransactionSynchronizationManager);
    • 方式2:使用Spring 4.2+提供的@TransactionalEventListener注解,指定事件在事务提交后触发:
      @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
      public void createMember(UserRegisteredEvent event) {
       // 事务提交后执行,可读取到已提交的用户数据
       memberService.createMember(event.getUserId(), MemberLevel.NORMAL);
      }
  5. 事件溯源:复杂业务场景下,可通过“事件溯源”记录所有事件,实现业务流程回溯、数据恢复(如用户注册→会员升级→订单创建,所有事件可回溯用户行为)。

总结:Spring Boot 进阶能力的核心价值

本文介绍的10个Spring Boot进阶功能,覆盖了配置管理、可观测性、开发效率、容错能力、代码复用、并发处理、业务解耦等核心场景,其本质是帮助开发者:

  1. 解耦:通过事件驱动、自定义Starter减少模块间依赖;
  2. 提效:通过DevTools、Test、Cache减少重复工作,提升开发与运行效率;
  3. 稳定:通过Actuator、Retry、异步处理提升系统可观测性、容错能力与并发能力;
  4. 灵活:通过@Conditional、@ConfigurationProperties实现配置动态调整,适配多环境、多场景。

掌握这些进阶功能,不仅能解决日常开发中的复杂问题,更能帮助你从“单纯写代码”转变为“设计高可用、可维护的系统”,真正发挥Spring Boot的生态优势。建议结合实际项目场景逐步实践,将这些功能融入到你的开发流程中,实现“写更少的代码,构建更强大的系统”。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/hou-duan/4523

相关文章

  • 从3秒到30毫秒!SpringBoot树形结构深度优化指南:不止于O(n)算法的全链路提速方案
  • Spring WebFlux深度解析:异步非阻塞架构与实战落地指南
  • Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
  • 重构 Controller 终极指南:从臃肿到优雅的 7 大黄金法则 + 实战技巧
  • MyBatis vs Spring Data JPA 从原理到实战全解析
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA Spring SpringBoot
最后更新:2025年10月15日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

春宵一刻值千金,花有清香月有阴。
歌管楼台声细细,秋千院落夜沉沉。

那年今日(05月15日)

  • 2012年:墨西哥作家卡洛斯·富恩特斯逝世
  • 1980年:中华人民共和国恢复了世界银行与国际货币基金组织的合法席位
  • 1918年:鲁迅发表小说《狂人日记》
  • 1859年:法国物理学家皮埃尔·居里出生
  • 1773年:奥国外交家克莱门斯·梅特涅出生
  • 更多历史事件
最新 热点 随机
最新 热点 随机
踩坑60+次后,我终于搞懂 Claude Skill 怎么写才会真的触发 Everything Claude Code 详细使用文档 配置Jackson使用字段而不是getter/setter来序列化和反序列化 这个域名注册整整十年了,十年时间,真快啊 Claude Code全维度实战指南:从入门到精通,解锁AI编程新范式 Apollo配置中心中的protalDB的作用是什么
AI时代,个人技术博客的出路在哪里?这个域名注册整整十年了,十年时间,真快啊WordPress实现用户评论等级排行榜插件WordPress网站换了个字体,差点儿把样式换崩了做了一个WordPress文章热力图插件千万级大表新增字段实战指南:告别锁表与业务中断
中国文学史上最霸气的诗句是哪一首?这首诗当仁不让 AI 协作新范式:以文档为中心的开发实践指南 妹妹的画【2019.07.09】 ThreadPoolExecutor如何实现线程复用及超时销毁 CPU飙高,系统性能问题如何排查? Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
标签聚合
K8s docker MySQL 设计模式 Redis WordPress 架构 AI IDEA Spring ElasticSearch JAVA JVM SQL 分布式 SpringBoot 数据库 多线程 AI编程 日常
友情链接
  • Blogs·CN
  • Honesty
  • Mr.Sun的博客
  • 临窗旋墨
  • 哥斯拉
  • 彬红茶日记
  • 志文工作室
  • 懋和道人
  • 拾趣博客导航
  • 搬砖日记
  • 旧时繁华
  • 林羽凡
  • 瓦匠个人小站
  • 皮皮社
  • 知向前端
  • 蜗牛工作室
  • 韩小韩博客
  • 风渡言

COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

域名年龄

Theme Kratos Made By Dylan

津ICP备2024022503号-3

京公网安备11011502039375号