java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > springboot2 jackson动态返回类字段

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

作者:被遗忘的优雅

这篇文章主要介绍了springboot2 jackson实现动态返回类字段方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

问题与需求

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

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

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

@Getter
@Setter
public class User extends GlobalEntity {
    private Integer id;       // 主键
    private String roleCode;  // 外键,角色代码
    private String name;      // 用户昵称
    private String account;   // 账号
    private String pwd;       // 密码
}

修改 @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());
    }
}

测试使用

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");

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文