XStream 源码解析

项目地址:XStream, 分析的版本: v1.4.11,Demo地址:TestStream, 相关jar下载地址

本文结构

1、功能介绍
2、总体设计
3、详细设计
4、如何提升XStream 解析速度
5、自定义Mapper和自定义Converter
6、总结

1、功能介绍

1.1简介

提供给Java使用的XML序列化工具

1.2 如何使用

public void testPopulationOfAnObjectGraphStartingWithALiveRootObject() throws Exception {
        final XStream xstream = new XStream();
        //设置允许类型带有$*字符
        xstream.allowTypesByWildcard(this.getClass().getName() + "$*");
        //开启注解自动检测
        xstream.autodetectAnnotations(true);
        final String xml = ""//
                + "<component>"
                + "  <Host>host</Host>"
                + "  <port>8000</port>"
                + "</component>";
        //通过注解设置Component.class的别名为component    
        xstream.alias("component", Component.class);
        //把XML转化成Component对象,(PS:如果想对象转化成XML,则使用toXML()方法)
        final Component component = xstream.fromXML(xml);
    }
    
    public static class Component {
                //设置host的别名为Host
                @XStreamAlias("Host")
        String host;
        int port;
    }

2、总体设计

2.1 XStream 总体由五部分组成

  • XStream 作为客户端对外提供XML解析与转换的相关方法。

  • AbstractDriver 为XStream提供流解析器和编写器的创建。目前支持XML(DOM,PULL)、JSON解析器。解析器HierarchicalStreamReader,编写器HierarchicalStreamWriter(PS:XStream默认使用了XppDriver)。

  • MarshallingStrategy 编组和解组策略的核心接口,两个方法:
    marshal:编组对象图
    unmarshal:解组对象图
    TreeUnmarshaller 树解组程序,调用mapper和Converter把XML转化成java对象,里面的start方法开始解组,convertAnother方法把class转化成java对象。
    TreeMarshaller 树编组程序,调用mapper和Converter把java对象转化成XML,里面的start方法开始编组,convertAnother方法把java对象转化成XML。
    它的抽象子类AbstractTreeMarshallingStrategy有抽象两个方法
    createUnmarshallingContext
    createMarshallingContext
    用来根据不同的场景创建不同的TreeUnmarshaller子类和TreeMarshaller子类,使用了策略模式,如ReferenceByXPathMarshallingStrategy创建ReferenceByXPathUnmarshaller,ReferenceByIdMarshallingStrategy创建ReferenceByIdUnmarshaller(PS:XStream默认使用ReferenceByXPathMarshallingStrategy)。

  • Mapper 映射器,XML的elementName通过mapper获取对应类、成员、属性的class对象。支持解组和编组,所以方法是成对存在real 和serialized,他的子类MapperWrapper作为装饰者,包装了不同类型映射的映射器,如AnnotationMapper,ImplicitCollectionMapper,ClassAliasingMapper。

  • ConverterLookup 通过Mapper获取的Class对象后,接着调用lookupConverterForType获取对应Class的转换器,将其转化成对应实例对象。DefaultConverterLookup是该接口的实现类,同时实现了ConverterRegistry的接口,所有DefaultConverterLookup具备查找converter功能和注册converter功能。所有注册的转换器按一定优先级组成由TreeSet保存的有序集合(PS:XStream 默认使用了DefaultConverterLookup)。

3、详细设计

image.png

StartUML写的XStreamUML原文件和图片下载地址

4、核心功能介绍

4.1 Xstream中Mapper的原理

1. Mapper的构建过程

