Skip to content

Commit 60a8278

Browse files
authored
Merge pull request #49834 from manovotn/issue49812
Arc - detect usage of unsupported @specializes annotation and fail fast
2 parents f702d2b + 81ae265 commit 60a8278

File tree

6 files changed

+184
-16
lines changed

6 files changed

+184
-16
lines changed

extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
import java.util.Set;
1414
import java.util.stream.Collectors;
1515

16+
import jakarta.enterprise.inject.spi.DefinitionException;
17+
1618
import org.jboss.jandex.AnnotationInstance;
19+
import org.jboss.jandex.AnnotationTarget;
1720
import org.jboss.jandex.AnnotationTarget.Kind;
1821
import org.jboss.jandex.ClassInfo;
1922
import org.jboss.jandex.CompositeIndex;
@@ -178,15 +181,41 @@ private IndexView buildApplicationIndex(ArcConfig config, ApplicationArchivesBui
178181
if (isExplicitBeanArchive(archive)
179182
|| isImplicitBeanArchive(index, beanDefiningAnnotations)
180183
|| isAdditionalBeanArchive(archive, beanArchivePredicates)) {
184+
// check for occurrences of incompatible annotations - currently only @Specializes
185+
validateArchiveCompatibility(archive, index, knownCompatibleBeanArchives);
181186
indexes.add(index);
182187
}
183188
}
184189
if (rootIsAlwaysBeanArchive) {
185-
indexes.add(applicationArchivesBuildItem.getRootArchive().getIndex());
190+
ApplicationArchive rootArchive = applicationArchivesBuildItem.getRootArchive();
191+
validateArchiveCompatibility(rootArchive, rootArchive.getIndex(), knownCompatibleBeanArchives);
192+
indexes.add(rootArchive.getIndex());
186193
}
187194
return CompositeIndex.create(indexes);
188195
}
189196

