【问题标题】:Serialize only mentioned fields in Spring MVC to JSON response仅将 Spring MVC 中提到的字段序列化为 JSON 响应
【发布时间】:2014-11-18 19:56:57
【问题描述】:

我正在使用 spring MVC 编写一个休息服务,它产生 JSON 响应。它应该允许客户端仅选择给定的字段作为响应,这意味着客户端可以将他感兴趣的字段作为 url 参数提及,例如?fields=field1,field2

使用 Jackson 注释并不能提供我正在寻找的东西,因为它不是动态的,而且 Jackson 中的过滤器似乎也没有足够的希望。 到目前为止,我正在考虑实现一个可以解决这个问题的自定义消息转换器。

还有其他更好的方法来实现这一点吗?我希望这个逻辑不与我的服务或控制器耦合。

【问题讨论】:

  • 看看我对类似问题的回答,利用 Jackon 的 ObjectWriterModifier/ObjectReaderModifier 功能在 JAX-RS 资源方法中自定义过滤器:stackoverflow.com/a/23816382/3537858

标签: json spring spring-mvc jackson


【解决方案1】:

从 Spring 4.2 开始,MappingJacksonValue 支持 @JsonFilter

您可以在控制器中直接将 PropertyFilter 注入 MappingJacksonValue。

@RestController
public class BookController {
    private static final String INCLUSION_FILTER = "inclusion";

    @RequestMapping("/novels")
    public MappingJacksonValue novel(String[] include) {
        @JsonFilter(INCLUSION_FILTER)
        class Novel extends Book {}

        Novel novel = new Novel();
        novel.setId(3);
        novel.setTitle("Last summer");
        novel.setAuthor("M.K");

        MappingJacksonValue res = new MappingJacksonValue(novel);
        PropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept(include);
        FilterProvider provider = new SimpleFilterProvider().addFilter(INCLUSION_FILTER, filter);
        res.setFilters(provider);
        return res;
    }

或者您可以通过 ResponseBodyAdvice 声明全局策略。以下示例通过“exclude”参数实现过滤策略。

@ControllerAdvice
public class DynamicJsonResponseAdvice extends AbstractMappingJacksonResponseBodyAdvice {
    public static final String EXCLUDE_FILTER_ID = "dynamicExclude";
    private static final String WEB_PARAM_NAME = "exclude";
    private static final String DELI = ",";
    private static final String[] EMPTY = new String[]{};

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue container, MediaType contentType,
            MethodParameter returnType, ServerHttpRequest req, ServerHttpResponse res) {
        if (container.getFilters() != null ) {
            // It will be better to merge FilterProvider
            // If 'SimpleFilterProvider.addAll(FilterProvider)' is provided in Jackson, it will be easier.
            // But it isn't supported yet. 
            return;
        }

        HttpServletRequest baseReq = ((ServletServerHttpRequest) req).getServletRequest();
        String exclusion = baseReq.getParameter(WEB_PARAM_NAME);

        String[] attrs = StringUtils.split(exclusion, DELI);
        container.setFilters(configFilters(attrs));
    }

    private FilterProvider configFilters(String[] attrs) {
        String[] ignored = (attrs == null) ? EMPTY : attrs;
        PropertyFilter filter = SimpleBeanPropertyFilter.serializeAllExcept(ignored);
        return new SimpleFilterProvider().addFilter(EXCLUDE_FILTER_ID, filter);
    }
}

【讨论】:

  • 哇,这拯救了我的一天!
【解决方案2】:

恕我直言,最简单的方法是使用自省动态生成包含选定字段的哈希,然后使用 Json 序列化该哈希。您只需决定可用字段的列表是什么(见下文)。

这里有两个示例函数可以做到这一点,第一个获取所有公共字段和公共 getter,第二个获取当前类及其所有父类中的所有声明字段(包括私有字段):

public Map<String, Object> getPublicMap(Object obj, List<String> names)
        throws IllegalAccessException, IllegalArgumentException, InvocationTargetException  {
    List<String> gettedFields = new ArrayList<String>();
    Map<String, Object> values = new HashMap<String, Object>();
    for (Method getter: obj.getClass().getMethods()) {
        if (getter.getName().startsWith("get") && (getter.getName().length > 3)) {
            String name0 = getter.getName().substring(3);
            String name = name0.substring(0, 1).toLowerCase().concat(name0.substring(1));
            gettedFields.add(name);
            if ((names == null) || names.isEmpty() || names.contains(name)) {
                values.put(name, getter.invoke(obj));
            }
        }
    }
    for (Field field: obj.getClass().getFields()) {
        String name = field.getName();
        if ((! gettedFields.contains(name)) && ((names == null) || names.isEmpty() || names.contains(name))) {
            values.put(name, field.get(obj));
        }
    }
    return values;
}

