Статьи

Прокси-серверы выполнены правильно с помощью Guava’s AbstractInvocationHandler

Не слишком часто, но иногда мы вынуждены писать собственный  динамический прокси-класс,  используя java.lang.reflect.Proxy. В этом механизме нет ничего магического, и стоит знать, что вы никогда не будете его использовать, потому что прокси-серверы Java широко распространены в различных инфраструктурах и библиотеках.

Идея довольно проста: динамически создать объект, который реализует один или несколько интерфейсов, но каждый раз, когда любой метод этих интерфейсов вызывается, наш пользовательский обработчик обратного вызова вызывается. Этот обработчик получает дескриптор метода, который был вызван ( java.lang.reflect.Method instance), и может вести себя любым образом. Прокси-серверы часто используются для реализации бесшовного моделирования, кэширования, транзакций, безопасности — то есть они являются основой для АОП.

Прежде чем объяснить, какова цельcom.google.common.reflect.AbstractInvocationHandler из названия давайте начнем с простого примера. Скажем, мы хотим прозрачно запускать методы данного интерфейса асинхронно в пуле потоков. Популярные стеки, такие как Spring (see:)  27.4.3 The @Async Annotationи Java EE (see:),  Asynchronous Method Invocationуже поддерживают это, используя ту же технику.

Представьте, что у нас есть следующий сервис:

public interface MailServer {
void send(String msg);
int unreadCount();
}

Наша цель — работать  send() асинхронно, чтобы несколько последующих вызовов не блокировались, а ставились в очередь и выполнялись одновременно во внешнем пуле потоков, а не в вызывающем потоке. Сначала нам нужен заводской код, который создаст экземпляр прокси:

public class  AsyncProxy {
public static <T> T wrap(T underlying, ExecutorService pool) {
final ClassLoader classLoader = underlying.getClass().getClassLoader();
final Class<T> intf = (Class<T>) underlying.getClass().getInterfaces()[0];
return (T)Proxy.newProxyInstance(
classLoader,
new Class<?>[] {intf},
new AsyncHandler<T>(underlying, pool));
}
}

Приведенный выше код делает несколько смелых предположений, например, что  underlying объект (реальный экземпляр, который мы проксируем) реализует ровно один интерфейс. В реальной жизни класс, конечно, может реализовывать несколько интерфейсов, так же как и прокси-серверы, но мы немного упрощаем это для образовательных целей. Теперь для начала мы создадим прокси-сервер no-op, который делегирует базовый объект без какого-либо дополнительного значения:

class AsyncHandler<T> implements InvocationHandler {
private static final Logger log = LoggerFactory.getLogger(AsyncHandler.class);
private final T underlying;
private final ExecutorService pool;
AsyncHandler1(T underlying, ExecutorService pool) {
this.underlying = underlying;
this.pool = pool;
}
@Override
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
return method.invoke(underlying, args);
}
}

ExecutorService pool будет использоваться позже. Последняя строка имеет решающее значение — мы вызываем  method на underlying экземпляр с тем же  args. На данный момент мы можем:

  • вызывать  underlying или нет (например, если данный вызов кэшируется / запоминается)
  • изменить аргументы (то есть в целях безопасности)
  • выполнить код до / после / вокруг / при исключении
  • изменить результат, возвращая другое значение (оно должно соответствовать типу method.getReturnType())
  • …и многое другое

В нашем случае мы будем обернуть  method.invoke() с  Callable и запустить его асинхронно:

class AsyncHandler<T> implements InvocationHandler {
private final T underlying;
private final ExecutorService pool;
AsyncHandler(T underlying, ExecutorService pool) {
this.underlying = underlying;
this.pool = pool;
}
@Override
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
final Future<Object> future = pool.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
return method.invoke(underlying, args);
}
});
return handleResult(method, future);
}
private Object handleResult(Method method, Future<Object> future) throws Throwable {
if (method.getReturnType() == void.class)
return null;
try {
return future.get();
} catch (ExecutionException e) {
throw e.getCause();
}
}
}

Дополнительный  handleResult() метод был извлечен, чтобы правильно обрабатывать не voidметоды. Использовать такой прокси просто:

final MailServer mailServer = new RealMailServer();
final ExecutorService pool = Executors.newFixedThreadPool(10);
final MailServer asyncMailServer = AsyncProxy.wrap(mailServer, pool);

Now even if RealMailServer.send() takes a second to complete, invoking it twice viaasyncMailServer.send() takes no time because both invocations run asynchronously in background.

Broken equals()hashCode() and toString()

Some developers are not aware of potential issues with default InvocationHandlerimplementation. Quoting the official documentation:

An invocation of the 
hashCode
equals, or 
toString methods declared in
java.lang.Object on a proxy instance will be encoded and dispatched to the invocation handler’s 
invoke method in the same manner as interface method invocations are encoded and dispatched, as described above.In our case case this means that for example 
toString() is executed in the same thread pool as other methods of 
MailServer, quite surprising. Now imagine you have a local proxy where every method invocation triggers remote call. Dispatching 
equals(),
hashCode() and 
toString() via network is definitely not what we want.

Fixing with AbstractInvocationHandler

AbstractInvocationHandler от Guava — это простой абстрактный класс, который правильно решает вышеуказанные проблемы. По умолчанию он отправляет 
equals()
hashCode() и 
toString() в
Object классе , а не передавать его обработчик вызова. Рефакторинг от простого
InvocationHandler к 
AbstractInvocationHandler мертвому простому:

import com.google.common.reflect.AbstractInvocationHandler;
class AsyncHandler<T> extendsAbstractInvocationHandler {
//...
@Override
protected Object handleInvocation(Object proxy, finalMethod method, finalObject[] args) throwsThrowable {
//...
}
@Override
public String toString() {
return"Proxy of "+ underlying;
}
}

Это оно! Я решил переопределить, 
toString() чтобы помочь отладке. 
equals() и
hashCode() унаследованы из 
Object которых хорошо для начала. Теперь, пожалуйста, осмотрите свою базу кода и найдите собственные прокси. Если вы AbstractInvocationHandler до сих пор не пользовались или не пользовались
подобным, скорее всего, вы вносите несколько незначительных ошибок.