//XStream 构建映射器,通过MapperWrapper装饰者,将各个不同功能的映射器进行包装成Mapper
//通过映射器获取节点名的类型Class。
private Mapper buildMapper() {
        Mapper mapper = new DefaultMapper(classLoaderReference);
        if (useXStream11XmlFriendlyMapper()) {
            mapper = new XStream11XmlFriendlyMapper(mapper);
        }
        mapper = new DynamicProxyMapper(mapper);
        mapper = new PackageAliasingMapper(mapper);
        mapper = new ClassAliasingMapper(mapper);
        mapper = new ElementIgnoringMapper(mapper);
        mapper = new FieldAliasingMapper(mapper);
        mapper = new AttributeAliasingMapper(mapper);
        mapper = new SystemAttributeAliasingMapper(mapper);
        mapper = new ImplicitCollectionMapper(mapper, reflectionProvider);
        mapper = new OuterClassMapper(mapper);
        mapper = new ArrayMapper(mapper);
        mapper = new DefaultImplementationsMapper(mapper);
        mapper = new AttributeMapper(mapper, converterLookup, reflectionProvider);
        mapper = new EnumMapper(mapper);
        mapper = new LocalConversionMapper(mapper);
        mapper = new ImmutableTypesMapper(mapper);
        if (JVM.isVersion(8)) {
            mapper = buildMapperDynamically("com.thoughtworks.xstream.mapper.LambdaMapper", new Class[]{Mapper.class},
                new Object[]{mapper});
        }
        mapper = new SecurityMapper(mapper);
        mapper = new AnnotationMapper(mapper, converterRegistry, converterLookup, classLoaderReference,
            reflectionProvider);
        mapper = wrapMapper((MapperWrapper)mapper);
        mapper = new CachingMapper(mapper);
        return mapper;
    }
//PackageAliasingMapper继承自MapperWrapper
public PackageAliasingMapper(final Mapper wrapped) {
        //调用MapperWrapper构造器
        super(wrapped);
    }
//包装过程----》把Mapper中的方法按不同功能划分成不同实现类,并通过装饰者进行装载,包装过程核心方法如下:
public MapperWrapper(final Mapper wrapped) {
        this.wrapped = wrapped;
        
        if (wrapped instanceof MapperWrapper) {
            // 2.开始包装
            final MapperWrapper wrapper = (MapperWrapper)wrapped;
            final Map<String, Mapper> wrapperMap = new HashMap<>();
            wrapperMap.put("aliasForAttribute", wrapper.aliasForAttributeMapper);
            wrapperMap.put("aliasForSystemAttribute", wrapper.aliasForSystemAttributeMapper);
            wrapperMap.put("attributeForAlias", wrapper.attributeForAliasMapper);
            wrapperMap.put("defaultImplementationOf", wrapper.defaultImplementationOfMapper);
            wrapperMap.put("getConverterFromAttribute", wrapper.getConverterFromAttributeMapper);
            wrapperMap.put("getConverterFromItemType", wrapper.getConverterFromItemTypeMapper);
            wrapperMap.put("getFieldNameForItemTypeAndName", wrapper.getFieldNameForItemTypeAndNameMapper);
            wrapperMap.put("getImplicitCollectionDefForFieldName", wrapper.getImplicitCollectionDefForFieldNameMapper);
            wrapperMap.put("getItemTypeForItemFieldName", wrapper.getItemTypeForItemFieldNameMapper);
            wrapperMap.put("getLocalConverter", wrapper.getLocalConverterMapper);
            wrapperMap.put("isIgnoredElement", wrapper.isIgnoredElementMapper);
            wrapperMap.put("isImmutableValueType", wrapper.isImmutableValueTypeMapper);
            wrapperMap.put("isReferenceable", wrapper.isReferenceableMapper);
            wrapperMap.put("realClass", wrapper.realClassMapper);
            wrapperMap.put("realMember", wrapper.realMemberMapper);
            wrapperMap.put("serializedClass", wrapper.serializedClassMapper);
            wrapperMap.put("serializedMember", wrapper.serializedMemberMapper);
            wrapperMap.put("shouldSerializeMember", wrapper.shouldSerializeMemberMapper);

            final Method[] methods = wrapped.getClass().getMethods();
            for (final Method method : methods) {
                
                if (method.getDeclaringClass() != MapperWrapper.class) {
                    final String name = method.getName();
                    if (wrapperMap.containsKey(name)) {
                        //wrapped类中实现的方法且wrapperMap包含的,放入map中
                        wrapperMap.put(name, wrapped);
                    }
                }
            }
            //把wrapperMap中的值重新赋值给mapper中的变量
            aliasForAttributeMapper = wrapperMap.get("aliasForAttribute");
            aliasForSystemAttributeMapper = wrapperMap.get("aliasForSystemAttribute");
            attributeForAliasMapper = wrapperMap.get("attributeForAlias");
            defaultImplementationOfMapper = wrapperMap.get("defaultImplementationOf");
            getConverterFromAttributeMapper = wrapperMap.get("getConverterFromAttribute");
            getConverterFromItemTypeMapper = wrapperMap.get("getConverterFromItemType");
            getFieldNameForItemTypeAndNameMapper = wrapperMap.get("getFieldNameForItemTypeAndName");
            getImplicitCollectionDefForFieldNameMapper = wrapperMap.get("getImplicitCollectionDefForFieldName");
            getItemTypeForItemFieldNameMapper = wrapperMap.get("getItemTypeForItemFieldName");
            getLocalConverterMapper = wrapperMap.get("getLocalConverter");
            isIgnoredElementMapper = wrapperMap.get("isIgnoredElement");
            isImmutableValueTypeMapper = wrapperMap.get("isImmutableValueType");
            isReferenceableMapper = wrapperMap.get("isReferenceable");
            realClassMapper = wrapperMap.get("realClass");
            realMemberMapper = wrapperMap.get("realMember");
            serializedClassMapper = wrapperMap.get("serializedClass");
            serializedMemberMapper = wrapperMap.get("serializedMember");
            shouldSerializeMemberMapper = wrapperMap.get("shouldSerializeMember");
        } else {
            // 1. 初始化DefaultMapper初始化各个映射器
            aliasForAttributeMapper = wrapped;
            aliasForSystemAttributeMapper = wrapped;
            attributeForAliasMapper = wrapped;
            defaultImplementationOfMapper = wrapped;
            getConverterFromAttributeMapper = wrapped;
            getConverterFromItemTypeMapper = wrapped;
            getFieldNameForItemTypeAndNameMapper = wrapped;
            getImplicitCollectionDefForFieldNameMapper = wrapped;
            getItemTypeForItemFieldNameMapper = wrapped;
            getLocalConverterMapper = wrapped;
            isIgnoredElementMapper = wrapped;
            isImmutableValueTypeMapper = wrapped;
            isReferenceableMapper = wrapped;
            realClassMapper = wrapped;
            realMemberMapper = wrapped;
            serializedClassMapper = wrapped;
            serializedMemberMapper = wrapped;
            shouldSerializeMemberMapper = wrapped;
        }

    }

