聊聊Mybatis的实现原理

使用示例

平时我们使用的一般是集成了Spring或是Spring Boot的Mybatis,封装了一层,看源码不直接;如下,看看原生的Mybatis使用示例

image

示例解析

通过代码可以清晰地看出,MyBatis的操作主要分为两大阶段:

  • 第一阶段:MyBatis初始化阶段。该阶段用来完成MyBatis运行环境的准备工作,读取配置并初始化关键的对象,提供给后续使用,只在 MyBatis启动时运行一次。
  • 第二阶段:数据读写阶段。该阶段由数据读写操作触发,将根据要求完成具体的增、删、改、查等数据库操作。

在第一阶段,最关键的就是SqlSessionFactory对象。在Spring集成Mybatis的源码中,SqlSessionFactoryBean也是做这个事情,读取配置并初始化构建SqlSessionFactory

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
    // Spring Bean的生命周期会调用此方法
    public void afterPropertiesSet() throws Exception {
        this.sqlSessionFactory = this.buildSqlSessionFactory();
    }
    protected SqlSessionFactory buildSqlSessionFactory(){
        // 构建Configuration....
        Configuration configuration;
        if (this.configuration != null) {
            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(), (String)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();
            configuration.setVariables(this.configurationProperties);
        }

        /// 其它code...

        return this.sqlSessionFactoryBuilder.build(configuration);
    }
}

配置文件的解析,最终会生成一个Configuration对象。

private void parseConfiguration(XNode root) {
    try {
        Properties settings = this.settingsAsPropertiess(root.evalNode("settings"));
        this.propertiesElement(root.evalNode("properties"));
        this.loadCustomVfs(settings);
        this.typeAliasesElement(root.evalNode("typeAliases"));
        this.pluginElement(root.evalNode("plugins"));
        this.objectFactoryElement(root.evalNode("objectFactory"));
        this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        this.reflectionFactoryElement(root.evalNode("reflectionFactory"));
        this.settingsElement(settings);
        this.environmentsElement(root.evalNode("environments"));
        this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        this.typeHandlerElement(root.evalNode("typeHandlers"));
        // 解析mappers节点
        this.mapperElement(root.evalNode("mappers"));
    } catch (Exception var3) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
    }
}

前期的准备已就绪,关键的配置已解析且构建并初始化了SqlSessionFactory了。接下来就是创建数据库连接并执行业务的CRUD。在第二阶段的OpenSession方法负责创建并打开数据库链接。

public SqlSession openSession(Connection connection) {
    return this.openSessionFromConnection(this.configuration.getDefaultExecutorType(), connection);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;

    DefaultSqlSession var8;
    try {
        Environment environment = this.configuration.getEnvironment();
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        Executor executor = this.configuration.newExecutor(tx, execType);
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }

    return var8;
}

最后就是调用Mapper接口的业务方法,返回业务数据。

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

// configuration.getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return this.mapperRegistry.getMapper(type, sqlSession);
}

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

// mapperProxyFactory.newInstance()
protected T newInstance(MapperProxy<T> mapperProxy) {
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}

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

最后,打完收工,示例代码所涉及到的关键代码就这些。

反思

上面的示例是比较简单的,那么其实现思路到底是什么样的?首先就有几个问题:

  1. Mapper接口中的方法没有实现,那客户端调用接口的方法时,返回的数据是从哪来的?
  2. Mapper文件与Mapper接口是怎么关联绑定上的?

第一个问题,绝对离不开动态代理,因为只有接口的时候,那么一定会有动态代理生成代理类同时有拦截处理器(InvocationHandler)来增强其执行逻辑。

第二个问题,Mapper文件中有一个<mapper>节点,其namespace就是接口的全限定名称,而其下节点<select>|<update>|..都有一个id值,该值与接口的方法是一致的。因此从这里就可以看出来,业务的crud操作节点是通过namespace+id来对应mapper接口及其方法的。那么我们就可以考虑到,在第一个问题中的拦截处理器执行方法method时,我们就可以通过此关联关系找到要执行的SQL。

如上,这么一分析来看,其实大概的实现思路已经出来了。就是动态代理+<mapper>节点解析实现了接口方法的调用与业务SQL的执行。

因此在第一阶段的解析时,Mapper文件里的<Mapper>节点解析出来的对象就起到了关键的作用。如下,有几个关键的抽象:

MapperRegistry类的knownMappers属性保存着接口及其代理对象的关系。

