Spring 事件机制在微服务中实现最终一致性的设计

Spring 事件机制在微服务中实现最终一致性的设计

1. 从一次跨服务扣库存说起

假设你负责一个电商系统,订单服务负责创建订单,库存服务负责扣减库存。用户下单时,订单服务必须先扣库存,否则可能超卖。最简单的做法是:订单服务在创建订单后,直接调用库存服务的 REST 接口扣减库存。这种同步调用的写法很直观,但问题也明显:

  • 如果库存服务响应慢或不可用,订单创建就会阻塞或失败,用户体验差。
  • 订单服务和库存服务被强耦合在一起:订单服务必须知道库存服务的地址、接口细节。
  • 为了确保一致性,你可能会把两个操作放进一个分布式事务里,例如两阶段提交,但分布式事务实现复杂、性能开销大,很多团队不愿意用。

那有没有一种轻量级方案?有的。我们可以让订单服务只负责创建订单,然后把"订单已创建"这个事实以事件的形式发布出去,库存服务监听这个事件,自己去扣减库存。两个服务之间不再同步调用,而是通过事件异步解耦。这就是 Spring 事件机制在微服务中实现最终一致性的典型场景。

这篇文章会带你从零开始搭建这样的方案。我会从 Spring 的事件机制讲起,逐步引入事务事件、异步处理、重试和死信队列,最终让你能设计出可靠、可落地的最终一致性系统。

先记住一个最小模型:服务 A 在业务操作成功后发布一个事件,服务 B 监听该事件并在稍后执行自己的业务,最终所有服务达到一致状态。 在这个过程中,A 和 B 不直接通信,而是通过事件这个中间人。你可以把事件理解为一个"通知",A 发出通知,B 收到通知后干活。

2. 整体框架:三个角色和一条链路

要把这个模型落地,我们需要三个角色:

  • 事件发布者 :通常是业务操作所在的类或服务,负责在业务成功后构造并发布事件。对应到 Spring,就是 ApplicationEventPublisher
  • 事件本身 :一个普通的 Java 对象,用来携带业务数据,例如订单 ID、用户 ID 等。Spring 中继承自 ApplicationEvent 或直接实现 ApplicationEvent 的类,但通常我们使用普通的 POJO 加上 Spring 4.2 之后的 @EventListener 标注来简化。
  • 事件监听器 :处理事件的组件,对应到 Spring,就是一个带有 @EventListener 注解的方法所在的 Bean。

它们之间的连接关系很简单:发布者调用 publishEvent 方法,Spring 容器根据事件类型找到对应的监听器,然后同步或异步调用它们。

一次完整的请求流转如下:

text 复制代码
客户端请求
  |
  v
服务A 的 Service 方法
  |  1. 执行业务操作(如保存订单)
  |  2. 发布订单创建事件
  v
Spring 容器
  |  3. 找到匹配的监听器(本地或通过消息队列)
  v
服务B 的监听器方法
  |  4. 执行业务操作(如扣减库存)
  |  5. 完成,返回或记录日志
  v
最终一致

需要注意的是,这个链条可以在同一个 JVM 内(本地事件),也可以跨 JVM(通过消息中间件)。Spring 原生的 ApplicationEvent 是 JVM 内的事件,但我们可以将它与 JMS、RabbitMQ 等结合,实现分布式事件。不过本文重点在 Spring 原生机制,用本地事件演示原理;在工程落地时,通常需要引入消息队列。但本地事件是基础,理解了它,扩展会更容易。

3. 从一个最小的本地事件示例学起

先看一个最简单的例子:订单服务在订单创建后发布一个事件,另一个处理器(模拟库存服务)监听到事件后打印日志。这个例子证明核心规则:事件发布后,监听器能够被触发,且默认是同步执行。

3.1 完整示例:最小事件发布与监听

目标:展示 Spring 事件的基本用法,观察同步执行的行为。

前置环境 :需要 Spring Boot 3.x 项目,引入 spring-boot-starter-web(用于启动)和 spring-boot-starter。下面的代码可以直接运行。

步骤:定义一个事件类,一个发布者,一个监听器,在启动时触发。

事件类(普通 POJO,无需继承):

