Co się dzieje gdy używasz adnotacji @Inject/@Autowired?

pater.dev 5 lat temu

Niektóre osoby mogą powiedzieć, iż to wiedza niepotrzebna. Spotkałem kilku programistów, którzy mimo wieloletniego doświadczenia nie do końca wiedzieli jak to działa i jakoś z tym żyli. Ale co to za życie.

Wydaje mi się, iż znajomość tego jak tak na prawdę działa wstrzykiwanie zależności, chociażby w takim podstawowym stopniu, to wiedza obowiązkowa – otwiera oczy i często oszczędzi Ci wielogodzinnego debugowania.

Pokażę może na starcie prosty przykład – co tutaj jest nie tak?

W standardowym springu (bez użycia innych zależności) w tym przypadku @Transactional w ogóle nie zadziała – czemu?

Method visibility and @Transactional
When you use proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. If you need to annotate non-public methods, consider using AspectJ (described later).

https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction

Ten przypadek opiszemy trochę później. Zacznijmy pierw od czegoś prostszego.

Spis treści

  1. Do czego mi to w ogóle się przyda? Przecież mogę oznaczyć klasę jako @Component i dzieje się magia.
  2. Napiszmy własne wstrzykiwanie poprzez @Inject
  3. ApplicationContext, AppConfiguration i definiowanie beanów – po co tyle zachodu?

Do czego mi to w ogóle się przyda?

… przecież mogę oznaczyć klasę jako @Component i dzieje się magia.

No i jest super o ile wszystko działa. W sumie zwykle dorzucimy ten kolejny serwis, oznaczymy go jako @Component/@Service, ewentualnie dodamy go do konfiguracji. I rzeczywiście to w większości przypadków wystarczy.

Gorzej jak dzieje się coś złego. I tutaj zaczynają się schody – o ile nie znasz tych fundamentów to nie ma opcji, abyś zrozumiał problem. Kojarzysz to uczucie – gdy coś nie działa i jeszcze do końca nie wiesz co, ale masz kilka pomysłów od czego zacząć analizę problemu. No i schody się robią wtedy, gdy już wszystkie pomysły Ci się skończyły, a problem przez cały czas istnieje. Czemu ten cholerny obiekt nie ma tych zależności?!!

Napiszmy własne wstrzykiwanie poprzez @Inject

Tutaj wystarczy na prawdę niewiele. Zobaczymy, iż nie jest to takie trudne.

Zacznijmy od utworzenia własnej adnotacji @Inject

@Retention(RetentionPolicy.RUNTIME) public @interface Inject {}

Zasady gry są takie: o ile oznaczymy jakieś pole w klasie adnotacją @Inject to dane pole powinno zostać wypełnione implementacją. Stwórzmy zatem przykładowe DAO:

public interface UserDAO { void save(User user); } public class UserDAOImpl implements UserDAO { @Inject private EntityManager em; public void save(User user) { em.persist(user); } }

Jak widzimy – EntityManager powinien zostać w jakiś sposób wstrzyknięty do naszej klasy.

Jak otrzymać teraz nasz wypełniony obiekt?

Podejście pierwsze – użyjmy po prostu new

UserDAO userDAO = new UserDAOImpl();

Wywołanie metody save na obiekcie userDAO rzuci (tak jak się z resztą powinniśmy spodziewać) NullPointerException – bo przecież zależność EntityManager jest pusta. Znikąd jej nie wyczarujemy.

Podejście drugie – zawołajmy, aby ktoś nam ten obiekt zwrócił, najlepiej już wypełniony zależnościami. Gotowy do użycia!

UserDAO userDAO = blackBox.getImplementation();

Tylko czegoś tutaj brakuje – chcemy, aby ta magiczna metoda działała dla każdej klasy (zwracała implementacje dowolnego interfejsu). W takim razie musimy jej przekazać w parametrze której dokładnie klasy chcemy implementację:

UserDAO userDAO = blackBox.getImplementation(UserDAO.class);

Ekstra! Magiczna skrzynka zwróciła nam gotowy obiekt z gotowym już EntityManagerem. To w takim razie co się dzieje pod getImplementation?

