Java内省:对象转映射

46
我有一个Java对象obj,它有属性obj.attr1obj.attr2等。如果不是公共的,这些属性可能通过额外的间接层进行访问:obj.getAttr1()obj.getAttr2()等。 挑战:我想要一个函数,它接受一个对象,并返回一个 Map<String, Object>,其中键是字符串"attr1""attr2"等,值是相应的对象obj.attr1obj.attr2等。我想象中的函数调用应该像这样:
  • toMap(obj)
  • 或者toMap(obj, "attr1", "attr3")(其中attr1attr3obj的属性子集),
  • 或者如果必要,则为toMap(obj,"getAttr1","getAttr3")
我不太了解Java的内省:你如何在Java中实现这个功能呢?
目前,我对每种我关心的对象类型都有一个专门的toMap()实现,这太繁琐了。
注意:对于那些了解Python的人,我想要类似于obj.__dict__的东西。或者是子集变体的dict((attr, obj.__getattribute__(attr)) for attr in attr_list)

需要使用递归吗? - biziclop
7个回答

66

使用JacksonObjectMapper的另一种方法是使用convertValue,例如:

 ObjectMapper m = new ObjectMapper();
 Map<String,Object> mappedObject = m.convertValue(myObject, new TypeReference<Map<String, String>>() {});

这个答案简洁而专业地解决了原始问题。做得好! - Jake Toronto
Android的Gradle依赖是什么? - Shajeel Afzal
5
我遇到这个解决方案的问题是它会进行递归转换对象,可能会破坏更深层次对象组合中的类型(例如,在第三层中的日期实例将被转换为Long,因为这是ObjectMapper默认的操作)。由于我需要这种方法来进行数据规范化处理,所以这是一个问题... - Petr Dvořák
1
有没有办法消除类型安全警告? - user7294900
1
我用于该解决方案的Gradle依赖项是: implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' 希望这有所帮助! - hfunes.com
显示剩余3条评论

48

使用Apache Commons BeanUtils:http://commons.apache.org/beanutils/

一个针对JavaBeans的Map实现,它使用内省来获取和设置bean中的属性:

Map<Object, Object> introspected = new org.apache.commons.beanutils.BeanMap(object); 

注意:尽管API返回Map<Object,Object>(自1.9.0起),但返回的映射中键的实际类是java.lang.String


1
目前看来,这个解决方案只处理了“put”方法 :) - Andrey
这将创建Map<Object, Object>,而不是Map<String, Object> - vvondra
@vvondra 谢谢,写这篇文章时它返回的是无类型 Map,所以代码会产生未经检查的赋值警告。在 1.9.0 中,他们将其更改为 Map<Object, Object>,但实际上它始终只包含字符串键。 - Andrey
1
它很糟糕,他们之所以在1.9中添加泛型,是为了保持BC。http://commons.apache.org/proper/commons-beanutils/javadocs/v1.9.2/RELEASE-NOTES.txt - vvondra
1
问题在于你获取的BeanMap对象1)没有实现Map接口。2)有一个过多的条目“Class”->“class.name.with.package”。来自https://www.programcreek.com/java-api-examples/?api=org.apache.commons.beanutils.BeanMap的第一个示例可以很好地解决这两个问题。 - Gangnus
显示剩余6条评论

30
你可以使用JavaBeans内省来实现这个功能。请阅读有关java.beans.Introspector类的介绍:
public static Map<String, Object> introspect(Object obj) throws Exception {
    Map<String, Object> result = new HashMap<String, Object>();
    BeanInfo info = Introspector.getBeanInfo(obj.getClass());
    for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
        Method reader = pd.getReadMethod();
        if (reader != null)
            result.put(pd.getName(), reader.invoke(obj));
    }
    return result;
}

需要注意的是:我的代码只处理getter方法; 它将无法找到裸露的字段。对于字段,请参见highlycaffeinated的答案。 :-) (您可能希望结合这两种方法。)


Introspector和反射之间有什么区别? - Amir Raminfar
@Amir:内省使用反射来完成其工作,但工作级别高于反射。反射可以找到Java级别的字段和方法;而内省可以找到JavaBeans级别的属性和事件。 - C. K. Young
所有的答案都很好。我会接受这个答案,因为它对我的最终解决方案(涉及BeanInfojava.io.Serializable)最有用。谢谢大家。 - Radim

17

这里是一个粗略的估计,希望足以让您朝正确的方向前进:

