From 3ffc5f2a301186c67fa2d59cbdb83c2eaa7eaf48 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 26 Sep 2019 21:14:46 -0700 Subject: [PATCH] Polish 'Support programmatic lazy-int exclusion' See gh-16615 --- .../EagerLoadingBeanDefinitionPredicate.java | 53 ----------- ...nitializationBeanFactoryPostProcessor.java | 87 ++++++++----------- .../boot/LazyInitializationExcludeFilter.java | 76 ++++++++++++++++ .../LazyInitializationExcludeFilterTests.java | 47 ++++++++++ .../boot/SpringApplicationTests.java | 20 ++--- 5 files changed, 171 insertions(+), 112 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/EagerLoadingBeanDefinitionPredicate.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationExcludeFilter.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/LazyInitializationExcludeFilterTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/EagerLoadingBeanDefinitionPredicate.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/EagerLoadingBeanDefinitionPredicate.java deleted file mode 100644 index fd9c5706d87..00000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/EagerLoadingBeanDefinitionPredicate.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 - * - * https://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; - -import java.util.function.Predicate; - -/** - * This predicate can be implemented by downstream projects to customize the behavior of - * the {@link LazyInitializationBeanFactoryPostProcessor}. - * - *

- * There are edge cases (such as in DSLs that dynamically create additional beans) in - * which it is not easy to explicitly exclude a class from the lazy-loading behavior. - * Adding an instance of this predicate to the application context can be used for these - * edge cases. - *

- * Returning "true" from this predicate will exclude a class from the lazy-loading - * process. - * - *

- * Example: - *

- *

