Skip to content

数据源容器

huangchengxing edited this page Feb 15, 2023 · 1 revision

概述

万事万物皆对象,从某种程度来说,不管是枚举,可调用的方法,甚至是简单的 Map 集合,任何能够返回所需数据源对象的实体我们都可以将其视作作为“盛放”数据的“容器”

基于这个理念,在 crane4j 中,每一个数据源都是一个容器 Container,而每个容器都对应一个独一无二的 namespace 作为它们的唯一标识。

image-20230210133633050

一、数据源容器

crane4j 中,容器即 Container 接口的实现类,默认提供了四种容器实现,可以覆盖大部分业务场景的四种实现:

  • ConstantContainer:基于内存的常量容器,可以用于支持字典项常量、枚举、常量类等数据源;
  • MethodInvokerContainer:基于方法的容器,可以将实例或者静态方法作为数据源;
  • LambdaContainer:基于 lambda 表达式的容器,支持将任何输入 key 值并返回 Map 集合的lambda 表达式适配为数据源,是上述两者的补充;
  • EmptyContainer:特殊的空容器,用于占位并表示数据源就是待处理对象本身,即用于内省的场景;

用户也可以自己实现 Container 接口,创建自己的容器。

目前 crane4j 还没有依赖任何第三方数据源的容器实现,不过后续可能在扩展模块中,基于第三方的 RPC 或者 ORM 框架提供一些扩展实现。

二、注册容器

1、自动注册

Spring 上下文中的任何实现了 Container 接口的 Bean 都会被自动注册到 crane4j 中。

用户可以实现 Container 接口,然后在配置类中直接通过 @Bean 注解的方法、或者在实现类上添加 @Component 注解将该容器直接声明为 bean,随后即可用直接通过该容器的 namespace 引用。

2、手动注册

用户也可以通过获取容器中的 Crane4jApplicationContext,手动向其注册容器实例:

// 从 spring 容器中获取 Crane4jApplicationContext 实例
Crane4jApplicationContext context = SpringUtils.getBean(Crane4jApplicationContext.class);

// 创建一个常量容器,namespace 为 gender
Map<String, Object> gender = new HashMap<>();
gender.put("0", "女");
gender.put("1", "男");
Container<String> container = ConstantContainer.forMap("gender", gender);

// 注册容器
context.registerContainer(container);

3、通过配置文件配置扫描

除上述两者方式外,还可以通过在配置文件中手动配置:

crane4j:
 # 扫描指定包路径,将该路径下被注解的枚举适配并注册为数据源容器
 container-enum-packages: cn.createsequence.crane4j.springboot.config.*
 # 是否只加载被ContainerEnum注解的枚举
 only-load-annotated-enum: true
 # 扫描指定包路径,将该路径下被注解的常量适配并注册为数据源容器
 container-constant-packages: cn.createsequence.crane4j.springboot.config.*

在项目启动后,会将扫描 cn.createsequence.crane4j.springboot.config 包下:

  • 所有带有 @ContainerEnum 注解的枚举;
  • 所有带有 @ContainerConstant 注解的常量类;

上述类将会根据配置适配为容器并注册。

二、创建容器

1、键值对缓存

通过 ConstantContainer 的工厂方法 forMap 方法可以基于 Map 快速构建:

Map<String, ?> map = new HashMap<>();
map.put(key, value);
Container<String> container = ConstantContainer.forMap(map);

2、普通枚举

枚举同样基于 ConstantContainer 实现:

@Getter
@RequiredArgsConstructor
private enum Num {
    ONE(1, "one"),
    TWO(2, "two");
    private final int code;
    private final String value;
}

// 使用 ConstantContainer.forEnum 方法构建容器
// 容器缓存的数据为: {1 = ONE}, {2 = TWO}
Container<String> container = ConstantContainer.forEnum("num", FooEnum.class, Enum::getCode); 

使用 ConstantContainer.forEnum 工厂方法构建容器:

使用后通过 key 值即可从容器中获得对应的 Enum 实例。

枚举容器可以在配置文件中通过 container-enum-packages 配置直接整包扫描

3、带注解的枚举

除普通枚举外,我们也可以通过 @ContainerEnum 注解来进一步定义容器的具体信息:

@ContainerEnum(namespace = "AnnotatedEnum", key = "key", value = "value")
@Getter
@RequiredArgsConstructor
private enum Num {
    ONE(1, "one"),
    TWO(2, "two");
    private final int key;
    private final String value;
}

// 使用 ConstantContainer.forEnum 方法构建容器
// 容器缓存的数据为: {1 = "one"}, {2 = "two"}
Container<String> container = ConstantContainer.forAnnotatedEnum(DefaultAnnotatedEnum.class, new SimpleAnnotationFinder()); 

在这里,我们通过注解配置了一些额外的信息:

  • namespace:命名空间为 AnnotatedEnum,若不配置则默认为枚举类的 Class.getSimpleName()
  • key:数据源对象的 key 值取枚举项的 key 字段值,若不配置则默认为枚举的 Enum.name()
  • value:数据源对象为枚举项的 value 字段值,若不配置则默认为枚举项本身;

4、常量类

类似枚举,我们也可以将常量类定义为一个数据源容器:

@ContainerConstant
public static class FooConstant {
    public static final String ONE = "one";
    public static final String TWO = "two";
    public static final String THREE = "three";
}

// 使用 ConstantContainer.forConstantClass 方法构建容器
// 容器缓存的数据为: {"ONE" = "one"}, {"TWO" = "two"}, {"THREE" = "three"}
Container<String> container = ConstantContainer.forConstantClass(FooConstant.class, new SimpleAnnotationFinder()); 

同样的,我们也支持通过注解去配置容器的一些更具体的参数:

@ContainerConstant(
    namespace = "foo", // 指定命名空间
    onlyExplicitlyIncluded = true,  // 是否只保存带有 @Include 注解的属性
    onlyPublic = false // 是否只保存公共变量
)
public static class FooConstant2 {
    @ContainerConstant.Include      // onlyExplicitlyIncluded 为 true 时,仅包含带有该注解的属性
    public static final String ONE = "one";
    @ContainerConstant.Exclude      // 默认情况下排除该属性
    public static final String TWO = "two";
    @ContainerConstant.Name("THREE") // 指定key名称为“THREE”
    private static final String SAN = "three";
}

常量容器可以在配置文件中通过 container-constant-packages 配置直接整包扫描

5、lambda 表达式

我们可以通过 LambdaContainer 将输入 Collection 集合并返回 Map 集合的 lambda 表达式定义为容器:

// 定义一个输入key值返回Map集合的方法
private static Map<String, Object> getData(Collection<String> keys) {
    return keys.stream().collect(Collectors.toMap(Function.identity(), Function.identity()));
}
// 将该方法定义为缓存
Container<String> container = LambdaContainer.forLambda(namespace, keys -> this.getData(keys));

6、方法

MethodInvokerContainer 允许将方法适配为容器,有点像 LambdaContainer ,但是一般并不直接使用,而是配合 MethodContainerFactory 一起,批量的扫描类,并将类中带有 @ContainerMethod 注解的方法适配容器并自动注册。

比如,若我们现有一个被 Spring 管理的 Bean,该 bean 中有方法 onoToOneMethod

@ContainerMethod(
    namespace = "onoToOneMethod",
    resultType = Foo.class, resultKey = "id", // 返回的数据源对象类型为 Foo,并且需要按 id 分组
    type = MappingType.ONE_TO_ONE // 返回数据源对象与待处理对象类型为一对一,即待一个处理对象的 key 值对应一个数据源对象的 key
)
public Set<Foo> onoToOneMethod(List<String> args) {
    // do something
}

则在 Bean 的后处理阶段,该方法会被自动被扫描并注册成 namespaceonoToOneMethod 的容器。

该注解也可以加在类上,一般用于将父类或者父接口中的方法声明为容器,比如:

// 父类
public class SuperClass {
    public Set<Foo> onoToOneMethod(List<String> args) {
        // do something
    }
}

// 子类
@ContainerMethod(
    namespace = "onoToOneMethod",
    resultType = Foo.class, resultKey = "id", // 返回的数据源对象类型为 Foo,并且需要按 id 分组
    type = MappingType.ONE_TO_ONE // 返回数据源对象与待处理对象类型为一对一,即待一个处理对象的 key 值对应一个数据源对象的 key
    bind = @Bind(value = "onoToOneMethod", paramTypes = List.class) // 通过@Bind注解绑定指定方法
)
public class Child extend SuperClass {}

此处方法可以是实例方法也可以是静态方法,支持无参方法,并且若为有参方法则第一个参数必须为 Collection 类型。

返回值支持单个对象、对象数组,对象的 Collection 集合,或者已按 key 分组的 Map 集合。

自定义方法容器工厂

该功能基于AnnotationMethodContainerProcessor 后处理器实现,在遍历方法的过程中会调用 Spring 上下文中的 MethodContainerFactory 链,并选取优先级最高的对象进行调用。因此,若有必要,用户可以向 Spring 注册优先级更高的自定义 MethodContainerFactory 实现。

7、内省

默认情况下,若我们在 @Assemble 中未指定容器的 namespace,则解析时默认会指派一个 EmptyContainer 用于占位,不过实际执行时并不会调用容器,而是会进行内省,即以待处理对象本身作为数据源。比如说:

public class Foo {
    @Assemble(props = @Mapping(ref = "alias"))
    private String name;
    private String alias;
}

上述操作将会将当前对象的 name 字段映射至 alias 上。

该功能一般用于同步冗余的别名字段。

Clone this wiki locally