Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ private Object getProxyInstance(String groupName, String httpServiceType) {
* @param basePackage the names of packages to look under
* @return match bean definitions
*/
private Stream<BeanDefinition> findHttpServices(String basePackage) {
protected final Stream<BeanDefinition> findHttpServices(String basePackage) {
if (this.scanner == null) {
Assert.state(this.environment != null, "Environment has not been set");
Assert.state(this.resourceLoader != null, "ResourceLoader has not been set");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,26 @@

package org.springframework.web.service.registry;

import java.util.Arrays;
import java.util.function.Consumer;

import org.jspecify.annotations.Nullable;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.function.ThrowingFunction;
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;

/**
* Built-in implementation of {@link AbstractHttpServiceRegistrar} that uses
Expand All @@ -29,31 +47,115 @@
* @author Olga Maciaszek-Sharma
* @since 7.0
*/
class ImportHttpServiceRegistrar extends AbstractHttpServiceRegistrar {
public class ImportHttpServiceRegistrar extends AbstractHttpServiceRegistrar {

private @Nullable MetadataReaderFactory metadataReaderFactory;

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
super.setResourceLoader(resourceLoader);
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
}

@Override
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {

MergedAnnotation<?> groupsAnnot = metadata.getAnnotations().get(ImportHttpServices.Container.class);
if (groupsAnnot.isPresent()) {
for (MergedAnnotation<?> annot : groupsAnnot.getAnnotationArray("value", ImportHttpServices.class)) {
processImportAnnotation(annot, registry);
processImportAnnotation(annot, registry, metadata);
}
}

metadata.getAnnotations().stream(ImportHttpServices.class)
.forEach(annot -> processImportAnnotation(annot, registry));
.forEach(annot -> processImportAnnotation(annot, registry, metadata));
}

private void processImportAnnotation(MergedAnnotation<?> annotation, GroupRegistry groupRegistry) {
private void processImportAnnotation(MergedAnnotation<?> annotation, GroupRegistry groupRegistry,
AnnotationMetadata metadata) {

String groupName = annotation.getString("group");
ImportHttpServices.GroupProvider groupProvider = getGroupProvider(annotation);
HttpServiceGroup.ClientType clientType = annotation.getEnum("clientType", HttpServiceGroup.ClientType.class);
Class<?>[] types = annotation.getClassArray("types");
Class<?>[] basePackageClasses = annotation.getClassArray("basePackageClasses");
String[] basePackages = annotation.getStringArray("basePackages");

if (ObjectUtils.isEmpty(types) && ObjectUtils.isEmpty(basePackages) && ObjectUtils.isEmpty(basePackageClasses)) {
basePackages = new String[] { ClassUtils.getPackageName(metadata.getClassName()) };
}

registerHttpServices(groupRegistry, groupProvider, clientType, types, basePackageClasses, basePackages);
}

private ImportHttpServices.GroupProvider getGroupProvider(MergedAnnotation<?> annotation) {
String group = annotation.getString("group");
Class<?> groupProvider = annotation.getClass("groupProvider");
if (groupProvider == ImportHttpServices.GroupProvider.class) {
return new FixedGroupProvider(StringUtils.hasText(group) ? group : HttpServiceGroup.DEFAULT_GROUP_NAME);
}
Assert.state(!StringUtils.hasText(group), "'group' cannot be mixed with 'groupProvider'");
return (ImportHttpServices.GroupProvider) BeanUtils.instantiateClass(groupProvider);
}

/**
* Register HTTP service to given registry.
* @param groupRegistry the group registry
* @param groupProvider the group provider to use
* @param clientType the client type to use
* @param types the types to register
* @param basePackages the base packages to register
*/
protected final void registerHttpServices(GroupRegistry groupRegistry,
ImportHttpServices.GroupProvider groupProvider, ClientType clientType, Class<?>[] types,
Class<?>[] basePackageClasses, String[] basePackages) {

if (groupProvider instanceof FixedGroupProvider fixedGroupProvider) {
String groupName = fixedGroupProvider.group();
groupRegistry.forGroup(groupName, clientType)
.register(types)
.detectInBasePackages(basePackageClasses)
.detectInBasePackages(basePackages);
}
else {
MetadataReaderFactory metadataReaderFactory = (this.metadataReaderFactory != null) ?
this.metadataReaderFactory : new CachingMetadataReaderFactory();

Consumer<AnnotationMetadata> register = metadata -> {
String group = groupProvider.group(metadata);
if (group != null) {
groupRegistry.forGroup(group, clientType).registerTypeNames(metadata.getClassName());
}
};

Arrays.stream(types)
.map(Class::getName)
.map(ThrowingFunction.of(metadataReaderFactory::getMetadataReader))
.map(MetadataReader::getAnnotationMetadata)
.forEach(register);
Arrays.stream(basePackageClasses)
.map(Class::getPackageName)
.flatMap(this::findHttpServices)
.map(this::getMetadata)
.forEach(register);
Arrays.stream(basePackages)
.flatMap(this::findHttpServices)
.map(this::getMetadata)
.forEach(register);
}
}

private AnnotationMetadata getMetadata(BeanDefinition beanDefinition) {
Assert.state(beanDefinition instanceof AnnotatedBeanDefinition,
"AnnotatedBeanDefinition required when using 'groupProvider'");
return ((AnnotatedBeanDefinition) beanDefinition).getMetadata();
}

private static record FixedGroupProvider(String group) implements ImportHttpServices.GroupProvider {

groupRegistry.forGroup(groupName, clientType)
.register(annotation.getClassArray("types"))
.detectInBasePackages(annotation.getStringArray("basePackages"))
.detectInBasePackages(annotation.getClassArray("basePackageClasses"));
@Override
public String group(AnnotationMetadata metadata) {
return this.group;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.jspecify.annotations.Nullable;

import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.web.service.annotation.HttpExchange;

/**
Expand All @@ -39,12 +42,15 @@
*
* <p>The HTTP Services for each group can be listed via {@link #types()}, or
* detected via {@link #basePackageClasses()} or {@link #basePackages()}.
* If neither types or base packages are defined, detection will occur recursively
* beginning with the package of the class that declares this annotation.
*
* <p>An application can autowire HTTP Service proxy beans, or autowire the
* {@link HttpServiceProxyRegistry} from which to obtain proxies.
*
* @author Olga Maciaszek-Sharma
* @author Rossen Stoyanchev
* @author Phillip Webb
* @since 7.0
* @see Container
* @see AbstractHttpServiceRegistrar
Expand Down Expand Up @@ -72,8 +78,17 @@
* The name of the HTTP Service group.
* <p>If not specified, declared HTTP Services are grouped under the
* {@link HttpServiceGroup#DEFAULT_GROUP_NAME}.
* @see #groupProvider()
*/
String group() default "";

/**
* Strategy used to provide the name of the HTTP Service group.
* <p>May be used as an alternative to {@link #group()} when the group name can
* be determined from the type.
* @see #group()
*/
String group() default HttpServiceGroup.DEFAULT_GROUP_NAME;
Class<? extends GroupProvider> groupProvider() default GroupProvider.class;

/**
* Detect HTTP Services in the packages of the specified classes, looking
Expand Down Expand Up @@ -110,4 +125,19 @@
ImportHttpServices[] value();
}

/**
* Strategy interface to provide the group name for HTTP Service interface.
*/
@FunctionalInterface
interface GroupProvider {

/**
* Provide the group name that should be used for the given HTTP Service interface
* metadata.
* @param metadata the HTTP Service interface metadata
* @return the provided group name or {@code null} if the HTTP Service client
* should not be registered
*/
@Nullable String group(AnnotationMetadata metadata);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
import org.springframework.web.service.registry.echo.EchoA;
import org.springframework.web.service.registry.echo.EchoB;
Expand Down Expand Up @@ -123,6 +126,18 @@ void noRegistrations() {
assertBeanDefinitionCount(0);
}

@Test
void registrarUsingFindHttpService() {
TestRegistrarUsingFindHttpServices registrar = new TestRegistrarUsingFindHttpServices();
registrar.setEnvironment(new StandardEnvironment());
registrar.setResourceLoader(new DefaultResourceLoader());
registrar.registerBeanDefinitions(null, beanDefRegistry);

assertRegistryBeanDef(
new TestGroup("EchoA", EchoA.class),
new TestGroup("EchoB", EchoB.class));
}


@SuppressWarnings("unchecked")
private void doRegister(Consumer<AbstractHttpServiceRegistrar.GroupRegistry>... registrars) {
Expand Down Expand Up @@ -192,6 +207,21 @@ protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata m
}
}

private static class TestRegistrarUsingFindHttpServices extends AbstractHttpServiceRegistrar {

@Override
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata importingClassMetadata) {
findHttpServices(EchoA.class.getPackageName())
.map(definition -> ((AnnotatedBeanDefinition) definition).getMetadata())
.forEach(metadata -> {
String className = metadata.getClassName();
String shortName = ClassUtils.getShortName(className);
registry.forGroup(shortName).registerTypeNames(className);
});
}

}

private record TestGroup(String name, Set<Class<?>> httpServiceTypes, ClientType clientType)
implements HttpServiceGroup {

Expand Down
Loading