public Map<String, Object> getMap(Object o) {
    Map<String, Object> result = new HashMap<String, Object>();
    Field[] declaredFields = o.getClass().getDeclaredFields();
    for (Field field : declaredFields) {
        result.put(field.getName(), field.get(o));
    }
    return result;
}

4
+1我认为在你的解决方案和我的解决方案之间,原帖的作者应该已经想通了。 (你的方案只涉及字段; 我的方案则只涉及getter方法。 :-P) - C. K. Young
我的工作涉及到所有的事情哈 :) - Amir Raminfar
最好遍历所有字段,包括已声明和继承的字段。 - biziclop

5

以下是一个非常简单的方法。

使用Jackson JSON库将对象转换为JSON。

然后读取JSON并将其转换为Map。

Map将包含您想要的所有内容。

这是4行代码:

ObjectMapper om = new ObjectMapper();
StringWriter sw = new StringWriter();
om.writeValue(object, sw);
Map<String, Object> map = om.readValue(sw.toString(), Map.class);

当然,另一个好处是它是递归的,如果需要,它将创建地图的地图。

1
当你可以使用直接的JavaBeans内省时,那就太绕了。 - C. K. Young
我喜欢这种方式,因为我不需要关心反射或对象的实际结构。 - Amir Raminfar
此外,它如何处理不兼容JSON的对象?高度咖啡因和我的解决方案将很好地处理这个问题。 - C. K. Young
1
@Chris Jester-Young 这也是递归的,而另一种方式则不是。我也不必担心 getters。 - Amir Raminfar
你在结果映射中没有对象的类型,你的对象是纯粹的。那样是无用的。 - Gangnus

3

这些方法都不适用于嵌套属性,对象映射器做得还行,但你必须在要在地图上看到的所有字段上设置所有值,即使如此,你仍然无法轻松地避免/忽略对象自己的@Json注释,因为ObjectMapper基本上会跳过一些属性。所以不幸的是,你必须像下面这样做,这只是一个草稿,只是为了给你一个想法。

/*
     * returns fields that have getter/setters including nested fields as
     * field0, objA.field1, objA.objB.field2, ... 
     * to take care of recursive duplicates, 
     * simply use a set<Class> to track which classes
     * have already been traversed
     */
    public static void getBeanUtilsNestedFields(String prefix, 
            Class clazz,  List<String> nestedFieldNames) throws Exception {
        PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(clazz);
        for(PropertyDescriptor descr : descriptors){
            // if you want values, use: descr.getValue(attributeName)
            if(descr.getPropertyType().getName().equals("java.lang.Class")){
                continue;
            }
            // a primitive, a CharSequence(String), Number, Date, URI, URL, Locale, Class, or corresponding array
            // or add more like UUID or other types
            if(!BeanUtils.isSimpleProperty(descr.getPropertyType())){
                Field collectionfield = clazz.getDeclaredField(descr.getName());
                if(collectionfield.getGenericType() instanceof ParameterizedType){
                    ParameterizedType integerListType = (ParameterizedType) collectionfield.getGenericType();
                    Class<?> actualClazz = (Class<?>) integerListType.getActualTypeArguments()[0];
                    getBeanUtilsNestedFields(descr.getName(), actualClazz, nestedFieldNames);
                }
                else{   // or a complex custom type to get nested fields
                    getBeanUtilsNestedFields(descr.getName(), descr.getPropertyType(), nestedFieldNames);
                }
            }
            else{
                nestedFieldNames.add(prefix.concat(".").concat(descr.getDisplayName()));
            }
        }
    }

0

maven 依赖

    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>

....

ObjectMapper m = new ObjectMapper();
Map<String,Object> mappedObject = m.convertValue(myObject,Map.class);

对于JSR310新的日期/时间API,有一些需要改进的问题,例如:

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.Test;

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;

@Data
@NoArgsConstructor
public class QueryConditionBuilder
{
    LocalDateTime startTime;
    LocalDateTime endTime;
    Long nodeId;
    Long fsId;
    Long memId;
    Long ifCardId;

    private QueryConditionBuilder(QueryConditionBuilder.Builder builder) {
        setStartTime(builder.startTime);
        setEndTime(builder.endTime);
        setNodeId(builder.nodeId);
        setFsId(builder.fsId);
        setMemId(builder.memId);
        setIfCardId(builder.ifCardId);
    }

    public static QueryConditionBuilder.Builder newBuilder() {
        return new QueryConditionBuilder.Builder();
    }

