Skip to content

Как связать Spring и WebSocket

Синопсис

В этом посте рассмотрим пример доступа к спринговому бину из из двух разнородных конекстов, спрингового и вебсокета, другими словами необходимо чтобы спринговый компонент шарился между собственно спрингом и веб сокетом. Чтобы лучше понять задачу, то представим такую ситуацию. Допустим у нас есть обычное спринговое приложение которое поднимает бины в которых хранинтся какая-то дата. Затем к этому приложению добавляется требование обмениваться этими данными, которые хранятся в бине, с другим приложением через веб сокет. Так как спринг и вебсокет поднимаются в разных контекстах, то получить вебсокету данные из спрингового бина чтобы их передать не получиться, в этом случае надо два контекста как-то объединить, другими словами надо настроить между ними мостик, чтобы контекст вебсокета имел доступ к спринговому контексту и передавать данные вытащенные из спринга. В этом посте рассмотрим как создавать этот мост между спрингом и вебсокетом.


Готовый проект можно взять с gitHub: https://github.com/dev-blogs/spring подпроект spring-and-websocket

Структура проекта

spring-and-websocket
    ├──src
    │   ├─main
    │   │   └─java
    │   │       └─com
    │   │           └─devblogs
    │   │               ├─spring
    │   │               │   ├─beans
    │   │               │   │   ├─MyBean.java
    │   │               │   │   ├─BeanA.java
    │   │               │   │   └─BeanB.java
    │   │               │   └─config
    │   │               │       └─SpringConfigurator.java
    │   │               ├─websocket
    │   │               │   ├─config
    │   │               │   │   ├─ServerWebsocketConfig.java
    │   │               │   │   └─SpringComponentProvider.java
    │   │               │   ├─AnotherWebSocketEndpoint.java
    │   │               │   └─WebSocketEndpoint.java
    │   │               └─App.java
    │   └─resources
    │       ├─META-INF
    │       │   └─services
    │       │       └─org.glassfish.tyrus.spi.ComponentProvider
    │       └─log4j.xml
    └──pom.xml

Java код

Начнем со спринговой конфигурации, добавим конфигуратор SpringConfigurator.java в котром сконфигурируем вебсокет сервер org.glassfish.tyrus.server.Server.
Бин webSocketEndpoint как следует из его названия это эндпоинт вебсокета, который подключается к вебсокету в конфигурационном классе вебсокета ServerWebsocketConfig будет привен далее. Необходимо, чтобы эндпоинт поднимался как спринговый бин.
Бит springComponentProvider это связующий объект между спрингом и вебсокетом. Так же необходимо, чтобы связвующий объект поднимался как спринговый бин.

SpringConfigurator.java

package com.devblogs.spring.config;

import org.glassfish.tyrus.server.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import com.devblogs.websocket.WebSocketEndpoint;
import com.devblogs.websocket.config.ServerWebsocketConfig;
import com.devblogs.websocket.config.SpringComponentProvider;

@Configuration
@ComponentScan("com.devblogs")
public class SpringConfigurator {
	@Bean
	public WebSocketEndpoint webSocketEndpoint() {
		return new WebSocketEndpoint();
	}
	
	@Bean
    public SpringComponentProvider springComponentProvider() {
        return new SpringComponentProvider();
    }
	
	@Bean
    public Server server() {
        return new Server("localhost", 9003, "/context", ServerWebsocketConfig.class);
    }
}

Класс ServerWebsocketConfig возвращает набор конфигов в свою очередь в каждом из которых по эндпоинту которые север будет загружать.

ServerWebsocketConfig.java

package com.devblogs.websocket.config;

import java.util.HashSet;
import java.util.Set;
import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;
import com.devblogs.websocket.AnotherWebSocketEndpoint;
import com.devblogs.websocket.WebSocketEndpoint;

public class ServerWebsocketConfig implements ServerApplicationConfig {
    public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
        Set<ServerEndpointConfig> configs = new HashSet<ServerEndpointConfig>();        
        
        ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/echo").build();
        ServerEndpointConfig anotherServerEndpointConfig = ServerEndpointConfig.Builder.create(AnotherWebSocketEndpoint.class, "/another").build();
        
        configs.add(serverEndpointConfig);
        configs.add(anotherServerEndpointConfig);
        
        return configs;
    }

    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
        return null;
    }
}

Класс SpringComponentProvider это связующий класс между спрингом и вебсокетом. Этот класс «осведомлен» о спринговом контейнере которым он был загружен для этого имплементируется интерфейс ApplicationContextAware. Так же этот класс наследуется от абстрактного класса ComponentProvider испоьзуемый тайуросом (либа которая предоставляет вебсокет). Тайрус, используя метод provideInstance() класса ComponentProvider через лукап запрашивает у спринга эндпоинт com.devblogs.websocket.WebSocketEndpoint который сконфигурирован в конфиг-классе SpringConfigurator. Но где размещен этот класс тайрус пока еще не знает, позже мы добавим эту информацию для тайруса в соответствующий файл внешнего ресурса:

SpringComponentProvider.java

package com.devblogs.websocket.config;

import org.glassfish.tyrus.spi.ComponentProvider;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ClassUtils;

public class SpringComponentProvider extends ComponentProvider implements ApplicationContextAware {
	public static ApplicationContext ctx;

    public boolean isApplicable(Class<?> c) {
        return ctx.containsBean(ClassUtils.getShortNameAsProperty(c));
    }

    public <T> T provideInstance(Class<T> c) {
        return ctx.getBean(c);
    }

    public boolean destroyInstance(Object o) {
        return false;
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ctx = applicationContext;
    }
}

