聊聊Spring的bean覆盖(存在同名name/id问题),介绍Spring名称生成策略接口BeanNameGenerator【享学Spring】(上)

简介: 聊聊Spring的bean覆盖(存在同名name/id问题),介绍Spring名称生成策略接口BeanNameGenerator【享学Spring】(上)

前言


众所周知,Spring容器可以简单粗暴的把它理解为一个大大的Map,存储着容器所管理的所有的单实例对象。我们从使用getBean(String beanName)方法,根据bean名称就能获得容器内唯一的Bean实例就能“证明”到这一点。


可你是否曾想过:既然它是Map,那万一我们写的@Bean的beanName重名了怎么办呢?Spring框架是怎么来处理这个事的呢?


Spring容器通俗描述


我们把它理解成一个Map,那Map里面的key-value你应该知道:


  • key:beanName
  • value:单例bean对象


其实为了辅助理解,从SingletonBeanRegistry注册Bean的方法中也可以看出:

public interface SingletonBeanRegistry {
  ...
  // ===注意它没有remove方法====
  void registerSingleton(String beanName, Object singletonObject);
  Object getSingleton(String beanName);
  boolean containsSingleton(String beanName);
  int getSingletonCount();
  ...
}


同样的bean定义信息的注册器BeanDefinitionRegistry也能类比的看到:


public interface BeanDefinitionRegistry extends AliasRegistry {
  ...
  void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException;
  // 请注意:SingletonBeanRegistry 可没有移除方法~
  void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
  BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
  boolean containsBeanDefinition(String beanName);
  int getBeanDefinitionCount();
  ...
}


在我们现在的Spring应用中,动不动容器就需要管理上千个Bean(更有甚者在过去的one in all应用中可能出现上万个Bean的情况)。

既然Spring容器是个Map,那key的重要性不言而喻,他指向着全局唯一的Bean实例,若key被覆盖了,就相当于Map的key被覆盖一样,旧的value值可能将永远就触达不到了~


从而可见,确保beanName的唯一性意义重大。但是呢管理的Bean多了,怎么去确保这件事肯定就成了一个难题,那么接下来就了解一下Spring它是怎么造的~

beanName的生成规则


我把beanName的生成规则放在最开始描述,是因为我觉得既然涉及到beanName,那么首先就应该知道beanName是怎么来的。


我们知道,Spring提供了非常非常多的方式允许我们向容器内注册一个Bean,下面总结出常用的注册Bean方式对应的BeanName如下:


  1. xml的<bean/>标签方式,由id属性决定(若没指定则为全类名)
  2. @Component模式注解方式(包含其所有派生注解如@Service、@Configuration等等)。指定了value值就是value值,否则是类名首字母小写1. 此种方式是最为常用,也是大批量注册Bean的首选方式
  3. @Bean方式。指定了value值就是它,否则就是方法名
  4. FactoryBean方式。(它其实需要结合上面任意一种方式使用,beanName请参考如上描述)


这是一个基本的结论。其实大多数时候我们自己都并不会去指定beanName,若没有自己没有指定的话,它怎么来的呢?Spring对它的生成有什么规律可循呢?那么接下来就就研究下这个策略:名称生成策略


BeanNameGenerator


为bean定义生成bean名称的策略接口。


BeanNameGenerator接口位于 org.springframework.beans.factory.support 包下面,只声明了一个方法,接受两个参数:definition 被生成名字的BeanDefinition实例;registry 生成名字后注册进的BeanDefinitionRegistry。


// @since 2.0.3  是2.0后出来的
public interface BeanNameGenerator {
  // 入参竟然有两个  definition和bean定义注册器
  String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry);
}


看看它的继承树:

image.png


BeanNameGenerator有两个实现版本,DefaultBeanNameGenerator和AnnotationBeanNameGenerator。其中DefaultBeanNameGenerator是给资源文件加载bean时使用(BeanDefinitionReader中使用);AnnotationBeanNameGenerator是为了处理注解生成bean name的情况。


DefaultBeanNameGenerator


它是用来处理xml资源文件的Bean name生成器

// @since 2.0.3
public class DefaultBeanNameGenerator implements BeanNameGenerator {
  @Override
  public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
    // isInnerBean 如果是内部类表示true,这个工具类也能处理
    return BeanDefinitionReaderUtils.generateBeanName(definition, registry);
  }
}


将具体的处理方式委托给了BeanDefinitionReaderUtils.generateBeanName这个方法来处理:


