Skip to content

Commit

Permalink
Merge pull request #8296 from mkouba/issue-8135
Browse files Browse the repository at this point in the history
ArC - select alternatives through the MicroProfile Config
  • Loading branch information
mkouba authored Apr 1, 2020
2 parents e931b13 + 7fae100 commit 020c4d0
Show file tree
Hide file tree
Showing 23 changed files with 701 additions and 197 deletions.
47 changes: 47 additions & 0 deletions docs/src/main/asciidoc/cdi-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,24 @@ class MyBeanStarter {
<1> The `AmazingService` is created during injection.
<2> The `CoolService` is a normal scoped bean so we have to invoke a method upon the injected proxy to force the instantiation.

* Annotate the bean with `@io.quarkus.runtime.Startup` as described in <<startup_annotation>>:
+
[source,java]
----
@Startup // <1>
@ApplicationScoped
public class EagerAppBean {
private final String name;
EagerAppBean(NameGenerator generator) { // <2>
this.name = generator.createName();
}
}
----
1. For each bean annotated with `@Startup` a synthetic observer of `StartupEvent` is generated. The default priority is used.
2. The bean constructor is called when the application starts and the resulting contextual instance is stored in the application context.

NOTE: Quarkus users are encouraged to always prefer the `@Observes StartupEvent` to `@Initialized(ApplicationScoped.class)` as explained in the link:lifecycle[Application Initialization and Termination] guide.

=== Request Context Lifecycle
Expand Down Expand Up @@ -408,6 +426,35 @@ public class CustomTracerConfiguration {
`@DefaultBean` allows extensions (or any other code for that matter) to provide defaults while backing off if beans of that type are supplied in any
way Quarkus supports.

=== Declaring Selected Alternatives

In CDI, an alternative bean may be selected either globally for an application by means of `@Priority`, or for a bean archive using a `beans.xml` descriptor.
Quarkus has a simplified bean discovery and the content of `beans.xml` is ignored.

The disadvantage of `@Priority` is that it has `@Target({ TYPE, PARAMETER })` and so it cannot be used for producer methods and fields.
To address this problem and to simplify the code Quarkus provides the `io.quarkus.arc.AlternativePriority` annotation.
It's basically a shortcut for `@Alternative` plus `@Priority`.
Additionaly, it can be used for producers.

However, it is also possible to select alternatives for an application using the unified configuration.
The `quarkus.arc.selected-alternatives` property accepts a list of string values that are used to match alternative beans.
If any value matches then the priority of `Integer#MAX_VALUE` is used for the relevant bean.
The priority declared via `@Priority` or `@AlernativePriority` is overriden.

.Value Examples
|===
|Value|Description
|`org.acme.Foo`| Match the fully qualified name of the bean class or the bean class of the bean that declares the producer
|`org.acme.*`| Match beans where the package of the bean class is `org.acme`
|`org.acme.**`| Match beans where the package of the bean class starts with `org.acme`
|`Bar`| Match the simple name of the bean class or the bean class of the bean that declares the producer
|===

.Example application.properties
[source,properties]
----
quarkus.arc.selected-alternatives=org.acme.Foo,org.acme.*,Bar
----

== Build Time Extension Points

Expand Down
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/lifecycle.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ See link:writing-extensions#bootstrap-three-phases[Three Phases of Bootstrap and

NOTE: In CDI applications, an event with qualifier `@Initialized(ApplicationScoped.class)` is fired when the application context is initialized. See https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#application_context[the spec, window="_blank"] for more info.

[[startup_annotation]]
=== Using `@Startup` to initialize a CDI bean at application startup

A bean represented by a class, producer method or field annotated with `@Startup` is initialized at application startup:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import io.quarkus.arc.config.ConfigProperties;
Expand All @@ -19,10 +21,12 @@ public class ArcConfig {

/**
* <ul>
* <li>If set to `all` (or `true`) the container will attempt to remove all unused beans.</li>
* <li>If set to none (or `false`) no beans will ever be removed even if they are unused (according to the criteria set out
* <li>If set to {@code all} (or {@code true}) the container will attempt to remove all unused beans.</li>
* <li>If set to {@code none} (or {@code false}) no beans will ever be removed even if they are unused (according to the
* criteria set out
* below)</li>
* <li>If set to `fwk`, then all unused beans will be removed, except the unused beans whose classes are declared in the
* <li>If set to {@code fwk}, then all unused beans will be removed, except the unused beans whose classes are declared in
* the
* application code</li>
* </ul>
* <p>
Expand Down Expand Up @@ -78,6 +82,24 @@ public class ArcConfig {
@ConfigItem(defaultValue = "kebab-case")
public ConfigProperties.NamingStrategy configPropertiesDefaultNamingStrategy;

/**
* The list of selected alternatives for an application.
* <p>
* An element value can be:
* <ul>
* <li>a fully qualified class name, i.e. {@code org.acme.Foo}</li>
* <li>a simple class name as defined by {@link Class#getSimpleName()}, i.e. {@code Foo}</li>
* <li>a package name with suffix {@code .*}, i.e. {@code org.acme.*}, matches a package</li>
* <li>a package name with suffix {@code .**}, i.e. {@code org.acme.**}, matches a package that starts with the value</li>
* </ul>
* Each element value is used to match an alternative bean class, an alternative stereotype annotation type or a bean class
* that declares an alternative producer. If any value matches then the priority of {@link Integer#MAX_VALUE} is used for
* the relevant bean. The priority declared via {@link javax.annotation.Priority} or
* {@link io.quarkus.arc.AlternativePriority} is overriden.
*/
@ConfigItem
public Optional<List<String>> selectedAlternatives;

public final boolean isRemoveUnusedBeansFieldValid() {
return ALLOWED_REMOVE_UNUSED_BEANS_VALUES.contains(removeUnusedBeans.toLowerCase());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
Expand Down Expand Up @@ -31,6 +32,7 @@
import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanClassAnnotationExclusion;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanClassNameExclusion;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.AlternativePriorities;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BeanConfigurator;
import io.quarkus.arc.processor.BeanDefiningAnnotation;
Expand All @@ -39,9 +41,11 @@
import io.quarkus.arc.processor.BytecodeTransformer;
import io.quarkus.arc.processor.ContextConfigurator;
import io.quarkus.arc.processor.ContextRegistrar;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.ObserverConfigurator;
import io.quarkus.arc.processor.ReflectionRegistration;
import io.quarkus.arc.processor.ResourceOutput;
import io.quarkus.arc.processor.StereotypeInfo;
import io.quarkus.arc.runtime.AdditionalBean;
import io.quarkus.arc.runtime.ArcRecorder;
import io.quarkus.arc.runtime.BeanContainer;
Expand Down Expand Up @@ -248,6 +252,42 @@ public boolean test(BeanInfo bean) {
builder.setGenerateSources(BootstrapDebug.DEBUG_SOURCES_DIR != null);
builder.setAllowMocking(launchModeBuildItem.getLaunchMode() == LaunchMode.TEST);

if (arcConfig.selectedAlternatives.isPresent()) {
final List<Predicate<ClassInfo>> selectedAlternatives = initAlternativePredicates(
arcConfig.selectedAlternatives.get());
builder.setAlternativePriorities(new AlternativePriorities() {

@Override
public Integer compute(AnnotationTarget target, Collection<StereotypeInfo> stereotypes) {
ClassInfo clazz;
switch (target.kind()) {
case CLASS:
clazz = target.asClass();
break;
case FIELD:
clazz = target.asField().declaringClass();
break;
case METHOD:
clazz = target.asMethod().declaringClass();
break;
default:
return null;
}
if (selectedAlternatives.stream().anyMatch(p -> p.test(clazz))) {
return Integer.MAX_VALUE;
}
if (!stereotypes.isEmpty()) {
for (StereotypeInfo stereotype : stereotypes) {
if (selectedAlternatives.stream().anyMatch(p -> p.test(stereotype.getTarget()))) {
return Integer.MAX_VALUE;
}
}
}
return null;
}
});
}

BeanProcessor beanProcessor = builder.build();
ContextRegistrar.RegistrationContext context = beanProcessor.registerCustomContexts();
return new ContextRegistrationPhaseBuildItem(context, beanProcessor);
Expand Down Expand Up @@ -400,6 +440,50 @@ CustomScopeAnnotationsBuildItem exposeCustomScopeNames(List<ContextRegistrarBuil
return new CustomScopeAnnotationsBuildItem(names);
}

private List<Predicate<ClassInfo>> initAlternativePredicates(List<String> selectedAlternatives) {
final String packMatch = ".*";
final String packStarts = ".**";
List<Predicate<ClassInfo>> predicates = new ArrayList<>();
for (String val : selectedAlternatives) {
if (val.endsWith(packMatch)) {
// Package matches
final String pack = val.substring(0, val.length() - packMatch.length());
predicates.add(new Predicate<ClassInfo>() {
@Override
public boolean test(ClassInfo c) {
return DotNames.packageName(c.name()).equals(pack);
}
});
} else if (val.endsWith(packStarts)) {
// Package starts with
final String prefix = val.substring(0, val.length() - packStarts.length());
predicates.add(new Predicate<ClassInfo>() {
@Override
public boolean test(ClassInfo c) {
return DotNames.packageName(c.name()).startsWith(prefix);
}
});
} else if (val.contains(".")) {
// Fully qualified name matches
predicates.add(new Predicate<ClassInfo>() {
@Override
public boolean test(ClassInfo c) {
return c.name().toString().equals(val);
}
});
} else {
// Simple name matches
predicates.add(new Predicate<ClassInfo>() {
@Override
public boolean test(ClassInfo c) {
return DotNames.simpleName(c).equals(val);
}
});
}
}
return predicates;
}

private abstract static class AbstractCompositeApplicationClassesPredicate<T> implements Predicate<T> {

private final IndexView applicationClassesIndex;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.arc.test.alternatives;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class Foo {

public String ping() {
return getClass().getName();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.arc.test.alternatives;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;
import javax.enterprise.inject.Produces;

@ApplicationScoped
class Producers {

@Alternative
@Produces
static final int CHARLIE = 10;

@Produces
@Alternative
public String bravo() {
return "bravo";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.quarkus.arc.test.alternatives;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.test.alternatives.bar.Bar;
import io.quarkus.arc.test.alternatives.bar.MyStereotype;
import io.quarkus.test.QuarkusUnitTest;

public class SelectedAlternativesFqcnTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(SelectedAlternativesFqcnTest.class, Alpha.class, Producers.class, Foo.class, Bar.class,
MyStereotype.class)
.addAsResource(new StringAsset(
"quarkus.arc.selected-alternatives=io.quarkus.arc.test.alternatives.SelectedAlternativesFqcnTest$Alpha,io.quarkus.arc.test.alternatives.Producers,io.quarkus.arc.test.alternatives.bar.MyStereotype"),
"application.properties"));

@Inject
Instance<Alpha> alpha;

@Inject
Instance<String> bravo;

@Inject
Instance<Integer> charlie;

@Inject
Instance<Foo> foo;

@Test
public void testSelectedAlternatives() {
assertTrue(alpha.isResolvable());
assertEquals("ok", alpha.get().ping());
assertTrue(bravo.isResolvable());
assertEquals("bravo", bravo.get());
assertTrue(charlie.isResolvable());
assertEquals(10, charlie.get());
// Note that we should get Bar because it's annotated with an alternative stereotype selected in app properties
assertTrue(foo.isResolvable());
assertEquals(Bar.class.getName(), foo.get().ping());
}

@Alternative
@ApplicationScoped
static class Alpha {

public String ping() {
return "ok";
}

}

}
Loading

0 comments on commit 020c4d0

Please sign in to comment.