Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability of specifying base package for configuration beans #63

Merged
merged 7 commits into from
May 22, 2022
Merged
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 @@ -2,6 +2,7 @@

import com.asyncapi.v2.model.channel.ChannelItem;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner;
import java.util.HashMap;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -20,18 +21,17 @@
public class DefaultChannelsService implements ChannelsService {

private final Set<? extends ChannelsScanner> channelsScanners;
private Map<String, ChannelItem> channels;
private final Map<String, ChannelItem> channels = new HashMap<>();

@PostConstruct
void findChannels() {
try {
channels = channelsScanners.stream()
.map(ChannelsScanner::scan)
.map(Map::entrySet).flatMap(Collection::stream)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
} catch (Exception e) {
log.error("An error was encountered during channel scanning: {}", e.getMessage());
channels = Collections.emptyMap();

for (ChannelsScanner scanner : channelsScanners) {
try {
channels.putAll(scanner.scan());
} catch (Exception e) {
log.error("An error was encountered during channel scanning with {}: {}", scanner, e.getMessage());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@
@Slf4j
public abstract class AbstractChannelScanner<T extends Annotation> implements ChannelsScanner {

@Autowired
private AsyncApiDocket docket;

@Autowired
private ComponentsScanner componentsScanner;

Expand All @@ -33,7 +30,7 @@ public abstract class AbstractChannelScanner<T extends Annotation> implements Ch

@Override
public Map<String, ChannelItem> scan() {
return componentsScanner.scanForComponents(docket.getBasePackage()).stream()
return componentsScanner.scanForComponents().stream()
.map(this::getAnnotatedMethods).flatMap(Collection::stream)
.map(this::mapMethodToChannel)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import io.github.stavshamir.springwolf.asyncapi.scanners.components.ComponentsScanner;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -22,9 +21,6 @@
@Slf4j
public abstract class AbstractMethodLevelListenerScanner implements ChannelsScanner {

@Autowired
private AsyncApiDocket docket;

@Autowired
private ComponentsScanner componentsScanner;

Expand Down Expand Up @@ -53,9 +49,8 @@ public abstract class AbstractMethodLevelListenerScanner implements ChannelsScan
@Override
public Map<String, ChannelItem> scan() {
annotationClass = loadAnnotationClass();
String basePackage = docket.getBasePackage();

return componentsScanner.scanForComponents(basePackage).stream()
return componentsScanner.scanForComponents().stream()
.map(this::getAnnotatedMethods).flatMap(Collection::stream)
.map(this::mapMethodToChannel)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.components;

import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.BeanDefinition;

@Slf4j
public abstract class AbstractClassPathComponentsScanner implements ComponentsScanner {

protected Stream<Class<?>> filterBeanDefinitionsToClasses(Set<BeanDefinition> beanDefinitions) {
return beanDefinitions.stream()
.map(BeanDefinition::getBeanClassName)
.map(this::getClass)
.filter(Optional::isPresent)
.filter(it -> this.isSuitableComponent(it.get()))
.map(Optional::get);
}

protected boolean isSuitableComponent(Class<?> clazz) {

if (FactoryBean.class.isAssignableFrom(clazz)) {

log.debug("Skipping FactoryBean '{}'. If needed, instead use ApplicationContextComponentsScanner", clazz);
return false;
}

if (clazz.isInterface()) {

// Skipping interface. This is possible if a @Configuration gives back a @Bean with interface return type.
return false;
}

return true;
}

private Optional<Class<?>> getClass(String className) {
try {
log.debug("Found candidate class: {}", className);
return Optional.of(Class.forName(className));
} catch (ClassNotFoundException e) {
log.warn("Class {} not found", className);
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.components;

import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
import org.springframework.stereotype.Component;

/**
* Scans the started {@link ApplicationContext} for possible components.
* <p>
* The preferred method for accurate probing of runtime components created by:
* <ul>
* <li>@{@link Component}-annotated classes</li>
* <li>@{@link Bean}-annotated methods inside @{@link Configuration}-annotated classes</li>
* <li>@{@link Bean}-annotated methods that give back a {@link org.springframework.beans.factory.FactoryBean}</li>
* <li>Any external library that enhances your runtime with custom instances</li>
* </ul>
*/
public class ApplicationContextComponentsScanner implements ComponentsScanner, ApplicationContextAware, InitializingBean {

private final Predicate<Class<?>> predicate;
private ListableBeanFactory listableBeanFactory;

public ApplicationContextComponentsScanner(Predicate<Class<?>> predicate) {
this.predicate = predicate;
}

public ApplicationContextComponentsScanner(String basePackage) {
this(clazz -> {
String clazzName = clazz.getName();
return clazzName.equals(basePackage) || clazzName.startsWith(basePackage + ".");
});
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.listableBeanFactory = applicationContext;
}

@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.listableBeanFactory, "The application context must be set");
}

@Override
public Set<Class<?>> scanForComponents() {

Set<Class<?>> components = new LinkedHashSet<>();
for (String beanName : this.listableBeanFactory.getBeanDefinitionNames()) {
Class<?> beanType = this.listableBeanFactory.getType(beanName);
if (this.predicate == null || this.predicate.test(beanType)) {
components.add(beanType);
}
}

return components;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.components;

import static java.util.stream.Collectors.toSet;

import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Component;

@Slf4j
public class ComponentComponentsScanner extends AbstractClassPathComponentsScanner {

private final String basePackage;

public ComponentComponentsScanner(String basePackage) {
if (StringUtils.isEmpty(basePackage)) {
throw new IllegalArgumentException("There must be a non-empty basePackage given");
}

this.basePackage = basePackage;
}

@Override
public Set<Class<?>> scanForComponents() {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(Component.class));

log.debug("Scanning for component classes in {}", basePackage);
return this.filterBeanDefinitionsToClasses(provider.findCandidateComponents(basePackage))
.collect(toSet());
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.components;

import java.util.Set;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;

public interface ComponentsScanner {

/**
* Scan a package and its descendants for classes annotated with @Component, its specializations or meta-annotations.
* @param basePackage The root package to scan.
* Scan your project for components potentially containing asynchronous listeners,
* based on the configuration of your registered {@link AsyncApiDocket}.
*
* @return A set of found classes.
*/
Set<Class<?>> scanForComponents(Package basePackage);

/**
* Scan a package and its descendants for classes annotated with @Component, its specializations or meta-annotations.
* @param basePackage The root package to scan.
* @return A set of found classes.
*/
Set<Class<?>> scanForComponents(String basePackage);

Set<Class<?>> scanForComponents();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.components;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public class CompositeComponentsScanner implements ComponentsScanner {

private final ComponentsScanner[] componentsScanners;

public CompositeComponentsScanner(ComponentsScanner... componentsScanners) {

List<ComponentsScanner> scannerList = new ArrayList<>();
for (ComponentsScanner scanner : componentsScanners) {
if (scanner != null) {
scannerList.add(scanner);
}
}

if (scannerList.isEmpty()) {
throw new IllegalArgumentException("There must be at least one valid/non-null beans scanner given to the composite scanner");
}

this.componentsScanners = scannerList.toArray(new ComponentsScanner[0]);
}

@Override
public Set<Class<?>> scanForComponents() {

final Set<Class<?>> components = new LinkedHashSet<>();
for (ComponentsScanner componentsScanner : this.componentsScanners) {
components.addAll(componentsScanner.scanForComponents());
}

return components;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.components;

import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.filter.AnnotationTypeFilter;

@Slf4j
public class ConfigurationComponentsScanner extends AbstractClassPathComponentsScanner {

private final String basePackage;
private final Predicate<Class<?>> classPredicate;

public ConfigurationComponentsScanner(String basePackage) {
this(basePackage, it -> true);
}

public ConfigurationComponentsScanner(String basePackage, Predicate<Class<?>> classPredicate) {
if (StringUtils.isEmpty(basePackage)) {
throw new IllegalArgumentException("There must be a non-empty basePackage given");
}

this.basePackage = basePackage;
this.classPredicate = classPredicate;
}

@Override
public Set<Class<?>> scanForComponents() {
final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(Configuration.class));

log.debug("Scanning for component classes in configuration package {}", basePackage);
return this.filterBeanDefinitionsToClasses(provider.findCandidateComponents(basePackage))
.map(configurationClass -> {

Set<Class<?>> beans = new LinkedHashSet<>();
for (final Method method : configurationClass.getDeclaredMethods()) {
final Bean bean = AnnotationUtils.getAnnotation(method, Bean.class);
if (bean != null) {
final Class<?> returnType = method.getReturnType();
if (this.classPredicate.test(returnType) && this.isSuitableComponent(returnType)) {
beans.add(returnType);
}
}
}

return beans;
})
.flatMap(Set::stream)
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.components;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

@Slf4j
public class DefaultClassPathComponentsScanner extends CompositeComponentsScanner {

public DefaultClassPathComponentsScanner(String basePackage, String configurationBasePackage) {
super(
StringUtils.isNotBlank(basePackage) ? new ComponentComponentsScanner(basePackage) : null,
StringUtils.isNotBlank(configurationBasePackage) ? new ConfigurationComponentsScanner(configurationBasePackage) : null
);
}
}
Loading