-
Notifications
You must be signed in to change notification settings - Fork 39
数据源容器
万事万物皆对象,从某种程度来说,不管是枚举,可调用的方法,甚至是简单的 Map 集合,任何能够返回所需数据源对象的实体我们都可以将其视作作为“盛放”数据的“容器”。
基于这个理念,在 crane4j 中,每一个数据源都是一个容器 Container,而每个容器都对应一个独一无二的 namespace 作为它们的唯一标识。
在 crane4j 中,容器即 Container 接口的实现类,默认提供了四种容器实现,可以覆盖大部分业务场景的四种实现:
-
ConstantContainer:基于内存的常量容器,可以用于支持字典项常量、枚举、常量类等数据源; -
MethodInvokerContainer:基于方法的容器,可以将实例或者静态方法作为数据源; -
LambdaContainer:基于lambda表达式的容器,支持将任何输入 key 值并返回 Map 集合的lambda 表达式适配为数据源,是上述两者的补充; -
EmptyContainer:特殊的空容器,用于占位并表示数据源就是待处理对象本身,即用于内省的场景;
用户也可以自己实现 Container 接口,创建自己的容器。
目前 crane4j 还没有依赖任何第三方数据源的容器实现,不过后续可能在扩展模块中,基于第三方的 RPC 或者 ORM 框架提供一些扩展实现。
Spring 上下文中的任何实现了 Container 接口的 Bean 都会被自动注册到 crane4j 中。
用户可以实现 Container 接口,然后在配置类中直接通过 @Bean 注解的方法、或者在实现类上添加 @Component 注解将该容器直接声明为 bean,随后即可用直接通过该容器的 namespace 引用。
用户也可以通过获取容器中的 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);除上述两者方式外,还可以通过在配置文件中手动配置:
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注解的常量类;
上述类将会根据配置适配为容器并注册。
通过 ConstantContainer 的工厂方法 forMap 方法可以基于 Map 快速构建:
Map<String, ?> map = new HashMap<>();
map.put(key, value);
Container<String> container = ConstantContainer.forMap(map);枚举同样基于 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配置直接整包扫描
除普通枚举外,我们也可以通过 @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字段值,若不配置则默认为枚举项本身;
类似枚举,我们也可以将常量类定义为一个数据源容器:
@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配置直接整包扫描
我们可以通过 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));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 的后处理阶段,该方法会被自动被扫描并注册成 namespace 为 onoToOneMethod 的容器。
该注解也可以加在类上,一般用于将父类或者父接口中的方法声明为容器,比如:
// 父类
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 实现。
默认情况下,若我们在 @Assemble 中未指定容器的 namespace,则解析时默认会指派一个 EmptyContainer 用于占位,不过实际执行时并不会调用容器,而是会进行内省,即以待处理对象本身作为数据源。比如说:
public class Foo {
@Assemble(props = @Mapping(ref = "alias"))
private String name;
private String alias;
}上述操作将会将当前对象的 name 字段映射至 alias 上。
该功能一般用于同步冗余的别名字段。