Skip to content

Commit 455e798

Browse files
Merge pull request #6293 from eclipse/jetty-10.0.x-6287-WebSocketClientClassLoading
Issue #6287 - fix classloading for WebSocketClient in webapp
2 parents 67e2b4a + 0761aee commit 455e798

File tree

8 files changed

+760
-2
lines changed

8 files changed

+760
-2
lines changed

jetty-websocket/websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/CoreClientUpgradeRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ else if (values.length == 1)
442442
WebSocketConstants.SPEC_VERSION_STRING);
443443

444444
WebSocketCoreSession coreSession = new WebSocketCoreSession(frameHandler, Behavior.CLIENT, negotiated, wsClient.getWebSocketComponents());
445+
coreSession.setClassLoader(wsClient.getClassLoader());
445446
customizer.customize(coreSession);
446447

447448
HttpClient httpClient = wsClient.getHttpClient();

jetty-websocket/websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class WebSocketCoreClient extends ContainerLifeCycle
3838
private static final Logger LOG = LoggerFactory.getLogger(WebSocketCoreClient.class);
3939
private final HttpClient httpClient;
4040
private final WebSocketComponents components;
41+
private ClassLoader classLoader;
4142

4243
// TODO: Things to consider for inclusion in this class (or removal if they can be set elsewhere, like HttpClient)
4344
// - AsyncWrite Idle Timeout
@@ -61,12 +62,23 @@ public WebSocketCoreClient(HttpClient httpClient, WebSocketComponents webSocketC
6162
if (httpClient == null)
6263
httpClient = Objects.requireNonNull(HttpClientProvider.get());
6364

65+
this.classLoader = Thread.currentThread().getContextClassLoader();
6466
this.httpClient = httpClient;
6567
this.components = webSocketComponents;
6668
addBean(httpClient);
6769
addBean(webSocketComponents);
6870
}
6971

