• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.59
today-context/src/main/java/infra/context/annotation/AnnotationBeanNameGenerator.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.context.annotation;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.lang.annotation.Annotation;
23
import java.util.ArrayList;
24
import java.util.Collections;
25
import java.util.List;
26
import java.util.Objects;
27
import java.util.Set;
28
import java.util.concurrent.ConcurrentHashMap;
29

30
import infra.beans.factory.annotation.AnnotatedBeanDefinition;
31
import infra.beans.factory.config.BeanDefinition;
32
import infra.beans.factory.config.BeanNameHolder;
33
import infra.beans.factory.support.BeanDefinitionRegistry;
34
import infra.beans.factory.support.BeanNameGenerator;
35
import infra.core.annotation.AliasFor;
36
import infra.core.annotation.MergedAnnotation;
37
import infra.core.type.AnnotationMetadata;
38
import infra.lang.Assert;
39
import infra.logging.Logger;
40
import infra.logging.LoggerFactory;
41
import infra.stereotype.Component;
42
import infra.stereotype.Repository;
43
import infra.stereotype.Service;
44
import infra.util.ClassUtils;
45
import infra.util.ObjectUtils;
46
import infra.util.StringUtils;
47

48
/**
49
 * {@link BeanNameGenerator} implementation for bean classes annotated with the
50
 * {@link Component @Component} annotation or
51
 * with another annotation that is itself annotated with {@code @Component} as a
52
 * meta-annotation. For example, Framework's stereotype annotations (such as
53
 * {@link Repository @Repository}) are
54
 * themselves annotated with {@code @Component}.
55
 *
56
 * <p>Also supports Jakarta EE's {@link jakarta.annotation.ManagedBean} and
57
 * JSR-330's {@link jakarta.inject.Named} annotations (as well as their pre-Jakarta
58
 * {@code javax.annotation.ManagedBean} and {@code javax.inject.Named} equivalents),
59
 * if available. Note that Infra component annotations always override such
60
 * standard annotations.
61
 *
62
 * <p>If the annotation's value doesn't indicate a bean name, an appropriate
63
 * name will be built based on the short name of the class (with the first
64
 * letter lower-cased), unless the first two letters are uppercase. For example:
65
 *
66
 * <pre class="code">com.xyz.FooServiceImpl -&gt; fooServiceImpl</pre>
67
 * <pre class="code">com.xyz.URLFooServiceImpl -&gt; URLFooServiceImpl</pre>
68
 *
69
 * @author Juergen Hoeller
70
 * @author Mark Fisher
71
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
72
 * @see Component#value()
73
 * @see Repository#value()
74
 * @see Service#value()
75
 * @see jakarta.inject.Named#value()
76
 * @see FullyQualifiedAnnotationBeanNameGenerator
77
 * @since 4.0
78
 */
79
public class AnnotationBeanNameGenerator implements BeanNameGenerator {
2✔
80

81
  private static final Logger logger = LoggerFactory.getLogger(AnnotationBeanNameGenerator.class);
3✔
82

83
  /**
84
   * A convenient constant for a default {@code AnnotationBeanNameGenerator} instance,
85
   * as used for component scanning purposes.
86
   */
87
  public static final AnnotationBeanNameGenerator INSTANCE = new AnnotationBeanNameGenerator();
4✔
88

89
  private static final String COMPONENT_ANNOTATION_CLASSNAME = Component.class.getName();
3✔
90

91
  private final ConcurrentHashMap<String, Set<String>> metaAnnotationTypesCache = new ConcurrentHashMap<>();
6✔
92

93
  /**
94
   * Set used to track which stereotype annotations have already been checked
95
   * to see if they use a convention-based override for the {@code value}
96
   * attribute in {@code @Component}.
97
   *
98
   * @see #determineBeanNameFromAnnotation(AnnotatedBeanDefinition)
99
   */
100
  private static final Set<String> conventionBasedStereotypeCheckCache = ConcurrentHashMap.newKeySet();
3✔
101

102
  @Override
103
  public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
104
    if (definition instanceof AnnotatedBeanDefinition) {
3!
105
      String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
5✔
106
      if (StringUtils.hasText(beanName)) {
3✔
107
        // Explicit bean name found.
108
        return beanName;
2✔
109
      }
110
    }
111
    // Fallback: generate a unique default bean name.
112
    return buildDefaultBeanName(definition, registry);
5✔
113
  }
