API接口使用Jackson序列化JSON数据动态过滤字段

背景

编写API接口过程中,不可避免的会遇到一个问题,对于不同的接口,需要的字段不一样,但大多数情况下,使用的Service层方法是相同的,也就是说,获取到的数据字段是一样的,但是往往不需要返回所有的字段。

解决方案

常用的解决思路有两种,一种是针对每个接口定义VO类,在数据返回时,将Service层查到的数据复制到VO类后再返回,这样的话就可以返回需要的字段,但这样也有缺点,不同的接口,需要定义专属的VO类,这样会使类的数量增多,后期如果需要添加一个通用字段,那么需要在每个VO类都添加字段,否则无法返回,后期维护工作量大,不好维护,其次是性能问题,数据返回到浏览器之前,都需要将数据复制到VO类,这样会产生许多的中间实例,影响性能;

第二种方案,在数据序列化为JSON字符串的时候,只序列化需要返回的字段,这种方法相对第一种方法,可以很好的避免第一种方法出现的缺点,对于Jackson原生的注解,无法实现动态过滤需求,如果把注解加在实体字段上,无法实现动态过滤,于是有了改进方案,自定义注解,通过自定义注解获取需要返回或需要过滤的字段,在序列化时处理。

代码实现

自定义注解

为了实现多注解,我们定义两个注解来实现,多注解可实现嵌套对象的字段过滤。

package com.jeeplus.common.annotation;

import java.lang.annotation.*;

/**
 * JSON 返回字段过滤
 *
 * @author zhufeihong
 * @since 2020/12/28 14:32
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(JsonFieldFilters.class)
public @interface JsonFieldFilter {

    /**
     * 对象类
     */
    Class<?> type();

    /**
     * 只包含的字段
     */
    String[] include() default {};

    /**
     * 不包含的字段,如果重新赋值那么默认值失效
     */
    String[] exclude() default {"createBy", "updateBy"};

    /**
     * 不包含的字段,在exclude()默认值的条件下继续添加排除字段
     */
    String[] addExclude() default {};
}
package com.jeeplus.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * JSON 返回字段过滤
 *
 * @author zhufeihong
 * @since 2020/12/28 14:32
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonFieldFilters {

    JsonFieldFilter[] value();
}

自定义JSON过滤器

由于Jackson自带的过滤器com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter不能满足需求,需要自定义过滤器,用于序列化时动态过滤。

package com.jeeplus.config.handler;

import com.fasterxml.jackson.annotation.JsonFilter;
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.*;

/**
 * 自定义JSON序列化过滤器
 *
 * @author zhufeihong
 * @since 2021/1/4 16:30
 */
@JsonFilter("JacksonJsonFilter")
public class SuberJacksonFilterProvider extends FilterProvider {

    /**
     * 包含字段 Map
     */
    Map<Class<?>, Set<String>> includeMap = new HashMap<>();

    /**
     * 排除字段 Map
     */
    Map<Class<?>, Set<String>> excludeMap = new HashMap<>();

    /**
     * 添加包含字段
     *
     * @param type   字段所属类
     * @param fields 字段名数组
     * @since 2021/1/4 17:03
     */
    public void include(Class<?> type, String... fields) {
        addToMap(includeMap, type, fields);
    }

    /**
     * 添加排除字段
     *
     * @param type   字段所属类
     * @param fields 字段名数组
     * @since 2021/1/4 17:03
     */
    public void exclude(Class<?> type, String... fields) {
        addToMap(excludeMap, type, fields);
    }

    /**
     * 实际执行添加包含/排除字段进对应Map的方法
     *
     * @param map    包含字段Map OR 排除字段Map
     * @param type   字段所属类
     * @param fields 字段名称数组
     * @since 2021/1/4 17:04
     */
    private void addToMap(Map<Class<?>, Set<String>> map, Class<?> type, String... fields) {
        Set<String> fieldSet = map.getOrDefault(type, new HashSet<>());
        fieldSet.addAll(Arrays.asList(fields));
        map.put(type, fieldSet);
    }

    @Deprecated
    @Override
    public BeanPropertyFilter findFilter(Object filterId) {
        throw new UnsupportedOperationException("Access to deprecated filters not supported");
    }

