Tworzenie beanów to odwieczna część pracy programisty, który “babra się” w ekosystemie Springa. Jednak nawet w tak rutynowej czynności można znaleźć coś ciekawego, czego niekoniecznie jest się świadomym. Przejdźmy zatem do sedna i spradźmy czym @Configuration może nas zaskoczyć. Jednak najpierw zarejestrujmy kilka beanów wykorzystując adnotację @Bean.

1
2
3
4
5
6
7
8
9
10
11
public class BaseClassForComponent {
}

public class AggregationClassForComponent {

    public final BaseClassForComponent baseClassForComponent;

    public AggregationClassForComponent(BaseClassForComponent baseClassForComponent) {
        this.baseClassForComponent = baseClassForComponent;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class ComponentClass {

    @Bean
    public AggregationClassForComponent aggregationClassForComponent() {
        return new AggregationClassForComponent(baseClassForComponent());
    }

    @Bean
    public BaseClassForComponent baseClassForComponent() {
        return new BaseClassForComponent();
    }

}

Czy my właśnie zarejestrowaliśmy beany w klasie oznaczonej adnotacją @Component? Zgadza się. Jest to jak najbardziej możliwe, ponieważ jeśli ktoś zajrzał do dokumetacji to na pewno dopatrzył się, że w adnotacji @Configuration znajduje się meta-adnotacja @Component. Czyli klasy konfiguracyjne to nic innego jak po prostu zwykłe beany. A to w jakiś sposób sprawia, że nawet klasa oznaczona przez @Component może służyć jako agregacja beanów niezbędnych do zarejestowania w kontenerze Springa.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";

    boolean proxyBeanMethods() default true;

    boolean enforceUniqueMethods() default true;
}

Żeby przekonać się jak przygotowane przez nas wyżej rozwiązanie działa napiszmy sobie test sprawdzający czy kontekst Springa w ogóle wstał, a jeśli tak to co się stało z zarejestowanymi beanami.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootTest
class ComponentClassTest {

    @Autowired
    BaseClassForComponent baseClassForComponent;

    @Autowired
    AggregationClassForComponent aggregationClassForComponent;

    @Test
    void checkCreationOfBeans() {
        System.out.println("just bean - " + baseClassForComponent);
        System.out.println("aggregated bean - " + aggregationClassForComponent.baseClassForComponent);
    }

}

Kontekst wstał jak najbardziej i ma się dobrze. Jednak patrząć w output coś powinno przykuć naszą uwagę.

1
2
just bean - pl.cezarysanecki.springdemotests.injectingdependencies.BaseClassForComponent@5f0bab7e
aggregated bean - pl.cezarysanecki.springdemotests.injectingdependencies.BaseClassForComponent@6b4125ed

Bean BaseClassForComponent oraz ten przypisany do pola w instancji klasy AggregationClassForComponent to dwie różne instancje! Takie zachowanie nie jest oczekiwane w kontekście aplikacji opartych o Springa. Beany domyślnie powinny być przecież singletonami! Zgadza się, ale to mamy zapewnione, gdy rejestrujemy beany poprzez wykorzystanie adnotacji @Component.

W powyższym przypadku, jeśli ktoś korzysta z IntelliJ to dla tej sytuacji dostanie ostrzeżenie znajdujące się przy wywołaniu metody baseClassForComponent - “Method annotated with @Bean is called directly. Use dependency injection instead.”. W ten sposób zostaliśmy pośrednio poinforowani o braku unikalności beana przed uruchomieniem aplikacji. Jak temu zaradzić? Oczywiście skorzystać z przeznaczonej do takiej funkcjonalności adnotacji @Configuration.

1
2
3
4
5
6
7
8
9
10
11
public class BaseClassForConfiguration {
}

public class AggregationClassForConfiguration {

    public final BaseClassForConfiguration baseClassForConfiguration;

