文章目录
- 1、项目介绍
- 1.1、项目描述
- 1.2、项目结构
- 2、创建项目(idea)
- 2.1、依赖引入
- 2.2、 配置文件
- 2.3、 数据库表
- 2.4、 实体类
- 2.5、 配置类
- 2.6、 验证码服务类
- 2.7、 短信发送服务类
- 2.8、 消费者类
- 2.9、发送服务类
- 2.10、定时任务类
- 2.11、启动类
- 2.12、测试控制器
- 3、效果测试
- 4、总结
- 5、附件-整个源码仓库
1、项目介绍
1.1、项目描述
以下是一个完整的 Spring Boot 项目案例,整合 RabbitMQ 实现阿里云短信异步收发,并将发送情况存入数据库,同时使用 Redis 缓存验证码;
这个项目旨在实现一个可靠的短信发送系统,结合了多种技术来确保短信的高效发送和管理。以下是对各个部分的详细描述:
1.2、项目结构
项目结构如下:
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── tigerhhzz
│ │ │ ├── SpringbootMqAliyunAmsApplication.java
│ │ │ ├── config
│ │ │ │ └── RabbitMQConfig.java
│ │ │ ├── model
│ │ │ │ └── SmsLog.java
│ │ │ ├── service
│ │ │ │ ├── SmsConsumer.java
│ │ │ │ ├── SmsSenderService.java
│ │ │ │ ├── SmsService.java
│ │ │ │ └── VerificationCodeService.java
│ │ │ └── task
│ │ │ └── SmsResendTask.java
│ │ ├── resources
│ │ │ ├── application.yml
│ │ │ └── schema.sql
│ └── test
│ └── java
└── pom.xml
2、创建项目(idea)
2.1、依赖引入
- 通过
pom.xml
文件引入了必要的依赖,包括 Spring Boot 的 AMQP 模块用于与 RabbitMQ 集成、JDBC 模块用于数据库操作、MySQL 数据库驱动、阿里云短信服务 SDK 和 Redis 模块用于缓存验证码。
pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.tigerhhzz</groupId>
<artifactId>springboot-rabbitmq-aliyun-sms-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.0.6</version> <!-- 注:如提示报错,先升级基础包版,无法解决可联系技术支持 -->
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
<version>0.2.2</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-util</artifactId>
<version>0.2.20</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2、 配置文件
application.yml
文件中配置了 RabbitMQ、阿里云短信服务、数据库和 Redis 的连接信息。这些配置使得项目能够连接到相应的服务和资源。
server:
port: 8080
spring:
application:
name: springboot-rabbitmq-demo
# 添加数据源配置
datasource:
url: jdbc:mysql://localhost:3306/springbootrabbitmq_db_msg?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
#rabbitmq
rabbitmq:
host: xxxxxxxxxxx
port: 5672
virtual-host: /
username: guest
password: guest
# 开启confirms回调 P -> Exchange
publisher-confirms: true
# 开启returnedMessage回调 Exchange -> Queue
publisher-confrms-type: correlation
publisher-returns: true
# 设置手动确认(ack) Queue -> C
listener:
simple:
acknowledge-mode: manual
prefetch: 100
#redis
redis:
host: 8.217.205.108
port: 6379
password: xxxxxxxx
timeout: 50000
#阿里短信配置
aliyun:
sms:
accessKey: xxxxxxxx
accessKeySecret: xxxxxxxxx
signName: 珑湾崖头
templateCode: SMS_474285221
domain: dysmsapi.aliyuncs.com
regionId: cn-beijing
# 配置mybatis的xml配置文件扫描目录
mybatis:
mapper-locations:
- classpath:mapper/*.xml
configuration:
# 打印SQL语句,需要注射掉这个mybatis属性配置,否则启动报错
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2.3、 数据库表
schema.sql
文件中创建了sms_log
表,用于记录短信的发送状态和时间。这个表有助于跟踪短信的发送历史,并可以用于后续的分析和故障排除。
CREATE TABLE sms_log (
id INT AUTO_INCREMENT PRIMARY KEY,
unique_id VARCHAR(36), -- 存储 UUID
phone_number VARCHAR(20),
message_content VARCHAR(255),
send_status TINYINT, -- 0 表示未发送成功,1 表示发送成功
send_time TIMESTAMP
);
2.4、 实体类
SmsLog
类是一个简单的 Java 对象,用于表示数据库中的短信记录。它包含了短信的相关信息,如手机号码、发送状态和发送时间。
package com.tigerhhzz.model;
import lombok.Data;
import java.util.Date;
/**
* @Author tigerhhzz
* @Date 2024 10 05 11 37
**/
@Data
public class SmsLog {
private int id;
private String uniqueId;
private String phoneNumber;
private String messageContent;
private int sendStatus;
private Date sendTime;
}
2.5、 配置类
RabbitMQConfig
类配置了 RabbitMQ 的队列、交换器和绑定关系。通过定义这些元素,项目可以使用 RabbitMQ 进行异步消息传递。
package com.tigerhhzz.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author tigerhhzz
* @Date 2024 10 05 11 38
**/
@Configuration
public class RabbitMQConfig {
public static final String SMS_QUEUE = "smsQueue";
public static final String SMS_EXCHANGE = "smsExchange";
public static final String SMS_ROUTING_KEY = "smsRoutingKey";
@Bean
public Queue smsQueue() {
return new Queue(SMS_QUEUE);
}
@Bean
public TopicExchange smsExchange() {
return new TopicExchange(SMS_EXCHANGE);
}
@Bean
public Binding binding(Queue smsQueue, TopicExchange smsExchange) {
return BindingBuilder.bind(smsQueue).to(smsExchange).with(SMS_ROUTING_KEY);
}
}
2.6、 验证码服务类
VerificationCodeService
类负责生成和验证验证码。它使用 Redis 来缓存生成的验证码,并设置了过期时间。生成验证码时,它会生成一个随机的 6 位数字验证码,并将其存储在 Redis 中,同时返回给调用者。验证验证码时,它会从 Redis 中获取存储的验证码,并与用户输入的验证码进行比较。
package com.tigerhhzz.service;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Service
public class VerificationCodeService {
private final RedisTemplate<String, String> redisTemplate;
public VerificationCodeService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public String generateVerificationCode(String key) {
// 生成随机验证码
String verificationCode = generateRandomCode();
// 将验证码存入 Redis,并设置过期时间,比如 5 分钟
redisTemplate.opsForValue().set(key, verificationCode, 5, TimeUnit.MINUTES);
return verificationCode;
}
public boolean verifyVerificationCode(String key, String code) {
String storedCode = redisTemplate.opsForValue().get(key);
return storedCode!= null && storedCode.equals(code);
}
private String generateRandomCode() {
// 生成 6 位随机数字验证码
Random random = new Random();
int code = random.nextInt(900000) + 100000;
return String.valueOf(code);
}
}
2.7、 短信发送服务类
SmsService
类负责实际的短信发送操作。它使用阿里云短信服务的 SDK 来发送短信,并在发送成功或失败时更新数据库中的发送状态。此外,它还与验证码服务类交互,生成并包含验证码在短信内容中。
package com.tigerhhzz.service;
import com.aliyun.teaopenapi.Client;
import com.aliyun.teaopenapi.models.Config;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.http.MethodType;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import com.aliyuncs.profile.DefaultProfile;
/**
* @Author tigerhhzz
* @Date 2024 10 05 11 40
**/
@Service
public class SmsService {
@Value("${aliyun.sms.accessKey}")
private String accessKeyId;
@Value("${aliyun.sms.regionId}")
private String regionId;
@Value("${aliyun.sms.domain}")
private String domain;
@Value("${aliyun.sms.accessKeySecret}")
private String accessKeySecret;
@Value("${aliyun.sms.signName}")
private String signName;
@Value("${aliyun.sms.templateCode}")
private String templateCode;
private final JdbcTemplate jdbcTemplate;
private final VerificationCodeService verificationCodeService;
public SmsService(JdbcTemplate jdbcTemplate, VerificationCodeService verificationCodeService) {
this.jdbcTemplate = jdbcTemplate;
this.verificationCodeService = verificationCodeService;
}
public String sendSms(String phoneNumber, String uniqueId) {
try {
// 生成验证码
String verificationCode = verificationCodeService.generateVerificationCode(phoneNumber);
DefaultProfile profile = DefaultProfile.getProfile(regionId,accessKeyId, accessKeySecret);
IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest();
request.setMethod(MethodType.POST);
request.setDomain(domain);
request.setVersion("2017-05-25");
request.setAction("SendSms");
request.putQueryParameter("RegionId", regionId);
request.putQueryParameter("PhoneNumbers", phoneNumber); //目标手机号
request.putQueryParameter("SignName", signName); //签名名称
request.putQueryParameter("TemplateCode", templateCode); //短信模板code
request.putQueryParameter("TemplateParam", "{\"code\":\"" + verificationCode + "\"}");//模板中变量替换
CommonResponse response = client.getCommonResponse(request);
String data = response.getData();
System.out.println("发送短信后的响应结果:"+data);
if (StringUtils.contains(data, "\"Message\":\"OK\"")) {
// 发送成功,更新数据库状态
jdbcTemplate.update("UPDATE sms_log SET send_status = 1, send_time = NOW() WHERE unique_id =?", uniqueId);
return "发送成功,验证码:"+verificationCode;
} else {
// 发送失败,也可以记录失败原因等
jdbcTemplate.update("UPDATE sms_log SET send_status = 0 WHERE unique_id =?", uniqueId);
return "发送失败,验证码:"+verificationCode;
}
} catch (Exception e) {
e.printStackTrace();
// 发送异常,更新数据库状态为失败
jdbcTemplate.update("UPDATE sms_log SET send_status = 0 WHERE unique_id =?", uniqueId);
System.out.println("发送短信验证码失败~ phoneNumber = " + phoneNumber + e);
}
return null;
}
}
2.8、 消费者类
SmsConsumer
类是 RabbitMQ 的消费者,它监听特定的队列,并在接收到消息时调用短信发送服务类的方法来发送短信。
package com.tigerhhzz.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class SmsConsumer {
private static final Logger LOGGER = LoggerFactory.getLogger(SmsConsumer.class);
@Autowired
private SmsService smsService;
// @RabbitListener(queues = "smsQueue")
@RabbitListener(queues = {"smsQueue"})
public void receiveSmsTask( Map<String,String> map) {
String phoneNumber = map.get("phoneNumber");
String uniqueId = map.get("uniqueId");
// 解析消息,获取手机号码和 UUID
smsService.sendSms(phoneNumber, uniqueId);
}
}
2.9、发送服务类
SmsSenderService
类提供了一个方法,用于将短信发送任务异步发送到 RabbitMQ。在发送任务之前,它会将任务记录到数据库中,初始状态为未发送。
package com.tigerhhzz.service;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class SmsSenderService {
@Autowired
private AmqpTemplate amqpTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
public void sendSmsAsync(String phoneNumber) {
// 生成 UUID 作为唯一 ID
String uniqueId = UUID.randomUUID().toString();
// 先记录发送任务到数据库,初始状态为未发送
jdbcTemplate.update("INSERT INTO sms_log (phone_number, message_content, send_status, unique_id) VALUES (?,?, 0,?)", phoneNumber, null, uniqueId);
String message = phoneNumber + "," + uniqueId;
amqpTemplate.convertAndSend("smsExchange", "smsRoutingKey", message);
}
}
2.10、定时任务类
SmsResendTask
类定义了一个定时任务,用于检查数据库中未发送成功的短信,并重新发送它们。这个定时任务每分钟执行一次,确保未发送成功的短信能够尽快得到重新发送。
package com.tigerhhzz.task;
import com.tigerhhzz.service.SmsSenderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.config.ScheduledTask;
import org.springframework.stereotype.Component;
/**
* 定时任务
* @Author tigerhhzz
* @Date 2024 10 05 13 15
**/
@Component
public class SmsResendTask {
private static final Logger LOGGER = LoggerFactory.getLogger(SmsResendTask.class);
private final SmsSenderService smsSenderService;
private final JdbcTemplate jdbcTemplate;
public SmsResendTask(SmsSenderService smsSenderService, JdbcTemplate jdbcTemplate) {
this.smsSenderService = smsSenderService;
this.jdbcTemplate = jdbcTemplate;
}
//@Scheduled(fixedRate = 60000) // 每分钟检查一次
@Scheduled(cron = "0/30 * * * * ?")
public void checkAndResendSms() {
LOGGER.info("开始执行重新发送失败的消息!");
jdbcTemplate.query("SELECT phone_number, unique_id FROM sms_log WHERE send_status = 0", (rs, rowNum) -> {
String phoneNumber = rs.getString("phone_number");
String uniqueId = rs.getString("unique_id");
smsSenderService.sendSmsAsync(phoneNumber);
return null;
});
}
}
2.11、启动类
Application
类是项目的启动类,它使用 Spring Boot 的自动配置功能来启动应用程序。通过@SpringBootApplication
注解和@ComponentScan
注解,确保项目中的所有组件都能够被正确扫描和加载。
package com.tigerhhzz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
/**
* @Author tigerhhzz
* @Date 2024 10 05 10 10
**/
@SpringBootApplication
@ComponentScan(basePackages = "com.tigerhhzz")
public class SpringbootMqAliyunAmsApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootMqAliyunAmsApplication.class, args);
System.out.println("SpringbootMqAliyunAmsApplication启动成功!!!");
}
}
2.12、测试控制器
package com.tigerhhzz.controller;
import com.tigerhhzz.service.SmsSenderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @Author tigerhhzz
* @Date 2024 10 05 13 35
**/
@RestController
@RequestMapping("/sms")
public class SmsController {
@Autowired
private SmsSenderService smsSenderService;
/**
* 通过mq发送短信验证码
* @param phoneNumber
*/
@RequestMapping(value="/sendsmsbymq/{phoneNumber}",method= RequestMethod.POST)
public String sendCode(@PathVariable String phoneNumber ){
smsSenderService.sendSmsAsync(phoneNumber);
System.out.println("手机号:" +phoneNumber+";发送短信验证码成功");
return "短信发送任务已提交,将异步发送。";
}
}
3、效果测试
启动主启动类:
post请求:http://localhost:8080/sms/sendsmsbymq/150xxxx0598
使用postman进行测试:
idea后台打印结果:
rabbitmq监控消息:
查看数据库:
结果:消息成功发送!!!
4、总结
通过以上的设计和实现,这个项目可以实现可靠的短信发送功能,并使用 Redis 缓存验证码来提高系统的安全性和用户体验。同时,通过异步发送和定时任务重发机制,可以确保短信的高可用性和可靠性。在实际应用中,可以根据具体需求进一步扩展和优化这个项目。
5、附件-整个源码仓库
https://gitee.com/spring2020/springboot-mq-aliyun-ams-application
感谢关注点赞!!!
人生从来没有真正的绝境。只要一个人的心中还怀着一粒信念的种子,那么总有一天,他就能走出困境,让生命重新开花结果。