Статьи

CGLib: недостающее руководство

Библиотека инструментария байт-кода cglib является популярным выбором среди многих известных Java-фреймворков, таких как Hibernate ( больше ) или Spring, для выполнения своей грязной работы. Инструментарий байт-кода позволяет манипулировать или создавать классы после фазы компиляции Java-приложения. Поскольку классы Java динамически связаны во время выполнения, можно добавить новые классы в уже запущенную программу Java. Hibernate использует cglib, например, для генерации динамических прокси. Вместо возврата полного объекта, который вы сохранили в базе данных, Hibernate вернет вам инструментированную версию вашего хранимого класса, которая лениво загружает некоторые значения из базы данных только тогда, когда они запрашиваются. Spring использовал cglib, например, при добавлении ограничений безопасности к вызовам вашего метода. Вместо прямого вызова вашего метода, Spring Security сначала проверит, прошла ли указанная проверка безопасности, и делегирует только вашему фактическому методу после этой проверки. Другое популярное использование cglib — в рамках сред mocking , таких как mockito , где mocks — не более чем инструментальный класс, где методы были заменены пустыми реализациями (плюс некоторая логика отслеживания).

Помимо ASM — еще одной высокоуровневой библиотеки для манипулирования байтовым кодом, поверх которой построен cglib, — cglib предлагает довольно низкоуровневые преобразователи байтового кода, которые можно использовать, даже не зная деталей скомпилированного Java-класса. К сожалению, документация на cglib довольно короткая, не говоря уже о том, что ее в принципе нет. Помимо одной статьи в блоге 2005 года, в которой демонстрируется класс Enhancer, найти особо нечего. Эта статья блога является попыткой продемонстрировать cglib и его, к сожалению, часто неловкий API.

усилитель

Давайте начнем с класса Enhancer , вероятно, наиболее используемого класса библиотеки cglib. Энхансер позволяет создавать прокси Java для неинтерфейсных типов. Enhancer можно сравнить с классом Proxy стандартной библиотеки Java, который был представлен в Java 1.3. Enhancer динамически создает подкласс данного типа, но перехватывает все вызовы методов. За исключением класса Proxy , это работает как для классов, так и для типов интерфейсов. Следующий пример и некоторые примеры после основаны на этом простом Java POJO:

1
2
3
4
5
public static class SampleClass {
  public String test(String input) {
    return "Hello world!";
  }
}

Используя cglib, возвращаемое значение метода test(String) можно легко заменить другим значением, используя Enhancer и FixedValue вызов FixedValue :

01
02
03
04
05
06
07
08
09
10
11
12
13
@Test
public void testFixedValue() throws Exception {
  Enhancer enhancer = new Enhancer();
  enhancer.setSuperclass(SampleClass.class);
  enhancer.setCallback(new FixedValue() {
    @Override
    public Object loadObject() throws Exception {
      return "Hello cglib!";
    }
  });
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
}

В вышеприведенном примере, энхансер вернет экземпляр инструментированного подкласса SampleClass где все вызовы метода возвращают фиксированное значение, которое генерируется анонимной реализацией FixedValue выше. Объект создается с помощью Enhancer#create(Object...) где метод принимает любое количество аргументов, которые используются для выбора любого конструктора расширенного класса. (Даже если конструкторы являются только методами на уровне байт-кода Java, класс Enhancer не может инструктировать конструкторы. Он также не может инструктировать static или final классы.) Если вы хотите создать только класс, но не экземпляр, Enhancer#createClass создаст Экземпляр Class который можно использовать для динамического создания экземпляров. Все конструкторы расширенного класса будут доступны как конструкторы делегирования в этом динамически генерируемом классе.

Имейте в виду, что любой вызов метода будет делегирован в приведенном выше примере, а также вызовы методов, определенных в java.lang.Object . В результате вызов proxy.toString() также вернет «Hello cglib!». Напротив, вызов proxy.hashCode() приведет к ClassCastException поскольку перехватчик FixedValue всегда возвращает String даже если для сигнатуры Object#hashCode требуется примитивное целое число.

