需求释义:
用户自定义一些报表的展示字段:针对2B的项目的用户,也会有SaaS系统用户,当有些报表的字段比较长的时候,不同岗位的用户对于数据的敏感度,或者需要看到的字段都不同;这时候就需要通过用户自定义的字段展示配置,动态的展示某报表的限定字段。
设计思路:
设计一个数据库表,用来存储用户的对某些表,需要显示的字段配置;config表结构如下:
table_name varchar 表名
column_name varchar 列名
isRequired tinyint 是否必须
isHidden tinyint 是否隐藏
sort tinyint 排序
一个表名,对应多条数据。
对应的entity对象:
@Data
public class TableColumn {
private Integer userId;
@Schema(description = "表名")
private String tableName;
@Schema(description = "列名")
private String columnName;
@Schema(description = "是否必须(0-否,1-是)")
private Integer isRequired;
@Schema(description = "是否隐藏(0-否,1-是)")
private Integer isHidden;
@Schema(description = "顺序")
private Integer sort;
}
某个用户,对某张表的字段配置数据:
@Select("select * from system_table_column_config where user_id = #{userId} and table_name = #{tableName}")
List<TableColumnRespVO> selectTableColumnList(@Param("userId") Long userId, @Param("tableName") String tableName);
因为这类数据,用户一般配置好了极少会更改,可以将数据放入本地缓存,或者如果用户量比较多的话,可以专门配置一个缓存服务,比如redis,将数据放入缓存,数据结构采用hash的方式;比起用户每次查询都要从数据获取配置的话,查询速度要快得多。
当然缓存也可以做二级缓存,caffeine做本地缓存,本地缓存已过期就去缓存服务中取查找,如果缓存服务中还是没有就去数据库中去查找,数据库中没有配置的话就是全量数据显示或者导出了。
实现代码
下面以阿里巴巴的easyExcel为例,利用@ExcelProperty注解来释义字段名称的情况下,利用反射,获取需要被导出数据或者前端需要展示列表数据,所对应某个类 class的所有属性(列或字段),再对照用户配置的属性做过滤。
public void export(HttpServletResponse response, List<?> data, String filename, String tableName) {
Class<?> clazz = CollUtil.isNotEmpty(data) ? data.get(0).getClass() : null;
ExcelWriterSheetBuilder builder = EasyExcel.write(response.getOutputStream(), clazz).sheet(filename);
if (clazz != null) {
// 查出当前用户的自定义显示字段配置
List<TableColumnRespVO> tableColumns = tableColumnConfigService.listTableColumn(
SecurityFrameworkUtils.getRequiredLoginUser().getId(), tableName);
if (CollUtil.isNotEmpty(tableColumns)) {
Map<String, TableColumnRespVO> tableColumnMap = tableColumns.stream().collect(Collectors.toMap(TableColumnRespVO::getColumnName, c -> c));
Map<Integer, List<String>> headNameMap = new TreeMap<>();
Map<Integer, String> includeColumnFieldNameMap = new TreeMap<>();
//类clazz的所有声明的字段(包括私有字段)
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty != null && excelProperty.value() != null && excelProperty.value().length > 0) {
TableColumnRespVO tableColumn = tableColumnMap.get(field.getName());
// 过滤出用户自定义的字段,如果系统设置该字段被隐藏,则也会被筛除掉
if (tableColumn != null && Objects.equals(tableColumn.getIsHidden(), 0)) {
headNameMap.put(tableColumn.getSort(), CollUtil.newArrayList(excelProperty.value()));
// 直接以sort为key,Integer天然支持自然排序
includeColumnFieldNameMap.put(tableColumn.getSort(), field.getName());
}
}
}
if (!headNameMap.isEmpty()) {
builder.head(new ArrayList<>(headNameMap.values()));
builder.includeColumnFieldNames(new ArrayList<>(includeColumnFieldNameMap.values()));
builder.orderByIncludeColumn(true);
}
}
}
setResponse(response, filename);
builder.doWrite(data);
}
主要是通过反射获取类的属性列表,并且与用户自定义的属性列表做对比。假如用户不想自己定义字段的排序,只想筛选自己想要展示的字段,字段排序默认为系统排序,那么上述代码可以改为:
public void export(HttpServletResponse response, List<?> data, String filename, String tableName) {
Class<?> clazz = CollUtil.isNotEmpty(data) ? data.get(0).getClass() : null;
ExcelWriterSheetBuilder builder = EasyExcel.write(response.getOutputStream(), clazz).sheet(filename);
List<List<String>> headList = new ArrayList<>();
List<String> includeColumnFieldNameList = new ArrayList<>();
boolean sortFlag;
if (clazz != null) {
List<TableColumnRespVO> tableColumns = tableColumnConfigService.listTableColumn(
getRequiredLoginUser().getId(), tableName);
sortFlag = tableColumns.get(0).getSort() != null;
if (CollUtil.isNotEmpty(tableColumns)) {
Map<String, TableColumnRespVO> tableColumnMap = tableColumns.stream().collect(Collectors.toMap(TableColumnRespVO::getColumnName, c -> c));
Map<Integer, List<String>> headNameMap = new TreeMap<>();
Map<Integer, String> includeColumnFieldNameMap = new TreeMap<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty != null && excelProperty.value() != null && excelProperty.value().length > 0) {
TableColumnRespVO tableColumn = tableColumnMap.get(field.getName());
if (tableColumn != null && Objects.equals(tableColumn.getIsHidden(), 0)) {
if (sortFlag) {
headNameMap.put(tableColumn.getSort(), CollUtil.newArrayList(excelProperty.value()));
includeColumnFieldNameMap.put(tableColumn.getSort(), field.getName());
}else if (excelProperty.index() != -1){ // 如果注解标注了index属性,则按照index的排序导出
headNameMap.put(excelProperty.index(), CollUtil.newArrayList(excelProperty.value()));
includeColumnFieldNameMap.put(excelProperty.index(), field.getName());
}else { // 如果index也没有定义,则按照类字段的自然顺序
headList.add(CollUtil.newArrayList(excelProperty.value()));
includeColumnFieldNameList.add(field.getName());
}
}
}
}
if (!headNameMap.isEmpty() || !headList.isEmpty()) {
builder.head(sortFlag || !headNameMap.isEmpty() ? new ArrayList<>(headNameMap.values()) : headList);
builder.includeColumnFieldNames(sortFlag || !includeColumnFieldNameMap.isEmpty() ?
new ArrayList<>(includeColumnFieldNameMap.values()) : includeColumnFieldNameList);
builder.orderByIncludeColumn(true);
}
}
}
setResponse(response, filename);
builder.doWrite(data);
}
总结:
反射在Java中使用的是比较频繁的,很多设计都会使用到,值得注意的是有些私有属性getFields()是无法获取的,必须要使用 getDeclaredFields() 方法获取全部属性。如果要操作某些属性,比如spring中的自动装配,给私有属性赋值,则需要设置field.setAccessible(true)。
Java的反射机制允许程序在运行时检查和操作类、方法和字段。通过反射,你可以在运行时获取类的信息(如类名、字段、方法等),并且可以动态地创建对象、调用方法和访问/修改字段。
以下是Java反射机制的一些重要概念和用法:
-
Class类: Java中的每个类都有一个与之关联的Class对象,它包含了该类的完整信息。你可以使用以下方式获取Class对象:
Class<?> clazz = MyClass.class; // 通过类名 Class<?> clazz = obj.getClass(); // 通过对象实例 Class<?> clazz = Class.forName("com.example.MyClass"); // 通过类的完全限定名
-
获取类的信息: 一旦有了Class对象,你就可以获取关于类的信息,如类名、字段、方法等。
String className = clazz.getName(); Field[] fields = clazz.getDeclaredFields(); Method[] methods = clazz.getDeclaredMethods();
-
创建对象: 可以通过反射来动态创建对象实例。
MyClass obj = (MyClass) clazz.newInstance();
-
访问和修改字段: 可以通过反射来获取和修改对象的字段值。
Field field = clazz.getDeclaredField("fieldName"); field.setAccessible(true); // 如果字段是私有的,需要设置可访问性 Object value = field.get(obj); // 获取字段值 field.set(obj, newValue); // 设置字段值
-
调用方法: 可以通过反射来调用类的方法。
Method method = clazz.getDeclaredMethod("methodName", parameterTypes); method.setAccessible(true); // 如果方法是私有的,需要设置可访问性 Object result = method.invoke(obj, args); // 调用方法
-
动态代理: 可以使用反射来创建动态代理对象。
MyInterface proxy = (MyInterface) Proxy .newProxyInstance( MyInterface.class.getClassLoader(), new Class[] { MyInterface.class }, new MyInvocationHandler());
虽然反射提供了很大的灵活性,但也需要谨慎使用,因为它会降低代码的可读性和性能,而且可能会在编译时捕获不到的错误。
如果你觉得我的博客写的还不错,请关注我的公众号吧:
更多资源分享,请关注我的公众号:搜索或扫码 砥砺code