2. Mapper的映射的查找过程

比如,根据elementName查找对应的Class,首先调用realClass方法,然后realClass方法会在所有包装层中一层层往下找,并还原elementName的信息,比如在ClassAliasingMapper根据component别名得出Component类,最后在DefaultMapper中调用realClass创建出Class。
CachingMapper——>SecurityMapper——>ArrayMapper———>ClassAliasingMapper——>PackageAliasingMapper———>DynamicProxyMapper———>DefaultMapper
DefaultMapper的realClass方法

@Override
    public Class<?> realClass(final String elementName) {
            return Class.forName(elementName, initialize, classLoader);
    }

比如对于一个elementNode为component,下图是调用的方法栈。


image.png

4.2、XStream中Converter的原理

1.Converter的注册

注册转化器并设置优先级,Null最高,基本类型第二高,常用bean普通优先级,反射低最低优先级,序列化对象和Externalizable低优先级,。

   registerConverter(new ReflectionConverter(mapper, reflectionProvider), PRIORITY_VERY_LOW);

   registerConverter(new SerializableConverter(mapper, reflectionProvider, classLoaderReference), PRIORITY_LOW);
   registerConverter(new ExternalizableConverter(mapper, classLoaderReference), PRIORITY_LOW);

   registerConverter(new NullConverter(), PRIORITY_VERY_HIGH);
   registerConverter(new IntConverter(), PRIORITY_NORMAL);
   registerConverter(new FloatConverter(), PRIORITY_NORMAL);

使用二叉树TreeSet来保存Converter并根据优先级排序所有转换器,排好序的集合
方便后面对converter的查找。

private final PrioritizedList<Converter> converters = new PrioritizedList<>();
@Override
    public void registerConverter(final Converter converter, final int priority) {
        typeToConverterMap.clear();
        converters.add(converter, priority);
    }

public class PrioritizedList<E> implements Iterable<E> {
    //实际是TreeSet来存储Converters
    private final Set<PrioritizedItem<E>> set = new TreeSet<>();
}

2. 查找Converter