    @Override
    public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
        return new SimpleBeanPropertyFilter() {
            @Override
            public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider prov, PropertyWriter writer)
                    throws Exception {
                if (apply(pojo.getClass(), writer.getName())) {
                    writer.serializeAsField(pojo, jgen, prov);
                } else if (!jgen.canOmitFields()) {
                    writer.serializeAsOmittedField(pojo, jgen, prov);
                }
            }
        };
    }

    /**
     * 判断是否序列化当前字段,在includeMap或不在excludeMap中的字段进行序列化
     *
     * @param type 字段所属类
     * @param name 字段名称
     * @return boolean 是否序列化
     * @since 2021/1/4 17:09
     */
    public boolean apply(Class<?> type, String name) {
        Set<String> includeFields = includeMap.get(type);
        Set<String> excludeFields = excludeMap.get(type);
        if (includeFields != null && includeFields.contains(name)) {
            return true;
        } else if (excludeFields != null && !excludeFields.contains(name)) {
            return true;
        } else {
            return includeFields == null && excludeFields == null;
        }
    }
}

自定义JSON序列化方法

我们自定义了过滤器,要想实现字段过滤,需要自定义序列化方法。

package com.jeeplus.core.mapper;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ObjectArrays;
import com.jeeplus.common.annotation.JsonFieldFilter;
import com.jeeplus.common.annotation.JsonFieldFilters;
import com.jeeplus.config.handler.SuberJacksonFilterProvider;

/**
 * JSON 序列化,用于API返回字段过滤
 *
 * @author zhufeihong
 * @since 2020/12/28 15:11
 */
public class ResponseJsonFilterSerializer {

    // JsonMapper 继承自com.fasterxml.jackson.databind.ObjectMapper
    JsonMapper mapper = JsonMapper.getInstance();
    SuberJacksonFilterProvider filterProvider = new SuberJacksonFilterProvider();

    /**
     * json数据返回时过滤字段
     *
     * @param clazz   需要设置规则的Class
     * @param include 转换时包含哪些字段
     * @param exclude 转换时过滤哪些字段
     */
    public void filter(Class<?> clazz, String[] include, String[] exclude) {
        if (clazz == null) {
            return;
        }
        if (include != null && include.length > 0) {
            filterProvider.include(clazz, include);
        } else if (exclude != null && exclude.length > 0) {
            filterProvider.exclude(clazz, exclude);
        }
        mapper.addMixIn(clazz, filterProvider.getClass());
    }

    /**
     * json数据返回时过滤字段
     *
     * @param fieldFilters 注解数组进行过滤
     * @since 2021/1/4 17:26
     */
    public void filter(JsonFieldFilters fieldFilters) {
        for (JsonFieldFilter json : fieldFilters.value()) {
            this.filter(json.type(), json.include(),
                    ObjectArrays.concat(json.exclude(), json.addExclude(), String.class));
        }
    }

    public String toJson(Object object) throws JsonProcessingException {
        mapper.setFilterProvider(filterProvider);
        return mapper.toJson(object);
    }
}

ResponseJson处理器定义

以上准备就绪,我们需要定义一个处理器,用来调用我们的自定义序列化方法实现动态过滤字段。

package com.jeeplus.config.handler;

import com.google.common.collect.ObjectArrays;
import com.jeeplus.common.annotation.JsonFieldFilter;
import com.jeeplus.common.annotation.JsonFieldFilters;
import com.jeeplus.core.mapper.ResponseJsonFilterSerializer;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Objects;

/**
 * JSON返回字段过滤处理器
 *
 * @author zhufeihong
 * @since 2020/12/28 14:52
 */