public abstract class BeanDefinitionReaderUtils {
  // unique, "#1", "#2" etc will be appended, until the name becomes 
  public static final String GENERATED_BEAN_NAME_SEPARATOR = BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR;
  // isInnerBean:是为了区分内部bean(innerBean)和顶级bean(top-level bean).
  public static String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry, boolean isInnerBean) throws BeanDefinitionStoreException {
    // 拿到Bean定义信息里面的BeanClassName全类名
    // 注意这个不是必须的,因为如果是继承关系,配上父类的依旧行了
    String generatedBeanName = definition.getBeanClassName();
    if (generatedBeanName == null) {
      // 若没有配置本类全类名,去拿到父类的全类名+$child"俩表示自己
      if (definition.getParentName() != null) {
        generatedBeanName = definition.getParentName() + "$child";
      }
      // 工厂Bean的  就用方法的名字+"$created"
      else if (definition.getFactoryBeanName() != null) {
        generatedBeanName = definition.getFactoryBeanName() + "$created";
      }
    }
    // 若一个都没找到,抛错~
    if (!StringUtils.hasText(generatedBeanName)) {
      throw new BeanDefinitionStoreException("Unnamed bean definition specifies neither " + "'class' nor 'parent' nor 'factory-bean' - can't generate bean name");
    }
    //isInnerBean=true表示你是内部类的话,名字又增加了如下变化
    String id = generatedBeanName;
    if (isInnerBean) {
      // Inner bean: generate identity hashcode suffix.
      id = generatedBeanName + GENERATED_BEAN_NAME_SEPARATOR + ObjectUtils.getIdentityHexString(definition);
    }
    // 如果不是内部类(绝大多数情况下都如此)
    // 此方法注意:一定能够保证到你的BeanName是唯一的~~~~
    else {
      // Top-level bean: use plain class name with unique suffix if necessary.
      // Top-level表示最外层的Bean,也就是说非内部类  这里生成绝对唯一的BeanName~~~~
      return uniqueBeanName(generatedBeanName, registry);
    }
    return id;
  }
  public static String uniqueBeanName(String beanName, BeanDefinitionRegistry registry) {
    String id = beanName;
    int counter = -1;
    // Increase counter until the id is unique.
    while (counter == -1 || registry.containsBeanDefinition(id)) {
      counter++;
      id = beanName + GENERATED_BEAN_NAME_SEPARATOR + counter;
    }
    return id;
  }
}


对于它的处理逻辑,可以总结为如下步骤:


  1. 读取待生成Bean实例的类名称(未必是运行时的实际类型)。
  2. 如果类型为空,则判断是否存在parent bean,如果存在,读取parent bean的name + “$child”。
  3. 如果parent bean 为空,那么判断是否存在factory bean ,如存在,factory bean name + “$created”。 到此处前缀生成完毕
  4. 如果前缀为空,直接抛出异常,没有可以定义这个bean的任何依据。
  5. 前缀存在,判断是否为内部bean(innerBean,此处默认为false),如果是,最终为前缀+分隔符+十六进制的hashcode码
  6. 如果是顶级bean(top-level bean ),则判断前缀+数字的bean是否已存在,循环查询,知道查询到没有使用的id为止。处理完成(所以这个生成器肯定能保证Bean定义的唯一性,不会出现Bean name覆盖问题)


需要注意的是,DefaultBeanNameGenerator在Spring中已经几乎处于一个被弃用了的状态,唯一使用地方为


// @since 11.12.2003
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable {
  private BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator();
  public void setBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) {
    this.beanNameGenerator = (beanNameGenerator != null ? beanNameGenerator : new DefaultBeanNameGenerator());
  } 
}


而看看这个类的实现类们:


image.png


显然我们现在几乎不会再使用XmlBeanDefinitionReader,所以粗暴的可以理解为:此名称生成器已经废弃~


后来想了想其实这句话这么说不妥,毕竟我们使用@ImportResource的时候还是会导入xml文件进来的,因此各位自己感受吧


AnnotationBeanNameGenerator


javadoc的描述:它能够处理@Component以及它所有的派生注解,并且还支持JavaEE的javax.annotation.@ManagedBean、以及JSR 330的javax.inject.@Named注解。如果注解不指定bean名称,则将基于类的短名称(小写的第一个字母)生成适当的名称。