public Map<String, Object> getFieldMap(Object obj, List<String> names)
        throws IllegalArgumentException, IllegalAccessException  {
    Map<String, Object> values = new HashMap<String, Object>();
    for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
        for (Field field : clazz.getDeclaredFields()) {
            String name = field.getName();
            if ((names == null) || names.isEmpty() || names.contains(name)) {
                field.setAccessible(true);
                values.put(name, field.get(obj));
            }
        }
    }
    return values;
}

然后你只需要得到这个函数之一的结果(或者你可以适应你的要求的一个)并用 Jackson 序列化它。

如果您对域对象进行了自定义编码,则必须在两个不同的地方维护序列化规则:散列生成和杰克逊序列化。在这种情况下,您可以简单地使用 Jackson 生成完整的类序列化,然后过滤生成的字符串。这是一个这样的过滤函数的例子:

public String jsonSub(String json, List<String> names) throws IOException {
    if ((names == null) || names.isEmpty()) {
        return json;
    }
    ObjectMapper mapper = new ObjectMapper();
    Map<String, Object> map = mapper.readValue(json, HashMap.class);
    for (String name: map.keySet()) {
        if (! names.contains(name)) {
            map.remove(name);
        }
    }
    return mapper.writeValueAsString(map);
}

编辑:Spring MVC 中的集成

当您谈到 Web 服务和 Jackson 时,我假设您使用 Spring RestControllerResponseBody 注释和(在后台)MappingJackson2HttpMessageConverter。如果你改用 Jackson 1,它应该是 MappingJacksonHttpMessageConverter

我的建议只是添加一个新的HttpMessageConverter,它可以利用上述过滤功能之一,并将实际工作(以及辅助方法)委托给真正的MappingJackson2HttpMessageConverter。在该新转换器的write 方法中,可以访问最终的fields 请求参数,而无需显式的ThreadLocal 变量,这要归功于Spring RequestContextHolder。那样:

  • 您可以明确区分角色,而无需修改现有控制器
  • 您没有对 Jackson2 配置进行任何修改
  • 您不需要新的 ThreadLocal 变量,只需在已经绑定到 Spring 的类中使用 Spring 类,因为它实现了HttpMessageConverter

这是一个这样的消息转换器的例子:

public class JsonConverter implements HttpMessageConverter<Object> {

    private static final Logger logger = LoggerFactory.getLogger(JsonConverter.class);
    // a real message converter that will respond to ancilliary methods and do the actual work
    private HttpMessageConverter<Object> delegate =
            new MappingJackson2HttpMessageConverter();

    // allow configuration of the fields name
    private String fieldsParam = "fields";

    public void setFieldsParam(String fieldsParam) {
        this.fieldsParam = fieldsParam;
    }

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return delegate.canRead(clazz, mediaType);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return delegate.canWrite(clazz, mediaType);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return delegate.getSupportedMediaTypes();
    }

    @Override
    public Object read(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return delegate.read(clazz, inputMessage);
    }

    @Override
    public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        // is there a fields parameter in request
        String[] fields = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest().getParameterValues(fieldsParam);
        if (fields != null && fields.length != 0) {
            // get required field names
            List<String> names = new ArrayList<String>();
            for (String field : fields) {
                String[] f_names = field.split("\\s*,\\s*");
                names.addAll(Arrays.asList(f_names));
            }
            // special management for Map ...
            if (t instanceof Map) {
                Map<?, ?> tmap = (Map<?, ?>) t;
                Map<String, Object> map = new LinkedHashMap<String, Object>();
                for (Entry entry : tmap.entrySet()) {
                    String name = entry.getKey().toString();
                    if (names.contains(name)) {
                        map.put(name, entry.getValue());
                    }
                }
                t = map;
            } else {
                try {
                    Map<String, Object> map = getMap(t, names);
                    t = map;
                } catch (Exception ex) {
                    throw new HttpMessageNotWritableException("Error in field extraction", ex);
                }
            }
        }
        delegate.write(t, contentType, outputMessage);
    }

    /**
     * Create a Map by keeping only some fields of an object
     * @param obj the Object
     * @param names names of the fields to keep in result Map
     * @return a map containing only requires fields and their value
     * @throws IllegalArgumentException
     * @throws IllegalAccessException 
     */
    public static Map<String, Object> getMap(Object obj, List<String> names)
            throws IllegalArgumentException, IllegalAccessException  {
        Map<String, Object> values = new HashMap<String, Object>();
        for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
            for (Field field : clazz.getDeclaredFields()) {
                String name = field.getName();
                if (names.contains(name)) {
                    field.setAccessible(true);
                    values.put(name, field.get(obj));
                }
            }
        }
        return values;
    }    
}

