This commit is contained in:
Joe Chambers 2025-06-30 23:28:26 +03:00 committed by GitHub
commit c3029c0fc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 349 additions and 0 deletions

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);
}