Skip to content

Commit

Permalink
add support for programmatically adding bean configurations (#11527)
Browse files Browse the repository at this point in the history
Allows adding bean configurations at runtime that allow, for example, disabling certain beans within a package. Later it may make sense to allow `@MicronautTest` to use this disable certain beans as necessary.
  • Loading branch information
graemerocher authored Jan 30, 2025
1 parent a9cb2b3 commit be6262e
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
package io.micronaut.inject.configurations

import io.micronaut.context.ApplicationContext
import io.micronaut.context.exceptions.ConfigurationException
import io.micronaut.context.exceptions.NoSuchBeanException
import io.micronaut.inject.BeanConfiguration
import io.micronaut.inject.configurations.requiresbean.RequiresBean
import io.micronaut.inject.configurations.requiresconditionfalse.GitHubActionsBean
import io.micronaut.inject.configurations.requiresconditiontrue.TrueBean
import io.micronaut.inject.configurations.requiresconfig.RequiresConfig
import io.micronaut.inject.configurations.requiresproperty.RequiresProperty
import io.micronaut.inject.configurations.requiressdk.RequiresJava9
import org.atinject.jakartatck.auto.accessories.Cupholder
import spock.lang.IgnoreIf
import spock.lang.Specification
import spock.util.environment.Jvm
Expand Down Expand Up @@ -69,6 +72,40 @@ class RequiresBeanSpec extends Specification {
context.close()
}

void "test that a bean configuration cannot disable internal package"() {
when:
ApplicationContext
.builder()
.beanConfigurations(BeanConfiguration.of(
"io.micronaut.inject.configurations.requiresconditiontrue",
{false}
))
.start()

then:
def e = thrown(ConfigurationException)
e.message == 'Custom bean configurations cannot be added for internal Micronaut packages: io.micronaut.inject.configurations.requiresconditiontrue'
}

void "test runtime bean configuration condition returning #condition"() {
given:
def context = ApplicationContext
.builder()
.beanConfigurations(BeanConfiguration.of(
"org.atinject.jakartatck.auto",
{condition}
))
.start()

expect:
context.containsBean(Cupholder) == present

where:
condition | present
false | false
true | true
}

void "test requires property when not present"() {
when:
ApplicationContext context = ApplicationContext.run()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.inject.BeanConfiguration;
import io.micronaut.inject.BeanDefinitionReference;
import java.util.function.Predicate;

/**
* An abstract implementation of the {@link BeanConfiguration} method. Not typically used directly from user code,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.inject.BeanConfiguration;
import jakarta.inject.Singleton;

import java.lang.annotation.Annotation;
Expand Down Expand Up @@ -145,6 +146,16 @@ public interface ApplicationContextBuilder {
return this;
}

/**
* Register additional bean configurations.
* @param configurations The configurations.
* @return This builder
* @since 4.8.0
*/
default @NonNull ApplicationContextBuilder beanConfigurations(@NonNull BeanConfiguration... configurations) {
return this;
}

/**
* If set to {@code true} (the default is {@code true}) Micronaut will attempt to automatically deduce the environment
* it is running in using environment variables and/or stack trace inspection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ default <T> boolean containsBean(@NonNull Argument<T> beanType) {
);
}

/**
* Registers a bean configuration. This allows disabling a set of beans based on a condition.
*
* @param configuration The configuration
* @return The registry
* @since 4.8.0
*/
@NonNull
@Experimental
default BeanDefinitionRegistry registerBeanConfiguration(BeanConfiguration configuration) {
throw new UnsupportedOperationException("This implementation of BeanDefinitionRegistry doesn't support runtime registration of bean configurations");
}

/**
* Registers a new reference at runtime. Not that registering beans can impact
* the object graph therefore should this should be done as soon as possible prior to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.StringUtils;

import io.micronaut.inject.BeanConfiguration;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -51,6 +52,7 @@
public class DefaultApplicationContextBuilder implements ApplicationContextBuilder, ApplicationContextConfiguration {
private final List<Object> singletons = new ArrayList<>();
private final List<RuntimeBeanDefinition<?>> beanDefinitions = new ArrayList<>();
private final Collection<BeanConfiguration> beanConfigurations = new HashSet<>();
private final List<String> environments = new ArrayList<>();
private final List<String> defaultEnvironments = new ArrayList<>();
private final List<String> packages = new ArrayList<>();
Expand Down Expand Up @@ -187,6 +189,14 @@ public ApplicationContextBuilder beanDefinitions(@NonNull RuntimeBeanDefinition<
return this;
}

@Override
public ApplicationContextBuilder beanConfigurations(@NonNull BeanConfiguration... configurations) {
if (configurations != null) {
beanConfigurations.addAll(Arrays.asList(configurations));
}
return this;
}

@Override
public @NonNull ClassPathResourceLoader getResourceLoader() {
if (classPathResourceLoader == null) {
Expand Down Expand Up @@ -386,6 +396,12 @@ public boolean isEnvironmentPropertySource() {
}
}

if (!beanConfigurations.isEmpty()) {
for (BeanConfiguration beanConfiguration : beanConfigurations) {
applicationContext.registerBeanConfiguration(beanConfiguration);
}
}

if (!configurationIncludes.isEmpty()) {
environment.addConfigurationIncludes(configurationIncludes.toArray(StringUtils.EMPTY_STRING_ARRAY));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,13 @@ public Collection<BeanDefinitionReference<?>> getBeanDefinitionReferences() {
return Collections.emptyList();
}

@Override
public BeanContext registerBeanConfiguration(BeanConfiguration configuration) {
Objects.requireNonNull(configuration, "Configuration cannot be null");
this.beanConfigurations.put(configuration.getName(), configuration);
return this;
}

@Override
@NonNull
public <B> BeanContext registerBeanDefinition(@NonNull RuntimeBeanDefinition<B> definition) {
Expand Down
48 changes: 46 additions & 2 deletions inject/src/main/java/io/micronaut/inject/BeanConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/
package io.micronaut.inject;

import io.micronaut.context.BeanContext;
import io.micronaut.core.annotation.AnnotationMetadataProvider;
import io.micronaut.core.annotation.NonNull;
import java.util.Objects;
import java.util.function.Predicate;

/**
* A BeanConfiguration is a grouping of several {@link BeanDefinition} instances.
Expand Down Expand Up @@ -48,15 +52,22 @@ public interface BeanConfiguration extends AnnotationMetadataProvider, BeanConte
* @param beanDefinitionReference The bean definition class
* @return True if it is
*/
boolean isWithin(BeanDefinitionReference beanDefinitionReference);
default boolean isWithin(BeanDefinitionReference beanDefinitionReference) {
return isWithin(beanDefinitionReference.getBeanType());
}

/**
* Check whether the specified class is within this bean configuration.
*
* @param className The class name
* @return True if it is
*/
boolean isWithin(String className);
default boolean isWithin(String className) {
String packageName = getName();
final int i = className.lastIndexOf('.');
String pkgName = i > -1 ? className.substring(0, i) : className;
return pkgName.equals(packageName) || pkgName.startsWith(packageName + '.');
}

/**
* Check whether the specified class is within this bean configuration.
Expand All @@ -67,4 +78,37 @@ public interface BeanConfiguration extends AnnotationMetadataProvider, BeanConte
default boolean isWithin(Class cls) {
return isWithin(cls.getName());
}

/**
* Programmatically create a bean configuration for the given package.
* @param thePackage The package
* @param condition The condition
* @return The bean configuration
* @since 4.8.0
*/
static @NonNull BeanConfiguration of(@NonNull Package thePackage, @NonNull Predicate<BeanContext> condition) {
return of(thePackage.getName(), condition);
}

/**
* Programmatically create a bean configuration for the given package.
* @param thePackage The package
* @param condition The condition
* @return The bean configuration
* @since 4.8.0
*/
static @NonNull BeanConfiguration of(@NonNull String thePackage, @NonNull Predicate<BeanContext> condition) {
return new ConditionalBeanConfiguration(thePackage, condition);
}

/**
* Programmatically disable beans within a package.
*
* @param thePackage The package name
* @return The bean configuration
* @since 4.8.0
*/
static @NonNull BeanConfiguration disabled(@NonNull String thePackage) {
return new ConditionalBeanConfiguration(thePackage, (beanContext -> false));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2017-2025 original 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 io.micronaut.inject;

import io.micronaut.context.BeanContext;
import io.micronaut.context.BeanResolutionContext;
import io.micronaut.context.exceptions.ConfigurationException;
import java.util.Objects;
import java.util.function.Predicate;

/**
* Runtime implementation of {@link BeanConfiguration}.
* @param packageName The package name
* @param condition The condition
*/
record ConditionalBeanConfiguration(
String packageName, Predicate<BeanContext> condition) implements BeanConfiguration {

ConditionalBeanConfiguration {
Objects.requireNonNull(packageName, "Package cannot be null");
Objects.requireNonNull(condition, "Condition cannot be null");
if (packageName.startsWith("io.micronaut.")) {
throw new ConfigurationException("Custom bean configurations cannot be added for internal Micronaut packages: " + packageName);
}
}

@Override
public Package getPackage() {
throw new UnsupportedOperationException("Package not retrievable");
}

@Override
public String getName() {
return packageName;
}

@Override
public String getVersion() {
return null;
}

@Override
public boolean isEnabled(BeanContext context, BeanResolutionContext resolutionContext) {
return condition.test(context);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ConditionalBeanConfiguration that = (ConditionalBeanConfiguration) o;
return Objects.equals(packageName, that.packageName);
}

@Override
public int hashCode() {
return Objects.hashCode(packageName);
}
}

0 comments on commit be6262e

Please sign in to comment.