当前位置: 首页 > news >正文

【RabbitMQ实战】邮件发送(直连交换机、手动ack)

一、实现思路

二、异常情况测试现象及解决

在这里插入图片描述

说明:本文涵盖了关于RabbitMQ很多方面的知识点, 如:
消息发送确认机制 、消费确认机制 、消息的重新投递 、消费幂等性,

二、实现思路
1.简略介绍163邮箱授权码的获取
2.编写发送邮件工具类
3.编写RabbitMQ配置文件
4.生产者发起调用
5.消费者发送邮件
6.定时任务定时拉取投递失败的消息, 重新投递
7.各种异常情况的测试验证
8.拓展: 使用动态代理实现消费端幂等性验证和消息确认(ack)

三、 代码实现

配置版本如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.atguigu.gulimall</groupId><artifactId>provider-and-consumer</artifactId><version>0.0.1-SNAPSHOT</version><name>provider-and-consumer</name><description>Demo project for Spring Boot</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>1.8</java.version><!--        <spring-cloud.version>2021.0.4</spring-cloud.version>--><spring-cloud.version>2021.0.1</spring-cloud.version></properties><dependencies><!--joda time  ? 这个还有些问题,这个类库是做什么的--><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId><version>2.10</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency><dependency><groupId>com.atguigu.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version><exclusions><exclusion><artifactId>servlet-api</artifactId><groupId>javax.servlet</groupId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency><!--什么作用? --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-collections4</artifactId><version>4.2</version></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit</artifactId><version>2.4.2</version><scope>compile</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins><resources><resource><directory>src/main/java</directory><!--所在的目录--><includes><!--包括目录下的.properties,.xml文件都会扫描到--><include>**/*.properties</include><include>**/*.xml</include></includes><filtering>false</filtering></resource></resources></build></project>

完整代码可以参考我的GitHub, https://gitee.com/zhai_jiahao/gulimall

代码实现
1.163邮箱授权码的获取, 如图:
在这里插入图片描述
每次启用授权码的时候,就会出现一行字符串,其实就是三方发送邮件的时候,使用的密码(该授权码就是配置文件spring.mail.password需要的密码)

项目结构
在这里插入图片描述

1、rabbitmq、邮箱配置:

server:port: 8023#数据源配置
spring:datasource:url: jdbc:mysql://192.168.56.10:3306/gulimall_umsusername: rootpassword: rootdriver-class-name:  com.mysql.cj.jdbc.Driver#配置nacoscloud:nacos:discovery:server-addr: 127.0.0.1:8848#配置服务名称application:name: provider-and-consumer# 配置rabbitMq 服务器#spring.application.name=rabbitmq-consumer-truerabbitmq:host: 127.0.0.1port: 5672username: guestpassword: guest#虚拟host 可以不设置,使用server默认hostvirtual-host: /publisher-returns: true  #确认消息已发送到队列(Queue)  这个在生产者模块配置 这个后期再配置,这会还用不到publisher-confirm-type: correlated   #确认消息已发送到交换机(Exchange) 这个在生产者模块配置 这个后期再配置,这会还用不到listener:  #这个在测试消费多个消息的时候,不能有下面这些配置,否则只能消费一个消息后就不继续消费了simple:acknowledge-mode: manual  #指定MQ消费者的确认模式是手动确认模式  这个在消费者者模块配置  设置手动确认(ack)prefetch: 1 #一次只能消费一条消息   这个在消费者者模块配置#配置mailmail:host: smtp.163.comusername: 15131650119@163.comfrom: 15131650119@163.compassword: GTMCFUFBTNZERDJAdefault-encoding: UTF-8properties:mail:stmp:auth: truestarttls:enable: truerequired: true#配置日志输出级别
logging:level:com.atguigu.gulimall: debug   #level 日志等级 指定命名空间的日志输出pattern:console: "%d %-5level %logger : %msg%n"file: "%d %-5level [%thread] %logger : %msg%n"file:name: d://spring/log

说明: password即授权码, username和from要一致

2、表结构