В класс эндпоинт вебсокета WebSocketEndpoint проихсодит все самое интересное, ради чего и был затеян этот пост. Когда объект эндпоинта, который находится в контексте вебсокета, получает инфорамацию из веб сокета, в данном случае когда вызывается метод onOpen или onMessage неважно, то в этом случае можно обращаться к спринговым бинам, которые находятся в другом контексте, как будто бы мы это делали находясь в спринговом контексте.
Еще раз, объект WebSocketEndpoint это вебсокетовский контекст, бины beanA и beanB это спринговый контекст, доступ из объекта WebSocketEndpoint к объектам beanA и beanB пробрасывается между двумя разнородными контекстами.

WebSocketEndpoint.java

package com.devblogs.websocket;

import java.io.IOException;

import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import org.springframework.beans.factory.annotation.Autowired;
import com.devblogs.spring.beans.MyBean;

public class WebSocketEndpoint extends Endpoint {
	@Autowired
	private MyBean beanA;
	
	@Autowired
	private MyBean beanB;
	
	@Override
    public void onOpen(final Session session, EndpointConfig config) {
        System.out.println("onOpen");
        MessageHandler messageHandler = new MessageHandler.Whole<String>() {
            public void onMessage(String message) {
            	System.out.println(beanA.getMessage());
            	System.out.println(beanB.getMessage());
                System.out.println("Message: " + message);
                try {
                	session.getBasicRemote().sendText("Echo: " + message);
                } catch (IOException e) {
                	e.printStackTrace();
                }
            }
        };
        session.addMessageHandler(messageHandler);
    }
}

AnotherWebSocketEndpoint.java

package com.devblogs.websocket;

import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;

public class AnotherWebSocketEndpoint extends Endpoint {
	@Override
    public void onOpen(Session session, EndpointConfig config) {
        System.out.println("onOpen");
        MessageHandler messageHandler = new MessageHandler.Whole<String>() {
            public void onMessage(String message) {
                System.out.println("Another message: " + message);
            }
        };
        session.addMessageHandler(messageHandler);
    }
}

Сами бины реализуют достаточно простую бизнес логику назначение которой возвращать из бина сообщение.

MyBean.java

package com.devblogs.spring.beans;

public interface MyBean {
	public String getMessage();
}

BeanA.java

package com.devblogs.spring.beans;

import org.springframework.stereotype.Component;

@Component
public class BeanA implements MyBean {
	public String getMessage() {
		return "beanA";
	}
}

BeanB.java

package com.devblogs.spring.beans;

import org.springframework.stereotype.Component;

@Component
public class BeanB implements MyBean {
	public String getMessage() {
		return "beanB";
	}
}

Класс запуска, который поднимает спринговый контекст, вытаскивает из него вебсокет сервер и запускает его. В запущенном вебсервере два контекста будут связаны.

App.java

package com.devblogs;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.glassfish.tyrus.server.Server;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.devblogs.spring.config.SpringConfigurator;

public class App {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfigurator.class);
        Server server = context.getBean(Server.class);
        
        try {
            server.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            while (true) {
                System.out.print("Enter \"stop\" to stop the server: ");
                String answer = reader.readLine();
                if (answer.equalsIgnoreCase("stop")) {
                    break;
                }
                System.out.println();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            server.stop();
        }
	}
}

Конфигурация проекта

На этом еще не все, для того чтобы тайрус знал где искать связующий объект, нужно указать путь к связующему объекту во внешнем ресрусе META-INF/services/org.glassfish.tyrus.spi.ComponentProvider, а в него поместить путь от класспаса к связующему объекту:

META-INF/services/org.glassfish.tyrus.spi.ComponentProvider

com.devblogs.websocket.config.SpringComponentProvider

pom.xml

<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">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.devglogs</groupId>
  <artifactId>spring-and-websocket</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>sprign-and-websocket</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.version>4.3.2.RELEASE</spring.version>
  </properties>

  <dependencies>
  	<!-- spring -->
  	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-core</artifactId>
  		<version>${spring.version}</version>
  	</dependency>
  	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-context</artifactId>
  		<version>${spring.version}</version>
  	</dependency>
  
  	<!-- websocket -->
  	<dependency>
  		<groupId>javax.websocket</groupId>
  		<artifactId>javax.websocket-api</artifactId>
  		<version>1.0</version>
  	</dependency>
  	<dependency>
  		<groupId>org.glassfish.tyrus</groupId>
  		<artifactId>tyrus-server</artifactId>
  		<version>1.1</version>
  	</dependency>
  	<dependency>
  		<groupId>org.glassfish.tyrus</groupId>
  		<artifactId>tyrus-container-grizzly</artifactId>
  		<version>1.1</version>
  	</dependency>
  	
  	<!-- tests -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Client.html

<html>
    <head>
        <meta charset=UTF-8>
        <title>Tomcat WebSocket Chat</title>
        <script>
            var ws = new WebSocket("ws://localhost:9003/context/echo");
            ws.onopen = function(){
            };
            ws.onmessage = function(message){
                document.getElementById("chatlog").textContent += message.data + "\n";
            };
            function postToServer(){
                ws.send(document.getElementById("msg").value);
                document.getElementById("msg").value = "";
            }
            function closeConnect(){
                ws.close();
            }
        </script>
    </head>
    <body>
        <textarea id="chatlog" readonly></textarea><br/>
        <input id="msg" type="text" />
        <button type="submit" id="sendButton" onClick="postToServer()">Send!</button>
        <button type="submit" id="endButton" onClick="closeConnect()">End</button>
    </body>
</html>

Поделиться в социальных сетях

Опубликовать в Google Plus
Опубликовать в LiveJournal
Опубликовать в Мой Мир
Опубликовать в Одноклассники
Опубликовать в Яндекс

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *