From 0b2f9180a476f26cfbaac6008b2a57c70934c909 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 --- .../jpa/HibernateJpaAutoConfiguration.java | 6 ++-- .../orm/jpa/HibernateJpaConfiguration.java | 21 +++++++++++- .../HibernateJpaAutoConfigurationTests.java | 34 +++++++++++++++++++ 3 files changed, 58 insertions(+), 3 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..a6af4aa0ef54 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-2024 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,6 +22,7 @@ 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.transaction.TransactionAutoConfiguration; @@ -40,7 +41,8 @@ * @since 1.0.0 */ @AutoConfiguration( - after = { DataSourceAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class }, + after = { DataSourceAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class, + JacksonAutoConfiguration.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 d402c764822a..b2f9e734b30f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -27,12 +27,14 @@ import javax.sql.DataSource; +import com.fasterxml.jackson.databind.ObjectMapper; 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.type.format.jackson.JacksonJsonFormatMapper; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -42,6 +44,7 @@ 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.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration.HibernateRuntimeHints; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -51,7 +54,9 @@ 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.Import; import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.jndi.JndiLocatorDelegate; import org.springframework.orm.hibernate5.SpringBeanContainer; @@ -74,6 +79,7 @@ @EnableConfigurationProperties(HibernateProperties.class) @ConditionalOnSingleCandidate(DataSource.class) @ImportRuntimeHints(HibernateRuntimeHints.class) +@Import(HibernateJpaConfiguration.HibernateJsonFormatMapperConfiguration.class) class HibernateJpaConfiguration extends JpaBaseConfiguration { private static final Log logger = LogFactory.getLog(HibernateJpaConfiguration.class); @@ -226,6 +232,19 @@ 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) + static class HibernateJsonFormatMapperConfiguration { + + @Bean + HibernatePropertiesCustomizer jsonFormatMapperHibernatePropertiesCustomizer(ObjectMapper objectMapper) { + return (properties) -> properties.putIfAbsent(AvailableSettings.JSON_FORMAT_MAPPER, + new JacksonJsonFormatMapper(objectMapper)); + } + + } + private static class NamingStrategiesHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer { private final PhysicalNamingStrategy physicalNamingStrategy; 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 3d0d625c33b5..78c5b48c71c1 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,6 +30,7 @@ import javax.sql.DataSource; +import com.fasterxml.jackson.databind.ObjectMapper; import com.zaxxer.hikari.HikariDataSource; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; @@ -40,6 +41,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 +49,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.AbstractJsonFormatMapper; +import org.hibernate.type.format.jackson.JacksonJsonFormatMapper; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -59,6 +63,7 @@ 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.liquibase.LiquibaseAutoConfiguration; @@ -367,6 +372,35 @@ void hibernatePropertiesCustomizerTakesPrecedenceOverStrategyInstancesAndNamingS }); } + @Test + void jsonFormatMapperHibernatePropertiesCustomizerUsedAutoConfiguredObjectMapper() { + contextRunner().withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .run(vendorProperties( + (vendorProperties) -> assertThat(vendorProperties.get(AvailableSettings.JSON_FORMAT_MAPPER)) + .isInstanceOf(JacksonJsonFormatMapper.class))); + } + + @Test + void jsonFormatMapperHibernatePropertiesCustomizerShouldNotOverrideJsonFormatMapperIfAlreadyConfigured() { + AbstractJsonFormatMapper jsonFormatMapper = mock(AbstractJsonFormatMapper.class); + contextRunner().withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withBean(HibernatePropertiesCustomizer.class, + () -> (hibernateProperties) -> hibernateProperties.put(AvailableSettings.JSON_FORMAT_MAPPER, + jsonFormatMapper)) + .run(vendorProperties( + (vendorProperties) -> assertThat(vendorProperties.get(AvailableSettings.JSON_FORMAT_MAPPER)) + .isSameAs(jsonFormatMapper))); + } + + @Test + void jsonFormatMapperHibernatePropertiesCustomizerShouldNotBeRegisteredIfNoSingleCandidate() { + contextRunner().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 eventListenerCanBeRegisteredAsBeans() { contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class)