    public static QueryConditionBuilder newEmptyBuilder() {
        return new QueryConditionBuilder.Builder().build();
    }


    public Map<String,Object> toFilter()
    {
        Map<String,Object> filter = new ObjectMapper().convertValue(this,Map.class);
        System.out.printf("查询条件:%s\n", JSON.toJSONString(filter));
        return filter;
    }

    public static final class Builder {
        private LocalDateTime startTime;
        private LocalDateTime endTime;
        private Long nodeId = null;
        private Long fsId = null;
        private Long memId =null;
        private Long ifCardId = null;

        private Builder() {
        }

        public QueryConditionBuilder.Builder withStartTime(LocalDateTime val) {
            startTime = val;
            return this;
        }

        public QueryConditionBuilder.Builder withEndTime(LocalDateTime val) {
            endTime = val;
            return this;
        }

        public QueryConditionBuilder.Builder withNodeId(Long val) {
            nodeId = val;
            return this;
        }

        public QueryConditionBuilder.Builder withFsId(Long val) {
            fsId = val;
            return this;
        }

        public QueryConditionBuilder.Builder withMemId(Long val) {
            memId = val;
            return this;
        }

        public QueryConditionBuilder.Builder withIfCardId(Long val) {
            ifCardId = val;
            return this;
        }

        public QueryConditionBuilder build() {
            return new QueryConditionBuilder(this);
        }
    }

    @Test
    public void test()
    {     
        LocalDateTime now = LocalDateTime.now(ZoneId.of("+8"));
        LocalDateTime yesterday = now.plusHours(-24);

        Map<String, Object> condition = QueryConditionBuilder.newBuilder()
                .withStartTime(yesterday)
                .withEndTime(now)
                .build().toFilter();

        System.out.println(condition);
    }
}

期望值(伪代码):

查询条件:{"startTime":{"2019-07-15T20:43:15"},"endTime":{"2019-07-16T20:43:15"}
{startTime={2019-07-15T20:43:15}, endTime={"2019-07-16T20:43:15"}, nodeId=null, fsId=null, memId=null, ifCardId=null}

相反,我得到了这些:

查询条件:{"startTime":{"dayOfMonth":15,"dayOfWeek":"MONDAY","dayOfYear":196,"hour":20,"minute":38,"month":"JULY","monthValue":7,"nano":263000000,"year":2019,"second":12,"chronology":{"id":"ISO","calendarType":"iso8601"}},"endTime":{"dayOfMonth":16,"dayOfWeek":"TUESDAY","dayOfYear":197,"hour":20,"minute":38,"month":"JULY","monthValue":7,"nano":263000000,"year":2019,"second":12,"chronology":{"id":"ISO","calendarType":"iso8601"}}}
{startTime={dayOfMonth=15, dayOfWeek=MONDAY, dayOfYear=196, hour=20, minute=38, month=JULY, monthValue=7, nano=263000000, year=2019, second=12, chronology={id=ISO, calendarType=iso8601}}, endTime={dayOfMonth=16, dayOfWeek=TUESDAY, dayOfYear=197, hour=20, minute=38, month=JULY, monthValue=7, nano=263000000, year=2019, second=12, chronology={id=ISO, calendarType=iso8601}}, nodeId=null, fsId=null, memId=null, ifCardId=null}

在进行了一些研究之后,找到了一个有效的技巧。
ObjectMapper mapper = new ObjectMapper();
JavaTimeModule module = new JavaTimeModule();
//https://github.com/networknt/light-4j/issues/82
mapper.registerModule(module);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//incase of empty/null String
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
Map<String,Object> filter = mapper.convertValue(this,Map.class);
System.out.printf("查询条件:%s\n", JSON.toJSONString(filter));
return filter;

输出:

查询条件:{"startTime":"2019-07-15T21:29:13.711","endTime":"2019-07-16T21:29:13.711"}
{startTime=2019-07-15T21:29:13.711, endTime=2019-07-16T21:29:13.711, nodeId=null, fsId=null, memId=null, ifCardId=null}

我在MyBatis中使用了上述代码进行动态查询
例如:

 /***
     * 查询文件系统使用率
     * @param condition
     * @return
     */
    LinkedList<SnmpFileSystemUsage> queryFileSystemUsage(Map<String,Object> condition);

    List<SnmpFileSystemUsage> fooBar()
    { 
       return snmpBaseMapper.queryFileSystemUsage(QueryConditionBuilder
                .newBuilder()
                .withNodeId(nodeId)
                .build()
                .toFilter());
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接