3 changed files with 349 additions and 0 deletions
@ -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<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!
|
||||||
|
* } |
||||||
|
*}</pre> |
||||||
|
* |
||||||
|
* <p>A spring configuration file. |
||||||
|
* <pre class="code">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); |
||||||
|
* } |
||||||
|
*} |
||||||
|
* </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)); |
||||||
|
} |
||||||
|
} |
||||||
@ -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.
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
Loading…
Reference in new issue