需求:用户自定义字段展示,怎么设计?利用JAVA反射机制解决

需求释义:

        用户自定义一些报表的展示字段:针对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反射机制的一些重要概念和用法:

  1. Class类: Java中的每个类都有一个与之关联的Class对象,它包含了该类的完整信息。你可以使用以下方式获取Class对象:

    Class<?> clazz = MyClass.class; // 通过类名 
    Class<?> clazz = obj.getClass(); // 通过对象实例 
    Class<?> clazz = Class.forName("com.example.MyClass"); // 通过类的完全限定名

  2. 获取类的信息: 一旦有了Class对象,你就可以获取关于类的信息,如类名、字段、方法等。

    String className = clazz.getName(); 
    Field[] fields = clazz.getDeclaredFields(); 
    Method[] methods = clazz.getDeclaredMethods();

  3. 创建对象: 可以通过反射来动态创建对象实例。

    MyClass obj = (MyClass) clazz.newInstance();

  4. 访问和修改字段: 可以通过反射来获取和修改对象的字段值。

    Field field = clazz.getDeclaredField("fieldName"); 
    field.setAccessible(true); // 如果字段是私有的,需要设置可访问性 
    Object value = field.get(obj); // 获取字段值 
    field.set(obj, newValue); // 设置字段值

  5. 调用方法: 可以通过反射来调用类的方法。

    Method method = clazz.getDeclaredMethod("methodName", parameterTypes); 
    method.setAccessible(true); // 如果方法是私有的,需要设置可访问性 
    Object result = method.invoke(obj, args); // 调用方法

  6. 动态代理: 可以使用反射来创建动态代理对象。

    MyInterface proxy = (MyInterface) Proxy
    .newProxyInstance( MyInterface.class.getClassLoader(), new Class[] { MyInterface.class }, new MyInvocationHandler());

        虽然反射提供了很大的灵活性,但也需要谨慎使用,因为它会降低代码的可读性和性能,而且可能会在编译时捕获不到的错误。

如果你觉得我的博客写的还不错,请关注我的公众号吧:

 更多资源分享,请关注我的公众号:搜索或扫码 砥砺code

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值