Spring Cloud Alibaba:rocketmq - 组件实践

概述

本文主要讲下rocketmq的基本使用,rocketmq是一款开源的分布式消息队列与流处理平台,起源于阿里巴巴,具备万亿级消息吞吐能力。它支持事务、顺序、延时等多种消息类型,提供灵活的消息过滤、轨迹追踪和高可用架构,是实现微服务解耦、异步通信、数据同步和流量削峰的可靠基础设施,广泛用于金融、电商、物联网等对数据一致性要求极高的场景。
参考官网地址:https://rocketmq.apache.org/zh/docs/。

一、领域模型

说明:我用自己的理解简单介绍下以下概念,更详细的可以上官网查看,

主题:是一个逻辑概念,将一组相同处理逻辑的数据归类,方便数据的隔离和后续的处理。

队列:消息发送到rocketmq服务器,实际是发送到消息队列中进行存储,是消息的最小存储单元,队列自带顺序性,且底层使用数组(ConsumeQueue)和hash结构(indexFile)存储索引位置可以对消息进行快速定位,且可以通过增加队列来实现水平扩展。

消息:就是你要传输的数据,是rocketmq中的最小传输单元,消息产生后在整个传输过程中是不能修改的,rocketMq接收到消息后会持久化,默认采用异步刷盘,主从复制的的方式持久化。消息分为:普通消息、定时/延时消息、顺序消息、事务消息。

生产者:即我们的业务系统,将业务数据按照需要封装成消息对象,按照主题投递到rocketmq服务器中。供后续消费使用。同一个生产者可以向多个主题发送消息,同一个主题也可以接收多个生成者投递的数据。

消费者分组:是一个逻辑概念,即把多个相同的业务处理逻辑的消费者归为一组,实现负载均衡。

消费者:即我们的业务系统,用来接收并处理所订阅的rocketmq服务器的消息,转成业务数据进行处理。消费者必须关联一个指定的消费者分组,以获取分组内统一定义的行为配置和消费状态。消费者分三类, PushConsumer 、SimpleConsumer 和 PullConsumer,根据不通场景选择不同的类型,详见官方对比。

订阅关系:消费者分组和主题之间的关系,即消费者订阅了哪些主题,如何过滤数据,如何根据消息的状态继续消费还是丢弃消息等。简单理解就是定义了一套消费消息的规则。

二、本地安装和启动rocketmq

参考官网文章进行部署rocketmq,同时安装rocketmq-dashboard,可以根据实际情况自行选择传统方式部署或是docker方式部署,具体可以在官网查询,这里不做赘述。官网地址如下:https://rocketmq.apache.org/zh/docs/quickStart/01quickstart
https://rocketmq.apache.org/zh/docs/deploymentOperations/04Dashboard

三、代码集成

参考代码:https://github.com/apache/rocketmq-clients/tree/master/java/client/src/main/java/org/apache/rocketmq/client/java/example。

首先,创建主题

通过rocketmq-dashboard创建四类主题:普通,定时,顺序,事务。以普通消息主题为主,消息类型选择普通,因为是测试,读写队列各设置为2个,perm是topic的读写开关,具体数值含义如下。

7 :读写+继承权限 (4+2+1)
6:读写权限 (4+2)
4 :只读权限
2 :只写权限
0 :无权限
在这里插入图片描述
在这里插入图片描述

在项目工程pom文件中引入依赖

 <dependency>
     <groupId>org.apache.rocketmq</groupId>
     <artifactId>rocketmq-client-java</artifactId>
     <version>5.1.0</version>
 </dependency>

发送和消费普通消息

普通消息为rocketmq中最基础的消息,消息本身无特殊语义,消息之间也没有任何关联。普通消息的生命周期如下,具体介绍可以到官网查看。
在这里插入图片描述

生产者service层核心代码

private final static String MY_END_POINT = "localhost:18081";
private final static String MY_CONSUMER_GROUP_01 = "myConsumerGroup01";
//normal
private final static String MY_TOPIC_NORMAL = "my-rocketmq-send-normal";
private final static String MY_TAG_NORMAL = "myTagNormal";
private final static String MY_MSG_KEY_NORMAL = "myMsgKeyNormal";

