MyBatis 源碼分析

一、學習之前思考

在學習mybatis之前,我是首先帶着三個疑問去學習mybatis:

(1)數據庫鏈接怎樣管理:與數據庫鏈接的線程池創建,sql執行等(個人理解再牛逼的orm框架,最後都需要轉成

mysql(本次實例中是使用mysql數據)的原生的sql語句,最後執行的肯定是sql執行語句)

(2)java的dao層怎樣操作sql語句:一個dao接口,爲什麼可以直接執行sql語句,

(3)數據結果是怎樣封裝的

由上述三個疑問,總結下面的mybatis的orm框架大體圖:

二、結構化分析

由上述的描述,總結了下面的mybatis的需要實現的功能的結構化圖:

三、DAO層容器

 

2.1、dao層注入

(1)怎樣獲取dao??

通過註解@MapperScan獲取掃描到的包,而真正去處理的是MapperScannerRegistrar。

(2)MapperScannerRegistrar是怎樣處理掃描到的dao接口的??

MapperScannerRegistrar 主要處理過程核心方法:registerBeanDefinitions

MapperScannerRegistrar.registerBeanDefinitions:

@Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    //獲取註解的屬性
    AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    //通過spring上下文實例化一個掃描器
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    // 非空判斷,設置resourceLoader,3.1之後不需要檢測
    if (resourceLoader != null) {
      scanner.setResourceLoader(resourceLoader);
    }
    //Annotation 註解類設置
    Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
    if (!Annotation.class.equals(annotationClass)) {
      scanner.setAnnotationClass(annotationClass);
    }
    //接口標記類加載
    Class<?> markerInterface = annoAttrs.getClass("markerInterface");
    if (!Class.class.equals(markerInterface)) {
      scanner.setMarkerInterface(markerInterface);
    }
    //bean name自動生成類加載
    Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
    if (!BeanNameGenerator.class.equals(generatorClass)) {
      scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
    }
    //MapperFactoryBean類加載-----這個就是代理dao的BeanFactory
    Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
    if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
      scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
    }
    //設置掃描器數據庫管理模板和sqlSessionFactory
    scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
    scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));

    List<String> basePackages = new ArrayList<String>();
    for (String pkg : annoAttrs.getStringArray("value")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (String pkg : annoAttrs.getStringArray("basePackages")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) {
      basePackages.add(ClassUtils.getPackageName(clazz));
    }
    scanner.registerFilters();
    scanner.doScan(StringUtils.toStringArray(basePackages));//核心方法,具體掃描所做的邏輯都在此方法中
  }

ClassPathMapperScanner.doScan:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
		for (String basePackage : basePackages) {
      //掃描包,由於包掃描得到了所有的class,去掉一些非spring config的類,得到ScannedGenericBeanDefinition類型的BeanDefinition
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {
        //獲取需要註解的bean的生命週期,如:singleton
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
        //設置bean的生命週期
				candidate.setScope(scopeMetadata.getScopeName());
        //bean名字自動初始化
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
				if (candidate instanceof AbstractBeanDefinition) {
          //如果是繼承抽象的AbstractBeanDefinition,在進一步初始化,此處初始化主要是爲了設置默認值
					postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
				}
        //同上一步一樣,如果該bean繼承AnnotatedBeanDefinition接口,設置默認初始值
				if (candidate instanceof AnnotatedBeanDefinition) {
					AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
				}
				if (checkCandidate(beanName, candidate)) {
          //賦給bean定的持有者
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
          //判斷當前的bean對應scope註解的那種形式,mybatis 沒有使用任何形式故結果返回的還是definitionHolder
					definitionHolder =
							AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);
          //註冊bean
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
		return beanDefinitions;
	}

ClassPathMapperScanner.processBeanDefinitions:


  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;//mapperFactoryBean 註冊的是此bean,最終我們每個dao都轉換成mapperFactoryBean,注意一個dao對應一個mapperFactoryBean
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();
      if (logger.isDebugEnabled()) {
        logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
          + "' and '" + definition.getBeanClassName() + "' mapperInterface");
      }
      //definition設置參數,即被代理的bean,查看mapperFactoryBean構造器可以看到,只有一個參數mapperInterface------代理bean構造器初始化
      definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); 
      definition.setBeanClass(this.mapperFactoryBean.getClass());//設置beanClass,這個值代理對象bean
      //代理添加參數addToConfig
      definition.getPropertyValues().add("addToConfig", this.addToConfig);
      boolean explicitFactoryUsed = false;
      if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
        //如果當前的被代理的bean所指定的sqlSessionFactory有名字,將當前sqlSessionFactory作爲參數添加到代理bean中
        definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionFactory != null) {
        //爲空直接使用當前的sqlSessionFactory
        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
        explicitFactoryUsed = true;
      }
      //同sqlSessionFactory一樣原理
      if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionTemplate != null) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
        explicitFactoryUsed = true;
      }

      if (!explicitFactoryUsed) {
        if (logger.isDebugEnabled()) {
          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        }
        //設置根據類型自動注入
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);//這行代碼,,,,根據type實例化的,,,配置多數據源的時候這個地方就會出問題
      }
    }
  }

