IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Mybatis原理 -> 正文阅读

[Java知识库]Mybatis原理

文章目录

- 什么是Mybatis?

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。
1、Mybatis是一个半ORM(对象关系映射)框架,底层封装了JDBC,是程序员在开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。使得程序员可以花更多的精力放到业务开发中。另外,程序员直接编写原生态sql,严格控制sql执行性能,灵活度高。
2、MyBatis 可以使用简单的 XML文件注解方式配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
3、通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。

- Mybaits的优点:

1、基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用
2、与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
3、很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
4、能够与Spring很好的集成;
5、提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

- MyBatis框架的缺点:

1、SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
2、SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

- MyBatis与Hibernate有哪些不同?

1、Mybatis是一个半自动的ORM框架,在查询关联对象或关联集合对象时,需要手动编写sql语句来完成;Hibernate是全自动ORM映射工具,查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,不需要编写sql.
2、Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对性能要求高,需求变化频繁的项目;但是如果涉及到较多的字段或者关联多表时,sql语句编写量大且对开发人的sql语句编写功底要求高。
3、Hibernate对象/关系映射能力强,数据库无关性好,适合需求变化不大的项目,使用hibernate开发可以节省很多代码,提高效率。

MyBatisHibernate
半自动自动
灵活度高灵活度低
代码量大代码量小

- 架构

我们把Mybatis的功能架构分为三层:
(1)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
(2)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
(3)基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
功能架构三层
运行流程
Mybatis层次结构

1、SqlSessionFactoryBuilder【全局的对象】
每一个MyBatis的应用程序的入口是:SqlSessionFactoryBuilder。它的作用是通过XML配置文件创建Configuration对象(当然也可以在程序中自行创建),然后通过build方法创建SqlSessionFactory对象。

2、SqlSessionFactory【全局的对象】
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory是由SqlSessionFactoryBuilder 从 XML 配置文件或通过Java的方式构建出 的实例,主要功能是创建SqlSession(会话)对象(MybatisSqlSessionFactoryBean);SqlSessionFactory对象一个必要的属性是Configuration对象;SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,建议使用单例模式或者静态单例模式。一个SqlSessionFactory对应配置文件中的一个环境(environment),如果你要使用多个数据库就配置多个环境分别对应一个SqlSessionFactory

3、 SqlSession
作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。SqlSession通过调用api的Statement ID找到对应的MappedStatement对象。SqlSession是一个接口,它有2个实现类,分别是DefaultSqlSession(默认使用)以及SqlSessionManager;默认使用DefaultSqlSession,它有两个必须配置的属性:ConfigurationExecutor,SqlSession通过内部存放的执行器(Executor)来对数据进行CRUD。由于不是线程安全的,所以SqlSession对象的作用域需限制方法内;每一次操作完数据库后都要调用close对其进行关闭,官方建议通过try-finally来保证总是关闭SqlSession

4、 Executor
MyBatis执行器,是MyBatis 调度的核心Executor对象在创建Configuration对象的时候创建,并且缓存在Executor对象里。Executor:负责SQL语句的生成,调用StatementHandler访问数据库,查询缓存的维护;Executor(负责动态SQL的生成和查询缓存的维护)将MappedStatement对象进行解析,sql参数转化、动态sql拼接,生成jdbc Statement对象;Executor(执行器)接口有两个实现类,其中BaseExecutor有三个继承类分别是BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statements),SimpleExecutor(普通的执行器)。

5、 StatementHandler
封装了JDBC Statement操作,负责对JDBCstatement的操作,如设置参数、将Statement结果集转换成List集合,是真正访问数据库的地方,并调用ResultSetHandler处理查询结果。

6、ResultSetHandler
ParameterHandler 负责将用户传递的参数转换成JDBC Statement 所需要的参数
ResultSetHandler负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;处理查询结果。
TypeHandler负责java数据类型和jdbc数据类型之间的映射和转换

7、 MappedStatement :
MappedStatement就是用来存放我们SQL映射文件中的信息包括sql语句,输入参数,输出参数等等。一个SQL节点对应一个MappedStatement对象。借助MappedStatement中的结果映射关系,将返回结果转化成HashMap、JavaBean等存储结构并返回。

8、 SqlSource
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
BoundSql表示动态生成的SQL语句以及相应的参数信息
ConfigurationMyBatis所有的配置信息都维持在Configuration对象之中

Mybatis层次结构

MyBatis缓存