//  @since 2.5 它的出现是伴随着@Component出现
public class AnnotationBeanNameGenerator implements BeanNameGenerator {
  // 支持的最基本的注解(包含其派生注解)
  private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component";
  @Override
  public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
    // //判断是否是否是AnnotatedBeanDefinition的子类, AnnotatedBeanDefinition是BeanDefinition的一个子类
    // 显然这个生成器只为AnnotatedBeanDefinition它来自动生成名称
    if (definition instanceof AnnotatedBeanDefinition) {
      // determineBeanNameFromAnnotation这个方法简而言之,就是看你的注解有没有标注value值,若指定了就以指定的为准
      // 支持的所有注解:上面已经说明了~~~
      // 此处若配置了多个注解且都指定了value值,但发现value值有不同的,就抛出异常了~~~~~
      String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
      if (StringUtils.hasText(beanName)) {
        // Explicit bean name found.
        return beanName;
      }
    }
    // Fallback: generate a unique default bean name.
    // 若没指定,此处叫交给生成器来生成吧~~~
    return buildDefaultBeanName(definition, registry);
  }
  // 它的方法是protected 由此可见若我们想自定义生成器的话  可以继承它  然后复写
  protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
    return buildDefaultBeanName(definition);
  }
  // 这里是先拿到ClassUtils.getShortName 短名称
  protected String buildDefaultBeanName(BeanDefinition definition) {
    String beanClassName = definition.getBeanClassName();
    Assert.state(beanClassName != null, "No bean class name set");
    String shortClassName = ClassUtils.getShortName(beanClassName);
    // 调用java.beans.Introspector的方法  首字母小写
    return Introspector.decapitalize(shortClassName);
  }
}


对于它的处理逻辑,可以总结为如下步骤:


  1. 读取所有注解类型
  2. 遍历所有注解类型,找到所有为Component等所有支持的含有非空value属性的注解
  3. fallback到自己生成beanName


在注解大行其道的今天,AnnotationBeanNameGenerator的使用场景就非常非常之多了,整理如下:


AnnotatedBeanDefinitionReader

public class AnnotatedBeanDefinitionReader {
  ...
  private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
  ...
}

bean扫描器ClassPathBeanDefinitionScanner中会使用到AnnotatedBeanDefinitionReader去读取Bean定义信息们~

相关文章
|
22天前
|
消息中间件 Java 调度
Spring Boot 3.3 后台任务处理的高效策略
【10月更文挑战第18天】 在现代应用程序中,后台任务处理对于提升用户体验和系统性能至关重要。Spring Boot 3.3提供了多种机制来实现后台任务处理,包括异步方法、任务调度和使用消息系统。本文将探讨这些机制的最佳实践,帮助开发者提高应用程序的效率和响应速度。
29 0
|
8天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
1月前
|
XML Java 数据格式
Spring从入门到入土(bean的一些子标签及注解的使用)
本文详细介绍了Spring框架中Bean的创建和使用,包括使用XML配置文件中的标签和注解来创建和管理Bean,以及如何通过构造器、Setter方法和属性注入来配置Bean。
66 9
Spring从入门到入土(bean的一些子标签及注解的使用)
|
22天前
|
存储 安全 Java
|
27天前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
22天前
|
消息中间件 监控 Java
Spring Boot 3.3 后台任务处理:最佳实践与高效策略
【10月更文挑战第10天】 在现代应用程序中,后台任务处理对于提高应用程序的响应性和吞吐量至关重要。Spring Boot 3.3提供了多种机制来实现高效的后台任务处理,包括异步方法、任务调度和使用消息队列等。本文将探讨这些机制的最佳实践和高效策略。
62 0
|
1月前
|
安全 算法 Java
强大!基于Spring Boot 3.3 六种策略识别上传文件类型
【10月更文挑战第1天】在Web开发中,文件上传是一个常见的功能需求。然而,如何确保上传的文件类型符合预期,防止恶意文件入侵,是开发者必须面对的挑战。本文将围绕“基于Spring Boot 3.3 六种策略识别上传文件类型”这一主题,分享一些工作学习中的技术干货,帮助大家提升文件上传的安全性和效率。
45 0
|
1月前
|
自然语言处理 JavaScript Java
Spring 实现 3 种异步流式接口,干掉接口超时烦恼
本文介绍了处理耗时接口的几种异步流式技术,包括 `ResponseBodyEmitter`、`SseEmitter` 和 `StreamingResponseBody`。这些工具可在执行耗时操作时不断向客户端响应处理结果,提升用户体验和系统性能。`ResponseBodyEmitter` 适用于动态生成内容场景,如文件上传进度;`SseEmitter` 用于实时消息推送,如状态更新;`StreamingResponseBody` 则适合大数据量传输,避免内存溢出。文中提供了具体示例和 GitHub 地址,帮助读者更好地理解和应用这些技术。
171 0
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
161 2