// type = interface
knownMappers.put(type, new MapperProxyFactory(type));

MappedStatement类对应着<mapper>节点下的CRUD节点。

public void parseStatementNode() {
    String id = this.context.getStringAttribute("id");
    if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        Integer fetchSize = this.context.getIntAttribute("fetchSize");
        Integer timeout = this.context.getIntAttribute("timeout");
        String parameterMap = this.context.getStringAttribute("parameterMap");
        String parameterType = this.context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = this.resolveClass(parameterType);
        String resultMap = this.context.getStringAttribute("resultMap");
        String resultType = this.context.getStringAttribute("resultType");
        // 解析其他属性...
        this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
}

如上是抽象出来整个执行过程的简单流程。实际上还有动态参数绑定与事务等,这些都是在动态代理的拦截处理器中的增强逻辑;下篇再阐述。

本文转载于网络 如有侵权请联系删除

相关文章

  • 收购Slack背后,Salesforce的万亿市值梦

    来源 :我思锅我在 作者:我思锅我在GN 男子网坛流传着一句话:“我们就是要努力打进决赛,然后输给一个叫罗杰·费德勒的人。”这句话也适用于近年来的美国企业服务市场——早期软件公司就是要努力做到行业第一,然后卖给一家叫Salesforce的巨鳄。 12月1日,Salesforce官方宣布以277亿美金收购企业级通讯与协作平台Slack。把这句话化整为零,可以从以下四个角度来拆解这起令人瞩目的“Megadeal”:在疫情和强敌围剿下,Slack的基本面出现了什么问题?Salesforce与Slack合作已久,为什么要发起“收购”?为什么是现在,以及这个价格?以Slack为始,Salesforce的终极梦想究竟是什么?无论我自己还是媒体,总喜欢拿Slack与Zoom比较。以至于让我产生了一个错觉,都受益于疫情的“利好”刺激,两者业绩之所以有如此大差异,是由于产品传播属性、客户获取难度以及竞争态势等影响程度不一。 然而事实上,我犯了一个本末倒置的错误:这次疫情对Slack真的是利好吗?数据显示,并不是。疫情是把锋利的双刃剑尽管取消了电话会,Slack还是如约发布了Q3财报。结合前四季度,我对几

  • 异步 Q-Learning 的样本复杂度:更敏锐的分析和方差减少技术(CS LG)

    异步Q-learning的目的是基于行为策略诱导的马尔科夫样本的单一轨迹,学习马尔科夫决策过程(MDP)的最优行动值函数(或Q-function)。专注于一个具有状态空间\mathcal{S}和行动空间\mathcal{A}的γ折现MDP,我们证明了经典异步Q-learning的基于\ell_{\infty}的样本复杂度--即产生Q-function的入口\varepsilon精确估计所需的样本数--最多只在\begin{equation*}\frac{1}{μ_{\mathsf{min}}(1-γ)^5\varepsilon^2}+\frac{t_{\mathsf{mix}}}{μ_{\mathsf{min}}(1-γ)}\end{equation*}的数量上。在采用适当的恒定学习率的前提下,即可达某个对数系数。这里,t_{\mathsf{mix}}和μ_{\mathsf{min}}分别表示混合时间和样本轨迹的最小状态动作占用概率。该约束的第一项与从轨迹的固定分布中抽取独立样本的情况下的复杂性相匹配。第二项反映了马尔科夫轨迹的经验分布达到稳定状态所需的费用,它在一开始就产生,并随着算

  • 【STM32F429开发板用户手册】第3章 STM32F429整体把控

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255第3章  STM32F429整体把控3.1  初学者重要提示 学习一款新的芯片,优先掌握系统框架是比较重要的,建议逐渐养成这种学习习惯,然后各个击破即可。 本章节提供了多张STM32F429的框图,这些框图都非常具有代表性。很多时候记忆知识点比较费脑子,记录这些框图是一种非常好的方式。 对于本章节提供的部分知识点,无法理解透彻,暂时没有关系。随着后面的深入学习,基本都可以掌握。3.2  STM32F429硬件框图学习一款新的芯片,需要优先了解一下它的整体功能设计。需要的资料主要是来自官网和数据手册,比如我们V6开发板使用的STM32F429BIT6,直接在官方地址:链接(这是超链接)就可以看到对此芯片所做的介绍,页面中有一个如下的框图,对于了解STM32F429整体设计非常方便(当前ST官网显示bug,导致F429系列的框图都被搞丢了,下面先用F407做展示)。再稍微详细点,就需要大家读页面上的”KeyFeatures”,就是下图所示的内容:或者直接看数据

  • redis的多路复用是什么

    为了引出多路复用,我来大胆设想一下技术的发展路程.前提一个应用程序,想对外提供服务,一般都是通过建立套接字监听端口来实现,也就是socket.在这个时候,应用对外提供服务的过程大概是这样.创建套接字绑定端口号开始监听当监听到连接时,调用系统read去读取内容,但是读取操作是阻塞的(也就是说,如果主线程处理read,就不能接收其他连接了,所以只能开新的线程去处理这个事情)画个丑丑的流程图:问题分析这个流程的问题很明显,会不停的创建线程,当然,可以维护一个线程池.但是线程之间的不停切换也是消耗资源的.而且也不可能无限的创建线程.那我如果想一个线程处理呢?从上面流程图能看的出来,问题出在阻塞上面.如果read操作可以立刻返回结果,如果没有读到数据,就可以继续处理后边的事情了.简版整个简单版本.主线程维护一个所有连接的列表,每次循环读取所有列表,有数据就处理,没有就跳过.创建套接字绑定端口号开始监听监听到连接,将连接加到连接列表中,循环读取连接列表中的所有连接,对有数据的进行处理画个丑图:问题分析现在这样处理貌似是比开线程要好一些了,但是事实是这样么?众所周知,其中的read操作是调用系统函数

  • SAP CRM WebClient UI calculated fields的工作原理

  • OSCAR云计算开源产业大会召开——计算无处不在,开源引领未来

    2019年7月3日,OSCAR云计算开源产业大会在北京国际会议中心盛大召开。本次大会主题为“计算无处不在,开源引领未来”,由国信息通信研究院主办,中国IDC圈协办,吸引了开源行业的知名企业、专家和众多从业者参加。会上发布了众多行业研究成果及白皮书。2019OSCAR云计算开源产业大会现场会议开始,工业和信息化部信息化和软件服务业司信息服务业处副处长李琰首先为大会致辞。李琰表示,云计算已经成为提升我国信息化发展水平,打造数字经济发展新动能的重要保障。作为行业主管部门,信息化和软件服务业司将以习近平新时代中国特色社会主义思想为引领,加快探索应用开源技术推动云计算创新发展的新路径,促进云计算和实体经济的深度融合,为网络强国、制造强国的建设作出积极的贡献。同时,李琰也预祝云计算开源产业大会取得圆满成功,为各界分享云计算产业化方面的经验提供良好的平台。也希望到会嘉宾和观众深入研讨,充分交流,利用开源技术推动我国云计算产业的发展,为数字中国、智慧社会建设作出更大的贡献。工业和信息化部信息化和软件服务业司信息服务业处副处长李琰中国银行保险监督管理委员会统信部标准处领导出席并致辞。随后,会议在中国信息

  • 如何检测并清除WMI持久性后门

    当前,WindowsManagementInstrumentation(WMI)事件订阅已经变成了一种非常流行的在端点上建立持久性后门的技术。于是,我决定鼓捣一下Empire的WMI模块,并分析相关的代码,看看能不能清除这些持久化后门。此外,文中还介绍了用于查看和删除WMI事件订阅的一些PowerShell命令。 关于“WindowsManagementInstrumentation事件订阅”的介绍,可以参考MITREATT&CK网站上的相关文章。攻击者可以使用WMI的功能来订阅事件,并在事件发生时执行任意代码,从而在系统上留下持久性后门。WMI是啥?“WMI是微软为基于Web的企业管理(WBEM)规范提供的一个实现版本,而WBEM则是一项行业计划,旨在开发用于访问企业环境中管理信息的标准技术。WMI使用公共信息模型(CIM)行业标准来表示系统、应用程序、网络、设备和其他托管组件。”实际上,所谓事件过滤器只不过就是一个WMI类,用于描述WMI向事件使用者传递的事件。于此同时,事件过滤器还给出了WMI传递事件的条件。配置Sysmon日志记录我们可以将Sysmon配置为记录WmiEvent

  • 深入理解C#3.x的新特性(4):Automatically Implemented Property

    深入理解C#3.x的新特性系列在沉寂一个月之后,今天继续。在本系列前3部分中,我们分别讨论了AnonymousType,ExtensionMethod和LambdaExpression,今天我们来讨论另一个实用的、有意思的Newfeature:AutomaticallyImplementedProperty。一、繁琐的privatefield+publicpropertyDefinition相信大家大家已经习惯通过一个privatefield+publicproperty的发式来定义和实现一个publicProperty。就像下面一个Artech.AutoImpProperty.Point。namespace Artech.AutoImpProperty {     public class Point     {         private double x;         private double y;             public double X         {             get             {                 

  • 腾讯云AI临床助手产品概述

    AI临床助手((AIClinicalAssistant,ACA),基于大数据、语义分析、病历结构化等AI技术,打造集“临床辅助决策、合理用药管理、智能前置审方、智能处方点评”于一体的临床解决方案,覆盖病历质控、用药安全、继续教育等多个应用场景,为临床工作提供智慧支持,为广大基层医院、医联体、综合医院等医疗机构提供智能服务,协助提升医疗服务效率,推动医疗服务标准化。 产品优势精准疾病预测基于病历信息,结合疾病医学图谱与AI算法,精准预测550+种常见病,泛化预测覆盖3000+种疾病,TOP5推荐诊断准确率95.78%。 丰富医学知识库由人民卫生出版社定制化生产,提供临床指南、药品说明书、药品规则库、中药百科、教学视频、医学计算器等丰富的医学知识,满足医生/药师查询了解、学习及提升的需求。 AI技术能力提供病历结果话、语义理解、医学实体提取、术语标准化等技术能力,打造百人医学数据专家团队进行产品研发和迭代,为产品提供夯实的技术能力支持。 丰富审查规则提供病历内涵质控能力和20+维度的用药审查能力,覆盖病历质控、用药安全审核等应用场景,全方位提升医疗服务质量。 灵活部署模式产品支持私有化部署

  • JavaScript-2.0-变量

    变量 变量的作用是将用户的数据进行保存。 变量的本质是程序在内存中申请一块对数据进行存储的空间 变量使用 使用步骤: 声明变量 变量赋值 var是js中的关键字,用来声明变量,计算机自动为变量分配空间,不需要管理员来管理。 <script> //声明一个age变量 varage; age=18;//赋值 console.log(age);//在控制台打印输出 //变量初始化(一步完成) varmyname="史塔克"; sonsole.log(myname); </script> 复制 变量的更新 变量被重新赋值后,值以最后一次赋值的值为结果。 同时声明多个变量 //声明多个变量,用逗号隔开。 varmyname="asd",adress="asd",age=19; 复制 小tips: vara=b=c=9; //相当于vara=9,b=9,c=9,b和c开头没有var,是一个全局变量。 //要想定义多个,中间用逗号隔开。 vara=9,b=0,c=9; 复制 声明变量的特殊情况 只声明不赋值 结果是undefined; 不声明,不赋值,直接用 n

  • PHP-会员登录与注册例子解析-学习笔记

    1.开始    最近开始学习李炎恢老师的《PHP第二季度视频》中的“章节5:使用OOP注册会员”,做一个学习笔记,通过绘制基本页面流程和UML类图,来对加深理解。     2.基本页面流程     3.通过UML类图解析:     4.PHP代码:  我已经把有关这部分PHP代码,上传到git.oschina.net上,可以在 https://git.oschina.net/andywww/myTest的文件夹login1下看到相关的完整代码。   (完.)

  • topcoder SRM 628 DIV2 BishopMove

    题目比较简单。 注意看测试用例2,给的提示 Pleasenotethatthisisthelargestpossiblereturnvalue:wheneverthereisasolution,thereisasolutionthatusesatmosttwomoves. 最多只有两步 #include<vector> #include<string> #include<list> #include<map> #include<set> #include<deque> #include<stack> #include<bitset> #include<algorithm> #include<functional> #include<numeric> #include<utility> #include<sstream> #include<iostream> #include<iomanip> #inclu

  • Delphi 设计模式:《HeadFirst设计模式》Delphi7代码---模板方法模式之CoffeineBeverageWithHook[转]

     模板方法模式定义了一个算法骨架,允许子类对算法的某个或某些步骤进行重写(override)。     1  2{《HeadFirst设计模式》之模板方法模式 }  3{ 编译工具: Delphi7.0              }  4{ E-Mail : guzh-0417@163.com      }  5  6unit uCoffeineBeverageWithHook;  7  8interface  9 10uses 11  SysUtils; 12 1

  • 在网页中单行以及多行内容超出之后隐藏

    1.单行内容超出影藏 overflow:hidden; text-overflow:ellipsis; white-space:nowrap;复制 2.多行内容超出影藏 display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:3; overflow:hidden;复制

  • Android中Socket通信案例

      以下这个案例是基于TCP/UDP协议的。 服务端实现代码 基于TCP的服务端协议//声明一个ServerSocket对象 ServerSocketserverSocket=null; try{ serverSocket=newServerSocket(4567); Socketsocket=serverSocket.accept(); InputStreaminputStream=socket.getInputStream(); }catch(Exceptione){ //TODO:handleexception }复制 基于UDP的服务端协议try{         DatagramSocketsocket=newDatagramSocket(4567); byte[]bytes=newbyte[1024]; DatagramPacketpacket=newDatagramPacket(bytes,bytes.length); socket.receive(packet); }catch(Exceptione){ //TODO:handleexception

  • 配置-yaml简介

    一、配置文件 SpringBoot使用一个全局的配置文件,配置文件名是固定的;   application.properties   application.yml   配置文件的使用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好;   以前的配置文件;大多使用的是xxxx.xml文件; YAML:以数据为中心,比json、xml等更适合做配置文件;     YAML基本语法 k:(空格)v:表示一对键值对(空格必须有); 以空格的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的。 属性和值也是大小写敏感的;   值的写法   字面量:普通的值(数字、字符串、布尔)     k:v:字面量直接来写;       字符串默认不用加上单引号或者双引号;       "":双引号不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思       '':单引号会转义特殊字符   对象、Map(属性和值):     k:v:在下一行来写对象的属性和值的关系,注意缩进       frie

  • 洛谷——P2613 【模板】有理数取余

    P2613【模板】有理数取余   读入优化预处理 $\frac{a}{b}\mod19620817$  也就是$a\timesb^{-1}$ $a\timesb^{-1}\mod19620817=a\timesb^{19620815}\mod19620817$ 除法转化为了乘法,同余的性质。。。 求一个逆元即可,根据费马小定理,由于$19620817$是一个质数 #include<bits/stdc++.h> #defineLLlonglong usingnamespacestd; constLLmod=19260817; voidin(LL&x){ charc=getchar();x=0;intf=1; for(;!isdigit(c);c=getchar())if(c=='-')f=-1; for(;isdigit(c);c=getchar())x=x*10+c-'0',x%=mod; x*=f; } LLa,b; LLpow(LLx,LLy){ LLs=1; for(;y;y>>=1,x=x*x%mod) if(y&am

  • rocketmq linux 安装教程 以及本地连接远程

    rocketmq官网网址:http://rocketmq.apache.org/docs/quick-start/ 准备   linux服务器   操作系统CentOS    1.下载zip到linux系统上(下载二进制包,不要下载资源包)   随便下载一个镜像仓库下载:rocketmq-all-4.7.1-bin-release.zip 2.开始安装   2.1rocketmq是基于JVM运行的,所以要有java环境 java-version查看,没有则需要安装          2.2用unziprocketmq-all-4.7.1-bin-release.zip解压压缩包   2.3 重命名  renamerocketmq-all-4.7.1-bin-release/ rocketmqrocketmq-all-4.7.1-bin-release/ 3.启动   3.1修改日志位置:rocketmq默认的日志位置再${user.home} linux对应的位置在/root/home文件下,修改

  • 6.13---shiro

     

  • HTTP3次握手和4次挥手

    Http的3次握手:   第一次握手:客户端发送一个带SYN的TCP报文到服务器,表示客户端想要和服务器端建立连接。   第二次握手:服务器端接收到客户端的请求,返回客户端报文,这个报文带有SYN和ACK确认标示,访问客户端是否准备好。   第三次握手:客户端再次响应服务端一个ACK确认,表示我已经准备好了。 Http的4次挥手:  第一次挥手:TCP发送一个FIN(结束),用来关闭客户端到服务器端的连接。  第二次挥手:服务器端收到这个FIN后发回一个ACK确认标示,确认收到。  第三次挥手:服务器端发送一个FIN到客户端,服务器端关闭客户端的连接。  第四次挥手:客户端发送ACK报文确认,这样关闭完成。  

  • 各咨询网站

    36kr.com huxiu.com ifanr.com iresearch.cn leiphone.com yixieshi.com geekpark.net tmtpost.com mydrivers.com cnbeta.com chinabyte.com baijia.baidu.com www.heix.cn   AR expar.cn

相关推荐

推荐阅读