Наверно каждому java разработчику рано или поздно потребуется использовать прокси-классы.
Под катом представлены простые примеры, выполненные при помощи JDK proxy, cglib, javassist и byte buddy.


image

Поставим себе самую простую задачу:


  • создать прокси-класс для экземпляра класса User
  • в прокси-классе необходимо перехватить метод с названием "getName"
  • результат вывода перехваченного метода должен быть в upper case

Класс пользователя, над которым будем ставить эксперименты
public class User implements IUser {
    private final String name;

    public User() {
        this(null);
    }

    public User(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }
}

1 Стандартные средства — JDK proxy


Импорты
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

User user = new User("Вася");

InvocationHandler handler = (proxy, method, args) -> {
    if(method.getName().equals("getName")){
        return ((String)method.invoke(user, args)).toUpperCase();
    }
    return method.invoke(user, args);
};

IUser userProxy = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), User.class.getInterfaces(), handler);
assertEquals("ВАСЯ", userProxy.getName());

Недостаток, мы можем создать прокси-класс только который реализует интерфейсы класса User. Тоесть кастить прокси-класс в User нельзя


2 cglib


Импорты
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

User user = new User("Вася");

MethodInterceptor handler = (obj, method ,  args,  proxy) -> {
    if(method.getName().equals("getName")){
        return ((String)proxy.invoke(user, args)).toUpperCase() ;
    }
    return proxy.invoke(user, args);
};

User userProxy = (User) Enhancer.create(User.class, handler);
assertEquals("ВАСЯ", userProxy.getName());

3 Javassist


Импорты
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

User user = new User("Вася");

MethodHandler handler = (self, overridden, forwarder, args) -> {
    if(overridden.getName().equals("getName")){
        return ((String)overridden.invoke(user, args)).toUpperCase();
    }
    return overridden.invoke(user, args);
};

ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(User.class);
Object instance = factory.createClass().newInstance();
((ProxyObject) instance).setHandler(handler);

User userProxy = (User) instance;
assertEquals("ВАСЯ", userProxy.getName());

4 Byte Buddy


Импорты
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.MethodDelegation;
import static net.bytebuddy.matcher.ElementMatchers.named;

User user = new User("Вася");

User userProxy = new ByteBuddy()
    .subclass(User.class)
    .method(named("getName"))
    .intercept(MethodDelegation.to(new MyInterceptor(user)))
    .make()
    .load(User.class.getClassLoader())
    .getLoaded()
    .newInstance();

assertEquals("ВАСЯ", userProxy.getName());

MyInterceptor
public  class MyInterceptor {
    User user;

    public MyInterceptor(User user) {
        this.user = user;
    }

    public String getName() {
        return user.getName().toUpperCase();
    }
}

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


Для сравнения производительности предлагаю ознакомится со статьей
Testing the performance of 4 Java runtime code generators: cglib, javassist, JDK proxy & Byte Buddy

Комментарии (15)


  1. DSolodukhin
    13.02.2018 17:36

    А что для создания проксей используется в таких фреймворках, как Spring, Hibernate?


    1. arandomic
      13.02.2018 17:45

      jdk proxy и cglib


      1. Lure_of_Chaos
        14.02.2018 08:06

        еще можно вспомнить javassist


    1. PqDn Автор
      13.02.2018 18:25

      В Hibernate переходят на Byte Buddy


  1. olegchir
    13.02.2018 17:47

    Отличное начало! Теперь каждый из подходов можно развернуть сильней — получится цикл статей о проксировании.


  1. gkislin
    13.02.2018 18:33
    +1

    Похоже что Spring также собирается поддерживать Byte Buddy:
    https://jira.spring.io/browse/SPR-8190


    1. asm0dey
      14.02.2018 13:42

      Оно уже достаточно давно висит. И последний пост автора bytebuddy тоже уже давно. Фи гзнает, я движения не особо вижу. На гитхабе надо посмотреть, наверное.


  1. genew
    14.02.2018 11:39

    Кто-нибудь может объяснить зачем использовать прокси-классы, если есть наследование?
    Можно ведь создать класс — наследник User, переопределить в нем нужные методы, и вместо прокси-класса использовать этот класс-наследник?


    1. PqDn Автор
      14.02.2018 12:51

      Тут фишка в том что к уже созданному экземпляру мы подвязываем дополнительную логику и делаем это в runtime


    1. arandomic
      14.02.2018 12:52

      Dynamic proxy на то и dynamic, что они создаются на лету, в рантайме. На момент компиляции может не быть информации о том, какой класс мы будем проксировать.


    1. StanislavL
      14.02.2018 13:39

      Например на лету создать реализацию интерфейса. Класса как такового нету. Есть только прокси создаваемый из/для интерфейчса.


    1. ohotNik_alex
      14.02.2018 14:36

      самый простой пример. Аннотации спринга.
      Ставите над методом @transactional, а в прокси-классе создается обертка вокруг вызова.
      Частый вопрос на собеседовании — сработает ли такая аннотация если вызвать метод из того же класса, где он располагается. ответ — нет, так как произойдет вызов без обертки. чтобы сработал вызов нужно сделать @Autowire класса в себя и вызвать уже через поле.


      package ru.incbt.cds.api.rest.function;
      
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      import org.springframework.transaction.annotation.Transactional;
      
      /**
       * @author Okhonchenko Aleksander
       * @since 14.02.2018
       */
      @Service
      public class App {
      
          @Autowired
          private App app;
      
          @Transactional
          public void transactedMethod() {
              //some work with db
          }
      
          public void someMethod() {
              //вызов без обертки
              transactedMethod();
              //вызов с магической оберткой
              app.transactedMethod();
          }
      
      }


      1. PqDn Автор
        14.02.2018 14:56

        Не, частый вопрос это когда транзакция в транзакции)


  1. StanislavL
    14.02.2018 13:39

    Del


  1. dmitryk100
    14.02.2018 17:13

    Наверное если мы делаем динамический прокси, было бы логичнее использовать аргументы хэндлера а не завязываться на глобальный объект. Так например CGLib это будет вот так

    MethodInterceptor handler = (obj, method ,  args,  proxy) -> {
        if(method.getName().equals("getName")){
            return ((String)proxy.invokeSuper(proxy, args)).toUpperCase() ;
        }
        return proxy.invokeSuper(proxy, args);
    };