/**
* 普通消息发送 普通消息只能发送至类型为普通消息的主题中,发送的消息的类型必须和主题的类型一致。
* @param msg
*/
public void sendNormal(String msg) throws ClientException {
final ClientServiceProvider provider = ClientServiceProvider.loadService();
//设置代理,rmq-proxy.json中设置的对外暴露的端口
ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder().setEndpoints(MY_END_POINT).build();
Producer producer = provider.newProducerBuilder()
          .setClientConfiguration(clientConfiguration)
          .setTopics(MY_TOPIC_NORMAL)
          .build();
// 定义消息体
byte[] body = msg.getBytes(StandardCharsets.UTF_8);
final Message message = provider.newMessageBuilder()
                       //设置消息主题
			.setTopic(MY_TOPIC_NORMAL)
                       //消息二级分类
			.setTag(MY_TAG_NORMAL)
                      //消息Key:除消息ID外的另一种消息标记手段。
			.setKeys(MY_MSG_KEY_NORMAL)
			.setBody(body)
			.build();
try {
final SendReceipt sendReceipt = producer.send(message);
		log.info("成功发送普通消息, messageId={}", sendReceipt.getMessageId());
	} catch (Throwable t) {
		log.error("发送普通消息失败", t);
	}
}

执行,控制台打印,也可以在dashboard查看已发送的消息:
在这里插入图片描述

消费者service层核心代码

simpleConsumer消费

public void simpleConsumer() throws ClientException {
    final ClientServiceProvider provider = ClientServiceProvider.loadService();
    ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder()
            .setEndpoints(MY_END_POINT)
            .build();
    Duration awaitDuration = Duration.ofSeconds(30);
    FilterExpression filterExpression = new FilterExpression(MY_TAG_NORMAL, FilterExpressionType.TAG);
    SimpleConsumer simpleConsumer = provider.newSimpleConsumerBuilder()
            .setClientConfiguration(clientConfiguration)
            // 设置消费者组
            .setConsumerGroup(MY_CONSUMER_GROUP_01)
            // 设置长轮询的等待时长
            .setAwaitDuration(awaitDuration)
            //消费者订阅主题。
            .setSubscriptionExpressions(Collections.singletonMap(MY_TOPIC_NORMAL, filterExpression))
            .build();
    // 每次长轮询的最大消息数量。
    int maxMessageNum = 16;
    // 设置消息被接收后的不可见时长。
    Duration invisibleDuration = Duration.ofSeconds(15);
    List<MessageView> messageViewList = null;
    messageViewList = simpleConsumer.receive(maxMessageNum, invisibleDuration);
    messageViewList.forEach(messageView -> {
        if(null!=messageView){
            String msg = StandardCharsets.UTF_8.decode(messageView.getBody()).toString();
            log.info("消息体={}", msg);
            //消费处理完成后,需要主动调用ACK提交消费结果。
            try {
                simpleConsumer.ack(messageView);
            } catch (ClientException e) {
                log.error("消费消息失败:", e);
            }
        }
    });
}

pushConsumer消费

public void pushConsumer() throws ClientException, InterruptedException, IOException {
    final ClientServiceProvider provider = ClientServiceProvider.loadService();
    ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder()
            .setEndpoints(MY_END_POINT)
            .build();
    FilterExpression filterExpression = new FilterExpression(MY_TAG_NORMAL, FilterExpressionType.TAG);
     PushConsumer pushConsumer = provider.newPushConsumerBuilder()
             .setClientConfiguration(clientConfiguration)
             .setConsumerGroup(MY_CONSUMER_GROUP_01)
             .setSubscriptionExpressions(Collections.singletonMap(MY_TOPIC_NORMAL, filterExpression))
             .setMessageListener(messageView -> {
                 log.info("Consume message={}", messageView);
                 if(null!=messageView){
		      //把字节数组转换成字符串
                     String msg = StandardCharsets.UTF_8.decode(messageView.getBody()).toString();
                     log.info("消息体={}", msg);
                     return ConsumeResult.SUCCESS;
                 }else{
                     return ConsumeResult.FAILURE;
                 }
             })
             .build();
     Thread.sleep(1000);
     pushConsumer.close();
}