Другое наблюдение, которое можно сделать, заключается в том, что окончательные методы не перехватываются. Примером такого метода является Object#getClass который при вызове возвращает что-то вроде «SampleClass $$ EnhancerByCGLIB $$ e277c63c». Это имя класса генерируется случайным образом cglib, чтобы избежать конфликтов имен. Помните о другом классе расширенного экземпляра, когда вы используете явные типы в программном коде. Класс, сгенерированный cglib, однако, будет в том же пакете, что и расширенный класс (и, следовательно, сможет переопределять закрытые для пакета методы). Подобно конечным методам, подход с использованием подклассов делает невозможным расширение конечных классов. Поэтому фреймворки как Hibernate не могут сохраняться в финальных классах

Далее, давайте рассмотрим более мощный класс обратного вызова, InvocationHandler , который также можно использовать с Enhancer :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Test
public void testInvocationHandler() throws Exception {
  Enhancer enhancer = new Enhancer();
  enhancer.setSuperclass(SampleClass.class);
  enhancer.setCallback(new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable {
      if(method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) {
        return "Hello cglib!";
      } else {
        throw new RuntimeException("Do not know what to do.");
      }
    }
  });
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
  assertNotEquals("Hello cglib!", proxy.toString());
}

Этот обратный вызов позволяет нам ответить относительно вызванного метода. Однако вы должны быть осторожны при вызове метода для прокси-объекта, который поставляется с методом InvocationHandler#invoke . Все вызовы этого метода будут отправляться с одним и тем же InvocationHandler и, следовательно, могут привести к бесконечному циклу. Чтобы избежать этого, мы можем использовать еще один диспетчер обратного вызова:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testMethodInterceptor() throws Exception {
  Enhancer enhancer = new Enhancer();
  enhancer.setSuperclass(SampleClass.class);
  enhancer.setCallback(new MethodInterceptor() {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
        throws Throwable {
      if(method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) {
        return "Hello cglib!";
      } else {
        proxy.invokeSuper(obj, args);
      }
    }
  });
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
  assertNotEquals("Hello cglib!", proxy.toString());
  proxy.hashCode(); // Does not throw an exception or result in an endless loop.
}

MethodInterceptor обеспечивает полный контроль над перехваченным методом и предлагает некоторые утилиты для вызова метода расширенного класса в исходном состоянии. Но зачем все-таки использовать другие методы? Поскольку другие методы более эффективны, и cglib часто используется в базовых инфраструктурах, где эффективность играет важную роль. Создание и связывание MethodInterceptor требует, например, генерации байтового кода другого типа и создания некоторых объектов времени выполнения, которые не требуются с InvocationHandler. Из-за этого есть другие классы, которые могут использоваться с Enhancer:

  • LazyLoader : Даже если единственный метод LazyLoader имеет ту же сигнатуру метода, что и FixedValue , LazyLoader принципиально отличается от перехватчика FixedValue . Фактически предполагается, что LazyLoader должен возвращать экземпляр подкласса расширенного класса. Этот экземпляр запрашивается только тогда, когда метод вызывается для расширенного объекта и затем сохраняется для будущих вызовов сгенерированного прокси. Это имеет смысл, если ваш объект дорог в своем создании, не зная, будет ли этот объект когда-либо использоваться. Помните, что некоторый конструктор расширенного класса должен вызываться как для прокси-объекта, так и для лениво загруженного объекта. Таким образом, убедитесь, что есть другой дешевый (возможно, protected ) конструктор, или используйте тип интерфейса для прокси. Вы можете выбрать вызванную конструкцию, передав аргументы в Enhancer#create(Object...) .
  • Dispatcher : Dispatcher похож на LazyLoader но будет вызываться при каждом вызове метода без сохранения загруженного объекта. Это позволяет изменить реализацию класса без изменения ссылки на него. Опять же, имейте в виду, что некоторый конструктор должен вызываться как для прокси, так и для сгенерированных объектов.
  • ProxyRefDispatcher : этот класс содержит ссылку на прокси-объект, из которого он вызывается в своей подписи. Это позволяет, например, делегировать вызовы методов другому методу этого прокси. Имейте в виду, что это может легко вызвать бесконечный цикл и всегда вызовет бесконечный цикл, если тот же метод вызывается из ProxyRefDispatcher#loadObject(Object) .
  • NoOp : класс NoOp делает не то, что предлагает его имя. Вместо этого он делегирует каждый вызов метода реализации метода расширенного класса.