72+
public ClassLoader getClassLoader()
73+
{
74+
return classLoader;
75+
}
76+
77+
public void setClassLoader(ClassLoader classLoader)
78+
{
79+
this.classLoader = Objects.requireNonNull(classLoader);
80+
}
81+
7082
public CompletableFuture<CoreSession> connect(FrameHandler frameHandler, URI wsUri) throws IOException
7183
{
7284
CoreClientUpgradeRequest request = CoreClientUpgradeRequest.from(this, wsUri, frameHandler);

jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketCoreSession.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ public class WebSocketCoreSession implements IncomingFrames, CoreSession, Dumpab
8080
private long maxTextMessageSize = WebSocketConstants.DEFAULT_MAX_TEXT_MESSAGE_SIZE;
8181
private Duration idleTimeout = WebSocketConstants.DEFAULT_IDLE_TIMEOUT;
8282
private Duration writeTimeout = WebSocketConstants.DEFAULT_WRITE_TIMEOUT;
83+
private ClassLoader classLoader;
8384

8485
public WebSocketCoreSession(FrameHandler handler, Behavior behavior, Negotiated negotiated, WebSocketComponents components)
8586
{
87+
this.classLoader = Thread.currentThread().getContextClassLoader();
8688
this.components = components;
8789
this.handler = handler;
8890
this.behavior = behavior;
@@ -91,13 +93,32 @@ public WebSocketCoreSession(FrameHandler handler, Behavior behavior, Negotiated
9193
negotiated.getExtensions().initialize(new IncomingAdaptor(), new OutgoingAdaptor(), this);
9294
}
9395

96+
public ClassLoader getClassLoader()
97+
{
98+
return classLoader;
99+
}
100+
101+
public void setClassLoader(ClassLoader classLoader)
102+
{
103+
this.classLoader = classLoader;
104+
}
105+
94106
/**
95107
* Can be overridden to scope into the correct classloader before calling application code.
96108
* @param runnable the runnable to execute.
97109
*/
98110
protected void handle(Runnable runnable)
99111
{
100-
runnable.run();
112+
ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
113+
try
114+
{
115+
Thread.currentThread().setContextClassLoader(classLoader);
116+
runnable.run();
117+
}
118+
finally
119+
{
120+
Thread.currentThread().setContextClassLoader(oldClassLoader);
121+
}
101122
}
102123

103124
/**

jetty-websocket/websocket-core-server/src/main/java/org/eclipse/jetty/websocket/core/server/internal/AbstractHandshaker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ protected void handle(Runnable runnable)
209209
if (contextHandler != null)
210210
contextHandler.handle(runnable);
211211
else
212-
runnable.run();
212+
super.handle(runnable);
213213
}
214214
};
215215
}

jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ private WebApp(String contextName)
100100
// Configure the WebAppContext.
101101
context = new WebAppContext();
102102
context.setContextPath("/" + contextName);
103+
context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
103104
context.setBaseResource(new PathResource(contextDir));
104105
context.setAttribute("org.eclipse.jetty.websocket.javax", Boolean.TRUE);
105106
context.addConfiguration(new JavaxWebSocketConfiguration());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//
2+
// ========================================================================
3+
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
4+
//
5+
// This program and the accompanying materials are made available under the
6+
// terms of the Eclipse Public License v. 2.0 which is available at
7+
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
8+
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
9+
//
10+
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
11+
// ========================================================================
12+
//
13+
14+
package org.eclipse.jetty.websocket.javax.tests;
15+
16+
import java.net.URI;
17+
import java.util.concurrent.LinkedBlockingQueue;
18+
import java.util.concurrent.TimeUnit;
19+
import javax.servlet.annotation.WebServlet;
20+
import javax.servlet.http.HttpServlet;
21+
import javax.servlet.http.HttpServletRequest;
22+
import javax.servlet.http.HttpServletResponse;
23+
import javax.websocket.ClientEndpoint;
24+
import javax.websocket.ContainerProvider;
25+
import javax.websocket.OnMessage;
26+
import javax.websocket.OnOpen;
27+
import javax.websocket.Session;
28+
import javax.websocket.WebSocketContainer;
29+
import javax.websocket.server.ServerEndpoint;
30+
31+
import org.eclipse.jetty.client.HttpClient;
32+
import org.eclipse.jetty.client.api.ContentResponse;
33+
import org.eclipse.jetty.client.api.Response;
34+
import org.eclipse.jetty.http.BadMessageException;
35+
import org.eclipse.jetty.http.HttpStatus;
36+
import org.eclipse.jetty.io.ByteBufferPool;
37+
import org.eclipse.jetty.util.component.ContainerLifeCycle;
38+
import org.eclipse.jetty.webapp.Configuration;
39+
import org.eclipse.jetty.webapp.Configurations;
40+
import org.eclipse.jetty.websocket.core.WebSocketComponents;
41+
import org.eclipse.jetty.websocket.core.client.CoreClientUpgradeRequest;
42+
import org.eclipse.jetty.websocket.javax.client.JavaxWebSocketClientContainerProvider;
43+
import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketContainer;
44+
import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration;
45+
import org.eclipse.jetty.xml.XmlConfiguration;
46+
import org.junit.jupiter.api.AfterEach;
47+
import org.junit.jupiter.api.Test;
48+
49+
import static org.hamcrest.MatcherAssert.assertThat;
50+
import static org.hamcrest.Matchers.containsString;
51+
import static org.hamcrest.Matchers.is;
52+
import static org.junit.jupiter.api.Assertions.assertNotNull;
53+
54+
public class JavaxClientClassLoaderTest
55+
{
56+
private WSServer server;
57+
private HttpClient httpClient;
58+
59+
@FunctionalInterface
60+
interface ThrowingRunnable
61+
{
62+
void run() throws Exception;
63+
}
64+
65+
public void start(ThrowingRunnable configuration) throws Exception
66+
{
67+
server = new WSServer();
68+
configuration.run();
69+
server.start();
70+
httpClient = new HttpClient();
71+
httpClient.start();
72+
}
73+
74+
@AfterEach
75+
public void after() throws Exception
76+
{
77+
httpClient.stop();
78+
server.stop();
79+
}
80+
81+
@ClientEndpoint()
82+
public static class ClientSocket
83+
{
84+
LinkedBlockingQueue<String> textMessages = new LinkedBlockingQueue<>();
85+
86+
@OnOpen
87+
public void onOpen(Session session)
88+
{
89+
session.getAsyncRemote().sendText("ContextClassLoader: " + Thread.currentThread().getContextClassLoader());
90+
}
91+
92+
@OnMessage
93+
public void onMessage(String message)
94+
{
95+
textMessages.add(message);
96+
}
97+
}
98+
99+
@WebServlet("/servlet")
100+
public static class WebSocketClientServlet extends HttpServlet
101+
{
102+
private final WebSocketContainer clientContainer = ContainerProvider.getWebSocketContainer();
103+
104+
@Override
105+
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
106+
{
107+
URI wsEchoUri = URI.create("ws://localhost:" + req.getServerPort() + "/echo/");
108+
ClientSocket clientSocket = new ClientSocket();
109+
110+
try (Session ignored = clientContainer.connectToServer(clientSocket, wsEchoUri))
111+
{
112+
String recv = clientSocket.textMessages.poll(5, TimeUnit.SECONDS);
113+
assertNotNull(recv);
114+
resp.setStatus(HttpStatus.OK_200);
115+
resp.getWriter().println(recv);
116+
resp.getWriter().println("ClientClassLoader: " + clientContainer.getClass().getClassLoader());
117+
}
118+
catch (Exception e)
119+
{
120+
throw new RuntimeException(e);
121+
}
122+
}
123+
}
124+
125+
@ServerEndpoint("/")
126+
public static class EchoSocket
127+
{
128+
@OnMessage
129+
public void onMessage(Session session, String message) throws Exception
130+
{
131+
session.getBasicRemote().sendText(message);
132+
}
133+
}
134+
135+
public WSServer.WebApp createWebSocketWebapp(String contextName) throws Exception
136+
{
137+
WSServer.WebApp app = server.createWebApp(contextName);
138+
139+
// Exclude the Javax WebSocket configuration from the webapp (so we use versions from the webapp).
140+
Configuration[] configurations = Configurations.getKnown().stream()
141+
.filter(configuration -> !(configuration instanceof JavaxWebSocketConfiguration))
142+
.toArray(Configuration[]::new);
143+
app.getWebAppContext().setConfigurations(configurations);
144+
145+
// Copy over the individual jars required for Javax WebSocket.
146+
app.createWebInf();
147+
app.copyLib(JavaxWebSocketClientContainerProvider.class, "websocket-javax-client.jar");
148+
app.copyLib(JavaxWebSocketContainer.class, "websocket-javax-common.jar");
149+
app.copyLib(ContainerLifeCycle.class, "jetty-util.jar");
150+
app.copyLib(CoreClientUpgradeRequest.class, "websocket-core-client.jar");
151+
app.copyLib(WebSocketComponents.class, "websocket-core-common.jar");
152+
app.copyLib(Response.class, "jetty-client.jar");
153+
app.copyLib(ByteBufferPool.class, "jetty-io.jar");
154+
app.copyLib(BadMessageException.class, "jetty-http.jar");
155+
app.copyLib(XmlConfiguration.class, "jetty-xml.jar");
156+
157+
return app;
158+
}
159+
160+
@Test
161+
public void websocketProvidedByServer() throws Exception
162+
{
163+
start(() ->
164+
{
165+
WSServer.WebApp app1 = server.createWebApp("app");
166+
app1.createWebInf();
167+
app1.copyClass(WebSocketClientServlet.class);
168+
app1.copyClass(ClientSocket.class);
169+
app1.deploy();
170+
171+
WSServer.WebApp app2 = server.createWebApp("echo");
172+
app2.createWebInf();
173+
app2.copyClass(EchoSocket.class);
174+
app2.deploy();
175+
});
176+
177+
// After hitting each WebApp we will get 200 response if test succeeds.
178+
ContentResponse response = httpClient.GET(server.getServerUri().resolve("/app/servlet"));
179+
assertThat(response.getStatus(), is(HttpStatus.OK_200));
180+
181+
// The ContextClassLoader in the WebSocketClients onOpen was the WebAppClassloader.
182+
assertThat(response.getContentAsString(), containsString("ContextClassLoader: WebAppClassLoader"));
183+
184+
// Verify that we used Servers version of WebSocketClient.
185+
ClassLoader serverClassLoader = server.getServer().getClass().getClassLoader();
186+
assertThat(response.getContentAsString(), containsString("ClientClassLoader: " + serverClassLoader)); }
187+
188+
@Test
189+
public void websocketProvidedByWebApp() throws Exception
190+
{
191+
start(() ->
192+
{
193+
WSServer.WebApp app1 = createWebSocketWebapp("app");
194+
app1.createWebInf();
195+
app1.copyClass(WebSocketClientServlet.class);
196+
app1.copyClass(ClientSocket.class);
197+
app1.copyClass(EchoSocket.class);
198+
app1.deploy();
199+
200+
// Do not exclude JavaxWebSocketConfiguration for this webapp (we need the websocket server classes).
201+
WSServer.WebApp app2 = server.createWebApp("echo");
202+
app2.createWebInf();
203+
app2.copyClass(EchoSocket.class);
204+
app2.deploy();
205+
});
206+
207+
// After hitting each WebApp we will get 200 response if test succeeds.
208+
ContentResponse response = httpClient.GET(server.getServerUri().resolve("/app/servlet"));
209+
assertThat(response.getStatus(), is(HttpStatus.OK_200));
210+
211+
// The ContextClassLoader in the WebSocketClients onOpen was the WebAppClassloader.
212+
assertThat(response.getContentAsString(), containsString("ContextClassLoader: WebAppClassLoader"));
213+
214+
// Verify that we used WebApps version of WebSocketClient.
215+
assertThat(response.getContentAsString(), containsString("ClientClassLoader: WebAppClassLoader"));
216+
}
217+
}

0 commit comments

Comments
 (0)