如果你想让转换器更通用,你可以定义一个接口

public interface FieldsFilter {
    Map<String, Object> getMap(Object obj, List<String> names)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
}

并用它的实现注入它。

现在您必须要求 Spring MVC 使用该自定义消息控制器。

如果您使用 XML 配置,您只需在 &lt;mvc:annotation-driven&gt; 元素中声明它:

<mvc:annotation-driven  >
    <mvc:message-converters>
        <bean id="jsonConverter" class="org.example.JsonConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>

如果你使用 Java 配置,它几乎一样简单:

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Autowired JsonConverter jsonConv;

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(jsonConv);
    StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
    stringConverter.setWriteAcceptCharset(false);

    converters.add(new ByteArrayHttpMessageConverter());
    converters.add(stringConverter);
    converters.add(new ResourceHttpMessageConverter());
    converters.add(new SourceHttpMessageConverter<Source>());
    converters.add(new AllEncompassingFormHttpMessageConverter());
    converters.add(new MappingJackson2HttpMessageConverter());
  }
}

但在这里您必须明确添加您需要的所有默认消息转换器。

【讨论】:

  • 但这不是我想要的,我不想将字段复制到地图中而不是序列化地图,我只想返回我的域对象并有能力限制需要的字段转成json。
  • 休息服务实际上并不返回域对象,而是它们的序列化版本。并且对象的序列化版本和映射{ field_name : field_value } 之间没有区别。 Java 不会复制字段,而只会引用它们(原始类型除外),因此开销应该可以忽略不计。如果列表为空或为空,我编辑的版本将获取所有字段,这样如果用户未指定字段列表,您将获得域对象的完整序列化。
  • 我同意你的观点,但有一个不同的原因我不想这样做。你对我们应该如何以及何时调用这个函数有什么建议?我认为在它返回之前从几乎每个控制器调用这个函数并不是一个好主意,如果我们使用诸如 aop 的 http 消息转换器之类的东西,这一切都会自动发生,看起来很整洁。同样使用您的解决方案,我们如何处理比我们需要迭代所有内容更复杂的对象,例如列表或地图。
  • 您可以在我的回答中使用拦截器和/或自定义注释,但使用 Serge 和 Grep 建议的地图进行过滤。此外,您可以使用 jackson 将对象转换为地图(并返回):ObjectMapper m = new ObjectMapper(); Map&lt;String,Object&gt; props = m.convertValue(myBean, Map.class); MyBean anotherBean = m.convertValue(props, MyBean.class);
  • @varun :我已经使用自定义消息转换器通过经过全面测试的配置更新了我的答案。
【解决方案3】:

我从来没有这样做过,但是在查看此页面http://wiki.fasterxml.com/JacksonFeatureJsonFilter 之后,似乎可以通过这种方式做你想做的事:

1) 创建一个自定义 JacksonAnnotationIntrospector 实现(通过扩展默认实现),它将使用 ThreadLocal 变量为当前请求选择一个过滤器,并创建一个自定义 FilterProvider 来提供该过滤器。

2) 配置消息转换器的 ObjectMapper 以使用自定义的自省器和过滤器提供程序

3) 为 REST 服务创建一个 MVC 拦截器,该拦截器检测 fields 请求参数并通过您的自定义过滤器提供程序为当前请求配置一个新过滤器(这应该是一个线程本地过滤器)。 ObjectMapper 应该通过您的自定义 JacksonAnnotationIntrospector 来获取它。

我不能 100% 确定这个解决方案是线程安全的(这取决于 ObjectMapper 如何在内部使用注解内省器和过滤器提供程序)。

- 编辑-

好的,我做了一个测试实现,发现步骤 1) 不起作用,因为 Jackson 缓存了每个类的 AnnotationInterceptor 的结果。我修改了想法,仅在带注释的控制器方法上应用动态过滤,并且仅当对象没有定义另一个 JsonFilter 时。

这是解决方案(很长):

DynamicRequestJsonFilterSupport 类管理要过滤掉的每个请求字段:

public class DynamicRequestJsonFilterSupport {

    public static final String DYNAMIC_FILTER_ID = "___DYNAMIC_FILTER";

    private ThreadLocal<Set<String>> filterFields;
    private DynamicIntrospector dynamicIntrospector;
    private DynamicFilterProvider dynamicFilterProvider;

    public DynamicRequestJsonFilterSupport() {
        filterFields = new ThreadLocal<Set<String>>();
        dynamicFilterProvider = new DynamicFilterProvider(filterFields);
        dynamicIntrospector = new DynamicIntrospector();
    }

