Skip to content

Commit 46bcffc

Browse files
committed
Add JettyWebSocketClient
Also split out JSR-356 related configuration and load it conditionally.
1 parent f45ef75 commit 46bcffc

9 files changed

+259
-44
lines changed

README-WEBSOCKET.md

+11
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ Run the ant build:
5454

5555
A usable Tomcat installation can be found in `output/build`
5656

57+
### Jetty 9
58+
59+
Download and use the latest Jetty (currently 9.0.2.v20130417). It does not support JSR-356 yet but that's not an issue, since we're using the Jetty 9 native WebSocket API.
60+
61+
If using Java-based Servlet configuration instead of web.xml, add the following options to Jetty's start.ini:
62+
63+
OPTIONS=plus
64+
etc/jetty-plus.xml
65+
OPTIONS=annotations
66+
etc/jetty-annotations.xml
67+
5768
### Glassfish
5869

5970
Glassfish also provides JSR-356 support based on Tyrus (the reference implementation).

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ project("spring-websocket") {
532532
exclude group: "org.eclipse.jetty.orbit", module: "javax.servlet"
533533
}
534534
optional("org.eclipse.jetty.websocket:websocket-server:9.0.1.v20130408")
535+
optional("org.eclipse.jetty.websocket:websocket-client:9.0.1.v20130408")
535536

536537
optional("com.fasterxml.jackson.core:jackson-databind:2.0.1") // required for SockJS support currently
537538

spring-websocket/src/main/java/org/springframework/websocket/client/AbstractWebSocketConnectionManager.java

+55-36
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public abstract class AbstractWebSocketConnectionManager implements SmartLifecyc
4040

4141
private boolean autoStartup = false;
4242

43+
private boolean isRunning = false;
44+
4345
private int phase = Integer.MAX_VALUE;
4446

4547
private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("EndpointConnectionManager-");
@@ -92,68 +94,85 @@ protected URI getUri() {
9294
return this.uri;
9395
}
9496

97+
/**
98+
* Return whether this ConnectionManager has been started.
99+
*/
100+
public boolean isRunning() {
101+
synchronized (this.lifecycleMonitor) {
102+
return this.isRunning;
103+
}
104+
}
105+
95106
/**
96107
* Connect to the configured {@link #setDefaultUri(URI) default URI}. If already
97108
* connected, the method has no impact.
98109
*/
99110
public final void start() {
100111
synchronized (this.lifecycleMonitor) {
101112
if (!isRunning()) {
102-
this.taskExecutor.execute(new Runnable() {
103-
@Override
104-
public void run() {
105-
synchronized (lifecycleMonitor) {
106-
try {
107-
logger.info("Connecting to WebSocket at " + uri);
108-
openConnection();
109-
logger.info("Successfully connected");
110-
}
111-
catch (Throwable ex) {
112-
logger.error("Failed to connect", ex);
113-
}
114-
}
115-
}
116-
});
113+
startInternal();
117114
}
118115
}
119116
}
120117

118+
protected void startInternal() {
119+
if (logger.isDebugEnabled()) {
120+
logger.debug("Starting " + this.getClass().getSimpleName());
121+
}
122+
this.isRunning = true;
123+
this.taskExecutor.execute(new Runnable() {
124+
@Override
125+
public void run() {
126+
synchronized (lifecycleMonitor) {
127+
try {
128+
logger.info("Connecting to WebSocket at " + uri);
129+
openConnection();
130+
logger.info("Successfully connected");
131+
}
132+
catch (Throwable ex) {
133+
logger.error("Failed to connect", ex);
134+
}
135+
}
136+
}
137+
});
138+
}
139+
121140
protected abstract void openConnection() throws Exception;
122141

123-
/**
124-
* Closes the configured message WebSocket connection.
125-
*/
126142
public final void stop() {
127143
synchronized (this.lifecycleMonitor) {
128144
if (isRunning()) {
129-
try {
130-
closeConnection();
131-
}
132-
catch (Throwable e) {
133-
logger.error("Failed to stop WebSocket connection", e);
134-
}
145+
stopInternal();
146+
}
147+
}
148+
}
149+
150+
protected void stopInternal() {
151+
if (logger.isDebugEnabled()) {
152+
logger.debug("Stopping " + this.getClass().getSimpleName());
153+
}
154+
try {
155+
if (isConnected()) {
156+
closeConnection();
135157
}
136158
}
159+
catch (Throwable e) {
160+
logger.error("Failed to stop WebSocket connection", e);
161+
}
162+
finally {
163+
this.isRunning = false;
164+
}
137165
}
138166

