Querydsl 與 JPA Criteria 比較第6部分:Spring Data JPA 與 Querydsl 專案升級至 Spring Boot 3.2 指南

去年,我撰寫了名為 “Spring Boot 3.0 升級指南:針對 Spring Data JPA 與 Querydsl” 的文章,專為 Spring Boot 3.0.x 升級而寫。如今,Spring Boot 3.2 已經問世。讓我們來看看在升級至 Spring Boot 3.2.2 時可能遇到的兩個問題。

SAT 專案所採用的技術包括:

  1. Spring Boot 3.2.2 及 Spring Framework 6.1.3
  2. Hibernate + JPA 模型生成器 6.4.1 最終版
  3. Spring Data JPA 3.2.2
  4. 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構建)編譯時失敗,出現了這樣的錯誤:

Plain Text

 

[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-jpamodelgenquerydsl-apt(在我們的情況下)。取而代之的是,pom.xml 需要通過 maven-compiler-plugin 定義靜態元模型生成器,如下所示:

XML

 

<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>

參閱GitHub 上 SAT 項目的相關變更

由於hibernate-jpamodelgen的緣故,我們被迫採用此方法,因此必須將其應用於所有與註解處理緊密相關的依賴(如querydsl-aptlombok)。例如,當lombok未以此方式使用時,我們會遇到這樣的編譯錯誤:

Plain Text

 

[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。在此情況下,我們會看到類似的編譯錯誤:

Plain Text

 

[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類的變更有關。將Unpagedenum更改為class影響了通過Jackson庫對PageImpl的序列化(參見spring-projects/spring-data-commons#2987)。

  • Spring Boot 3.1.6:
Java

 

public interface Pageable {

	static Pageable unpaged() {
		return Unpaged.INSTANCE;
	}

	...
}

enum Unpaged implements Pageable {

	INSTANCE;

	...
}

  • Spring Boot 3.2.2:
Java

 

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)(我們習慣的方式)時,會拋出此錯誤:

Plain Text

 

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

解決方法是使用具有所有屬性的構造器,如:

Java

 

new PageImpl<City>(cities, ofSize(PAGE_SIZE), cities.size())

而不是:

Java

 

new PageImpl<City>(cities)

注意:此問題預計在Spring Boot 3.3中得到修復(參見此問題評論)。

結論

本文涵蓋了升級至撰寫本文時最新的Spring Boot 3.2.2版本所遇到的問題。文章首先探討了由於Hibernate依賴管理變更導致的註解處理器處理方式的改變。接著,解釋了Unpaged類別的變動以及使用PageImpl的解決方案。

所有這些變更(以及其他一些變更)均可在PR #64中查看。上述展示的完整源碼可於我的GitHub倉庫中獲得。

Source:
https://dzone.com/articles/upgrade-guide-to-spring-boot-32-for-spring-data-jp