将Spring MVC中仅提及的字段序列化为JSON响应


问题内容

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

使用Jackson注释不能提供我想要的内容,因为它不是动态的,而且Jackson中的过滤器似乎没有足够的前景。到目前为止,我正在考虑实现可解决此问题的自定义消息转换器。

还有其他更好的方法可以做到这一点吗?我希望此逻辑与我的服务或控制器不兼容。


问题答案:

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

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

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序列化。在这种情况下,您可以简单地使用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批注以及(在幕后)a
MappingJackson2HttpMessageConverter。如果您改用Jackson
1,则应为MappingJacksonHttpMessageConverter

我建议的只是添加一个HttpMessageConverter可以利用上述过滤功能之一的新功能,并将实际工作(以及辅助方法)委托给true
MappingJackson2HttpMessageConverter。在该write新转换器的方法中,fields由于Spring的帮助,不需要显式的ThreadLocal变量就可以访问最终的请求参数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 config,则只需在<mvc:annotation-driven>元素中声明它:

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

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