MyBatis 源碼分析(二):反射模塊

MyBatis 在進(jìn)行參數(shù)處理、結(jié)果映射時等操作時,會涉及大量的反射操作。為了簡化這些反射相關(guān)操作,MyBatisorg.apache.ibatis.reflection 包下提供了專門的反射模塊,對反射操作做了近一步封裝,提供了更為簡潔的 API。

Reflector

MyBatis 提供 Reflector 類來緩存類的字段名和 getter/setter 方法的元信息,使得反射時有更好的性能。使用方式是將原始類對象傳入其構(gòu)造方法,生成 Reflector 對象。

  public Reflector(Class<?> clazz) {
    type = clazz;
    // 如果存在,記錄無參構(gòu)造方法
    addDefaultConstructor(clazz);
    // 記錄字段名與get方法、get方法返回值的映射關(guān)系
    addGetMethods(clazz);
    // 記錄字段名與set方法、set方法參數(shù)的映射關(guān)系
    addSetMethods(clazz);
    // 針對沒有g(shù)etter/setter方法的字段,通過Filed對象的反射來設(shè)置和讀取字段值
    addFields(clazz);
    // 可讀的字段名
    readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
    // 可寫的字段名
    writablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
    // 保存一份所有字段名大寫與原始字段名的隱射
    for (String propName : readablePropertyNames) {
      caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
    for (String propName : writablePropertyNames) {
      caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
  }

addGetMethodsaddSetMethods 分別獲取類的所有方法,從符合 getter/setter 規(guī)范的方法中解析出字段名,并記錄方法的參數(shù)類型、返回值類型等信息:

  private void addGetMethods(Class<?> cls) {
    // 字段名-get方法
    Map<String, List<Method>> conflictingGetters = new HashMap<>();
    // 獲取類的所有方法,及其實現(xiàn)接口的方法,并根據(jù)方法簽名去重
    Method[] methods = getClassMethods(cls);
    for (Method method : methods) {
      if (method.getParameterTypes().length > 0) {
        // 過濾有參方法
        continue;
      }
      String name = method.getName();
      if ((name.startsWith("get") && name.length() > 3)
          || (name.startsWith("is") && name.length() > 2)) {
        // 由get屬性獲取對應(yīng)的字段名(去除前綴,首字母轉(zhuǎn)小寫)
        name = PropertyNamer.methodToProperty(name);
        addMethodConflict(conflictingGetters, name, method);
      }
    }
    // 保證每個字段只對應(yīng)一個get方法
    resolveGetterConflicts(conflictingGetters);
  }

getter/setter 方法進(jìn)行去重是通過類似 java.lang.String#getSignature:java.lang.reflect.Method 的方法簽名來實現(xiàn)的,如果子類在實現(xiàn)過程中,參數(shù)、返回值使用了不同的類型(使用原類型的子類),則會導(dǎo)致方法簽名不一致,同一字段就會對應(yīng)不同的 getter/setter 方法,因此需要進(jìn)行去重。

  private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
    for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
      Method winner = null;
      // 屬性名
      String propName = entry.getKey();
      for (Method candidate : entry.getValue()) {
        if (winner == null) {
          winner = candidate;
          continue;
        }
        // 字段對應(yīng)了多個get方法
        Class<?> winnerType = winner.getReturnType();
        Class<?> candidateType = candidate.getReturnType();
        if (candidateType.equals(winnerType)) {
          // 返回值類型相同
          if (!boolean.class.equals(candidateType)) {
            throw new ReflectionException(
                "Illegal overloaded getter method with ambiguous type for property "
                    + propName + " in class " + winner.getDeclaringClass()
                    + ". This breaks the JavaBeans specification and can cause unpredictable results.");
          } else if (candidate.getName().startsWith("is")) {
            // 返回值為boolean的get方法可能有多個,如getIsSave和isSave,優(yōu)先取is開頭的
            winner = candidate;
          }
        } else if (candidateType.isAssignableFrom(winnerType)) {
          // OK getter type is descendant
          // 可能會出現(xiàn)接口中的方法返回值是List,子類實現(xiàn)方法返回值是ArrayList,使用子類返回值方法
        } else if (winnerType.isAssignableFrom(candidateType)) {
          winner = candidate;
        } else {
          throw new ReflectionException(
              "Illegal overloaded getter method with ambiguous type for property "
                  + propName + " in class " + winner.getDeclaringClass()
                  + ". This breaks the JavaBeans specification and can cause unpredictable results.");
        }
      }
      // 記錄字段名對應(yīng)的get方法對象和返回值類型
      addGetMethod(propName, winner);
    }
  }