CREATE TABLE `msg_log` (`msg_id` varchar(255) NOT NULL DEFAULT '' COMMENT '消息唯一标识',`msg` text COMMENT '消息体, json格式化',`exchange` varchar(255) NOT NULL DEFAULT '' COMMENT '交换机',`routing_key` varchar(255) NOT NULL DEFAULT '' COMMENT '路由键',`status` int(11) NOT NULL DEFAULT '0' COMMENT '状态: 0投递中 1投递成功 2投递失败 3已消费',`try_count` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数',`next_try_time` datetime DEFAULT NULL COMMENT '下一次重试时间',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_time` datetime DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`msg_id`),UNIQUE KEY `unq_msg_id` (`msg_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息投递日志';select * from msg_log t order by t.create_time  desc;

说明: exchange routing_key字段是在定时任务重新投递消息时需要用到的

后面会用到的sql(设置时区使用)

#查询需要定时任务处理的数据
select msg_id, msg, exchange, routing_key, status, try_count,
next_try_time, create_time, update_time,SYSDATE(), now()  from msg_log where status = 0 and next_try_time <= now() #设置时区
SELECT @@global.time_zone;
SET GLOBAL time_zone = 'Asia/Shanghai';

3、启动类、服务接口、服务接口实现类

启动类ProviderAndConsumerApplication

package com.atguigu.gulimall.providerconsumer;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.scheduling.annotation.EnableScheduling;/*** MQ消息发送邮件功能实战(博客地址:https://blog.csdn.net/onceing/article/details/126407845)*/@EnableScheduling   //设置能使用定时任务
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.atguigu.gulimall.providerconsumer.mapper")
public class ProviderAndConsumerApplication {public static void main(String[] args) {SpringApplication.run(ProviderAndConsumerApplication.class, args);}}

4、TestController 向队列中入消息的入口

	package com.atguigu.gulimall.providerconsumer.controller;import com.atguigu.gulimall.providerconsumer.common.ServerResponse;
import com.atguigu.gulimall.providerconsumer.pojo.Mail;
import com.atguigu.gulimall.providerconsumer.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/**** 测试入库控制器类* @author: jd* @create: 2024-06-28*/@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {@Autowiredprivate TestService testService;/*** 发送邮件* @param mail 邮件对象* @param errors JSR303验证结果错误对象  ,(猜测是可以拿到验证的错误信息的用于返回校验的提示)* @return*/@PostMapping("/send")public ServerResponse sendMail(@RequestBody @Validated Mail mail, Errors errors){if(errors.hasErrors()){String defaultMessage = errors.getFieldError().getDefaultMessage();return ServerResponse.error(defaultMessage);}return testService.send(mail);}}

5、消息生产接口 TestService.java

package com.atguigu.gulimall.providerconsumer.service;import com.atguigu.gulimall.providerconsumer.common.ServerResponse;
import com.atguigu.gulimall.providerconsumer.pojo.Mail;/*** 消息生产接口*/
public interface TestService {ServerResponse testIdempotence();ServerResponse accessLimit();ServerResponse send(Mail mail);}

TestServiceImpl.java

package com.atguigu.gulimall.providerconsumer.service.impl;import com.atguigu.gulimall.providerconsumer.common.ResponseCode;
import com.atguigu.gulimall.providerconsumer.common.ServerResponse;
import com.atguigu.gulimall.providerconsumer.config.RabbitConfig;
import com.atguigu.gulimall.providerconsumer.mapper.MsgLogMapper;
import com.atguigu.gulimall.providerconsumer.mq.MessageHelper;
import com.atguigu.gulimall.providerconsumer.pojo.Mail;
import com.atguigu.gulimall.providerconsumer.pojo.MsgLog;
import com.atguigu.gulimall.providerconsumer.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.UUID;/*** 消息生产接口实现类* @author: jd* @create: 2024-06-27*/
@Service
@Slf4j
public class TestServiceImpl  implements TestService {@Autowiredprivate MsgLogMapper msgLogMapper;@Autowiredprivate RabbitTemplate rabbitTemplate;@Overridepublic ServerResponse testIdempotence() {return ServerResponse.success("testIdempotence: success");}@Overridepublic ServerResponse accessLimit() {return ServerResponse.success("accessLimit: success");}@Overridepublic ServerResponse send(Mail mail) {// 1. 生产唯一业务标识String msgId = String.valueOf(UUID.randomUUID());  //业务的唯一标识mail.setMsgId(msgId);//2.记录日志MsgLog msgLog = new MsgLog(msgId, mail, RabbitConfig.MAIL_EXCHANGE_NAME, RabbitConfig.MAIL_ROUTING_KEY_NAME);msgLogMapper.insertMsgLog(msgLog);// 消息入库  先记录日志//3.真正发送消息到MQ中CorrelationData correlationData = new CorrelationData(msgId);rabbitTemplate.convertAndSend(RabbitConfig.MAIL_EXCHANGE_NAME, RabbitConfig.MAIL_ROUTING_KEY_NAME,MessageHelper.objToMsg(mail), correlationData);// 发送消息log.info("====================>消息已发送队列");//返回公共的响应结果return ServerResponse.success(ResponseCode.MAIL_SEND_SUCCESS.getMsg());}
}

MsgLogMapper.java

package com.atguigu.gulimall.providerconsumer.mapper;import com.atguigu.gulimall.providerconsumer.batch.BatchProcessMapper;
import com.atguigu.gulimall.providerconsumer.pojo.MsgLog;
import org.apache.ibatis.annotations.Mapper;import java.util.List;/*** 日志操作mapper接口*/
@Mapper
public interface MsgLogMapper  extends BatchProcessMapper<MsgLog> {/*** 记录消息日志* @param msgLog*/void insertMsgLog(MsgLog msgLog);/*** 更新消息日志状态* @param msgLog*/void updateStatus(MsgLog msgLog);/*** 查询超时消息* @return*/List<MsgLog> selectTimeoutMsg();/*** 更新尝试的次数* @param msgLog*/void updateTryCount(MsgLog msgLog);/*** 通过主键筛选出消息日志对象* @param msgId* @return*/MsgLog selectByPrimaryKey(String msgId);}

MsgLogMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.gulimall.providerconsumer.mapper.MsgLogMapper" ><resultMap id="BaseResultMap" type="com.atguigu.gulimall.providerconsumer.pojo.MsgLog" ><id column="msg_id" property="msgId" jdbcType="VARCHAR" /><result column="msg" property="msg" jdbcType="VARCHAR" /><result column="exchange" property="exchange" jdbcType="VARCHAR" /><result column="routing_key" property="routingKey" jdbcType="VARCHAR" /><result column="status" property="status" jdbcType="INTEGER" /><result column="try_count" property="tryCount" jdbcType="INTEGER" /><result column="next_try_time" property="nextTryTime" jdbcType="TIMESTAMP" /><result column="create_time" property="createTime" jdbcType="TIMESTAMP" /><result column="update_time" property="updateTime" jdbcType="TIMESTAMP" /></resultMap><sql id="Base_Column_List" >msg_id, msg, exchange, routing_key, status, try_count, next_try_time, create_time, update_time</sql><insert id="insertMsgLog" parameterType="com.atguigu.gulimall.providerconsumer.pojo.MsgLog">INSERT INTO msg_log(msg_id, msg, exchange, routing_key, status, try_count, next_try_time, create_time, update_time)VALUES (#{msgId}, #{msg}, #{exchange}, #{routingKey}, #{status}, #{tryCount}, #{nextTryTime}, #{createTime}, #{updateTime})</insert><update id="updateStatus" parameterType="com.atguigu.gulimall.providerconsumer.pojo.MsgLog">update msg_log set status = #{status}, update_time = now()where msg_id = #{msgId}</update><select id="selectTimeoutMsg" resultMap="BaseResultMap">select <include refid="Base_Column_List"/>from msg_logwhere status = 0and next_try_time &lt;= now()</select><update id="updateTryCount">update msg_log set try_count = try_count + 1, next_try_time = #{nextTryTime}, update_time = now()where msg_id = #{msgId}</update><select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">select<include refid="Base_Column_List" />from msg_logwhere msg_id = #{msgId,jdbcType=VARCHAR}</select>
</mapper>

MsgLogService.java

package com.atguigu.gulimall.providerconsumer.service;import com.atguigu.gulimall.providerconsumer.pojo.MsgLog;import java.util.Date;
import java.util.List;/*** 日志记录接口类*/
public interface MsgLogService {void updateStatus(String msgId, Integer status);MsgLog selectByMsgId(String msgId);List<MsgLog> selectTimeoutMsg();void updateTryCount(String msgId, Date tryTime);
}

MsgLogServiceImpl.java 消息日志操作实现类

package com.atguigu.gulimall.providerconsumer.service.impl;import com.atguigu.gulimall.providerconsumer.mapper.MsgLogMapper;
import com.atguigu.gulimall.providerconsumer.pojo.MsgLog;
import com.atguigu.gulimall.providerconsumer.service.MsgLogService;
import com.atguigu.gulimall.providerconsumer.util.JodaTimeUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.Date;
import java.util.List;/*** 消息日志操作实现类* @author: jd* @create: 2024-06-27*/
@Service
public class MsgLogServiceImpl implements MsgLogService {@Autowiredprivate MsgLogMapper msgLogMapper;@Overridepublic void updateStatus(String msgId, Integer status) {MsgLog msgLog = new MsgLog();msgLog.setMsgId(msgId);msgLog.setStatus(status);msgLog.setUpdateTime(new Date());msgLogMapper.updateStatus(msgLog);}@Overridepublic MsgLog selectByMsgId(String msgId) {return msgLogMapper.selectByPrimaryKey(msgId);}@Overridepublic List<MsgLog> selectTimeoutMsg() {return msgLogMapper.selectTimeoutMsg();}@Overridepublic void updateTryCount(String msgId, Date tryTime) {//获取下一次重发发送时间,上一次发送时间 加一分钟Date nextTryTime = JodaTimeUtil.plusMinutes(tryTime, 1);//构建消息对象MsgLog msgLog = new MsgLog();msgLog.setMsgId(msgId);msgLog.setNextTryTime(nextTryTime);  //设置下一次消息重发时间msgLogMapper.updateTryCount(msgLog);}
}

通用BatchProcessMapper.java 所有的mapper可以继承的

package com.atguigu.gulimall.providerconsumer.batch;import java.util.List;/*** 通用manpper接口* @param <T>*/
public interface BatchProcessMapper<T> {void batchInsert(List<T> list);void batchUpdate(List<T> list);
}

通用manpper接口实现类 MapperProxy

package com.atguigu.gulimall.providerconsumer.batch.mapperproxy;import com.atguigu.gulimall.providerconsumer.batch.BatchProcessMapper;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;import java.util.List;import static com.atguigu.gulimall.providerconsumer.common.Constant.MAX_SIZE_PER_TIME;/*** 通用manpper接口实现类* @author: jd* @create: 2024-06-27*/
public class MapperProxy<T> implements BatchProcessMapper<T> {private BatchProcessMapper batchProcessMapper;public MapperProxy(BatchProcessMapper batchProcessMapper) {this.batchProcessMapper = batchProcessMapper;}@Overridepublic void batchInsert(List<T> list) {if (CollectionUtils.isEmpty(list)) {return;}List<List<T>> partition = Lists.partition(list, MAX_SIZE_PER_TIME);for (List<T> batchList : partition) {batchProcessMapper.batchInsert(batchList);}}@Overridepublic void batchUpdate(List<T> list) {if (CollectionUtils.isEmpty(list)) {return;}List<List<T>> partition = Lists.partition(list, MAX_SIZE_PER_TIME);for (List<T> batchList : partition) {batchProcessMapper.batchUpdate(batchList);}}}

常量类 Constant.java

package com.atguigu.gulimall.providerconsumer.common;import java.util.Arrays;
import java.util.stream.Collectors;/*** 常量 、枚举类* @author: jd* @create: 2024-06-27*/
public class Constant {public static final int MAX_SIZE_PER_TIME = 1000;public static final int INDEX_ZERO = 0;public static final int INDEX_ONE = 1;public static final int INDEX_TWO = 2;public static final int INDEX_THREE = 3;public static final int NUMBER_ZERO = 0;public static final int NUMBER_ONE = 1;public static final String COLON = ":";public static final String COMMA = ",";public static final String DOUBLE_STRIGULA = "--";public static final String REPLACEMENT_TARGET = "-99999%";public static final String UNKNOWN_TYPE = "未知类型";public interface Redis {String OK = "OK";// 过期时间, 60s, 一分钟Integer EXPIRE_TIME_MINUTE = 60;// 过期时间, 一小时Integer EXPIRE_TIME_HOUR = 60 * 60;// 过期时间, 一天Integer EXPIRE_TIME_DAY = 60 * 60 * 24;String TOKEN_PREFIX = "token:";String MSG_CONSUMER_PREFIX = "consumer:";String ACCESS_LIMIT_PREFIX = "accessLimit:";String FUND_RANK = "fundRank";String FUND_LIST = "fundList";}public interface LogType {// 登录Integer LOGIN = 1;// 登出Integer LOGOUT = 2;}/*** 相较于生产者对消息的角度来设置的此项枚举值*/public interface MsgLogStatus {// 消息投递中Integer DELIVERING = 0;// 投递成功Integer DELIVER_SUCCESS = 1;// 投递失败Integer DELIVER_FAIL = 2;// 已消费Integer CONSUMED_SUCCESS = 3;}public enum CalculateTypeEnum {ADD(1, "加"),SUBTRACT(2, "减"),MULTIPLY(3, "乘"),DIVIDE(4, "除");Integer type;String desc;CalculateTypeEnum(Integer type, String desc) {this.type = type;this.desc = desc;}public Integer getType() {return type;}public String getDesc() {return desc;}}public enum FundSortType {ASC("asc"),DESC("desc"),;private String type;FundSortType(String type) {this.type = type;}public String getType() {return type;}}
}

公共服务响应包装类【这个一般的项目中都会用到这个公共的封装】ServerResponse.java

package com.atguigu.gulimall.providerconsumer.common;import com.fasterxml.jackson.annotation.JsonIgnore;
import jdk.nashorn.internal.ir.annotations.Ignore;import java.io.Serializable;/*** 公共服务响应包装类【这个一般的项目中都会用到这个公共的封装】* @author: jd* @create: 2024-06-27*/
public class ServerResponse  implements Serializable {private static final long serialVersionUID = 7498483649536881777L;private Integer status;private String msg;private Object data;public ServerResponse() {}public ServerResponse(Integer status, String msg, Object data) {this.status = status;this.msg = msg;this.data = data;}/*** @JsonIgnore注解在Java中主要用于处理JSON序列化和反序列化过程,其具体作用如下:** 忽略属性:当在Java对象的某个属性或方法上使用@JsonIgnore注解时,该属性或方法对应的属性在序列化为JSON字符串时会被忽略,同样地,在将JSON字符串反序列化为Java对象时,该属性或方法对应的属性也不会被解析。* 当用在属性上时:表示忽略该属性的序列化和反序列化。* 当用在方法上时:表示忽略该方法对应的属性的序列化和反序列化。* 保护敏感信息:在实际应用中,@JsonIgnore注解可以用于隐藏一些敏感信息,比如密码、token等,确保这些信息不会被发送到客户端或存储在不安全的地方。* 减少数据大小:通过忽略一些不必要的属性,可以减少序列化后的JSON数据大小,提高数据传输效率。* 解决循环引用问题:当对象之间存在循环引用时,使用@JsonIgnore注解可以避免在序列化过程中出现无限递归的情况。* 提高程序的可维护性和安全性:通过精确控制哪些属性参与序列化和反序列化,可以使得程序更加健壮,减少潜在的安全风险。* 需要注意的是,@JsonIgnore注解是Jackson库提供的,因此需要确保项目中引入了Jackson库的相关依赖。同时,在使用@JsonIgnore注解时要确保被标记的属性或方法确实不需要参与序列化和反序列化,否则可能会导致意外的结果。** 总之,@JsonIgnore注解在Java对象和JSON之间的转换过程中起到了非常重要的作用,能够帮助我们更灵活地控制序列化和反序列化的行为。* @return*/@JsonIgnorepublic boolean isSuccess() {return this.status == ResponseCode.SUCCESS.getCode();}public static ServerResponse success() {return new ServerResponse(ResponseCode.SUCCESS.getCode(), null, null);}public static ServerResponse success(String msg) {return new ServerResponse(ResponseCode.SUCCESS.getCode(), msg, null);}public static ServerResponse success(Object data) {return new ServerResponse(ResponseCode.SUCCESS.getCode(), null, data);}public static ServerResponse success(String msg, Object data) {return new ServerResponse(ResponseCode.SUCCESS.getCode(), msg, data);}public static ServerResponse error(String msg) {return new ServerResponse(ResponseCode.ERROR.getCode(), msg, null);}public static ServerResponse error(Object data) {return new ServerResponse(ResponseCode.ERROR.getCode(), null, data);}public static ServerResponse error(String msg, Object data) {return new ServerResponse(ResponseCode.ERROR.getCode(), msg, data);}public Integer getStatus() {return status;}public void setStatus(Integer status) {this.status = status;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public Object getData() {return data;}public void setData(Object data) {this.data = data;}
}

服务响应状态码 大部分的服务中都会用到这个公共的状态码类 ResponseCode.java

package com.atguigu.gulimall.providerconsumer.common;/*** 服务响应状态码  大部分的服务中都会用到这个公共的状态码类*/
public enum ResponseCode {// 系统模块SUCCESS(0, "操作成功"),ERROR(1, "操作失败"),SERVER_ERROR(500, "服务器异常"),// 通用模块 1xxxxILLEGAL_ARGUMENT(10000, "参数不合法"),REPETITIVE_OPERATION(10001, "请勿重复操作"),ACCESS_LIMIT(10002, "请求太频繁, 请稍后再试"),MAIL_SEND_SUCCESS(10003, "邮件发送成功"),// 用户模块 2xxxxNEED_LOGIN(20001, "登录失效"),USERNAME_OR_PASSWORD_EMPTY(20002, "用户名或密码不能为空"),USERNAME_OR_PASSWORD_WRONG(20003, "用户名或密码错误"),USER_NOT_EXISTS(20004, "用户不存在"),WRONG_PASSWORD(20005, "密码错误"),;private Integer code;private String msg;ResponseCode(Integer code, String msg) {this.code = code;this.msg = msg;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}
}

4、工具类

时间字符操作类 JodaTimeUtil.java

package com.atguigu.gulimall.providerconsumer.util;import com.alibaba.cloud.commons.lang.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import java.util.Date;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
/*** 时间字符操作类 JodaTimeUtil* @author: jd* @create: 2024-06-27*/
@Slf4j
public class JodaTimeUtil {private static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";/*** date类型 -> string类型** @param date* @return*/public static String dateToStr(Date date) {return dateToStr(date, STANDARD_FORMAT);}/*** date类型 -> string类型** @param date* @param format 自定义日期格式* @return*/public static String dateToStr(Date date, String format) {if (date == null) {return null;}format = StringUtils.isBlank(format) ? STANDARD_FORMAT : format;DateTime dateTime = new DateTime(date);return dateTime.toString(format);}/*** string类型 -> date类型** @param timeStr* @return*/public static Date strToDate(String timeStr) {return strToDate(timeStr, STANDARD_FORMAT);}/*** string类型 -> date类型** @param timeStr* @param format  自定义日期格式* @return*/public static Date strToDate(String timeStr, String format) {if (StringUtils.isBlank(timeStr)) {return null;}format = StringUtils.isBlank(format) ? STANDARD_FORMAT : format;DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(format);DateTime dateTime;try {dateTime = dateTimeFormatter.parseDateTime(timeStr);} catch (Exception e) {log.error("strToDate error: timeStr: {}", timeStr, e);return null;}return dateTime.toDate();}/*** 判断date日期是否过期(与当前时刻比较)** @param date* @return*/public static Boolean isTimeExpired(Date date) {String timeStr = dateToStr(date);return isBeforeNow(timeStr);}/*** 判断date日期是否过期(与当前时刻比较)** @param timeStr* @return*/public static Boolean isTimeExpired(String timeStr) {if (StringUtils.isBlank(timeStr)) {return true;}return isBeforeNow(timeStr);}/*** 判断timeStr是否在当前时刻之前** @param timeStr* @return*/private static Boolean isBeforeNow(String timeStr) {DateTimeFormatter format = DateTimeFormat.forPattern(STANDARD_FORMAT);DateTime dateTime;try {dateTime = DateTime.parse(timeStr, format);} catch (Exception e) {log.error("isBeforeNow error: timeStr: {}", timeStr, e);return null;}return dateTime.isBeforeNow();}/*** 日期加天数** @param date* @param days* @return*/public static Date plusDays(Date date, int days) {return plusOrMinusDays(date, days, 0);}/*** 日期减天数** @param date* @param days* @return*/public static Date minusDays(Date date, int days) {return plusOrMinusDays(date, days, 1);}/*** 加减天数** @param date* @param days* @param type 0:加天数 1:减天数* @return*/private static Date plusOrMinusDays(Date date, int days, Integer type) {if (null == date) {return null;}DateTime dateTime = new DateTime(date);if (type == 0) {dateTime = dateTime.plusDays(days);} else {dateTime = dateTime.minusDays(days);}return dateTime.toDate();}/*** 日期加分钟** @param date* @param minutes* @return*/public static Date plusMinutes(Date date, int minutes) {return plusOrMinusMinutes(date, minutes, 0);}/*** 日期减分钟** @param date* @param minutes* @return*/public static Date minusMinutes(Date date, int minutes) {return plusOrMinusMinutes(date, minutes, 1);}/*** 加减分钟** @param date* @param minutes* @param type    0:加分钟 1:减分钟* @return*/private static Date plusOrMinusMinutes(Date date, int minutes, Integer type) {if (null == date) {return null;}DateTime dateTime = new DateTime(date);if (type == 0) {dateTime = dateTime.plusMinutes(minutes);} else {dateTime = dateTime.minusMinutes(minutes);}return dateTime.toDate();}/*** 日期加月份** @param date* @param months* @return*/public static Date plusMonths(Date date, int months) {return plusOrMinusMonths(date, months, 0);}/*** 日期减月份** @param date* @param months* @return*/public static Date minusMonths(Date date, int months) {return plusOrMinusMonths(date, months, 1);}/*** 加减月份** @param date* @param months* @param type   0:加月份 1:减月份* @return*/private static Date plusOrMinusMonths(Date date, int months, Integer type) {if (null == date) {return null;}DateTime dateTime = new DateTime(date);if (type == 0) {dateTime = dateTime.plusMonths(months);} else {dateTime = dateTime.minusMonths(months);}return dateTime.toDate();}/*** 判断target是否在开始和结束时间之间** @param target* @param startTime* @param endTime* @return*/public static Boolean isBetweenStartAndEndTime(Date target, Date startTime, Date endTime) {if (null == target || null == startTime || null == endTime) {return false;}DateTime dateTime = new DateTime(target);return dateTime.isAfter(startTime.getTime()) && dateTime.isBefore(endTime.getTime());}
}

Object 和String互转类 JsonUtil

package com.atguigu.gulimall.providerconsumer.util;import com.alibaba.cloud.commons.lang.StringUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;import java.text.SimpleDateFormat;/*** Object 和String互转类* @author: jd* @create: 2024-06-27*/
@Slf4j
public class JsonUtil {private static ObjectMapper objectMapper = new ObjectMapper();private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";static {// 对象的所有字段全部列入objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);// 取消默认转换timestamps形式objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);// 忽略空bean转json的错误objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);// 统一日期格式objectMapper.setDateFormat(new SimpleDateFormat(DATE_FORMAT));// 忽略在json字符串中存在, 但在java对象中不存在对应属性的情况, 防止错误objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);}/*** 将Object转化为String对象* @param obj* @param <T>* @return*/public static <T> String objToStr(T obj) {if (null == obj) {return null;}try {return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);} catch (Exception e) {log.warn("objToStr error: ", e);return null;}}/*** 将字符串转化成Object对象* @param str   待转的字符串* @param clazz 类名* @param <T>* @return*/public static <T> T strToObj(String str, Class<T> clazz) {if (StringUtils.isBlank(str) || null == clazz) {return null;}try {return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);} catch (Exception e) {log.warn("strToObj error: ", e);return null;}}public static <T> T strToObj(String str, TypeReference<T> typeReference) {if (StringUtils.isBlank(str) || null == typeReference) {return null;}try {return (T) (typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));} catch (Exception e) {log.error("strToObj error", e);return null;}}
}

发送邮件工具类 MailUtil.java

package com.atguigu.gulimall.providerconsumer.util;import com.atguigu.gulimall.providerconsumer.pojo.Mail;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;/**** 发送邮件工具类* @author: jd* @create: 2024-06-27*/@Component
@Slf4j
public class MailUtil {@Value("${spring.mail.from}")    //这里从application.xml中拿不到配置信息,所以从这里直接写死了private String from ="15131650119@163.com";@Autowiredprivate JavaMailSender mailSender;public boolean send(Mail mail) throws AddressException {//模拟消费成功,但是业务实际没成功,此时会重新入队列,不会造成消息丢失
//        if(true){
//            return false;
//        }String to = mail.getTo();// 目标邮箱String title = mail.getTitle();// 邮件标题String content = mail.getContent();// 邮件正文SimpleMailMessage message = new SimpleMailMessage();message.setFrom(String.valueOf(new InternetAddress(from)));  //设置发送人message.setTo(to);  //设置目标账户message.setSubject(title); //设置邮件标题message.setText(content);  //设置邮件内容try {log.info("===================>开始发送邮件");mailSender.send(message);log.info("===================>邮件发送成功");return true;} catch (MailException e) {log.error("=============>邮件发送失败, to: {}, title: {}", to, title, e);return false;}}}

SpringBeanUtil.java 获取BeanSpring容器类

package com.atguigu.gulimall.providerconsumer.util;import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;/*** @author: jd* @create: 2024-06-27*/
@Component
public class SpringBeanUtil implements ApplicationContextAware {private static ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext)throws BeansException {SpringBeanUtil.applicationContext = applicationContext;}/*** 通过名称在spring容器中获取对象** @param beanName* @return*/public static Object getBean(String beanName) {System.out.println(applicationContext);return applicationContext.getBean(beanName);}}

5、RabbitMQ消费者、生产者配置类

A、MQ生产者:

TestController.java

package com.atguigu.gulimall.providerconsumer.service.impl;import com.atguigu.gulimall.providerconsumer.common.ResponseCode;
import com.atguigu.gulimall.providerconsumer.common.ServerResponse;
import com.atguigu.gulimall.providerconsumer.config.RabbitConfig;
import com.atguigu.gulimall.providerconsumer.mapper.MsgLogMapper;
import com.atguigu.gulimall.providerconsumer.mq.MessageHelper;
import com.atguigu.gulimall.providerconsumer.pojo.Mail;
import com.atguigu.gulimall.providerconsumer.pojo.MsgLog;
import com.atguigu.gulimall.providerconsumer.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.UUID;/*** 消息生产接口实现类* @author: jd* @create: 2024-06-27*/
@Service
@Slf4j
public class TestServiceImpl  implements TestService {@Autowiredprivate MsgLogMapper msgLogMapper;@Autowiredprivate RabbitTemplate rabbitTemplate;@Overridepublic ServerResponse testIdempotence() {return ServerResponse.success("testIdempotence: success");}@Overridepublic ServerResponse accessLimit() {return ServerResponse.success("accessLimit: success");}@Overridepublic ServerResponse send(Mail mail) {// 1. 生产唯一业务标识String msgId = String.valueOf(UUID.randomUUID());  //业务的唯一标识mail.setMsgId(msgId);//2.记录日志MsgLog msgLog = new MsgLog(msgId, mail, RabbitConfig.MAIL_EXCHANGE_NAME, RabbitConfig.MAIL_ROUTING_KEY_NAME);msgLogMapper.insertMsgLog(msgLog);// 消息入库  先记录日志//3.真正发送消息到MQ中CorrelationData correlationData = new CorrelationData(msgId);rabbitTemplate.convertAndSend(RabbitConfig.MAIL_EXCHANGE_NAME, RabbitConfig.MAIL_ROUTING_KEY_NAME,MessageHelper.objToMsg(mail), correlationData);// 发送消息log.info("====================>消息已发送队列");//返回公共的响应结果return ServerResponse.success(ResponseCode.MAIL_SEND_SUCCESS.getMsg());}
}

队列 交换机配置,用于消息生产者:RabbitConfig.java

package com.atguigu.gulimall.providerconsumer.config;import com.atguigu.gulimall.providerconsumer.common.Constant;
import com.atguigu.gulimall.providerconsumer.service.MsgLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;/**** 队列 交换机配置,用于消息生产者* @author: jd* @create: 2024-06-27*/@Slf4j
@Component
@Configuration
public class RabbitConfig {@Autowiredprivate MsgLogService msgLogService;// 发送邮件public static final String MAIL_QUEUE_NAME = "mail.queue";public static final String MAIL_EXCHANGE_NAME = "mail.exchange";public static final String MAIL_ROUTING_KEY_NAME = "mail.routing.key";@Beanpublic Queue mailQueue() {return new Queue(MAIL_QUEUE_NAME, true);}@Beanpublic DirectExchange mailExchange() {return new DirectExchange(MAIL_EXCHANGE_NAME, true, false);}@Beanpublic Binding mailBinding() {return BindingBuilder.bind(mailQueue()).to(mailExchange()).with(MAIL_ROUTING_KEY_NAME);}//    @Autowired
//    private CachingConnectionFactory connectionFactory;//    ConnectionFactory connectionFactory = (ConnectionFactory) SpringBeanUtil.getBean("connectionFactory");/*** 设置生产者消息确认回调函数**/@Beanpublic RabbitTemplate createRabbitTemplate(ConnectionFactory  connectionFactory){RabbitTemplate rabbitTemplate = new RabbitTemplate();rabbitTemplate.setConnectionFactory(connectionFactory);rabbitTemplate.setMandatory(true);rabbitTemplate.setMessageConverter(converter());rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {@Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {if (ack) {log.info("消息成功发送到Exchange");String msgId = correlationData.getId();msgLogService.updateStatus(msgId, Constant.MsgLogStatus.DELIVER_SUCCESS);} else {log.info("消息发送到Exchange失败, {}, cause: {}", correlationData, cause);}System.out.println("ConfirmCallback回调:     "+"相关数据:"+correlationData);System.out.println("ConfirmCallback回调:     "+"确认情况:"+ack);System.out.println("ConfirmCallback回调:     "+"原因:"+cause);}});rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {@Overridepublic void returnedMessage(ReturnedMessage returnedMessage) {System.out.println("ReturnCallback回调:     "+"消息:"+returnedMessage.getMessage());System.out.println("ReturnCallback回调:     "+"回应码:"+returnedMessage.getReplyCode());System.out.println("ReturnCallback回调:     "+"回应信息:"+returnedMessage.getReplyText());System.out.println("ReturnCallback回调:     "+"交换机:"+returnedMessage.getExchange());System.out.println("ReturnCallback回调:     "+"路由键:"+returnedMessage.getRoutingKey());log.info("消息从Exchange路由到Queue失败: exchange: {}, route: {}, replyCode: {}, replyText: {}, message: {}",returnedMessage.getExchange(),returnedMessage.getRoutingKey(),returnedMessage.getReplyCode(),returnedMessage.getReplyText(),returnedMessage.getMessage());}});return rabbitTemplate;}@Beanpublic Jackson2JsonMessageConverter converter() {return new Jackson2JsonMessageConverter();}}
B、MQ 消费者 其实就完成了3件事: 1.保证消费幂等性, 2.发送邮件, 3.更新消息状态, 手动ack
package com.atguigu.gulimall.providerconsumer.mq.consumer;import com.atguigu.gulimall.providerconsumer.common.Constant;
import com.atguigu.gulimall.providerconsumer.config.RabbitConfig;
import com.atguigu.gulimall.providerconsumer.mq.MessageHelper;
import com.atguigu.gulimall.providerconsumer.pojo.Mail;
import com.atguigu.gulimall.providerconsumer.pojo.MsgLog;
import com.atguigu.gulimall.providerconsumer.service.MsgLogService;
import com.atguigu.gulimall.providerconsumer.util.JsonUtil;
import com.atguigu.gulimall.providerconsumer.util.MailUtil;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.mail.internet.AddressException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;/*** MQ 监听者,操作业务(发送邮件)* 其实就完成了3件事:*      1.保证消费幂等性, 2.发送邮件, 3.更新消息状态, 手动ack* @author: jd* @create: 2024-06-27*/
@Component
@Slf4j
@RabbitListener(queues = RabbitConfig.MAIL_QUEUE_NAME)  //指定监听队列
public class MailConsumer {@Autowiredprivate MsgLogService msgLogService;@Autowiredprivate MailUtil mailUtil;@RabbitHandler(isDefault = true)   //指定监听后的处理动作public void consume(Message message, Channel channel) throws IOException, AddressException {//将Message中的业务数据转化成Mail对象Mail mail = MessageHelper.msgToObj(message, Mail.class);log.info("================>消费者收到消息: {}", mail.toString());log.debug("=========测试debug和info有什么区别======");//根据ID查询Msg对象String msgId = mail.getMsgId();MsgLog msgLog = msgLogService.selectByMsgId(msgId);// 消费幂等性if (null == msgLog || msgLog.getStatus().equals(Constant.MsgLogStatus.CONSUMED_SUCCESS)) {log.info("===========>消费者重复消费,此时不进行消费 ,msgId: {}", msgId);//直接终止程序运行,程序返回return;}//拿到MQ中的每一条消息的唯一标识TagMessageProperties properties = message.getMessageProperties();long tag = properties.getDeliveryTag();//业务操作:发送邮件log.info("================>准备发送邮件");boolean send = mailUtil.send(mail);
//try {//如果发送邮件成功,则修改消息状态为 已消费if(send){//发送成功后更新消息日志表的消息记录状态msgLogService.updateStatus(msgId, Constant.MsgLogStatus.CONSUMED_SUCCESS);//取得进程IDThread t = Thread.currentThread();log.info("【消息队列】current request consumer success, request info: {}; thread info: {};", JsonUtil.objToStr(mail), t);// 消费确认,设置反馈给MQchannel.basicAck(tag, false);}else {log.error("【消息队列】consumer failed,, msg info: {}", JsonUtil.objToStr(mail));channel.basicNack(tag, false, true);  //这样会告诉rabbitmq该消息消费失败, 需要重新入队, 可以重新投递到其他正常的消费端进行消费, 从而保证消息不被丢失}} catch (Exception e) {//产生异常之后,则不消费,直接拒绝此消息,不进行消费;这样会导致这条失败的消息会一直存在队列里面,然后定时任务过一会在数据库中扫到这个信息之后,会再去MQ中拿这个消息进行消费e.printStackTrace();ByteArrayOutputStream bass = new ByteArrayOutputStream();e.printStackTrace(new PrintStream(bass));log.error("【消息队列】consumer error, error info: {}, msg info: {}", bass, JsonUtil.objToStr(mail));channel.basicNack(tag, false, true);}}}

6、定时任务重发: ResendMsg.java (说明: 每一条消息都和exchange routingKey绑定, 所有消息重投共用这一个定时任务即可)

package com.atguigu.gulimall.providerconsumer.task;import com.atguigu.gulimall.providerconsumer.common.Constant;
import com.atguigu.gulimall.providerconsumer.config.RabbitConfig;
import com.atguigu.gulimall.providerconsumer.mq.MessageHelper;
import com.atguigu.gulimall.providerconsumer.pojo.MsgLog;
import com.atguigu.gulimall.providerconsumer.service.MsgLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Correlation;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.util.List;/*** 消息重发定时任务* @author: jd* @create: 2024-06-28*/
@Component
@Slf4j
public class ResendMsg {@Autowiredprivate RabbitTemplate rabbitTemplate;// 最大投递次数。第四次投递失败private static final int MAX_TRY_COUNT = 3;@Autowiredprivate MsgLogService msgLogService;/*** 每30s拉取投递失败的消息, 重新投递*/@Scheduled(cron = "0/30 * * * * ?")public void reSend(){log.info("开始执行定时任务(重新投递消息)");List<MsgLog> msgLogs = msgLogService.selectTimeoutMsg();  //查询还在投递中的消息msgLogs.forEach(msgLog->{String msgId = msgLog.getMsgId();//超过投递次数则不会重新投递中的消息是否需要投递if(msgLog.getTryCount()>=MAX_TRY_COUNT){//不需要重新投递msgLogService.updateStatus(msgId, Constant.MsgLogStatus.DELIVER_FAIL);log.info("消息ID {}超过最大的投递次数 {} 次,投递失败,需要人工查看!",msgId,MAX_TRY_COUNT);}else {//拿到消息在表中的本次重试时间,去获取下一次重试时间  同时 投递次数+1msgLogService.updateTryCount(msgId,msgLog.getNextTryTime());CorrelationData correlationData = new CorrelationData(msgId);//携带业务信息,作为业务的唯一标识//重新发送消息到MQ,让MQ去重新尝试消费这一条之前没有发送到MQ的消息(因为我们现在查的消息的状态是status =0 的代表是消息还是投递中的,没有变成投递成功的消息,肯定是投递有问题)rabbitTemplate.convertAndSend(RabbitConfig.MAIL_EXCHANGE_NAME,RabbitConfig.MAIL_ROUTING_KEY_NAME,  //每一条消息都和exchange routingKey绑定, 所有消息重投共用这一个定时任务即可MessageHelper.objToMsg(msgLog),correlationData);log.info("第 " + (msgLog.getTryCount() + 1) + " 次重新投递消息");}});log.info("定时任务执行结束(重新投递消息)");  //}}

四、基本测试

OK, 目前为止, 代码准备就绪, 现在进行正常流程的测试 1.发送请求:
在这里插入图片描述
后台日志:
在这里插入图片描述
3.库消息记录:
在这里插入图片描述
状态为3, 表明已消费, 消息重试次数为0, 表明一次投递就成功了,此时就可以到目标邮箱中去查看是否接收到了这个邮件

五、异常情况测试

1.验证消息发送到Exchange失败情况下的回调, 对应上图P -> X

如何验证? 可以随便指定一个不存在的交换机名称, 请求接口, 看是否会触发回调
在这里插入图片描述
发送失败, 原因: reply-code=404, reply-text=NOT_FOUND - no exchange ‘mail.exchangeabcd’ in vhost ‘/’, 该回调能够保证消息正确发送到Exchange, 测试完成

2.验证消息从Exchange路由到Queue失败情况下的回调, 对应上图X -> Q 同理, 修改一下路由键为不存在的即可, 路由失败, 触发回调
在这里插入图片描述
发送失败, 原因: route: mail.routing.keyabcd, replyCode: 312, replyText: NO_ROUTE

3.验证在手动ack模式下, 消费端必须进行手动确认(ack), 否则消息会一直保存在队列中, 直到被消费, 对应上图Q -> C 将消费端代码channel.basicAck(tag, false);// 消费确认注释掉, 查看控制台和rabbitmq管控台
在这里插入图片描述
在这里插入图片描述
可以看到, 虽然消息确实被消费了, 但是由于是手动确认模式, 而最后又没手动确认, 所以, 消息仍被rabbitmq保存, 所以, 手动ack能够保证消息一定被消费, 但一定要记得basicAck

4.验证消费端幂等性 接着上一步, 去掉注释, 重启服务器, 由于有一条未被ack的消息, 所以重启后监听到消息, 进行消费, 但是由于消费前会判断该消息的状态是否未被消费, 发现status=3, 即已消费, 所以, 直接return, 这样就保证了消费端的幂等性, 即使由于网络等原因投递成功而未触发回调, 从而多次投递, 也不会重复消费进而发生业务异常
在这里插入图片描述

5.验证消费端发生异常消息也不会丢失 很显然, 消费端代码可能发生异常, 如果不做处理, 业务没正确执行, 消息却不见了, 给我们感觉就是消息丢失了, 由于我们消费端代码做了异常捕获, 业务异常时, 会触发: channel.basicNack(tag, false, true);, 这样会告诉rabbitmq该消息消费失败, 需要重新入队, 可以重新投递到其他正常的消费端进行消费, 从而保证消息不被丢失 测试: send方法直接返回false即可(这里跟抛出异常一个意思),因为我们向MQ插入了消息,但是实际业务消费了,但是发送邮件返回了false,这样会从新投递到MQ队列中,再进行消费,一直重复。
代码修改:
在这里插入图片描述
结果:
在这里插入图片描述

可以看到, 由于channel.basicNack(tag, false, true), 未被ack的消息(unacked)会重新入队并被消费, 这样就保证了消息不会走丢

6.验证定时任务的消息重投 实际应用场景中, 可能由于网络原因, 或者消息未被持久化MQ就宕机了, 使得投递确认的回调方法ConfirmCallback没有被执行, 从而导致数据库该消息状态一直是投递中的状态, 此时就需要进行消息重投, 即使也许消息已经被消费了 定时任务只是保证消息100%投递成功, 而多次投递的消费幂等性需要消费端自己保证 我们可以将回调和消费成功后更新消息状态的代码注释掉, 开启定时任务, 查看是否重投

这是没有异常信息的情况下,定时任务每次都不会做实际的业务:
在这里插入图片描述
当我们对一条消息,进行了实际的业务处理,而且也业务处理成功了,只是没有把状态修改成成功,这样定时任务会扫,重新入队列,但是有幂等性校验,所以一直发送到队列将这条信息,直到3次后,消息会被更新为发送失败
在这里插入图片描述

在这里插入图片描述

发送邮件其实很简单, 但深究起来其实有很多需要注意和完善的点, 一个看似很小的知识点, 也可以引申出很多问题, 甚至涉及到方方面面, 这些都需要自己踩坑, 当然我这代码肯定还有很多不完善和需要优化的点, 希望小伙伴多多提意见和建议 我的代码都是经过自测验证过的, 图也都是一点一点自己画的或认真截的, 希望小伙伴能学到一点东西, 路过的点个赞或点个关注呗, 谢谢

部分参考:springboot + rabbitmq发送邮件实战(保证消息100%投递成功并被消费)

相关文章:

【RabbitMQ实战】邮件发送(直连交换机、手动ack)

一、实现思路 二、异常情况测试现象及解决 说明:本文涵盖了关于RabbitMQ很多方面的知识点, 如: 消息发送确认机制 、消费确认机制 、消息的重新投递 、消费幂等性, 二、实现思路 1.简略介绍163邮箱授权码的获取 2.编写发送邮件工具类 3.编写RabbitMQ配置文件 4.生产者发起调用…...

python 笔试面试八股(自用版~)

1 解释型和编译型语言的区别 解释是翻译一句执行一句&#xff0c;更灵活&#xff0c;eg&#xff1a;python; 解释成机器能理解的指令&#xff0c;而不是二进制码 编译是整个源程序编译成机器可以直接执行的二进制可运行的程序&#xff0c;再运行这个程序 比如c 2 简述下 Pyth…...

《SpringBoot+Vue》Chapter04 SpringBoot整合Web开发

返回JSON数据 默认实现 依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>在springboot web依赖中加入了jackson-databind作为JSON处理器 创建一个实体类对象…...

腾讯地图异步调用

<template><!-- 定义地图显示容器 --><div id"container"></div> </template><script setup>import { onMounted } from vue;const mapKeys import.meta.env.VITE_GLOB_TX_MAP_KEYS;function initMap() {// //定义地图中心点坐…...

通过docker overlay2 目录名查找占用磁盘空间最大的容器名和容器ID

有时候经常会有个别容器占用磁盘空间特别大&#xff0c; 这个时候就需要通过docker overlay2 目录名查找占用磁盘空间最大的容器名和容器ID&#xff1a; 1、 首先进入到 /var/lib/docker/overlay2 目录下,查看谁占用的较多 [rootPPS-97-8-ALI-HD1H overlay2]# cd /var/lib/doc…...

每周算法:有向图强连通分量

题目链接 受欢迎的牛 题目描述 每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶牛都是自恋狂&#xff0c;每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果 A A A 喜欢 B B B&#xff0c; B B B 喜欢 C C C&#xff0c;那…...

Python习题 053:在逻辑值检测时会被认为是真值的是?

...

基于RackNerd + CentOS 7 64 Bit + aaPanel 的那些事

本文涉及以下几个站点&#xff1a; RackNerd - Introducing Infrastructure Stability NameSilo - https://www.namesilo.com/ aaPanel - https://www.aapanel.com/ 遇到错误 Cannot find a valid baseurl for repo: base/7/x86_64 解决办法 一、切换 yum源 首先可以去…...

大数据期末复习——hadoop、hive等基础知识

一、题型分析 1、Hadoop环境搭建 2、hadoop的三大组件 HDFS&#xff1a;NameNode&#xff0c;DataNode&#xff0c;SecondaryNameNode YARN&#xff1a;ResourceManager&#xff0c;NodeManager &#xff08;Yarn的工作原理&#xff09; MapReduce&#xff1a;Map&#xff0…...

什么是客户体验自动化?

客户体验自动化是近年来在企业界备受关注的一个概念。那么&#xff0c;究竟什么是客户体验自动化呢&#xff1f;本文将为您详细解析这一话题&#xff0c;帮助您更好地理解并应用客户体验自动化。 我们要先明确什么是客户体验。客户体验是指客户在使用产品或服务过程中的感受和体…...

高效除氟:探索CH-87up树脂在氟化工废水处理中的应用

摘要 本研究旨在评估Tulsimer CH-87up树脂针对经钙镁预处理后的氟化工废水的深度处理效果。实验结果显示&#xff0c;CH-87up树脂能显著降低废水中的氟离子浓度&#xff0c;从43.4mg/L降至0.34mg/L&#xff0c;远低于行业排放标准的5mg/L。此外&#xff0c;该树脂表现出卓越的…...

【Git】LFS

什么是lfs Git 是分布式 版本控制系统&#xff0c;这意味着在克隆过程中会将仓库的整个历史记录传输到客户端。对于包涵大文件&#xff08;尤其是经常被修改的大文件&#xff09;的项目&#xff0c;初始克隆需要大量时间&#xff0c;因为客户端会下载每个文件的每个版本**。Gi…...

隐式转换的魔法:Scala中隐式转换的深度解析

隐式转换的魔法&#xff1a;Scala中隐式转换的深度解析 在Scala编程语言的丰富特性中&#xff0c;隐式转换是一个强大而微妙的工具。它允许开发者在不改变现有代码的情况下&#xff0c;扩展或修改类的行为。本文将深入探讨Scala中隐式转换的工作原理&#xff0c;并通过详细的代…...

外贸企业选择什么网络?

随着全球化的深入发展&#xff0c;越来越多的国内企业将市场拓展到海外。为了确保外贸业务的顺利进行&#xff0c;企业需要建立一个稳定、安全且高速的网络。那么&#xff0c;外贸企业应该选择哪种网络呢&#xff1f;本文将为您详细介绍。 外贸企业应选择什么网络&#xff1f; …...

Redis 7.x 系列【14】数据类型之流(Stream)

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Redis 版本 7.2.5 源码地址&#xff1a;https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 概述2. 常用命令2.1 XADD2.2 XRANGE2.3 XREVRANGE2.4 XDEL2.5 XLEN2.6 XREAD2.7 XG…...

(四)opengl函数加载和错误处理

#include <glad/glad.h>//glad必须在glfw头文件之前包含 #include <GLFW/glfw3.h> #include <iostream>void frameBufferSizeCallbakc(GLFWwindow* window, int width, int height) {glViewport(0, 0, width, height);std::cout << width << &qu…...

RuoYi-Vue3不启动后端服务如何登陆?

RuoYi-Vue3不启动后端服务如何登陆?RuoYi-Vue3使用的前端技术栈 是:Vue3 + Element Plus + Vite。 github开源地址:https://github.com/yangzongzhuan/RuoYi-Vue3 前后的分离在线演示项目地址:https://vue.ruoyi.vip/ 这种方式是用若依提供的在线后端接口,可以在此基础上修…...

Typora(跨平台 Markdown 编辑器 )正版值得购买吗

Typora 是一款桌面 Markdown 编辑器&#xff0c;作为国人开发的优秀软件&#xff0c;一直深受用户的喜爱。 实时预览格式 Typora 是一款适配 Windows / macOS / Linux 平台的 Markdown 编辑器&#xff0c;编辑实时预览标记格式&#xff0c;所见即所得&#xff0c;轻巧而强大…...

springboot个人证书管理系统-计算机毕业设计源码16679

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了个人证书管理系统的开发全过程。通过分析个人证书管理系统管理的不足&#xff0c;创建了一个计算机管理个人证书管理系统的方案。文章介绍了个人证书管理系统的系…...

读-改-写操作

1 什么是读-改-写操作 “读-改-写”&#xff08;Read-Modify-Write&#xff0c;简称RMW&#xff09;是一种常见的操作模式&#xff0c;它通常用于需要更新数据的场景。 这个模式包含三个基本步骤&#xff1a; 1.读&#xff08;Read&#xff09;&#xff1a;首先读取当前的数据…...

海外仓系统应用教程:解决了小型海外仓哪些问题

大型海外仓通过对海外仓WMS系统的使用&#xff0c;大大提升了业务流程的效率和利润率。这也给很多小型海外仓造成了误区&#xff0c;觉得海外仓系统就是为大型海外仓设计的。其实小型海外仓对海外仓系统的需求同样强烈&#xff0c;现在也有很多专门转对中小型海外仓设计的WMS系…...

shell 脚本编程

简介&#xff1a; 用户通过shell向计算机发送指令的计算机通过shell给用户返回指令的执行结果 通过shell编程可以达到的效果 提高工作效率可以实现自动化 需要学习的内容&#xff1a; linuxshell的语法规范 编写shell的流程 第一步&#xff1a;用vi/vim创建一个.sh的文件…...

gin参数验证

一. 结构体验证 用gin框架的数据验证&#xff0c;可以不用解析数据&#xff0c;减少if else。如下面的代码&#xff0c;如果需要增加判断条件&#xff0c;就需要增加if或者if else。 type MyApi struct {a intb string }func checkMyApi(val *MyApi) bool {if val.a 0 {retur…...

【web3】分享一个web入门学习平台-HackQuest

前言 一直想进入web3行业&#xff0c;但是没有什么途径&#xff0c;偶然在电鸭平台看到HackQuest的共学营&#xff0c;发现真的不错&#xff0c;并且还接触到了黑客松这种形式。 链接地址&#xff1a;HackQuest 平台功能 学习路径&#xff1a;平台有完整的学习路径&#xff…...

Sectigo或RapidSSL DV通配符SSL证书哪个性价比更高?

在当前的网络安全领域&#xff0c;选择一款合适的SSL证书对于保护网站和用户数据至关重要。Sectigo和RapidSSL作为市场上知名的SSL证书提供商&#xff0c;以其高性价比和快速的服务响应而受到市场的青睐。本文将对Sectigo和RapidSSL DV通配符证书进行深入对比&#xff0c;帮助用…...

金蝶云星空字段之间连续触发值更新

文章目录 金蝶云星空字段之间连续触发值更新场景说明具体需求&#xff1a;解决方案 金蝶云星空字段之间连续触发值更新 场景说明 字段A配置了字段B的计算公式&#xff0c;字段B配置了自动C的计算公式&#xff0c;修改A的时候&#xff0c;触发了B的重算&#xff0c;但是C触发不…...

Python 获取字典中的值(八种方法)

Python 字典(dictionary)是一种可变容器模型&#xff0c;可以存储任意数量的任意类型的数据。字典通常用于存储键值对&#xff0c;每个元素由一个键&#xff08;key&#xff09;和一个值(value&#xff09;组成&#xff0c;键和值之间用冒号分隔。 以下是 Python 字典取值的几…...

Day49

Day49 代理模式proxy 概念&#xff1a; 代理(Proxy)是一种设计模式&#xff0c;提供了对目标对象另外的访问方式&#xff0c;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 代理模式分为静态代理和动态代理…...

OpenCV 车牌检测

OpenCV 车牌检测 级联分类器算法流程车牌检测相关链接 级联分类器 假设我们需要识别汽车图像中车牌的位置&#xff0c;利用深度学习目标检测技术可以采取基于锚框的模型&#xff0c;但这需要在大量图像上训练模型。 但是&#xff0c;级联分类器可以作为预训练文件直接使用&…...

机器学习/pytorch笔记:time2vec

1 概念部分 对于给定的标量时间概念 t&#xff0c;Time2Vec 的表示 t2v(t)是一个大小为 k1的向量&#xff0c;定义如下&#xff1a; 其中&#xff0c;t2v(t)[i]是 t2v(t)的第 i 个元素&#xff0c;F是一个周期性激活函数&#xff0c;ω和 ϕ是可学习的参数。 以下是个人理解&am…...

降低开关电源噪声的设计总结

开关电源的特征就是产生强电磁噪声&#xff0c;若不加严格控制&#xff0c;将产生极大的干扰。下面介绍的技术有助于降低开关电源噪声&#xff0c;能用于高灵敏度的模拟电路。 电路和器件的选择 一个关键点是保持dv/dt和di/dt在较低水平&#xff0c;有许多电路通过减小dv/dt和…...

rust嵌入式开发2024

老的rust embedded book 其实过时了. 正确的姿势是embassy 入手. 先说下以前rust写嵌入怎么教学小白的. 第一步,从这里 svd2rust 工具,自己生成库第二部,有了这个库,相当于就有了pac外设访问文件,然后其实就可以搞起来了. 那么为啥不好搞了. 因为太乱了. 小白喜欢你告我咋弄…...

字符串

对应练习题&#xff1a;力扣平台 14. 最长公共前缀 class Solution { public:string longestCommonPrefix(vector<string>& strs) {string strs1strs[0];//初始前缀字符串for (int i 1; i < strs.size(); i) {while(strs[i].find(strs1)!0)//遍历找到共同最长前…...

mysql8 锁表与解锁

方法1不行&#xff0c;就按方法2来执行&#xff1b; (一) 解锁方法1 连接mysql &#xff0c;直接执行UNLOCK TABLES&#xff0c;细节如下&#xff1a; – 查询是否锁表 SHOW OPEN TABLES WHERE in_use >0 ; – 查询进程 show processlist ; – 查询到相对应的进程&#xf…...

第2篇 区块链的历史和发展:从比特币到以太坊

想象一下&#xff0c;你住在一个小镇上&#xff0c;每个人都有一个大账本&#xff0c;记录着所有的交易。这个账本很神奇&#xff0c;每当有人买卖东西&#xff0c;大家都会在自己的账本上记一笔&#xff0c;确保每个人的账本都是一致的。这就是区块链的基本思想。而区块链的故…...

从理论到实践的指南:企业如何建立有效的EHS管理体系?

企业如何建立有效的EHS管理体系&#xff1f;对于任何企业&#xff0c;没有安全就谈不上稳定生产和经济效益&#xff0c;因此建立EHS管理体系是解决企业长期追求的建立安全管理长效机制的最有效手段。良好的体系运转&#xff0c;可以最大限度地减少事故发生。 这篇借着开头这个…...

内网和外网的区别及应用

内网和外网的区别及应用 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们来探讨一下计算机网络中的内网和外网&#xff0c;它们的区别以及在实际应用中的…...

电驱失效类型和风险分析,如何用精益思维提升电驱可靠性?

在电动车日益普及的今天&#xff0c;电驱系统作为电动车的“心脏”&#xff0c;其可靠性直接关系到整车的性能与用户体验。然而&#xff0c;电驱失效问题却一直困扰着电动车行业&#xff0c;如何提升电驱可靠性成为了业内关注的焦点。今天&#xff0c;深圳天行健精益管理咨询公…...

自动扫描范围在减少剂量多相CT肝脏成像中的应用:基于CNN和高斯模型| 文献速递-深度学习自动化疾病检查

Title 题目 Automatic scan range for dose-reduced multiphase CT imaging of theliver utilizing CNNs and Gaussian models 自动扫描范围在减少剂量多相CT肝脏成像中的应用&#xff1a;基于CNN和高斯模型 01 文献速递介绍 肝癌是全球癌症死亡的第四大原因&#xff0c;每…...

【机器学习】基于层次的聚类方法:理论与实践

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 基于层次的聚类方法&#xff1a;理论与实践引言1. 层次聚类基础1.1 概述1.2 距离…...

C# 验证PDF数字签名的有效性

数字签名作为PDF文档中的重要安全机制&#xff0c;不仅能够验证文件的来源&#xff0c;还能确保文件内容在传输过程中未被篡改。然而&#xff0c;如何正确验证PDF文件的数字签名&#xff0c;是确保文件完整性和可信度的关键。本文将详细介绍如何使用免费.NET控件通过C#验证PDF签…...

2小时动手学习扩散模型(pytorch版)【入门版】【代码讲解】

2小时动手学习扩散模型&#xff08;pytorch版&#xff09; 课程地址 2小时动手学习扩散模型&#xff08;pytorch版&#xff09; 课程目标 给零基础同学快速了解扩散模型的核心模块&#xff0c;有个整体框架的理解。知道扩散模型的改进和设计的核心模块。 课程特色&#xf…...

Centos7网络配置(设置固定ip)

文章目录 1进入虚拟机设置选中【网络适配器】选择【NAT模式】2 进入windows【控制面板\网络和 Internet\网络和共享中心\更改适配器设置】设置网络状态。3 设置VM的【虚拟网络编辑器】4 设置系统网卡5 设置虚拟机固定IP 刚安装完系统&#xff0c;有的人尤其没有勾选自动网络配置…...

英伟达被“压制”的25年

十九世纪中叶的美国西部&#xff0c;掀起了一场轰轰烈烈的淘金热&#xff0c;但最终赚到钱的&#xff0c;并不是拿命去赌的淘金者。一个名叫萨姆布瑞南的商人&#xff0c;通过向淘金者出售铲子&#xff0c;成了加州历史上第一位百万富翁。 每一次风口出现时&#xff0c;总有企…...

windows安装Gitblit还是Bonobo Git Server

Gitblit 和 Bonobo Git Server 都是用于托管Git仓库的工具&#xff0c;但它们是基于不同平台的不同软件。 Gitblit 是一个纯 Java 写的服务器&#xff0c;支持托管 Git&#xff0c;Mercurial 和 SVN 仓库。它需要 Java 运行环境&#xff0c;适合在 Windows、Linux 和 Mac 平台…...

仪器校准的概念与定义,计量校准是什么?

仪器校准的定义&#xff0c;在之前所颁布的《国际计量学词汇 基础和通用概念及相关术语》文件中&#xff0c;已经有了明确说明&#xff0c;而该文件做了修改以后&#xff0c;在后续新的定义中&#xff0c;仪器校准具体被分为两部分&#xff0c;第一步是将被计量仪器和计量校准的…...

Vue3+Pinia

1.单纯调接口(安装pinia及引入如下第一张图) 1.npm install pinia2.在main.js里引入即可import { createPinia } from piniaapp.use(createPinia()) 1.stores建立你文件的ts、内容如下&#xff1a;1-1 import { defineStore } from pinia1-2 import { findPageJobSet } from …...

label studio数据标注平台的自动化标注使用

&#xff08;作者&#xff1a;陈玓玏&#xff09; 开源项目&#xff0c;欢迎star哦&#xff0c;https://github.com/data-infra/cube-studio 做图文音项目过程中&#xff0c;我们通常会需要进行数据标注。label studio是一个比较好上手的标注平台&#xff0c;可以直接搜索…...

高并发场景下的热点key问题探析与应对策略

目录 一、问题描述 二、发现机制 三、解决策略分析 &#xff08;一&#xff09;解决策略一&#xff1a;多级缓存策略 客户端本地缓存 代理节点本地缓存 &#xff08;二&#xff09;解决策略二&#xff1a;多副本策略 &#xff08;三&#xff09;解决策略三&#xff1a;热点…...

学习一下C++中的枚举的定义

目录 普通枚举 强类型枚举 普通枚举 枚举类型在C中是通过关键字enum来定义的。下面是一个简单的例子&#xff1a; enum Color { RED, GREEN, BLUE }; 在这个例子中&#xff0c;我们定义了一个名为Color的枚举类型&#xff0c;它包含了三个枚举值&#xff1a;RED、GRE…...

常见的块元素、行内元素以及行内块元素,三者有何不同?

在HTML中&#xff0c;元素可以分为块级元素&#xff08;block-level elements&#xff09;、行内元素&#xff08;inline elements&#xff09;和行内块元素&#xff08;inline-block elements&#xff09;。它们之间的主要区别如下&#xff1a; 块级元素&#xff08;block-le…...

单片机中有FLASH为啥还需要EEROM?

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 一是EEPROM操作简单&…...

从入门到深入,Docker新手学习教程

编译整理&#xff5c;TesterHome社区 作者&#xff5c;Ishaan Gupta 以下为作者观点&#xff1a; Docker 彻底改变了我们开发、交付和运行应用程序的方式。它使开发人员能够将应用程序打包到容器中 - 标准化的可执行组件&#xff0c;将应用程序源代码与在任何环境中运行该代码…...

提高论文发表机会:Nature Communications 最新研究教你如何巧妙回复审稿意见

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 对于科研搬砖人来说&#xff0c;在论文投稿过程中&#xff0c;如何有效回复审稿意见才能得到审稿人的认可&#xff0c;一直是一个让人困惑又带点玄学的问题。 但是&#xff0c…...

前端实现坐标系转换

一、地理坐标系和投影坐标系 地理坐标系和投影坐标系是地理信息系统&#xff08;GIS&#xff09;中常见的两种坐标系统&#xff0c;它们用于描述和定位地球表面上的点和区域&#xff0c;但在实现方式和应用场景上有所不同。 1. 地理坐标系&#xff08;Geographic Coordinate …...

Go语言--工程管理、临时/永久设置GOPATH、main函数以及init函数

工作区 Go 代码必须放在工作区中。工作区其实就是一个对应于特定工程的目录&#xff0c;它应包含3个子目录:src 目录、pkg目录和bin 目录。 src 目录:用于以代码包的形式组织并保存 Go源码文件。(比如:.go.chs等)pkg 目录:用于存放经由 go install 命令构建安装后的代码包(包…...

引领SUV新风尚:新一代哈弗H6预售,科技与美学双重革新

哈弗H6作为长城汽车旗下的紧凑型SUV,一直以来都备受消费者的青睐。近日,新一代哈弗H6正式开启了预售,吸引了众多目光。外观方面,新一代哈弗H6采用了“星河美学”设计语言,整体造型更加时尚、动感。前脸配备了全新点阵式前中网,格栅尺寸更大,取消了镀铬边框,使前脸看上去…...

比亚迪BYDSHARK墨西哥上市,开启南美出行新篇章

全球瞩目:BYD SHARK墨西哥首发,开启新能源皮卡新纪元在5月14日这个值得纪念的日子里,比亚迪首款皮卡BYD SHARK在墨西哥城举行了盛大的全球产品发布暨墨西哥上市发布会。BYD SHARK的惊艳亮相不仅彰显了比亚迪作为世界级新能源科技公司的强大实力,也宣告了全球新能源皮卡时代的正…...

【全开源】知识库文档系统源码(ThinkPHP+FastAdmin)

知识库文档系统源码&#xff1a;构建智慧知识库的基石 引言 在当今信息爆炸的时代&#xff0c;知识的有效管理和利用对于企业和个人来说至关重要。知识库文档系统源码正是为了满足这一需求而诞生的&#xff0c;它提供了一个高效、便捷的平台&#xff0c;帮助用户构建、管理、…...

【网络安全】新的恶意软件:无文件恶意软件GhostHook正在广泛传播

文章目录 推荐阅读 一种新的恶意软件 GhostHook v1.0 正在一个网络犯罪论坛上迅速传播。这种创新的无文件浏览器恶意软件由 Native-One 黑客组织开发&#xff0c;具有独特的分发方式和多功能性&#xff0c;对各种平台和浏览器构成重大威胁。 GhostHook v1.0 支持 Windows、Andr…...

Kibana创建ElasticSearch 用户角色

文章目录 1, ES 权限参考2, 某应用的管理员权限&#xff1a;可以open/close/delete/cat/read/write 索引3, 某应用的读写权限&#xff1a;可以cat/read/write 索引 &#xff08;不能删除索引或数据&#xff09;4, 某应用的只读权限 1, ES 权限参考 https://www.elastic.co/gui…...

Linux内核 -- 启用 Linux 内核调试信息

启用 Linux 内核调试信息 本文档提供了如何在编译 Linux 内核时启用调试信息的逐步指南。调试信息对于调试和诊断内核问题至关重要。 启用调试信息的步骤 1. 进入内核源代码目录 打开终端并导航到 Linux 内核源代码目录&#xff1a; cd /path/to/linux-kernel2. 配置内核 …...