Skip to content

Commit

Permalink
Draft interface clients autoconfiguration
Browse files Browse the repository at this point in the history
  • Loading branch information
OlgaMaciaszek authored Aug 22, 2024
1 parent 25954d2 commit ad87582
Show file tree
Hide file tree
Showing 30 changed files with 1,306 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ void documentConfigurationProperties() throws IOException {
snippets.add("application-properties.server", "Server Properties", this::serverPrefixes);
snippets.add("application-properties.security", "Security Properties", this::securityPrefixes);
snippets.add("application-properties.rsocket", "RSocket Properties", this::rsocketPrefixes);
snippets.add("application-properties.interfaceclients", "Interface Clients Properties",
this::interfaceClientsPrefixes);
snippets.add("application-properties.actuator", "Actuator Properties", this::actuatorPrefixes);
snippets.add("application-properties.devtools", "Devtools Properties", this::devtoolsPrefixes);
snippets.add("application-properties.docker-compose", "Docker Compose Properties", this::dockerComposePrefixes);
Expand Down Expand Up @@ -205,6 +207,10 @@ private void rsocketPrefixes(Config prefix) {
prefix.accept("spring.rsocket");
}

private void interfaceClientsPrefixes(Config prefix) {
prefix.accept("spring.interfaceclients");
}

private void actuatorPrefixes(Config prefix) {
prefix.accept("management");
prefix.accept("micrometer");
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ include "spring-boot-project:spring-boot-actuator-autoconfigure"
include "spring-boot-project:spring-boot-docker-compose"
include "spring-boot-project:spring-boot-devtools"
include "spring-boot-project:spring-boot-docs"
include "spring-boot-project:spring-boot-interface-clients"
include "spring-boot-project:spring-boot-test"
include "spring-boot-project:spring-boot-testcontainers"
include "spring-boot-project:spring-boot-test-autoconfigure"
Expand Down
4 changes: 4 additions & 0 deletions spring-boot-project/spring-boot-autoconfigure/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ configurations.all {
dependencies {
api(project(":spring-boot-project:spring-boot"))

// TODO: Have added it to be able to use CaseUtils and avoid rewriting the code;
// can remove it and duplicate the required method instead
implementation("org.apache.commons:commons-text")

dockerTestImplementation(project(":spring-boot-project:spring-boot-test"))
dockerTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support-docker"))
dockerTestImplementation("org.assertj:assertj-core")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2012-2024 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.autoconfigure.interfaceclients;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;

/**
* @author Olga Maciaszek-Sharma
*/
public abstract class AbstractInterfaceClientsFactoryBean implements FactoryBean<Object>, ApplicationContextAware {

protected Class<?> type;

protected String beanName;

protected String clientId;

protected ConfigurableApplicationContext applicationContext;

@Override
public Class<?> getObjectType() {
return this.type;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext,
"ApplicationContext must be an instance of " + ConfigurableApplicationContext.class.getSimpleName());
this.applicationContext = (ConfigurableApplicationContext) applicationContext;

}

public Class<?> getType() {
return this.type;
}

public void setType(Class<?> type) {
this.type = type;
}

public String getBeanName() {
return this.beanName;
}

public void setBeanName(String beanName) {
this.beanName = beanName;
}

public String getClientId() {
return this.clientId;
}

public void setClientId(String clientId) {
this.clientId = clientId;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright 2012-2024 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.autoconfigure.interfaceclients;

import java.lang.annotation.Annotation;
import java.text.Normalizer;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.text.CaseUtils;

import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
* @author Josh Long
* @author Olga Maciaszek-Sharma
*/
// TODO: Handle AOT
public abstract class AbstractInterfaceClientsImportRegistrar
implements ImportBeanDefinitionRegistrar, EnvironmentAware, ResourceLoaderAware {

private static final String INTERFACE_CLIENT_SUFFIX = "InterfaceClient";

private static final String BEAN_NAME_ATTRIBUTE_NAME = "beanName";

private static final String CLIENT_ID_ATTRIBUTE_NAME = "clientId";

private static final String BEAN_CLASS_ATTRIBUTE_NAME = "type";

private Environment environment;

private ResourceLoader resourceLoader;

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
Assert.isInstanceOf(ListableBeanFactory.class, registry,
"Registry must be an instance of " + ListableBeanFactory.class.getSimpleName());
ListableBeanFactory beanFactory = (ListableBeanFactory) registry;
Set<BeanDefinition> candidateComponents = discoverCandidateComponents(beanFactory);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition beanDefinition) {
registerInterfaceClient(registry, beanDefinition);
}
}
}

private void registerInterfaceClient(BeanDefinitionRegistry registry, AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata annotatedBeanMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotatedBeanMetadata.isInterface(),
getAnnotation().getSimpleName() + "can only be placed on an interface.");
MergedAnnotation<? extends Annotation> annotation = annotatedBeanMetadata.getAnnotations().get(getAnnotation());
String beanClassName = annotatedBeanMetadata.getClassName();
// The value of the annotation is the qualifier to look for related beans
// while the default beanName corresponds to the simple class name suffixed with
// `InterfaceClient`
String clientId = annotation.getString(MergedAnnotation.VALUE);
String beanName = !ObjectUtils.isEmpty(annotation.getString(BEAN_NAME_ATTRIBUTE_NAME))
? annotation.getString(BEAN_NAME_ATTRIBUTE_NAME) : buildBeanName(clientId);
BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(getFactoryBeanClass());
definitionBuilder.addPropertyValue(BEAN_NAME_ATTRIBUTE_NAME, beanName);
definitionBuilder.addPropertyValue(CLIENT_ID_ATTRIBUTE_NAME, clientId);
definitionBuilder.addPropertyValue(BEAN_CLASS_ATTRIBUTE_NAME, beanClassName);
definitionBuilder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
AbstractBeanDefinition definition = definitionBuilder.getBeanDefinition();
BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, beanName, new String[] { clientId });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

protected Set<BeanDefinition> discoverCandidateComponents(ListableBeanFactory beanFactory) {
Set<BeanDefinition> candidateComponents = new HashSet<>();
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(getAnnotation()));
List<String> basePackages = AutoConfigurationPackages.get(beanFactory);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
return candidateComponents;
}

private ClassPathScanningCandidateComponentProvider getScanner() {
return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
boolean isCandidate = false;
if (beanDefinition.getMetadata().isIndependent()) {
if (!beanDefinition.getMetadata().isAnnotation()) {
isCandidate = true;
}
}
return isCandidate;
}
};
}