再次发送一条普通消息,pushConsumer断点执行结果

说明:simpleConsumer消费和pushConsumer消费的主要差异是前者灵活度高,可以根据场景自定义消费,后者是高度封装的,通过监听实现,灵活度小,更详细的信息可以在官网找到,以下只实现simpleConsumer消费。

发送和消费定时消息

定时/延时消息为rocketmq中的高级特性消息,通过指定延时时间控制消息生产后不要立即投递,而是在延时间隔后才对消费者可见。通过设置一定的定时时间可以实现分布式场景的延时调度触发效果。定时消息的生命周期,具体介绍可以到官网查看。

生产者service层核心代码

private final static String MY_TOPIC_DELAY = "my-rocketmq-send-delay";
private final static String MY_TAG_DELAY = "myTagDelay";
private final static String MY_MSG_KEY_DELAY = "myMsgKeyDelay"

public void sendDelay(String msg,int delay) throws ClientException {
        final ClientServiceProvider provider = ClientServiceProvider.loadService();
        //设置代理,rmq-proxy.json中设置的对外暴露的端口
        ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder().setEndpoints(MY_END_POINT).build();
        Producer producer = provider.newProducerBuilder()
                .setClientConfiguration(clientConfiguration)
                .setTopics(MY_TOPIC_DELAY)
                .build();
        byte[] body = msg.getBytes(StandardCharsets.UTF_8);
        Duration messageDelayTime = Duration.ofSeconds(delay);
        final Message message = provider.newMessageBuilder()
                .setTopic(MY_TOPIC_DELAY)
                .setTag(MY_TAG_DELAY)
                .setKeys(MY_MSG_KEY_DELAY)
                .setDeliveryTimestamp(System.currentTimeMillis() + messageDelayTime.toMillis())
                .setBody(body)
                .build();
        try {
            final SendReceipt sendReceipt = producer.send(message);
            log.info(DateUtils.formatDate(new Date(),"yyyy-MM-dd HH:mm:ss")+ "成功发送定时消息, messageId={},{}s后可以消费", sendReceipt.getMessageId(),delay);
        } catch (Throwable t) {
            log.error("发送定时消息失败", t);
        }
    }

消费者service层核心代码

public void simpleConsumerDelay() throws ClientException {
final ClientServiceProvider provider = ClientServiceProvider.loadService();
ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder()
			.setEndpoints(MY_END_POINT)
			.build();
Duration awaitDuration = Duration.ofSeconds(30);
FilterExpression filterExpression = new FilterExpression(MY_TAG_DELAY, FilterExpressionType.TAG);
SimpleConsumer simpleConsumer = provider.newSimpleConsumerBuilder()
			.setClientConfiguration(clientConfiguration)
                       // 设置消费者组
			.setConsumerGroup(MY_CONSUMER_GROUP_01)
                       // 设置长轮询的等待时长
			.setAwaitDuration(awaitDuration)
                       //消费者订阅主题。
			.setSubscriptionExpressions(Collections.singletonMap(MY_TOPIC_DELAY, filterExpression))
			.build();
// 每次长轮询的最大消息数量。
int maxMessageNum = 16;
// 设置消息被接收后的不可见时长。
Duration invisibleDuration = Duration.ofSeconds(15);
	List<MessageView> messageViewList = null;
	messageViewList = simpleConsumer.receive(maxMessageNum, invisibleDuration);
	messageViewList.forEach(messageView -> {
if(null!=messageView){
String msg = StandardCharsets.UTF_8.decode(messageView.getBody()).toString();
			log.info(DateUtils.formatDate(new Date(),"yyyy-MM-dd HH:mm:ss")+"消费消息,simpleConsumer,消息体={}", msg);
//消费处理完成后,需要主动调用ACK提交消费结果。
try {
				simpleConsumer.ack(messageView);
			} catch (ClientException e) {
				log.error("消费消息失败:", e);
			}
		}
	});
}

说明:官方文档指出定时消息“默认精度为1000ms,即定时消息为秒级精度”, 因此误差在1s左右是在误差范围内。同时提到,若大量消息同时触发,会导致“消息分发延迟,影响定时精度。

