Skip to content

方法容器

方法容器指以实例方法或静态方法作为数据源的容器,它是我们在日常中最经常使用的容器之一。

和其他的容器不同,方法容器通常不直接创建使用,而是通过在目标方法上添加注解的方式,将该方法 “声明” 为一个方法容器。你可以直接将带有容器方法的类交由 crane4j 进行解析,而在 Spring 环境中,crane4j 将会通过后处理器自动解析并注册。

crane4j 在设计上参考了 Spring 处理监听器注解 @EventListener 的责任链机制,它基于注解处理器 MethodContainerAnnotationProcessor 和方法容器工厂链 MethodContainerFactory 实现了扫描和适配的功能,你可以通过添加自己的 MethodContainerFactory 实现从而扩展这部分功能。

1.声明容器

你可以直接在类或方法上添加 @ContainerMethod 注解,在 Spring 环境中,当项目启动后,会在后处理阶段扫描该方法,并将其注册为一个方法容器。

声明在方法上

java
@ContainerMethod(
    namespace = "onoToOneMethod",
    resultType = Foo.class, resultKey = "id" // 返回的数据源对象类型为 Foo,并且需要按 id 分组
)
public Set<Foo> onoToOneMethod(List<String> args) {
    // do something
}

声明在类上

当你在类上声明时,你需要使用 bindMethodbindMethodParamTypes 属性显式的进行方法绑定:

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

// 子类
@ContainerMethod(
    namespace = "onoToOneMethod",
    resultType = Foo.class, resultKey = "id", // 返回的数据源对象类型为 Foo,并且需要按 id 分组
    bindMethod = "onoToOneMethod", // 指定要绑定的方法名称
    bindMethodParamTypes = List.class // 指定要绑定方法的参数类型
)
public class ChildClass extends SuperClass {}

此时,你可以在子类中绑定父类方法作为方法容器。