197+
private void validateArchiveCompatibility(ApplicationArchive archive, IndexView index,
198+
KnownCompatibleBeanArchives knownCompatibleBeanArchives) {
199+
// check for occurrences of incompatible annotations - currently only @Specializes
200+
Collection<AnnotationInstance> annotations = index.getAnnotations(DotNames.SPECIALIZES);
201+
if (!annotations.isEmpty() && !knownCompatibleBeanArchives.isKnownCompatible(archive,
202+
KnownCompatibleBeanArchiveBuildItem.Reason.SPECIALIZES_ANNOTATION)) {
203+
Set<String> definitionErrors = new HashSet<>();
204+
for (AnnotationInstance annInstance : annotations) {
205+
DotName targetClassName = annInstance.target().kind().equals(AnnotationTarget.Kind.CLASS)
206+
? annInstance.target().asClass().name()
207+
: annInstance.target().asMethod().declaringClass().name();
208+
definitionErrors.add(targetClassName.toString());
209+
throw new DefinitionException(
210+
"Quarkus does not support CDI Full @Specializes annotation; try using an @Alternative instead. "
211+
+
212+
"If you want to mark one or more archives as Quarkus compatible, take a " +
213+
"look at io.quarkus.arc.deployment.KnownCompatibleBeanArchiveBuildItem.\n" +
214+
"Annotation was found in the following classes: " + definitionErrors);
215+
}
216+
}
217+
}
218+
190219
private boolean isExplicitBeanArchive(ApplicationArchive archive) {
191220
return archive.apply(tree -> tree.contains("META-INF/beans.xml") || tree.contains("WEB-INF/beans.xml"));
192221
}
@@ -229,7 +258,8 @@ private boolean possiblyBeanArchive(ApplicationArchive archive,
229258
if (text.contains("bean-discovery-mode='all'")
230259
|| text.contains("bean-discovery-mode=\"all\"")) {
231260

232-
if (!knownCompatibleBeanArchives.isKnownCompatible(archive)) {
261+
if (!knownCompatibleBeanArchives.isKnownCompatible(archive,
262+
KnownCompatibleBeanArchiveBuildItem.Reason.BEANS_XML_ALL)) {
233263
LOGGER.warnf("Detected bean archive with bean discovery mode of 'all', "
234264
+ "this is not portable in CDI Lite and is treated as 'annotated' in Quarkus! "
235265
+ "Path to beans.xml: %s",
Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,100 @@
11
package io.quarkus.arc.deployment;
22

3+
import java.util.HashSet;
34
import java.util.Objects;
5+
import java.util.Set;
46

57
import io.quarkus.builder.item.MultiBuildItem;
68

79
/**
810
* Marks a bean archive with given coordinates (groupId, artifactId and optionally classifier)
9-
* as known compatible with Quarkus. This is only useful for bean archives whose {@code beans.xml}
10-
* defines a bean discovery mode of {@code all}; bean archives with discovery mode of {@code none}
11-
* or {@code annotated} are always compatible. If a bean archive is known to be compatible with
12-
* Quarkus, no warning about {@code all} discovery is logged during application build.
11+
* as known compatible with Quarkus. If a bean archive is known to be compatible with
12+
* Quarkus, any error logging or exception throwing related to that compatibility is skipped.
13+
* <p>
14+
* This is useful in the following cases:
15+
* <li>Bean archives whose {@code beans.xml} defines a bean discovery mode of {@code all}; bean archives with discovery mode of
16+
* {@code none} or {@code annotated} are always compatible.</li>
17+
* <li>Bean archives that contain the unsupported {@link jakarta.enterprise.inject.Specializes} annotation.</li>
1318
*/
1419
public final class KnownCompatibleBeanArchiveBuildItem extends MultiBuildItem {
20+
final Set<Reason> reasons;
1521
final String groupId;
1622
final String artifactId;
1723
final String classifier;
1824

25+
/**
26+
* Deprecated, use {@link KnownCompatibleBeanArchiveBuildItem#builder(String, String)} method instead.
27+
* For compatibility reasons, this method automatically registers the artifact with {@link Reason#BEANS_XML_ALL}.
28+
*/
29+
@Deprecated
1930
public KnownCompatibleBeanArchiveBuildItem(String groupId, String artifactId) {
2031
this(groupId, artifactId, "");
2132
}
2233

34+
/**
35+
* Deprecated, use {@link KnownCompatibleBeanArchiveBuildItem#builder(String, String)} method instead.
36+
* For compatibility reasons, this method automatically registers the artifact with {@link Reason#BEANS_XML_ALL}.
37+
*/
38+
@Deprecated
2339
public KnownCompatibleBeanArchiveBuildItem(String groupId, String artifactId, String classifier) {
24-
this.groupId = Objects.requireNonNull(groupId, "groupId must be set");
25-
this.artifactId = Objects.requireNonNull(artifactId, "artifactId must be set");
26-
this.classifier = Objects.requireNonNull(classifier, "classifier must be set");
40+
this(groupId, artifactId, classifier, Set.of(Reason.BEANS_XML_ALL));
41+
}
42+
43+
private KnownCompatibleBeanArchiveBuildItem(String groupId, String artifactId, String classifier, Set<Reason> reasons) {
44+
Objects.requireNonNull(groupId, "groupId must be set");
45+
Objects.requireNonNull(artifactId, "artifactId must be set");
46+
Objects.requireNonNull(classifier, "classifier must be set");
47+
if (reasons.isEmpty()) {
48+
throw new IllegalStateException(
49+
"KnownCompatibleBeanArchiveBuildItem.Builder needs to declare at least one compatibility reason. Artifact with following coordinates had no reason associated: "
50+
+ groupId + ":" + artifactId);
51+
}
52+
this.groupId = groupId;
53+
this.artifactId = artifactId;
54+
this.classifier = classifier;
55+
this.reasons = reasons;
56+
}
57+
58+
public static Builder builder(String groupId, String artifactId) {
59+
return new Builder(groupId, artifactId);
60+
}
61+
62+
/**
63+
* An enum listing known reasons for which an archive might be marked as compatible despite using some unsupported
64+
* feature such as {@code beans.xml} discovery mode {@code all} or using {@link jakarta.enterprise.inject.Specializes}
65+
* annotation on its classes.
66+
*/
67+
public enum Reason {
68+
BEANS_XML_ALL,
69+
SPECIALIZES_ANNOTATION;
70+
}
71+
72+
public static class Builder {
73+
74+
private final String groupId;
75+
private final String artifactId;
76+
private final Set<Reason> reasons;
77+
private String classifier;
78+
79+
private Builder(String groupId, String artifactId) {
80+
this.groupId = groupId;
81+
this.artifactId = artifactId;
82+
this.classifier = "";
83+
this.reasons = new HashSet<>();
84+
}
85+
86+
public Builder setClassifier(String classifier) {
87+
this.classifier = classifier;
88+
return this;
89+
}
90+
91+
public Builder addReason(Reason reason) {
92+
this.reasons.add(reason);
93+
return this;
94+
}
95+
96+
public KnownCompatibleBeanArchiveBuildItem build() {
97+
return new KnownCompatibleBeanArchiveBuildItem(groupId, artifactId, classifier, reasons);
98+
}
2799
}
28100
}

extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/KnownCompatibleBeanArchives.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package io.quarkus.arc.deployment;
22

3+
import java.util.HashMap;
34
import java.util.HashSet;
45
import java.util.List;
6+
import java.util.Map;
57
import java.util.Objects;
68
import java.util.Set;
79

810
import io.quarkus.deployment.ApplicationArchive;
911
import io.quarkus.maven.dependency.ArtifactKey;
1012

1113
final class KnownCompatibleBeanArchives {
12-
private static class Key {
14+
static class Key {
1315
final String groupId;
1416
final String artifactId;
1517
final String classifier;
@@ -40,22 +42,28 @@ public int hashCode() {
4042
}
4143
}
4244

43-
private final Set<Key> keys;
45+
private final Map<KnownCompatibleBeanArchiveBuildItem.Reason, Set<Key>> compatArchivesByReason;
4446

4547
KnownCompatibleBeanArchives(List<KnownCompatibleBeanArchiveBuildItem> list) {
46-
Set<Key> keys = new HashSet<>();
48+
Map<KnownCompatibleBeanArchiveBuildItem.Reason, Set<Key>> allCompatArchivesMap = new HashMap<>();
4749
for (KnownCompatibleBeanArchiveBuildItem item : list) {
48-
keys.add(new Key(item.groupId, item.artifactId, item.classifier));
50+
for (KnownCompatibleBeanArchiveBuildItem.Reason reason : item.reasons) {
51+
allCompatArchivesMap.computeIfAbsent(reason, unused -> new HashSet<>())
52+
.add(new Key(item.groupId, item.artifactId, item.classifier));
53+
}
4954
}
50-
this.keys = keys;
55+
this.compatArchivesByReason = allCompatArchivesMap;
5156
}
5257

53-
boolean isKnownCompatible(ApplicationArchive archive) {
58+
boolean isKnownCompatible(ApplicationArchive archive, KnownCompatibleBeanArchiveBuildItem.Reason reason) {
5459
ArtifactKey artifact = archive.getKey();
5560
if (artifact == null) {
5661
return false;
5762
}
5863

59-
return keys.contains(new Key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier()));
64+
Set<Key> archives = compatArchivesByReason.get(reason);
65+
return archives == null ? false
66+
: archives
67+
.contains(new Key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier()));
6068
}
6169
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.quarkus.arc.test.specialization;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
5+
import jakarta.enterprise.context.ApplicationScoped;
6+
import jakarta.enterprise.inject.Produces;
7+
import jakarta.enterprise.inject.Specializes;
8+
9+
import org.junit.jupiter.api.Assertions;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.runtime.util.ExceptionUtil;
14+
import io.quarkus.test.QuarkusUnitTest;
15+
16+
public class SpecializationUnsupportedTest {
17+
18+
@RegisterExtension
19+
static QuarkusUnitTest runner = new QuarkusUnitTest()
20+
.withApplicationRoot((jar) -> jar
21+
.addClasses(SomeBean.class))
22+
.assertException(t -> {
23+
Throwable rootCause = ExceptionUtil.getRootCause(t);
24+
assertTrue(
25+
rootCause.getMessage().contains(
26+
"Quarkus does not support CDI Full @Specializes annotation"),
27+
t.toString());
28+
});
29+
30+
@Test
31+
public void trigger() {
32+
Assertions.fail();
33+
}
34+
35+
@ApplicationScoped
36+
@Specializes
37+
public static class SomeBean {
38+
39+
@Specializes
40+
@Produces
41+
String someProducer() {
42+
return "foo";
43+
}
44+
}
45+
46+
}

extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
3131
import io.quarkus.arc.deployment.BeanContainerBuildItem;
3232
import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
33+
import io.quarkus.arc.deployment.KnownCompatibleBeanArchiveBuildItem;
3334
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
3435
import io.quarkus.arc.processor.BuiltinScope;
3536
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
@@ -881,6 +882,15 @@ void indexPanacheClasses(BuildProducer<AdditionalIndexedClassesBuildItem> additi
881882
}
882883
}
883884

885+
// This build step can be removed after Quarkus updated to any version newer than 2.14.1
886+
// See also https://github.com/smallrye/smallrye-graphql/pull/2299
887+
@BuildStep
888+
void registerKnownSpecializationAnnotation(
889+
BuildProducer<KnownCompatibleBeanArchiveBuildItem> compatArchiveProducer) {
890+
compatArchiveProducer.produce(KnownCompatibleBeanArchiveBuildItem.builder("io.smallrye", "smallrye-graphql-cdi")
891+
.addReason(KnownCompatibleBeanArchiveBuildItem.Reason.SPECIALIZES_ANNOTATION).build());
892+
}
893+
884894
// In dev mode, when you click on the logo, you should go to Dev UI
885895
private String getLogoUrl(LaunchModeBuildItem launchMode, String devUIValue, String defaultValue) {
886896
if (launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT)) {

independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import jakarta.enterprise.inject.Instance;
3232
import jakarta.enterprise.inject.Intercepted;
3333
import jakarta.enterprise.inject.Produces;
34+
import jakarta.enterprise.inject.Specializes;
3435
import jakarta.enterprise.inject.Stereotype;
3536
import jakarta.enterprise.inject.TransientReference;
3637
import jakarta.enterprise.inject.Typed;
@@ -142,6 +143,7 @@ public final class DotNames {
142143
public static final DotName VETOED_PRODUCER = create(VetoedProducer.class);
143144
public static final DotName LIST = create(List.class);
144145
public static final DotName ALL = create(All.class);
146+
public static final DotName SPECIALIZES = create(Specializes.class);
145147
/**
146148
* @see Identified
147149
*/

0 commit comments

Comments
 (0)