Commit de50cfa2 authored by Phillip Webb's avatar Phillip Webb

Make OnClassCondition an AutoConfigurationImportFilter

Update OnClassCondition to implement AutoConfigurationImportFilter so
that auto-configuration candidates can be filtered early. The
optimization helps to improve application startup time by reducing
the number of classes that are loaded.

See gh-7573
parent 20a20b77
...@@ -16,11 +16,20 @@ ...@@ -16,11 +16,20 @@
package org.springframework.boot.autoconfigure.condition; package org.springframework.boot.autoconfigure.condition;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; 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.boot.autoconfigure.condition.ConditionMessage.Style;
import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.ConditionContext;
...@@ -31,24 +40,113 @@ import org.springframework.util.ClassUtils; ...@@ -31,24 +40,113 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap; 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 * @author Phillip Webb
* @see ConditionalOnClass * @see ConditionalOnClass
* @see ConditionalOnMissingClass * @see ConditionalOnMissingClass
*/ */
@Order(Ordered.HIGHEST_PRECEDENCE) @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<String> candidates = autoConfigurationMetadata
.getSet(autoConfigurationClass, "ConditionalOnClass");
if (candidates != null) {
outcomes[i - start] = getOutcome(candidates);
}
}
return outcomes;
}
private ConditionOutcome getOutcome(Set<String> candidates) {
try {
List<String> 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 @Override
public ConditionOutcome getMatchOutcome(ConditionContext context, public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) { AnnotatedTypeMetadata metadata) {
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty(); ConditionMessage matchMessage = ConditionMessage.empty();
MultiValueMap<String, Object> onClasses = getAttributes(metadata, List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
ConditionalOnClass.class);
if (onClasses != null) { if (onClasses != null) {
List<String> missing = getMatchingClasses(onClasses, MatchType.MISSING, List<String> missing = getMatches(onClasses, MatchType.MISSING, classLoader);
context);
if (!missing.isEmpty()) { if (!missing.isEmpty()) {
return ConditionOutcome return ConditionOutcome
.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class) .noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
...@@ -57,13 +155,13 @@ class OnClassCondition extends SpringBootCondition { ...@@ -57,13 +155,13 @@ class OnClassCondition extends SpringBootCondition {
} }
matchMessage = matchMessage.andCondition(ConditionalOnClass.class) matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
.found("required class", "required classes").items(Style.QUOTE, .found("required class", "required classes").items(Style.QUOTE,
getMatchingClasses(onClasses, MatchType.PRESENT, context)); getMatches(onClasses, MatchType.PRESENT, classLoader));
} }
MultiValueMap<String, Object> onMissingClasses = getAttributes(metadata, List<String> onMissingClasses = getCandidates(metadata,
ConditionalOnMissingClass.class); ConditionalOnMissingClass.class);
if (onMissingClasses != null) { if (onMissingClasses != null) {
List<String> present = getMatchingClasses(onMissingClasses, MatchType.PRESENT, List<String> present = getMatches(onMissingClasses, MatchType.PRESENT,
context); classLoader);
if (!present.isEmpty()) { if (!present.isEmpty()) {
return ConditionOutcome.noMatch( return ConditionOutcome.noMatch(
ConditionMessage.forCondition(ConditionalOnMissingClass.class) ConditionMessage.forCondition(ConditionalOnMissingClass.class)
...@@ -71,30 +169,23 @@ class OnClassCondition extends SpringBootCondition { ...@@ -71,30 +169,23 @@ class OnClassCondition extends SpringBootCondition {
.items(Style.QUOTE, present)); .items(Style.QUOTE, present));
} }
matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class) matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class)
.didNotFind("unwanted class", "unwanted classes") .didNotFind("unwanted class", "unwanted classes").items(Style.QUOTE,
.items(Style.QUOTE, getMatchingClasses(onMissingClasses, getMatches(onMissingClasses, MatchType.MISSING, classLoader));
MatchType.MISSING, context));
} }
return ConditionOutcome.match(matchMessage); return ConditionOutcome.match(matchMessage);
} }
private MultiValueMap<String, Object> getAttributes(AnnotatedTypeMetadata metadata, private List<String> getCandidates(AnnotatedTypeMetadata metadata,
Class<?> annotationType) { Class<?> annotationType) {
return metadata.getAllAnnotationAttributes(annotationType.getName(), true); MultiValueMap<String, Object> attributes = metadata
} .getAllAnnotationAttributes(annotationType.getName(), true);
List<String> candidates = new ArrayList<String>();
private List<String> getMatchingClasses(MultiValueMap<String, Object> attributes, if (attributes == null) {
MatchType matchType, ConditionContext context) { return Collections.emptyList();
List<String> matches = new LinkedList<String>();
addAll(matches, attributes.get("value"));
addAll(matches, attributes.get("name"));
Iterator<String> iterator = matches.iterator();
while (iterator.hasNext()) {
if (!matchType.matches(iterator.next(), context)) {
iterator.remove();
}
} }
return matches; addAll(candidates, attributes.get("value"));
addAll(candidates, attributes.get("name"));
return candidates;
} }
private void addAll(List<String> list, List<Object> itemsToAdd) { private void addAll(List<String> list, List<Object> itemsToAdd) {
...@@ -105,13 +196,34 @@ class OnClassCondition extends SpringBootCondition { ...@@ -105,13 +196,34 @@ class OnClassCondition extends SpringBootCondition {
} }
} }
private List<String> getMatches(Collection<String> candiates, MatchType matchType,
ClassLoader classLoader) {
List<String> matches = new ArrayList<String>(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 { private enum MatchType {
PRESENT { PRESENT {
@Override @Override
public boolean matches(String className, ConditionContext context) { public boolean matches(String className, ClassLoader classLoader) {
return isPresent(className, context.getClassLoader()); return isPresent(className, classLoader);
} }
}, },
...@@ -119,8 +231,8 @@ class OnClassCondition extends SpringBootCondition { ...@@ -119,8 +231,8 @@ class OnClassCondition extends SpringBootCondition {
MISSING { MISSING {
@Override @Override
public boolean matches(String className, ConditionContext context) { public boolean matches(String className, ClassLoader classLoader) {
return !isPresent(className, context.getClassLoader()); return !isPresent(className, classLoader);
} }
}; };
...@@ -146,8 +258,39 @@ class OnClassCondition extends SpringBootCondition { ...@@ -146,8 +258,39 @@ class OnClassCondition extends SpringBootCondition {
return Class.forName(className); 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;
}
}
} }
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -87,7 +87,7 @@ public abstract class SpringBootCondition implements Condition { ...@@ -87,7 +87,7 @@ public abstract class SpringBootCondition implements Condition {
+ methodMetadata.getMethodName(); + methodMetadata.getMethodName();
} }
private void logOutcome(String classOrMethodName, ConditionOutcome outcome) { protected final void logOutcome(String classOrMethodName, ConditionOutcome outcome) {
if (this.logger.isTraceEnabled()) { if (this.logger.isTraceEnabled()) {
this.logger.trace(getLogMessage(classOrMethodName, outcome)); this.logger.trace(getLogMessage(classOrMethodName, outcome));
} }
......
...@@ -11,6 +11,10 @@ org.springframework.boot.autoconfigure.BackgroundPreinitializer ...@@ -11,6 +11,10 @@ org.springframework.boot.autoconfigure.BackgroundPreinitializer
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\ org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition
# Auto Configure # Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
......
/*
* 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.<String>singleton("java.io.InputStream"));
given(metadata.wasProcessed("test.nomatch")).willReturn(true);
given(metadata.getSet("test.nomatch", "ConditionalOnClass"))
.willReturn(Collections.<String>singleton("java.io.DoesNotExist"));
return metadata;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment