From 59d5ed58428d8cb6c6d9fb723d0e334fe3e7d9be Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 5 Jan 2018 12:19:22 +0100 Subject: [PATCH] Add support for advanced customization of Hibernate settings As HibernateJpaConfiguration is package private, it is no longer possible to extend the default Spring Boot configuration to apply advanced settings. The most notable use case for this is the customization of Hibernate properties using instance value vs. string value that can be set using the "spring.jpa.properties" namespace. This commit adds a HibernatePropertiesCustomizer callback interface that can be implemented to tune Hibernate properties at will. Closes gh-11211 --- .../orm/jpa/HibernateJpaConfiguration.java | 14 ++++--- .../jpa/HibernatePropertiesCustomizer.java | 36 ++++++++++++++++++ .../orm/jpa/HibernateSettings.java | 18 ++++++++- .../autoconfigure/orm/jpa/JpaProperties.java | 10 ++++- ...tomHibernateJpaAutoConfigurationTests.java | 38 +++++++++++++++++-- .../orm/jpa/JpaPropertiesTests.java | 37 +++++++++++++++++- .../src/main/asciidoc/howto.adoc | 5 +++ 7 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernatePropertiesCustomizer.java 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 c325a27903..512cf140f2 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 @@ -87,13 +87,16 @@ class HibernateJpaConfiguration extends JpaBaseConfiguration { private final ImplicitNamingStrategy implicitNamingStrategy; + private final List hibernatePropertiesCustomizers; + HibernateJpaConfiguration(DataSource dataSource, JpaProperties jpaProperties, ObjectProvider jtaTransactionManager, ObjectProvider transactionManagerCustomizers, ObjectProvider> metadataProviders, ObjectProvider> providers, ObjectProvider physicalNamingStrategy, - ObjectProvider implicitNamingStrategy) { + ObjectProvider implicitNamingStrategy, + ObjectProvider> hibernatePropertiesCustomizers) { super(dataSource, jpaProperties, jtaTransactionManager, transactionManagerCustomizers); this.defaultDdlAutoProvider = new HibernateDefaultDdlAutoProvider( @@ -102,6 +105,8 @@ class HibernateJpaConfiguration extends JpaBaseConfiguration { metadataProviders.getIfAvailable()); this.physicalNamingStrategy = physicalNamingStrategy.getIfAvailable(); this.implicitNamingStrategy = implicitNamingStrategy.getIfAvailable(); + this.hibernatePropertiesCustomizers = hibernatePropertiesCustomizers + .getIfAvailable(() -> Collections.EMPTY_LIST); } @Override @@ -111,14 +116,13 @@ class HibernateJpaConfiguration extends JpaBaseConfiguration { @Override protected Map getVendorProperties() { - Map vendorProperties = new LinkedHashMap<>(); String defaultDdlMode = this.defaultDdlAutoProvider .getDefaultDdlAuto(getDataSource()); - vendorProperties.putAll(getProperties() + return new LinkedHashMap<>(getProperties() .getHibernateProperties(new HibernateSettings().ddlAuto(defaultDdlMode) .implicitNamingStrategy(this.implicitNamingStrategy) - .physicalNamingStrategy(this.physicalNamingStrategy))); - return vendorProperties; + .physicalNamingStrategy(this.physicalNamingStrategy) + .hibernatePropertiesCustomizers(this.hibernatePropertiesCustomizers))); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernatePropertiesCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernatePropertiesCustomizer.java new file mode 100644 index 0000000000..bb561db45b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernatePropertiesCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.orm.jpa; + +import java.util.Map; + +/** + * Callback interface that can be implemented by beans wishing to customize the Hibernate + * properties before it is used by an auto-configured {@code EntityManagerFactory}. + * + * @author Stephane Nicoll + */ +@FunctionalInterface +public interface HibernatePropertiesCustomizer { + + /** + * Customize the specified JPA vendor properties. + * @param hibernateProperties the current JPA vendor properties + */ + void customize(Map hibernateProperties); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateSettings.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateSettings.java index 7711c0312a..6e4aeb9963 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateSettings.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateSettings.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.orm.jpa; +import java.util.ArrayList; +import java.util.Collection; + import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy; @@ -33,6 +36,8 @@ public class HibernateSettings { private PhysicalNamingStrategy physicalNamingStrategy; + private Collection hibernatePropertiesCustomizers; + public HibernateSettings ddlAuto(String ddlAuto) { this.ddlAuto = ddlAuto; return this; @@ -62,4 +67,15 @@ public class HibernateSettings { return this.physicalNamingStrategy; } + public HibernateSettings hibernatePropertiesCustomizers( + Collection hibernatePropertiesCustomizers) { + this.hibernatePropertiesCustomizers = new ArrayList<>(); + this.hibernatePropertiesCustomizers.addAll(hibernatePropertiesCustomizers); + return this; + } + + public Collection getHibernatePropertiesCustomizers() { + return this.hibernatePropertiesCustomizers; + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java index d4bb20ba96..6ddac88415 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.orm.jpa; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,6 +29,7 @@ import org.hibernate.boot.model.naming.PhysicalNamingStrategy; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.orm.jpa.vendor.Database; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -222,6 +224,12 @@ public class JpaProperties { else { result.remove("hibernate.hbm2ddl.auto"); } + Collection hibernatePropertiesCustomizers = + settings.getHibernatePropertiesCustomizers(); + if (!ObjectUtils.isEmpty(hibernatePropertiesCustomizers)) { + hibernatePropertiesCustomizers.forEach((customizer) + -> customizer.customize(result)); + } return result; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/CustomHibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/CustomHibernateJpaAutoConfigurationTests.java index a651bbeffd..d06caac3e4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/CustomHibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/CustomHibernateJpaAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.test.util.ReflectionTestUtils; @@ -55,6 +56,7 @@ import static org.mockito.Mockito.mock; public class CustomHibernateJpaAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.datasource.generate-unique-name=true") .withUserConfiguration(TestConfiguration.class) .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class)); @@ -86,10 +88,23 @@ public class CustomHibernateJpaAutoConfigurationTests { .getVendorProperties(); assertThat(hibernateProperties .get("hibernate.implicit_naming_strategy")).isEqualTo( - NamingStrategyConfiguration.implicitNamingStrategy); + NamingStrategyConfiguration.implicitNamingStrategy); assertThat(hibernateProperties .get("hibernate.physical_naming_strategy")).isEqualTo( - NamingStrategyConfiguration.physicalNamingStrategy); + NamingStrategyConfiguration.physicalNamingStrategy); + }); + } + + @Test + public void hibernatePropertiesCustomizersAreAppliedInOrder() { + this.contextRunner + .withUserConfiguration(HibernatePropertiesCustomizerConfiguration.class) + .run((context) -> { + HibernateJpaConfiguration jpaConfiguration = context + .getBean(HibernateJpaConfiguration.class); + Map hibernateProperties = jpaConfiguration + .getVendorProperties(); + assertThat(hibernateProperties.get("test.counter")).isEqualTo(2); }); } @@ -149,4 +164,21 @@ public class CustomHibernateJpaAutoConfigurationTests { } + @Configuration + static class HibernatePropertiesCustomizerConfiguration { + + @Bean + @Order(2) + public HibernatePropertiesCustomizer sampleCustomizer() { + return ((hibernateProperties) -> hibernateProperties.put("test.counter", 2)); + } + + @Bean + @Order(1) + public HibernatePropertiesCustomizer anotherCustomizer() { + return ((hibernateProperties) -> hibernateProperties.put("test.counter", 1)); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/JpaPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/JpaPropertiesTests.java index dc115f840a..8bc909cdd7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/JpaPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/JpaPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.orm.jpa; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; +import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -126,6 +127,40 @@ public class JpaPropertiesTests { })); } + + @Test + public void hibernatePropertiesCustomizerTakePrecedenceOverStrategyInstancesAndNamingStrategyProperties() { + this.contextRunner + .withPropertyValues( + "spring.jpa.hibernate.naming.implicit-strategy:com.example.Implicit", + "spring.jpa.hibernate.naming.physical-strategy:com.example.Physical" + ).run(assertJpaProperties((properties) -> { + ImplicitNamingStrategy implicitStrategy = mock(ImplicitNamingStrategy.class); + PhysicalNamingStrategy physicalStrategy = mock(PhysicalNamingStrategy.class); + ImplicitNamingStrategy effectiveImplicitStrategy = mock( + ImplicitNamingStrategy.class); + PhysicalNamingStrategy effectivePhysicalStrategy = mock( + PhysicalNamingStrategy.class); + HibernatePropertiesCustomizer customizer = (hibernateProperties) -> { + hibernateProperties.put("hibernate.implicit_naming_strategy", + effectiveImplicitStrategy); + hibernateProperties.put("hibernate.physical_naming_strategy", + effectivePhysicalStrategy); + }; + Map hibernateProperties = properties + .getHibernateProperties(new HibernateSettings().ddlAuto("none") + .implicitNamingStrategy(implicitStrategy) + .physicalNamingStrategy(physicalStrategy) + .hibernatePropertiesCustomizers( + Collections.singleton(customizer))); + assertThat(hibernateProperties).contains( + entry("hibernate.implicit_naming_strategy", effectiveImplicitStrategy), + entry("hibernate.physical_naming_strategy", effectivePhysicalStrategy)); + assertThat(hibernateProperties) + .doesNotContainKeys("hibernate.ejb.naming_strategy"); + })); + } + @Test public void hibernate5CustomNamingStrategiesViaJpaProperties() { this.contextRunner diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc index 6e57d8bbb3..d4994b4e83 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -1787,6 +1787,11 @@ In addition, all properties in `+spring.jpa.properties.*+` are passed through as JPA properties (with the prefix stripped) when the local `EntityManagerFactory` is created. +TIP: if you need to apply advanced customization to Hibernate properties, consider +registering a `HibernatePropertiesCustomizer` bean that will be invoked prior to creating +the `EntityManagerFactory`. This takes precedence to anything that is applied by the +auto-configuration. + [[howto-configure-hibernate-naming-strategy]]