Implementação de Comunicação Bidirecional com Spring Boot e WebSocket

O protocolo WebSocket estabelece um canal de comunicação full-duplex sobre uma única conexão TCP persistente. Diferente do modelo tradicional HTTP, baseado em requisição e resposta (pull), o WebSocket permite que o servidor envie dados ativamente para o cliente (push), tornando-o ideal para sistemas de monitoramento em tempo real, dashboards financeiros e aplicações de mensagens.

Configuração de Dependências

Para iniciar a integração no ecossistema Spring Boot, é necessário incluir o starter de WebSocket no arquivo pom.xml do projeto:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Em seguida, é preciso registrar o ServerEndpointExporter. Este Bean é responsável por escanear as classes anotadas com @ServerEndpoint e registrá-las no container de WebSocket integrado.

@Configuration
public class WebSocketBrokerConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

Desenvolvimento do Servidor de Mensagens

A classe de gerenciamento de conexões utiliza anotações padrão da especificação Java WebSocket (JSR 356). No exemplo abaixo, utilizamos um ConcurrentHashMap para gerenciar as sessões ativas de forma thread-safe, permitindo o envio de mensagens para usuários específicos via identificador único.

@Component
@ServerEndpoint("/ws/notifier/{clientId}")
public class RealTimeSocketHandler {

    private static final Map<String, Session> clientSessions = new ConcurrentHashMap<>();

    @OnOpen
    public void onConnect(Session session, @PathParam("clientId") String clientId) {
        clientSessions.put(clientId, session);
        System.out.println("Conexão estabelecida: " + clientId);
    }

    @OnClose
    public void onDisconnect(@PathParam("clientId") String clientId) {
        clientSessions.remove(clientId);
        System.out.println("Conexão encerrada: " + clientId);
    }

    @OnMessage
    public void handleIncomingMessage(String payload, Session session) {
        // Lógica para processar mensagens recebidas do frontend, se necessário
    }

    @OnError
    public void handleError(Session session, Throwable error) {
        error.printStackTrace();
    }

    public static void dispatchMessage(String clientId, String content) {
        Session targetSession = clientSessions.get(clientId);
        if (targetSession != null && targetSession.isOpen()) {
            targetSession.getAsyncRemote().sendText(content);
        }
    }
}

Desafios de Injeção de Dependência

Um ponto crítico na implementação é que instâncias anotadas com @ServerEndpoint são gerenciadas pelo container WebSocket, e não diretamente pelo Spring Context. Isso significa que a injeção via @Autowired em campos de instância falhará (resultando em null). Para contornar isso, é comum utilizar o padrão Singleton ou acessar o contexto do Spring de forma estática para recuperar serviços necessários durante o ciclo de vida do socket.

Interface de Teste no Frontend

Para validar a comunicação, o cliente JavaScript pode se conectar ao endpoint utilizando a API nativa de WebSockets do navegador:

const socket = new WebSocket("ws://localhost:8080/ws/notifier/user_001");

socket.onopen = () => console.log("Conectado ao servidor");
socket.onmessage = (event) => {
    const data = event.data;
    console.log("Notificação recebida:", data);
};
socket.onclose = () => console.log("Conexão perdida");

Disparando Mensagens via API REST

É possível integrar o fluxo de dados de outras partes do sistema (como eventos de banco de dados ou mensagens MQTT) e repassá-los ao frontend através de um Controller:

@RestController
@RequestMapping("/api/notifications")
public class PushController {

    @PostMapping("/send/{id}")
    public ResponseEntity<String> triggerPush(@PathVariable String id, @RequestBody String message) {
        RealTimeSocketHandler.dispatchMessage(id, message);
        return ResponseEntity.ok("Mensagem enviada com sucesso");
    }
}

Considerações de Produção

Embora a implementação básica seja funcional, ambientes de produção exigem cuidados adicionais:

  • Escalabilidade Vertical: Como as sessões são mantidas em memória, o escalonamento horizontal exige um mecanismo de sincronização (como Redis Pub/Sub) para garantir que uma mensagem chegue ao cliente mesmo que ele esteja conectado a um nó diferente do emissor da mensagem.
  • Keep-Alive: Implementar mecanismos de ping-pong para evitar que firewalls ou proxies encerrem conexões inativas por timeout.
  • Segurança: Validar tokens de autenticação durante o handshake inicial do protocolo.

Tags: Spring Boot WebSocket java real-time backend

Publicado em 10-24 11:50