java 复制代码
public class OrderCreatedEvent {
    private String orderId;
    public OrderCreatedEvent(String orderId) { this.orderId = orderId; }
    public String getOrderId() { return orderId; }
}

发布者(通过注入 ApplicationEventPublisher 发布):

java 复制代码
@Component
public class OrderService {
    private final ApplicationEventPublisher publisher;
    public OrderService(ApplicationEventPublisher publisher) { this.publisher = publisher; }

    public void createOrder(String orderId) {
        System.out.println("[OrderService] 创建订单: " + orderId);
        publisher.publishEvent(new OrderCreatedEvent(orderId));
    }
}

监听器(用 @EventListener 标注方法):

java 复制代码
@Component
public class InventoryListener {
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        System.out.println("[InventoryListener] 处理订单事件: " + event.getOrderId() + ", 当前线程: " + Thread.currentThread().getName());
    }
}

启动类或测试代码中调用:

java 复制代码
@SpringBootApplication
public class EventDemoApplication {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(EventDemoApplication.class, args);
        OrderService orderService = ctx.getBean(OrderService.class);
        orderService.createOrder("12345");
    }
}

预期输出 :创建订单和监听器处理都在同一个线程(如 main 或 http-nio)中输出。这就证明了 Spring 事件默认是同步调用:publishEvent 会阻塞等待监听器执行完毕。

关键步骤和解释 :发布者调用了 publishEvent,Spring 容器内部根据事件类型找到匹配的监听器,并直接调用其方法。由于没有标注 @Async,所以是调用线程执行。

适用场景:适用于事件处理逻辑很快、且需要保证事件处理后才能继续的场景,比如在同一个事务内更新缓存等。但如果在事件处理中耗时长,或需要容错,就需要异步。

容易改错的地方 :忘记在监听器类上标注 @Component,导致 Spring 无法扫描到;或者事件类没有提供 getter,导致监听器无法获取数据。

3.2 事件发布与事务的纠缠

上面的例子没有涉及数据库事务。在真实业务中,订单创建往往伴随着数据库写操作。如果你在事务尚未提交时就发布事件,监听器可能在事务提交前就被触发。如果监听器去查询数据库,可能读不到订单数据,因为事务还没提交,造成数据不一致。

这个问题在 Spring 中有一个专门的处理:事务事件监听器。我们来看第二个例子。

4. 事务事件:与数据库事务同步

4.1 引入事务事件的原因

假设订单保存逻辑是这样的:

java 复制代码
@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);
        publisher.publishEvent(new OrderCreatedEvent(order.getId()));
        // ... 其他逻辑
    }
}

如果不做特殊处理,publishEvent 会在事务提交之前被调用,此时监听器内的代码如果去数据库查询这个订单,可能会因为事务尚未提交而查不到(取决于隔离级别)。更严重的是,如果事务最终回滚了,但事件已经被消费了,比如监听器已经扣减了库存,就会导致订单没创建成功却扣了库存。

因此,我们需要让事件在事务提交之后再发布。Spring 为此提供了 @TransactionalEventListener 注解,它允许你指定事件的执行时机,例如在事务提交后(AFTER_COMMIT)、事务回滚后(AFTER_ROLLBACK)等。

4.2 事务事件示例

目标 :展示使用 @TransactionalEventListener 后,监听器只在事务提交后才执行。

前置环境:需要数据库支持事务,这里我们用 H2 内存数据库,并开启事务管理。

步骤 :定义订单实体和 Repository,修改监听器为 @TransactionalEventListener

数据库实体(简化):

java 复制代码
@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;
    private String item;
    // 省略构造器、getter/setter
}

Repository:

java 复制代码
public interface OrderRepository extends JpaRepository<Order, Long> { }

修改 OrderService 使用事务,并发布事件:

java 复制代码
@Transactional
public void createOrder(String item) {
    Order order = new Order(item);
    orderRepository.save(order);
    // 注意:这里将事件发布放在事务方法内,但使用事务事件监听器后,实际执行时会延迟到事务提交后
    publisher.publishEvent(new OrderCreatedEvent(order.getId()));
}

修改监听器:

java 复制代码
@Component
public class InventoryListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderCreated(OrderCreatedEvent event) {
        System.out.println("[InventoryListener] 处理订单事件: " + event.getOrderId());
    }
}

预期输出:发布事件时,不会立即打印监听器日志,而是在事务提交后才打印。你可以通过日志观察顺序。如果没有事务,监听器会照常执行。

关键步骤和解释@TransactionalEventListener 必须在一个活跃的事务中才会被延迟处理。如果某个事件是在无事务状态下发布的,默认不会触发监听器,除非你设置 fallbackExecution = true

适用场景:确保事件处理与数据库操作保持一致,例如:订单创建成功且提交后,发送消息通知其他服务。

容易改错的地方 :忘记将发布事件放在事务方法内,或者监听方法没有加 @TransactionalEventListener 而用了 @EventListener,导致事件仍然在事务内触发。另外,事务事件默认是同步执行的,如果处理慢会拖慢事务提交后的响应,因此通常配合异步使用。

4.3 事务事件还支持哪些时机?

下表总结了不同阶段的行为和适用场景:

阶段 监听器执行时机 典型用途 注意事项
AFTER_COMMIT 事务提交后 发送通知、触发后续业务 如果事件处理失败,业务已提交,需要引入补偿机制
AFTER_ROLLBACK 事务回滚后 记录失败原因、发送告警 注意事务是否真的回滚
AFTER_COMPLETION 事务完成(提交或回滚)后 清理资源 不确定是提交还是回滚
BEFORE_COMMIT 事务提交前(通常已准备就绪) 注册额外操作 TransactionSynchronization 实现

5. 异步处理:不阻塞主线程

在开头的场景中,库存扣减可能涉及网络调用,耗时不短。如果事件监听器同步执行,主线程(处理用户请求的线程)会被阻塞,影响接口响应时间。因此我们需要将事件处理放在独立的线程池中异步执行。

5.1 开启异步支持

Spring 提供了 @Async 注解,配合 @EnableAsync 开启异步。最简单的做法是在监听器方法上标注 @Async。但注意:如果监听器方法既标注了 @Async 又标注了事务事件,要小心线程和事务的传递问题。

5.2 异步+事务事件示例

目标 :演示如何同时使用 @TransactionalEventListener@Async,让事件在事务提交后由新线程执行。

实现 :在启动类上添加 @EnableAsync,在监听方法上添加 @Async

启动类修改:

java 复制代码
@SpringBootApplication
@EnableAsync
public class EventDemoApplication {
    // ...
}

监听器修改:

java 复制代码
@Component
public class InventoryListener {
    @Async // 让监听器在独立线程执行
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderCreated(OrderCreatedEvent event) {
        System.out.println("[InventoryListener] 异步处理订单: " + event.getOrderId() + ", 线程: " + Thread.currentThread().getName());
        // 模拟耗时操作
        try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

预期输出:主线程打印订单创建后立即返回,而监听器在事务提交后由另一个线程打印,且会延迟1秒。

关键解释@Async 会调用 Spring 的 TaskExecutor,默认使用 SimpleAsyncTaskExecutor,不过生产环境要配置线程池。异步操作失去了调用上下文,因此事务、SecurityContext 等不会自动传播,需要手动处理。

设计取舍

方式 优点 缺点
同步事件 简单、顺序保证,便于调试 阻塞主线程,响应慢,耦合增强
异步事件 主线程快速返回,提高吞吐量 无法控制执行顺序,需要处理任务失败和线程安全问题
事务事件 保证事件与事务一致 需要额外配置,且增加了处理时机复杂度

什么场景使用哪种? 如果事件处理很简单且需要确保结果可见,用同步;如果事件处理有网络 IO 或耗时,用异步;如果事件依赖事务的提交结果,必须用事务事件。

5.3 异步事件的代价

异步之后,原本的错误处理变得复杂。例如,异步线程中抛出的异常不会自动传播到主线程,你需要手动捕获并记录日志。更糟糕的是,如果异步处理失败,因为无法重试,可能导致最终不一致。因此,我们需要添加重试机制。

6. 用重试机制提升可靠性

异步事件处理可能因网络抖动、数据库锁等原因失败。为了保证最终一致性,一个简单的策略是失败后重试。Spring 没有内置的事件监听重试机制,但我们可以利用 @Retryable(Spring Retry)或者其他重试库来实现。

6.1 引入 Spring Retry

首先添加依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

然后在启动类上开启重试支持:

java 复制代码
@EnableRetry

接着在监听器方法上添加 @Retryable

java 复制代码
@Component
public class InventoryListener {
    private final InventoryClient inventoryClient;

    public InventoryListener(InventoryClient inventoryClient) { this.inventoryClient = inventoryClient; }

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public void onOrderCreated(OrderCreatedEvent event) {
        inventoryClient.deductStock(event.getOrderId()); // 假设有库存客户端
    }
}

6.2 重试策略的选择

在微服务环境中,如果事件是发布到消息队列,由消费者处理,可以考虑在消费者侧实现重试。下表比较了几种重试方式:

方式 优点 缺点 适用场景
同步重试 简单,立即执行 阻塞线程,可能加重系统负担 短时故障
指数退避重试 动态间隔,减轻服务器压力 实现稍复杂,可能延迟过长 网络抖动
消息队列重试(如 RabbitMQ DLX) 可靠,可持久化 需要配置额外组件 高可用场景
本地数据库重试表 完全自主可控 需要额外存储 无消息中间件的场景

重试并不能处理所有失败。如果重试多次后仍然失败,例如库存服务彻底不可用,或者业务逻辑错误导致永远无法成功,就需要将事件转移到死信队列,留待人工介入。

7. 死信队列:处理最终失败的事件

在本地事件机制中,没有内置的死信队列。但在实际微服务设计中,常用消息队列(如 RabbitMQ)和死信交换机(DLX)来兜底。这里我们先了解死信队列的概念,然后展示如何结合 RabbitMQ 和 Spring 实现容错。

7.1 什么是死信队列?

死信队列(DLQ)用于存放无法正常处理的消息(或本地的失败事件记录)。当消息消费失败且达到最大重试次数后,将消息发送到死信队列,以便分析原因、修复后重放或人工处理。

7.2 使用 RabbitMQ 实现最终一致性的事件处理

这是实际生产中最常用的方案之一。图:

text 复制代码
服务A 业务事务提交
  |
  v
发送消息到 topic-exchange (order.created)
  |
  v
RabbitMQ Broker
  |
  v
队列 orderQueue 绑定该 exchange
  |
  v
服务B 消费者
  | 失败
  v
重试(Spring Retry)
  | 最终失败
  v
死信交换机 DLX
  |
  v
orderQueue.dlx 死信队列

具体实现 :在消费者中用 @RabbitListener 接收消息,并在其中调用业务逻辑,如果抛出异常则触发重试。超过重试次数的消息会被 RabbitMQ 自动放入死信队列(通过设置 x-dead-letter-exchange)。

我们来看第三个完整示例:采用 RabbitMQ 和死信队列实现订单创建的最终一致性。

7.3 完整示例:使用 RabbitMQ 的最终一致性设计

目标:搭建一个模拟订单服务发布消息、库存服务消费消息并实现失败重试进入死信队列的完整流程。需要 Docker 运行 RabbitMQ。

环境 :首先启动 RabbitMQ:docker run -d --hostname my-rabbit --name rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management。然后创建 Spring Boot 项目,添加依赖:spring-boot-starter-amqpspring-boot-starter-web

配置(application.yml):

yaml 复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  # 定义交换机和队列绑定
  cloud:
    stream:
      bindings:
        order-out-0:
          destination: order.exchange
          group: order
        inventory-in-0:
          destination: order.exchange
          group: inventory

(为了简洁,不使用 Spring Cloud Stream,直接用 RabbitTemplate 和 RabbitListener。)

代码结构

  1. 配置队列、交换机和死信:
java 复制代码
@Configuration
public class RabbitConfig {
    public static final String EXCHANGE = "order.exchange";
    public static final String QUEUE = "order.queue";
    public static final String DLQ = "order.queue.dlx";
    public static final String DLX_EXCHANGE = "order.dlx";

    @Bean
    public TopicExchange orderExchange() {
        return new TopicExchange(EXCHANGE);
    }

    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable(QUEUE)
                .withArgument("x-dead-letter-exchange", DLX_EXCHANGE)
                .build();
    }

    @Bean
    public Queue orderDlq() {
        return QueueBuilder.durable(DLQ).build();
    }

    @Bean
    public Declarables bindings() {
        return new Declarables(
                new Binding(QUEUE, Binding.DestinationType.QUEUE, EXCHANGE, "order.created", null),
                new Binding(DLQ, Binding.DestinationType.QUEUE, DLX_EXCHANGE, "#", null)
        );
    }
}
  1. 发送者(订单服务):
java 复制代码
@Service
public class OrderService {
    private final RabbitTemplate rabbitTemplate;