public class JsonReturnFilterHandler implements HandlerMethodReturnValueHandler {

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        // 如果有自定义的 @JsonFieldFilter 注解 就用我们这个Handler 来处理
        return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), JsonFieldFilters.class)
                || returnType.hasMethodAnnotation(JsonFieldFilters.class)
                || AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), JsonFieldFilter.class)
                || returnType.hasMethodAnnotation(JsonFieldFilter.class);
    }

    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                                  ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        // 设置这个就是最终的处理类了,处理完不再去找下一个类进行处理
        mavContainer.setRequestHandled(true);

        // 获得注解并执行filter方法 最后返回
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        Annotation[] annotations = returnType.getMethodAnnotations();
        ResponseJsonFilterSerializer jsonFilterSerializer = new ResponseJsonFilterSerializer();
        Arrays.asList(annotations).forEach(a -> {
            if (a instanceof JsonFieldFilter) {
                JsonFieldFilter json = (JsonFieldFilter) a;
                jsonFilterSerializer.filter(json.type(), json.include(),
                        ObjectArrays.concat(json.exclude(), json.addExclude(), String.class));
            } else if (a instanceof JsonFieldFilters) {
                jsonFilterSerializer.filter((JsonFieldFilters) a);
            }
        });

        Objects.requireNonNull(response).setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        String json = jsonFilterSerializer.toJson(returnValue);
        response.getWriter().write(json);
    }
}

注册处理器对象

处理器定义好了,我们需要注册它,才能使用,在Spring Boot项目中,对于API接口返回的JSON数据,交由org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor处理后返回,我们需要在这个处理器前添加我们的自定义处理器,才能实现字段过滤,否则进行到这个过滤器后,就不会往下处理,直接返回数据了。

package com.jeeplus.config.handler;

import com.jeeplus.common.annotation.JsonFieldFilter;
import com.jeeplus.common.utils.collection.CollectionUtil;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;

import java.util.ArrayList;
import java.util.List;

/**
 * 配置 HandlerMethodReturnValueHandler
 * 在此配置 API 接口返回的 JSON 字段过滤处理器
 * 在处理器 {@link RequestResponseBodyMethodProcessor} 前添加 {@link JsonReturnFilterHandler}
 * 默认的返回值处理配置 {@link RequestMappingHandlerAdapter#getDefaultReturnValueHandlers()}
 *
 * @author zhufeihong
 * @see JsonFieldFilter
 * @since 2020/12/30 10:10
 */
@Configuration
public class InitializingRequestMappingHandler implements InitializingBean {

    @Autowired
    private RequestMappingHandlerAdapter adapter;

    @Override
    public void afterPropertiesSet() throws Exception {
        List<HandlerMethodReturnValueHandler> returnValueHandlers = adapter.getReturnValueHandlers();
        if (CollectionUtil.isEmpty(returnValueHandlers)) {
            return;
        }
        // 不能直接使用 returnValueHandlers集合,因为此集合被方法unmodifiableList设置为不可修改
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(returnValueHandlers);
        this.decorateHandlers(handlers);
        adapter.setReturnValueHandlers(handlers);
    }

    private void decorateHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        for (int i = 0; i < handlers.size(); i++) {
            // 在RequestResponseBodyMethodProcessor前添加自定义json数据返回处理器,用于返回字段过滤
            if (handlers.get(i) instanceof RequestResponseBodyMethodProcessor) {
                handlers.add(i, new JsonReturnFilterHandler());
                break;
            }
        }
    }
}

使用示例

在方法上使用注解,填写只包含的字段或需要排除的字段,返回的JSON数据就可以实现动态过滤了,对于实体无侵入,不影响之前的方法。

/**
 * 分页查询建筑物信息
 *
 * @param queryParam 查询参数
 * @param page       分页参数
 * @return java.util.Map
 * @author zhufeihong
 * @date 2020/11/4 16:48
 */
@Api
@RequestMapping("/findPage")
@JsonFieldFilter(type = BuildLocation.class, include = {"createDate", "updateDate", "id"})
@JsonFieldFilter(type = Page.class, include = {"pageSize", "pageNo", "count", "list"})
public Map<String, Object> findPage(@RequestAttribute BuildLocation queryParam,
                                    @RequestAttribute Page<BuildLocation> page) {
    Page<BuildLocation> pageList;
    if (Strings.isBlank(queryParam.getAddressId())) {
        pageList = new Page<>();
    } else {
        pageList = buildLocationService.findPage(page, queryParam);
    }
    return super.success(pageList);
}

你可能感兴趣的:(经验分享,Java后端,SpringBoot,java,springboot,jackson,json,api字段过滤)