Наконец-то выпущен Jetty 9.1 , в результате чего Java WebSockets (JSR-356) используется в средах без EE. Это потрясающая новость, и сегодня мы расскажем об использовании этого великолепного нового API вместе со Spring Framework .
JSR-356 определяет краткую модель на основе аннотаций, позволяющую современным веб-приложениям Java легко создавать двунаправленные каналы связи с помощью API WebSockets . Он охватывает не только серверную, но и клиентскую сторону, что делает этот API действительно простым для использования везде.
Давайте начнем! Нашей целью было бы создать сервер WebSockets, который будет принимать сообщения от клиентов и транслировать их всем остальным подключенным клиентам. Для начала давайте определим формат сообщения, которым будет обмениваться сервер и клиент, как этот простой класс Message . Мы можем ограничиться чем-то вроде String , но я хотел бы представить вам мощь другого нового API — Java API для обработки JSON (JSR-353) .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package com.example.services;public class Message { private String username; private String message; public Message() { } public Message( final String username, final String message ) { this.username = username; this.message = message; } public String getMessage() { return message; } public String getUsername() { return username; } public void setMessage( final String message ) { this.message = message; } public void setUsername( final String username ) { this.username = username; }} |
Чтобы разделить объявления, относящиеся к серверу и клиенту, JSR-356 определяет две основные аннотации: @ServerEndpoint и @ClientEndpoit соответственно. Наша клиентская конечная точка, назовем ее BroadcastClientEndpoint , просто прослушивает сообщения, отправленные сервером:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package com.example.services;import java.io.IOException;import java.util.logging.Logger;import javax.websocket.ClientEndpoint;import javax.websocket.EncodeException;import javax.websocket.OnMessage;import javax.websocket.OnOpen;import javax.websocket.Session;@ClientEndpointpublic class BroadcastClientEndpoint { private static final Logger log = Logger.getLogger( BroadcastClientEndpoint.class.getName() ); @OnOpen public void onOpen( final Session session ) throws IOException, EncodeException { session.getBasicRemote().sendObject( new Message( "Client", "Hello!" ) ); } @OnMessage public void onMessage( final Message message ) { log.info( String.format( "Received message '%s' from '%s'", message.getMessage(), message.getUsername() ) ); }} |
Это буквально это! Очень чистый, не требующий пояснений фрагмент кода: @OnOpen вызывается, когда клиент подключается к серверу, и @ OnMessage вызывается каждый раз, когда сервер отправляет сообщение клиенту. Да, это очень просто, но есть предостережение: реализация JSR-356 может обрабатывать любые простые объекты, но не такие сложные, как Message . Чтобы справиться с этим, JSR-356 представляет концепцию кодеров и декодеров .
Мы все любим JSON , так почему бы не определить наш собственный кодер и декодер JSON ? Это простая задача, которую Java API для JSON Processing (JSR-353) может решить для нас. Чтобы создать кодировщик, вам нужно всего лишь реализовать Encoder.Text <Message> и в основном сериализовать ваш объект в некоторую строку, в нашем случае в строку JSON , используя JsonObjectBuilder .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package com.example.services;import javax.json.Json;import javax.json.JsonReaderFactory;import javax.websocket.EncodeException;import javax.websocket.Encoder;import javax.websocket.EndpointConfig;public class Message { public static class MessageEncoder implements Encoder.Text< Message > { @Override public void init( final EndpointConfig config ) { } @Override public String encode( final Message message ) throws EncodeException { return Json.createObjectBuilder() .add( "username", message.getUsername() ) .add( "message", message.getMessage() ) .build() .toString(); } @Override public void destroy() { } }} |
Что касается декодера, все выглядит очень похоже, мы должны реализовать Decoder.Text <Message> и десериализовать наш объект из строки, на этот раз используя JsonReader .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
package com.example.services;import javax.json.JsonObject;import javax.json.JsonReader;import javax.json.JsonReaderFactory;import javax.websocket.DecodeException;import javax.websocket.Decoder;public class Message { public static class MessageDecoder implements Decoder.Text< Message > { private JsonReaderFactory factory = Json.createReaderFactory( Collections.< String, Object >emptyMap() ); @Override public void init( final EndpointConfig config ) { } @Override public Message decode( final String str ) throws DecodeException { final Message message = new Message(); try( final JsonReader reader = factory.createReader( new StringReader( str ) ) ) { final JsonObject json = reader.readObject(); message.setUsername( json.getString( "username" ) ); message.setMessage( json.getString( "message" ) ); } return message; } @Override public boolean willDecode( final String str ) { return true; } @Override public void destroy() { } }} |
И в качестве последнего шага, мы должны сообщить клиенту (и серверу, они используют одни и те же декодеры и кодеры), что у нас есть кодер и декодер для наших сообщений. Проще всего это сделать, просто объявив их как часть аннотаций @ServerEndpoint и @ClientEndpoit .
|
1
2
3
4
5
6
|
import com.example.services.Message.MessageDecoder;import com.example.services.Message.MessageEncoder;@ClientEndpoint( encoders = { MessageEncoder.class }, decoders = { MessageDecoder.class } )public class BroadcastClientEndpoint {} |
Чтобы завершить пример клиента, нам нужен способ подключения к серверу с помощью BroadcastClientEndpoint и обмена сообщениями. Класс ClientStarter завершает картину:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package com.example.ws;import java.net.URI;import java.util.UUID;import javax.websocket.ContainerProvider;import javax.websocket.Session;import javax.websocket.WebSocketContainer;import org.eclipse.jetty.websocket.jsr356.ClientContainer;import com.example.services.BroadcastClientEndpoint;import com.example.services.Message;public class ClientStarter { public static void main( final String[] args ) throws Exception { final String client = UUID.randomUUID().toString().substring( 0, 8 ); final WebSocketContainer container = ContainerProvider.getWebSocketContainer(); final String uri = "ws://localhost:8080/broadcast"; try( Session session = container.connectToServer( BroadcastClientEndpoint.class, URI.create( uri ) ) ) { for( int i = 1; i <= 10; ++i ) { session.getBasicRemote().sendObject( new Message( client, "Message #" + i ) ); Thread.sleep( 1000 ); } } // Application doesn't exit if container's threads are still running ( ( ClientContainer )container ).stop(); }} |
Просто пара комментариев о том, что делает этот код: мы подключаемся к конечной точке WebSockets по адресу ws: // localhost: 8080 / broadcast , выбираем случайное имя клиента (из UUID) и генерируем 10 сообщений, каждое с задержкой в 1 секунду (просто чтобы убедиться, у нас есть время, чтобы получить их все обратно).
Серверная часть не выглядит по-другому, и в этот момент ее можно понять без каких-либо дополнительных комментариев (за исключением того, что сервер просто передает каждое сообщение, которое он получает, всем подключенным клиентам). Здесь важно упомянуть: новый экземпляр конечной точки сервера создается каждый раз, когда к нему подключается новый клиент (именно поэтому коллекция пиров является статической), это поведение по умолчанию и его можно легко изменить.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
package com.example.services;import java.io.IOException;import java.util.Collections;import java.util.HashSet;import java.util.Set;import javax.websocket.EncodeException;import javax.websocket.OnClose;import javax.websocket.OnMessage;import javax.websocket.OnOpen;import javax.websocket.Session;import javax.websocket.server.ServerEndpoint;import com.example.services.Message.MessageDecoder;import com.example.services.Message.MessageEncoder;@ServerEndpoint( value = "/broadcast", encoders = { MessageEncoder.class }, decoders = { MessageDecoder.class } ) public class BroadcastServerEndpoint { private static final Set< Session > sessions = Collections.synchronizedSet( new HashSet< Session >() ); @OnOpen public void onOpen( final Session session ) { sessions.add( session ); } @OnClose public void onClose( final Session session ) { sessions.remove( session ); } @OnMessage public void onMessage( final Message message, final Session client ) throws IOException, EncodeException { for( final Session session: sessions ) { session.getBasicRemote().sendObject( message ); } }} |
Чтобы эта конечная точка была доступна для подключения, мы должны запустить контейнер WebSockets и зарегистрировать эту конечную точку внутри него. Как всегда, Jetty 9.1 легко запускается во встроенном режиме:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package com.example.ws;import org.eclipse.jetty.server.Server;import org.eclipse.jetty.servlet.DefaultServlet;import org.eclipse.jetty.servlet.ServletContextHandler;import org.eclipse.jetty.servlet.ServletHolder;import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;import org.springframework.web.context.ContextLoaderListener;import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;import com.example.config.AppConfig;public class ServerStarter { public static void main( String[] args ) throws Exception { Server server = new Server( 8080 ); // Create the 'root' Spring application context final ServletHolder servletHolder = new ServletHolder( new DefaultServlet() ); final ServletContextHandler context = new ServletContextHandler(); context.setContextPath( "/" ); context.addServlet( servletHolder, "/*" ); context.addEventListener( new ContextLoaderListener() ); context.setInitParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() ); context.setInitParameter( "contextConfigLocation", AppConfig.class.getName() ); server.setHandler( context ); WebSocketServerContainerInitializer.configureContext( context ); server.start(); server.join(); }} |
Наиболее важной частью приведенного выше фрагмента является WebSocketServerContainerInitializer.configureContext : он фактически создает экземпляр контейнера WebSockets . Поскольку мы еще не добавили никаких конечных точек, контейнер в основном сидит здесь и ничего не делает. Spring Framework и класс конфигурации AppConfig сделают эту последнюю проводку за нас.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
package com.example.config;import javax.annotation.PostConstruct;import javax.inject.Inject;import javax.websocket.DeploymentException;import javax.websocket.server.ServerContainer;import javax.websocket.server.ServerEndpoint;import javax.websocket.server.ServerEndpointConfig;import org.eclipse.jetty.websocket.jsr356.server.AnnotatedServerEndpointConfig;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.context.WebApplicationContext;import com.example.services.BroadcastServerEndpoint;@Configurationpublic class AppConfig { @Inject private WebApplicationContext context; private ServerContainer container; public class SpringServerEndpointConfigurator extends ServerEndpointConfig.Configurator { @Override public < T > T getEndpointInstance( Class< T > endpointClass ) throws InstantiationException { return context.getAutowireCapableBeanFactory().createBean( endpointClass ); } } @Bean public ServerEndpointConfig.Configurator configurator() { return new SpringServerEndpointConfigurator(); } @PostConstruct public void init() throws DeploymentException { container = ( ServerContainer )context.getServletContext(). getAttribute( javax.websocket.server.ServerContainer.class.getName() ); container.addEndpoint( new AnnotatedServerEndpointConfig( BroadcastServerEndpoint.class, BroadcastServerEndpoint.class.getAnnotation( ServerEndpoint.class ) ) { @Override public Configurator getConfigurator() { return configurator(); } } ); } } |
Как мы упоминали ранее, контейнер по умолчанию будет создавать новый экземпляр конечной точки сервера каждый раз, когда подключается новый клиент, и делает это, вызывая конструктор, в нашем случае BroadcastServerEndpoint.class.newInstance () . Это может быть желательным поведением, но поскольку мы используем Spring Framework и внедрение зависимостей, такие новые объекты в основном являются неуправляемыми компонентами. Благодаря очень продуманному (на мой взгляд) дизайну JSR-356 , на самом деле довольно легко предоставить свой собственный способ создания экземпляров конечных точек с помощью реализации ServerEndpointConfig.Configurator . SpringServerEndpointConfigurator является примером такой реализации: он создает новый управляемый компонент каждый раз, когда запрашивается новый экземпляр конечной точки (если вам нужен один экземпляр, вы можете создать синглтон конечной точки как компонент в AppConfig и возвращать его все время).
Способ извлечения контейнера WebSockets зависит от Jetty : из атрибута контекста с именем «javax.websocket.server.ServerContainer» (он, вероятно, может измениться в будущем). Как только контейнер будет добавлен, мы просто добавляем новую (управляемую!) Конечную точку, предоставляя наш собственный ServerEndpointConfig (на основе AnnotatedServerEndpointConfig, который любезно предоставляет Jetty ).
Чтобы построить и запустить наш сервер и клиенты, нам нужно просто сделать это:
|
1
2
3
|
mvn clean packagejava -jar target\jetty-web-sockets-jsr356-0.0.1-SNAPSHOT-server.jar // run serverjava -jar target/jetty-web-sockets-jsr356-0.0.1-SNAPSHOT-client.jar // run yet another client |
Например, запустив сервер и пару клиентов (я запускаю 4 из них, ‘ 392f68ef ‘, ‘ 8e3a869d ‘, ‘ ca3a06d0 ‘, ‘ 6cb82119 ‘), вы можете увидеть по выводу в консоли, что каждый клиент получает все сообщения от всех других клиентов (включая собственные сообщения):
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Hello!' from 'Client'Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #1' from '392f68ef'Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #2' from '8e3a869d'Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #7' from 'ca3a06d0'Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #4' from '6cb82119'Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #2' from '392f68ef'Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #3' from '8e3a869d'Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #8' from 'ca3a06d0'Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #5' from '6cb82119'Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #3' from '392f68ef'Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #4' from '8e3a869d'Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #9' from 'ca3a06d0'Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #6' from '6cb82119'Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #4' from '392f68ef'Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #5' from '8e3a869d'Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #10' from 'ca3a06d0'Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #7' from '6cb82119'Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #5' from '392f68ef'Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #6' from '8e3a869d'Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #8' from '6cb82119'Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #6' from '392f68ef'Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #7' from '8e3a869d'Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #9' from '6cb82119'Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #7' from '392f68ef'Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #8' from '8e3a869d'Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #10' from '6cb82119'Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #8' from '392f68ef'Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #9' from '8e3a869d'Nov 29, 2013 9:21:37 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #9' from '392f68ef'Nov 29, 2013 9:21:37 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #10' from '8e3a869d'Nov 29, 2013 9:21:38 PM com.example.services.BroadcastClientEndpoint onMessageINFO: Received message 'Message #10' from '392f68ef'2013-11-29 21:21:39.260:INFO:oejwc.WebSocketClient:main: Stopped org.eclipse.jetty.websocket.client.WebSocketClient@3af5f6dc |
Потрясающие! Я надеюсь, что это вступительное сообщение блога покажет, как легко стало использовать современные протоколы веб-коммуникации в Java, благодаря Java WebSockets (JSR-356) , Java API для обработки JSON (JSR-353) и замечательным проектам, таким как Jetty 9.1 !
- Как всегда, полный проект доступен на GitHub .