(3)總結

一句話:dao層注入,dao接口->definition->BeanDefinitionHolder->MapperFactoryBean,是通過MapperFactoryBean來代理bean的

2.2、xml與dao綁定

上面我們已經注入了dao的MapperFactoryBean代理bean,每一個dao都有一個對應的MapperFactoryBean代理對象,現在就是想知道xml或者sql語句怎麼和MapperFactoryBean聯繫在一起的。

2.2.1、xml文件掃面映射

由於掃描xml文件是在sqlSessionFactory中掃描的,所以我是先從sqlSessionFactory 的bean注入代碼去了解的:

javaconfig的SqlSessionFactory:

@Bean(name="sqlSessionFactory1")
    @Primary
    public SqlSessionFactory sqlSessionFactory1() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(testOne);
        List<Resource> resources=new ArrayList<>();
        resources.addAll(Arrays.asList(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/*/person.xml")));
        factoryBean.setMapperLocations(resources.toArray(new Resource[resources.size()]));
        return factoryBean.getObject();
    }

核心方法SqlSessionFactoryBean.buildSqlSessionFactory:

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
    Configuration configuration;//整個SqlSessionFactory的核心,沒有他SqlSessionFactory就沒法去往後操作的,主要是保存SqlSessionFactory的一些配置信息
    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {//如果不爲空直接解析xml文件
      configuration = this.configuration;
      if (configuration.getVariables() == null) {
        configuration.setVariables(this.configurationProperties);//這個不知道什麼參數可以在這裏設置,你要是懂,可留言告知我
      } else if (this.configurationProperties != null) {
        configuration.getVariables().putAll(this.configurationProperties);
      }
    } else if (this.configLocation != null) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }

    if (this.objectFactory != null) {
      configuration.setObjectFactory(this.objectFactory);
    }

    if (this.objectWrapperFactory != null) {
      configuration.setObjectWrapperFactory(this.objectWrapperFactory);
    }

    if (this.vfs != null) {
      configuration.setVfsImpl(this.vfs);
    }

    if (hasLength(this.typeAliasesPackage)) {
      String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeAliasPackageArray) {
        configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
        }
      }
    }

    if (!isEmpty(this.typeAliases)) {
      for (Class<?> typeAlias : this.typeAliases) {
        configuration.getTypeAliasRegistry().registerAlias(typeAlias);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered type alias: '" + typeAlias + "'");
        }
      }
    }

    if (!isEmpty(this.plugins)) {
      for (Interceptor plugin : this.plugins) {
        configuration.addInterceptor(plugin);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered plugin: '" + plugin + "'");
        }
      }
    }

    if (hasLength(this.typeHandlersPackage)) {
      String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeHandlersPackageArray) {
        configuration.getTypeHandlerRegistry().register(packageToScan);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
        }
      }
    }

    if (!isEmpty(this.typeHandlers)) {
      for (TypeHandler<?> typeHandler : this.typeHandlers) {
        configuration.getTypeHandlerRegistry().register(typeHandler);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered type handler: '" + typeHandler + "'");
        }
      }
    }

    if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
      try {
        configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
      } catch (SQLException e) {
        throw new NestedIOException("Failed getting a databaseId", e);
      }
    }

    if (this.cache != null) {
      configuration.addCache(this.cache);
    }

    if (xmlConfigBuilder != null) {
      try {
        xmlConfigBuilder.parse();

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
        }
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }

    if (this.transactionFactory == null) {
      this.transactionFactory = new SpringManagedTransactionFactory();
    }

    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());//此處是重點,解析了sql的xml文件
          xmlMapperBuilder.parse();//解析xml文件,注意,這裏解析返回的會有72中mybatis已經規定好的類型別名(我們在定義別名的時候注意不要重複)
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }

    return this.sqlSessionFactoryBuilder.build(configuration);
  }

真正解析類:XMLMapperBuilder.parse()

 public void parse() {
    if (!configuration.isResourceLoaded(resource)) {//資源如果未加載,開始解析
      configurationElement(parser.evalNode("/mapper"));//解析mapper節點---對應xml文件<mapper></mapper>
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();//命名空間綁定:namespace="bootdemo.dao.db1.PersonDaoOne",此處會生成一個對應的MapperProxyFactory 來代理MapperProxyFactoryBean
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

xml解析方法實現:XMLMapperBuilder.configurationElement

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");//加載命名空間
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);//設置命名空間名稱---留個疑問:爲什麼命名空間必須是dao類路徑
      cacheRefElement(context.evalNode("cache-ref"));//mapper緩存設置
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));//xml文件定的參數map,對應xml標籤:<parameterMap></parameterMap>
      resultMapElements(context.evalNodes("/mapper/resultMap"));//xml文件定的結果DTO對應map,對應xml標籤:<resultMap></resultMap>
      sqlElement(context.evalNodes("/mapper/sql"));//xml文件定的結果sql標籤,對應xml標籤:<sql></sql>
      //增刪改語句解析,增刪改對應的映射類爲:XMLStatementBuilder,解析成的XMLStatementBuilder放在configuration中
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

生成MapperProxyFactory:XMLMapperBuilder.bindMapperForNamespace----實際上xml文件是由MapperRegissry來映射的

 private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          configuration.addLoadedResource("namespace:" + namespace);//空間命名添加到容器中
          configuration.addMapper(boundType);//將MapperProxyFactory 代理類添加到容器中
        }
      }
    }
  }

//添加MapperProxyFactory代理的核心代碼
 public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

總結:基於上述的代碼,我們已經將整個的xml配置文件解析到configuration,並賦給SqlSessionFactory屬中的屬性:xml->configuration->SqlSessionFactory

四、調用方怎樣調用

由第二部分介紹了,一個dao層怎麼變成一個bean實例的(MapperFactoryBean),第三部分介紹了怎樣xml文件怎樣

生成java實例(MapperRegissry),接下來就是怎麼將兩者聯繫在一起和怎樣去執行方法的。

4.1、xml和注入的dao bean關聯

java的類dao生成了MapperFactoryBean實例,從MapperFactoryBean源碼可以看到MapperFactoryBean繼承了FactoryBean,所以他是一個Bean工廠類,我們可以看到他的bean獲取實例方法getObject()。

MapperFactoryBean.getObject():獲取實例對象

@Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

對於採用SqlSessionTemplate模板管理的getMapper方法:

public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
  }

MapperRegistry.getMapper():

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

protected T newInstance(MapperProxy<T> mapperProxy) {
    //MapperProxy 又是一個代理對象,最終我們生成的bean是由MapperProxy來代理的
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

總結:MapperFactoryBean.getObject()->SqlSession.getMapper()->MapperRegistry.getMapper()->MapperProxyFactory.newInstance()->MapperProxy代理,最終獲取到實例

4.2、MapperProxy怎樣去執行dao中的方法

首先看MapperProxy代理類的invoke是怎樣執行的方法的。

MapperProxy.invoke():

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //如果直接實現代理類,直接運行方法
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        //如果直接實現代理類,直接運行方法----據說是JDK 1.8屬性
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //獲取需要執行的方法mapper,從Configuration容器中獲取到對應的MapperMethod:
    //MapperMethod.SqlCommand 靜態類,其構造器有個方法resolveMappedStatement爲後去mapper 接口
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);//真正執行的接口
  }

sql執行接口MapperMethod.execute():

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
    	Object param = method.convertArgsToSqlCommandParam(args);//參數封裝
        result = rowCountResult(sqlSession.insert(command.getName(), param));//執行添加方法
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);//參數封裝
        result = rowCountResult(sqlSession.update(command.getName(), param));//執行修改方法
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);//參數封裝
        result = rowCountResult(sqlSession.delete(command.getName(), param));//執行刪除語句
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

以查詢爲例,真正查詢的接sql:BaseExecutor.query()

@Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();//執行查詢
    return resultSetHandler.<E> handleResultSets(ps);//結果封裝到dto中
  }

有啥疑問請指出,聯繫方式qq:158479841

拒絕轉載!!!!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章