Статьи

Spring 4: прокси-классы на основе CGLIB без конструктора по умолчанию

Весной, если класс целевого объекта, который должен быть прокси, не реализует никаких интерфейсов, то будет создан прокси на основе CGLIB. До Spring 4 прокси-классам на основе CGLIB требовался конструктор по умолчанию. И это не ограничение библиотеки CGLIB, а сама Spring. К счастью, с весны 4 это больше не проблема. Прокси-классы на основе CGLIB больше не требуют конструктора по умолчанию. Как это может повлиять на ваш код? Давайте посмотрим.

Одним из идиом внедрения зависимости является внедрение конструктора. Обычно его можно использовать, когда требуется внедрить зависимости, и они не должны изменяться после инициации объекта. В этой статье я не собираюсь обсуждать, почему и когда вы должны использовать внедрение зависимости конструктора. Я предполагаю, что вы используете эту идиому в своем коде или вы планируете использовать ее. Если вы заинтересованы в получении дополнительной информации, см. Раздел ресурсов в нижней части этой статьи.

Контролирующая инъекция с безоболоченными бобами

Наличие следующего сотрудника:

01
02
03
04
05
06
07
08
09
10
package pl.codeleak.services;
 
import org.springframework.stereotype.Service;
 
@Service
public class Collaborator {
    public String collaborate() {
        return "Collaborating";
    }
}

мы можем легко внедрить его через конструктор:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package pl.codeleak.services;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
@Service
public class SomeService {
 
    private final Collaborator collaborator;
 
    @Autowired
    public SomeService(Collaborator collaborator) {
        this.collaborator = collaborator;
    }
 
    public String businessMethod() {
        return collaborator.collaborate();
    }
 
}

Вы можете заметить, что и Collaborator и Service имеют интерфейсов, но они не являются кандидатами в прокси. Так что этот код будет отлично работать с Spring 3 и Spring 4:

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
32
33
34
35
package pl.codeleak.services;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import pl.codeleak.Configuration;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Configuration.class)
public class WiringTests {
 
    @Autowired
    private SomeService someService;
 
    @Autowired
    private Collaborator collaborator;
 
    @Test
    public void hasValidDependencies() {
        assertThat(someService)
                .isNotNull()
                .isExactlyInstanceOf(SomeService.class);
 
        assertThat(collaborator)
                .isNotNull()
                .isExactlyInstanceOf(Collaborator.class);
 
        assertThat(someService.businessMethod())
                .isEqualTo("Collaborating");
    }
}

Contructor инъекция с прокси-бобами

Во многих случаях ваши компоненты должны быть украшены AOP proxy во время выполнения, например, когда вы хотите использовать декларативные транзакции с аннотацией @Transactional . Чтобы визуализировать это, я создал аспект, который будет советовать всем методам в SomeService . С определенным ниже аспектом SomeService становится кандидатом на проксирование:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package pl.codeleak.aspects;
 
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
 
@Aspect
@Component
public class DummyAspect {
 
    @Before("within(pl.codeleak.services.SomeService)")
    public void before() {
        // do nothing
    }
 
}

Когда я повторно запускаю тест с Spring 3.2.9, я получаю следующее исключение:

1
Could not generate CGLIB subclass of class [class pl.codeleak.services.SomeService]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given

Это может быть просто исправлено путем предоставления конструктора по умолчанию, без аргументов, для SomeService , но это не то, что я хочу сделать — поскольку мне также необходимо сделать зависимости не окончательными.

Другое решение — предоставить интерфейс для SomeService . Но опять же, есть много ситуаций, когда вам не нужно создавать интерфейсы.

Обновление до Spring 4 решает проблему немедленно. В документации говорится:

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

Тест, который я создал, потерпит неудачу, но он визуализирует, что CGLIB-прокси был создан для SomeService :

1
2
3
4
5
6
7
java.lang.AssertionError:
Expecting:
 <pl.codeleak.services.SomeService@6a84a97d>
to be exactly an instance of:
 <pl.codeleak.services.SomeService>
but was an instance of:
 <pl.codeleak.services.SomeService$$EnhancerBySpringCGLIB$$55c3343b>

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

Ресурсы