    public OrderService(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; }

    @Transactional
    public void createOrder(String orderId) {
        // 业务数据库操作省略
        System.out.println("订单创建: " + orderId);
        // 发布消息
        rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "order.created", orderId);
    }
}

注意:这里模拟事务,但实际使用 MQ 时,要确保消息发送与本地事务的一致性,通常用事务消息模式,如本地消息表。此处简化。

  1. 消费者(库存服务):
java 复制代码
@Component
public class InventoryConsumer {

    @RabbitListener(queues = RabbitConfig.QUEUE)
    public void handleOrder(String orderId) throws Exception {
        System.out.println("收到消息: " + orderId);
        try {
            // 模拟扣库存失败
            if ("fail".equals(orderId)) {
                throw new RuntimeException("扣库存失败");
            }
            System.out.println("库存扣减成功: " + orderId);
        } catch (Exception e) {
            // 手动重试:简单示例只失败一次,但我们可以让 RabbitMQ 不自动重回队列,我们抛出异常后会触发重试机制。
            // 在真实场景,Spring Retry 可以在此处配置。这里为了看死信,我们直接抛出异常并拒绝消息,且不重回队列。
            System.out.println("处理失败,发送到死信队列");
            throw e; // 抛出异常会触发重试,默认会重回队列,除非配置 requeue-rejected: false
        }
    }
}

为了演示死信,在配置中设置默认拒收不进队:

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        default-requeue-rejected: false

消费者方法抛出异常后,消息会被拒绝并路由到死信队列,而不是无限重试。如果想实现有限重试,通常结合 Spring Retry 和自定义 MessageRecoverer。

步骤和预期 :先启动 RabbitMQ,再启动应用。访问端点如 /app/order?orderId=test/app/order?orderId=fail。观察控制台,test 消息成功处理,fail 消息最终出现在死信队列(通过 RabbitMQ 管理界面看到)。

解释:这个示例演示了如何用消息队列实现跨服务的事件通信,并用死信队列处理最终失败。它是本地事件机制的工程化扩展。

边界: RabbitMQ 的可靠投递需要解决消息发送与本地事务的一致性问题,通常会使用事务消息或本地消息表。另外,消费者幂等性必须保证,否则重复消息会导致问题。

8. 常见误区:你容易踩的坑

  • 误区1:@TransactionalEventListener 在没有事务时会失效。 默认情况下,如果在非事务方法中发布事件,监听器不会执行。要启用必须设置 fallbackExecution = true,或者确保发布事件的方法被事务标记。
  • 误区2:@Async 和 @TransactionalEventListener 的顺序。 先运行事务事件监听器,再进入 @Async 方法。也就是说,@TransactionalEventListener 会确保事务已提交,然后才调度异步线程。因此监听器方法内的 @Async 是在事务提交后生效的。
  • 误区3:事件监听器内直接调用可变的 Spring Bean。 异步线程环境下,要注意 Bean 是否为线程安全,特别是如果有实例变量。
  • 误区4:以为 Spring 事件可以跨 JVM。 原生的是本地事件,无法跨服务。要跨服务必须借助 MQ,或者像 Spring Cloud Stream 这样的增强工具。
  • 误区5:忽略幂等性。 异步事件可能被重复消费,因为网络重发导致消息重复。因此消费者必须实现幂等操作,比如用订单 ID 做去重表。

9. 生产实践建议:从零到一的设计决策

在真实项目中,选择哪种方式取决于场景:

需求 推荐方案
模块内简单事件,且希望保持代码简洁 Spring 原生 @EventListener + 事务事件
需要跨服务,且能接受复杂中间件 RabbitMQ/Kafka + 重试 + 死信队列
需要高吞吐且消息可以乱序 使用 Kafka,事件驱动 + 幂等设计
需要严格顺序 在同步或持久化消息队列中分区有序

建议步骤:

  1. 梳理事件流:先画一张业务流程图,确定哪些领域事件需要发布,由谁消费。
  2. 定义事件模型:事件是领域事件,包含必要的数据(例如订单ID、用户ID)。建议使用版本策略避免字段冲突。
  3. 先本地再分布式:在单体中先用 Spring 事件验证逻辑,再迁移到消息队列。
  4. 设计重试与补偿:为每种业务设定最大重试次数和间隔;无法自动恢复的设置告警并转入死信或异常记录。
  5. 确保幂等:为消费者设计幂等机制。
  6. 监控与追踪:需要记录事件发布和消费的标记,配合链路追踪。

10. 排障清单:当你的事件没生效或异常时

症状 可能原因 检查点
监听器从未被触发 检查是否注入容器;事件类型是否匹配;监听器方法可见 日志、Bean 扫描范围
监听器被触发多次 可能有多个容器或重复消费;消息被重回队列 配置 retry/requeue
事件在事务回滚后仍被消费 使用了同步事件且发布位置在事务内 改用 @TransactionalEventListener
异步监听器不执行 缺少 @EnableAsync 或线程池配置 看是否注入自定义 Executor
事务事件监听器未触发 检查是否有事务;设置了 fallbackExecution 没有 查看调用栈
消息重复消费 消费者没有幂等处理 添加去重表或检查幂等性
死信队列不生效 队列参数未配置 DLX 借助管理界面检查队列参数

11. 面试/复盘问题

  • 在微服务架构中,如何保证两个服务数据最终一致?事件驱动和强一致分布式事务有什么区别?
  • 什么是 Spring 的事务事件?它解决了什么问题?
  • 什么情况下需要异步事件?异步事件带来了哪些新问题?
  • 事件驱动架构中,如何保证不丢消息、不重复处理?
  • 你如何实现可靠的重试机制?与死信队列结合时有什么注意点?
  • 如果使用消息队列,如何确保发送消息与本地事务的一致性?

12. 总结:回到整体框架

最后,下图概括了本文的核心设计思路:

text 复制代码
业务操作 --[发布事件]--> 事件匹配 --[事务事件?]--> 异步? --> 处理逻辑 --[失败]--> 重试 --[最终失败]--> 死信队列

决策清单:

  • 如果事件处理简单、与业务同事务,可直接用同步 @EventListener
  • 如果依赖事务提交结果,用 @TransactionalEventListener
  • 如果担心主线程阻塞,用 @Async
  • 如果需要跨服务或更可靠的持久化,将事件发布到消息队列。
  • 重试机制处理瞬时故障,死信队列让终态可见。

这篇文章从一个实际场景出发,介绍了用 Spring 事件机制实现最终一致性的完整链路。希望通过这些设计取舍,你能为自己的微服务系统选择合理的方案,并在面试或项目中从容应对。

参考资料

相关推荐
wuminyu1 小时前
Kafka利用sendfile与Page Cache实现高性能传输剖析
java·linux·c语言·jvm·c++
我命由我123451 小时前
Android 开发问题:android.permission.CAMERA...duplicated with element declared at
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Bs_MoneyMagnet1 小时前
基于springboot+vue的医院陪诊服务预约平台的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
SimonKing1 小时前
一个Docker命令,40万首古诗词API开箱即用
java·后端·程序员
青春易逝丶1 小时前
SpringDataJPA
java·spring·hibernate
某不知名網友1 小时前
C++ 深浅拷贝:从默认拷贝到 Rule of Five
java·开发语言
青山木1 小时前
Hot 100 --- 划分字母区间
java·数据结构·算法·leetcode·贪心算法
Zzzzmo_1 小时前
SpringBoot 配置文件
java·spring boot
君顾11 小时前
外卖CPS小程序开发实战指南:从零到上线的完整流程
java·开发语言·外卖