Generate auto-configuration OnBean data
Update the auto-configuration annotation processor to generate properties for `@ConditionalOnBean` and `@ConditionalOnSingleCandidate`. See gh-13328
This commit is contained in:
@@ -18,14 +18,15 @@ package org.springframework.boot.autoconfigureprocessor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
@@ -36,10 +37,8 @@ import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.tools.FileObject;
|
||||
import javax.tools.StandardLocation;
|
||||
|
||||
@@ -52,6 +51,8 @@ import javax.tools.StandardLocation;
|
||||
*/
|
||||
@SupportedAnnotationTypes({ "org.springframework.context.annotation.Configuration",
|
||||
"org.springframework.boot.autoconfigure.condition.ConditionalOnClass",
|
||||
"org.springframework.boot.autoconfigure.condition.ConditionalOnBean",
|
||||
"org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate",
|
||||
"org.springframework.boot.autoconfigure.AutoConfigureBefore",
|
||||
"org.springframework.boot.autoconfigure.AutoConfigureAfter",
|
||||
"org.springframework.boot.autoconfigure.AutoConfigureOrder" })
|
||||
@@ -60,7 +61,9 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
|
||||
protected static final String PROPERTIES_PATH = "META-INF/"
|
||||
+ "spring-autoconfigure-metadata.properties";
|
||||
|
||||
private Map<String, String> annotations;
|
||||
private final Map<String, String> annotations;
|
||||
|
||||
private final Map<String, ValueExtractor> valueExtractors;
|
||||
|
||||
private final Properties properties = new Properties();
|
||||
|
||||
@@ -68,6 +71,9 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
|
||||
Map<String, String> annotations = new LinkedHashMap<>();
|
||||
addAnnotations(annotations);
|
||||
this.annotations = Collections.unmodifiableMap(annotations);
|
||||
Map<String, ValueExtractor> valueExtractors = new LinkedHashMap<>();
|
||||
addValueExtractors(valueExtractors);
|
||||
this.valueExtractors = Collections.unmodifiableMap(valueExtractors);
|
||||
}
|
||||
|
||||
protected void addAnnotations(Map<String, String> annotations) {
|
||||
@@ -75,6 +81,10 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
|
||||
"org.springframework.context.annotation.Configuration");
|
||||
annotations.put("ConditionalOnClass",
|
||||
"org.springframework.boot.autoconfigure.condition.ConditionalOnClass");
|
||||
annotations.put("ConditionalOnBean",
|
||||
"org.springframework.boot.autoconfigure.condition.ConditionalOnBean");
|
||||
annotations.put("ConditionalOnSingleCandidate",
|
||||
"org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate");
|
||||
annotations.put("AutoConfigureBefore",
|
||||
"org.springframework.boot.autoconfigure.AutoConfigureBefore");
|
||||
annotations.put("AutoConfigureAfter",
|
||||
@@ -83,6 +93,17 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
|
||||
"org.springframework.boot.autoconfigure.AutoConfigureOrder");
|
||||
}
|
||||
|
||||
private void addValueExtractors(Map<String, ValueExtractor> attributes) {
|
||||
attributes.put("Configuration", ValueExtractor.allFrom("value"));
|
||||
attributes.put("ConditionalOnClass", ValueExtractor.allFrom("value", "name"));
|
||||
attributes.put("ConditionalOnBean", new OnBeanConditionValueExtractor());
|
||||
attributes.put("ConditionalOnSingleCandidate",
|
||||
new OnBeanConditionValueExtractor());
|
||||
attributes.put("AutoConfigureBefore", ValueExtractor.allFrom("value", "name"));
|
||||
attributes.put("AutoConfigureAfter", ValueExtractor.allFrom("value", "name"));
|
||||
attributes.put("AutoConfigureOrder", ValueExtractor.allFrom("value"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
return SourceVersion.latestSupported();
|
||||
@@ -123,10 +144,10 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
|
||||
private void processElement(Element element, String propertyKey,
|
||||
String annotationName) {
|
||||
try {
|
||||
String qualifiedName = getQualifiedName(element);
|
||||
String qualifiedName = Elements.getQualifiedName(element);
|
||||
AnnotationMirror annotation = getAnnotation(element, annotationName);
|
||||
if (qualifiedName != null && annotation != null) {
|
||||
List<Object> values = getValues(annotation);
|
||||
List<Object> values = getValues(propertyKey, annotation);
|
||||
this.properties.put(qualifiedName + "." + propertyKey,
|
||||
toCommaDelimitedString(values));
|
||||
this.properties.put(qualifiedName, "");
|
||||
@@ -158,58 +179,12 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private List<Object> getValues(AnnotationMirror annotation) {
|
||||
return annotation.getElementValues().entrySet().stream()
|
||||
.filter(this::isNameOrValueAttribute).flatMap(this::getValues)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean isNameOrValueAttribute(Entry<? extends ExecutableElement, ?> entry) {
|
||||
String attributeName = entry.getKey().getSimpleName().toString();
|
||||
return "name".equals(attributeName) || "value".equals(attributeName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Stream<Object> getValues(Entry<?, ? extends AnnotationValue> entry) {
|
||||
Object value = entry.getValue().getValue();
|
||||
if (value instanceof List) {
|
||||
return ((List<AnnotationValue>) value).stream()
|
||||
.map((annotation) -> processValue(annotation.getValue()));
|
||||
private List<Object> getValues(String propertyKey, AnnotationMirror annotation) {
|
||||
ValueExtractor extractor = this.valueExtractors.get(propertyKey);
|
||||
if (extractor == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Stream.of(processValue(value));
|
||||
}
|
||||
|
||||
private Object processValue(Object value) {
|
||||
if (value instanceof DeclaredType) {
|
||||
return getQualifiedName(((DeclaredType) value).asElement());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String getQualifiedName(Element element) {
|
||||
if (element != null) {
|
||||
TypeElement enclosingElement = getEnclosingTypeElement(element.asType());
|
||||
if (enclosingElement != null) {
|
||||
return getQualifiedName(enclosingElement) + "$"
|
||||
+ ((DeclaredType) element.asType()).asElement().getSimpleName()
|
||||
.toString();
|
||||
}
|
||||
if (element instanceof TypeElement) {
|
||||
return ((TypeElement) element).getQualifiedName().toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private TypeElement getEnclosingTypeElement(TypeMirror type) {
|
||||
if (type instanceof DeclaredType) {
|
||||
DeclaredType declaredType = (DeclaredType) type;
|
||||
Element enclosingElement = declaredType.asElement().getEnclosingElement();
|
||||
if (enclosingElement != null && enclosingElement instanceof TypeElement) {
|
||||
return (TypeElement) enclosingElement;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return extractor.getValues(annotation);
|
||||
}
|
||||
|
||||
private void writeProperties() throws IOException {
|
||||
@@ -222,4 +197,71 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ValueExtractor {
|
||||
|
||||
List<Object> getValues(AnnotationMirror annotation);
|
||||
|
||||
static ValueExtractor allFrom(String... attributes) {
|
||||
Set<String> names = new HashSet<>(Arrays.asList(attributes));
|
||||
return new AbstractValueExtractor() {
|
||||
|
||||
@Override
|
||||
public List<Object> getValues(AnnotationMirror annotation) {
|
||||
List<Object> result = new ArrayList<>();
|
||||
annotation.getElementValues().forEach((key, value) -> {
|
||||
if (names.contains(key.getSimpleName().toString())) {
|
||||
extractValues(value).forEach(result::add);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private abstract static class AbstractValueExtractor implements ValueExtractor {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Stream<Object> extractValues(AnnotationValue annotationValue) {
|
||||
if (annotationValue == null) {
|
||||
return Stream.empty();
|
||||
}
|
||||
Object value = annotationValue.getValue();
|
||||
if (value instanceof List) {
|
||||
return ((List<AnnotationValue>) value).stream()
|
||||
.map((annotation) -> extractValue(annotation.getValue()));
|
||||
}
|
||||
return Stream.of(extractValue(value));
|
||||
}
|
||||
|
||||
private Object extractValue(Object value) {
|
||||
if (value instanceof DeclaredType) {
|
||||
return Elements.getQualifiedName(((DeclaredType) value).asElement());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class OnBeanConditionValueExtractor extends AbstractValueExtractor {
|
||||
|
||||
@Override
|
||||
public List<Object> getValues(AnnotationMirror annotation) {
|
||||
Map<String, AnnotationValue> attributes = new LinkedHashMap<>();
|
||||
annotation.getElementValues().forEach((key, value) -> attributes
|
||||
.put(key.getSimpleName().toString(), value));
|
||||
if (attributes.containsKey("name")) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Object> result = new ArrayList<>();
|
||||
extractValues(attributes.get("value")).forEach(result::add);
|
||||
extractValues(attributes.get("type")).forEach(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.autoconfigureprocessor;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with {@link Element} classes.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class Elements {
|
||||
|
||||
private Elements() {
|
||||
}
|
||||
|
||||
static String getQualifiedName(Element element) {
|
||||
if (element != null) {
|
||||
TypeElement enclosingElement = getEnclosingTypeElement(element.asType());
|
||||
if (enclosingElement != null) {
|
||||
return getQualifiedName(enclosingElement) + "$"
|
||||
+ ((DeclaredType) element.asType()).asElement().getSimpleName()
|
||||
.toString();
|
||||
}
|
||||
if (element instanceof TypeElement) {
|
||||
return ((TypeElement) element).getQualifiedName().toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static TypeElement getEnclosingTypeElement(TypeMirror type) {
|
||||
if (type instanceof DeclaredType) {
|
||||
DeclaredType declaredType = (DeclaredType) type;
|
||||
Element enclosingElement = declaredType.asElement().getEnclosingElement();
|
||||
if (enclosingElement != null && enclosingElement instanceof TypeElement) {
|
||||
return (TypeElement) enclosingElement;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
@@ -50,18 +50,38 @@ public class AutoConfigureAnnotationProcessorTests {
|
||||
@Test
|
||||
public void annotatedClass() throws Exception {
|
||||
Properties properties = compile(TestClassConfiguration.class);
|
||||
assertThat(properties).hasSize(3);
|
||||
assertThat(properties).hasSize(5);
|
||||
assertThat(properties).containsEntry(
|
||||
"org.springframework.boot.autoconfigureprocessor."
|
||||
+ "TestClassConfiguration.ConditionalOnClass",
|
||||
"java.io.InputStream,org.springframework.boot.autoconfigureprocessor."
|
||||
+ "TestClassConfiguration$Nested");
|
||||
assertThat(properties).containsKey(
|
||||
"org.springframework.boot.autoconfigureprocessor.TestClassConfiguration");
|
||||
assertThat(properties).containsKey(
|
||||
"org.springframework.boot.autoconfigureprocessor.TestClassConfiguration.Configuration");
|
||||
assertThat(properties).doesNotContainKey(
|
||||
"org.springframework.boot.autoconfigureprocessor.TestClassConfiguration$Nested");
|
||||
assertThat(properties)
|
||||
.containsKey("org.springframework.boot.autoconfigureprocessor."
|
||||
+ "TestClassConfiguration");
|
||||
assertThat(properties)
|
||||
.containsKey("org.springframework.boot.autoconfigureprocessor."
|
||||
+ "TestClassConfiguration.Configuration");
|
||||
assertThat(properties)
|
||||
.doesNotContainKey("org.springframework.boot.autoconfigureprocessor."
|
||||
+ "TestClassConfiguration$Nested");
|
||||
assertThat(properties).containsEntry(
|
||||
"org.springframework.boot.autoconfigureprocessor."
|
||||
+ "TestClassConfiguration.ConditionalOnBean",
|
||||
"java.io.OutputStream");
|
||||
assertThat(properties).containsEntry(
|
||||
"org.springframework.boot.autoconfigureprocessor."
|
||||
+ "TestClassConfiguration.ConditionalOnSingleCandidate",
|
||||
"java.io.OutputStream");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void annoatedClassWithOnBeanThatHasName() throws Exception {
|
||||
Properties properties = compile(TestOnBeanWithNameClassConfiguration.class);
|
||||
assertThat(properties).hasSize(3);
|
||||
assertThat(properties).containsEntry(
|
||||
"org.springframework.boot.autoconfigureprocessor.TestOnBeanWithNameClassConfiguration.ConditionalOnBean",
|
||||
"");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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.
|
||||
@@ -23,6 +23,8 @@ package org.springframework.boot.autoconfigureprocessor;
|
||||
*/
|
||||
@TestConfiguration
|
||||
@TestConditionalOnClass(name = "java.io.InputStream", value = TestClassConfiguration.Nested.class)
|
||||
@TestConditionalOnBean(type = "java.io.OutputStream")
|
||||
@TestConditionalOnSingleCandidate(type = "java.io.OutputStream")
|
||||
public class TestClassConfiguration {
|
||||
|
||||
@TestAutoConfigureOrder
|
||||
|
||||
@@ -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.
|
||||
@@ -32,6 +32,8 @@ import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
@SupportedAnnotationTypes({
|
||||
"org.springframework.boot.autoconfigureprocessor.TestConfiguration",
|
||||
"org.springframework.boot.autoconfigureprocessor.TestConditionalOnClass",
|
||||
"org.springframework.boot.autoconfigure.condition.TestConditionalOnBean",
|
||||
"org.springframework.boot.autoconfigure.condition.TestConditionalOnSingleCandidate",
|
||||
"org.springframework.boot.autoconfigureprocessor.TestAutoConfigureBefore",
|
||||
"org.springframework.boot.autoconfigureprocessor.TestAutoConfigureAfter",
|
||||
"org.springframework.boot.autoconfigureprocessor.TestAutoConfigureOrder" })
|
||||
@@ -48,6 +50,9 @@ public class TestConditionMetadataAnnotationProcessor
|
||||
protected void addAnnotations(Map<String, String> annotations) {
|
||||
put(annotations, "Configuration", TestConfiguration.class);
|
||||
put(annotations, "ConditionalOnClass", TestConditionalOnClass.class);
|
||||
put(annotations, "ConditionalOnBean", TestConditionalOnBean.class);
|
||||
put(annotations, "ConditionalOnSingleCandidate",
|
||||
TestConditionalOnSingleCandidate.class);
|
||||
put(annotations, "AutoConfigureBefore", TestAutoConfigureBefore.class);
|
||||
put(annotations, "AutoConfigureAfter", TestAutoConfigureAfter.class);
|
||||
put(annotations, "AutoConfigureOrder", TestAutoConfigureOrder.class);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.autoconfigureprocessor;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Alternative to Spring Boot's {@code ConditionalOnBean} for testing (removes the need
|
||||
* for a dependency on the real annotation).
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface TestConditionalOnBean {
|
||||
|
||||
Class<?>[] value() default {};
|
||||
|
||||
String[] type() default {};
|
||||
|
||||
Class<? extends Annotation>[] annotation() default {};
|
||||
|
||||
String[] name() default {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.autoconfigureprocessor;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Alternative to Spring Boot's {@code ConditionalOnSingleCandidate} for testing (removes
|
||||
* the need for a dependency on the real annotation).
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface TestConditionalOnSingleCandidate {
|
||||
|
||||
Class<?> value() default Object.class;
|
||||
|
||||
String type() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.autoconfigureprocessor;
|
||||
|
||||
/**
|
||||
* Test configuration with an annotated class.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@TestConfiguration
|
||||
@TestConditionalOnBean(name = "test", type = "java.io.OutputStream")
|
||||
public class TestOnBeanWithNameClassConfiguration {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user