Search code examples
javaspringdependency-injectioninversion-of-controlioc-container

Spring Dependency Injection - Private fields - Anti Pattern? Why does it even work?


I am generally a c# developer but working on Java now and then I see a lot of dependency injection using Spring on private properties, with no public way of setting the value. I was surprised this actually works, but I guess it’s possible via reflection?

Surely this is terrible practice?! I can't see how anyone unit testing or inspecting the class would possibly know that a private member needs to be set from some external framework.

How would you even set the property when you are unit testing? Or just using the class stand alone?

I guess you have to use spring in your unit tests which seems really overkill. Surely you should be able to unit test without your IOC container? The class becomes completely dependent on spring...

Have I missed anything here?

Should dependency injection not always involve a public setter of some kind, and preferably use the constructor if possible? Or is there something about Java I am missing...?

Thanks


Solution

  • You can always mock injected beans even if you have private fields. You should have a look on @MockBean from Spring documentation. Essentially, you could do the following:

    @ExtendWith({SpringExtension.class})
    class MyServiceTest{
    
        @MockBean
        private RepositoryInterface repository;
    
        @Autowired
        private MyService service;
    
    }
    

    Supposing that RepositoryInterface is an interface (and not a concrete class) that is injected in MyService. What happens is that the SpringExtension for JUnit5, which should be already in your dependencies if you created your pom.xml from Spring Initialzr, will build a mock for that interface using another framework that is called Mockito (maybe have a look to it). Then Spring IoC will inject the created mock in the service. This works for field injection:

    @Service
    public class MyService{
    
        @Autowired
        private RepositoryInterface repositoryInterface
    }
    

    setter injection:

    @Service
    public class MyService{
    
        private RepositoryInterface repositoryInterface
    
        @Autowired
        public void setRepository(RepositoryInterface repositoryInterface){
            this.repositoryInterface = repositoryInterface;
        }
    }
    

    or constructor injection:

    @Service
    public class MyService{
    
        private RepositoryInterface repositoryInterface
    
        public MyService(RepositoryInterface repositoryInterface){
            this.repositoryInterface = repositoryInterface;
        }
    }
    

    Essentially, the last one is the recommended one because in this way your service's dependencies will be explicit. It's more on code style. Field injection is not recommended because iy hides your class dependencies. So, the recommended way of building a test using the constructor injection would be the following:

    @ExtendWith({SpringExtension.class})
    class MyServiceTest{
    
        @MockBean
        private RepositoryInterface repository;
    
        private MyService service;
    
        @BeforeEach
        void setup(){
            service = new MyService(repository);
        }
    }
    

    Hope this helps your understanding.