Java w swoim API dostarcza klas pozwalających na łatwą implementację wzorca pośrednik. Są to odpowiednio Proxy oraz InvocationHandler. Dzięki nim możliwe jest dynamiczne tworzenie pośredników dla klas. Implementacja ta wykorzystuje mechanizm oparty o refleksje i opiera się o interfejsy, stąd nie będzie możliwe użycie klasy nie implementującej żadnego interfejsu.

Dynamiczny pośrednik
Ktoś mógłby zapytać po co korzystać z tego rozwiązania skoro możemy statycznie stworzyć odpowiednie pośredniki? No właśnie kluczem jest tutaj dynamiczność. Dzięki temu możliwe jest dynamiczne nakładanie pośredników na nieznane dotąd interfejsy. Dlatego najczęściej to rozwiązanie stosuje się w przypadku różnym frameworków (np. Spring, Hibernate).
Jak z tego skorzystać?
Jako że, klasa Proxy wykorzystuje klasę InvocationHandler. A klasa InvocationHandler może (ale nie musi) wykorzystywać oryginalny obiekt, zacznijmy więc od stworzenia struktury – interfejsu, klasy – oraz obiektu:
interface SomeInterface {
String doSth(String arg);
}
static class ConcreteClass implements SomeInterface {
public String doSth(String arg) {
return arg.toUpperCase();
}
}
public static void main(String[] args) {
ConcreteClass concrete = new ConcreteClass();
}Następnie możemy przejść do stworzenia implementacji InvocationHandlera. To tutaj możemy wrzucić logikę odpowiednią dla naszego pośrednika. W naszym wypadku będzie to zwykłe wypisanie tekstu i wywołanie metody na oryginalnym obiekcie (patrz concerete):
public static void main(String[] args) {
ConcreteClass concrete = new ConcreteClass();
InvocationHandler invocationHandler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("In invocation handler");
return method.invoke(concrete, args);
}
};
}Czas na stworzenie pośrednika:
public static void main(String[] args) {
ConcreteClass concrete = new ConcreteClass();
InvocationHandler invocationHandler = //...
SomeInterface someInterface = (SomeInterface)Proxy.newProxyInstance(
SomeInterface.class.getClassLoader(),
new Class[]{SomeInterface.class},
invocationHandler);
}Jak widać konieczne jest jawne rzutowanie obiektu zwracanego przez metodę newProxyInstance. Jest to spowodowane tym, że metoda ta zwraca typ Object.
Przykład prostej biblioteki wspierającej programowanie aspektowe
Wzorzec pośrednik w wersji dynamicznej może być wykorzystany do różnych celów. Jednym z ciekawych zastosowań jest programowanie aspektowe. Spróbujemy stworzyć prosty przykład pokazujący jego możliwości. W naszym przykładzie będzie on wspierał wykonywanie jakiegoś kodu przed wykonaniem się metod danego interfejsu. Dlatego zdefiniujmy sobie szkielet takiego aspektu:
static class InterfaceAspect{
private Class<?> interfaceClass; // jakiego interfejsu dotyczy
private Runnable runnable; // co ma sie zadziac
//constructors
//getters
}Następnie zaimplementujmy logikę działania aspektu poprzez rozszerzenie klasy InvocationHandler:
static class AOPHandler implements InvocationHandler{
private final Object original;
private final List<InterfaceAspect> beforeAspects;
public AOPHandler(Object original, List<InterfaceAspect> beforeAspects) {
this.original = original;
this.beforeAspects = beforeAspects;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
runBeforeAspects(proxy);
method.setAccessible(true);
return method.invoke(original, args);
}
private void runBeforeAspects(Object proxy) {
for (InterfaceAspect beforeAspect : beforeAspects) {
if (beforeAspect.getInterfaceClass().isAssignableFrom(proxy.getClass())){
beforeAspect.runnable.run();
}
}
}
}Klasa AOPHandler przyjmuje w konstruktorze oryginalny obiekt jak i listę aspektów do wykonania przed wywołaniem metody na oryginalnym obiekcie (stąd nazwa beforeAspects). W metodzie invoke uruchamiana jest metoda runBeforeAspects, która to uruchamia kod jeśli obiekt implementuje dany interfejs.
Jak to działa w akcji możemy zobaczyć tutaj:
public class AOPContainerTest {
private static int counter = 0;
interface StringTransformer{
String doTheJob(String arg);
}
interface IntegerTransformer{
Integer doTheJob(Integer arg);
}
static class ToUpperCaseTransformer implements StringTransformer {
public String doTheJob(String arg) {
return arg.toUpperCase();
}
}
static class PlusOneTransformer implements IntegerTransformer {
public Integer doTheJob(Integer arg) {
return arg + 1;
}
}
@Test
public void shouldAddAspectForStringTransformerOnly() {
AOPContainer.addBeforeAspect(StringTransformer.class, () -> counter++);
AOPContainer.add("ToUpperCaseTransformer", new ToUpperCaseTransformer(), StringTransformer.class);
AOPContainer.add("PlusOneTransformer", new PlusOneTransformer(), IntegerTransformer.class);
AOPContainer.get("ToUpperCaseTransformer", StringTransformer.class).doTheJob("test");
AOPContainer.get("PlusOneTransformer", IntegerTransformer.class).doTheJob(22);
assertEquals(1, counter);
}
}Oczywiście słabą praktyką jest wykorzystywanie statycznych pól w testach, ale na potrzeby prostego testu można czasami nagiąć dobre praktyki 😉
Brak wsparcia dla klas nie implementujących interfejsu
Niestety klasy Proxy i InvocationHandler nie pozwalają na obsługę klas nie implementujących interfejsu. Żeby móc zrealizować dynamiczne nakładanie pośrednika na takie klasy, można wykorzystać bibliotekę cglib.
