当前位置: 代码网 > it编程>编程语言>Java > springboot2 jackson实现动态返回类字段方式

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

2024年08月09日 Java 我要评论
问题与需求自从前后端分离的开发模式广泛普及之后,json 便成为了端到端交互的首选数据结构。我们在使用 java 开发后端接口的时候,往往会出现我们一个类有十来个字段,但是前端使用到的可能就两三个字段

问题与需求

自从前后端分离的开发模式广泛普及之后,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接口时不需要这个字段,该注解就无法实现

@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.jackson.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() {
            @override
            public void serializeasfield(
                    object pojo,
                    jsongenerator jg,
                    serializerprovider sp,
                    propertywriter 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);
}
  • 查询结果展示

抗干扰

上面的方式虽然实现了线程隔离,防止了线程安全问题,但是 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 拦截实现,代码逻辑差不多

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com