public Converter lookupConverterForType(final Class<?> type) {
        //先查询缓存的类型对应的转换器集合
        final Converter cachedConverter = type != null ? typeToConverterMap.get(type.getName()) : null;
        if (cachedConverter != null) {
            //返回找到的缓存转换器
            return cachedConverter;
        }
        
        final Map<String, String> errors = new LinkedHashMap<>();
        //遍历转换器集合
        for (final Converter converter : converters) {
            try {
                //判断是不是符合的转换器
                if (converter.canConvert(type)) {
                    if (type != null) {
                        //缓存类型对应的转换器
                        typeToConverterMap.put(type.getName(), converter);
                    }
                    //返回找到的转换器
                    return converter;
                }
            } catch (final RuntimeException | LinkageError e) {
                errors.put(converter.getClass().getName(), e.getMessage());
            }
        }
}

4.3、XML中HierarchicalStreamDriver的原理

HierarchicalStreamDriver通过下面方法创建Writer和Reader

HierarchicalStreamWriter createWriter(Writer out);
HierarchicalStreamReader createReader(Reader in);

HierarchicalStreamDriver的实现类为AbstractDriver,使用XML DOM解析,把整个XML加载进内存,缺点是占内存。

private HierarchicalStreamReader createReader(final InputSource source) {
            documentBuilderFactory = createDocumentBuilderFactory();
            final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            if (encoding != null) {
                source.setEncoding(encoding);
            }
}

AbstractDriver实现类XppDriver,创建的是XmlPullParser 解析器XML的PULL解析,他是Xpp版本1(XStream默认使用该解析器)

public static synchronized XmlPullParser createDefaultParser() throws XmlPullParserException {
        if (factory == null) {
            factory = XmlPullParserFactory.newInstance();
        }
        return factory.newPullParser();
    }

AbstractDriver实现类Xpp3Driver,创建的是Xpp3的XML解析器MXParser,他是Xpp3 解析器速度要快于Xpp,也是PULL解析XML。

@Override
    protected XmlPullParser createParser() {
        return new MXParser();
    }

AbstractDriver实现类JsonHierarchicalStreamDriver创建的是Json解析器,当目前只支持把java对象序列化成JSON,不支持把JSON反序列化成java对象。

4.4、XML是如何转化成Java对象的

Xstream 调用fromXML

  1. 把String转化成StringReader,HierarchicalStreamDriver通过StringReader创建HierarchicalStreamReader,最后调用MarshallingStrategy的unmarshal方法开始解组
fromXML(final String xml)
fromXML(new StringReader(xml)); 
unmarshal(hierarchicalStreamDriver.createReader(xml), root);
final T t = (T)marshallingStrategy.unmarshal(root, reader, dataHolder, converterLookup, mapper);
  1. marshallingStrategy创建出TreeUnmarshaller来并启动解析
final TreeUnmarshaller context = createUnmarshallingContext(root, reader, converterLookup, mapper);
//start转化
context.start(dataHolder);
  1. 开始组码—————>TreeUnmarshaller的start方法
public Object start(final DataHolder dataHolder) {
        this.dataHolder = dataHolder;
        //通过mapper获取对应节点的Class对象
        final Class<?> type = HierarchicalStreams.readClassType(reader, mapper);
        //Converter根据Class的类型转化成java对象
        final Object result = convertAnother(null, type);
        for (final Runnable runnable : validationList) {
            runnable.run();
        }
        return result;
    }
  1. 通过节点名获取Mapper中对应的Class
