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

Niektóre osoby mogą powiedzieć, że to wiedza niepotrzebna. I fakt – spotkałem kilku programistów, którzy mimo wieloletniego doświadczenia, nie do końca ogarniali ten temat, jakoś z tym żyją.

Ale jednak wydaje mi się, że 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

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 jeżeli wszystko działa. W sumie zazwyczaj 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 – jeżeli 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 nadal istnieje. Czemu ten cholerny obiekt nie ma tych zależności?!!

Napiszmy własne wstrzykiwanie poprzez @Inject

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

Zacznijmy od utworzenia własnej adnotacji @Inject

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

Zasady gry są takie: jeżeli 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 - jeżeli 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) {
         // jeżeli 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ż, że 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ż, że 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, że jeżeli 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.