You are currently viewing Java/Spring Boot – Jak wstrzyknąć beana do statycznego pola klasy

Java/Spring Boot – Jak wstrzyknąć beana do statycznego pola klasy

Zdarzają się sytuacje, w których z metody statycznej musisz uzyskać dostęp do ApplicationContext lub dowolnego innego beana Spring. W tym poradniku pokażę Ci jak można to zrobić.

Na wstępie zaznaczę, że zdecydowanie odradzam wstrzykiwanie beana w statyczne pole klasy. Ten artykuł jest przeznaczony dla tych, którzy są świadomi tego, co robią i nie mają innego wyjścia.

Zasoby

Problem

Załóżmy, że mamy klasę pomocniczą o nazwie StaticUtils, podobną do poniższej:

import com.bettercoding.spring.config.MyConfig;
import org.springframework.beans.factory.annotation.Autowired;

public final class StaticUtils {

    @Autowired
    private static MyConfig myConfig;

    private StaticUtils() {
    }

    public static void printFoo() {
        System.out.println("Foo:" + myConfig.getFoo());
    }
}

Jak widać, w szóstej linii znajduje się adnotacja @Autowired i spodziewamy się, że Spring wstrzyknie tutaj odpowiedniego beana. Ale zwróć uwagę, że owe pole jest statyczne.

Poniżej znajduje się przykładowa klasa MyConfig używana w naszej klasie pomocniczej. Wymaga przekazania własności foo.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {
    @Value("${foo}")
    private String foo;

    public String getFoo() {
        return foo;
    }
}

Napiszmy klasę aplikacji i przetestujmy ją. Pamiętaj, aby przekazać propertisa -Dfoo=bar.

import com.bettercoding.spring.utils.StaticUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return new CommandLineRunner() {
            @Override
            public void run(String... args) throws Exception {
                System.out.println("Hello World");
                StaticUtils.printFoo();
            }
        };
    }
}

Wynik to NullPointerException.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.2.RELEASE)

2018-06-02 13:45:47.435  INFO 8077 --- [           main] com.bettercoding.spring.MyApp            : Starting MyApp on bc-vbox with PID 8077 (/home/better-coding/Workspace/static-initializer/out/production/classes started by better-coding in /home/better-coding/Workspace/static-initializer)
2018-06-02 13:45:47.442  INFO 8077 --- [           main] com.bettercoding.spring.MyApp            : No active profile set, falling back to default profiles: default
2018-06-02 13:45:47.539  INFO 8077 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@1b68b9a4: startup date [Sat Jun 02 13:45:47 CEST 2018]; root of context hierarchy
2018-06-02 13:45:48.495  INFO 8077 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-06-02 13:45:48.516  INFO 8077 --- [           main] com.bettercoding.spring.MyApp            : Started MyApp in 1.689 seconds (JVM running for 2.339)
Hello World
2018-06-02 13:45:48.519  INFO 8077 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2018-06-02 13:45:48.524 ERROR 8077 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute CommandLineRunner
  at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:800) [spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
  at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:781) [spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) [spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:1255) [spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:1243) [spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
  at com.bettercoding.spring.MyApp.main(MyApp.java:13) [classes/:na]
Caused by: java.lang.NullPointerException: null
  at com.bettercoding.spring.utils.StaticUtils.printFoo(StaticUtils.java:19) ~[classes/:na]
  at com.bettercoding.spring.MyApp$1.run(MyApp.java:22) ~[classes/:na]
  at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:797) [spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
  ... 5 common frames omitted

2018-06-02 13:45:48.525  INFO 8077 --- [           main] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@1b68b9a4: startup date [Sat Jun 02 13:45:47 CEST 2018]; root of context hierarchy
2018-06-02 13:45:48.527  INFO 8077 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown

Process finished with exit code 1

Jak widać Spring nie jest w stanie wstrzykiwać beanów bezpośrednio do pól statycznych i wystąpił wyjątek NullPointerException.

Rozwiązanie: Wykorzystaj klasę StaticContextInitializer

Najbardziej elegancką i całkiem bezpieczną metodą jaką znam, jest ta, którą nazwałem StaticContextInitializer. Pomysł polega na stworzeniu komponentu o nazwie StaticContextInitializer. Jest on odpowiedzialny za inicjowanie klas statycznych poprzez metodę z adnotacją @PostConstruct. Zobaczmy to w praktyce.

Krok 1 – Zmiany w klasie pomocniczej

Edytuj klasę StaticUtils – usuń adnotację @Autowired i dodaj metodę ustawiającą dla pola myConfig. Klasa po zmianach powinna wyglądać jak poniżej.

import com.bettercoding.spring.config.MyConfig;

public final class StaticUtils {

    private static MyConfig myConfig;

    public static void setMyConfig(MyConfig myConfig) {
        StaticUtils.myConfig = myConfig;
    }

    private StaticUtils() {
    }

    public static void printFoo() {
        System.out.println("Foo:" + myConfig.getFoo());
    }
}

Krok 2  – Tworzenie komponentu StaticContextInitializer

Utwórz klasę o nazwie StaticContextInitializer z metodą init z adnotacją @PostConstruct. Metoda init zostanie wykonana zaraz po utworzeniu instancji komponentu więc możemy ją wykorzystać aby ustawić statyczne pole myConfig w klasie StaticUtils.

import com.bettercoding.spring.config.MyConfig;
import com.bettercoding.spring.utils.StaticUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class StaticContextInitializer {

    @Autowired
    private MyConfig myConfig;

    @Autowired
    private ApplicationContext context;

    @PostConstruct
    public void init() {
        StaticUtils.setMyConfig(myConfig);
    }
}

Krok 3 – Test

Uruchom aplikację jeszcze raz, aby sprawdzić czy zachowuje się ona zgodnie z oczekiwaniami.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.2.RELEASE)

2018-06-02 14:04:21.993  INFO 8556 --- [           main] com.bettercoding.spring.MyApp            : Starting MyApp on bc-vbox with PID 8556 (/home/better-coding/Workspace/static-initializer/out/production/classes started by better-coding in /home/better-coding/Workspace/static-initializer)
2018-06-02 14:04:21.998  INFO 8556 --- [           main] com.bettercoding.spring.MyApp            : No active profile set, falling back to default profiles: default
2018-06-02 14:04:22.141  INFO 8556 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@3d36e4cd: startup date [Sat Jun 02 14:04:22 CEST 2018]; root of context hierarchy
2018-06-02 14:04:23.078  INFO 8556 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-06-02 14:04:23.093  INFO 8556 --- [           main] com.bettercoding.spring.MyApp            : Started MyApp in 1.679 seconds (JVM running for 2.253)
Hello World
Foo:bar
2018-06-02 14:04:23.101  INFO 8556 --- [       Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@3d36e4cd: startup date [Sat Jun 02 14:04:22 CEST 2018]; root of context hierarchy
2018-06-02 14:04:23.106  INFO 8556 --- [       Thread-2] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown

Process finished with exit code 0

To już wszystko co dla Ciebie przygotowałem w tym poradniku. Udostępnij proszę ten post aby pomóc mi dotrzeć do większego grona odbiorców. Dzięki i do zobaczenia w kolejnym poradniku.