SpringBoot进阶操作
SpringBoot使用WebSocket(Java 标准 WebSocket API)
1.引入依赖
这里用的是gradle,Maven依赖的话自己搜一下,大差不差
implementation("org.springframework.boot:spring-boot-starter-websocket")
2.创建websocket配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
ServerEndpointExporter 的作用是“在 Spring 启动时,自动扫描并注册@ServerEndpoint 类”。
如果不加这个配置,前端就连不上websocket,报404
3.写后端websocket代码
@ServerEndpoint(value = "/api/websocket/{userId}")
@Controller
public class WebSocketController {
private String userId;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId){
this.userId = userId;
System.out.println("用户:"+userId+"连接成功!");
}
/**
* 连接关闭时自动调用的方法
*/
@OnClose
public void onClose(){
System.out.println("连接已关闭");
}
/**
* 收到客户端主动发送消息时调用的方法
*/
@OnMessage
public void onMessage(String message, Session session){
try {
System.out.println("用户:"+userId+"发送了消息:"+message);
//给客户端发消息
session.getBasicRemote().sendText("用户:"+userId+",已收到消息:"+message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Session session代表 一次 WebSocket 连接的上下文对象,用它来 给客户端发消息、获取连接信息、管理连接状态。
Session类型参数的作用
1.给当前客户端发消息
session.getBasicRemote().sendText("消息内容");
2.判断连接是否还存活
session.isOpen();
3.获取连接唯一标识
session.getId();
4.存储连接级数据(类似 request scope)
session.getUserProperties().put("key", value);
5.主动关闭连接
session.close();
Session 就是 WebSocket 里的“连接句柄”
@PathParam("userId")注解的作用
从 WebSocket URL 路径中 提取参数值,并注入到方法参数中。
合法参数组合
WebSocket 容器会做 方法签名匹配
1.文本消息
@OnMessage
public void onMessage(String message) {
}
说明:
message接收客户端发送的文本内容- 最基础、最常用
2.接收消息内容 + Session
@OnMessage
public void onMessage(String message, Session session) {
}
说明:
String message:文本消息内容Session session:当前 WebSocket 连接会话
3.二进制消息(Binary Message)合法参数组合
@OnMessage
public void onMessage(byte[] data) {
}
说明:
- 接收客户端发送的二进制数据
- 常用于文件、图片、音频等
4.使用 ByteBuffer 接收 + Session
@OnMessage
public void onMessage(ByteBuffer buffer, Session session) {
}
说明:
ByteBuffer性能更好- 适合大数据或流式处理
5.系统控制消息(Pong)接收 Pong 消息
@OnMessage
public void onMessage(PongMessage pongMessage) {
}
说明:
- 用于心跳检测
- 较少在业务中直接使用
SpringBoot使用WebSocket(更适合SpringBoot的继承 Handler)
| Java标准WebSocket | Spring WebSocket |
|---|---|
@OnOpen |
afterConnectionEstablished() |
@OnMessage |
handleTextMessage() |
@OnClose |
afterConnectionClosed() |
@OnError |
handleTransportError() |
两者最大的区别是创建对象的方式不同。Spring Handler方式对象是由SpringIOC容器创建的,由SpringIOC容器管理。更适合SpringBoot项目
1.在Server层创建一个自定义Handler,用于管理WebSocket对象(必要)
@Component
public class DeviceWebSocketHandler
extends TextWebSocketHandler {
@Autowired
ObjectMapper objectMapper;
/**
* 保存用户与WebSocket连接关系
*
* key:用户email
* value:该用户所有浏览器窗口的Session
* */ // ConcurrentHashMap 是一个支持高并发访问的 HashMap。是一个线程安全的Map。todo 整理笔记
private final Map<String, List<WebSocketSession>> users
= new ConcurrentHashMap<>();
/**
* WebSocket连接建立
*
* 相当于 @OnOpen
*/ @Override
public void afterConnectionEstablished(WebSocketSession session)
throws Exception {
/**
* 从HandshakeInterceptor中获取用户email
*/ // WebSocket 建立连接后,从握手阶段保存的数据中取出用户身份信息。里面的类型是Object,所以需要强转一下String
String email = (String) session
.getAttributes()
.get("email");
if(email == null){
//没有用户身份,关闭连接
session.close(
CloseStatus.NOT_ACCEPTABLE
);
return;
}
/**
* 一个用户的信息可能被多个浏览器窗口打开
* put():无论有没有旧值,直接覆盖。
* computeIfAbsent():只有不存在这个 key 时,才创建并放入。
*
* CopyOnWriteArrayList是 JDK 提供的一个线程安全 List 实现。
* 线程安全的原因:读操作直接读原数组,写操作时复制一份新数组,然后修改新数组,最后替换旧数组。
*/
users.computeIfAbsent(
email,
key -> new CopyOnWriteArrayList<>()
)
.add(session);
// 当前观看人数
System.out.println(
"WebSocket连接建立:"
+ email
+ " 当前连接数:"
+ users.get(email).size()
);
}
/**
* 接收客户端消息
*
* 相当于 @OnMessage
* * 一般你的项目中浏览器不会主动发送数据
* 所以可以暂时不用
*/
@Override
protected void handleTextMessage(
WebSocketSession session,
TextMessage message)
throws Exception {
String email = (String) session
.getAttributes()
.get("email");
System.out.println(
"收到客户端消息:"
+ email
+ " -> "
+ message.getPayload()
);
}
/**
* WebSocket关闭
*
* 相当于 @OnClose
*/ @Override
public void afterConnectionClosed(
WebSocketSession session,
CloseStatus status)
throws Exception {
String email = (String) session
.getAttributes()
.get("email");
if(email == null){
return;
}
List<WebSocketSession> sessions = users.get(email);
if(sessions != null){
//删除关闭的连接
sessions.remove(session);
/**
* 如果该用户已经没有连接
* 删除用户
*/
if(sessions.isEmpty()){
users.remove(email);
}
}
System.out.println("WebSocket关闭:" + email);
}
/**
* WebSocket异常
*
* 相当于 @OnError
*/ @Override
public void handleTransportError(
WebSocketSession session,
Throwable exception)
throws Exception {
String email = (String) session
.getAttributes()
.get("email");
System.out.println(
"WebSocket异常:" + email + " " + exception.getMessage()
);
if(session.isOpen()){
session.close(
CloseStatus.SERVER_ERROR
);
}
}
/**
* 给指定用户推送消息
*
* 一个用户多个浏览器窗口都会收到
*/
public void sendToUser(String email, DataUploadMainDTO data)
throws Exception {
//将DataUploadMainDTO对象序列化为json
String message = objectMapper.writeValueAsString(data);
// 获取用户对应的所以连接对象
List<WebSocketSession> sessions = users.get(email);
if(sessions == null){
return;
}
for(WebSocketSession session : sessions){
if(session.isOpen()){
session.sendMessage(
new TextMessage(message)
);
}
}
}
/**
* 广播给所有用户
*
* 如果以后需要公告类消息可以使用
*/
public void broadcast(DataUploadMainDTO data) throws Exception {
//将DataUploadMainDTO对象序列化为json
String message = objectMapper.writeValueAsString(data);
for(List<WebSocketSession> sessions : users.values()){
for(WebSocketSession session : sessions){
if(session.isOpen()){
session.sendMessage(new TextMessage(message));
}
}
}
}
}
2.创建Config配置类(必要)
请仔细看下面的注释,不要直接复制。因为有一个配置不是必须的
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final DeviceWebSocketHandler handler;
private final JwtHandshakeInterceptor interceptor;
public WebSocketConfig(
DeviceWebSocketHandler handler,
JwtHandshakeInterceptor interceptor){
this.handler = handler;
this.interceptor = interceptor;
}
/**
* 向 Spring WebSocket 注册一个连接入口 /ws/device,并且在建立连接前执行 JWT 拦截认证。
* @param registry SpringBoot提供的WebSocket注册中心,一个保存「URL路径 → WebSocket处理器」映射关系的容器。
*/
@Override
public void registerWebSocketHandlers(
WebSocketHandlerRegistry registry){
registry.addHandler(
//handler负责接收客户端消息,发送消息。管理session
handler,
//这是 WebSocket 访问地址。
"/ws/device"
)
// 添加认证。WebSocket正式建立之前,先执行认证。如果不加这个拦截器,则任何人都可以连接 (注:这个配置不是必须的。如果你不需要拦截器认证可以删掉该方法)
.addInterceptors(
interceptor
)
//跨域配置。*表示允许所有来源。todo 在生产环境下应该设为自己的域名。
.setAllowedOrigins("*");
}
}
3.创建拦截器实现认证(非必要)(以Jwt为例)
关于拦截器的注册,已在上一步Config中实现
/*Jwt握手拦截器*/
@Slf4j
@Component
public class JwtHandshakeInterceptor
implements HandshakeInterceptor {
private final JwtUtil jwtUtil;
public JwtHandshakeInterceptor(JwtUtil jwtUtil){
this.jwtUtil = jwtUtil;
}
//握手之前
@Override
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String,Object> attributes) {
// 获取URL参数
URI uri = request.getURI();
String query = uri.getQuery();
if(query == null){
return false;
}
// token=xxxx
String token =
query.substring(
query.indexOf("=")+1
);
try {
//解析JWT
String email = jwtUtil.parse(token);
//保存用户信息
attributes.put("email", email);
return true;
}catch(Exception e){
log.warn("token可能已过期");
log.error("JwtHandshakeInterceptor:",e);
return false;
}
}
//握手之后,因为没什么需要处理的,所以没有填充具体代码
@Override
public void afterHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception){
}
}
操作Redis数据库
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Boot 邮件发送(spring-boot-starter-mail)
一、简介
spring-boot-starter-mail 是 Spring Boot 官方提供的邮件发送组件,底层基于 Jakarta Mail(JavaMail)。
主要用途:
- 邮箱验证码
- 注册激活邮件
- 找回密码
- 系统通知
- HTML 邮件
- 附件邮件
二、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
三、QQ 邮箱开启 SMTP
QQ 邮箱:
设置 → 账户 → POP3/SMTP 服务
开启后会生成一个授权码。
注意:
SMTP 登录使用的是授权码,不是 QQ 登录密码。
四、配置 application.yml
spring:
mail:
host: smtp.qq.com
port: 465
protocol: smtp
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
ssl:
enable: true
推荐配置:
- SMTP服务器:smtp.qq.com
- 端口:465
- SSL:开启
五、发送普通文本邮件
注入 JavaMailSender
@Service
public class MailService {
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
}
发送邮件
public void sendMail(String to) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject("测试邮件");
message.setText("Hello Spring Boot!");
mailSender.send(message);
}
六、发送 HTML 邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper =
new MimeMessageHelper(mimeMessage, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject("邮箱验证码");
helper.setText("""
<h2>DeviceUsageTime</h2>
<p>您的验证码:</p>
<h1 style="color:#4CAF50">
654321
</h1>
<p>验证码5分钟内有效。</p>
""", true);
mailSender.send(mimeMessage);
第二个参数:
true
表示发送 HTML。
七、生成六位验证码
String code = String.format(
"%06d",
new Random().nextInt(1000000)
);
示例:
038521
八、推荐业务流程
用户输入邮箱
│
▼
点击获取验证码
│
▼
生成6位验证码
│
▼
Redis 保存验证码
TTL:300 秒
│
▼
发送邮件
│
▼
用户输入验证码
│
▼
注册接口校验
│
▼
注册成功

浙公网安备 33010602011771号