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 秒
        │
        ▼
发送邮件
        │
        ▼
用户输入验证码
        │
        ▼
注册接口校验
        │
        ▼
注册成功
posted @ 2026-07-02 17:42  畅畅c  阅读(6)  评论(0)    收藏  举报