- * {@code
- *
- * @Bean
- * public static EagerLoadingBeanDefinitionPredicate eagerLoadingBeanDefinitionPredicate() {
- *   return IntegrationFlow.class::isAssignableFrom;
- * }}
- * - * WARNING: Beans of this type will be instantiated very early in the spring application - * life cycle. - * - * @author Tyler Van Gorder - * @since 2.2.0 - */ -public interface EagerLoadingBeanDefinitionPredicate extends Predicate> { - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java index 64aeb9d4191..ef1fbfe155c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java @@ -16,11 +16,10 @@ package org.springframework.boot; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.Collection; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -28,71 +27,61 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.core.Ordered; /** - * {@link BeanFactoryPostProcessor} to set the lazy attribute on bean definition. - * - *

- * This processor will not touch a bean definition that has already had its "lazy" flag - * explicitly set to "false". - * - *

- * There are edge cases in which it is not easy to explicitly set the "lazy" flag to - * "false" (such as in DSLs that dynamically create additional beans) and therefore this - * class uses a customizer strategy that allows downstream projects to contribute - * predicates which impact if a class is considered for lazy-loading. - * - *

- * Because this is a BeanFactoryPostProcessor, this class does not use dependency - * injection to collect the customizers. The post processor actually makes two passes - * through the bean definitions; the first is used to find and instantiate any - * {@link org.springframework.boot.EagerLoadingBeanDefinitionPredicate} and the second - * pass is where bean definitions are marked as lazy. + * {@link BeanFactoryPostProcessor} to set lazy-init on bean definitions that not + * {@link LazyInitializationExcludeFilter excluded} and have not already had a value + * explicitly set. * * @author Andy Wilkinson * @author Madhura Bhave * @author Tyler Van Gorder + * @author Phillip Webb * @since 2.2.0 + * @see LazyInitializationExcludeFilter */ public final class LazyInitializationBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - - List eagerPredicateList = getEagerLoadingPredicatesFromContext( - beanFactory); - + // Take care not to force the eager init of factory beans when getting filters + Collection filters = beanFactory + .getBeansOfType(LazyInitializationExcludeFilter.class, false, false).values(); for (String beanName : beanFactory.getBeanDefinitionNames()) { BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); - if (eagerPredicateList.stream() - .anyMatch((predicate) -> predicate.test(beanFactory.getType(beanName, false)))) { - continue; - } if (beanDefinition instanceof AbstractBeanDefinition) { - Boolean lazyInit = ((AbstractBeanDefinition) beanDefinition).getLazyInit(); - if (lazyInit != null && !lazyInit) { - continue; - } + postProcess(beanFactory, filters, beanName, (AbstractBeanDefinition) beanDefinition); } - beanDefinition.setLazyInit(true); } } - /** - * This method extracts the list of - * {@link org.springframework.boot.EagerLoadingBeanDefinitionPredicate} beans from the - * bean factory. Because this method is called early in the factory life cycle, we - * take care not to force the eager initialization of factory beans. - * @param beanFactory bean factory passed into the post-processor. - * @return a list of {@link EagerLoadingBeanDefinitionPredicate} that can be used to - * customize the behavior of this processor. - */ - private List getEagerLoadingPredicatesFromContext( - ConfigurableListableBeanFactory beanFactory) { - - Map eagerPredicates = beanFactory - .getBeansOfType(EagerLoadingBeanDefinitionPredicate.class, false, false); + private void postProcess(ConfigurableListableBeanFactory beanFactory, + Collection filters, String beanName, + AbstractBeanDefinition beanDefinition) { + Boolean lazyInit = beanDefinition.getLazyInit(); + Class beanType = getBeanType(beanFactory, beanName); + if (lazyInit == null && !isExcluded(filters, beanName, beanDefinition, beanType)) { + beanDefinition.setLazyInit(true); + } + } - return new ArrayList<>(eagerPredicates.values()); + private Class getBeanType(ConfigurableListableBeanFactory beanFactory, String beanName) { + try { + return beanFactory.getType(beanName, false); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } + private boolean isExcluded(Collection filters, String beanName, + AbstractBeanDefinition beanDefinition, Class beanType) { + if (beanType != null) { + for (LazyInitializationExcludeFilter filter : filters) { + if (filter.isExcluded(beanName, beanDefinition, beanType)) { + return true; + } + } + } + return false; } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationExcludeFilter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationExcludeFilter.java new file mode 100644 index 00000000000..63bf3cb79a1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationExcludeFilter.java @@ -0,0 +1,76 @@ +/* + * 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 + * + * https://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; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; + +/** + * Filter that can be used to exclude beans definitions from having their + * {@link AbstractBeanDefinition#setLazyInit(boolean) lazy-int} set by the + * {@link LazyInitializationBeanFactoryPostProcessor}. + *

+ * Primarily intended to allow downstream projects to deal with edge-cases in which it is + * not easy to support lazy-loading (such as in DSLs that dynamically create additional + * beans). Adding an instance of this filter to the application context can be used for + * these edge cases. + *

+ * A typical example would be something like this: + *

+ *


+ * @Bean
+ * public static LazyInitializationExcludeFilter integrationLazyInitializationExcludeFilter() {
+ *   return LazyInitializationExcludeFilter.forBeanTypes(IntegrationFlow.class);
+ * }
+ *

+ * NOTE: Beans of this type will be instantiated very early in the spring application + * lifecycle so should generally be declared static and not have any dependencies. + * + * @author Tyler Van Gorder + * @author Philip Webb + * @since 2.2.0 + */ +@FunctionalInterface +public interface LazyInitializationExcludeFilter { + + /** + * Returns {@code true} if the specified bean definition should be excluded from + * having {@code lazy-int} automatically set. + * @param beanName the bean name + * @param beanDefinition the bean definition + * @param beanType the bean type + * @return {@code true} if {@code lazy-int} should not be automatically set + */ + boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class beanType); + + /** + * Factory method that creates a filter for the given bean types. + * @param types the filtered types + * @return a new filter instance + */ + static LazyInitializationExcludeFilter forBeanTypes(Class... types) { + return (beanName, beanDefinition, beanType) -> { + for (Class type : types) { + if (type.isAssignableFrom(beanType)) { + return true; + } + } + return false; + }; + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/LazyInitializationExcludeFilterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/LazyInitializationExcludeFilterTests.java new file mode 100644 index 00000000000..5e8debf0248 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/LazyInitializationExcludeFilterTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2019 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 + * + * https://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; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LazyInitializationExcludeFilter}. + * + * @author Phillip Webb + */ +class LazyInitializationExcludeFilterTests { + + @Test + void forBeanTypesMatchesTypes() { + LazyInitializationExcludeFilter filter = LazyInitializationExcludeFilter.forBeanTypes(CharSequence.class, + Number.class); + String beanName = "test"; + BeanDefinition beanDefinition = mock(BeanDefinition.class); + assertThat(filter.isExcluded(beanName, beanDefinition, CharSequence.class)).isTrue(); + assertThat(filter.isExcluded(beanName, beanDefinition, String.class)).isTrue(); + assertThat(filter.isExcluded(beanName, beanDefinition, StringBuilder.class)).isTrue(); + assertThat(filter.isExcluded(beanName, beanDefinition, Number.class)).isTrue(); + assertThat(filter.isExcluded(beanName, beanDefinition, Long.class)).isTrue(); + assertThat(filter.isExcluded(beanName, beanDefinition, Boolean.class)).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 63d9b4087b7..a90c97f64f5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -1107,15 +1107,15 @@ class SpringApplicationTests { } @Test - void lazyInitializationShouldNotApplyToBeansThatAreExplicitlyNotLazy() { + void lazyInitializationIgnoresBeansThatAreExplicitlyNotLazy() { assertThat(new SpringApplication(NotLazyInitializationConfig.class) .run("--spring.main.web-application-type=none", "--spring.main.lazy-initialization=true") .getBean(AtomicInteger.class)).hasValue(1); } @Test - void lazyInitializationShouldNotApplyToBeansThatMatchPredicate() { - assertThat(new SpringApplication(NotLazyInitializationPredicateConfig.class) + void lazyInitializationIgnoresLazyInitializationExcludeFilteredBeans() { + assertThat(new SpringApplication(LazyInitializationExcludeFilterConfig.class) .run("--spring.main.web-application-type=none", "--spring.main.lazy-initialization=true") .getBean(AtomicInteger.class)).hasValue(1); } @@ -1430,7 +1430,7 @@ class SpringApplicationTests { } @Configuration(proxyBeanMethods = false) - static class NotLazyInitializationPredicateConfig { + static class LazyInitializationExcludeFilterConfig { @Bean AtomicInteger counter() { @@ -1443,16 +1443,16 @@ class SpringApplicationTests { } @Bean - static EagerLoadingBeanDefinitionPredicate eagerLoadingBeanDefinitionPredicate() { - return NotLazyBean.class::isAssignableFrom; + static LazyInitializationExcludeFilter lazyInitializationExcludeFilter() { + return LazyInitializationExcludeFilter.forBeanTypes(NotLazyBean.class); } - static class NotLazyBean { + } - NotLazyBean(AtomicInteger counter) { - counter.getAndIncrement(); - } + static class NotLazyBean { + NotLazyBean(AtomicInteger counter) { + counter.getAndIncrement(); } }