DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Terence Pan
Terence Pan

Posted on

Why use Spring Constructor Dependency Injection instead of @Autowired

Autowired

Most Spring developers I have seen are still using the @Autowired syntax for injecting dependencies into their classes. This is not recommended anymore and Intellij even gives you a warning when you try to use it.
Image description

Today we'll go over one reason why not to use dependency injection in this manner. I have also seen this cause a production incident as well before. Having @Autowired on the field allows the field to be potentially mutable and harder to test. Having the injected field be mutable can interfere with how Spring proxies these injected classes through the use of the CGLIB library.

Set up DTO

In the test project we are setting up a DTO that is supposed to be Session Scoped to the user. So each user should only see the instance of the bean relating their session.

UserDataDto.java class:

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;

@Component
@SessionScope
public class UserDataDto {
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
Enter fullscreen mode Exit fullscreen mode

Set up Spring Security config

We also have some Spring Security set up as well to initialize both user instances we are using for testing here.

SecurityConfig.class:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user1 = User.withUsername("user1")
                .password(passwordEncoder.encode("password"))
                .roles("ADMIN")
                .build();
        UserDetails user2 = User.withUsername("user2")
                .password(passwordEncoder.encode("password"))
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user1, user2);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .httpBasic()
                .and()
                .authorizeRequests()
                .antMatchers("/**")
                .hasRole("ADMIN")
                .anyRequest()
                .authenticated();
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }
}
Enter fullscreen mode Exit fullscreen mode

Set up Controller class

And finally here's the controller class.
UserController.class:

@RestController("/")
public class UserController {
    @Autowired
    UserDataDto userDataDto;

    @PostMapping("/saveUserName")
    public void saveUserName(@RequestBody String userName){
        userDataDto.setUserName(userName);
    }

    @PostMapping("/saveUserNameReplaceInstance")
    public void saveUserNameReplaceInstance(@RequestBody String userName){
        UserDataDto userDataDto = new UserDataDto();
        userDataDto.setUserName(userName);
        this.userDataDto = userDataDto;
    }

    @GetMapping("/getUserName")
    public String getUserName(){
        return userDataDto.getUserName();
    }
}
Enter fullscreen mode Exit fullscreen mode

Test intended effect

Ok now let's run the Spring Boot application and get started with testing!
I will use Postman to POST to the /saveUserName endpoint with the user1

Image description

Image description
And now to use a separate Chrome incognito window to try to GET from the /getUserName endpoint with the user2. Nothing shows, good!

Image description

Image description

Overwrite Session Scope bean into Singleton

Now lets intentionally cause a problem with a new endpoint /saveUserNameReplaceInstance that will replace the Autowired bean.

    @PostMapping("/saveUserNameReplaceInstance")
    public void saveUserNameReplaceInstance(@RequestBody String userName){
        UserDataDto userDataDto = new UserDataDto();
        userDataDto.setUserName(userName);
        this.userDataDto = userDataDto;
    }
Enter fullscreen mode Exit fullscreen mode

This time we will send the same thing through Postman using the new endpoint with user1

Image description
And in Chrome with user2 I now see user1's data!

Image description

Oh NO!! Big problems here, we essentially turned a SessionScope bean into a singleton that is shared between all instances!

Constructor Injection

Now how does constructor injection prevent this? Intellij will refactor Autowired code for you automagically if you click on the @Autowired annotation, hit Alt-Enter on windows and click Create constructor.

Image description

But for convenience I did this in another class.
ConstructorInjectionController.java:

@RestController
public class ConstructorInjectionController {
    final
    UserDataDto userDataDto;

    public ConstructorInjectionController(UserDataDto userDataDto) {
        this.userDataDto = userDataDto;
    }

    @PostMapping("/saveUserNameCi")
    public void saveUserName(@RequestBody String userName){
        userDataDto.setUserName(userName);
    }

    @PostMapping("/saveUserNameReplaceInstanceCi")
    public void saveUserNameReplaceInstance(@RequestBody String userName){
        UserDataDto userDataDto = new UserDataDto();
        userDataDto.setUserName(userName);
        //this.userDataDto = userDataDto;
    }

    @GetMapping("/getUserNameCi")
    public String getUserName(){
        return userDataDto.getUserName();
    }
}
Enter fullscreen mode Exit fullscreen mode

try to uncomment the line with the mapping saveUserNameReplaceInstanceCi and the IDE will prevent you from causing a huge mistake. This allows you to declare the injected field as final and immutable after instantiation of the controller class.

Image description

As always code is on Github

Top comments (0)

50 CLI Tools You Can't Live Without

>> Check out this classic DEV post <<