ApplicationContext, AppConfiguration i definiowanie beanów – po co tyle zachodu?

O to implementacja naszej prostej funkcji getImplementation

public <T> T getImplementation(Class<T> clazz) { try { // getBeanFromConfiguration zwraca zainicjalizowany obiekt) T obj = getBeanFromConfiguration(clazz); // przejdź po polach - o ile są oznaczone @Inject to rekurencyjnie wywołaj na nich getImplementation Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(Inject.class)) { field.setAccessible(true); field.set(obj, getImplementation(field.getType())); } } return obj; } catch (IllegalAccessException | InstantiationException e) { e.printStackTrace(); } return null; } private <T> T getBeanFromConfiguration(Class<T> clazz) throws InstantiationException, IllegalAccessException { // pobierz wszystkie metody klasy AppConfiguration (patrz kod wyżej) Method[] methods = appConfiguration.getClass().getMethods(); for (Method method : methods) { // o ile typ zwracany przez metodę == typ clazz to dokładnie o tę metodę chodzi if (clazz.equals(method.getReturnType())) { try { // wywołaj tę metodę (najzwyczajniej zwróci obiekt zwracany przez tę metodę) return (T) method.invoke(appConfiguration); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } } } return (T) clazz.newInstance(); }
Czemu Class.newInstance() zamiast new?
Zauważ, iż działamy na obiekcie typu Class – metoda nie wie (w momencie kompilowania) jaka to jest dokładnie klasa. I właśnie do tego służy newInstance() – do dynamicznego inicjalizowania obiektu, którego klasy jeszcze nie znamy. Zauważ, iż nasza metoda przyjmować będzie różnego rodzaju typy.

Koncepcje getBeanFromConfiguration pewnie znacie. Mamy sobie plik konfiguracyjny:

public class AppConfiguration { public UserDAO userDAO() { return new UserDAOImpl(); } public EntityManager entityManager() { return new EntityManager(); } public UserService userService() { return new UserServiceImpl(); } }

I tutaj od razu dowiadujemy się – po co tak na prawdę my musimy te metody tworzyć w konfiguracji Springa (zwykłym, nie mówię o Spring Boot, bo tam sytuacja jest troszkę inna). Przeanalizujmy co tutaj się tak na prawdę dzieje krok po kroku:

  1. Wylistuj wszystkie metody pliku konfiguracyjnego (tych konfiguracji może być kilka, ale my podajemy naszą klasę AppConfiguration)
  2. Jeżeli AppConfiguration zawiera metodę, która zwraca typ, który oczekujemy to wywołaj ją.
  3. W przeciwnym wypadku utwórz standardową instancję klasy, którą oczekujemy ( linia: return (T) clazz.newInstance() )

Oznacza to, iż o ile chcemy instancję klasy B i nie znajdziemy w AppConfiguration metody która zwraca typ B to metoda getBeanFromConfiguration zwróci nam po prostu new B();

Czyli teoretycznie nasza klasa AppConfiguration może być pusta i będzie działać. Ale co gdy nie chcemy standardowej inicjalizacji obiektu? Przykładowo zamiast

public EntityManager entityManager() { return new EntityManager(); }

Chcemy, aby instancja EntityManagera była pobierana z ThreadLocal:

public EntityManager entityManager() { return EMThreadLocalStorage.getEntityManager(); }

Wtedy już musimy to wpisać do naszej konfiguracji. Jak inaczej framework ma się domyślić skąd pobrać nasz obiekt? Utworzyć/ pobrać z pamięci/ zaciągnąć z pliku… Możliwości jest mnóstwo!

I to tyle. Widzisz – nie ma tutaj żadnej czarnej magii. Możemy zatem zmienić trochę nazwy naszych metod getImplementation => getBean oraz blackBox => ApplicationContext. Znajomo wygląda, co?

I tak o to mamy prostą implementację adnotacji @Inject. Nie było to chyba takie straszne? Teraz już chociaż widzisz sens wypełniania pliku AppConfiguration inny niż a, bo w tutorialu było, że…

W kolejnym wpisie pokażę inną, również bardzo popularną adnotację – @Transactional.

Idź do oryginalnego materiału