    public FilterProvider getFilterProvider() {
        return dynamicFilterProvider;
    }

    public AnnotationIntrospector getAnnotationIntrospector() {
        return dynamicIntrospector;
    }

    public void setFilterFields(Set<String> fieldsToFilter) {
        filterFields.set(Collections.unmodifiableSet(new HashSet<String>(fieldsToFilter)));
    }

    public void setFilterFields(String... fieldsToFilter) {
        filterFields.set(Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(fieldsToFilter))));
    }

    public void clear() {
        filterFields.remove();
    }

    public static class DynamicIntrospector extends JacksonAnnotationIntrospector {


        @Override
        public Object findFilterId(Annotated annotated) {
            Object result = super.findFilterId(annotated);
            if (result != null) {
                return result;
            } else {
                return DYNAMIC_FILTER_ID;
            }
        }
    }

    public static class DynamicFilterProvider extends FilterProvider {

        private ThreadLocal<Set<String>> filterFields;

        public DynamicFilterProvider(ThreadLocal<Set<String>> filterFields) {
            this.filterFields = filterFields;
        }

        @Override
        public BeanPropertyFilter findFilter(Object filterId) {
            return null;
        }

        @Override
        public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
            if (filterId.equals(DYNAMIC_FILTER_ID) && filterFields.get() != null) {
                return SimpleBeanPropertyFilter.filterOutAllExcept(filterFields.get());
            }
            return super.findPropertyFilter(filterId, valueToFilter);
        }
    }
}

JsonFilterInterceptor 拦截带有自定义 @ResponseFilter 注解的控制器方法。

public class JsonFilterInterceptor implements HandlerInterceptor {

    @Autowired
    private DynamicRequestJsonFilterSupport filterSupport;
    private ThreadLocal<Boolean> requiresReset = new ThreadLocal<Boolean>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod) handler;
            ResponseFilter filter = method.getMethodAnnotation(ResponseFilter.class);
            String[] value = filter.value();
            String param = filter.param();
            if (value != null && value.length > 0) {
                filterSupport.setFilterFields(value);
                requiresReset.set(true);
            } else if (param != null && param.length() > 0) {
                String filterParamValue = request.getParameter(param);
                if (filterParamValue != null) {
                    filterSupport.setFilterFields(filterParamValue.split(","));
                }
            }
        }
        requiresReset.remove();
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        Boolean reset = requiresReset.get();
        if (reset != null && reset) {
            filterSupport.clear();
        }
    }
}

这是自定义的@ResponseFilter 注解。您可以定义一个静态过滤器(通过注解的 value 属性)或基于请求参数的过滤器(通过注解的 param 属性):

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseFilter {
    String[] value() default {};
    String param() default "";
}

您需要在配置类中设置消息转换器和拦截器:

...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(converter());
}

@Bean
JsonFilterInterceptor jsonFilterInterceptor() {
    return new JsonFilterInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(jsonFilterInterceptor);
}

@Bean
DynamicRequestJsonFilterSupport filterSupport() {
    return new DynamicRequestJsonFilterSupport();
}

@Bean
MappingJackson2HttpMessageConverter converter() {
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    ObjectMapper mapper = new ObjectMapper();
    mapper.setAnnotationIntrospector(filterSupport.getAnnotationIntrospector());
    mapper.setFilters(filterSupport.getFilterProvider());
    converter.setObjectMapper(mapper);
    return converter;
}
...

最后,您可以像这样使用过滤器:

@RequestMapping("/{id}")
@ResponseFilter(param = "fields")
public Invoice getInvoice(@PathVariable("id") Long id) { ... }

当向 /invoices/1?fields=id 发出请求时,号码响应将是 过滤后只会返回 id 和 number 属性。

请注意,我尚未对此进行彻底测试,但它应该可以帮助您入门。

【讨论】:

  • 你的想法和我的想法很接近,我的想法也涉及到threadlocal的使用。我相信很多人已经这样做了而且我并不着急,所以我会等待接受任何答案,但确保您的解决方案应该是线程安全的,绝对值得 +1
  • 虽然我同意你的观点,但我同意你也应该等待看到其他答案。
【解决方案4】:

从对象填充 HashMap 会不符合要求吗?然后,您可以只解析 HashMap。过去我用 GSON 做过类似的事情,我必须提供一个简单的实体,最后只填充一个 HashMap,然后对其进行序列化,它比过度设计一个全新的系统更易于维护。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-03-31
    • 1970-01-01
    • 1970-01-01
    • 2012-01-03
    • 2015-08-10
    • 1970-01-01
    • 2013-08-30
    • 1970-01-01
    相关资源
    最近更新 更多