Querydsl vs. JPA Criteria, Parte 6: Guía de actualización para Spring Boot 3.2 en proyectos de Spring Data JPA y Querydsl

El año pasado, escribí el artículo, “Guía de actualización para Spring Boot 3.0 para Spring Data JPA y Querydsl“, para la actualización de Spring Boot 3.0.x. Ahora, tenemos Spring Boot 3.2. Veamos dos problemas que podrías enfrentar al actualizar a Spring Boot 3.2.2.

Las tecnologías utilizadas en el proyecto SAT son:

  1. Spring Boot 3.2.2 y Spring Framework 6.1.3
  2. Generador de modelo Hibernate + JPA 6.4.1. Final 
  3. Spring Data JPA 3.2.2
  4. Querydsl 5.0.0.

Cambios

Todas las modificaciones en Spring Boot 3.2 se describen en Notas de lanzamiento de Spring Boot 3.2 y Novedades en la versión 6.1 para Spring Framework 6.1.

Los últimos cambios en Spring Boot 3.2.2 pueden encontrarse en GitHub.

Problemas encontrados

  • A different treatment of Hibernate dependencies due to the changed hibernate-jpamodelgen behavior for annotation processors
  • Unpaged class fue rediseñada.

Comencemos con las dependencias de Hibernate primero.

Integración de Generación de Metamodelo Estático

El mayor cambio proviene de la dependencia hibernate-jpamodelgen, que genera un metamodelo estático metamodel. En Hibernate 6.3, el manejo de dependencias se modificó para mitigar las dependencias transitivas. Spring Boot 3.2.0 actualizó la dependencia hibernate-jpamodelgen a la versión 6.3 (ver Actualizaciones de Dependencias). Desafortunadamente, la nueva versión provoca errores de compilación (ver más abajo).

Nota: Spring Boot 3.2.2 utilizado aquí ya utiliza Hibernate 6.4 con el mismo comportamiento.

Error de Compilación

Con este cambio, la compilación de nuestro proyecto (construcción con Maven) con Spring Boot 3.2.2 falla con un error como este:

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

Esto se debe al enfoque modificado en la generación del metamodelo estático anunciado en la guía de migración de Hibernate (ver Integración de Generación de Metamodelo Estático y el problema original HHH-17362). Su explicación para tal cambio es la siguiente:

“… en versiones anteriores de Hibernate ORM, estabas filtrando dependencias de hibernate-jpamodelgen en tu classpath de compilación sin darte cuenta. Con Hibernate ORM 6.3, ahora podrías experimentar un error de compilación durante el procesamiento de anotaciones relacionado con clases Antlr faltantes.”

Cambios en las Dependencias

Como se puede observar en las capturas de pantalla, las dependencias de Hibernate realmente han cambiado.

  • Spring Boot 3.1.6:

  • Spring Boot 3.2.2:

Explicación

Como se menciona en la guía de migración, necesitamos cambiar nuestro pom.xml de una simple dependencia de Maven a las rutas de procesadores de anotaciones del plugin de compilador de Maven (ver documentación).

Solución

Podemos eliminar las dependencias de Maven hibernate-jpamodelgen y querydsl-apt (en nuestro caso) como se recomienda en el último artículo. En su lugar, pom.xml debe definir los generadores de metamodelos estáticos a través de maven-compiler-plugin de esta manera:

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>

Vea los cambios relacionados en el proyecto SAT en GitHub.

Debido a que estamos obligados a utilizar este enfoque por hibernate-jpamodelgen, necesitamos aplicarlo a todas las dependencias relacionadas con el procesamiento de anotaciones (querydsl-apt o lombok). Por ejemplo, cuando lombok no se usa de esta manera, obtenemos un error de compilación como este:

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

Lo mismo se aplica a querydsl-apt. En este caso, podemos ver el error de compilación como este:

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]

La razón es evidente. Necesitamos aplicar todos los procesadores de anotaciones al mismo tiempo. De lo contrario, algunas partes del código pueden faltar, y obtenemos un error de compilación.

Rediseño sin paginar

El segundo problema menor está relacionado con un cambio en la clase Unpaged. La serialización de PageImpl por la biblioteca Jackson se vio afectada al cambiar Unpaged de enum a class (ver 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());

	...

}

Cuando se utiliza new PageImpl<City>(cities) (como solíamos usarlo), entonces se lanza este error:

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

La solución alternativa es usar el constructor con todos los atributos como:

Java

 

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

En lugar de:

Java

 

new PageImpl<City>(cities)

Nota: Debería solucionarse en Spring Boot 3.3 (ver este comentario de problema).

Conclusión

Este artículo ha abordado tanto los problemas encontrados al actualizar a la versión más reciente de Spring Boot 3.2.2 (al momento de escribir este artículo). El artículo comenzó con el manejo de los procesadores de anotaciones debido al cambio en la gestión de dependencias de Hibernate. A continuación, se explicó el cambio en la clase Unpaged y la solución alternativa para usar PageImpl.

Todas estas modificaciones (junto con otras) pueden verse en PR #64. El código fuente completo mostrado anteriormente está disponible en mi repositorio de GitHub.

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