На этом этапе последние два перехватчика могут не иметь смысла для вас. Зачем вам даже хотеть улучшить класс, если вы всегда будете делегировать вызовы методов расширенному классу? И ты прав. Эти перехватчики должны использоваться только вместе с CallbackFilter как показано в следующем фрагменте кода:

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
@Test
public void testCallbackFilter() throws Exception {
  Enhancer enhancer = new Enhancer();
  CallbackHelper callbackHelper = new CallbackHelper(SampleClass.class, new Class[0]) {
    @Override
    protected Object getCallback(Method method) {
      if(method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) {
        return new FixedValue() {
          @Override
          public Object loadObject() throws Exception {
            return "Hello cglib!";
          };
        }
      } else {
        return NoOp.INSTANCE; // A singleton provided by NoOp.
      }
    }
  };
  enhancer.setSuperclass(MyClass.class);
  enhancer.setCallbackFilter(callbackHelper);
  enhancer.setCallbacks(callbackHelper.getCallbacks());
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
  assertNotEquals("Hello cglib!", proxy.toString());
  proxy.hashCode(); // Does not throw an exception or result in an endless loop.
}

Экземпляр Enhancer принимает CallbackFilter в своем Enhancer#setCallbackFilter(CallbackFilter) где он ожидает, что методы расширенного класса будут сопоставлены с индексами массива массива экземпляров Callback . Когда метод вызывается на созданном прокси, Enhancer затем выбирает соответствующий перехватчик и отправляет вызванный метод на соответствующий Callback (который является интерфейсом маркера для всех перехватчиков, которые были представлены до сих пор). Чтобы сделать этот API менее неловким, cglib предлагает CallbackHelper который будет представлять CallbackFilter и который может создать для вас массив Callback s. Вышеупомянутый расширенный объект будет функционально эквивалентен объекту в примере для MethodInterceptor но он позволяет вам писать специализированные перехватчики, сохраняя при этом логику диспетчеризации для этих перехватчиков раздельно.

Как это работает?

Когда Enhancer создает класс, он устанавливает private static поле для каждого перехватчика, который был зарегистрирован как Callback для расширенного класса после его создания. Это также означает, что определения классов, созданные с помощью cglib, не могут быть повторно использованы после их создания, поскольку регистрация обратных вызовов не становится частью фазы инициализации сгенерированного класса, а подготавливается вручную cglib после того, как класс уже был инициализирован JVM. Это также означает, что классы, созданные с помощью cglib, технически не готовы после их инициализации и, например, не могут быть отправлены по проводам, так как обратные вызовы не будут существовать для класса, загруженного на целевой машине.

В зависимости от зарегистрированных перехватчиков, cglib может зарегистрировать дополнительные поля, такие как, например, для MethodInterceptor где два private static поля (одно содержит отражающий Method а другое содержит MethodProxy ), зарегистрированы для метода, который перехватывается в расширенном классе или любом из его подклассы. Имейте в MethodProxy что MethodProxy чрезмерно использует FastClass который запускает создание дополнительных классов и более подробно описан ниже.

По всем этим причинам будьте осторожны при использовании Enhancer . И всегда регистрируйте типы обратного вызова с защитой, поскольку MethodInterceptor , например, инициирует создание дополнительных классов и регистрирует дополнительные static поля в расширенном классе. Это особенно опасно, поскольку переменные обратного вызова также хранятся в виде static переменных в расширенном классе: это означает, что экземпляры обратного вызова никогда не собираются сборщиком мусора (если их ClassLoader является чем-то необычным). Это особенно опасно при использовании анонимных классов, которые молча переносят ссылку на свой внешний класс. Вспомните пример выше:

01
02
03
04
05
06
07
08
09
10
11
12
13
@Test
public void testFixedValue() throws Exception {
  Enhancer enhancer = new Enhancer();
  enhancer.setSuperclass(SampleClass.class);
  enhancer.setCallback(new FixedValue() {
    @Override
    public Object loadObject() throws Exception {
      return "Hello cglib!";
    }
  });
  SampleClass proxy = (SampleClass) enhancer.create();
  assertEquals("Hello cglib!", proxy.test(null));
}

Анонимный подкласс FixedValue вряд ли будет ссылаться из расширенного SampleClass , так что ни анонимный экземпляр FixedValue ни класс, содержащий метод @Test , никогда не будут собираться мусором. Это может привести к неприятным утечкам памяти в ваших приложениях. Поэтому не используйте static внутренние классы с cglib. (Я использую их только в этой записи блога для краткости примеров.)

