Il existe plusieurs méthodes pour injecter une dépendance dans un objet Java :

  • Injection sur un setter
  • Injection sur le constructeur
  • Injection directe sur la déclaration de l’attribut

Cet article a pour objectif de définir ces différentes méthodes, de décrire leurs avantages et inconvénients et indiquer leurs cas d’utilisation.

Dans la plupart des applications Java d’entreprise, il y a des services ayant besoin de DAO pour accéder à la base de données. Le code suivant est une illustration de ce cas :

public class UserServiceImpl implements UserService {

   private UserDao userDao;

   @Override
   public User save(String name) {

      User user = new User(name);

      // NullPointerException car userDao n'est pas injecté.
      return userDao.save(user);
   }
}

Dans le cadre de cet article, l’annotation @Autowired de Spring sera utilisée pour injecter userDao dans le service.

Injection sur un setter

Il est possible de créer un setter et de l’annoter avec @Autowired. Spring va alors utiliser ce setter pour injecter userDao dans le service.

public class UserServiceImpl implements UserService {

   private UserDao userDao;

   @Autowired
   public void setUserDao(UserDao userDao) {
      this.userDao = userDao;
   }

   @Override
   public User save(String name) {

      User user = new User(name);

      // userDao est injecté via le setter annoté.
      return userDao.save(user);
   }
}

Cette méthode a pour avantage de rendre le userDao facilement injectable dans un test unitaire sans avoir à utiliser de framework particulier. Comme montré dans l’exemple ci-après :

public class UserServiceTest {

   @Test
   public void test_save() {

      // given
      String name = "Martin";
      UserService userService = new UserServiceImpl();
      userService.setUserDao(new FakeUserDaoImpl());

      // when
      User user = userService.save(name);

      // then
      assertThat(user.getName()).isEqualTo(name);

   }

   private class FakeUserDaoImpl implements UserDao {

      @Override
      public User save(User user) {
         return user;
      }
   }

}

Cependant, elle a pour inconvénient de rendre l’attribut userDao du service modifiable par tous les objets qui disposent d’une instance du service (ils peuvent donc même le rendre null).

Injection sur le constructeur

Dans cette méthode d’injection, le userDao est injecté dans le service via son constructeur annoté avec @Autowired.

public class UserServiceImpl implements UserService {

   private final UserDao userDao;

   @Autowired
   public UserServiceImpl(UserDao userDao) {
      this.userDao = userDao;
   }

   @Override
   public User save(String name) {

      User user = new User(name);

      // userDao est injecté via le constructeur annoté.
      return userDao.save(user);
   }
}

Comme la méthode d’injection à l’aide du setter, celle-ci permet de rendre le userDao facilement injectable dans un test unitaire.

public class UserServiceTest {

   @Test
   public void test_save() {

      // given
      String name = "Martin";
      UserService userService = new UserServiceImpl(new FakeUserDaoImpl());

      // when
      User user = userService.save(name);

      // then
      assertThat(user.getName()).isEqualTo(name);

   }

   private class FakeUserDaoImpl implements UserDao {

      @Override
      public User save(User user) {
         return user;
      }
   }

}

De plus, elle permet également d’assurer que le userDao ne sera jamais modifié. Il suffirait donc de mettre un contrôle de nullité dans le constructeur pour certifier qu’il ne sera jamais null.

Cependant, elle a pour inconvénient d’imposer la création de la dépendance dès l’instanciation du service même si elle n’est pas nécessaire.

Injection sur la déclaration de l’attribut

Cette méthode consiste à ajouter l’annotation @Autowired directement sur la déclaration de l’attribut.

public class UserServiceImpl implements UserService {

   @Autowired
   private UserDao userDao;

   @Override
   public User save(String name) {

      User user = new User(name);

      // userDao est injecté via l'annotation @Autowired.
      return userDao.save(user);
   }
}

Cette manière d’injecter a l’avantage d’être simple à utiliser.

Néanmoins, elle n’est pas conseillée car elle force à employer la réfléxion, ce qui la rend notamment plus compliquée à tester (utilisation obligatoire d’un framework de test).

public class UserServiceTest {

   @Test
   public void test_save() {

      // given
      String name = "Martin";
      UserService userService = new UserServiceImpl();
      UserDao fakeUserDao = new FakeUserDaoImpl();

      // Utilisation de la classe ReflectionTestUtils de Spring.
      ReflectionTestUtils.setField(userService, "userDao", fakeUserDao);

      // when
      User user = userService.save(name);

      // then
      assertThat(user.getName()).isEqualTo(name);

   }

   private class FakeUserDaoImpl implements UserDao {

      @Override
      public User save(User user) {
         return user;
      }
   }

}

De plus, elle rompt le principe de la programmation orientée objet qui stipule que les objets sont responsables de leurs attributs privés. En effet, l’attribut privé est ici manipulé directement par Spring ou le framework de test choisi.

Conclusion

L’injection d’un attribut d’un objet peut se faire de différentes manières. L’injection par setter a pour avantage de rendre le code facilement testable. L’injection par constructeur a, en plus, l’avantage de pouvoir contrôler la nullité de l’attribut. Elle est donc conseilée pour les attributs obligatoires. Finalement, l’injection sur la déclaration de l’attribut est déconseilée car elle rend le code moins facilement testable et crée des dépendances cachées.

La testabilité du code est importante. En effet, un code non testable est signe d’un code qui sera difficile à comprendre et à maintenir.