可以作为方法容器的方法需要满足下述条件

  • 声明类:不限制,你可以将注解声明在接口或抽象类上,如果声明在类父类或者父接口上,那么子类/实现类同样会获得此方法;
  • 方法类型:不限制,方法可以是实例方法(包括接口或抽象类中的抽象方法)或静态方法;
  • 返回值类型:方法必须有返回值,且返回值类型必须为 Collection 集合或 Map 集合(取决于 @ContainerMethod#type 属性);
  • 参数类型:可以是无参方法,若是有参方法,则首个参数必须为 Collection 类型(这类方法在调用时其他参数都是 null);

常见的各种 xxxByIds 都是非常典型的方法。

TIP

crane4j 将根据现有的条件自动查找最匹配的方法,因此 bindMethodParamTypes 并不总是必须的。

当没有多个重载方法时,你可以只填写方法名,而当有多个重载方法时,你只需要填写足以区分出两个方法的前部分参数即可(比如 a, b, c 与 a, c, d,只需要 a, c 即可确认 a, c, d)。

不过出于代码的可维护性考虑,还是推荐把参数类型写全。

WARNING

注意,如果你声明的方法容器数量较多,那么最好在一个统一的常量类中管理方法容器的 namespace,否则各种容器的 namespace 散落在代码里,会对后续的维护带来麻烦。

2.可选配置项

@ContainerMethod 注解中,提供了一些可选的配置项:

API作用类型默认值
namespace定义枚举容器的命名空间任意字符串Method#getName
type映射类型,表示如何对结果集按 key 分组MappingType 枚举MappingType.ONE_TO_ONE,即结果总是与 key 一对一分组
duplicateStrategy当 key 出现重复值时的处理策略DuplicateStrategy 枚举DuplicateStrategy.ALERT,出现重复值时直接抛异常
resultKey分组的 key 值方法返回的对象列表的 key"id"
resultType返回值类型返回值参数类型(如果是集合,则为其中的元素类型)无,必填
bindMethod绑定方法的名称方法名当注解声明在类上时必填,声明在方法上时不填
bindMethodParamTypes绑定方法的参数类型方法参数类型无,不填时默认获取首个符合条件的同名方法
filterNullKey是否过滤入参集合中的 null 值booleantrue
skipQueryIfKeyCollIsEmpty入参集合为空时,是否跳过查询直接返回空集合booleantrue

3.对结果分组

这里需要强调一下 @ContainerMethod#type 属性,它用于指定如何对结果集按 key 分组,它通常与 resultKeyresultType 结合使用。

类型说明分组结果场景
MappingType.ONE_TO_ONE按 key 值一对一分组Map<key, value>默认
MappingType.ONE_TO_MANY按 key 值一对多分组Map<key, List<value>>一个 key 对应多个值
比如一个 classId 对应多个 Student
MappingType.NO_MAPPING返回值已经是分组后的 Map 集合,无需分组原始的方法返回值当返回值已经是 Map
MappingType.ORDER_OF_KEYS将输入的 key 值与结果按顺序合并Map<key, value>方法的返回值是 String 或基础数据类型(及其包装类)的时候

下面是它们的一些使用场景,你可以参照着理解一下:

java

// ========== MappingType.ONE_TO_ONE ==========

@ContainerMethod(
    namespace = "userName", type = MappingType.ONE_TO_ONE,
    resultType = User.class, resultKey = "deptId"
)
public List<User> listUserByIds(List<Integer> ids);  // 查询用户,并按用户 ID 一对一分组

// ========== MappingType.ONE_TO_MANY ==========

@ContainerMethod(
    namespace = "userName", type = MappingType.ONE_TO_MANY,
    resultType = User.class, resultKey = "deptId"
)
public List<User> listUserByDeptId(List<Integer> deptIds); // 查询用户,并按用户的所属部门 ID 一对多分组

// ========== MappingType.NO_MAPPING ==========

@ContainerMethod(namespace = "userName", type = MappingType.NO_MAPPING)
public Map<Integer, User> listUserMapByIds(List<Integer> ids); // 查询结果集已经分好组了

@ContainerMethod(namespace = "userName", type = MappingType.NO_MAPPING)
public Map<Integer, List<User>> listUserByDeptIds(List<Integer> deptIds);

// ========== MappingType.ORDER_OF_KEYS ==========

@ContainerMethod(namespace = "userName", type = MappingType.ORDER_OF_KEYS)
public String getUserNameById(Integer id);  // 查询结果集是 String 类型,无法获取 key 值,因此直接按顺序合并即可

@ContainerMethod(namespace = "userName", type = MappingType.ORDER_OF_KEYS)
public List<Integer> listUserAgeNameByIds(List<Integer> ids);

4.接受参数对象

有时候,我们要声明为数据源容器的方法会将对象作为查询参数,在 2.7.0 及以上版本,你可以通过下述方式来配置如何生成参数对象:

java
@Assemble(
    container = "dict", props = @Mapping(src = "name", ref = "dictName"),
    keyType = DictItemQueryDTO.class, // 指定参数对象类型,该类必须有一个公开的无参构造方法
    keyDesc = "dictId:id, dictType:type", // 指定如何将属性值映射到参数对象
)
@Data
public class Foo {
    private Integer dictId;
  	private String dictType;
    private String dictName;
}

// 对应的参数对象
@Data
public class CustomerQueryDTO {
  private String id;
  private String type;
}

// 对应的接受参数对象的查询方法
@ContainerMethod(
    namespace = "onoToOneMethod", resultType = DictItemQueryVO.class,
  	type = MappingType.ORDER_OF_KEYS
)
public List<DictItemQueryDTO> listItemByIdsAndTypes(List<DictItemQueryDTO> dtos) {
    // do something
}

具体可参见 声明装配操作 中 “键的解析策略” 一节。

5.缓存

在 2.0 及以上版本,你可以在方法上添加 @ContainerCache 注解,使其具备缓存功能:

java
@ContainerCache(
    expirationTime = 1000L, // 配置过期时间
    timeUnit = TimeUnit.SECONDS // 指定过期时间单位
)
@ContainerMethod(
    namespace = "onoToOneMethod",
    resultType = Foo.class, resultKey = "id" // 返回的数据源对象类型为 Foo,并且需要按 id 分组
)
public Set<Foo> onoToOneMethod(List<String> args) {
    // do something
}

如果你的方法上同时声明了多个方法容器,那么它们都将具备缓存功能。

具体可参见后文 缓存 一节。

6.手动注册

手动注册一般只在你的目标类未被 Spring 管理,或者干脆项目没有使用 Spring 的时候会使用。

在 Spring 环境中,针对方法容器的扫描和注册是自动完成的。不过你也可以手动完成这个过程:

java
// 获取操作门面,如果在 Spring 环境也可以直接从 Spring 容器获取
Crane4jTemplate crane4jTemplate = Crane4jTemplate.withDefaultConfiguration();
Foo foo = new Foo();
crane4jTemplate.opsForContainer().registerMethodContainers(foo);

7.包装类提取

有时候,我们会在 Controller 中显式的使用通用响应体包装返回值,比如:

java
@ContainerMethod(resultType = UserVO.class)
public Result<List<UserVO>> listUser(List<Integer> ids) {
    // 返回值被通用响应体包装
}

// 通用响应体
@AllArgsConstructor
@Data
public class Result<T> {
    private String msg = "ok";
    private Integer code = 200;
    private T data;
    public Result(T data) {
        this.data = data;
    }
}

然而,我们真正需要填充的数据其实是 Result.data,在 2.8.0 及以上版本,你可以在 @ContainerMethod 注解中通过 on 属性指定:

java
@ContainerMethod(resultType = UserVO.class, on = "data")
public Result<List<UserVO>> listUser(List<Integer> ids) {
    // 返回值被通用响应体包装
}

image-20240505121816627

TIP

当对方法返回进行自动填充时,你可以通过类似的方法指定从返回的包装类中获取实际数据,具体可参见:自动填充 一节中包装类提取部分。

8.空值处理

从 2.9.0 开始,ContainerMethod 注解添加了两个用于空值处理的属性:

filterNullKey

该配置表示是否需要过滤入参集合中的空值,为 true 时,如果方法入参为 [null, null, 1, 3, null, 4],那么最终在调用方法前里面的 null 值会被过滤,最后实际的调用参数为 [1, 3, 4]

skipQueryIfKeyCollIsEmpty

该配置表示当入参集合为空时,是否不调用方法而直接返回空集合。

并且,当 filterNullKey 配置为 true 时,将会先过滤控制再做判断,比如当入参为 [null, null] 的时候,方法就不会被调用。

9.选项式配置

在 2.2 及以上版本,你可以使用 @AssembleMethod 注解进行选项式风格的配置。通过在类或属性上添加 @AssembleMethod 注解,并指定要绑定的目标类中的指定方法。

在这种情况下,你可以快速的使用 spring 容器中的 bean 里面的方法、或任意类中的静态方法作为数据源容器。

比如:

java
@RequiredArgsConstructor
@Data
private static class Foo {
    @AssembleMethod(
        targetType = FooService.class, // 填充数据源为 FooService#listByIds 方法
        method = @ContainerMethod(bindMethod = "listByIds", resultType = Foo.class, resultKey = "id"),
        prop = { "name", "type" }
    )
    private id;
    private String name;
    private String type;
}

出于降低理解成本的目的,这种配置方式直接复用了 @ContainerMethod 注解。

@AssembleMethod 注解提供了一些参数:

API作用类型默认值
targetType指定调用类的类型目标类无,与 target 二选一必填
target指定调用类的类型全限定名,或者容器中的 beanName调用类的全限定名字符串,如果在 Spring 容器中,则可以是 beanName无,与 targetType 二选一必填
method指定绑定方法@ContainerMethod无,必填
enableCache是否启用缓存配置booleanfalse
cache指定缓存配置@ContainerCache

此外,在选项式配置中,你同样可以通过在被 @ContainerMethod 注解绑定的方法上添加 @ContainerCache 注解的方式实现配置缓存。

不过,在 2.6.0 及以上版本,缓存配置同样集成到了 @AssembleMethod 中:

java
@RequiredArgsConstructor
@Data
private static class Foo {
    @AssembleMethod(
        targetType = FooService.class,
        method = @ContainerMethod(bindMethod = "listByIds", resultType = Foo.class, resultKey = "id"),
        props = { @Mapping("name"), @Mapping("type") },
        enableCache = true,  // 启用缓存
        cache = @ContainerCache(expirationTime = 1000L, timeUnit = TimeUnit.SECONDS) // 设置缓存
    )
    private id;
    private String name;
    private String type;
}

需要注意的是,如果目标方法上已经通过 @ContainerCache 或配置文件的方式配置缓存时,你在 @AssembleMethod 中的缓存配置将不会生效,因为前者的优先级更高。