diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnClassCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnClassCondition.java index 62200e4007e..e838c95adf5 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnClassCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnClassCondition.java @@ -16,11 +16,20 @@ package org.springframework.boot.autoconfigure.condition; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; import java.util.List; +import java.util.Set; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter; +import org.springframework.boot.autoconfigure.AutoConfigurationMetadata; import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; @@ -31,24 +40,113 @@ import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; /** - * {@link Condition} that checks for the presence or absence of specific classes. + * {@link Condition} and {@link AutoConfigurationImportFilter} that checks for the + * presence or absence of specific classes. * * @author Phillip Webb * @see ConditionalOnClass * @see ConditionalOnMissingClass */ @Order(Ordered.HIGHEST_PRECEDENCE) -class OnClassCondition extends SpringBootCondition { +class OnClassCondition extends SpringBootCondition + implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware { + + private BeanFactory beanFactory; + + private ClassLoader beanClassLoader; + + @Override + public boolean[] match(String[] autoConfigurationClasses, + AutoConfigurationMetadata autoConfigurationMetadata) { + ConditionEvaluationReport report = getConditionEvaluationReport(); + ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, + autoConfigurationMetadata); + boolean[] match = new boolean[outcomes.length]; + for (int i = 0; i < outcomes.length; i++) { + match[i] = (outcomes[i] == null || outcomes[i].isMatch()); + if (!match[i] && outcomes[i] != null) { + logOutcome(autoConfigurationClasses[i], outcomes[i]); + if (report != null) { + report.recordConditionEvaluation(autoConfigurationClasses[i], this, + outcomes[i]); + } + } + } + return match; + } + + private ConditionEvaluationReport getConditionEvaluationReport() { + if (this.beanFactory != null + && this.beanFactory instanceof ConfigurableBeanFactory) { + return ConditionEvaluationReport + .get((ConfigurableListableBeanFactory) this.beanFactory); + } + return null; + } + + private ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, + AutoConfigurationMetadata autoConfigurationMetadata) { + // Split the work and perform half in a background thread. Using a single + // additional thread seems to offer the best performance. More threads make + // things worse + int split = autoConfigurationClasses.length / 2; + GetOutcomesThread thread = new GetOutcomesThread(autoConfigurationClasses, 0, + split, autoConfigurationMetadata); + thread.start(); + ConditionOutcome[] secondHalf = getOutcomes(autoConfigurationClasses, split, + autoConfigurationClasses.length, autoConfigurationMetadata); + try { + thread.join(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + ConditionOutcome[] firstHalf = thread.getResult(); + ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length]; + System.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length); + System.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length); + return outcomes; + } + + private ConditionOutcome[] getOutcomes(final String[] autoConfigurationClasses, + int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) { + ConditionOutcome[] outcomes = new ConditionOutcome[end - start]; + for (int i = start; i < end; i++) { + String autoConfigurationClass = autoConfigurationClasses[i]; + Set candidates = autoConfigurationMetadata + .getSet(autoConfigurationClass, "ConditionalOnClass"); + if (candidates != null) { + outcomes[i - start] = getOutcome(candidates); + } + } + return outcomes; + } + + private ConditionOutcome getOutcome(Set candidates) { + try { + List missing = getMatches(candidates, MatchType.MISSING, + this.beanClassLoader); + if (!missing.isEmpty()) { + return ConditionOutcome + .noMatch(ConditionMessage.forCondition(ConditionalOnClass.class) + .didNotFind("required class", "required classes") + .items(Style.QUOTE, missing)); + } + } + catch (Exception ex) { + // We'll get another chance later + } + return null; + } @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ClassLoader classLoader = context.getClassLoader(); ConditionMessage matchMessage = ConditionMessage.empty(); - MultiValueMap onClasses = getAttributes(metadata, - ConditionalOnClass.class); + List onClasses = getCandidates(metadata, ConditionalOnClass.class); if (onClasses != null) { - List missing = getMatchingClasses(onClasses, MatchType.MISSING, - context); + List missing = getMatches(onClasses, MatchType.MISSING, classLoader); if (!missing.isEmpty()) { return ConditionOutcome .noMatch(ConditionMessage.forCondition(ConditionalOnClass.class) @@ -57,13 +155,13 @@ class OnClassCondition extends SpringBootCondition { } matchMessage = matchMessage.andCondition(ConditionalOnClass.class) .found("required class", "required classes").items(Style.QUOTE, - getMatchingClasses(onClasses, MatchType.PRESENT, context)); + getMatches(onClasses, MatchType.PRESENT, classLoader)); } - MultiValueMap onMissingClasses = getAttributes(metadata, + List onMissingClasses = getCandidates(metadata, ConditionalOnMissingClass.class); if (onMissingClasses != null) { - List present = getMatchingClasses(onMissingClasses, MatchType.PRESENT, - context); + List present = getMatches(onMissingClasses, MatchType.PRESENT, + classLoader); if (!present.isEmpty()) { return ConditionOutcome.noMatch( ConditionMessage.forCondition(ConditionalOnMissingClass.class) @@ -71,30 +169,23 @@ class OnClassCondition extends SpringBootCondition { .items(Style.QUOTE, present)); } matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class) - .didNotFind("unwanted class", "unwanted classes") - .items(Style.QUOTE, getMatchingClasses(onMissingClasses, - MatchType.MISSING, context)); + .didNotFind("unwanted class", "unwanted classes").items(Style.QUOTE, + getMatches(onMissingClasses, MatchType.MISSING, classLoader)); } return ConditionOutcome.match(matchMessage); } - private MultiValueMap getAttributes(AnnotatedTypeMetadata metadata, + private List getCandidates(AnnotatedTypeMetadata metadata, Class annotationType) { - return metadata.getAllAnnotationAttributes(annotationType.getName(), true); - } - - private List getMatchingClasses(MultiValueMap attributes, - MatchType matchType, ConditionContext context) { - List matches = new LinkedList(); - addAll(matches, attributes.get("value")); - addAll(matches, attributes.get("name")); - Iterator iterator = matches.iterator(); - while (iterator.hasNext()) { - if (!matchType.matches(iterator.next(), context)) { - iterator.remove(); - } + MultiValueMap attributes = metadata + .getAllAnnotationAttributes(annotationType.getName(), true); + List candidates = new ArrayList(); + if (attributes == null) { + return Collections.emptyList(); } - return matches; + addAll(candidates, attributes.get("value")); + addAll(candidates, attributes.get("name")); + return candidates; } private void addAll(List list, List itemsToAdd) { @@ -105,13 +196,34 @@ class OnClassCondition extends SpringBootCondition { } } + private List getMatches(Collection candiates, MatchType matchType, + ClassLoader classLoader) { + List matches = new ArrayList(candiates.size()); + for (String candidate : candiates) { + if (matchType.matches(candidate, classLoader)) { + matches.add(candidate); + } + } + return matches; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + private enum MatchType { PRESENT { @Override - public boolean matches(String className, ConditionContext context) { - return isPresent(className, context.getClassLoader()); + public boolean matches(String className, ClassLoader classLoader) { + return isPresent(className, classLoader); } }, @@ -119,8 +231,8 @@ class OnClassCondition extends SpringBootCondition { MISSING { @Override - public boolean matches(String className, ConditionContext context) { - return !isPresent(className, context.getClassLoader()); + public boolean matches(String className, ClassLoader classLoader) { + return !isPresent(className, classLoader); } }; @@ -146,8 +258,39 @@ class OnClassCondition extends SpringBootCondition { return Class.forName(className); } - public abstract boolean matches(String className, ConditionContext context); + public abstract boolean matches(String className, ClassLoader classLoader); } + private class GetOutcomesThread extends Thread { + + private final String[] autoConfigurationClasses; + + private final int start; + + private final int end; + + private final AutoConfigurationMetadata autoConfigurationMetadata; + + private ConditionOutcome[] outcomes; + + GetOutcomesThread(String[] autoConfigurationClasses, int start, int end, + AutoConfigurationMetadata autoConfigurationMetadata) { + this.autoConfigurationClasses = autoConfigurationClasses; + this.start = start; + this.end = end; + this.autoConfigurationMetadata = autoConfigurationMetadata; + } + + @Override + public void run() { + this.outcomes = getOutcomes(this.autoConfigurationClasses, this.start, + this.end, this.autoConfigurationMetadata); + } + + public ConditionOutcome[] getResult() { + return this.outcomes; + } + + } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java index d19a733080d..1345df71737 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2017 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. @@ -87,7 +87,7 @@ public abstract class SpringBootCondition implements Condition { + methodMetadata.getMethodName(); } - private void logOutcome(String classOrMethodName, ConditionOutcome outcome) { + protected final void logOutcome(String classOrMethodName, ConditionOutcome outcome) { if (this.logger.isTraceEnabled()) { this.logger.trace(getLogMessage(classOrMethodName, outcome)); } diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 876a630e29c..9963275324a 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -11,6 +11,10 @@ org.springframework.boot.autoconfigure.BackgroundPreinitializer org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\ org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener +# Auto Configuration Import Filters +org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\ +org.springframework.boot.autoconfigure.condition.OnClassCondition + # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/OnClassConditionAutoConfigurationImportFilterTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/OnClassConditionAutoConfigurationImportFilterTests.java new file mode 100644 index 00000000000..a8f34dd52ee --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/OnClassConditionAutoConfigurationImportFilterTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2017 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.condition; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter; +import org.springframework.boot.autoconfigure.AutoConfigurationMetadata; +import org.springframework.core.io.support.SpringFactoriesLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for the {@link AutoConfigurationImportFilter} part of {@link OnClassCondition}. + * + * @author Phillip Webb + */ +public class OnClassConditionAutoConfigurationImportFilterTests { + + private OnClassCondition filter = new OnClassCondition(); + + private DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Before + public void setup() { + this.filter.setBeanClassLoader(getClass().getClassLoader()); + this.filter.setBeanFactory(this.beanFactory); + } + + @Test + public void shouldBeRegistered() throws Exception { + assertThat(SpringFactoriesLoader + .loadFactories(AutoConfigurationImportFilter.class, null)) + .hasAtLeastOneElementOfType(OnClassCondition.class); + } + + @Test + public void matchShouldMatchClasses() throws Exception { + String[] autoConfigurationClasses = new String[] { "test.match", "test.nomatch" }; + boolean[] result = this.filter.match(autoConfigurationClasses, + getAutoConfigurationMetadata()); + assertThat(result).containsExactly(true, false); + } + + @Test + public void matchShouldRecordOutcome() throws Exception { + String[] autoConfigurationClasses = new String[] { "test.match", "test.nomatch" }; + this.filter.match(autoConfigurationClasses, getAutoConfigurationMetadata()); + ConditionEvaluationReport report = ConditionEvaluationReport + .get(this.beanFactory); + assertThat(report.getConditionAndOutcomesBySource()).hasSize(1) + .containsKey("test.nomatch"); + } + + private AutoConfigurationMetadata getAutoConfigurationMetadata() { + AutoConfigurationMetadata metadata = mock(AutoConfigurationMetadata.class); + given(metadata.wasProcessed("test.match")).willReturn(true); + given(metadata.getSet("test.match", "ConditionalOnClass")) + .willReturn(Collections.singleton("java.io.InputStream")); + given(metadata.wasProcessed("test.nomatch")).willReturn(true); + given(metadata.getSet("test.nomatch", "ConditionalOnClass")) + .willReturn(Collections.singleton("java.io.DoesNotExist")); + return metadata; + } + +}