Библиотека инструментария байт-кода 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 и их байт-кода.