发送和消费顺序消息

顺序消息为rocketmq中的高级特性消息,通过消息分组MessageGroup标记一组特定消息的先后顺序,可以保证消息的投递顺序严格按照消息发送时的顺序。顺序消息的生命周期如下,具体介绍可以到官网查看。
在这里插入图片描述

消息的顺序性分为两部分,生产顺序性和消费顺序性。如需保证消息生产的顺序性,则必须满足以下条件:单一生产者,串行发送,单一生产者很好理解,串行发送则需要借助 MessageGroup实现。消费顺序性 :rocketmq5.x在消费消息时,消费者的消费行为从关联的消费组中获取,如下图,开启有序消费。
在这里插入图片描述

生产者service层核心代码

private final static String MY_TOPIC_FIFO = "my-rocketmq-send-fifo";
private final static String MY_TAG_FIFO = "myTagFifo";
private final static String MY_KEY_FIFO = "myMsgKeyFifo";
private final static String MY_MSG_GROUP_01 = "myMsgGroup01";
private final static String MY_CONSUMER_GROUP_FIFO = "myConsumerGroupFifo";

public void sendFifo(String msg) throws ClientException {
    final ClientServiceProvider provider = ClientServiceProvider.loadService();
    //设置代理,rmq-proxy.json中设置的对外暴露的端口
    ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder().setEndpoints(MY_END_POINT).build();
    Producer producer = provider.newProducerBuilder()
          .setClientConfiguration(clientConfiguration)
          .setTopics(MY_TOPIC_FIFO)
          .build();
    //模拟生成4条数据,序号依次递减,查看消费顺序是否是递减的
    for (int i = 3; i >= 0; i--) {
        String newMsg = i + msg;
        byte[] body = newMsg.getBytes(StandardCharsets.UTF_8);
        /**
         * 顺序消息的顺序关系通过消息组(MessageGroup)判定和识别,发送顺序消息时需要为每条消息设置归属的消息组,
         * 相同消息组的多条消息之间遵循先进先出的顺序关系,不同消息组、无消息组的消息之间不涉及顺序性
         */
        final Message message = provider.newMessageBuilder()
                .setTopic(MY_TOPIC_FIFO)
                .setTag(MY_TAG_FIFO)
                .setKeys(MY_KEY_FIFO)
                .setMessageGroup(MY_MSG_GROUP_01)
                .setBody(body)
                .build();
        try {
            final SendReceipt sendReceipt = producer.send(message);
            log.info("成功发送顺序消息, msg={},messageID={}", newMsg, sendReceipt.getMessageId());
            newMsg = msg;
        } catch (Throwable t) {
            log.error("发送顺序消息失败", t);
        }

    }
}

消费者service层核心代码

public void simpleConsumerFifo() throws ClientException, IOException {
    final ClientServiceProvider provider = ClientServiceProvider.loadService();
    ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder()
            .setEndpoints(MY_END_POINT)
            .build();
    Duration awaitDuration = Duration.ofSeconds(30);
    FilterExpression filterExpression = new FilterExpression(MY_TAG_FIFO, FilterExpressionType.TAG);
    SimpleConsumer simpleConsumer = provider.newSimpleConsumerBuilder()
            .setClientConfiguration(clientConfiguration)
            // 设置消费者组
            .setConsumerGroup(MY_CONSUMER_GROUP_FIFO)
            // 设置长轮询的等待时长
            .setAwaitDuration(awaitDuration)
            // 消费者订阅主题
            .setSubscriptionExpressions(Collections.singletonMap(MY_TOPIC_FIFO, filterExpression))
            .build();
    int maxMessageNum = 16;
    Duration invisibleDuration = Duration.ofSeconds(15);
    final List<MessageView> messages = simpleConsumer.receive(maxMessageNum, invisibleDuration);
    for (MessageView message : messages) {
        String msg = StandardCharsets.UTF_8.decode(message.getBody()).toString();
        try {
            simpleConsumer.ack(message);
            log.info("顺序消息消费成功,msg={}, messageId={}", msg, message.getMessageId());
        } catch (Throwable t) {
            log.error("顺序消息消费失败,msg={}, messageId={}", msg, message.getMessageId(), t);
        }
    }
}