Наконец, вы никогда не должны перехватывать Object#finalize() . Благодаря подходу cglib к созданию подклассов, перехват finalize осуществляется путем переопределения его, что в целом является плохой идеей . Расширенные экземпляры, которые перехватывают финализацию, будут обрабатываться сборщиком мусора по-разному, а также приводить к тому, что эти объекты помещаются в очередь в очереди финализации JVM. Кроме того, если вы (случайно) создали жесткую ссылку на расширенный класс в своем перехваченном вызове для finalize , вы фактически создали несобираемый экземпляр. Это вообще ничего, что вы хотите. Обратите внимание, что final методы никогда не перехватываются cglib. Таким образом, Object#wait , Object#notify Object#notifyAll и Object#notifyAll не Object#notifyAll одинаковых проблем. Однако помните, что Object#clone может быть перехвачен, что вы, возможно, не хотите делать.

Неизменный боб

ImmutableBean cglib позволяет вам создавать обертку неизменяемости, подобную, например, Collections#immutableSet . Все изменения базового компонента будут предотвращены IllegalStateException (однако, не IllegalStateException UnsupportedOperationException как рекомендуется Java API). Глядя на боб

1
2
3
4
5
6
7
8
9
public class SampleBean {
  private String value;
  public String getValue() {
    return value;
  }
  public void setValue(String value) {
    this.value = value;
  }
}

мы можем сделать этот боб неизменным:

01
02
03
04
05
06
07
08
09
10
@Test(expected = IllegalStateException.class)
public void testImmutableBean() throws Exception {
  SampleBean bean = new SampleBean();
  bean.setValue("Hello world!");
  SampleBean immutableBean = (SampleBean) ImmutableBean.create(bean);
  assertEquals("Hello world!", immutableBean.getValue());
  bean.setValue("Hello world, again!");
  assertEquals("Hello world, again!", immutableBean.getValue());
  immutableBean.setValue("Hello cglib!"); // Causes exception.
}

Как видно из примера, неизменный компонент предотвращает все изменения состояния, генерируя IllegalStateException . Тем не менее, состояние компонента может быть изменено путем изменения исходного объекта. Все такие изменения будут отражены ImmutableBean .

Бобовый генератор

BeanGenerator — это еще одна служебная программа bean-компонента cglib. Он создаст компонент для вас во время выполнения:

01
02
03
04
05
06
07
08
09
10
11
@Test
public void testBeanGenerator() throws Exception {
  BeanGenerator beanGenerator = new BeanGenerator();
  beanGenerator.addProperty("value", String.class);
  Object myBean = beanGenerator.create();
    
  Method setter = myBean.getClass().getMethod("setValue", String.class);
  setter.invoke(myBean, "Hello cglib!");
  Method getter = myBean.getClass().getMethod("getValue");
  assertEquals("Hello cglib!", getter.invoke(myBean));
}

Как видно из примера, BeanGenerator сначала принимает некоторые свойства в качестве пар имя-значение. При создании BeanGenerator создает BeanGenerator доступа

  • <type> get<name>()
  • void set<name>(<type>)

для вас. Это может быть полезно, когда другая библиотека ожидает bean-компоненты, которые были разрешены отражением, но вы не знаете эти bean-компоненты во время выполнения. (Примером может служить Apache Wicket, который много работает с бобами.)

Бобовый копир

BeanCopier — это еще одна утилита bean, которая копирует компоненты по значениям их свойств. Рассмотрим еще один компонент с такими же свойствами, как у SampleBean :

1
2
3
4
5
6
7
8
9
public class OtherSampleBean {
  private String value;
  public String getValue() {
    return value;
  }
  public void setValue(String value) {
    this.value = value;
  }
}

Теперь вы можете копировать свойства из одного компонента в другой:

1
2
3
4
5
6
7
8
9
@Test
public void testBeanCopier() throws Exception {
  BeanCopier copier = BeanCopier.create(SampleBean.class, OtherSampleBean.class, false);
  SampleBean bean = new SampleBean();
  myBean.setValue("Hello cglib!");
  OtherSampleBean otherBean = new OtherSampleBean();
  copier.copy(bean, otherBean, null);
  assertEquals("Hello cglib!", otherBean.getValue());
}