114

115
  /**
116
   * Derive a bean name from one of the annotations on the class.
117
   *
118
   * @param annotatedDef the annotation-aware bean definition
119
   * @return the bean name, or {@code null} if none is found
120
   */
121
  @Nullable
122
  protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {
123
    AnnotationMetadata metadata = annotatedDef.getMetadata();
3✔
124

125
    BeanNameHolder holder = getExplicitBeanName(metadata);
4✔
126
    if (holder != null) {
2✔
127
      annotatedDef.setAttribute(BeanNameHolder.AttributeName, holder);
4✔
128
      return holder.getBeanName();
3✔
129
    }
130

131
    String beanName = null;
2✔
132
    for (String annotationType : metadata.getAnnotationTypes()) {
11✔
133
      MergedAnnotation<Annotation> annotation = metadata.getAnnotation(annotationType);
4✔
134
      if (annotation.isPresent()) {
3!
135
        Set<String> metaAnnotationTypes = metaAnnotationTypesCache.computeIfAbsent(annotationType, key -> {
8✔
136
          Set<String> result = metadata.getMetaAnnotationTypes(key);
4✔
137
          return result.isEmpty() ? Collections.emptySet() : result;
7✔
138
        });
139
        if (isStereotype(annotationType, metaAnnotationTypes)) {
5✔
140
          BeanNameHolder beanNameHolder = getBeanNameHolder(annotation);
4✔
141
          if (beanNameHolder != null) {
2✔
142
            String currentName = beanNameHolder.getBeanName();
3✔
143
            if (!currentName.isBlank()) {
3✔
144
              if (conventionBasedStereotypeCheckCache.add(annotationType)
6✔
145
                      && metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) {
5!
146
                logger.warn("""
×
147
                        Support for convention-based stereotype names is deprecated and will \
148
                        be removed in a future version of the framework. Please annotate the \
149
                        'value' attribute in @{} with @AliasFor(annotation=Component.class) \
150
                        to declare an explicit alias for @Component's 'value' attribute.""", annotationType);
151
              }
152

153
              if (beanName != null && !currentName.equals(beanName)) {
6✔
154
                throw new IllegalStateException("Stereotype annotations suggest inconsistent component names: '%s' versus '%s'"
13✔
155
                        .formatted(beanName, currentName));
3✔
156
              }
157
              annotatedDef.setAttribute(BeanNameHolder.AttributeName, beanNameHolder);
4✔
158
              beanName = currentName;
2✔
159
            }
160
          }
161
        }
162
      }
163
    }
1✔
164
    return beanName;
2✔
165
  }
166

167
  @Nullable
168
  private BeanNameHolder getBeanNameHolder(MergedAnnotation<?> annotation) {
169
    Object attribute = annotation.getValue(MergedAnnotation.VALUE);
4✔
170
    if (attribute != null) {
2✔
171
      if (attribute instanceof String beanName) {
6✔
172
        return new BeanNameHolder(beanName, null);
6✔
173
      }
174
      else if (attribute instanceof String[] nameArray && ObjectUtils.isNotEmpty(nameArray)) {
9✔
175
        String beanName = nameArray[0];
4✔
176
        String[] aliasesArray = null;
2✔
177
        if (nameArray.length > 1) {
4✔
178
          ArrayList<String> aliases = new ArrayList<>();
4✔
179
          for (int i = 1; i < nameArray.length; i++) {
8✔
180
            if (StringUtils.hasText(nameArray[i])) {
5✔
181
              aliases.add(nameArray[i]);
6✔
182
            }
183
          }
184
          if (!aliases.isEmpty()) {
3!
185
            aliasesArray = StringUtils.toStringArray(aliases);
3✔
186
          }
187
        }
188

189
        return new BeanNameHolder(beanName, aliasesArray);
6✔
190
      }
191
    }
192

193
    return null;
2✔
194
  }