@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}

protected abstract Class<? extends Annotation> getAnnotation();

protected abstract Class<?> getFactoryBeanClass();

private String buildBeanName(String clientId) {
String normalised = Normalizer.normalize(clientId, Normalizer.Form.NFD);
String camelCased = CaseUtils.toCamelCase(normalised, false, '-', '_');
return camelCased + INTERFACE_CLIENT_SUFFIX;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2012-2024 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.autoconfigure.interfaceclients;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

/**
* @author Josh Long
* @author Olga Maciaszek-Sharma
*/
public final class QualifiedBeanProvider {

private static final Log logger = LogFactory.getLog(QualifiedBeanProvider.class);

public static <T> T qualifiedBean(ConfigurableListableBeanFactory beanFactory, Class<T> type, String clientId) {
Map<String, T> matchingClientBeans = getQualifiedBeansOfType(beanFactory, type, clientId);
if (matchingClientBeans.size() > 1) {
throw new NoUniqueBeanDefinitionException(type, matchingClientBeans.keySet());
}
if (matchingClientBeans.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.debug("No qualified bean of type " + type + " found for " + clientId);
}
Map<String, T> matchingDefaultBeans = getQualifiedBeansOfType(beanFactory, type, clientId);
if (matchingDefaultBeans.size() > 1) {
throw new NoUniqueBeanDefinitionException(type, matchingDefaultBeans.keySet());
}
if (matchingDefaultBeans.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.debug("No qualified bean of type " + type + " found for default id");
}
return null;
}
}
return matchingClientBeans.values().iterator().next();
}

private static <T> Map<String, T> getQualifiedBeansOfType(ConfigurableListableBeanFactory beanFactory,
Class<T> type, String clientId) {
Map<String, T> beansOfType = BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type);
Map<String, T> matchingClientBeans = new HashMap<>();
for (String beanName : beansOfType.keySet()) {
Qualifier qualifier = (beanFactory.findAnnotationOnBean(beanName, Qualifier.class));
if (qualifier != null && clientId.equals(qualifier.value())) {
matchingClientBeans.put(beanName, beanFactory.getBean(beanName, type));
}
}
return matchingClientBeans;
}

}
Loading

0 comments on commit ad87582

Please sign in to comment.