去重的方式是使用更規(guī)范的方法以及使用子類的方法。在確認(rèn)字段名對應(yīng)的唯一 getter/setter 方法后,記錄方法名對應(yīng)的方法、參數(shù)、返回值等信息。MethodInvoker 可用于調(diào)用 Method 類的 invoke 方法來執(zhí)行 getter/setter 方法(addSetMethods 記錄映射關(guān)系的方式與 addGetMethods 大致相同)。

private void addGetMethod(String name, Method method) {
  // 過濾$開頭、serialVersionUID的get方法和getClass()方法
  if (isValidPropertyName(name)) {
    // 字段名-對應(yīng)get方法的MethodInvoker對象
    getMethods.put(name, new MethodInvoker(method));
    Type returnType = TypeParameterResolver.resolveReturnType(method, type);
    // 字段名-運(yùn)行時方法的真正返回類型
    getTypes.put(name, typeToClass(returnType));
  }
}

接下來會執(zhí)行 addFields 方法,此方法針對沒有 getter/setter 方法的字段,通過包裝為 SetFieldInvoker 在需要時通過 Field 對象的反射來設(shè)置和讀取字段值。

private void addFields(Class<?> clazz) {
  Field[] fields = clazz.getDeclaredFields();
  for (Field field : fields) {
    if (!setMethods.containsKey(field.getName())) {
      // issue #379 - removed the check for final because JDK 1.5 allows
      // modification of final fields through reflection (JSR-133). (JGB)
      // pr #16 - final static can only be set by the classloader
      int modifiers = field.getModifiers();
      if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) {
        // 非final的static變量,沒有set方法,可以通過File對象做賦值操作
        addSetField(field);
      }
    }
    if (!getMethods.containsKey(field.getName())) {
      addGetField(field);
    }
  }
  if (clazz.getSuperclass() != null) {
    // 遞歸查找父類
    addFields(clazz.getSuperclass());
  }
}

Invoker

Invoker 接口用于抽象設(shè)置和讀取字段值的操作。對于有 getter/setter 方法的字段,通過 MethodInvoker 反射執(zhí)行;對應(yīng)其它字段,通過 GetFieldInvokerSetFieldInvoker 操作 Field 對象的 getter/setter 方法反射執(zhí)行。

/**
 * 用于抽象設(shè)置和讀取字段值的操作
 *
 * {@link MethodInvoker} 反射執(zhí)行g(shù)etter/setter方法
 * {@link GetFieldInvoker} {@link SetFieldInvoker} 反射執(zhí)行Field對象的get/set方法
 *
 * @author Clinton Begin
 */
public interface Invoker {

  /**
   * 通過反射設(shè)置或讀取字段值
   *
   * @param target
   * @param args
   * @return
   * @throws IllegalAccessException
   * @throws InvocationTargetException
   */
  Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException;

  /**
   * 字段類型
   *
   * @return
   */
  Class<?> getType();
}

TypeParameterResolver

針對 Java-Type 體系的多種實現(xiàn),TypeParameterResolver 提供一系列方法來解析指定類中的字段、方法返回值或方法參數(shù)的類型。

Type 接口包含 4 個子接口和 1 個實現(xiàn)類:

Type接口
  • Class:原始類型
  • ParameterizedType:泛型類型,如:List<String>
  • TypeVariable:泛型類型變量,如: List<T> 中的 T
  • GenericArrayType:組成元素是 ParameterizedTypeTypeVariable 的數(shù)組類型,如:List<String>[]、T[]
  • WildcardType:通配符泛型類型變量,如:List<?> 中的 ?

TypeParameterResolver 分別提供 resolveFieldType、resolveReturnType、resolveParamTypes 方法用于解析字段類型、方法返回值類型和方法入?yún)㈩愋?,這些方法均調(diào)用 resolveType 來獲取類型信息:

/**
 * 獲取類型信息
 *
 * @param type 根據(jù)是否有泛型信息簽名選擇傳入泛型類型或簡單類型
 * @param srcType 引用字段/方法的類(可能是子類,字段和方法在父類聲明)
 * @param declaringClass 字段/方法聲明的類
 * @return
 */
private static Type resolveType(Type type, Type srcType, Class<?> declaringClass) {
  if (type instanceof TypeVariable) {
    // 泛型類型變量,如:List<T> 中的 T
    return resolveTypeVar((TypeVariable<?>) type, srcType, declaringClass);
  } else if (type instanceof ParameterizedType) {
    // 泛型類型,如:List<String>
    return resolveParameterizedType((ParameterizedType) type, srcType, declaringClass);
  } else if (type instanceof GenericArrayType) {
    // TypeVariable/ParameterizedType 數(shù)組類型
    return resolveGenericArrayType((GenericArrayType) type, srcType, declaringClass);
  } else {
    // 原始類型,直接返回
    return type;
  }
}

resolveTypeVar 用于解析泛型類型變量參數(shù)類型,如果字段或方法在當(dāng)前類中聲明,則返回泛型類型的上界或 Object 類型;如果在父類中聲明,則遞歸解析父類;父類也無法解析,則遞歸解析實現(xiàn)的接口。

private static Type resolveTypeVar(TypeVariable<?> typeVar, Type srcType, Class<?> declaringClass) {
  Type result;
  Class<?> clazz;
  if (srcType instanceof Class) {
    // 原始類型
    clazz = (Class<?>) srcType;
  } else if (srcType instanceof ParameterizedType) {
    // 泛型類型,如 TestObj<String>
    ParameterizedType parameterizedType = (ParameterizedType) srcType;
    // 取原始類型TestObj
    clazz = (Class<?>) parameterizedType.getRawType();
  } else {
    throw new IllegalArgumentException("The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass());
  }

  if (clazz == declaringClass) {
    // 字段就是在當(dāng)前引用類中聲明的
    Type[] bounds = typeVar.getBounds();
    if (bounds.length > 0) {
      // 返回泛型類型變量上界,如:T extends String,則返回String
      return bounds[0];
    }
    // 沒有上界返回Object
    return Object.class;
  }

  // 字段/方法在父類中聲明,遞歸查找父類泛型
  Type superclass = clazz.getGenericSuperclass();
  result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superclass);
  if (result != null) {
    return result;
  }

  // 遞歸泛型接口
  Type[] superInterfaces = clazz.getGenericInterfaces();
  for (Type superInterface : superInterfaces) {
    result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superInterface);
    if (result != null) {
      return result;
    }
  }
  return Object.class;
}

通過調(diào)用 scanSuperTypes 實現(xiàn)遞歸解析:

private static Type scanSuperTypes(TypeVariable<?> typeVar, Type srcType, Class<?> declaringClass, Class<?> clazz, Type superclass) {
  if (superclass instanceof ParameterizedType) {
    // 父類是泛型類型
    ParameterizedType parentAsType = (ParameterizedType) superclass;
    Class<?> parentAsClass = (Class<?>) parentAsType.getRawType();
    // 父類中的泛型類型變量集合
    TypeVariable<?>[] parentTypeVars = parentAsClass.getTypeParameters();
    if (srcType instanceof ParameterizedType) {
      // 子類可能對父類泛型變量做過替換,使用替換后的類型
      parentAsType = translateParentTypeVars((ParameterizedType) srcType, clazz, parentAsType);
    }
    if (declaringClass == parentAsClass) {
      // 字段/方法在當(dāng)前父類中聲明
      for (int i = 0; i < parentTypeVars.length; i++) {
        if (typeVar == parentTypeVars[i]) {
          // 使用變量對應(yīng)位置的真正類型(可能已經(jīng)被替換),如父類 A<T>,子類 B extends A<String>,則返回String
          return parentAsType.getActualTypeArguments()[i];
        }
      }
    }
    // 字段/方法聲明的類是當(dāng)前父類的父類,繼續(xù)遞歸
    if (declaringClass.isAssignableFrom(parentAsClass)) {
      return resolveTypeVar(typeVar, parentAsType, declaringClass);
    }
  } else if (superclass instanceof Class && declaringClass.isAssignableFrom((Class<?>) superclass)) {
    // 父類是原始類型,繼續(xù)遞歸父類
    return resolveTypeVar(typeVar, superclass, declaringClass);
  }
  return null;
}

解析方法返回值和方法參數(shù)的邏輯大致與解析字段類型相同,MyBatis 源碼的TypeParameterResolverTest 類提供了相關(guān)的測試用例。

ReflectorFactory

MyBatis 還提供 ReflectorFactory 接口用于實現(xiàn) Reflector 容器,其默認(rèn)實現(xiàn)為 DefaultReflectorFactory,其中可以使用 classCacheEnabled 屬性來配置是否使用緩存。

public class DefaultReflectorFactory implements ReflectorFactory {

  /**
   * 是否緩存Reflector類信息
   */
  private boolean classCacheEnabled = true;

  /**
   * Reflector緩存容器
   */
  private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<>();

  public DefaultReflectorFactory() {
  }

  @Override
  public boolean isClassCacheEnabled() {
    return classCacheEnabled;
  }

  @Override
  public void setClassCacheEnabled(boolean classCacheEnabled) {
    this.classCacheEnabled = classCacheEnabled;
  }

  /**
   * 獲取類的Reflector信息
   *
   * @param type
   * @return
   */
  @Override
  public Reflector findForClass(Class<?> type) {
    if (classCacheEnabled) {
      // synchronized (type) removed see issue #461
      // 如果緩存Reflector信息,放入緩存容器
      return reflectorMap.computeIfAbsent(type, Reflector::new);
    } else {
      return new Reflector(type);
    }
  }

}

ObjectFactory

ObjectFactory 接口是 MyBatis 對象創(chuàng)建工廠,其默認(rèn)實現(xiàn) DefaultObjectFactory 通過構(gòu)造器反射創(chuàng)建對象,支持使用無參構(gòu)造器和有參構(gòu)造器。

Property 工具集

MyBatis 在映射文件定義 resultMap 支持如下形式:

<resultMap id="map" type="Order">
    <result property="orders[0].items[0].name" column="col1"/>
    <result property="orders[0].items[1].name" column="col2"/>
    ...
</resultMap>

orders[0].items[0].name 這樣的表達(dá)式是由 PropertyTokenizer 解析的,其構(gòu)造方法能夠?qū)Ρ磉_(dá)式進(jìn)行解析;同時還實現(xiàn)了 Iterator 接口,能夠迭代解析表達(dá)式。

public PropertyTokenizer(String fullname) {
  // orders[0].items[0].name
  int delim = fullname.indexOf('.');
  if (delim > -1) {
    // name = orders[0]
    name = fullname.substring(0, delim);
    // children = items[0].name
    children = fullname.substring(delim + 1);
  } else {
    name = fullname;
    children = null;
  }
  // orders[0]
  indexedName = name;
  delim = name.indexOf('[');
  if (delim > -1) {
    // 0
    index = name.substring(delim + 1, name.length() - 1);
    // order
    name = name.substring(0, delim);
  }
}

  /**
   * 是否有children表達(dá)式繼續(xù)迭代
   *
   * @return
   */
  @Override
  public boolean hasNext() {
    return children != null;
  }

  /**
   * 分解出的 . 分隔符的 children 表達(dá)式可以繼續(xù)迭代
   * @return
   */
  @Override
  public PropertyTokenizer next() {
    return new PropertyTokenizer(children);
  }

