Last week marked the release of the new major version of the Spring Boot.
I normally prefer to move my applications to the new version as soon as it is physically possible but always dread migration. It is never straightforward, especially when Open Source libraries or frameworks are involved.
So, I spent last weekend trying to migrate a relatively simple Spring Boot 2.x web service to the Spring Boot 3.x.
The web service in question is a fairly standard application that uses Spring JPA to communicate with the backend, JPA Security to authorise requests and uses Rest and Websockets to communicate with clients.
What went well
The official migration guide was not available yet over the weekend so I mostly used this blog post for the guidance as well as Spring Framework documentation and google search to try and resolve any migration issues.
The good news is that switching to new version was as easy as changing the version of the org.springframework.boot gradle plugin.
As expected, this lead immediately to lots of compilation errors due to the switch to the Jakarta EE9.
This issue was fixed relatively easily by using global search and replace function in IDE, replacing the following packages:
- javax.servlet -> jakarta.servlet
- javax.persistence -> jakarta.persistence
- javax.annotations -> jakarta.annotations
- javax.transaction -> jakarta.transaction
I did not use legacy properties files, so had no issues in that area.
What did not go very well
The first issue I encountered was related to the removal of WebSecurityConfigurerAdapter. With Spring Security 5 a security configuration class or classes would normally inherit WebSecurityConfigurerAdapter and override the configure method.
In Spring Security 6, it is now a Bean that takes HttpSecurity object as a parameter and returns SecurityFilterChain object.
It was a relatively simple change, moreover, it simplified things a lot: previously if I wanted to use multiple security mechanisms for various parts of the application I had to create a number of static classes for each configuration. Now it is just number of beans in the main Web Security configuration class.
One change requires source modification: with Spring Security 5, we used .antMatchers method of authorizeRequest object. With Spring Security 6, it has been renamed to .requestMatchers. Otherwise, everything is as it was before, except that this method must now have a call to create SecurityFilterChain object from http parameter by calling http.build().
To summarise, we modify the following web security configuration
@Configuration
public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public WebSecurityConfig() {
super(false);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
logger.info("Configure application security******************************************************");
http.cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.antMatcher("/ws/**")
.authorizeRequests(autorizeRequests -> autorizeRequests
.antMatchers(HttpMethod.GET, "/ws/healthz", "/ws/ready", "/ws/version").permitAll()
.antMatchers(HttpMethod.GET,
"/ws/user/*",
"/ws/user/avatar/*",
"/ws/user/search").hasAnyAuthority("SCOPE_tmt:user")
.antMatchers(HttpMethod.POST,
"/ws/friend",
"/ws/user/trip",
"/ws/trip/*").hasAnyAuthority("SCOPE_tmt:user")
).oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
http.csrf().disable();
http.headers().frameOptions().disable();
}
}
into the following one:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
...
@Bean
@Order(1)
public SecurityFilterChain auth0FilterChain(HttpSecurity http) throws Exception {
logger.info("Configure application security******************************************************");
http.cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.securityMatcher("/ws/**")
.authorizeHttpRequests(autorizeRequests -> autorizeRequests
.requestMatchers(HttpMethod.GET, "/ws/healthz", "/ws/ready", "/ws/version").permitAll()
.requestMatchers(HttpMethod.GET,
"/ws/user/**",
"/ws/user/avatar/*",
"/ws/user/search").hasAnyAuthority("SCOPE_tmt:user")
.requestMatchers(HttpMethod.POST,
"/ws/friend",
"/ws/user/trip",
"/ws/trip/*").hasAnyAuthority("SCOPE_tmt:user")
).oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
http.csrf().disable();
http.headers().frameOptions().disable();
return http.build();
}
...
}
The biggest surprise when dealing with Web Security changes that it became stricter. So, in the previous version I could define the following rule:
.requestMatchers(HttpMethod.GET,
"/ws/user/*",
and it would handle all requests under /ws/user, e.g. /ws/user/1 or /ws/user/1/details.
After switching to Spring Security 6, the previous configuration does not work any longer. In order to handle requests properly, it has to be as follows:
.requestMatchers(HttpMethod.GET,
"/ws/user/**",
Conclusion
I personally was pleasantly surprised that it was a fairly easy experience. Obviously, it is a relatively simple project, and migration of the more complex one might be less easy, but generally I like that a major release of the Spring Framework had almost no breaking changes.