195

196
  /**
197
   * Get the explicit bean name for the underlying class, as configured via
198
   * {@link Component @Component} and taking into
199
   * account {@link AliasFor @AliasFor}
200
   * semantics for annotation attribute overrides for {@code @Component}'s
201
   * {@code value} attribute.
202
   *
203
   * @param metadata the {@link AnnotationMetadata} for the underlying class
204
   * @return the explicit bean name, or {@code null} if not found
205
   * @see Component#value()
206
   */
207
  @Nullable
208
  private BeanNameHolder getExplicitBeanName(AnnotationMetadata metadata) {
209
    List<BeanNameHolder> names = metadata.getAnnotations().stream(COMPONENT_ANNOTATION_CLASSNAME)
6✔
210
            .map(this::getBeanNameHolder)
2✔
211
            .filter(Objects::nonNull)
2✔
212
            .filter(holder -> StringUtils.hasText(holder.getBeanName()))
5✔
213
            .distinct()
1✔
214
            .toList();
2✔
215

216
    if (names.size() == 1) {
4✔
217
      return names.get(0);
5✔
218
    }
219
    if (names.size() > 1) {
4✔
220
      throw new IllegalStateException(
3✔
221
              "Stereotype annotations suggest inconsistent component names: " + names.stream().map(BeanNameHolder::getBeanName).toList());
8✔
222
    }
223
    return null;
2✔
224
  }
225

226
  /**
227
   * Check whether the given annotation is a stereotype that is allowed
228
   * to suggest a component name through its annotation {@code value()}.
229
   *
230
   * @param annotationType the name of the annotation class to check
231
   * @param metaAnnotationTypes the names of meta-annotations on the given annotation
232
   * @return whether the annotation qualifies as a stereotype with component name
233
   */
234
  protected boolean isStereotype(String annotationType, Set<String> metaAnnotationTypes) {
235
    return metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME)
7✔
236
            || annotationType.equals("jakarta.annotation.ManagedBean")
4✔
237
            || annotationType.equals("jakarta.inject.Named")
4✔
238
            || annotationType.equals("javax.annotation.ManagedBean")
4✔
239
            || annotationType.equals("javax.inject.Named");
5✔
240
  }
241

242
  /**
243
   * Derive a default bean name from the given bean definition.
244
   * <p>The default implementation delegates to {@link #buildDefaultBeanName(BeanDefinition)}.
245
   *
246
   * @param definition the bean definition to build a bean name for
247
   * @param registry the registry that the given bean definition is being registered with
248
   * @return the default bean name (never {@code null})
249
   */
250
  protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
251
    return buildDefaultBeanName(definition);
4✔
252
  }
253

254
  /**
255
   * Derive a default bean name from the given bean definition.
256
   * <p>The default implementation simply builds a decapitalized version
257
   * of the short class name: e.g. "mypackage.MyJdbcDao" &rarr; "myJdbcDao".
258
   * <p>Note that inner classes will thus have names of the form
259
   * "outerClassName.InnerClassName", which because of the period in the
260
   * name may be an issue if you are autowiring by name.
261
   *
262
   * @param definition the bean definition to build a bean name for
263
   * @return the default bean name (never {@code null})
264
   */
265
  protected String buildDefaultBeanName(BeanDefinition definition) {
266
    String beanClassName = definition.getBeanClassName();
3✔
267
    Assert.state(beanClassName != null, "No bean class name set");
6!
268
    String shortClassName = ClassUtils.getShortName(beanClassName);
3✔
269
    return StringUtils.uncapitalizeAsProperty(shortClassName);
3✔
270
  }
271

272
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc