Spring?Boot?2.x升3.x的那些事
序言
手頭上有個(gè)項(xiàng)目,準(zhǔn)備從Spring Boot 2.x升級(jí)到3.x,升級(jí)后發(fā)現(xiàn)編譯器報(bào)了一堆錯(cuò)誤。一般來(lái)說(shuō)大版本升級(jí),肯定會(huì)有諸多問(wèn)題,對(duì)于程序開(kāi)發(fā)來(lái)說(shuō)能不升就不升。但是對(duì)于系統(tǒng)架構(gòu)來(lái)說(shuō),能用最新的肯定是用最新的,實(shí)在不行再降回去嘛??墒悄?,不知道是發(fā)布沒(méi)多久,還是我搜索技巧的問(wèn)題,很多問(wèn)題在網(wǎng)上找不到答案。沒(méi)辦法,還是得自己研究,所以呢這次我們就一起來(lái)研究一下Spring Boot 3.x究竟有什么改變。
一、關(guān)于Spring Session
一般來(lái)說(shuō),如果一個(gè)Spring Boot 2.x項(xiàng)目一開(kāi)始只需要單實(shí)例部署,用不上redis共享會(huì)話(huà)的話(huà),會(huì)在application.properties里加上這個(gè)參數(shù)。
spring.session.store-type=none
當(dāng)需要改為多實(shí)例部署,需要redis共享會(huì)話(huà)的時(shí)候,只需要改為這樣就行了。
spring.session.store-type=redis
但是在Spring Boot 3.x項(xiàng)目里,這個(gè)參數(shù)就不復(fù)存在了。查了Spring Session的官方文檔也沒(méi)有收獲。于是去翻Spring Boot的官方文檔,在2.x的參考文檔中有這么一條提示“You can disable Spring Session by setting the store-type to none.”。而在3.x的文檔中,這個(gè)提示被刪掉了。好家伙,原來(lái)store-type=none是直接禁用整個(gè)Spring Session,而不是Api文檔中所說(shuō)的"No session data-store."