MyBatis缓存
如果要在同一个会话里面共享一级缓存,这个对象肯定是在SqlSession 里面创建的,作为SqlSession 的一个属性。DefaultSqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在Executor 里面维护缓存。Executor执行查询时先判断缓存,若缓存命中,直接返回缓存中的结果,否则继续之后的步骤查询数据库。
MyBatis使用底层为HashMap的Cache对象,通过各种装饰类对其层层装饰,以实现各种功能和隔离不同功能逻辑
MyBatis缓存分为一级缓存和二级缓存,默认开启一级缓存
数据查询优先级:二级缓存 -> 一级缓存 -> 数据库

一级缓存

缓存默认Session级别(可设为Statement),同一会话的重复Sql语句可使用缓存
不同Session间缓存相互独立,分布式查询可能读到脏数据
Executor收到Sql语句后,先查询自己的localCache(HashMap),缓存命中则直接返回结果,缓存命中失败则继续查询数据库后将结果写入localCache

一级缓存和sqlsession之间的关系

一个sqlsession会有一个自己专属的map用作一级缓存,但是不同的会话里面,即使执行的SQL 一模一样(通过一个Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。
多数据源的情况下会连接不同的sqlsessionfactory,不同的sqlsessionfactory则对应不同的sqlsession,所以一级缓存不同,因此查询和更新的时候不会互相影响。

一级缓存的生命周期有多长?

MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;

SqlSession 一级缓存的工作流程:

对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。

  1. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
  2. 如果命中,则直接将缓存结果返回;
  3. 如果没命中:
  4. 去数据库中查询数据,得到查询结果;
  5. 将key和查询到的结果分别作为key,value对存储到Cache中;
  6. 将查询结果返回;

一级缓存的不足:

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。例如:
(1)A线程内的SqlSession先获取id为1的News对象。
(2)CPU切换给B线程,B线程内的SqlSession修改id为1的News对象。
(3)A线程内的SqlSession再次获取id为1的News对象。
A线程内的SqlSession第二次获取id为1的News对象时,MyBatis不应该使用缓存——因为B线程已经更新了底层数据表中的数据,A线程内SqlSession内缓存的News对象是脏数据。

为了避免MyBatis一级缓存产生这种脏数据,最佳实践有两个要点:

  1. 尽量使用短生命周期的SqlSession!
    先说说第一种实践方式适合的场景:对于数据实时性要求没那么高(允许有一定的脏数据)的应用,只要项目避免使用长生命周期的SqlSession,即使MyBatis的一级缓存产生了脏数据,但由于SqlSession的生命周期特别短暂,这种脏数据也许处于可控范围之内。

  2. 避免使用SqlSession的一级缓存。
    再说说第二种实践方式适合的场景:对于数据实时性要求非常高的引用,项目基本不允许使用脏数据,此时就应该避免使用MyBatis的一级缓存!但请记住:MyBatis并不允许关闭一级缓存,因为它需要一级缓存来处理循环引用等问题。
    为了避免使用MyBatis一级缓存,程序有两种方式:
    ① 每个SqlSession永远只执行单次查询。如果要执行第二次查询,请重新打开另一个SqlSession!上面示例之所以产生脏数据,关键就因为程序用同一个SqlSession两次查询了id为1的News对象。如果每个SqlSession只执行单次查询,那么一级缓存几乎就不会产生作用了,这样可避免一级缓存产生脏数据。
    ② 将localCacheScope设为STATEMENT级,这样可避免在SqlSession范围内使用一级缓存,但这种方式依然有产生脏数据的风险。

可以手动清理缓存吗?

可以通过sqlSession.clearCache();操作实现

二级缓存

为解决跨SqlSession的缓存问题,MyBatis提供二级缓存功能。
二级缓存全局有效,划分到mapper级别(namespace定义),而非整个application使用同一缓存。
二级缓存使用CachingExecutor包装Executor,CachingExecutor负责与全局二级缓存交互
CachingExecutor优先查询二级缓存,如没有匹配则委托给它包装的Executor匹配的一级缓存
在这里插入图片描述

什么时候开启二级缓存?

一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?
因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。

二级缓存的脏数据

如果多个namespace 中有针对于同一个表的操作,比如Blog 表,如果在一个namespace 中刷新了缓存,另一个namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper 里面只操作单表的情况使用

如果要让多个namespace 共享一个二级缓存,应该怎么做?跨namespace 的缓存共享的问题,可以使用来解决:

cache-ref 代表引用别的命名空间的Cache 配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用
注意:在这种情况下,多个Mapper 的操作都会引起缓存刷新,粒度过大,频繁刷新缓存,缓存的意义已经不大了。

第三方缓存方案

除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。
MyBatis Cache是基于本地的,分布式环境下必然会读取脏数据,用户自定义实现MyBatis的Cache接口开发成本较高,直接使用Redis/Memcached等分布式缓存成本更低,安全性更高。

- mybatis技巧和细节

1、#{}和${}的区别是什么?

#{}用来接收参数,可以是简单类型也可以是pojo 类型;使用#{}可以有效地防止SQL注入,提高系统安全性。会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
{}时占位符,就是把${}替换成变量的值。

2、通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。
Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个 、、、 标签,都会被解析为一个MapperStatement对象。
Mapper接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。

3、Mybatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

4、Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

第一种:通过 来映射字段名和实体类属性名的一一对应的关系。
第二种是使用sql列的别名功能,让字段名的别名和实体类的属性名一致。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

5、Mybatis动态sql有什么用?执行原理?有哪些动态sql?

Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接sql的功能。
Mybatis提供了9种动态sql标签: trim|where|set|foreach|if|choose|when|otherwise|bind。

6、Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?

、、、、 ,加上动态sql的9个标签,其中 为sql片段标签,通过 标签引入sql片段, 为不支持自增的主键生成策略标签。

7、如何获取自动生成的(主)键值?

insert 方法总是返回一个int值 ,这个值代表的是插入的行数。如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。

select last_insert_id(); insert into stu2.user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address}); 9:在mapper中如何传递多个参数?