    public AggregationClassForConfiguration(BaseClassForConfiguration baseClassForConfiguration) {
        this.baseClassForConfiguration = baseClassForConfiguration;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class ConfigurationClass {

    @Bean
    public AggregationClassForConfiguration aggregationClassForConfiguration() {
        return new AggregationClassForConfiguration(baseClassForConfiguration());
    }

    @Bean
    public BaseClassForConfiguration baseClassForConfiguration() {
        return new BaseClassForConfiguration();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootTest
class ConfigurationClassTest {

    @Autowired
    BaseClassForConfiguration baseClassForConfiguration;

    @Autowired
    AggregationClassForConfiguration aggregationClassForConfiguration;

    @Test
    void checkCreationOfBeans() {
        System.out.println("just bean - " + baseClassForConfiguration);
        System.out.println("aggregated bean - " + aggregationClassForConfiguration.baseClassForConfiguration);
    }

}
1
2
just bean - pl.cezarysanecki.springdemotests.injectingdependencies.BaseClassForConfiguration@337a6d30
aggregated bean - pl.cezarysanecki.springdemotests.injectingdependencies.BaseClassForConfiguration@337a6d30

Problem rozwiązany! Jednak wnikliwe osoby, które zaglądają do dokumentacji (albo dużo debugują…) dowiedzą się o czymś naprawdę ciekawym. No, bo w końcu jakim cudem wywołanie metody baseClassForConfiguration nie stworzyło nowej instancji tylko posłużyło się już wcześniej utworzoną instancją? Normalne zachowanie Javy powinno utworzyć nam dwa obiekty jak widzieliśmy to na przykładzie wyżej.

Spring domyślnie tworzy proxy dla klas konfiguracyjnych

Wyjaśnienie znajdziemy oczywiście w dokumentacji.

In common scenarios, @Bean methods are to be declared within @Configuration classes, ensuring that full configuration class processing applies and that cross-method references therefore get redirected to the container’s lifecycle management. This prevents the same @Bean method from accidentally being invoked through a regular Java method call, which helps to reduce subtle bugs that can be hard to track down.

Co oczywiście jest możliwe dzięki proxy tworzonemu w oparciu o CGLIB. Jeśli ktoś nie chce korzystać z takiego “dobrodziejstwa”, które dodaje narzut wydajnościowy na naszą aplikację podczas jej wstawania, to może skorzystać z atrybutu proxyBeanMethods znajdującego się w adnotacji @Configuration. Wystarczy ustawić tą flagę na false i problem z głowy!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration(proxyBeanMethods = false)
public class ConfigurationClass {

    @Bean
    public AggregationClassForConfiguration aggregationClassForConfiguration() {
        return new AggregationClassForConfiguration(baseClassForConfiguration());
    }

    @Bean
    public BaseClassForConfiguration baseClassForConfiguration() {
        return new BaseClassForConfiguration();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootTest
class ConfigurationClassTest {

    @Autowired
    BaseClassForConfiguration baseClassForConfiguration;

    @Autowired
    AggregationClassForConfiguration aggregationClassForConfiguration;

    @Test
    void checkCreationOfBeansWithoutProxy() {
        System.out.println("just bean - " + baseClassForConfiguration);
        System.out.println("aggregated bean - " + aggregationClassForConfiguration.baseClassForConfiguration);
    }

}
1
2
just bean - pl.cezarysanecki.springdemotests.injectingdependencies.BaseClassForConfiguration@6579cdbb
aggregated bean - pl.cezarysanecki.springdemotests.injectingdependencies.BaseClassForConfiguration@469a7575

No dobra, nie do końca… Wróciliśmy do pierwotnego podejścia z wykorzystaniem @Component jako klasy konfiguracyjnej. W tym podejściu cześć pracy została przerzucona na nas, deweloperów. Musimy zmienić swoje dotychczasowe przyzwyczajenie i nie korzystać z wywoływania metod podczas tworzenia beanów. Alternatywą do tego sposobu jest przekazywanie niezbędnych zależności przez parametr metody.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration(proxyBeanMethods = false)
public class ConfigurationClass {

    @Bean
    public AggregationClassForConfiguration aggregationClassForConfiguration(BaseClassForConfiguration baseClassForConfiguration) {
        return new AggregationClassForConfiguration(baseClassForConfiguration);
    }

    @Bean
    public BaseClassForConfiguration baseClassForConfiguration() {
        return new BaseClassForConfiguration();
    }

}

Teraz uruchamiając test dowiadujemy się, że aplikacja działa, a my mamy tylko jedną instancję klasy BaseClassForConfiguration. Dodatkowo żadne proxy dla klasy ConfigurationClass się nie utworzyło. Także pełen sukces!