Compare commits

...

4 Commits

Author SHA1 Message Date
Joe Chambers 8b1cf5b3e5
Merge 19459d09ec into 7e6874ad80 2025-10-07 23:10:35 +03:00
Sam Brannen 7e6874ad80 Polish @⁠Autowired section of the reference manual
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions Details
Deploy Docs / Dispatch docs deployment (push) Waiting to run Details
2025-10-07 17:17:27 +02:00
Sam Brannen 097463e3b7 Remove outdated reference to JSR 305 in the reference documentation
Closes gh-35580
2025-10-07 17:10:40 +02:00
Joe Chambers 19459d09ec Adding AdapterFinderBean and interceptor functionality.
AdapterFinderBean and AdapterFinderInterceptor allow for similar
functionality to ServiceLocatorFactoryBean but without the call from
the client to find the appropriate service bean for each call.

It would be anticipated that spring-boot functionality would be
created in the future to search the bean definition registry after
the registry is created to find finder beans and build proxies for
the interfaces exposed by them and register those implementations
within the application context.

Signed-off-by: Joe Chambers <25091819+joe-chambers@users.noreply.github.com>
2025-04-11 21:35:12 -04:00
4 changed files with 370 additions and 26 deletions

View File

@ -37,18 +37,18 @@ Kotlin::
----
======
[NOTE]
[TIP]
====
As of Spring Framework 4.3, an `@Autowired` annotation on such a constructor is no longer
necessary if the target bean defines only one constructor to begin with. However, if
several constructors are available and there is no primary/default constructor, at least
one of the constructors must be annotated with `@Autowired` in order to instruct the
container which one to use. See the discussion on
xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution] for details.
An `@Autowired` annotation on such a constructor is not necessary if the target bean
defines only one constructor. However, if several constructors are available and there is
no primary or default constructor, at least one of the constructors must be annotated
with `@Autowired` in order to instruct the container which one to use. See the discussion
on xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution]
for details.
====
You can also apply the `@Autowired` annotation to _traditional_ setter methods,
as the following example shows:
You can apply the `@Autowired` annotation to _traditional_ setter methods, as the
following example shows:
[tabs]
======
@ -84,8 +84,8 @@ Kotlin::
----
======
You can also apply the annotation to methods with arbitrary names and multiple
arguments, as the following example shows:
You can apply `@Autowired` to methods with arbitrary names and multiple arguments, as the
following example shows:
[tabs]
======
@ -176,14 +176,15 @@ Kotlin::
====
Make sure that your target components (for example, `MovieCatalog` or `CustomerPreferenceDao`)
are consistently declared by the type that you use for your `@Autowired`-annotated
injection points. Otherwise, injection may fail due to a "no type match found" error at runtime.
injection points. Otherwise, injection may fail due to a "no type match found" error at
runtime.
For XML-defined beans or component classes found via classpath scanning, the container
usually knows the concrete type up front. However, for `@Bean` factory methods, you need
to make sure that the declared return type is sufficiently expressive. For components
that implement several interfaces or for components potentially referred to by their
implementation type, consider declaring the most specific return type on your factory
method (at least as specific as required by the injection points referring to your bean).
implementation type, declare the most specific return type on your factory method (at
least as specific as required by the injection points referring to your bean).
====
.[[beans-autowired-annotation-self-injection]]Self Injection
@ -312,8 +313,8 @@ through `@Order` values in combination with `@Primary` on a single bean for each
====
Even typed `Map` instances can be autowired as long as the expected key type is `String`.
The map values contain all beans of the expected type, and the keys contain the
corresponding bean names, as the following example shows:
The map values are all beans of the expected type, and the keys are the corresponding
bean names, as the following example shows:
[tabs]
======
@ -431,7 +432,7 @@ annotated constructor does not have to be public.
====
Alternatively, you can express the non-required nature of a particular dependency
through Java 8's `java.util.Optional`, as the following example shows:
through Java's `java.util.Optional`, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@ -445,8 +446,8 @@ through Java 8's `java.util.Optional`, as the following example shows:
----
You can also use a parameter-level `@Nullable` annotation (of any kind in any package --
for example, `javax.annotation.Nullable` from JSR-305) or just leverage Kotlin built-in
null-safety support:
for example, `org.jspecify.annotations.Nullable` from JSpecify) or just leverage Kotlin's
built-in null-safety support:
[tabs]
======
@ -477,13 +478,6 @@ Kotlin::
----
======
[NOTE]
====
A type-level `@Nullable` annotation such as from JSpecify is not supported in Spring
Framework 6.2 yet. You need to upgrade to Spring Framework 7.0 where the framework
detects type-level annotations and consistently declares JSpecify in its own codebase.
====
You can also use `@Autowired` for interfaces that are well-known resolvable
dependencies: `BeanFactory`, `ApplicationContext`, `Environment`, `ResourceLoader`,
`ApplicationEventPublisher`, and `MessageSource`. These interfaces and their extended
@ -528,5 +522,6 @@ class MovieRecommender {
The `@Autowired`, `@Inject`, `@Value`, and `@Resource` annotations are handled by Spring
`BeanPostProcessor` implementations. This means that you cannot apply these annotations
within your own `BeanPostProcessor` or `BeanFactoryPostProcessor` types (if any).
These types must be 'wired up' explicitly by using XML or a Spring `@Bean` method.
====

View File

@ -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.
*
* <p>By way of an example, consider the following adapter interface.
* Note that this interface is not dependent on any Spring APIs.
*
* <pre class="code">package a.b.c;
*
*public interface MyService {
*
* byte[] convert(IMAGE_TYPE to, IMAGE_TYPE from, byte[] source);
* enum IMAGE_TYPE {
* GIF,
* JPEG,
* PNG
* }
*}</pre>
*
* <p>An {@link AdapterFinderBean}.
* <pre class="code">package a.b.c;
*
*public class MyServiceFinder implements AdapterFinderBean&lt;MyService&gt; {
*
* 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;
* }
*
* &#064;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!
* }
*}</pre>
*
* <p>A spring configuration file.
* <pre class="code">package a.b.c;
*
*&#064;Configuration
*class MyServiceConfiguration {
*
* &#064;Bean
* MyServiceFinder myServiceFinder(MyGifService gifService, MyJpegService jpgService, MyPngService pngService) {
* return new MyServiceFinder(gifService, jpgService, pngService);
* }
*
* &#064;Bean
* &#064;Primary
* MyService myService(MyServiceFinder finder) {
* return AdapterFinderInterceptor.proxyOf(finder, MyService.class);
* }
*}
* </pre>
*
* <p>A client bean may look something like this:
*
* <pre class="code">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.
* }
*}</pre>
*
* @author Joe Chambers
* @param <T> the service the interceptor proxy's.
*/
public final class AdapterFinderInterceptor<T> implements MethodInterceptor {
private final AdapterFinderBean<T> finder;
/**
* Constructor.
* @param finder the {@code AdapterFinder} to use for obtaining concrete instances
*/
private AdapterFinderInterceptor(AdapterFinderBean<T> 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 <T> 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> T proxyOf(AdapterFinderBean<T> finder, Class<T> proxyClass) {
return ProxyFactory.getProxy(proxyClass, new AdapterFinderInterceptor<>(finder));
}
}

View File

@ -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<EvenOddService> {
@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.
}
}
}

View File

@ -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.
*
* <p>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.
*
* <p>If the {@code AdapterFinderBean} cannot find an implementation appropriate for
* the parameters, then it will return {@code null}.
*
* @author Joe Chambers
* @param <T> the service type the finder returns
*/
public interface AdapterFinderBean<T> {
/**
* 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);
}