去年,我撰寫了名為 “Spring Boot 3.0 升級指南:針對 Spring Data JPA 與 Querydsl” 的文章,專為 Spring Boot 3.0.x 升級而寫。如今,Spring Boot 3.2 已經問世。讓我們來看看在升級至 Spring Boot 3.2.2 時可能遇到的兩個問題。
SAT 專案所採用的技術包括:
- Spring Boot 3.2.2 及 Spring Framework 6.1.3
- Hibernate + JPA 模型生成器 6.4.1 最終版
- Spring Data JPA 3.2.2
- Querydsl 5.0.0.
變更內容
Spring Boot 3.2 的所有變更均記載於 Spring Boot 3.2 版本發布說明 以及 Spring Framework 6.1 版本新特性。
Spring Boot 3.2.2 的最新變更可於 GitHub 上查閱。
發現的問題
- A different treatment of Hibernate dependencies due to the changed
hibernate-jpamodelgen
behavior for annotation processors Unpaged
類別經過重新設計。
首先從Hibernate的相依性開始談起。
整合靜態元模型生成
最大的變化來自於hibernate-jpamodelgen
相依性,它正在生成一個靜態元模型。在Hibernate 6.3中,為了減輕傳遞性相依性的影響,對相依性的處理方式進行了更改。Spring Boot 3.2.0將hibernate-jpamodelgen
相依性提升到了6.3版本(參見相依性升級)。不幸的是,新版本導致了編譯錯誤(如下所示)。
注意:這裡使用的Spring Boot 3.2.2已經採用了Hibernate 6.4,並且表現出相同的行為。
編譯錯誤
隨著這一變化,使用Spring Boot 3.2.2進行我們的專案(Maven構建)編譯時失敗,出現了這樣的錯誤:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.049 s
[INFO] Finished at: 2024-01-05T08:43:10+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure: Compilation failure:
[ERROR] on the class path. A future release of javac may disable annotation processing
[ERROR] unless at least one processor is specified by name (-processor), or a search
[ERROR] path is specified (--processor-path, --processor-module-path), or annotation
[ERROR] processing is enabled explicitly (-proc:only, -proc:full).
[ERROR] Use -Xlint:-options to suppress this message.
[ERROR] Use -proc:none to disable annotation processing.
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:[3,41] error: cannot find symbol
[ERROR] symbol: class City_
[ERROR] location: package com.github.aha.sat.jpa.city
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:[3] error: static import only from classes and interfaces
...
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4] error: static import only from classes and interfaces
[ERROR] java.lang.NoClassDefFoundError: net/bytebuddy/matcher/ElementMatcher
[ERROR] at org.hibernate.jpamodelgen.validation.ProcessorSessionFactory.<clinit>(ProcessorSessionFactory.java:69)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.handleNamedQuery(AnnotationMeta.java:104)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.handleNamedQueryRepeatableAnnotation(AnnotationMeta.java:78)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.checkNamedQueries(AnnotationMeta.java:57)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMetaEntity.init(AnnotationMetaEntity.java:297)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMetaEntity.create(AnnotationMetaEntity.java:135)
[ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.handleRootElementAnnotationMirrors(JPAMetaModelEntityProcessor.java:360)
[ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.processClasses(JPAMetaModelEntityProcessor.java:203)
[ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.process(JPAMetaModelEntityProcessor.java:174)
[ERROR] at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:1021)
[ER...
[ERROR] at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:348)
[ERROR] Caused by: java.lang.ClassNotFoundException: net.bytebuddy.matcher.ElementMatcher
[ERROR] at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:445)
[ERROR] at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:593)
[ERROR] at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
[ERROR] ... 51 more
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
這是由於Hibernate遷移指南中宣布的靜態元模型生成方法的改變所導致的(參見整合靜態元模型生成和原始問題HHH-17362)。他們對這種改變的解釋如下:
“… 在之前的 Hibernate ORM 版本中,您可能不知不覺地在編譯路徑中洩漏了對
hibernate-jpamodelgen
的依賴。使用 Hibernate ORM 6.3 時,您現在可能會在註解處理過程中遇到與缺少 Antlr 類相關的編譯錯誤。”
依賴變更
如以下截圖所示,Hibernate 的依賴確實發生了變化。
- Spring Boot 3.1.6:
- Spring Boot 3.2.2:
解釋
如遷移指南所述,我們需要將 pom.xml
從簡單的 Maven 依賴更改為 Maven 編譯器插件的註解處理器路徑(參見文檔)。
解決方案
我們可以按照上一篇文章的建議,移除 Maven 依賴 hibernate-jpamodelgen
和 querydsl-apt
(在我們的情況下)。取而代之的是,pom.xml
需要通過 maven-compiler-plugin
定義靜態元模型生成器,如下所示:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</path>
<path>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jakarta</classifier>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
由於hibernate-jpamodelgen
的緣故,我們被迫採用此方法,因此必須將其應用於所有與註解處理緊密相關的依賴(如querydsl-apt
或lombok
)。例如,當lombok
未以此方式使用時,我們會遇到這樣的編譯錯誤:
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityService.java:[15,30] error: variable repository not initialized in the default constructor
[INFO] 1 error
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.535 s
[INFO] Finished at: 2024-01-08T08:40:29+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityService.java:[15,30] error: variable repository not initialized in the default constructor
同樣情況也適用於querydsl-apt
。在此情況下,我們會看到類似的編譯錯誤:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.211 s
[INFO] Finished at: 2024-01-11T08:39:18+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure: Compilation failure:
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryRepository.java:[3,44] error: cannot find symbol
[ERROR] symbol: class QCountry
[ERROR] location: package com.github.aha.sat.jpa.country
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryRepository.java:[3] error: static import only from classes and interfaces
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[3,41] error: cannot find symbol
[ERROR] symbol: class QCity
[ERROR] location: package com.github.aha.sat.jpa.city
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[3] error: static import only from classes and interfaces
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4,44] error: cannot find symbol
[ERROR] symbol: class QCountry
[ERROR] location: package com.github.aha.sat.jpa.country
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4] error: static import only from classes and interfaces
[ERROR] -> [Help 1]
原因很明顯,我們需要同時應用所有的註解處理器。否則,某些代碼可能會缺失,導致編譯錯誤。
重新設計的Unpaged
第二個小問題與Unpaged
類的變更有關。將Unpaged
從enum
更改為class
影響了通過Jackson庫對PageImpl
的序列化(參見spring-projects/spring-data-commons#2987)。
- Spring Boot 3.1.6:
public interface Pageable {
static Pageable unpaged() {
return Unpaged.INSTANCE;
}
...
}
enum Unpaged implements Pageable {
INSTANCE;
...
}
- Spring Boot 3.2.2:
public interface Pageable {
static Pageable unpaged() {
return unpaged(Sort.unsorted());
}
static Pageable unpaged(Sort sort) {
return Unpaged.sorted(sort);
}
...
}
final class Unpaged implements Pageable {
private static final Pageable UNSORTED = new Unpaged(Sort.unsorted());
...
}
當使用new PageImpl<City>(cities)
(我們習慣的方式)時,會拋出此錯誤:
2024-01-11T08:47:56.446+01:00 WARN 5168 --- [sat-elk] [ main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: (was java.lang.UnsupportedOperationException)]
MockHttpServletRequest:
HTTP Method = GET
Request URI = /api/cities/country/Spain
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = com.github.aha.sat.elk.city.CityController
Method = com.github.aha.sat.elk.city.CityController#searchByCountry(String, Pageable)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = org.springframework.http.converter.HttpMessageNotWritableException
解決方法是使用具有所有屬性的構造器,如:
new PageImpl<City>(cities, ofSize(PAGE_SIZE), cities.size())
而不是:
new PageImpl<City>(cities)
注意:此問題預計在Spring Boot 3.3中得到修復(參見此問題評論)。
結論
本文涵蓋了升級至撰寫本文時最新的Spring Boot 3.2.2版本所遇到的問題。文章首先探討了由於Hibernate依賴管理變更導致的註解處理器處理方式的改變。接著,解釋了Unpaged
類別的變動以及使用PageImpl
的解決方案。
Source:
https://dzone.com/articles/upgrade-guide-to-spring-boot-32-for-spring-data-jp