diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AdapterFinderInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AdapterFinderInterceptor.java
new file mode 100644
index 000000000000..bb374d173320
--- /dev/null
+++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AdapterFinderInterceptor.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aop.interceptor;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.aop.framework.ProxyFactory;
+import org.springframework.beans.factory.AdapterFinderBean;
+/**
+ * A proxy interceptor for finding a concrete adapter implementation of an abstracted interface using a
+ * {@link AdapterFinderBean}. This is in the vein of
+ * {@link org.springframework.beans.factory.config.ServiceLocatorFactoryBean}, but also without the client code
+ * pollution of acquiring the service prior to calling the intended interceptor. The {@code AdapterFinderBean} uses
+ * the method and arguments to determine the appropriate concrete adapter to call.
+ *
+ *
By way of an example, consider the following adapter interface.
+ * Note that this interface is not dependent on any Spring APIs.
+ *
+ *
package a.b.c;
+ *
+ *public interface MyService {
+ *
+ * byte[] convert(IMAGE_TYPE to, IMAGE_TYPE from, byte[] source);
+ * enum IMAGE_TYPE {
+ * GIF,
+ * JPEG,
+ * PNG
+ * }
+ *}
+ *
+ * An {@link AdapterFinderBean}.
+ *
package a.b.c;
+ *
+ *public class MyServiceFinder implements AdapterFinderBean<MyService> {
+ *
+ * private final MyService gifService;
+ * private final MyService jpgService;
+ * private final MyService pngService;
+ *
+ * public MyServiceFinder(MyService gifService, MyService jpgService, MyService pngService) {
+ * this.gifService = gifService;
+ * this.jpgService = jpgService;
+ * this.pngService = pngService;
+ * }
+ *
+ * @Nullable MyService findBean(Method method, Object[] args) {
+ * IMAGE_TYPE type = (IMAGE_TYPE) args[0];
+ * if (type == GIF) {
+ * return gifService;
+ * }
+ *
+ * if (type == JPEG) {
+ * return jpgService;
+ * }
+ *
+ * if (type == PNG) {
+ * return pngService;
+ * }
+ *
+ * return null; // will throw an IllegalArgumentException!
+ * }
+ *}
+ *
+ * A spring configuration file.
+ *
package a.b.c;
+ *
+ *@Configuration
+ *class MyServiceConfiguration {
+ *
+ * @Bean
+ * MyServiceFinder myServiceFinder(MyGifService gifService, MyJpegService jpgService, MyPngService pngService) {
+ * return new MyServiceFinder(gifService, jpgService, pngService);
+ * }
+ *
+ * @Bean
+ * @Primary
+ * MyService myService(MyServiceFinder finder) {
+ * return AdapterFinderInterceptor.proxyOf(finder, MyService.class);
+ * }
+ *}
+ *
+ *
+ * A client bean may look something like this:
+ *
+ *
package a.b.c;
+ *
+ *public class MyClientBean {
+ *
+ * private final MyService myService;
+ *
+ * public MyClientBean(MyService myService) {
+ * this.myService = myService;
+ * }
+ *
+ * public void doSomeBusinessMethod(byte[] background, byte[] foreground, byte[] border) {
+ * byte[] gifBackground = myService.convert(PNG, GIF, background);
+ * byte[] gifForeground = myService.convert(PNG, GIF, foreground);
+ * byte[] gifBorder = myService.convert(PNG, GIF, border);
+ *
+ * // no do something with the gif stuff.
+ * }
+ *}
+ *
+ * @author Joe Chambers
+ * @param the service the interceptor proxy's.
+ */
+public final class AdapterFinderInterceptor implements MethodInterceptor {
+
+ private final AdapterFinderBean finder;
+
+ /**
+ * Constructor.
+ * @param finder the {@code AdapterFinder} to use for obtaining concrete instances
+ */
+ private AdapterFinderInterceptor(AdapterFinderBean finder) {
+ this.finder = finder;
+ }
+
+ /**
+ * The implementation of the {@link MethodInterceptor#invoke(MethodInvocation)} method called by the proxy.
+ * @param invocation the method invocation joinpoint
+ * @return the results of the concrete invocation call
+ * @throws Throwable if no adapter is found will throw {@link IllegalArgumentException} otherwise will re-throw what the concrete invocation throws
+ */
+ @Override
+ public @Nullable Object invoke(@NonNull MethodInvocation invocation) throws Throwable {
+ T implementation = this.finder.findAdapter(invocation.getMethod(), invocation.getArguments());
+ if (implementation == null) {
+ throw new IllegalArgumentException("Adapter not found: " + invocation.getMethod());
+ }
+
+ try {
+ return invocation.getMethod().invoke(implementation, invocation.getArguments());
+ }
+ catch (InvocationTargetException ex) {
+ throw ex.getTargetException();
+ }
+ }
+
+ /**
+ * Create a proxy using an {@code AdapterFinderInterceptor}.
+ * @param finder the finder bean to create the proxy around.
+ * @param proxyClass the {@link Class} of the {@code Interface} being exposed.
+ * @param the type of the interface the proxy exposes.
+ * @return a {@code Proxy} that uses the finder to determine which adapter to direct on a call by call basis.
+ */
+ public static T proxyOf(AdapterFinderBean finder, Class proxyClass) {
+ return ProxyFactory.getProxy(proxyClass, new AdapterFinderInterceptor<>(finder));
+ }
+}
diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/AdapterFinderInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/AdapterFinderInterceptorTests.java
new file mode 100644
index 000000000000..24e443e1cd92
--- /dev/null
+++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/AdapterFinderInterceptorTests.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aop.interceptor;
+
+import java.lang.reflect.Method;
+
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import org.springframework.beans.factory.AdapterFinderBean;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for class {@link AdapterFinderInterceptor}
+ *
+ * @author Joe Chambers
+ */
+@ExtendWith(MockitoExtension.class)
+class AdapterFinderInterceptorTests {
+
+ @Mock
+ EvenOddService evenService;
+
+ @Mock
+ EvenOddService oddService;
+
+ @Spy
+ EvenOddServiceFinder evenOddFinder = new EvenOddServiceFinder();
+
+ Method expectedMethod;
+
+ EvenOddService evenOddService;
+
+ @BeforeEach
+ void setup() throws Exception {
+ evenOddService = AdapterFinderInterceptor.proxyOf(evenOddFinder, EvenOddService.class);
+ expectedMethod = EvenOddService.class.getMethod("toMessage", int.class, String.class);
+ }
+
+ @Test
+ void callEvenService() {
+ String expectedMessage = "4 even message";
+
+ given(evenService.toMessage(eq(4), eq("message"))).willReturn(expectedMessage);
+
+ String actualMessage = evenOddService.toMessage(4, "message");
+
+ assertThat(actualMessage)
+ .isEqualTo(expectedMessage);
+
+ verify(evenService).toMessage(eq(4), eq("message"));
+ verify(oddService, never()).toMessage(anyInt(), anyString());
+ verify(evenOddFinder).findAdapter(eq(expectedMethod), eq(new Object[] { 4, "message" }));
+ }
+
+ @Test
+ void callOddService() {
+ String expectedMessage = "5 odd message";
+
+ given(oddService.toMessage(eq(5), eq("message")))
+ .willReturn(expectedMessage);
+
+ String actualMessage = evenOddService.toMessage(5, "message");
+
+ assertThat(actualMessage)
+ .isEqualTo(expectedMessage);
+
+ verify(oddService).toMessage(eq(5), eq("message"));
+ verify(evenService, never()).toMessage(anyInt(), anyString());
+ verify(evenOddFinder).findAdapter(eq(expectedMethod), eq(new Object[] { 5, "message" }));
+ }
+
+ @Test
+ void throwExceptionWhenNumberIsZero() {
+ String expectedMessage = "Adapter not found: public abstract java.lang.String org.springframework.aop.interceptor.AdapterFinderInterceptorTests$EvenOddService.toMessage(int,java.lang.String)";
+
+ assertThatThrownBy(() -> evenOddService.toMessage(0, "message"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(expectedMessage)
+ .hasNoCause();
+
+ verify(evenService, never()).toMessage(anyInt(), anyString());
+ verify(oddService, never()).toMessage(anyInt(), anyString());
+ verify(evenOddFinder).findAdapter(eq(expectedMethod), eq(new Object[] { 0, "message" }));
+ }
+
+ protected interface EvenOddService {
+ String toMessage(int number, String message);
+ }
+
+ protected class EvenOddServiceFinder implements AdapterFinderBean {
+ @Override
+ @Nullable
+ public EvenOddService findAdapter(Method method, @Nullable Object[] args) {
+ if (method.getParameterCount() > 0 && method.getParameterTypes()[0] == int.class && args[0] != null) {
+ int number = (int) args[0];
+ if (number != 0) {
+ return ((number % 2 == 0) ? evenService : oddService);
+ }
+ }
+ return null; // method not found, or 0.
+ }
+ }
+}
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/AdapterFinderBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/AdapterFinderBean.java
new file mode 100644
index 000000000000..a4e34736eae7
--- /dev/null
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/AdapterFinderBean.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.beans.factory;
+
+import java.lang.reflect.Method;
+
+import org.jspecify.annotations.Nullable;
+
+/**
+ * A {@code bean} to locate an {@code Adapter} based on the method called and
+ * the parameters passed in.
+ *
+ * For use when multiple implementations of an {@code interface} (Adapters) exist
+ * to handle functionality in various ways such as sending and monitoring shipments
+ * from different providers. The {@link AdapterFinderBean} can look at the method
+ * parameters and determine which shipping provider {@code Adapter} to use.
+ *
+ *
If the {@code AdapterFinderBean} cannot find an implementation appropriate for
+ * the parameters, then it will return {@code null}.
+ *
+ * @author Joe Chambers
+ * @param the service type the finder returns
+ */
+public interface AdapterFinderBean {
+
+ /**
+ * Lookup the adapter appropriate for the {@link Method} and {@code Arguments}
+ * passed to the implementation.
+ * @param method the {@link Method} being called
+ * @param args the {@code Arguments} being passed to the invocation
+ * @return the implementation of the {@code Adapter} that is appropriate or {@code null}
+ */
+ @Nullable
+ T findAdapter(Method method, @Nullable Object[] args);
+}