说明:消费者类型为pushConsumer时,rocketmq保证消息按照存储顺序一条一条投递给消费者,若消费者类型为simpleConsumer,则消费者有可能一次拉取多条消息。此时,消息消费的顺序性需要由业务方自行保证。

发送和消费事务消息

事务消息为rocketmq中的高级特性消息, 分布式系统中,一个核心业务逻辑的执行,同时需要调用多个下游业务进行处理。因此,如何保证核心业务和多个下游业务的执行结果完全一致,是分布式事务需要解决的主要问题。事务消息的生命周期如下,具体介绍可以到官网查看。
在这里插入图片描述
在这里插入图片描述

基于xa协议的实现解决数据一致性问题,最大的弊端就是锁资源的范围大,性能不足;基于rocketmq普通消息的方式解决数据一致性问题,最大弊端就是无法可靠保证上下游事务的一致性;基于rockemq事务消息的方式,上游系统到消息服务器之间采用二阶段提交,可以很好的保证上下游之间的一致性

流程说明:
1、准备阶段:发送者向消息服务器发送"半消息"
2、本地事务执行:发送者执行本地业务逻辑
3、执行本地事务逻辑
4、生产者根据本地事务的结果,成功则发送"提交",服务器把半消息标记为可投递并投递给消费者,失败则发送"回滚",服务器不会将半消息投递给消费者
5、容错机制:如果消息服务器未收到确认,服务器会主动向生成者发起消息回查
6、查询最终的事务执行结果给到消息服务器
7、最终决策:与步骤4相同

生产者service层核心代码,具体可以看注释

private final static String MY_TOPIC_TX = "my-rocketmq-send-tx";
private final static String MY_TAG_TX = "myTagTx";
private final static String MY_MSG_KEY_TX = "myMsgKeyTx";

public void sendTransaction(String msg) throws ClientException {
    final ClientServiceProvider provider = ClientServiceProvider.loadService();
    //设置代理,rmq-proxy.json中设置的对外暴露的端口
    ClientConfiguration clientConfiguration = ClientConfiguration
			.newBuilder()
            .setEndpoints(MY_END_POINT)
            .build();
    //事务检查器,用来执行本地事务检查和异常事务恢复的监听器。事务检查器应该通过业务侧数据的状态来检查和判断事务消息的状态
    TransactionChecker checker = messageView -> {
        log.info("接收事务性消息检查, message={}", messageView);
        /**
         * 事务检查器一般是根据业务的ID去检查本地事务是否正确提交还是回滚,此处以订单ID属性为例。
         * 在订单表找到了这个订单,说明本地事务插入订单的操作已经正确提交;如果订单表没有订单,说明本地事务已经回滚。
         */
        final String orderId = messageView.getProperties().get("OrderId");
        if (Strings.isNullOrEmpty(orderId)) {
            // 错误的消息,直接返回Rollback。
            return TransactionResolution.ROLLBACK;
        }
        return checkOrderById(orderId) ? TransactionResolution.COMMIT : TransactionResolution.ROLLBACK;
    };
    Producer producer = provider.newProducerBuilder()
            .setClientConfiguration(clientConfiguration)
            .setTopics(MY_TOPIC_TX)
            .setTransactionChecker(checker)
            .build();
    //开启事务
    final Transaction transaction = producer.beginTransaction();
    byte[] body = msg.getBytes(StandardCharsets.UTF_8);
    final Message message = provider.newMessageBuilder()
            .setTopic(MY_TOPIC_TX)
            .setTag(MY_TAG_TX)
            .setKeys(MY_MSG_KEY_TX)
            .setBody(body)
            //设置一个本地事务关联的唯一ID,用来做本地事务回查的校验。
            .addProperty("OrderId","111")
            .build();
    try {
        final SendReceipt sendReceipt = producer.send(message,transaction);
        log.info("成功发送事务消息, messageId={}", sendReceipt.getMessageId());
    } catch (Throwable t) {
        log.error("发送事务消息失败", t);
        return;
    }
    /**
     * 执行本地事务,并确定本地事务结果。
     * 1. 如果本地事务提交成功,则提交消息事务。
     * 2. 如果本地事务提交失败,则回滚消息事务。
     * 3. 如果本地事务未知异常,则不处理,等待事务消息回查。
     *
     */
    boolean localTransactionOk = doLocalTransaction();
    if (localTransactionOk) {
        try {
            transaction.commit();
        } catch (ClientException e) {
            // 业务可以自身对实时性的要求选择是否重试,如果放弃重试,可以依赖事务消息回查机制进行事务状态的提交。
            e.printStackTrace();
        }
    } else {
        try {
            transaction.rollback();
        } catch (ClientException e) {
            // 建议记录异常信息,回滚异常时可以无需重试,依赖事务消息回查机制进行事务状态的提交。
            e.printStackTrace();
        }
    }
}
//模拟订单表查询服务,用来确认订单事务是否提交成功。
private static boolean checkOrderById(String orderId) {
    return true;
}
//模拟本地事务的执行结果。
private static boolean doLocalTransaction() {
    return true;
}