public static Class<?> readClassType(final HierarchicalStreamReader reader, final Mapper mapper) {
        if (classAttribute == null) {
        // 通过节点名获取Mapper中对应的Class
        Class<?> type = mapper.realClass(reader.getNodeName());
        return type;
    }
  1. 根据Class把它转化成对应的java对象—————>TreeUnmarshaller的convertAnother方法

public Object convertAnother(final Object parent, Class<?> type, Converter converter) {
        //根据mapper获取type的正确类型
        type = mapper.defaultImplementationOf(type);
        if (converter == null) {
            //根据type找到对应的converter
            converter = converterLookup.lookupConverterForType(type);
        } else {
            if (!converter.canConvert(type)) {
                final ConversionException e = new ConversionException("Explicitly selected converter cannot handle type");
                e.add("item-type", type.getName());
                e.add("converter-type", converter.getClass().getName());
                throw e;
            }
        }
         // 进行把type转化成对应的object
        return convert(parent, type, converter);
    }
  1. 如何查找对应的Converter———>ConverterLookup中的lookupConverterForType方法
public Converter lookupConverterForType(final Class<?> type) {
        // 先从缓存集合中查找Converter
        final Converter cachedConverter = type != null ? typeToConverterMap.get(type.getName()) : null;
        if (cachedConverter != null) {
            return cachedConverter;
        }
        // 遍历converters找到符合的Converter
        for (final Converter converter : converters) {
                if (converter.canConvert(type)) {
                    if (type != null) {
                        把找到的放入缓存集合中
                        typeToConverterMap.put(type.getName(), converter);
                    }
                    return converter;
                }
        }
    }
  1. 根据找到的Converter把Type转化成java对象————>TreeUnmarshaller的convert()
protected Object convert(final Object parent, final Class<?> type, final Converter converter) {
            //调用converter unmarshal进行组码
            return converter.unmarshal(reader, this);
}


  1. 组码的过程,如当Class对应的Converter为AbstractReflectionConverter时,
    根据获取的对象,继续读取子节点,并转化成对象对应的变量。
public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
        // 创建class的instance
        Object result = instantiateNewInstance(reader, context);
        //执行组码
        result = doUnmarshal(result, reader, context);
        return serializationMembers.callReadResolve(result);
    }

protected Object instantiateNewInstance(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
            //创建class的instance
            return reflectionProvider.newInstance(context.getRequiredType());
    }

public Object doUnmarshal(final Object result, final HierarchicalStreamReader reader,
            final UnmarshallingContext context) {
    //读取里面所有的节点
     while (reader.hasMoreChildren()) {
            reader.moveDown();
            //获取class中的变量
            Object field = reflectionProvider.getFieldOrNull(fieldDeclaringClass, fieldName);
            //判断了class中的变量的类型
            final String classAttribute = HierarchicalStreams.readClassAttribute(reader, mapper);
            if (classAttribute != null) {
                 type = mapper.realClass(classAttribute);
            } else {
                 type = mapper.defaultImplementationOf(field.getType());
                      }
            //读取class中的变量的值
            value = unmarshallField(context, result, type, field);
            //把变量的值赋值给Class的实例
            reflectionProvider.writeField(result, fieldName, value, field.getDeclaringClass());
  }
}
  1. 获取Class变量的值过程同5到8,是一个循环过程,直到读取到最后一个节点退出循环。最终获取到java对象中的变量值也都设置,整个XML解析过程就结束了。
protected Object unmarshallField(final UnmarshallingContext context, final Object result, final Class<?> type,
            final Field field) {
        // Converter根据Class的类型转化成对象
        return context.convertAnother(result, type, mapper.getLocalConverter(field.getDeclaringClass(), field
            .getName()));
    }

4、如何提升XStream 解析速度

  1. 减少Converter的查找时间
    我们XML都是JavaBean对象的话,那我们registerConverter时设置为高优先级,这样在查找对应的转换器时就减少了查找的时间。
  2. 减少Mapper和Converter的查找时间
    持久化Mapper查找缓存集合和Converter的查找缓存集合,在初始化时加载这两个集合后再开始解析,。
  3. 使用Xpp3Driver的解析器
    XStream 默认使用的是XppDriver,解析速度要慢于第三版本的Xpp3Driver。原因:第三版本的Xpp3Driver使用了Xpp3 parser,具体原理本文不做分析。

5、自定义Mapper和自定义Converter

demo连接

6、总结

全文通过举XStream的使用例子作为文章开头部分,接着分析了XStream的总体设计,并把XStream分为五部分一一做了介绍。接着详细地介绍了Mapper的原理、Converter的原理、HierarchicalStreamDriver的原理,XML是如何转化成Java对象的,最后根据原理讲了如何提高XStream 解析XML速度。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 01 “我把我的快乐分享给你是希望你也有,而你却觉得我在装逼。” 越来越觉得,快乐分享错了人,就成了显摆。 你运动...
    喵喵苗小姐阅读 490评论 0 1
  • 于士淋 阅读打卡《兔子坡》1~138 这本书讲了一个关于兔子坡的动物与人类和谐相处的故事。兔子坡要有新人家...
    于士淋阅读 608评论 0 0
  • 排毒反应 郁气的外排以烦躁易怒,看谁都不顺眼,悲伤或委屈易哭等情绪变化为主,多伴有打嗝,肛门排气等反应。特别是性格...
    玉和堂传统艾灸阅读 132评论 0 0