JAVA: Каковы преимущества InvocationHandler по сравнению с реализацией интерфейса?

Сегодня в классе мы говорили об отражении в программировании на Java. Часть сегодняшнего урока была посвящена использованию InvocationHandler в Java, а не просто реализации интерфейса. Когда я спросил учителя, каковы преимущества использования обработчика вызовов, однозначного ответа не было. Итак, скажем, у нас есть интерфейсный плагин

public interface Plugin {
    void calculate(double a, double b);
    String getCommand();
}

Вы можете легко реализовать этот интерфейс в классе Multiply

public class Multiply implements Plugin {
    @Override
    public void calculate(double a, double b){
         return a * b;
    }

    @Override
    public String getCommand(){
         return "*";
    }
}

Тогда почему я предпочел бы другую реализацию, использующую InvocationHandler?

 public class MyMock {
     public static Object createMock(Class myClass) {
         InvocationHandler handler = new MyInvocationHandler();
         Object result = Proxy.newProxyInstance(myClass.getClassLoader(), new Class[]{myClass}, handler);
         return result;
     }
 }

Заранее спасибо:)

1 ответ

Решение

Proxy это dynamic proxyЭто означает, что он дает вам большую гибкость во время выполнения, тогда как с интерфейсами и реализующими классами все должно решаться во время компиляции.

Например, предположим, что по какой-то причине вы хотите переопределить возвращаемые значения на null для определенных вызовов методов в течение ночи. Если бы вы реализовали это статически, вам нужно было бы написать логику во все классы с чем-то вроде

if(isNight())
    return null;
return normalValue;

Однако с ProxyВы можете написать приведенную выше логику в InvocationHandler и нормальные классы даже не будут знать, что их значения не используются в течение ночи. Это также позволяет вам иметь несколько InvocationHandlersТаким образом, вы можете запустить свой код с параметрами, чтобы решить, хотите ли вы регистрировать вызовы, предотвращать вызовы по соображениям безопасности или любые другие подобные вещи, что было бы совершенно невозможно сделать со статическими реализациями.

Приведенный выше пример, возможно, не совсем ясен, но в основном это проблема дизайна, а не того, что один может делать то, чего не может другой.

InvocationHandler вместе с Proxyразрешить реализацию интерфейса во время выполнения без ошибок компиляции кода интерфейса. Он часто используется для обеспечения доступа к объекту класса, который реализует тот же интерфейс.Proxy не позволяет изменять поведение существующих объектов или классов.

Например, его можно использовать в удаленном вызове метода на стороне клиента, перенаправляя вызов метода по сети на сервер.

Мое первое использование Proxyбыл для регистрации вызовов методов к широкому интерфейсу, который представлял команду, полученную в проводном формате. Это легко давало очень согласованные выходные данные отладки, но требовало небольшого обслуживания при изменении интерфейса.

Интерфейсы аннотаций Java могут быть представлены Proxy прокси-объект во время выполнения, чтобы предотвратить взрыв классов.

java.beans.EventHandler был полезен до появления лямбда-выражений и ссылок на методы для реализации слушателей событий без раздувания jar-файлов.

Что касается более конкретного или реального примера, вы можете столкнуться с подобным типом отражения, чаще используя сторонний API или API с открытым исходным кодом. Очень популярным примером этого может быть майнкрафт, в частности Bukkit/Spigot.

Этот api используется для написания плагинов, которые затем загружает и запускает главный сервер. Это означает, что вы не на 100% контролируете часть кода, который существует в этой кодовой базе, предлагая решения с использованием отражения. В частности, если вы хотите перехватывать вызовы, выполняемые в API (или даже в API другого плагина, например Vault для знакомых), вы можете использоватьProxy.

Мы будем придерживаться примера minecraft, но здесь мы расстаемся с api bukkit (и делаем вид, что он не принимает PR). Скажем, есть часть API, что просто не совсем работает так, как вам нужно.

public interface Player {
    //This method handles all damage! Hooray!
    public void damagePlayer(Player source, double damage);
}

Это здорово, но если мы хотим закодировать что-то, где мы могли бы узнать, был ли поврежден проигрыватель (может быть, для создания крутых эффектов?), Нам нужно будет изменить исходный код (невозможно для распределенных плагинов), или мы бы нужно найти способ выяснить, когда #damagePlayerбыл вызван и с какими ценностями. Итак, приходитProxy:

public class PlayerProxy implements IvocationHandler {

    private final Player src;

    public PlayerProxy(Player src) {
        this.src = src;
    }

    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
        //Proceed to call the original Player object to adhere to the API
        Object back = m.invoke(this.src, args);
        if (m.getName().equals("damagePlayer") && args.length == 2) {
            //Add our own effects!
            //Alternatively, add a hook so you can register multiple things here, and avoid coding directly inside a Proxy
            if (/* 50% random chance */) {
                //double damage!
                args[1] = (double) args[1] * 2;
                //or perhaps use `source`/args[0] to add to a damage count?
            }
        }
    }
}

С помощью нашего прокси мы фактически создали поддельный класс Player, который будет просто вызывать методы на месте дляPlayer. Если нашPlayerProxy вызывается с myPlayerProxy.someOtherMethod(...), то он с радостью передаст вызов myPlayerProxy.src.someOtherMethod(...) через отражение (m#invoke в методе выше).

Проще говоря, вы подбираете объекты в библиотеке под свои нужды:

//we'll use this to demonstrate "replacing" the player variables inside of the server
Map<String, Player> players = /* a reflected instance of the server's Player objects, mapped by name. Convenient! */;
players.replaceAll((name, player) -> 
    (PlayerProxy) Proxy.newProxyInstance(/* class loader */, new Class<?>[]{Player.class}, new PlayerProxy(player)));

InvocationHandler также может обрабатывать несколько интерфейсов. Используя общийObject чтобы передать вызовы, вы можете прослушивать различные методы в API, все в одном Proxy пример.

Другие вопросы по тегам