java-topology/defects/micronaut/patch/micronaut-0002-mutableannotationmetadata-annotationlist-contains.md

2.8 KiB
Raw Permalink Blame History

UNDF: UNDF-2026-000000462

micronaut-0002: MutableAnnotationMetadata — O(P×L) annotationList.contains in loop

CWE-407 — Algorithmic Complexity: Linear Membership Test in Loop

Field Value
ID micronaut-0002
Severity MEDIUM
Ecosystem micronaut-core
Package micronaut-inject
File inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java
Lines 332, 350, 428, 491
Complexity O(P × L) where P =
Fix Change backing store from ArrayList<String> to LinkedHashSet<String>

Description

getAnnotationsByStereotypeInternal() returns a List<String> backed by new ArrayList<>(). Both addRepeatableStereotype and addDeclaredRepeatableStereotype iterate over parents (a List<String>) and for each call annotationList.contains(parentAnnotation) — O(L) per parent. This pattern appears at lines 332, 350, 428, and 491, so it affects all four stereotype-addition paths.

List<String> annotationList = getAnnotationsByStereotypeInternal(stereotype); // ArrayList
for (String parentAnnotation : parents) {             // O(P)
    if (!annotationList.contains(parentAnnotation)) { // O(L) ArrayList scan
        annotationList.add(parentAnnotation);
    }
}

Since annotationList grows as parents are added, and this runs for every stereotype registration, the total cost per stereotype with P parents is O(P × L) where L can approach P in the worst case → O(P²).

Impact

Annotation metadata is built at startup for every bean, every method, every field that carries annotations. In Micronaut applications with extensive annotation-based configuration (validation, security, caching, AOP), the startup time is directly impacted.

Fix

Change the backing store in getAnnotationsByStereotypeInternal:

// Before
return getAnnotationsByStereotypeInternal().computeIfAbsent(stereotype, s -> new ArrayList<>());

// After
return getAnnotationsByStereotypeInternal().computeIfAbsent(stereotype, s -> new ArrayList<>());
// (keep the List<String> API but use a separate Set for dedup tracking)

Better: change the map value type to Set<String> (LinkedHashSet for order preservation):

// In getAnnotationsByStereotypeInternal():
private Map<String, List<String>> getAnnotationsByStereotypeInternal()
// → change to Map<String, Set<String>> or use LinkedHashSet and adapt callers

Simplest compatible fix: use LinkedHashSet<String> via the existing List interface — change the computeIfAbsent lambda to new LinkedHashSet<>() and cast where needed.

Speedup Estimate

For P=15 parents per stereotype, L≈P: 15² = 225 ops → 15 ops. 15x speedup per stereotype registration. Multiplied across all beans at startup.