без ограничения определенного типа. BeanCopier#copy использует (в конце концов) дополнительный Converter который позволяет выполнять некоторые дополнительные манипуляции с каждым свойством bean-компонента. Если BeanCopier создается с false в качестве третьего аргумента конструктора, Converter игнорируется и поэтому может иметь значение null .

Массовая фасоль

BulkBean позволяет использовать указанный набор методов доступа к BulkBean по массивам вместо вызовов методов:

01
02
03
04
05
06
07
08
09
10
11
12
13
@Test
public void testBulkBean() throws Exception {
  BulkBean bulkBean = BulkBean.create(SampleBean.class,
      new String[]{"getValue"},
      new String[]{"setValue"},
      new Class[]{String.class});
  SampleBean bean = new SampleBean();
  bean.setValue("Hello world!");
  assertEquals(1, bulkBean.getPropertyValues(bean).length);
  assertEquals("Hello world!", bulkBean.getPropertyValues(bean)[0]);
  bulkBean.setPropertyValues(bean, new Object[] {"Hello cglib!"});
  assertEquals("Hello cglib!", bean.getValue());
}

BulkBean принимает массив имен получателей, массив имен сеттеров и массив типов свойств в качестве аргументов конструктора. Полученный инструментальный класс может быть затем извлечен как массив с помощью BulkBean#getPropertyBalues(Object) . Аналогично, свойства bean-компонента могут быть установлены с помощью BulkBean#setPropertyBalues(Object, Object[]) .

Бобовая карта

Это последняя утилита bean в библиотеке cglib. BeanMap преобразует все свойства компонента в Java- Map типа String to- Object :

1
2
3
4
5
6
7
@Test
public void testBeanGenerator() throws Exception {
  SampleBean bean = new SampleBean();
  BeanMap map = BeanMap.create(bean);
  bean.setValue("Hello cglib!");
  assertEquals("Hello cglib", map.get("value"));
}

Кроме того, метод BeanMap#newInstance(Object) позволяет создавать карты для других компонентов, повторно используя тот же Class .

Ключ фабрика

Фабрика KeyFactory позволяет динамически создавать ключи, состоящие из нескольких значений, которые можно использовать, например, в реализациях Map . Для этого KeyFactory требуется некоторый интерфейс, который определяет значения, которые должны использоваться в таком ключе. Этот интерфейс должен содержать один метод с именем newInstance, который возвращает Object . Например:

1
2
3
public interface SampleKeyFactory {
  Object newInstance(String first, int second);
}

Теперь экземпляр ключа может быть создан:

1
2
3
4
5
6
7
8
@Test
public void testKeyFactory() throws Exception {
  SampleKeyFactory keyFactory = (SampleKeyFactory) KeyFactory.create(Key.class);
  Object key = keyFactory.newInstance("foo", 42);
  Map<Object, String> map = new HashMap<Object, String>();
  map.put(key, "Hello cglib!");
  assertEquals("Hello cglib!", map.get(keyFactory.newInstance("foo", 42)));
}

KeyFactory обеспечит правильную реализацию методов Object#equals(Object) и Object#hashCode , так что результирующие ключевые объекты могут быть использованы в Map или Set . KeyFactory также довольно часто используется внутри библиотеки cglib.

Mixin

Некоторые могут уже знать концепцию класса Mixin из других языков программирования, таких как Ruby или Scala (где миксины называются чертами). cglib Mixin s позволяет объединять несколько объектов в один объект. Однако, чтобы сделать это, эти объекты должны быть поддержаны интерфейсами:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public interface Interface1 {
  String first();
}
  
public interface Interface2 {
  String second();
}
  
public class Class1 implements Interface1 {
  @Override
  public String first() {
    return "first";
  }
}
  
public class Class2 implements Interface2 {
  @Override
  public String second() {
    return "second";
  }
}

Теперь классы Class1 и Class2 могут быть объединены в один класс с помощью дополнительного интерфейса:

01
02
03
04
05
06
07
08
09
10
public interface MixinInterface extends Interface1, Interface2 { /* empty */ }
  
