Skip to content

Commit 852d56e

Browse files
committed
fix: Spring Boot 4.0 bean lifecycle + DevTools classloader compatibility
- AtmosphereFramework bean not available during auto-config creation - Use ApplicationReadyEvent for deferred handler registration - Use reflection for cross-classloader DevTools compatibility - Lazy framework lookup via ApplicationContext - Add spring-devtools.properties for classloader inclusion
1 parent 05da41d commit 852d56e

5 files changed

Lines changed: 65 additions & 15 deletions

File tree

src/main/java/ai/javaclaw/channels/atmosphere/AtmosphereChannelAutoConfiguration.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import ai.javaclaw.agent.Agent;
1919
import ai.javaclaw.channels.ChannelRegistry;
20-
import org.atmosphere.cpr.AtmosphereFramework;
2120
import org.springframework.ai.chat.client.ChatClient;
2221
import org.springframework.ai.chat.model.ChatModel;
2322
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -43,7 +42,7 @@
4342
* {@code Agent} are on the classpath.</p>
4443
*/
4544
@AutoConfiguration
46-
@ConditionalOnClass(AtmosphereFramework.class)
45+
@ConditionalOnClass(org.atmosphere.cpr.AtmosphereFramework.class)
4746
@ConditionalOnBean(Agent.class)
4847
public class AtmosphereChannelAutoConfiguration {
4948

@@ -57,18 +56,28 @@ public ChatModel syntheticChatModel() {
5756
@Bean
5857
public AtmosphereChatChannel atmosphereChatChannel(Agent agent,
5958
ChannelRegistry channelRegistry,
60-
AtmosphereFramework framework) {
61-
return new AtmosphereChatChannel(agent, channelRegistry, framework);
59+
org.springframework.context.ApplicationContext ctx) {
60+
// Lazy lookup: AtmosphereFramework bean is created during servlet init
61+
return new AtmosphereChatChannel(agent, channelRegistry, ctx);
6262
}
6363

6464
@Bean
6565
public AtmosphereChatHandler atmosphereChatHandler(ChatClient chatClient,
6666
ChannelRegistry channelRegistry,
6767
ObjectMapper objectMapper,
68-
AtmosphereFramework framework) {
69-
var handler = new AtmosphereChatHandler(chatClient, channelRegistry, objectMapper);
70-
framework.addAtmosphereHandler(AtmosphereChatChannel.BROADCASTER_PATH, handler);
71-
return handler;
68+
org.springframework.context.ApplicationContext ctx) {
69+
return new AtmosphereChatHandler(chatClient, channelRegistry, objectMapper, ctx);
70+
}
71+
72+
/**
73+
* Registers the chat handler with Atmosphere after the web server starts.
74+
* Uses ApplicationReadyEvent
75+
* to ensure the servlet container (and Atmosphere) is fully initialized.
76+
*/
77+
@Bean
78+
public org.springframework.context.ApplicationListener<org.springframework.boot.context.event.ApplicationReadyEvent>
79+
atmosphereHandlerRegistrar(AtmosphereChatHandler handler) {
80+
return event -> handler.ensureRegistered();
7281
}
7382

7483
/**

src/main/java/ai/javaclaw/channels/atmosphere/AtmosphereChatChannel.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,25 @@ public class AtmosphereChatChannel implements Channel {
4040

4141
private final Agent agent;
4242
private final ChannelRegistry channelRegistry;
43-
private final AtmosphereFramework framework;
43+
private final org.springframework.context.ApplicationContext ctx;
44+
private volatile AtmosphereFramework framework;
4445

4546
public AtmosphereChatChannel(Agent agent, ChannelRegistry channelRegistry,
46-
AtmosphereFramework framework) {
47+
org.springframework.context.ApplicationContext ctx) {
4748
this.agent = agent;
4849
this.channelRegistry = channelRegistry;
49-
this.framework = framework;
50+
this.ctx = ctx;
5051
channelRegistry.registerChannel(this);
5152
log.info("Started Atmosphere Chat channel");
5253
}
5354

55+
private AtmosphereFramework framework() {
56+
if (framework == null) {
57+
framework = ctx.getBean(AtmosphereFramework.class);
58+
}
59+
return framework;
60+
}
61+
5462
@Override
5563
public String getName() {
5664
return CHANNEL_NAME;
@@ -73,7 +81,7 @@ public String chat(String message) {
7381
*/
7482
@Override
7583
public void sendMessage(String message) {
76-
BroadcasterFactory factory = framework.getBroadcasterFactory();
84+
BroadcasterFactory factory = framework().getBroadcasterFactory();
7785
if (factory == null) {
7886
log.warn("Atmosphere framework not yet initialized, message dropped");
7987
return;

src/main/java/ai/javaclaw/channels/atmosphere/AtmosphereChatHandler.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,46 @@ public class AtmosphereChatHandler implements AtmosphereHandler {
4949
private final ChatClient chatClient;
5050
private final ChannelRegistry channelRegistry;
5151
private final ObjectMapper objectMapper;
52+
private final org.springframework.context.ApplicationContext ctx;
53+
private volatile boolean registered;
5254

5355
public AtmosphereChatHandler(ChatClient chatClient, ChannelRegistry channelRegistry,
54-
ObjectMapper objectMapper) {
56+
ObjectMapper objectMapper,
57+
org.springframework.context.ApplicationContext ctx) {
5558
this.chatClient = chatClient;
5659
this.channelRegistry = channelRegistry;
60+
this.ctx = ctx;
5761
this.objectMapper = objectMapper;
5862
}
5963

64+
/**
65+
* Lazily registers this handler with the Atmosphere framework on first request.
66+
* This deferred registration avoids the Spring Boot 4.0 bean lifecycle ordering
67+
* issue where the AtmosphereFramework bean isn't available at auto-config time.
68+
*/
69+
void ensureRegistered() {
70+
if (!registered) {
71+
synchronized (this) {
72+
if (!registered) {
73+
try {
74+
// Use full reflection to avoid DevTools dual-classloader ClassCastException
75+
var reg = ctx.getBean("atmosphereServletRegistration");
76+
var servlet = reg.getClass().getMethod("getServlet").invoke(reg);
77+
var framework = servlet.getClass().getMethod("framework").invoke(servlet);
78+
framework.getClass().getMethod("addAtmosphereHandler",
79+
String.class, org.atmosphere.cpr.AtmosphereHandler.class)
80+
.invoke(framework, AtmosphereChatChannel.BROADCASTER_PATH, this);
81+
registered = true;
82+
log.info("Registered Atmosphere chat handler at {}",
83+
AtmosphereChatChannel.BROADCASTER_PATH);
84+
} catch (Exception e) {
85+
log.error("Failed to register Atmosphere handler: {}", e.getMessage());
86+
}
87+
}
88+
}
89+
}
90+
}
91+
6092
@Override
6193
public void onRequest(AtmosphereResource resource) throws IOException {
6294
String body = resource.getRequest().body().asString();

src/main/java/ai/javaclaw/channels/atmosphere/JavaClawAgentBridge.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
* <p>Exposes:</p>
4040
* <ul>
4141
* <li>{@code @Prompt} — streaming AI chat via WebSocket</li>
42-
* <li>{@code @AgentSkill("chat")} — A2A-discoverable chat skill</li>
43-
* <li>{@code @AgentSkill("ask")} — synchronous Q&A via JavaClaw agent</li>
42+
* <li>AgentSkill chat — A2A-discoverable chat skill</li>
43+
* <li>AgentSkill ask — synchronous QA via JavaClaw agent</li>
4444
* </ul>
4545
*/
4646
public class JavaClawAgentBridge {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
restart.include.javaclaw=/javaclaw-.*\.jar

0 commit comments

Comments
 (0)