PropertyNamer 可以根據(jù) getter/setter 規(guī)范解析字段名稱;PropertyCopier 則支持對有相同父類的對象,通過反射拷貝字段值。

MetaClass

MetaClass 類依賴 PropertyTokenizerReflector 查找表達(dá)式是否可以匹配 Java 對象中的字段,以及對應(yīng)字段是否有 getter/setter 方法。

/**
 * 驗證傳入的表達(dá)式,是否存在指定的字段
 *
 * @param name
 * @param builder
 * @return
 */
private StringBuilder buildProperty(String name, StringBuilder builder) {
  // 映射文件表達(dá)式迭代器
  PropertyTokenizer prop = new PropertyTokenizer(name);
  if (prop.hasNext()) {
    // 復(fù)雜表達(dá)式,如name = items[0].name,則prop.getName() = items
    String propertyName = reflector.findPropertyName(prop.getName());
    if (propertyName != null) {
      builder.append(propertyName);
      // items.
      builder.append(".");
      // 加載內(nèi)嵌字段類型對應(yīng)的MetaClass
      MetaClass metaProp = metaClassForProperty(propertyName);
      // 迭代子字段
      metaProp.buildProperty(prop.getChildren(), builder);
    }
  } else {
    // 非復(fù)雜表達(dá)式,獲取字段名,如:userid->userId
    String propertyName = reflector.findPropertyName(name);
    if (propertyName != null) {
      builder.append(propertyName);
    }
  }
  return builder;
}

MetaObject 與 ObjectWrapper

相對于 MetaClass 關(guān)注類信息,MetalObject 關(guān)注的是對象的信息,除了保存?zhèn)魅氲膶ο蟊旧恚€會為對象指定一個 ObjectWrapper 將對象包裝起來。ObejctWrapper 體系如下:

ObjectWrapper體系

ObjectWrapper 的默認(rèn)實現(xiàn)包括了對 MapCollection 和普通 JavaBean 的包裝。MyBatis 還支持通過 ObjectWrapperFactory 接口對 ObejctWrapper 進(jìn)行擴(kuò)展,生成自定義的包裝類。MetaObject 對對象的具體操作,就委托給真正的 ObjectWrapper 處理。

private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
  this.originalObject = object;
  this.objectFactory = objectFactory;
  this.objectWrapperFactory = objectWrapperFactory;
  this.reflectorFactory = reflectorFactory;

  // 根據(jù)傳入object類型不同,指定不同的wrapper
  if (object instanceof ObjectWrapper) {
    this.objectWrapper = (ObjectWrapper) object;
  } else if (objectWrapperFactory.hasWrapperFor(object)) {
    this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
  } else if (object instanceof Map) {
    this.objectWrapper = new MapWrapper(this, (Map) object);
  } else if (object instanceof Collection) {
    this.objectWrapper = new CollectionWrapper(this, (Collection) object);
  } else {
    this.objectWrapper = new BeanWrapper(this, object);
  }
}

例如賦值操作,BeanWrapper 的實現(xiàn)如下:

  @Override
  public void set(PropertyTokenizer prop, Object value) {
    if (prop.getIndex() != null) {
      // 當(dāng)前表達(dá)式是集合,如:items[0],就需要獲取items集合對象
      Object collection = resolveCollection(prop, object);
      // 在集合的指定索引上賦值
      setCollectionValue(prop, collection, value);
    } else {
      // 解析完成,通過Invoker接口做賦值操作
      setBeanProperty(prop, object, value);
    }
  }

  protected Object resolveCollection(PropertyTokenizer prop, Object object) {
    if ("".equals(prop.getName())) {
      return object;
    } else {
      // 在對象信息中查到此字段對應(yīng)的集合對象
      return metaObject.getValue(prop.getName());
    }
  }

