Не слишком часто, но иногда мы вынуждены писать собственный динамический прокси-класс, используя 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 InvocationHandler
implementation. 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
до сих пор не пользовались или не пользовались
подобным, скорее всего, вы вносите несколько незначительных ошибок.