开发者

springboot2 jackson实现动态返回类字段方式

开发者 https://www.devze.com 2024-08-12 12:20 出处:网络 作者: 被遗忘的优雅
目录问题与需求修改 @jsonFilter 的执行机制Jackson 本身支持实现一个自己的过滤规则类将实现的过滤规则注入到 ObjectMapper测试使用抗干扰扩展总结问题与需求
目录
  • 问题与需求
  • 修改 @jsonFilter 的执行机制
    • Jackson 本身支持
    • 实现一个自己的过滤规则类
    • 将实现的过滤规则注入到 ObjectMapper
    • 测试使用
    • 抗干扰
    • 扩展
  • 总结

    问题与需求

    自从前后端分离的开发模式广泛普及之后,JSON 便成为了端到端交互的首选数据结构。

    我们在使用 Java 开发后端接口的时候,往往会出现我们一个类有十来个字段,但是前端使用到的可能就两三个字段,产生大量冗余字段的情况,虽然对开发没什么影响,但是感觉上就很不爽,并且好些敏感字段返回出去,会降低程序的安全性。

    比如下面这个典型的例子:

    @Getter
    @Setter
    public class User extends GlobalEntity {
        private Integer id;       // 主键
        private String roleCode;  // 外键,角色代码
        private String name;      // 用户昵称
        private String account;   // 账号
        private String pwd;       // 密码
    }
    • 我们在给前端返回用户信息的时候,pwd 密码字段肯定要过滤掉,不然会有安全隐患
    • 我们在某些绑定场景,比如一个下拉选择框选择绑定用户,这时候只需要 id 和 name 字段,其它字段就比较多余
    • 但是我们在 service 层肯定都是调用相同的方法,这时候我们就会想要 在控制器层 控制该实体类 需要返回的字段

    修改 @JsonFilter 的执行机制

    Jackson 本身支持

    @JsonIgnore 只要在字段上加上该注解,就会被过滤掉,但是无法做到动态,比如同一个类在 A 接口需要一个字段在B接口时android不需要这个字段,该注解就无法实现

    @JsonFilter 虽然支持动态配置过滤规则,但是这要求我们针对不同的类,写入不同的过滤规则,而且像 springboot 程序中,我们一般是全局共享一个 ObjectMapper 对象,如果要对同一个类实现不同过滤规则,多线程情况下,会出现线程安全问题

    笔者针对以上需求,结合 @JsonFilter 注解,修改并实现了自己过滤机制,从而实现动态过滤字段功能,废话不多说,上代码

    实现一个自己的过滤规则类

    package com.hwq.common.api.config;
    
    import com.fasterXML.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import com.fasterxml.jackson.databind.ser.BeanPropertyFilter;
    import com.fasterxml.androidjackson.databind.ser.FilterProvider;
    import com.fasterxml.jackson.databind.ser.PropertyFilter;
    import com.fasterxml.jackson.databind.ser.PropertyWriter;
    import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class JsonFilter extends FilterProvider {
    
        /**
         * 对于规则我们采用 ThreadLocal 封装,防止出现线程安全问题
         */
        private static final ThreadLocal<Map<Class<?>, String[]>> include = new ThreadLocal<>();
    
        /**
         * 清空规则
         */
        public static void clear() {
            include.remove();
        }
    
        /**
         * 设置过滤规则
         * @param clazz 规则
         */
        public static void add(Class<?> clazz, String ... fields) {
            Map<Class<?>, String[]> map = include.get();
            if (map == null) {
                map = new HashMap<>();
                include.set(map);
            }
            map.put(clazz, fields);
        }
    
        /**
         * 一个将过期的方法,但是目前还是需要实现,抛个异常即可
         */
        @Deprecated
        @Override
        public BeanPropertyFilter findFilter(Object filterId) {
            throw new UnsupportedOperationException("不支持访问即将过期的过滤器");
        }
    
        /**
         * 重写规律规则
         */
        @Override
        public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
            return new SimpleBeanPropertyFilter() {
      android          @Override
                public void serializeAsField(
                        Object pojo,
                        JsonGenerator jg,
                        SerializerProvider sp,
                        PropejavascriptrtyWriter pw
                ) throws Exception {
                    if (apply(pojo.getClass(), pw.getName())) {
                        pw.serializeAsField(pojo, jg, sp);
                    } else if (!jg.canOmitFields()) {
                        pw.serializeAsOmittedField(pojo, jg, sp);
                    }
                }
            };
        }
    
        /**
         * 判断该字段是否需要,返回 true 序列化,返回 false 则过滤
         * @param type 实体类类型
         * @param name 字段名
         */
        public boolean apply(Class<?> type, String name) {
            Map<Class<?>, String[]> map = include.get();
            if (map == null) {
                return true;
            }
            String[] fields = map.get(type);
            for (String field : fields) {
                if (field.equals(name)) {
                    return true;
                }
            }
            return false;
        }
    
    }

    将实现的过滤规则注入到 ObjectMapper

    我们直接在启动类中,注入自己的过滤规则

    package com.hwq.admin.back;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.hwq.common.api.config.JsonFilter;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    import org.springframework.context.ConfigurableApplicationContext;
    
    @SpringBootApplication         // 注明为微服务程序
    public class AdminBackApp {
    
        public static void main(String[] args) {
            ConfigurableApplicationContext app = SpringApplication.run(AdminBackApp.class);
    
            ObjectMapper objectMapper = app.getBean(ObjectMapper.class);
            objectMapper.setFilterProvider(new JsonFilter());
        }
    }
    

    测试使用

    • 实体类,需要加上 @JsonFilter(“f”) 注解,f 内容随便填写
    package com.hwq.common.api.model.entity;
    
    import com.baomidou.myBATisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import com.fasterxml.jackson.annotation.JsonFilter;
    import com.hwq.common.api.model.common.GlobalEntity;
    import lombok.Getter;
    import lombok.Setter;
    
    @JsonFilter("f") 
    @TableName("t_menu")
    @Getter
    @Setter
    public class Menu extends GlobalEntity {
    
        @TableId(value = "id", type = IdType.AUTO)
        private Integer id;       // 主键
        private Integer pid;      // 菜单的主键,一级菜单为0
        private String name;      // 菜单名称
        private String path;      // 菜单路径
        private String icon;      // 菜单图标
        private Integer ordered;  // 菜单序号
        
    }
    • 接口使用
    @PostMapping("menu/list")
    public ResultVO<Object> list() {
        // 这里配置某个类需要返回的字段,如果有多个类,可以多次 add
        JsonFilter.add(Menu.class, "id", "name", "icon", "path");
        
    	List<Menu> list = menuService.list();
        return ResultVO.ok("查询成功", list);
    }
    • 查询结果展示

    springboot2 jackson实现动态返回类字段方式

    抗干扰

    上面的方式虽然实现了线程隔离,防止了线程安全问题,但是 springboot 的接口是以线程池的方式运行的,如果我们在一个线程给某个类设置了过滤的字段,下一次访问如果也用到了该线程,并且没对之前的规则做清理操作,那么他就会使用上一次的过滤规则,使接口出现奇怪的现象

    解决方式一:

    在所有接口前面,执行 JsonFilter 的 clear 方法;

    @PostMapping("menu/tree")
    public ResultVO<List<Menu>> tree() {
        JsonFilter.clear(); // 清理之前的过滤设置
        List<Menu> vos = menuService.tree();
        return ResultVO.ok("查询成功", vos);
    }

    这种方式就要求我们在所有控制层的方法都要执行 JsonFilter.clear(); 语句,明显代码不够优雅

    解决方法二:

    使用 AOP 的前置拦截,执行该代码

    package com.hwq.admin.back.config;
    
    import com.hwq.common.api.config.JsonFilter;
    import org.ASPectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    public class JsonFilterAop {
    
        /**
         * (1)@annotation:用来拦截所有被某个注解修饰的方法
         * (2)@within:用来拦截所有被某个注解修饰的类
         * (3)within:用来指定扫描的包的范围
         */
        @Before("@within(org.springframework.web.bind.annotation.RestController)")
        public void doBefore() {
            JsonFilter.clear();
        }
    
    }
    @within(org.springframework.web.bind.annotation.RestController) 

    表示拦截所有带 @RestController 注解的类,如果有其他需求,可以做适当修改

    扩展

    JsonFilter.add(Menu.class, "id", "name");
    • 针对这里的情况,我们还可以让前端传递过来,实现一个接口让前端控制返回哪些字段的功能
    • 当前笔者实现的是包含关系,只有被配置的字段才会出现,如果出现一个类编程需要很多字段,只过滤一两个字段,我们也可以通过修改 过滤规则类 JsonFilter 实现,就扩展一下 apply 方法就好,很简单。不过笔者不推荐,如果一个接口大部分字段都需要,那就全部返回好了,冗余一两个字段也不是不能接受的
    • 如果觉得在业务代码里写上上面的代码不好看什么的,也可以通过 注解的方式实现,然后用 AOP 拦截实现,代码逻辑差不多

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号