根據(jù) PropertyTokenizer 對象解析出的當(dāng)前字段是否存在 index 索引來判斷字段是否為集合。如果當(dāng)前字段對應(yīng)集合,則需要在對象信息中查到此字段對應(yīng)的集合對象:

public Object getValue(String name) {
  PropertyTokenizer prop = new PropertyTokenizer(name);
  if (prop.hasNext()) {
    // 如果表達(dá)式仍可迭代,遞歸尋找字段對應(yīng)的對象
    MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
    if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
      return null;
    } else {
      return metaValue.getValue(prop.getChildren());
    }
  } else {
      // 字段解析完成
    return objectWrapper.get(prop);
  }
}

如果字段是簡單類型,BeanWrapper 獲取字段對應(yīng)的對象邏輯如下:

@Override
public Object get(PropertyTokenizer prop) {
  if (prop.getIndex() != null) {
    // 集合類型,遞歸獲取
    Object collection = resolveCollection(prop, object);
    return getCollectionValue(prop, collection);
  } else {
    // 解析完成,反射讀取
    return getBeanProperty(prop, object);
  }
}

可以看到,仍然是會判斷表達(dá)式是否迭代完成,如果未解析完字段會不斷遞歸,直至找到對應(yīng)的類型。前面說到 Reflector 創(chuàng)建過程中將對字段的讀取和賦值操作通過 Invoke 接口抽象出來,針對最終獲取的字段,此時就會調(diào)用 Invoke 接口對字段反射讀取對象值:

/**
 * 通過Invoker接口反射執(zhí)行讀取操作
 *
 * @param prop
 * @param object
 */
private Object getBeanProperty(PropertyTokenizer prop, Object object) {
  try {
    Invoker method = metaClass.getGetInvoker(prop.getName());
    try {
      return method.invoke(object, NO_ARGUMENTS);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  } catch (RuntimeException e) {
    throw e;
  } catch (Throwable t) {
    throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ".  Cause: " + t.toString(), t);
  }
}

對象讀取完畢再通過 setCollectionValue 方法對集合指定索引進(jìn)行賦值或通過 setBeanProperty 方法對簡單類型反射賦值。MapWrapper 的操作與 BeanWrapper 大致相同,CollectionWrapper 相對更會簡單,只支持對原始集合對象進(jìn)行添加操作。

小結(jié)

MyBatis 根據(jù)自身需求,對反射 API 做了近一步封裝。其目的是簡化反射操作,為對象字段的讀取和賦值提供更好的性能。

  • org.apache.ibatis.reflection.Reflector:緩存類的字段名和 getter/setter 方法的元信息,使得反射時有更好的性能。
  • org.apache.ibatis.reflection.invoker.Invoker::用于抽象設(shè)置和讀取字段值的操作。
  • org.apache.ibatis.reflection.TypeParameterResolver:針對 Java-Type 體系的多種實現(xiàn),解析指定類中的字段、方法返回值或方法參數(shù)的類型。
  • org.apache.ibatis.reflection.DefaultReflectorFactory:默認(rèn)的 Reflector 創(chuàng)建工廠。
  • org.apache.ibatis.reflection.factory.ObjectFactory:MyBatis 對象創(chuàng)建工廠,其默認(rèn)實現(xiàn) DefaultObjectFactory 通過構(gòu)造器反射創(chuàng)建對象。
  • org.apache.ibatis.reflection.property:property 工具包,針對映射文件表達(dá)式進(jìn)行解析和 Java 對象的反射賦值。
  • org.apache.ibatis.reflection.MetaClass:依賴 PropertyTokenizer 和 Reflector 查找表達(dá)式是否可以匹配 Java 對象中的字段,以及對應(yīng)字段是否有 getter/setter 方法。
  • org.apache.ibatis.reflection.MetaObject:對原始對象進(jìn)行封裝,將對象操作委托給 ObjectWrapper 處理。
  • org.apache.ibatis.reflection.wrapper.ObjectWrapper:對象包裝類,封裝對象的讀取和賦值等操作。

注釋源碼

注釋源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容