一键搞定内网穿透 联行号查询|开户行查询 在线工具箱 藏经阁
当前位置:首页 / 互联网与IT技术 / 正文
创建BeanDefinition

现在,我们可以用ResourceResolver扫描Class,用PropertyResolver获取配置,下面,我们开始实现IoC容器。

在IoC容器中,每个Bean都有一个唯一的名字标识。Spring还允许为一个Bean定义多个名字,这里我们简化一下,一个Bean只允许一个名字,因此,很容易想到用一个Map<String, Object>保存所有的Bean:

public class AnnotationConfigApplicationContext {
  Map<String, Object> beans;
}

这么做不是不可以,但是丢失了大量Bean的定义信息,不便于我们创建Bean以及解析依赖关系。合理的方式是先定义BeanDefinition,它能从Annotation中提取到足够的信息,便于后续创建Bean、设置依赖、调用初始化方法等:

public class BeanDefinition {
  // 全局唯一的Bean Name:
  String name;

  // Bean的声明类型:
  Class<?> beanClass;

  // Bean的实例:
  Object instance = null;

  // 构造方法/null:
  Constructor<?> constructor;

  // 工厂方法名称/null:
  String factoryName;

  // 工厂方法/null:
  Method factoryMethod;

  // Bean的顺序:
  int order;

  // 是否标识@Primary:
  boolean primary;

  // init/destroy方法名称:
  String initMethodName;
  String destroyMethodName;

  // init/destroy方法:
  Method initMethod;
  Method destroyMethod;
}

对于自己定义的带@Component注解的Bean,我们需要获取Class类型,获取构造方法来创建Bean,然后收集@PostConstruct@PreDestroy标注的初始化与销毁的方法,以及其他信息,如@Order定义Bean的内部排序顺序,@Primary定义存在多个相同类型时返回哪个“主要”Bean。一个典型的定义如下:

@Component
public class Hello {
  @PostConstruct
  void init() {}

  @PreDestroy
  void destroy() {}
}

对于@Configuration定义的@Bean方法,我们把它看作Bean的工厂方法,我们需要获取方法返回值作为Class类型,方法本身作为创建Bean的factoryMethod,然后收集@Bean定义的initMethoddestroyMethod标识的初始化于销毁的方法名,以及其他@Order@Primary等信息。一个典型的定义如下:

@Configuration
public class AppConfig {
  @Bean(initMethod="init", destroyMethod="close")
  DataSource createDataSource() {
    return new HikariDataSource(...);
  }
}

Bean的声明类型

这里我们要特别注意一点,就是Bean的声明类型。对于@Component定义的Bean,它的声明类型就是其Class本身。然而,对于用@Bean工厂方法创建的Bean,它的声明类型与实际类型不一定是同一类型。上述createDataSource()定义的Bean,声明类型是DataSource,实际类型却是某个子类,例如HikariDataSource,因此要特别注意,我们在BeanDefinition中,存储的beanClass声明类型,实际类型不必存储,因为可以通过instance.getClass()获得:

public class BeanDefinition {
  // Bean的声明类型:
  Class<?> beanClass;

  // Bean的实例:
  Object instance = null;
}

这也引出了下一个问题:如果我们按照名字查找Bean或BeanDefinition,要么拿到唯一实例,要么不存在,即通过查询Map<String, BeanDefinition>即可完成:

public class AnnotationConfigApplicationContext {
  Map<String, BeanDefinition> beans;

  // 根据Name查找BeanDefinition,如果Name不存在,返回null
  @Nullable
  public BeanDefinition findBeanDefinition(String name) {
    return this.beans.get(name);
  }
}

但是通过类型查找Bean或BeanDefinition,我们没法定义一个Map<Class, BeanDefinition>,原因就是Bean的声明类型与实际类型不一定相符,举个例子:

@Configuration
public class AppConfig {
  @Bean
  AtomicInteger counter() {
    return new AtomicInteger();
  }
  
  @Bean
  Number bigInt() {
    return new BigInteger("1000000000");
  }
}

当我们调用getBean(AtomicInteger.class)时,我们会获得counter()方法创建的唯一实例,但是,当我们调用getBean(Number.class)时,counter()方法和bigInt()方法创建的实例均符合要求,此时,如果有且仅有一个标注了@Primary,就返回标注了@Primary的Bean,否则,直接报NoUniqueBeanDefinitionException错误。

因此,对于getBean(Class)方法,必须遍历找出所有符合类型的Bean,如果不唯一,再判断@Primary,才能返回唯一Bean或报错。

我们编写一个找出所有类型的findBeanDefinitions(Class)方法如下:

// 根据Type查找若干个BeanDefinition,返回0个或多个:
List<BeanDefinition> findBeanDefinitions(Class<?> type) {
  return this.beans.values().stream()
    // 按类型过滤:
    .filter(def -> type.isAssignableFrom(def.getBeanClass()))
    // 排序:
    .sorted().collect(Collectors.toList());
  }
}

我们再编写一个findBeanDefinition(Class)方法如下:

// 根据Type查找某个BeanDefinition,如果不存在返回null,如果存在多个返回@Primary标注的一个:
@Nullable
public BeanDefinition findBeanDefinition(Class<?> type) {
  List<BeanDefinition> defs = findBeanDefinitions(type);
  if (defs.isEmpty()) { // 没有找到任何BeanDefinition
    return null;
  }
  if (defs.size() == 1) { // 找到唯一一个
    return defs.get(0);
  }
  // 多于一个时,查找@Primary:
  List<BeanDefinition> primaryDefs = defs.stream().filter(def -> def.isPrimary()).collect(Collectors.toList());
  if (primaryDefs.size() == 1) { // @Primary唯一
    return primaryDefs.get(0);
  }
  if (primaryDefs.isEmpty()) { // 不存在@Primary
    throw new NoUniqueBeanDefinitionException(String.format("Multiple bean with type '%s' found, but no @Primary specified.", type.getName()));
  } else { // @Primary不唯一
    throw new NoUniqueBeanDefinitionException(String.format("Multiple bean with type '%s' found, and multiple @Primary specified.", type.getName()));
  }
}

现在,我们已经定义好了数据结构,下面开始获取所有BeanDefinition信息,实际分两步:

public class AnnotationConfigApplicationContext {
  Map<String, BeanDefinition> beans;

  public AnnotationConfigApplicationContext(Class<?> configClass, PropertyResolver propertyResolver) {
    // 扫描获取所有Bean的Class类型:
    Set<String> beanClassNames = scanForClassNames(configClass);

    // 创建Bean的定义:
    this.beans = createBeanDefinitions(beanClassNames);
  }
  ...
}

第一步是扫描指定包下的所有Class,然后返回Class名字,这一步比较简单:

Set<String> scanForClassNames(Class<?> configClass) {
  // 获取@ComponentScan注解:
  ComponentScan scan = ClassUtils.findAnnotation(configClass, ComponentScan.class);
  // 获取注解配置的package名字,未配置则默认当前类所在包:
  String[] scanPackages = scan == null || scan.value().length == 0 ? new String[] { configClass.getPackage().getName() } : scan.value();

  Set<String> classNameSet = new HashSet<>();
  // 依次扫描所有包:
  for (String pkg : scanPackages) {
    logger.atDebug().log("scan package: {}", pkg);
    // 扫描一个包:
    var rr = new ResourceResolver(pkg);
    List<String> classList = rr.scan(res -> {
      // 遇到以.class结尾的文件,就将其转换为Class全名:
      String name = res.name();
      if (name.endsWith(".class")) {
        return name.substring(0, name.length() - 6).replace("/", ".").replace("\\", ".");
      }
      return null;
    });
    // 扫描结果添加到Set:
    classNameSet.addAll(classList);
  }

  // 继续查找@Import(Xyz.class)导入的Class配置:
  Import importConfig = configClass.getAnnotation(Import.class);
  if (importConfig != null) {
    for (Class<?> importConfigClass : importConfig.value()) {
      String importClassName = importConfigClass.getName();
      classNameSet.add(importClassName);
    }
  }
  return classNameSet;
}

注意到扫描结果是指定包的所有Class名称,以及通过@Import导入的Class名称,下一步才会真正处理各种注解:

Map<String, BeanDefinition> createBeanDefinitions(Set<String> classNameSet) {
  Map<String, BeanDefinition> defs = new HashMap<>();
  for (String className : classNameSet) {
    // 获取Class:
    Class<?> clazz = null;
    try {
      clazz = Class.forName(className);
    } catch (ClassNotFoundException e) {
      throw new BeanCreationException(e);
    }
    // 是否标注@Component?
    Component component = ClassUtils.findAnnotation(clazz, Component.class);
    if (component != null) {
      // 获取Bean的名称:
      String beanName = ClassUtils.getBeanName(clazz);
      var def = new BeanDefinition(
        beanName, clazz, getSuitableConstructor(clazz),
        getOrder(clazz), clazz.isAnnotationPresent(Primary.class),
        // init/destroy方法名称:
        null, null,
        // 查找@PostConstruct方法:
        ClassUtils.findAnnotationMethod(clazz, PostConstruct.class),
        // 查找@PreDestroy方法:
        ClassUtils.findAnnotationMethod(clazz, PreDestroy.class));
      addBeanDefinitions(defs, def);
      // 查找是否有@Configuration:
      Configuration configuration = ClassUtils.findAnnotation(clazz, Configuration.class);
      if (configuration != null) {
        // 查找@Bean方法:
        scanFactoryMethods(beanName, clazz, defs);
      }
    }
  }
  return defs;
}

上述代码需要注意的一点是,查找@Component时,并不是简单地在Class定义查看@Component注解,因为Spring的@Component是可以扩展的,例如,标记为Controller的Class也符合要求:

@Controller
public class MvcController {...}

原因就在于,@Controller注解的定义包含了@Component

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
  String value() default "";
}

所以,判断是否存在@Component,不但要在当前类查找@Component,还要在当前类的所有注解上,查找该注解是否有@Component,因此,我们编写了一个能递归查找注解的方法:

public class ClassUtils {
  public static <A extends Annotation> A findAnnotation(Class<?> target, Class<A> annoClass) {
    A a = target.getAnnotation(annoClass);
    for (Annotation anno : target.getAnnotations()) {
      Class<? extends Annotation> annoType = anno.annotationType();
      if (!annoType.getPackageName().equals("java.lang.annotation")) {
        A found = findAnnotation(annoType, annoClass);
        if (found != null) {
          if (a != null) {
            throw new BeanDefinitionException("Duplicate @" + annoClass.getSimpleName() + " found on class " + target.getSimpleName());
          }
          a = found;
        }
      }
    }
    return a;
  }
}

带有@Configuration注解的Class,视为Bean的工厂,我们需要继续在scanFactoryMethods()中查找@Bean标注的方法:

void scanFactoryMethods(String factoryBeanName, Class<?> clazz, Map<String, BeanDefinition> defs) {
  for (Method method : clazz.getDeclaredMethods()) {
    // 是否带有@Bean标注:
    Bean bean = method.getAnnotation(Bean.class);
    if (bean != null) {
      // Bean的声明类型是方法返回类型:
      Class<?> beanClass = method.getReturnType();
      var def = new BeanDefinition(
        ClassUtils.getBeanName(method), beanClass,
        factoryBeanName,
        // 创建Bean的工厂方法:
        method,
        // @Order
        getOrder(method),
        // 是否存在@Primary标注?
        method.isAnnotationPresent(Primary.class),
        // init方法名称:
        bean.initMethod().isEmpty() ? null : bean.initMethod(),
        // destroy方法名称:
        bean.destroyMethod().isEmpty() ? null : bean.destroyMethod(),
        // @PostConstruct / @PreDestroy方法:
        null, null);
      addBeanDefinitions(defs, def);
    }
  }
}

注意到@Configuration注解本身又用@Component注解修饰了,因此,对于一个@Configuration来说:

@Configuration
public class DateTimeConfig {
  @Bean
  LocalDateTime local() { return LocalDateTime.now(); }

  @Bean
  ZonedDateTime zoned() { return ZonedDateTime.now(); }
}

实际上创建了3个BeanDefinition

  • DateTimeConfig本身;
  • LocalDateTime;
  • ZonedDateTime。

不创建DateTimeConfig行不行?不行,因为后续没有DateTimeConfig的实例,无法调用local()zoned()方法。因为当前我们只创建了BeanDefinition,所以对于LocalDateTimeZonedDateTimeBeanDefinition来说,还必须保存DateTimeConfig的名字,将来才能通过名字查找DateTimeConfig的实例。

有的同学注意到我们同时存储了initMethodNameinitMethod,以及destroyMethodNamedestroyMethod,这是因为在@Component声明的Bean中,我们可以根据@PostConstruct@PreDestroy直接拿到Method本身,而在@Bean声明的Bean中,我们拿不到Method,只能从@Bean注解提取出字符串格式的方法名称,因此,存储在BeanDefinition的方法名称与方法,其中总有一个为null

最后,仔细编写BeanDefinitiontoString()方法,使之能打印出详细的信息。我们编写测试,运行,打印出每个BeanDefinition如下:

define bean: BeanDefinition [name=annotationDestroyBean, beanClass=com.itranswarp.scan.destroy.AnnotationDestroyBean, factory=null, init-method=null, destroy-method=destroy, primary=false, instance=null]

define bean: BeanDefinition [name=nestedBean, beanClass=com.itranswarp.scan.nested.OuterBean$NestedBean, factory=null, init-method=null, destroy-method=null, primary=false, instance=null]

define bean: BeanDefinition [name=createSpecifyInitBean, beanClass=com.itranswarp.scan.init.SpecifyInitBean, factory=SpecifyInitConfiguration.createSpecifyInitBean(String, String), init-method=null, destroy-method=null, primary=false, instance=null]

...

现在,我们已经能扫描并创建所有的BeanDefinition,只是目前每个BeanDefinition内部的instance还是null,因为我们后续才会创建真正的Bean。

可以从GitHub或Gitee下载源码。

GitHub

转载