Skip to content

Commit 205c66f

Browse files
Split SpringAiPlugin into conditional auto-configuration (T6)
Split the monolithic SpringAiPlugin into one core plugin + three optional plugins, each with its own @ConditionalOnClass-guarded auto-configuration: - SpringAiPlugin: core chat + ExecuteToolLocalActivity (always) - VectorStorePlugin: VectorStore activity (when spring-ai-rag present) - EmbeddingModelPlugin: EmbeddingModel activity (when spring-ai-rag present) - McpPlugin: MCP activity (when spring-ai-mcp present) This fixes ClassNotFoundException when optional deps aren't on the runtime classpath. compileOnly scopes now work correctly because Spring skips loading the conditional classes entirely when the @ConditionalOnClass check fails. Also resolves T10 (unnecessary MCP reflection) — McpPlugin directly references McpClientActivityImpl instead of using Class.forName(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7705523 commit 205c66f

File tree

10 files changed

+326
-385
lines changed

10 files changed

+326
-385
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.temporal.springai.autoconfigure;
2+
3+
import io.temporal.springai.plugin.EmbeddingModelPlugin;
4+
import org.springframework.ai.embedding.EmbeddingModel;
5+
import org.springframework.boot.autoconfigure.AutoConfiguration;
6+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
7+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
8+
import org.springframework.context.annotation.Bean;
9+
10+
/**
11+
* Auto-configuration for EmbeddingModel integration with Temporal.
12+
*
13+
* <p>Conditionally creates an {@link EmbeddingModelPlugin} when {@code spring-ai-rag} is on the
14+
* classpath and an {@link EmbeddingModel} bean is available.
15+
*/
16+
@AutoConfiguration(after = SpringAiTemporalAutoConfiguration.class)
17+
@ConditionalOnClass(name = "org.springframework.ai.embedding.EmbeddingModel")
18+
@ConditionalOnBean(EmbeddingModel.class)
19+
public class SpringAiEmbeddingAutoConfiguration {
20+
21+
@Bean
22+
public EmbeddingModelPlugin embeddingModelPlugin(EmbeddingModel embeddingModel) {
23+
return new EmbeddingModelPlugin(embeddingModel);
24+
}
25+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.temporal.springai.autoconfigure;
2+
3+
import io.temporal.springai.plugin.McpPlugin;
4+
import org.springframework.boot.autoconfigure.AutoConfiguration;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
6+
import org.springframework.context.annotation.Bean;
7+
8+
/**
9+
* Auto-configuration for MCP (Model Context Protocol) integration with Temporal.
10+
*
11+
* <p>Conditionally creates a {@link McpPlugin} when {@code spring-ai-mcp} and the MCP client
12+
* library are on the classpath.
13+
*/
14+
@AutoConfiguration(after = SpringAiTemporalAutoConfiguration.class)
15+
@ConditionalOnClass(name = "io.modelcontextprotocol.client.McpSyncClient")
16+
public class SpringAiMcpAutoConfiguration {
17+
18+
@Bean
19+
public McpPlugin mcpPlugin() {
20+
return new McpPlugin();
21+
}
22+
}
Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
11
package io.temporal.springai.autoconfigure;
22

33
import io.temporal.springai.plugin.SpringAiPlugin;
4+
import java.util.Map;
5+
import org.springframework.ai.chat.model.ChatModel;
6+
import org.springframework.beans.factory.annotation.Autowired;
47
import org.springframework.boot.autoconfigure.AutoConfiguration;
58
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
6-
import org.springframework.context.annotation.Import;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.lang.Nullable;
711

812
/**
9-
* Auto-configuration for the Spring AI Temporal plugin.
13+
* Core auto-configuration for the Spring AI Temporal plugin.
1014
*
11-
* <p>Automatically registers {@link SpringAiPlugin} as a bean when Spring AI and Temporal SDK are
12-
* on the classpath. The plugin then auto-registers Spring AI activities with all Temporal workers.
15+
* <p>Creates the {@link SpringAiPlugin} bean which registers {@link
16+
* io.temporal.springai.activity.ChatModelActivity} and {@link
17+
* io.temporal.springai.tool.ExecuteToolLocalActivity} with all Temporal workers.
18+
*
19+
* <p>Optional integrations are handled by separate auto-configuration classes:
20+
*
21+
* <ul>
22+
* <li>{@link SpringAiVectorStoreAutoConfiguration} - VectorStore support
23+
* <li>{@link SpringAiEmbeddingAutoConfiguration} - EmbeddingModel support
24+
* <li>{@link SpringAiMcpAutoConfiguration} - MCP support
25+
* </ul>
1326
*/
1427
@AutoConfiguration
1528
@ConditionalOnClass(
1629
name = {"org.springframework.ai.chat.model.ChatModel", "io.temporal.worker.Worker"})
17-
@Import(SpringAiPlugin.class)
18-
public class SpringAiTemporalAutoConfiguration {}
30+
public class SpringAiTemporalAutoConfiguration {
31+
32+
@Bean
33+
public SpringAiPlugin springAiPlugin(
34+
@Autowired Map<String, ChatModel> chatModels,
35+
@Autowired(required = false) @Nullable ChatModel primaryChatModel) {
36+
return new SpringAiPlugin(chatModels, primaryChatModel);
37+
}
38+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.temporal.springai.autoconfigure;
2+
3+
import io.temporal.springai.plugin.VectorStorePlugin;
4+
import org.springframework.ai.vectorstore.VectorStore;
5+
import org.springframework.boot.autoconfigure.AutoConfiguration;
6+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
7+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
8+
import org.springframework.context.annotation.Bean;
9+
10+
/**
11+
* Auto-configuration for VectorStore integration with Temporal.
12+
*
13+
* <p>Conditionally creates a {@link VectorStorePlugin} when {@code spring-ai-rag} is on the
14+
* classpath and a {@link VectorStore} bean is available.
15+
*/
16+
@AutoConfiguration(after = SpringAiTemporalAutoConfiguration.class)
17+
@ConditionalOnClass(name = "org.springframework.ai.vectorstore.VectorStore")
18+
@ConditionalOnBean(VectorStore.class)
19+
public class SpringAiVectorStoreAutoConfiguration {
20+
21+
@Bean
22+
public VectorStorePlugin vectorStorePlugin(VectorStore vectorStore) {
23+
return new VectorStorePlugin(vectorStore);
24+
}
25+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.temporal.springai.plugin;
2+
3+
import io.temporal.common.SimplePlugin;
4+
import io.temporal.springai.activity.EmbeddingModelActivityImpl;
5+
import io.temporal.worker.Worker;
6+
import javax.annotation.Nonnull;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.ai.embedding.EmbeddingModel;
10+
11+
/**
12+
* Temporal plugin that registers {@link io.temporal.springai.activity.EmbeddingModelActivity} with
13+
* workers.
14+
*
15+
* <p>This plugin is conditionally created by auto-configuration when Spring AI's {@link
16+
* EmbeddingModel} is on the classpath and an EmbeddingModel bean is available.
17+
*/
18+
public class EmbeddingModelPlugin extends SimplePlugin {
19+
20+
private static final Logger log = LoggerFactory.getLogger(EmbeddingModelPlugin.class);
21+
22+
private final EmbeddingModel embeddingModel;
23+
24+
public EmbeddingModelPlugin(EmbeddingModel embeddingModel) {
25+
super("io.temporal.spring-ai-embedding");
26+
this.embeddingModel = embeddingModel;
27+
}
28+
29+
@Override
30+
public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {
31+
worker.registerActivitiesImplementations(new EmbeddingModelActivityImpl(embeddingModel));
32+
log.info("Registered EmbeddingModelActivity for task queue {}", taskQueue);
33+
}
34+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.temporal.springai.plugin;
2+
3+
import io.modelcontextprotocol.client.McpSyncClient;
4+
import io.temporal.common.SimplePlugin;
5+
import io.temporal.springai.mcp.McpClientActivityImpl;
6+
import io.temporal.worker.Worker;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import javax.annotation.Nonnull;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.beans.BeansException;
13+
import org.springframework.beans.factory.SmartInitializingSingleton;
14+
import org.springframework.context.ApplicationContext;
15+
import org.springframework.context.ApplicationContextAware;
16+
17+
/**
18+
* Temporal plugin that registers {@link io.temporal.springai.mcp.McpClientActivity} with workers.
19+
*
20+
* <p>This plugin is conditionally created by auto-configuration when MCP classes are on the
21+
* classpath. MCP clients may be created late by Spring AI's auto-configuration, so this plugin
22+
* supports deferred registration via {@link SmartInitializingSingleton}.
23+
*/
24+
public class McpPlugin extends SimplePlugin
25+
implements ApplicationContextAware, SmartInitializingSingleton {
26+
27+
private static final Logger log = LoggerFactory.getLogger(McpPlugin.class);
28+
29+
private List<McpSyncClient> mcpClients = List.of();
30+
private ApplicationContext applicationContext;
31+
private final List<Worker> pendingWorkers = new ArrayList<>();
32+
33+
public McpPlugin() {
34+
super("io.temporal.spring-ai-mcp");
35+
}
36+
37+
@Override
38+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
39+
this.applicationContext = applicationContext;
40+
}
41+
42+
@SuppressWarnings("unchecked")
43+
private List<McpSyncClient> getMcpClients() {
44+
if (!mcpClients.isEmpty()) {
45+
return mcpClients;
46+
}
47+
48+
if (applicationContext != null && applicationContext.containsBean("mcpSyncClients")) {
49+
try {
50+
Object bean = applicationContext.getBean("mcpSyncClients");
51+
if (bean instanceof List<?> clientList && !clientList.isEmpty()) {
52+
mcpClients = (List<McpSyncClient>) clientList;
53+
log.info("Found {} MCP client(s) in ApplicationContext", mcpClients.size());
54+
}
55+
} catch (Exception e) {
56+
log.debug("Failed to get mcpSyncClients bean: {}", e.getMessage());
57+
}
58+
}
59+
60+
return mcpClients;
61+
}
62+
63+
@Override
64+
public void initializeWorker(@Nonnull String taskQueue, @Nonnull Worker worker) {
65+
List<McpSyncClient> clients = getMcpClients();
66+
if (!clients.isEmpty()) {
67+
worker.registerActivitiesImplementations(new McpClientActivityImpl(clients));
68+
log.info(
69+
"Registered McpClientActivity ({} clients) for task queue {}", clients.size(), taskQueue);
70+
} else {
71+
pendingWorkers.add(worker);
72+
log.debug("MCP clients not yet available; will attempt registration after initialization");
73+
}
74+
}
75+
76+
@Override
77+
public void afterSingletonsInstantiated() {
78+
if (pendingWorkers.isEmpty()) {
79+
return;
80+
}
81+
82+
List<McpSyncClient> clients = getMcpClients();
83+
if (clients.isEmpty()) {
84+
log.debug("No MCP clients found after all beans initialized");
85+
pendingWorkers.clear();
86+
return;
87+
}
88+
89+
for (Worker worker : pendingWorkers) {
90+
worker.registerActivitiesImplementations(new McpClientActivityImpl(clients));
91+
log.info("Registered deferred McpClientActivity ({} clients)", clients.size());
92+
}
93+
pendingWorkers.clear();
94+
}
95+
}

0 commit comments

Comments
 (0)