1. 程式人生 > 其它 >API介面使用Jackson序列化JSON資料動態過濾欄位

API介面使用Jackson序列化JSON資料動態過濾欄位

技術標籤:經驗分享Java後端SpringBootjavaspringbootjacksonjsonapi欄位過濾

背景

編寫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<java.lang.String, java.lang.Object>
 * @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);
}