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.
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;
}
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();
}
}
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();
}
}
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
And now to use a separate Chrome incognito window to try to GET from the /getUserName endpoint with the user2. Nothing shows, good!
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;
}
This time we will send the same thing through Postman using the new endpoint with user1
And in Chrome with user2 I now see user1's data!
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.
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();
}
}
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.
As always code is on Github
Top comments (0)