167+
protected abstract boolean isConnected();
168+
139169
protected abstract void closeConnection() throws Exception;
140170

141-
public void stop(Runnable callback) {
171+
public final void stop(Runnable callback) {
142172
synchronized (this.lifecycleMonitor) {
143173
this.stop();
144174
callback.run();
145175
}
146176
}
147177

148-
/**
149-
* Return whether the configured message endpoint is currently active.
150-
*/
151-
public boolean isRunning() {
152-
synchronized (this.lifecycleMonitor) {
153-
return isConnected();
154-
}
155-
}
156-
157-
protected abstract boolean isConnected();
158-
159178
}

spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketClient.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
import org.springframework.websocket.WebSocketSession;
2323

2424
/**
25-
* Contract for starting a WebSocket handshake request.
26-
*
27-
* <p>To automatically start a WebSocket connection when the application starts, see
28-
* {@link WebSocketConnectionManager}.
25+
* Contract for programmatically starting a WebSocket handshake request. For most cases it
26+
* would be more convenient to use the declarative style
27+
* {@link WebSocketConnectionManager} that starts a WebSocket connection to a
28+
* pre-configured URI when the application starts.
2929
*
3030
* @author Rossen Stoyanchev
3131
* @since 4.0

spring-websocket/src/main/java/org/springframework/websocket/client/WebSocketConnectionManager.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121

22+
import org.springframework.context.SmartLifecycle;
2223
import org.springframework.http.HttpHeaders;
2324
import org.springframework.util.CollectionUtils;
2425
import org.springframework.websocket.WebSocketHandler;
@@ -40,13 +41,16 @@ public class WebSocketConnectionManager extends AbstractWebSocketConnectionManag
4041

4142
private final List<String> subProtocols = new ArrayList<String>();
4243

44+
private final boolean syncClientLifecycle;
4345

44-
public WebSocketConnectionManager(WebSocketClient webSocketClient,
46+
47+
public WebSocketConnectionManager(WebSocketClient client,
4548
WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables) {
4649

4750
super(uriTemplate, uriVariables);
48-
this.client = webSocketClient;
51+
this.client = client;
4952
this.webSocketHandler = decorateWebSocketHandler(webSocketHandler);
53+
this.syncClientLifecycle = ((client instanceof SmartLifecycle) && !((SmartLifecycle) client).isRunning());
5054
}
5155

5256
/**
@@ -71,6 +75,22 @@ public List<String> getSubProtocols() {
7175
return this.subProtocols;
7276
}
7377

78+
@Override
79+
public void startInternal() {
80+
if (this.syncClientLifecycle) {
81+
((SmartLifecycle) this.client).start();
82+
}
83+
super.startInternal();
84+
}
85+
86+
@Override
87+
public void stopInternal() {
88+
if (this.syncClientLifecycle) {
89+
((SmartLifecycle) client).stop();
90+
}
91+
super.stopInternal();
92+
}
93+
7494
@Override
7595
protected void openConnection() throws Exception {
7696
HttpHeaders headers = new HttpHeaders();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.websocket.client.jetty;
18+
19+
import java.net.URI;
20+
import java.util.concurrent.Future;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import org.eclipse.jetty.websocket.api.Session;
25+
import org.springframework.context.SmartLifecycle;
26+
import org.springframework.http.HttpHeaders;
27+
import org.springframework.web.util.UriComponentsBuilder;
28+
import org.springframework.websocket.WebSocketHandler;
29+
import org.springframework.websocket.WebSocketSession;
30+
import org.springframework.websocket.adapter.JettyWebSocketListenerAdapter;
31+
import org.springframework.websocket.adapter.JettyWebSocketSessionAdapter;
32+
import org.springframework.websocket.client.WebSocketClient;
33+
import org.springframework.websocket.client.WebSocketConnectFailureException;
34+
35+
36+
/**
37+
* @author Rossen Stoyanchev
38+
* @since 4.0
39+
*/
40+
public class JettyWebSocketClient implements WebSocketClient, SmartLifecycle {
41+
42+
private static final Log logger = LogFactory.getLog(JettyWebSocketClient.class);
43+
44+
private final org.eclipse.jetty.websocket.client.WebSocketClient client;
45+
46+
private boolean autoStartup = true;
47+
48+
private int phase = Integer.MAX_VALUE;
49+
50+
private final Object lifecycleMonitor = new Object();
51+
52+
53+
public JettyWebSocketClient() {
54+
this.client = new org.eclipse.jetty.websocket.client.WebSocketClient();
55+
}
56+
57+
58+
// TODO: configure Jetty WebSocketClient properties
59+
60+
public void setAutoStartup(boolean autoStartup) {
61+
this.autoStartup = autoStartup;
62+
}
63+
64+
@Override
65+
public boolean isAutoStartup() {
66+
return this.autoStartup;
67+
}
68+
69+
public void setPhase(int phase) {
70+
this.phase = phase;
71+
}
72+
73+
@Override
74+
public int getPhase() {
75+
return this.phase;
76+
}
77+
78+
@Override
79+
public boolean isRunning() {
80+
synchronized (this.lifecycleMonitor) {
81+
return this.client.isStarted();
82+
}
83+
}
84+
85+
@Override
86+
public void start() {
87+
synchronized (this.lifecycleMonitor) {
88+
if (!isRunning()) {
89+
try {
90+
if (logger.isDebugEnabled()) {
91+
logger.debug("Starting Jetty WebSocketClient");
92+
}
93+
this.client.start();
94+
}
95+
catch (Exception e) {
96+
throw new IllegalStateException("Failed to start Jetty client", e);
97+
}
98+
}
99+
}
100+
}
101+
102+
@Override
103+
public void stop() {
104+
synchronized (this.lifecycleMonitor) {
105+
if (isRunning()) {
106+
try {
107+
if (logger.isDebugEnabled()) {
108+
logger.debug("Stopping Jetty WebSocketClient");
109+
}
110+
this.client.stop();
111+
}
112+
catch (Exception e) {
113+
logger.error("Error stopping Jetty WebSocketClient", e);
114+
}
115+
}
116+
}
117+
}
118+
119+
@Override
120+
public void stop(Runnable callback) {
121+
this.stop();
122+
callback.run();
123+
}
124+
125+
@Override
126+
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables)
127+
throws WebSocketConnectFailureException {
128+
129+
URI uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVariables).encode().toUri();
130+
return doHandshake(webSocketHandler, null, uri);
131+
}
132+
133+
@Override
134+
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri)
135+
throws WebSocketConnectFailureException {
136+
137+
JettyWebSocketListenerAdapter listener = new JettyWebSocketListenerAdapter(webSocketHandler);
138+
139+
try {
140+
// block for now
141+
Future<org.eclipse.jetty.websocket.api.Session> future = this.client.connect(listener, uri);
142+
Session session = future.get();
143+
return new JettyWebSocketSessionAdapter(session);
144+
}
145+
catch (Exception e) {
146+
throw new WebSocketConnectFailureException("Failed to connect to " + uri, e);
147+
}
148+
}
149+
150+
}

spring-websocket/src/main/java/org/springframework/websocket/server/support/JettyRequestUpgradeStrategy.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,14 @@ public void upgrade(ServerHttpRequest request, ServerHttpResponse response,
100100
private void upgrade(HttpServletRequest request, HttpServletResponse response,
101101
String selectedProtocol, final WebSocketHandler webSocketHandler) throws IOException {
102102

103-
Assert.state(this.factory.isUpgradeRequest(request, response), "Not a suitable WebSocket upgrade request");
104-
Assert.state(this.factory.acceptWebSocket(request, response), "Unable to accept WebSocket");
103+
Assert.state(this.factory.isUpgradeRequest(request, response), "Expected websocket upgrade request");
105104

106105
request.setAttribute(HANDLER_PROVIDER_ATTR_NAME, webSocketHandler);
106+
107+
if (!this.factory.acceptWebSocket(request, response)) {
108+
// should never happen
109+
throw new IllegalStateException("WebSocket request not accepted by Jetty");
110+
}
107111
}
108112

109113
}

spring-websocket/src/main/java/org/springframework/websocket/support/PerConnectionWebSocketHandlerProxy.java

+5
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,9 @@ private void destroy(WebSocketSession session) {
125125
}
126126
}
127127

128+
@Override
129+
public String toString() {
130+
return "PerConnectionWebSocketHandlerProxy [handlerType=" + this.provider.getHandlerType() + "]";
131+
}
132+
128133
}

0 commit comments

Comments
 (0)