那么解決辦法就很簡(jiǎn)單了,單實(shí)例部署,不需要用redis的時(shí)候,刪掉pom.xml里org.springframework.session的依賴(lài)就好。需要redis共享會(huì)話(huà)的時(shí)候就要把依賴(lài)加回去了,就是沒(méi)有原來(lái)修改配置文件來(lái)得方便而已。
二、關(guān)于redis
在application.properties里關(guān)于redis的配置也有所變化。如果你是這么配置redis的:
spring.redis.host=127.0.0.1 spring.redis.port=6379
這時(shí)編譯器就會(huì)警告你:“Property ‘spring.redis.host’ is Deprecated: Use ‘spring.data.redis.host’ instead.”、“Property ‘spring.redis.password’ is Deprecated: Use ‘spring.data.redis.password’ instead.”按照警告所說(shuō)的,把“spring.redis”替換成“spring.data.redis”即可。
spring.data.redis.host=127.0.0.1 spring.data.redis.port=6379
三、關(guān)于servlet
由于tomcat 10包名的更換,如果你的程序是這么寫(xiě)的:
import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; ...
那么編譯器就會(huì)報(bào)"The import javax.servlet cannot be resolved"錯(cuò)誤。原因是包名從javax.servlet 調(diào)整為了jakarta.servlet 。解決辦法很簡(jiǎn)單,把javax.servlet 替換為 jakarta.servlet 即可。
import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; ...
四、關(guān)于thymeleaf模板
當(dāng)你使用片段表達(dá)式(fragment expression)而沒(méi)有使用“~{}”時(shí),會(huì)獲得運(yùn)行警告。例如你模板里這么寫(xiě):
<footer th:replace="footer::copy"></footer>
則會(huì)得到這樣的運(yùn)行警告:“Deprecated unwrapped fragment expression “footer::copy” found in template index, line 7, col 9. Please use the complete syntax of fragment expressions instead (“~{footer::copy}”). The old, unwrapped syntax for fragment expressions will be removed in future versions of Thymeleaf.”
原因是在thymeleaf 3.1中,未封裝的片段表達(dá)式不再被推薦。解決方法也很簡(jiǎn)單,按照警告所說(shuō)的改為完整版的片段表達(dá)式,即加上“~{}”即可。
<footer th:replace="~{footer::copy}"></footer>
五、關(guān)于Spring Security
重點(diǎn)來(lái)了,隨著Spring Boot升級(jí)到3.x,Spring Security也升級(jí)到了6.x。話(huà)不多說(shuō),先來(lái)看看代碼,在6.x之前,如果你想要實(shí)現(xiàn)動(dòng)態(tài)權(quán)限,你的代碼可能會(huì)是這樣的:
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserService myUserService;
@Autowired
MyUrlFilter myUrlFilter;
@Autowired
MyDecisionManager myDecisionManager;
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
protected SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**");
}
@Override
public void configure(HttpSecurity http) throws Exception{
http.apply(new UrlAuthorizationConfigurer<>(http.getSharedObject(ApplicationContext.class)))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(myDecisionManager);
o.setSecurityMetadataSource(myUrlFilter);
return o;
}
})
.and().formLogin().loginProcessingUrl("/login/process").loginPage("/login/page")
.and().logout().logoutUrl("/logout/page")
.and().sessionManagement().maximumSessions(-1).expiredUrl("/login/page").sessionRegistry(sessionRegistry())
.and().and().csrf().disable();
}
}
如果要把上面的代碼改成可以在Spring Security 6.x里運(yùn)行,那么你需要這么寫(xiě):
@Configuration
public class MySecurityConfig {
@Autowired
MyAuthorizationManager myAuthorizationManager;
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
protected SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/static/**");
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(authz -> authz.anyRequest().access(myAuthorizationManager))
.formLogin(login -> login.loginProcessingUrl("/login/process").loginPage("/login/page").permitAll())
.logout(logout -> logout.logoutUrl("/logout/page").permitAll())
.sessionManagement(session -> session.maximumSessions(-1).expiredUrl("/login/page").sessionRegistry(sessionRegistry()))
.csrf(csrf -> csrf.disable());
return http.build();
}
}
我們來(lái)逐個(gè)講解一下。
1.關(guān)于WebSecurityConfigurerAdapter
在Spring Security 6.x之前,我們通常是寫(xiě)一個(gè)配置類(lèi),繼承WebSecurityConfigurerAdapter 然后重寫(xiě)(@Override)對(duì)應(yīng)的方法來(lái)完成Security的配置的。而在Spring Security 6.x里WebSecurityConfigurerAdapter 已經(jīng)被棄用了,現(xiàn)在推薦使用的是基于組件的編碼方式,只要在配置類(lèi)里注冊(cè)對(duì)應(yīng)的組件(@Bean)即可。另外,使用組件配置時(shí)and()方法已經(jīng)不再推薦使用,官方建議使用lambda DSL。
2.關(guān)于UserDetailsService
按上面所說(shuō)的,下面這段代碼。
@Autowired
MyUserService myUserService;
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserService);
}
理論上是要改成這樣的。
@Autowired
MyUserService myUserService;
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(myUserService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
但實(shí)際上只要你的用戶(hù)服務(wù)(MyUserService)實(shí)現(xiàn)了UserDetailsService接口,并且注冊(cè)到了Spring容器中(加了@Service或者@Component注解),Spring Security 6.x就會(huì)自動(dòng)綁定用戶(hù)服務(wù),只需注冊(cè)密碼加密組件即可。所以上面的代碼直接改成下面的就可以了。
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
(由于篇幅關(guān)系,這里就不貼MyUserService的代碼了,自己按實(shí)際情況實(shí)現(xiàn)對(duì)應(yīng)接口功能就好)
3.關(guān)于WebSecurity
WebSecurity可以控制哪些地址不進(jìn)入Security過(guò)濾器鏈。原來(lái)的代碼是這么寫(xiě)的。
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**");
}
現(xiàn)在,除了需要改為基于組件的寫(xiě)法外,antMatchers()方法也改成了requestMatchers()方法。
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/static/**");
}
4.關(guān)于HttpSecurity
原來(lái)在HttpSecurity中實(shí)現(xiàn)動(dòng)態(tài)權(quán)限,是先要寫(xiě)一個(gè)訪(fǎng)問(wèn)地址過(guò)濾器(MyUrlFilter),來(lái)判斷當(dāng)前訪(fǎng)問(wèn)地址需要什么權(quán)限,然后將所需權(quán)限送給決策管理器(MyDecisionManager)進(jìn)行判斷是否有權(quán)限。
@Autowired
MyUrlFilter myUrlFilter;
@Autowired
MyDecisionManager myDecisionManager;
@Bean
protected SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Override
public void configure(HttpSecurity http) throws Exception{
http.apply(new UrlAuthorizationConfigurer<>(http.getSharedObject(ApplicationContext.class)))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(myDecisionManager);
o.setSecurityMetadataSource(myUrlFilter);
return o;
}
})
.and().formLogin().loginProcessingUrl("/login/process").loginPage("/login/page")
.and().logout().logoutUrl("/logout/page")
.and().sessionManagement().maximumSessions(-1).expiredUrl("/login/page").sessionRegistry(sessionRegistry())
.and().and().csrf().disable();
}
MyUrlFilter.java
@Component
public class MyUrlFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
AccessPermitService accessPermitService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//若當(dāng)前頁(yè)面是登錄頁(yè)面,則直接放行,否則會(huì)進(jìn)入死循環(huán),最終報(bào)重定向次數(shù)過(guò)多的錯(cuò)誤
if(antPathMatcher.match("/login/**", requestUrl)) {
//權(quán)限數(shù)量為0時(shí),不會(huì)調(diào)用AccessDecisionManager.decide()方法,無(wú)需登錄,直接放行
return SecurityConfig.createList(new String[0]);
}
//基于數(shù)據(jù)庫(kù)的動(dòng)態(tài)權(quán)限,獲取整個(gè)系統(tǒng)的訪(fǎng)問(wèn)路徑權(quán)限配置(建議緩存起來(lái))
List<AccessPermit> accessPermits = accessPermitService.list();
//遍歷訪(fǎng)問(wèn)路徑權(quán)限配置列表,判斷當(dāng)前請(qǐng)求url和哪個(gè)訪(fǎng)問(wèn)路徑配置匹配
for (AccessPermit accessPermit : accessPermits) {
//如果匹配上了,獲取這個(gè)訪(fǎng)問(wèn)路徑的角色
if(antPathMatcher.match(accessPermit.getPattern(), requestUrl)){
String roles = accessPermit.getRoles();
//如果沒(méi)有設(shè)置角色,則視為需要登錄但不需要對(duì)應(yīng)權(quán)限,設(shè)置一個(gè)默認(rèn)權(quán)限給該訪(fǎng)問(wèn)地址;否則根據(jù)逗號(hào)切分,返回對(duì)應(yīng)的權(quán)限
if(roles.equals("")) {
return SecurityConfig.createList("login_required");
}
else{
return SecurityConfig.createList(roles.split(","));
}
}
}
//沒(méi)有匹配上,則視為需要登錄但不需要對(duì)應(yīng)權(quán)限,設(shè)置一個(gè)默認(rèn)權(quán)限給該訪(fǎng)問(wèn)地址
return SecurityConfig.createList("login_required");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
MyDecisionManager.java
@Component
public class MyDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute configAttribute : configAttributes) {
//需要登錄但不需要權(quán)限時(shí),myUrlFilter過(guò)濾器默認(rèn)返回一個(gè)默認(rèn)權(quán)限,需要進(jìn)行特殊處理
if(configAttribute.getAttribute().equals("login_required")) {
//如果沒(méi)有登錄,則返回登錄頁(yè)面;否則用戶(hù)已登錄,直接放行
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("沒(méi)有登錄,請(qǐng)登錄!");
}
else {
return;
}
}
//需要權(quán)限的情況
for (GrantedAuthority authority : authorities) {
//判斷當(dāng)前用戶(hù)是否有對(duì)應(yīng)權(quán)限,有則放行
if(configAttribute.getAttribute().equals(authority.getAuthority())){
return;
}
}
}
//沒(méi)有權(quán)限則不放行
throw new AccessDeniedException("權(quán)限不足,無(wú)法訪(fǎng)問(wèn)!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
LogoutController.java
@Controller
@RequestMapping("/logout")
public class LogoutController {
@Autowired
SessionRegistry sessionRegistry;
@RequestMapping("/page")
public String page(HttpSession session) {
SessionInformation sessionInformation = sessionRegistry.getSessionInformation(session.getId());
sessionInformation.expireNow();
return "redirect:login?logout";
}
}
現(xiàn)在改成這樣:
@Autowired
MyAuthorizationManager myAuthorizationManager;
@Bean
protected SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(authz -> authz.anyRequest().access(myAuthorizationManager))
.formLogin(login -> login.loginProcessingUrl("/login/process").loginPage("/login/page").permitAll())
.logout(logout -> logout.logoutUrl("/logout/page").permitAll())
.sessionManagement(session -> session.maximumSessions(-1).expiredUrl("/login/page").sessionRegistry(sessionRegistry()))
.csrf(csrf -> csrf.disable());
return http.build();
}
MyAuthorizationManager.java
@Component
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Autowired
AccessPermitService accessPermitService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
String requestUrl = context.getRequest().getServletPath();
Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
//基于數(shù)據(jù)庫(kù)的動(dòng)態(tài)權(quán)限,獲取整個(gè)系統(tǒng)的訪(fǎng)問(wèn)路徑權(quán)限配置(建議緩存起來(lái))
List<AccessPermit> accessPermits = accessPermitService.list();
//遍歷訪(fǎng)問(wèn)路徑權(quán)限配置列表,判斷當(dāng)前請(qǐng)求url和哪個(gè)訪(fǎng)問(wèn)路徑配置匹配
for (AccessPermit accessPermit : accessPermits) {
//如果匹配上了,獲取這個(gè)訪(fǎng)問(wèn)路徑的角色
if(antPathMatcher.match(accessPermit.getPattern(), requestUrl)){
String roles = accessPermit.getRoles();
//如果沒(méi)有設(shè)置角色,則視為需要登錄但不需要對(duì)應(yīng)權(quán)限;否則根據(jù)逗號(hào)切分,返回對(duì)應(yīng)的權(quán)限
if(roles.equals("")) {
break;
}
else{
for(String role : roles.split(",")) {
for (GrantedAuthority authority : authorities) {
//判斷當(dāng)前用戶(hù)是否有對(duì)應(yīng)權(quán)限,有則放行
if(role.equals(authority.getAuthority())){
return new AuthorizationDecision(true);
}
}
}
return new AuthorizationDecision(false);
}
}
}
if (authentication.get() instanceof AnonymousAuthenticationToken) {
return new AuthorizationDecision(false);
}
else return new AuthorizationDecision(true);
}
}
這里改動(dòng)挺多的,一是使用lambda DSL的格式去寫(xiě)相關(guān)代碼。二是現(xiàn)在只需要使用authorizeHttpRequests()方法配置一個(gè)自定義的授權(quán)管理器(MyAuthorizationManager)就可以了??梢岳斫鉃?strong>這個(gè)授權(quán)管理器(MyAuthorizationManager)取代了原來(lái)的訪(fǎng)問(wèn)地址過(guò)濾器(MyUrlFilter)和決策管理器(MyDecisionManager)。三是現(xiàn)在的過(guò)濾器鏈?zhǔn)窍冉?jīng)過(guò)HttpSecurity 的過(guò)濾器再到授權(quán)管理器(MyAuthorizationManager)的,之前給登錄頁(yè)面放行的相關(guān)邏輯也不用自己實(shí)現(xiàn)了,但是formLogin和logout都要設(shè)置.permitAll()。四是logoutUrl(“/logout/page”)無(wú)需自行實(shí)現(xiàn)了,這個(gè)頁(yè)面與loginProcessingUrl(“/login/process”)一樣,已經(jīng)交由Security 托管了,自行實(shí)現(xiàn)也不會(huì)執(zhí)行。五是現(xiàn)在sessionRegistry會(huì)自動(dòng)銷(xiāo)毀登出的會(huì)話(huà)了,也無(wú)需自行實(shí)現(xiàn)了。
(由于篇幅關(guān)系,這里就不貼AccessPermitService 的相關(guān)代碼了,大家按自己實(shí)際情況去實(shí)現(xiàn)即可)
總結(jié)
從Spring Boot 2.x升級(jí)到3.x肯定還有很多改動(dòng),是我這里沒(méi)列舉的,雖然改動(dòng)挺多的,但還是建議能用最新的版本就用最新的版本。特別是Spring Security 升級(jí)到了6.x之后,代碼邏輯清晰了許多,不會(huì)像之前那樣繞到云里霧里,僅這點(diǎn)就值得了。
參考資料
Spring Session #2.7.15-SNAPSHOT
Spring Session #3.0.10-SNAPSHOT
Spring Security without the WebSecurityConfigurerAdapter
到此這篇關(guān)于Spring Boot 2.x升3.x的那些事的文章就介紹到這了,更多相關(guān)Spring Boot 2.x升3.x的那些事內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在Windows上為Java配置多個(gè)版本的環(huán)境變量的操作指南
本文介紹了在Windows系統(tǒng)上配置多個(gè)Java版本環(huán)境變量的方法,包括配置CLASSPATH和PATH,設(shè)置多個(gè)JAVA_HOME變量,以及如何切換和驗(yàn)證Java版本,通過(guò)合理配置,實(shí)現(xiàn)不同Java版本的高效切換,需要的朋友可以參考下2026-04-04
idea創(chuàng)建Spring項(xiàng)目的方法步驟(圖文)
這篇文章主要介紹了idea創(chuàng)建Spring項(xiàng)目的方法步驟(圖文),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
Java 刪除與重置 Excel 文件密碼的完整示例(文件級(jí)與工作表級(jí)示例)
本文詳細(xì)介紹了如何使用Java實(shí)現(xiàn)Excel文件密碼的刪除與重置,涵蓋了文件和工作表級(jí)別的保護(hù)機(jī)制,通過(guò)使用Spire.XLSforJava組件,可以高效地完成這些操作,確保數(shù)據(jù)的安全性和系統(tǒng)的穩(wěn)定性,感興趣的朋友跟隨小編一起看看吧2026-02-02
maven導(dǎo)入本地倉(cāng)庫(kù)jar包,報(bào):Could?not?find?artifact的解決
這篇文章主要介紹了maven導(dǎo)入本地倉(cāng)庫(kù)jar包,報(bào):Could?not?find?artifact的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
MyBatis實(shí)現(xiàn)if-else的示例代碼
MyBatis實(shí)現(xiàn)if-else功能可以通過(guò)和標(biāo)簽來(lái)完成,這些標(biāo)簽提供了一種類(lèi)似于Java中switch語(yǔ)句的方式來(lái)處理多分支條件邏輯,下面就來(lái)具體了解一下2025-11-11
詳解Spring?中?Bean?對(duì)象的存儲(chǔ)和取出
由于?Spring?擁有對(duì)象的管理權(quán),所以我們也需要擁有較為高效的對(duì)象存儲(chǔ)和取出的手段,下面我們來(lái)分別總結(jié)一下,對(duì)Spring?中?Bean?對(duì)象的存儲(chǔ)和取出知識(shí)感興趣的朋友跟隨小編一起看看吧2022-11-11
Java數(shù)據(jù)庫(kù)連接池之proxool_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Proxool是一種Java數(shù)據(jù)庫(kù)連接池技術(shù)。方便易用,便于發(fā)現(xiàn)連接泄漏的情況2017-08-08

