From 573e254ef86acd8cea25ab167326973192a57830 Mon Sep 17 00:00:00 2001 From: fatan Date: Sat, 12 Sep 2026 21:09:31 +0900 Subject: [PATCH] Cache MCP annotation scans by target class Reuse annotation discovery results for repeated bean classes while preserving registration of each annotated bean instance. Fixes #6961 Signed-off-by: fatan --- ...tractAnnotatedMethodBeanPostProcessor.java | 6 ++++- ...AnnotatedMethodBeanPostProcessorTests.java | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/spring/scan/AbstractAnnotatedMethodBeanPostProcessor.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/spring/scan/AbstractAnnotatedMethodBeanPostProcessor.java index b0d8d216f99..c35d3cf6116 100644 --- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/spring/scan/AbstractAnnotatedMethodBeanPostProcessor.java +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/spring/scan/AbstractAnnotatedMethodBeanPostProcessor.java @@ -17,7 +17,9 @@ package org.springframework.ai.mcp.annotation.spring.scan; import java.lang.annotation.Annotation; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; @@ -33,6 +35,8 @@ public abstract class AbstractAnnotatedMethodBeanPostProcessor extends Annotated private final AbstractMcpAnnotatedBeans registry; + private final Map, Set>> annotationCache = new ConcurrentHashMap<>(); + public AbstractAnnotatedMethodBeanPostProcessor(AbstractMcpAnnotatedBeans registry, Set> targetAnnotations) { super(targetAnnotations); @@ -44,7 +48,7 @@ public AbstractAnnotatedMethodBeanPostProcessor(AbstractMcpAnnotatedBeans regist @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Class beanClass = AopUtils.getTargetClass(bean); // Handle proxied beans - Set> foundAnnotations = scan(beanClass); + Set> foundAnnotations = this.annotationCache.computeIfAbsent(beanClass, this::scan); // Register the bean if it has any of our target annotations if (!foundAnnotations.isEmpty()) { this.registry.addMcpAnnotatedBean(bean, foundAnnotations); diff --git a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/scan/AbstractAnnotatedMethodBeanPostProcessorTests.java b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/scan/AbstractAnnotatedMethodBeanPostProcessorTests.java index 9bb22ab68ff..b46fe65f504 100644 --- a/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/scan/AbstractAnnotatedMethodBeanPostProcessorTests.java +++ b/mcp/mcp-annotations/src/test/java/org/springframework/ai/mcp/annotation/spring/scan/AbstractAnnotatedMethodBeanPostProcessorTests.java @@ -41,6 +41,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.same; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -143,6 +144,30 @@ void testCorrectAnnotationsAreCaptured() { assertTrue(capturedAnnotations.contains(TestAnnotation.class)); } + @Test + void scansUnannotatedClassOnlyOnce() { + var processor = spy(this.processor); + for (int i = 0; i < 100; i++) { + NoAnnotationBean bean = new NoAnnotationBean(); + assertSame(bean, processor.postProcessAfterInitialization(bean, "prototypeBean")); + } + verify(processor).scan(NoAnnotationBean.class); + verify(this.registry, never()).addMcpAnnotatedBean(any(), any()); + } + + @Test + void cachesAnnotationsWhileRegisteringEachBeanInstance() { + var processor = spy(this.processor); + AnnotatedBean first = new AnnotatedBean(); + AnnotatedBean second = new AnnotatedBean(); + processor.postProcessAfterInitialization(first, "first"); + processor.postProcessAfterInitialization(second, "second"); + + verify(processor).scan(AnnotatedBean.class); + verify(this.registry).addMcpAnnotatedBean(same(first), any()); + verify(this.registry).addMcpAnnotatedBean(same(second), any()); + } + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface TestAnnotation {