Статьи

Прокси сделаны правильно с помощью Гуавы AbstractInvocationHandler

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

Справка: Прокси сделаны правильно с помощью AbstractInvocationHandler от Guava от нашего партнера по JCG Томаша Нуркевича из блога Java и соседей .