Не слишком часто, но иногда мы вынуждены писать собственный динамический прокси-класс, используя java.lang.reflect.Proxy
. В этом механизме нет ничего магического, и стоит знать, что вы никогда не будете его использовать, потому что прокси-серверы Java широко распространены в различных инфраструктурах и библиотеках.
Идея довольно проста: динамически создать объект, который реализует один или несколько интерфейсов, но каждый раз, когда любой метод этих интерфейсов вызывается, наш пользовательский обработчик обратного вызова вызывается. Этот обработчик получает дескриптор метода, который был вызван (экземпляр java.lang.reflect.Method
), и он может вести себя любым образом. Прокси-серверы часто используются для реализации бесшовного моделирования, кэширования, транзакций, безопасности — то есть они являются основой для АОП.
Прежде чем я объясню, какова цель com.google.common.reflect.AbstractInvocationHandler
из заголовка, давайте начнем с простого примера. Скажем, мы хотим прозрачно запускать методы данного интерфейса асинхронно в пуле потоков. Популярные стеки, такие как Spring (см .: 27.4.3 The @Async Annotation
) и Java EE (см. Asynchronous Method Invocation
), уже поддерживают это, используя ту же технику.
Представьте, что у нас есть следующий сервис:
1
2
3
4
|
public interface MailServer { void send(String msg); int unreadCount(); } |
Наша цель — запустить send()
асинхронно, чтобы несколько последующих вызовов не блокировали, а ставили в очередь и выполнялись одновременно в пуле внешних потоков, а не в вызывающем потоке. Сначала нам нужен заводской код, который создаст экземпляр прокси:
01
02
03
04
05
06
07
08
09
10
|
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, который делегирует базовый объект без какого-либо дополнительного значения:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
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
и запустим его асинхронно:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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()
дополнительный handleResult()
для правильной обработки не void
методов. Использовать такой прокси просто:
1
2
3
4
|
final MailServer mailServer = new RealMailServer(); final ExecutorService pool = Executors.newFixedThreadPool( 10 ); final MailServer asyncMailServer = AsyncProxy.wrap(mailServer, pool); |
Теперь, даже если RealMailServer.send()
требуется секунда для завершения, asyncMailServer.send()
его дважды с помощью asyncMailServer.send()
занимает много времени, потому что оба вызова выполняются асинхронно в фоновом режиме.
Сломанный equals()
, hashCode()
и toString()
Некоторые разработчики не знают о потенциальных проблемах с реализацией InvocationHandler
умолчанию. Цитирование официальной документации :
Вызов методов
hashCode
,equals
илиtoString
объявленных вjava.lang.Object
для экземпляра прокси, будет кодироваться и отправляться методу вызова обработчикаinvoke
таким же образом, как и вызовы метода интерфейса, кодируются и отправляются, как описано выше.
В нашем случае это означает, что, например, toString()
выполняется в том же пуле потоков, что и другие методы MailServer
, что довольно удивительно. Теперь представьте, что у вас есть локальный прокси, где каждый вызов метода вызывает удаленный вызов. Диспетчеризация equals()
, hashCode()
и toString()
через сеть определенно не то, что мы хотим.
Исправление с помощью AbstractInvocationHandler
AbstractInvocationHandler
из Guava — это простой абстрактный класс, который правильно решает вышеуказанные проблемы. По умолчанию он отправляет equals()
, hashCode()
и toString()
в класс Object
а не передает его в обработчик вызова. Рефакторинг от прямого InvocationHandler
к AbstractInvocationHandler
очень прост:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
import com.google.common.reflect.AbstractInvocationHandler; class AsyncHandler<T> extends AbstractInvocationHandler { //... @Override protected Object handleInvocation(Object proxy, final Method method, final Object[] args) throws Throwable { //... } @Override public String toString() { return "Proxy of " + underlying; } } |
Это оно! Я решил переопределить toString()
чтобы помочь отладке. equals()
и hashCode()
наследуются от Object
что хорошо для начала. Теперь, пожалуйста, осмотрите свою базу кода и найдите собственные прокси. Если вы до сих пор не использовали AbstractInvocationHandler
или подобное, скорее всего, вы вносите несколько незначительных ошибок.