1、DAO层的函数
2、使用 @param 注解:然后,就可以在xml像下面这样使用(推荐封装为一个map,作为单个参数传递给mapper):
3、多个参数封装成map

8、Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?(结合4)

不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;
原因就是namespace+id是作为Map <String,MapperStatement> 的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。

9、MyBatis实现一对一有几种方式?具体怎么操作的?

有联合查询和嵌套查询;联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成(使用resultType);嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置(使用resultMap)。

10、MyBatis实现一对多有几种方式,怎么操作的?

有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成(使用resultType);嵌套查询是先查一个表,根据这个表里面的 结果的外键id,去再另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置(使用resultMap)。

11、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB.getName,拦截器invoke方法发现a.getB是值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB.getName方法的调用。这就是延迟加载的基本原理。

12、什么是MyBatis的接口绑定?有哪些实现方式?

接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式:一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;另外一种就是通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。

13、使用MyBatis的mapper接口调用时有哪些要求?

1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同;
2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同;
3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同;
4、Mapper.xml文件中的namespace即是mapper接口的类路径。

14、Mapper编写有哪几种方式?

第一种:接口实现类继承SqlSessionDaoSupport:使用此种方法需要编写mapper接口,mapper接口实现类、mapper.xml文件。1、在sqlMapConfig.xml中配置mapper.xml的位置2、定义mapper接口3、实现类集成SqlSessionDaoSupport mapper方法中可以this.getSqlSession进行数据增删改查。4、spring 配置

第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean :1、在sqlMapConfig.xml中配置mapper.xml的位置,如果mapper.xml和mappre接口的名称相同且在同一个目录,这里可以不用配置2、定义mapper接口:3、mapper.xml中的namespace为mapper接口的地址4、mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致5、Spring中定义

第三种:使用mapper扫描器:1、mapper.xml文件编写:mapper.xml中的namespace为mapper接口的地址;mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致;如果将mapper.xml和mapper接口的名称保持一致则不用在sqlMapConfig.xml中进行配置。2、定义mapper接口:注意mapper.xml的文件名和mapper的接口名称保持一致,且放在同一个目录3、配置mapper扫描器:4、使用扫描器后从spring容器中获取mapper的实现对象。

15、简述Mybatis的插件运行原理,以及如何编写一个插件。

答:Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke方法,当然,只会拦截那些你指定需要拦截的方法。

编写插件:实现Mybatis的Interceptor接口并复写intercept方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

16、SqlSession创建过程和相关对象生命周期?

SqlSessionFactoryBuilder—SqlSessionFactory—SqlSession—SqlMapper

  • SqlSessionFactoryBuilder在构建SqlSessionFactory后即可销毁
  • SqlSessionFactory存在于整个MyBatis应用中,类似数据库连接池,不断创建SqlSession
  • SqlSession类似于数据库连接,在完成一次业务请求后回收
  • SqlMapper由SqlSession创建,生命周期SqlMapper<=SqlSession

参考
参考

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-04-24 09:15:04  更:2022-04-24 09:17:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 2:55:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码