@Test
public void testMixin() throws Exception {
  Mixin mixin = Mixin.create(new Class[]{Interface1.class, Interface2.class,
      MixinInterface.class}, new Object[]{new Class1(), new Class2()});
  MixinInterface mixinDelegate = (MixinInterface) mixin;
  assertEquals("first", mixinDelegate.first());
  assertEquals("second", mixinDelegate.second());
}

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

Струнный переключатель

StringSwitcher эмулирует Java- Map String to int:

1
2
3
4
5
6
7
8
9
@Test
public void testStringSwitcher() throws Exception {
  String[] strings = new String[]{"one", "two"};
  int[] values = new int[]{10, 20};
  StringSwitcher stringSwitcher = StringSwitcher.create(strings, values, true);
  assertEquals(10, stringSwitcher.intValue("one"));
  assertEquals(20, stringSwitcher.intValue("two"));
  assertEquals(-1, stringSwitcher.intValue("three"));
}

StringSwitcher позволяет эмулировать команду switch на String s, как это возможно с помощью встроенного оператора switch Java начиная с Java 7. Если использование StringSwitcher в Java 6 или менее действительно добавляет преимущество к вашему коду, остается сомнительным, и я бы StringSwitcher , лично не рекомендую его использовать.

Производитель интерфейса

InterfaceMaker делает то, что предлагает его название: он динамически создает новый интерфейс.

01
02
03
04
05
06
07
08
09
10
@Test
public void testInterfaceMaker() throws Exception {
  Signature signature = new Signature("foo", Type.DOUBLE_TYPE, new Type[]{Type.INT_TYPE});
  InterfaceMaker interfaceMaker = new InterfaceMaker();
  interfaceMaker.add(signature, new Type[0]);
  Class iface = interfaceMaker.create();
  assertEquals(1, iface.getMethods().length);
  assertEquals("foo", iface.getMethods()[0].getName());
  assertEquals(double.class, iface.getMethods()[0].getReturnType());
}

В отличие от любого другого класса открытого API cglib, создатель интерфейса полагается на типы ASM. Создание интерфейса в работающем приложении вряд ли будет иметь смысл, поскольку интерфейс представляет только тип, который может использоваться компилятором для проверки типов. Однако это может иметь смысл, когда вы генерируете код, который будет использоваться в дальнейшей разработке.

Метод делегат

MethodDelegate позволяет эмулировать C# -подобный делегат конкретному методу путем привязки вызова метода к некоторому интерфейсу. Например, следующий код привязывает метод SampleBean#getValue к делегату:

01
02
03
04
05
06
07
08
09
10
11
12
public interface BeanDelegate {
  String getValueFromDelegate();
}
  
@Test
public void testMethodDelegate() throws Exception {
  SampleBean bean = new SampleBean();
  bean.setValue("Hello cglib!");
  BeanDelegate delegate = (BeanDelegate) MethodDelegate.create(
      bean, "getValue", BeanDelegate.class);
  assertEquals("Hello world!", delegate.getValueFromDelegate());
}

Однако есть некоторые вещи, на которые стоит обратить внимание:

  • Фабричный метод MethodDelegate#create принимает в качестве второго аргумента ровно одно имя метода. Это метод, который MethodDelegate будет прокси для вас.
  • Для объекта должен быть определен метод без аргументов, который передается фабричному методу в качестве первого аргумента. Таким образом, MethodDelegate не так силен, как мог бы быть.
  • Третий аргумент должен быть интерфейсом с ровно одним аргументом. MethodDelegate реализует этот интерфейс и может быть приведен к нему. Когда метод вызывается, он вызывает метод прокси для объекта, который является первым аргументом.

Кроме того, рассмотрим эти недостатки:

  • cglib создает новый класс для каждого прокси. В конце концов, это засорит ваше пространство кучи постоянного поколения
  • Вы не можете использовать прокси-методы, которые принимают аргументы.
  • Если ваш интерфейс принимает аргументы, делегирование метода просто не будет работать без генерируемого исключения (возвращаемое значение всегда будет null ). Если ваш интерфейс требует другого возвращаемого типа (даже если он более общий), вы получите IllegalArgumentException .

Многоадресный делегат

MulticastDelegate работает немного по-другому, чем MethodDelegate хотя и нацелен на аналогичную функциональность. Для использования MulticastDelegate нам требуется объект, который реализует интерфейс:

01
02
03
04
05
06
07
08
09
10
11
12
13
public interface DelegatationProvider {
  void setValue(String value);
}
  
public class SimpleMulticastBean implements DelegatationProvider {
  private String value;
  public String getValue() {
    return value;
  }
  public void setValue(String value) {
    this.value = value;
  }
}

На основе этого поддерживаемого интерфейсом компонента мы можем создать MulticastDelegate который отправляет все вызовы setValue(String) нескольким классам, которые реализуют интерфейс DelegationProvider :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testMulticastDelegate() throws Exception {
  MulticastDelegate multicastDelegate = MulticastDelegate.create(
      DelegatationProvider.class);
  SimpleMulticastBean first = new SimpleMulticastBean();
  SimpleMulticastBean second = new SimpleMulticastBean();
  multicastDelegate = multicastDelegate.add(first);
  multicastDelegate = multicastDelegate.add(second);
  
  DelegatationProvider provider = (DelegatationProvider)multicastDelegate;
  provider.setValue("Hello world!");
  
  assertEquals("Hello world!", first.getValue());
  assertEquals("Hello world!", second.getValue());
}

Опять же, есть некоторые недостатки:

  • Объекты должны реализовывать интерфейс с одним методом. Это отстой для сторонних библиотек и неудобно, когда вы используете CGlib, чтобы творить магию, когда эта магия подвергается воздействию обычного кода . Кроме того, вы могли бы легко реализовать свой собственный делегат (но без байт-кода, но я сомневаюсь, что вы намного выиграете над делегированием вручную).
  • Когда ваши делегаты возвращают значение, вы получите только значение последнего добавленного вами делегата. Все остальные возвращаемые значения теряются (но в какой-то момент их получает делегат многоадресной рассылки).

Делегат конструктора

ConstructorDelegate позволяет создать заводской метод с байтовыми инструментами. Для этого нам сначала потребуется интерфейс с единственным методом newInstance который возвращает Object и принимает любое количество параметров, которые будут использоваться для вызова конструктора указанного класса. Например, чтобы создать ConstructorDelegate для SampleBean , нам требуется следующее для вызова конструктора SampleBean умолчанию (без аргументов):

01
02
03
04
05
06
07
08
09
10
11
public interface SampleBeanConstructorDelegate {
  Object newInstance();
}
  
@Test
public void testConstructorDelegate() throws Exception {
  SampleBeanConstructorDelegate constructorDelegate = (SampleBeanConstructorDelegate) ConstructorDelegate.create(
    SampleBean.class, SampleBeanConstructorDelegate.class);
  SampleBean bean = (SampleBean) constructorDelegate.newInstance();
  assertTrue(SampleBean.class.isAssignableFrom(bean.getClass()));
}

Параллельный сортировщик

ParallelSorter утверждает, что является более быстрой альтернативой сортировщикам массивов стандартной библиотеки Java при сортировке массивов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testParallelSorter() throws Exception {
  Integer[][] value = {
    {4, 3, 9, 0},
    {2, 1, 6, 0}
  };
  ParallelSorter.create(value).mergeSort(0);
  for(Integer[] row : value) {
    int former = -1;
    for(int val : row) {
      assertTrue(former < val);
      former = val;
    }
  }
}