消费者与消费普通消息的代码相同,代码略。

说明: rocketmq事务消息保证本地主分支事务和下游消息发送事务的一致性,但不保证消息消费结果和上游事务的一致性。因此需要下游业务分支自行保证消息正确处理,建议消费端做好消费重试。

在Maven项目中,如果遇到`Failed to resolve dependency`错误,特别是在引入`rocketmq-spring-boot-starter`时,通常是由以下几个原因导致的: ### 1. Maven仓库配置不完整或不正确 确保项目中配置了正确的Maven仓库地址,特别是对于Spring Boot和RocketMQ相关的依赖。例如,Maven中央仓库和Spring的仓库是必需的。如果使用了私有仓库或镜像,可能需要检查其配置是否正确。 ```xml <repositories> <repository> <id>central</id> <url>https://repo.maven.apache.org/maven2</url> </repository> <repository> <id>spring-releases</id> <url>https://repo.spring.io/release</url> </repository> </repositories> ``` ### 2. 版本号未指定或版本不兼容 确保在`pom.xml`中正确指定了`rocketmq-spring-boot-starter`的版本号。如果版本号为`unknown`,说明Maven无法解析该版本,通常是因为版本号未在`<properties>`中定义或拼写错误。 ```xml <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.2.3</version> </dependency> ``` 推荐使用Spring Boot 2.x兼容的版本,如`2.2.3`或更高版本[^1]。 ### 3. 依赖冲突或版本不兼容 RocketMQ Starter可能与其他Spring Boot组件Alibaba生态的依赖(如`aliyun-java-sdk-core`)存在版本冲突。例如,`NoSuchMethodError`通常表示不同库之间存在版本不兼容问题。可以通过升级或降级相关依赖的版本来解决。 例如,如果`rocketmq-spring-boot-starter`依赖的`rocketmq-client`版本与项目中其他地方引入的版本冲突,可以显式指定其版本: ```xml <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.9.4</version> </dependency> ``` ### 4. 网络问题或仓库不可达 有时候,Maven无法解析依赖是由于网络问题导致的。可以尝试清除Maven本地仓库缓存并重新下载依赖: ```bash mvn clean install -U ``` 如果使用了私有仓库,确保网络连接正常,并且有权限访问相关仓库。 ### 5. 使用Spring Cloud Alibaba时的兼容性问题 如果项目中同时使用了`spring-cloud-starter-alibaba`,需要确保`rocketmq-spring-boot-starter`与其版本兼容。例如,Spring Cloud Alibaba 2021.x通常推荐搭配Spring Boot 2.6.x或2.7.x,并且需要确认RocketMQ Starter的版本是否适配。 可以参考以下方式统一管理版本: ```xml <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.7.12</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.5.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ``` ### 6. IDE缓存问题 有时IDE(如IntelliJ IDEA或Eclipse)的Maven插件缓存可能导致依赖解析失败。可以尝试刷新Maven项目或重新导入依赖: - 在IntelliJ中,使用`File > Invalidate Caches / Restart`来清除缓存。 - 或者在Maven项目上右键选择`Maven > Reimport`。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小马不敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值