李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
    • 赞助
  • 留言
  • 走心评论
  • 系列文章
  • Now
  • 每日心情
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 后端
  3. 正文

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

2025年10月15日 约 17,104 字58 分钟 26点热度 0人点赞 0条评论
本文最后更新于 2025年10月15日,距今已 297 天,其中的信息可能已经发生变化,请注意甄别。

作为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

推荐阅读

  • Spring Boot 配置加载优先级总结
  • 从3秒到30毫秒!SpringBoot树形结构深度优化指南:不止于O(n)算法的全链路提速方案
  • Spring Boot 指定外部配置文件的方式
  • 关于服务的探活端口和业务端口不一致有什么问题
  • 记一次Apollo配置中心+Spring配置自动刷新导致的内存泄露问题
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA Spring SpringBoot
最后更新:2025年10月15日

岁月同一天 8 月 8 日

回望过去的今天,你在写什么

  • 4 年前 2022年8月8日
    减肥四个月~

    今年四月初开始每天跳绳减肥,到了今天刚好四个月了。从一开始的每天跳绳十分钟到半个小时,再从半个小时到一个小时,最后又稳定…

相关文章
  • SpringBoot DeferredLog 完整详解2026年6月18日
  • JAVA简单快速的读写Excel之EasyExcel2019年10月25日
  • Spring中的Aware接口2025年5月17日
  • JAVA线程池简析(JDK1.6)2019年6月13日
  • Java中ArrayList为什么比LinkedList查询速度快?2020年5月16日

李锋镝

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

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

还没有评论,快来抢沙发吧~

九年没联系的女友突然问我借钱,我开口道:四十万行么?不够了再说话。”前女友很是高兴地回复说:“够用,还是我男朋友有办法啊,你放心,我日后一定还给你,九年多没见了你现在干啥呢,这么有钱?”我呵呵一笑:“没事,不够用您尽管说话,俺现在改做抵押贷款了。”

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
Kratos+ v1.1.14版本更新说明 Spring Boot 指定外部配置文件的方式 Spring Boot 配置加载优先级总结 Claude Fable 5(claude-fable-5)深度详解 如何通过命令查看Java应用内存中对象数量 关于服务的探活端口和业务端口不一致有什么问题
给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能AI时代,个人技术博客的出路在哪里?增加了两套复古皮肤-牛皮纸、千禧网页这个域名注册整整十年了,十年时间,真快啊WordPress实现用户评论等级排行榜插件WordPress网站换了个字体,差点儿把样式换崩了
MybatisCodeHelperPro激活 解决kubectl exec -it xxxx-service-bfbd45bb9-ktvzj bash -n bit error: exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead See 'kubectl exec -h' for help and examples Apollo配置中心中的protalDB的作用是什么 k8s + docker + Jenkins使用Pipeline部署SpringBoot项目时Jenkins错误集锦 彻底搞懂mysql日志系统binlog,redolog,undolog Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
最近评论
Huo 发布于 8 小时前(08月07日) 挺有特色的主题,还是感觉 WP 的确是强大
李锋镝 发布于 21 小时前(08月07日) 没理解你想说啥
aboss 发布于 21 小时前(08月07日) 你的后台web-login?
李锋镝 发布于 22 小时前(08月07日) 这个专门的插件实现的功能更好更全,还能对接支付之类的
李锋镝 发布于 22 小时前(08月07日) 自定义登录地址是为了防止大部分机器人通过WP固定登录页面暴力破解用户账号密码
标签聚合
K8s 日常 Claude docker JVM WordPress Redis AI编程 多线程 SQL 架构 MySQL IDEA JAVA SpringBoot Spring AI 数据库 ElasticSearch 分布式
友情链接
  • Blogs·CN
  • 懋和道人
  • 旧时繁华
  • Honesty
  • 拾趣博客导航
  • 搬砖日记
  • 知向前端
  • 临窗旋墨
  • 皮皮社
  • 瓦匠个人小站
  • 志文工作室
  • 韩小韩博客
  • 风渡言
  • 彬红茶日记
  • 老张博客
  • Mr.Sun的博客
  • 韩情脉脉
  • 哥斯拉
  • 林羽凡

COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

正在博友圈履约中

域名年龄

Theme Kratos+ By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号