ParallelSorter принимает массив массивов и позволяет применять сортировку слиянием или быструю сортировку к каждой строке массива. Однако будьте осторожны при использовании:

  • При использовании массивов примитивов вы должны вызывать сортировку слиянием с явными диапазонами сортировки (например, ParallelSorter.create(value).mergeSort(0, 0, 3) в примере. В противном случае ParallelSorter имеет довольно очевидную ошибку, когда он пытается ClassCastException примитивный массив к массиву Object[] что вызовет ClassCastException .
  • Если строки массива неравномерны, первый аргумент будет определять длину строки, которую следует учитывать. Неровные строки приведут либо к тому, что дополнительные значения не будут рассматриваться для сортировки, либо к ArrayIndexOutOfBoundException .

Лично я сомневаюсь, что ParallelSorter действительно предлагает преимущество во времени. По общему признанию, я все еще не пытался измерить это. Если бы вы попробовали это, я был бы рад услышать об этом в комментариях.

Быстрый класс и быстрые участники

FastClass обещает более быстрый вызов методов, чем API отражения Java , оборачивая класс Java и предлагая методы, аналогичные API отражения:

1
2
3
4
5
6
7
8
@Test
public void testFastClass() throws Exception {
  FastClass fastClass = FastClass.create(SampleBean.class);
  FastMethod fastMethod = fastClass.getMethod(SampleBean.class.getMethod("getValue"));
  MyBean myBean = new MyBean();
  myBean.setValue("Hello cglib!");
  assertTrue("Hello cglib!", fastMethod.invoke(myBean, new Object[0]));
}

Помимо продемонстрированного FastMethod , FastClass может также создавать FastConstructor но не создавать быстрые поля. Но как FastClass может быть быстрее, чем обычное отражение? Отражение Java выполняется JNI, где вызовы методов выполняются некоторым C кодом. FastClass на другой стороне создает некоторый байтовый код, который вызывает метод непосредственно из JVM. Однако более новые версии JVM HotSpot (и, возможно, многие другие современные JVM) знают концепцию, называемую инфляцией, в которой JVM преобразует вызовы рефлексивных методов в собственные версии FastClass когда рефлексивный метод выполняется достаточно часто. Вы даже можете управлять этим поведением (по крайней мере в JVM HotSpot), установив для свойства sun.reflect.inflationThreshold более низкое значение. (По умолчанию установлено значение 15.) Это свойство определяет, через сколько отражательных вызовов вызов JNI должен быть заменен инструментированной версией байтового кода. Поэтому я бы рекомендовал не использовать FastClass на современных JVM, однако он может точно настроить производительность на старых виртуальных машинах Java.

cglib proxy

Proxy cglib — это переопределение класса Java Proxy упомянутого в начале этой статьи. Он предназначен для разрешения использования прокси библиотеки Java в версиях Java до Java 1.3 и отличается лишь незначительными деталями. Однако лучшую документацию по Proxy серверу cglib можно найти в javadoc Proxy стандартной библиотеки Java, где приведен пример его использования. По этой причине я пропущу более подробное обсуждение Proxy сервера cglib в этом месте.

Последнее слово предупреждения

После этого обзора функциональности cglib я хочу сказать последнее слово предупреждения. Все классы cglib генерируют байт-код, в результате чего дополнительные классы хранятся в специальном разделе памяти JVM: так называемом пространстве перми. Это постоянное пространство, как следует из названия, используется для постоянных объектов, которые обычно не собирают мусор. Это, однако, не совсем верно: после загрузки Class его нельзя выгрузить, пока загрузочный ClassLoader станет доступен для сборки мусора. Это только в том случае, если класс был загружен с пользовательским ClassLoader который не является родной системой JVM ClassLoader . Этот ClassLoader можно собирать мусором, если он сам, все Class он когда-либо загружал, и все экземпляры всех Class он когда-либо загружал, становятся доступными для сборки мусора. Это означает: если вы создаете все больше и больше классов в течение жизни Java-приложения и если вы не позаботитесь об удалении этих классов, вы рано или поздно запустите пространство пермиссии, что приведет к смерти вашего приложения руки OutOfMemoryError . Поэтому используйте cglib экономно. Однако, если вы будете использовать cglib с умом и осторожностью, вы действительно сможете делать удивительные вещи, выходящие за рамки того, что вы можете делать с неинструментированными Java-приложениями.

Наконец, при создании проектов, которые зависят от cglib, вы должны учитывать тот факт, что проект cglib не так хорошо поддерживается и активен, как и должно быть, учитывая его популярность. Недостающая документация является первым намеком. Часто грязный публичный API второй. Но тогда есть также сломанные развертывания cglib в Maven Central. Список рассылки читается как архив спам-сообщений. И циклы выпуска довольно нестабильны. Поэтому вы можете захотеть взглянуть на javassist , единственную настоящую низкоуровневую альтернативу cglib. Javassist поставляется в комплекте с псевдо-Java-компилятором, который позволяет создавать довольно удивительные инструментарии байт-кода, даже не понимая байт-код Java. Если вы хотите испачкать руки, вам также может понравиться ASM, поверх которого построен cglib. ASM поставляется с отличной документацией как библиотеки, так и файлов классов Java и их байт-кода.

Ссылка: cglib: недостающее руководство от нашего партнера по JCG Рафаэля Винтерхальтера в блоге My daily Java .