From 7247f5ec639dcc7fc3c65021b981522c010aae25 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Mon, 14 Oct 2024 21:14:14 +0300 Subject: [PATCH] Auto-configure Hibernate with a JsonFormatMapper Signed-off-by: Dmytro Nosan --- .../jpa/HibernateJpaAutoConfiguration.java | 7 +- .../orm/jpa/HibernateJpaConfiguration.java | 39 +++++++++++ ...itional-spring-configuration-metadata.json | 25 ++++++++ .../HibernateJpaAutoConfigurationTests.java | 64 +++++++++++++++++++ 4 files changed, 133 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java index 807a0b74b2ad..876bb7a21bf4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,10 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -40,7 +42,8 @@ * @since 1.0.0 */ @AutoConfiguration( - after = { DataSourceAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class }, + after = { DataSourceAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class, + JacksonAutoConfiguration.class, JsonbAutoConfiguration.class }, before = { TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class }) @ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class, SessionImplementor.class }) @EnableConfigurationProperties(JpaProperties.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java index 981f5dd5b0ec..576f97319a66 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java @@ -27,12 +27,17 @@ import javax.sql.DataSource; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.bind.Jsonb; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy; import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.ManagedBeanSettings; +import org.hibernate.type.format.jackson.JacksonJsonFormatMapper; +import org.hibernate.type.format.jakartajson.JsonBJsonFormatMapper; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -42,6 +47,8 @@ import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration.HibernateRuntimeHints; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -51,8 +58,10 @@ import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider; import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy; import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.annotation.Order; import org.springframework.jdbc.support.SQLExceptionTranslator; import org.springframework.jndi.JndiLocatorDelegate; import org.springframework.orm.hibernate5.SpringBeanContainer; @@ -233,6 +242,36 @@ private Object getNoJtaPlatformManager() { "No available JtaPlatform candidates amongst " + Arrays.toString(NO_JTA_PLATFORM_CLASSES)); } + @ConditionalOnClass({ ObjectMapper.class, JacksonJsonFormatMapper.class }) + @ConditionalOnSingleCandidate(ObjectMapper.class) + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(name = "spring.jpa.hibernate.auto-configure.json-format-mapper", havingValue = "jackson") + static class HibernateJacksonJsonFormatMapperConfiguration { + + @Bean + @Order(0) + HibernatePropertiesCustomizer jacksonJsonFormatMapperHibernatePropertiesCustomizer(ObjectMapper objectMapper) { + return (properties) -> properties.put(AvailableSettings.JSON_FORMAT_MAPPER, + new JacksonJsonFormatMapper(objectMapper)); + } + + } + + @ConditionalOnClass({ Jsonb.class, JsonBJsonFormatMapper.class }) + @ConditionalOnSingleCandidate(Jsonb.class) + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(name = "spring.jpa.hibernate.auto-configure.json-format-mapper", havingValue = "jsonb") + static class HibernateJsonbJsonFormatMapperConfiguration { + + @Bean + @Order(0) + HibernatePropertiesCustomizer jsonbJsonFormatMapperHibernatePropertiesCustomizer(Jsonb jsonb) { + return (properties) -> properties.putIfAbsent(AvailableSettings.JSON_FORMAT_MAPPER, + new JsonBJsonFormatMapper(jsonb)); + } + + } + private static class NamingStrategiesHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer { private final PhysicalNamingStrategy physicalNamingStrategy; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index eab0b5ab7a06..086b2246568c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1641,6 +1641,12 @@ "level": "error" } }, + { + "name": "spring.jpa.hibernate.auto-configure.json-format-mapper", + "type": "java.lang.String", + "description": "Preferred Hibernate JSON format mapper to use.", + "defaultValue": "none" + }, { "name": "spring.jpa.hibernate.use-new-id-generator-mappings", "type": "java.lang.Boolean", @@ -3109,6 +3115,25 @@ } ] }, + { + "name": "spring.jpa.hibernate.auto-configure.json-format-mapper", + "values": [ + { + "value": "jackson" + }, + { + "value": "jsonb" + }, + { + "value": "none" + } + ], + "providers": [ + { + "name": "any" + } + ] + }, { "name": "spring.jpa.hibernate.ddl-auto", "values": [ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index 26146a7eb9fb..6022ad7e12ac 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -30,7 +30,10 @@ import javax.sql.DataSource; +import com.fasterxml.jackson.databind.ObjectMapper; import com.zaxxer.hikari.HikariDataSource; +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.transaction.Synchronization; @@ -40,6 +43,7 @@ import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy; import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.ManagedBeanSettings; import org.hibernate.cfg.SchemaToolingSettings; import org.hibernate.dialect.H2Dialect; @@ -47,6 +51,8 @@ import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; import org.hibernate.internal.SessionFactoryImpl; import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hibernate.type.format.jackson.JacksonJsonFormatMapper; +import org.hibernate.type.format.jakartajson.JsonBJsonFormatMapper; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -59,8 +65,10 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfigurationTests.JpaUsingApplicationListenerConfiguration.EventCapturingApplicationListener; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration.HibernateRuntimeHints; @@ -387,6 +395,62 @@ void hibernatePropertiesCustomizerTakesPrecedenceOverStrategyInstancesAndNamingS }); } + @Test + void jacksonJsonFormatMapperHibernatePropertiesCustomizerUsedAutoConfiguredObjectMapper() { + contextRunner().withPropertyValues("spring.jpa.hibernate.auto-configure.json-format-mapper=jackson") + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .run(vendorProperties( + (vendorProperties) -> assertThat(vendorProperties.get(AvailableSettings.JSON_FORMAT_MAPPER)) + .isInstanceOf(JacksonJsonFormatMapper.class))); + } + + @Test + void jacksonJsonFormatMapperHibernatePropertiesCustomizerShouldNotBeRegisteredIfNoSingleCandidate() { + contextRunner().withPropertyValues("spring.jpa.hibernate.auto-configure.json-format-mapper=jackson") + .withBean("objectMapper1", ObjectMapper.class, ObjectMapper::new) + .withBean("objectMapper2", ObjectMapper.class, ObjectMapper::new) + .run(vendorProperties( + (vendorProperties) -> assertThat(vendorProperties.get(AvailableSettings.JSON_FORMAT_MAPPER)) + .isNull())); + } + + @Test + void jacksonJsonFormatMapperHibernatePropertiesCustomizerShouldNotBeRegistered() { + contextRunner().withPropertyValues("spring.jpa.hibernate.auto-configure.json-format-mapper=none") + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .run(vendorProperties( + (vendorProperties) -> assertThat(vendorProperties.get(AvailableSettings.JSON_FORMAT_MAPPER)) + .isNull())); + } + + @Test + void jsonbJsonFormatMapperHibernatePropertiesCustomizerUsedAutoConfiguredObjectMapper() { + contextRunner().withPropertyValues("spring.jpa.hibernate.auto-configure.json-format-mapper=jsonb") + .withConfiguration(AutoConfigurations.of(JsonbAutoConfiguration.class)) + .run(vendorProperties( + (vendorProperties) -> assertThat(vendorProperties.get(AvailableSettings.JSON_FORMAT_MAPPER)) + .isInstanceOf(JsonBJsonFormatMapper.class))); + } + + @Test + void jsonbJsonFormatMapperHibernatePropertiesCustomizerShouldNotBeRegisteredIfNoSingleCandidate() { + contextRunner().withPropertyValues("spring.jpa.hibernate.auto-configure.json-format-mapper=jsonb") + .withBean("jsonb1", Jsonb.class, JsonbBuilder::create) + .withBean("jsonb2", Jsonb.class, JsonbBuilder::create) + .run(vendorProperties( + (vendorProperties) -> assertThat(vendorProperties.get(AvailableSettings.JSON_FORMAT_MAPPER)) + .isNull())); + } + + @Test + void jsonbJsonFormatMapperHibernatePropertiesCustomizerShouldNotBeRegistered() { + contextRunner().withPropertyValues("spring.jpa.hibernate.auto-configure.json-format-mapper=none") + .withConfiguration(AutoConfigurations.of(JsonbAutoConfiguration.class)) + .run(vendorProperties( + (vendorProperties) -> assertThat(vendorProperties.get(AvailableSettings.JSON_FORMAT_MAPPER)) + .isNull())); + } + @Test void eventListenerCanBeRegisteredAsBeans() { contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class)