feat(core):优化表格构建逻辑以支持字段过滤与描述映射

- 在 `FsTableUtil` 中移除冗余的 `getHeadTemplateBuilder` 重载方法,统一构建入口
- 新增 `getIncludeFieldHeaders` 方法,增强字段包含逻辑,支持驼峰与下划线格式匹配
-修复表头排序问题,确保启用描述时正确应用字段顺序
- 在 `PropertyUtil` 中新增 `getHeaders`重载方法,支持传入字段过滤列表
-`优化 `SheetBuilder 构建流程,使其能正确传递 `includeFields` 并应用字段描述映射
- 移除不必要的包导入与空行,提升代码可读性- 增加 `@NotNull` 注解以强化静态检查能力
This commit is contained in:
liushuang 2025-09-28 16:03:45 +08:00
parent 6d9908bfbb
commit c8e2e2dc5e
3 changed files with 165 additions and 79 deletions

@ -24,17 +24,17 @@ import java.util.stream.Collectors;
* 提供链式调用方式创建飞书表格支持字段过滤等高级功能 * 提供链式调用方式创建飞书表格支持字段过滤等高级功能
*/ */
public class SheetBuilder<T> { public class SheetBuilder<T> {
private final String sheetName; private final String sheetName;
private final String spreadsheetToken; private final String spreadsheetToken;
private final Class<T> clazz; private final Class<T> clazz;
private List<String> includeFields; private List<String> includeFields;
private final Map<String, Object> customProperties = new HashMap<>(); private final Map<String, Object> customProperties = new HashMap<>();
private final Map<String, String> fieldDescriptions = new HashMap<>(); private final Map<String, String> fieldDescriptions = new HashMap<>();
/** /**
* 构造函数 * 构造函数
* *
* @param sheetName 工作表名称 * @param sheetName 工作表名称
* @param spreadsheetToken 电子表格Token * @param spreadsheetToken 电子表格Token
* @param clazz 实体类Class对象 * @param clazz 实体类Class对象
@ -44,12 +44,12 @@ public class SheetBuilder<T> {
this.spreadsheetToken = spreadsheetToken; this.spreadsheetToken = spreadsheetToken;
this.clazz = clazz; this.clazz = clazz;
} }
/** /**
* 设置包含的字段列表 * 设置包含的字段列表
* *
* 指定要包含在表格中的字段名称列表如果不设置则包含所有带有@TableProperty注解的字段 * 指定要包含在表格中的字段名称列表如果不设置则包含所有带有@TableProperty注解的字段
* *
* @param fields 要包含的字段名称列表 * @param fields 要包含的字段名称列表
* @return SheetBuilder实例支持链式调用 * @return SheetBuilder实例支持链式调用
*/ */
@ -57,12 +57,12 @@ public class SheetBuilder<T> {
this.includeFields = new ArrayList<>(fields); this.includeFields = new ArrayList<>(fields);
return this; return this;
} }
/** /**
* 设置自定义属性 * 设置自定义属性
* *
* 添加一个自定义属性可以在构建表格时使用 * 添加一个自定义属性可以在构建表格时使用
* *
* @param key 属性键 * @param key 属性键
* @param value 属性值 * @param value 属性值
* @return SheetBuilder实例支持链式调用 * @return SheetBuilder实例支持链式调用
@ -71,12 +71,12 @@ public class SheetBuilder<T> {
this.customProperties.put(key, value); this.customProperties.put(key, value);
return this; return this;
} }
/** /**
* 批量设置自定义属性 * 批量设置自定义属性
* *
* 批量添加自定义属性可以在构建表格时使用 * 批量添加自定义属性可以在构建表格时使用
* *
* @param properties 自定义属性映射 * @param properties 自定义属性映射
* @return SheetBuilder实例支持链式调用 * @return SheetBuilder实例支持链式调用
*/ */
@ -84,34 +84,34 @@ public class SheetBuilder<T> {
this.customProperties.putAll(properties); this.customProperties.putAll(properties);
return this; return this;
} }
/** /**
* 获取自定义属性 * 获取自定义属性
* *
* 根据键获取已设置的自定义属性值 * 根据键获取已设置的自定义属性值
* *
* @param key 属性键 * @param key 属性键
* @return 属性值如果不存在则返回null * @return 属性值如果不存在则返回null
*/ */
public Object getCustomProperty(String key) { public Object getCustomProperty(String key) {
return this.customProperties.get(key); return this.customProperties.get(key);
} }
/** /**
* 获取所有自定义属性 * 获取所有自定义属性
* *
* @return 包含所有自定义属性的映射 * @return 包含所有自定义属性的映射
*/ */
public Map<String, Object> getCustomProperties() { public Map<String, Object> getCustomProperties() {
return new HashMap<>(this.customProperties); return new HashMap<>(this.customProperties);
} }
/** /**
* 设置字段描述映射 * 设置字段描述映射
* *
* 为实体类字段设置自定义描述信息用于在表格描述行中显示 * 为实体类字段设置自定义描述信息用于在表格描述行中显示
* 如果字段在映射中存在描述则使用映射中的描述否则使用注解中的描述 * 如果字段在映射中存在描述则使用映射中的描述否则使用注解中的描述
* *
* @param fieldDescriptions 字段名到描述的映射key为字段名value为描述文本 * @param fieldDescriptions 字段名到描述的映射key为字段名value为描述文本
* @return SheetBuilder实例支持链式调用 * @return SheetBuilder实例支持链式调用
*/ */
@ -119,12 +119,12 @@ public class SheetBuilder<T> {
this.fieldDescriptions.putAll(fieldDescriptions); this.fieldDescriptions.putAll(fieldDescriptions);
return this; return this;
} }
/** /**
* 设置单个字段描述 * 设置单个字段描述
* *
* 为指定字段设置自定义描述信息 * 为指定字段设置自定义描述信息
* *
* @param fieldName 字段名 * @param fieldName 字段名
* @param description 描述文本 * @param description 描述文本
* @return SheetBuilder实例支持链式调用 * @return SheetBuilder实例支持链式调用
@ -133,60 +133,60 @@ public class SheetBuilder<T> {
this.fieldDescriptions.put(fieldName, description); this.fieldDescriptions.put(fieldName, description);
return this; return this;
} }
/** /**
* 获取字段描述映射 * 获取字段描述映射
* *
* @return 包含所有字段描述的映射 * @return 包含所有字段描述的映射
*/ */
public Map<String, String> getFieldDescriptions() { public Map<String, String> getFieldDescriptions() {
return new HashMap<>(this.fieldDescriptions); return new HashMap<>(this.fieldDescriptions);
} }
/** /**
* 构建表格并返回工作表ID * 构建表格并返回工作表ID
* *
* 根据配置的参数创建飞书表格包括表头样式单元格格式和下拉选项等 * 根据配置的参数创建飞书表格包括表头样式单元格格式和下拉选项等
* *
* @return 创建成功返回工作表ID * @return 创建成功返回工作表ID
*/ */
public String build() { public String build() {
// 获取所有字段映射 // 获取所有字段映射
Map<String, FieldProperty> allFieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); Map<String, FieldProperty> allFieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
// 根据includeFields过滤字段映射 // 根据includeFields过滤字段映射
Map<String, FieldProperty> fieldsMap = filterFieldsMap(allFieldsMap); Map<String, FieldProperty> fieldsMap = filterFieldsMap(allFieldsMap);
// 生成表头 // 生成表头
List<String> headers = PropertyUtil.getHeaders(fieldsMap); List<String> headers = PropertyUtil.getHeaders(fieldsMap, includeFields);
// 获取表格配置 // 获取表格配置
TableConf tableConf = PropertyUtil.getTableConf(clazz); TableConf tableConf = PropertyUtil.getTableConf(clazz);
// 创建飞书客户端 // 创建飞书客户端
FeishuClient client = FsClient.getInstance().getClient(); FeishuClient client = FsClient.getInstance().getClient();
// 1创建sheet // 1创建sheet
String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
// 2添加表头数据 // 2添加表头数据
FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, includeFields, tableConf, fieldDescriptions), client); FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), client);
// 3设置表格样式 // 3设置表格样式
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken); FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken);
// 4合并单元格 // 4合并单元格
List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap); List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap);
if (!mergeCell.isEmpty()) { if (!mergeCell.isEmpty()) {
mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken));
} }
// 5设置单元格为文本格式 // 5设置单元格为文本格式
if (tableConf.isText()) { if (tableConf.isText()) {
String column = FsTableUtil.getColumnNameByNuNumber(headers.size()); String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
} }
// 6设置表格下拉 // 6设置表格下拉
try { try {
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc(), customProperties); FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc(), customProperties);
@ -196,10 +196,10 @@ public class SheetBuilder<T> {
return sheetId; return sheetId;
} }
/** /**
* 根据包含字段列表过滤字段映射 * 根据包含字段列表过滤字段映射
* *
* @param allFieldsMap 所有字段映射 * @param allFieldsMap 所有字段映射
* @return 过滤后的字段映射 * @return 过滤后的字段映射
*/ */
@ -208,7 +208,7 @@ public class SheetBuilder<T> {
if (includeFields == null || includeFields.isEmpty()) { if (includeFields == null || includeFields.isEmpty()) {
return allFieldsMap; return allFieldsMap;
} }
// 根据字段名过滤保留指定的字段 // 根据字段名过滤保留指定的字段
return allFieldsMap.entrySet().stream() return allFieldsMap.entrySet().stream()
.filter(entry -> { .filter(entry -> {

@ -14,6 +14,7 @@ import cn.isliu.core.service.CustomValueService;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.*; import java.util.*;
@ -356,18 +357,18 @@ public class FsTableUtil {
setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, enableDesc, null); setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, enableDesc, null);
} }
// public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
// Map<String, FieldProperty> fieldsMap, TableConf tableConf) {
// return getHeadTemplateBuilder(sheetId, headers, fieldsMap, null, tableConf);
// }
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers, public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
Map<String, FieldProperty> fieldsMap, TableConf tableConf) { Map<String, FieldProperty> fieldsMap, TableConf tableConf) {
return getHeadTemplateBuilder(sheetId, headers, fieldsMap, null, tableConf); return getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, null);
} }
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers, public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
Map<String, FieldProperty> fieldsMap, List<String> includeFields, TableConf tableConf) { Map<String, FieldProperty> fieldsMap, TableConf tableConf, Map<String, String> fieldDescriptions) {
return getHeadTemplateBuilder(sheetId, headers, fieldsMap, includeFields, tableConf, null);
}
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
Map<String, FieldProperty> fieldsMap, List<String> includeFields, TableConf tableConf, Map<String, String> fieldDescriptions) {
String position = FsTableUtil.getColumnNameByNuNumber(headers.size()); String position = FsTableUtil.getColumnNameByNuNumber(headers.size());
@ -378,22 +379,15 @@ public class FsTableUtil {
int maxLevel = getMaxLevel(fieldsMap); int maxLevel = getMaxLevel(fieldsMap);
if (maxLevel == 1) { if (maxLevel == 1) {
// 单层级表头按order排序的headers
List<String> sortedHeaders;
if (includeFields != null && !includeFields.isEmpty()) {
sortedHeaders = includeFields.stream().sorted(Comparator.comparingInt(headers::indexOf)).collect(Collectors.toList());
} else {
sortedHeaders = getSortedHeaders(fieldsMap);
}
int titleRow = tableConf.titleRow(); int titleRow = tableConf.titleRow();
if (tableConf.enableDesc()) { if (tableConf.enableDesc()) {
int descRow = titleRow + 1; int descRow = titleRow + 1;
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + descRow); batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + descRow);
batchPutValuesBuilder.addRow(sortedHeaders.toArray()); batchPutValuesBuilder.addRow(headers.toArray());
batchPutValuesBuilder.addRow(getDescArray(sortedHeaders, fieldsMap, fieldDescriptions)); batchPutValuesBuilder.addRow(getDescArray(headers, fieldsMap, fieldDescriptions));
} else { } else {
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow); batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow);
batchPutValuesBuilder.addRow(sortedHeaders.toArray()); batchPutValuesBuilder.addRow(headers.toArray());
} }
} else { } else {
@ -421,21 +415,47 @@ public class FsTableUtil {
// 如果启用了描述在最后一行添加描述 // 如果启用了描述在最后一行添加描述
if (tableConf.enableDesc()) { if (tableConf.enableDesc()) {
List<String> finalHeaders;
if (includeFields != null && !includeFields.isEmpty()) {
finalHeaders = includeFields.stream().sorted(Comparator.comparingInt(headers::indexOf)).collect(Collectors.toList());
} else {
finalHeaders = getSortedHeaders(fieldsMap);
}
int descRow = maxLevel + 1; int descRow = maxLevel + 1;
batchPutValuesBuilder.addRange(sheetId + "!A" + descRow + ":" + position + descRow); batchPutValuesBuilder.addRange(sheetId + "!A" + descRow + ":" + position + descRow);
batchPutValuesBuilder.addRow(getDescArray(finalHeaders, fieldsMap, fieldDescriptions)); batchPutValuesBuilder.addRow(getDescArray(headers, fieldsMap, fieldDescriptions));
} }
} }
return batchPutValuesBuilder.build(); return batchPutValuesBuilder.build();
} }
@NotNull
private static List<String> getIncludeFieldHeaders(List<String> headers, Map<String, FieldProperty> fieldsMap, List<String> includeFields) {
return includeFields.stream()
.map(includeField -> {
// 查找匹配的fieldsMap key
for (Map.Entry<String, FieldProperty> entry : fieldsMap.entrySet()) {
FieldProperty fieldProperty = entry.getValue();
if (fieldProperty != null && fieldProperty.getTableProperty() != null) {
String field = fieldProperty.getField();
if (field != null) {
// 获取最后一个属性并转换为下划线格式
String[] split = field.split("\\.");
String lastValue = split[split.length - 1];
String underscoreFormat = StringUtil.toUnderscoreCase(lastValue);
// 如果匹配返回fieldsMap的key
if (underscoreFormat.equals(includeField)) {
return entry.getKey();
}
if (lastValue.equals(includeField)) {
return entry.getKey();
}
}
}
}
// 如果没有匹配到返回原始值
return includeField;
})
.sorted(Comparator.comparingInt(includeFields::indexOf))
.collect(Collectors.toList());
}
/** /**
* 获取按order排序的表头列表 * 获取按order排序的表头列表
* *
@ -480,6 +500,9 @@ public class FsTableUtil {
String fieldPath = fieldProperty.getField(); String fieldPath = fieldProperty.getField();
String fieldName = fieldPath.substring(fieldPath.lastIndexOf(".") + 1); String fieldName = fieldPath.substring(fieldPath.lastIndexOf(".") + 1);
desc = fieldDescriptions.get(fieldName); desc = fieldDescriptions.get(fieldName);
if (desc == null) {
desc = fieldDescriptions.get(StringUtil.toUnderscoreCase(fieldName));
}
} }
// 如果映射中没有找到则从注解中获取 // 如果映射中没有找到则从注解中获取

@ -26,14 +26,14 @@ public class PropertyUtil {
/** /**
* 获取类及其嵌套类上@TableProperty注解的字段映射关系 * 获取类及其嵌套类上@TableProperty注解的字段映射关系
* *
* 此方法是入口方法用于获取一个类及其所有嵌套类中 * 此方法是入口方法用于获取一个类及其所有嵌套类中
* @TableProperty注解标记的字段的映射关系 * @TableProperty注解标记的字段的映射关系
* 注解中的值作为keyFieldProperty对象作为value返回 * 注解中的值作为keyFieldProperty对象作为value返回
* *
* 对于嵌套属性使用'.'连接符来表示层级关系 * 对于嵌套属性使用'.'连接符来表示层级关系
* 该方法会过滤掉有子级的字段只返回最底层的字段映射 * 该方法会过滤掉有子级的字段只返回最底层的字段映射
* *
* @param clazz 要处理的类 * @param clazz 要处理的类
* @return 包含所有@TableProperty注解字段映射关系的Map嵌套属性使用'.'连接 * @return 包含所有@TableProperty注解字段映射关系的Map嵌套属性使用'.'连接
*/ */
@ -59,7 +59,7 @@ public class PropertyUtil {
* 收集所有被@TableProperty注解标记的字段信息 * 收集所有被@TableProperty注解标记的字段信息
* *
* 方法会处理循环引用问题并限制递归深度防止栈溢出 * 方法会处理循环引用问题并限制递归深度防止栈溢出
* *
* @param clazz 当前处理的类 * @param clazz 当前处理的类
* @param result 存储结果的Map * @param result 存储结果的Map
* @param keyPrefix key的前缀使用注解中的值构建 * @param keyPrefix key的前缀使用注解中的值构建
@ -73,7 +73,7 @@ public class PropertyUtil {
if (!isTargetPackageClass(clazz)) { if (!isTargetPackageClass(clazz)) {
return; return;
} }
// 检测循环引用限制递归深度 // 检测循环引用限制递归深度
Integer currentDepth = depthMap.getOrDefault(clazz, 0); Integer currentDepth = depthMap.getOrDefault(clazz, 0);
if (currentDepth > 5) { // 限制最大递归深度为5 if (currentDepth > 5) { // 限制最大递归深度为5
@ -129,14 +129,14 @@ public class PropertyUtil {
if (clazz == null) { if (clazz == null) {
return false; return false;
} }
String className = clazz.getName(); String className = clazz.getName();
// 只处理用户自定义的类排除系统类 // 只处理用户自定义的类排除系统类
return !className.startsWith("java.") && return !className.startsWith("java.") &&
!className.startsWith("javax.") && !className.startsWith("javax.") &&
!className.startsWith("sun.") && !className.startsWith("sun.") &&
!className.startsWith("com.sun.") && !className.startsWith("com.sun.") &&
!className.startsWith("jdk."); !className.startsWith("jdk.");
} }
/** /**
@ -276,7 +276,7 @@ public class PropertyUtil {
// 构建新的前缀 // 构建新的前缀
String newKeyPrefix; String newKeyPrefix;
String newValuePrefix = valuePrefix.isEmpty() ? field.getName() : valuePrefix + "." + field.getName(); String newValuePrefix = valuePrefix.isEmpty() ? field.getName() : valuePrefix + "." + field.getName();
// 关键修改如果父节点没有注解则不拼接父节点字段名 // 关键修改如果父节点没有注解则不拼接父节点字段名
if (parentHasAnnotation) { if (parentHasAnnotation) {
// 父节点有注解需要拼接 // 父节点有注解需要拼接
@ -349,7 +349,7 @@ public class PropertyUtil {
clazz.equals(Character.class) || clazz.equals(Character.class) ||
clazz.equals(Byte.class) || clazz.equals(Byte.class) ||
clazz.equals(Short.class) || clazz.equals(Short.class) ||
clazz.equals(java.util.Date.class) || clazz.equals(Date.class) ||
clazz.equals(java.time.LocalDate.class) || clazz.equals(java.time.LocalDate.class) ||
clazz.equals(java.time.LocalDateTime.class)); clazz.equals(java.time.LocalDateTime.class));
} }
@ -365,7 +365,70 @@ public class PropertyUtil {
*/ */
@NotNull @NotNull
public static List<String> getHeaders(Map<String, FieldProperty> fieldsMap) { public static List<String> getHeaders(Map<String, FieldProperty> fieldsMap) {
return getSortedHeaders(fieldsMap);
}
/**
* 从字段属性映射中提取表头列表
*
* 此方法根据字段的@TableProperty注解中的order属性对字段进行排序
* 返回按顺序排列的表头列表用于数据展示时的列顺序
*
* @param fieldsMap 字段属性映射
* @return 按顺序排列的表头列表
*/
@NotNull
public static List<String> getHeaders(Map<String, FieldProperty> fieldsMap, List<String> includeFields) {
List<String> sortedHeaders;
if (includeFields != null && !includeFields.isEmpty()) {
sortedHeaders = getIncludeFieldHeaders(fieldsMap, includeFields);
} else {
sortedHeaders = getSortedHeaders(fieldsMap);
}
return sortedHeaders;
}
@NotNull
private static List<String> getIncludeFieldHeaders(Map<String, FieldProperty> fieldsMap, List<String> includeFields) {
return includeFields.stream()
.map(includeField -> {
// 查找匹配的fieldsMap key
for (Map.Entry<String, FieldProperty> entry : fieldsMap.entrySet()) {
FieldProperty fieldProperty = entry.getValue();
if (fieldProperty != null && fieldProperty.getTableProperty() != null) {
String field = fieldProperty.getField();
if (field != null) {
// 获取最后一个属性并转换为下划线格式
String[] split = field.split("\\.");
String lastValue = split[split.length - 1];
String underscoreFormat = StringUtil.toUnderscoreCase(lastValue);
// 如果匹配返回fieldsMap的key
if (underscoreFormat.equals(includeField)) {
return entry.getKey();
}
if (lastValue.equals(includeField)) {
return entry.getKey();
}
}
}
}
// 如果没有匹配到返回原始值
return includeField;
})
.sorted(Comparator.comparingInt(includeFields::indexOf))
.collect(Collectors.toList());
}
/**
* 获取按order排序的表头列表
*
* @param fieldsMap 字段属性映射
* @return 按order排序的表头列表
*/
private static List<String> getSortedHeaders(Map<String, FieldProperty> fieldsMap) {
return fieldsMap.entrySet().stream() return fieldsMap.entrySet().stream()
.filter(entry -> entry.getValue() != null && entry.getValue().getTableProperty() != null)
.sorted(Comparator.comparingInt(entry -> entry.getValue().getTableProperty().order())) .sorted(Comparator.comparingInt(entry -> entry.getValue().getTableProperty().order()))
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.collect(Collectors.toList()); .collect(Collectors.toList());