Mybatis3.4.x技术内幕(二十一):参数设置、结果封装、级联查询、延迟加载原理分析 – 祖大俊的个人页面 – OSCHINA – 中文开源技术交流社区

沙海
沙海
沙海
744
文章
2
评论
2021年3月28日01:39:36
评论
2 16072字阅读53分34秒
摘要

速读摘要

速读摘要

Mybatis在执行查询时,其参数设置、结果封装、级联查询、延迟加载,是最基本的功能和用法,我们有必要了解其工作原理,重点阐述级联查询和延迟加载。通过Class对象反射创建对象实例的工厂类,比如创建一个Student对象。Mybatis的参数设置、结果封装、级联查询、延迟加载原理就分析结束了。学习数据结构与算法一个很重要的前提,就是至少熟练掌握一门编程语言。我们小学三年级的时候就知道,redis是一个纯内存存储的中间件,那它宕机会怎么样?

原文约 5902 | 图片 16 | 建议阅读 12 分钟 | 评价反馈

祖大俊的个人页面Mybatis3.4.x技术内幕

正文

Mybatis3.4.x技术内幕(二十一):参数设置、结果封装、级联查询、延迟加载原理分析

Mybatis3.4.x技术内幕(二十一):参数设置、结果封装、级联查询、延迟加载原理分析 – 祖大俊的个人页面 – OSCHINA – 中文开源技术交流社区

 祖大俊 发布于 2016/09/16 11:00

字数 3059

阅读 6.8K

收藏 9

点赞 2

评论 1

MyBatis技术内幕级联查询延迟加载关联查询

Mybatis在执行查询时,其参数设置、结果封装、级联查询、延迟加载,是最基本的功能和用法,我们有必要了解其工作原理,重点阐述级联查询和延迟加载。

1、MetaObject

MetaObject用于反射创建对象、反射从对象中获取属性值、反射给对象设置属性值,参数设置和结果封装,用的都是这个MetaObject提供的功能。

public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { if (object == null) { return SystemMetaObject.NULL_META_OBJECT; } else { return new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory); } } public Object getValue(String name) { //... } public void setValue(String name, Object value) { // ... }

Object object:要反射的对象,比如Student。

ObjectFactory objectFactory:通过Class对象反射创建对象实例的工厂类,比如创建一个Student对象。

ObjectWrapperFactory :对目标对象进行包装,比如可以将Properties对象包装成为一个Map并返回Map对象。

ReflectorFactory :为了避免多次反射同一个Class对象,ReflectorFactory提供了Class对象的反射结果缓存。

getValue(String name):属性取值。

setValue(String name, Object value):属性赋值。

2、参数设置实现原理

<insert id="insertStudent" parameterType="Student" > INSERT INTO STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE) VALUES(#{studId}, #{name}, #{email}, #{dob}, #{phone}) </insert>

Mybatis解析后,上面的#{studId}, #{name}占位符都会被替换为?号占位符,然后给?号设置参数值,Mybatis通过一个反射工具类MetaObject,从Student对象中,反射获取studId、name属性值,并赋值给?号参数。

如果是占位符是#{item.studId},也是一样,通过getValue("item.studId")取值。

详情请参见DefaultParameterHandler.java。

3、结果封装实现原理

Mybatis的结果封装,分为两种,一种是有ResultMap映射表,明确定义了结果集列名与对象属性名的配对关系,另外一种是对象类型,没有明确定义结果集列名与对象属性名的配对关系,如resultType是Student对象。

 <resultMap type="Teacher" id="TeacherResult"> <id property="id" column="t_id"/> <result property="name" column="t_name" /> </resultMap> <select id="findAllTeachers" resultMap="TeacherResult"> SELECT t_id, t_name FROM TEACHERS </select>

原理非常简单:使用ObjectFactory ,创建一个Teacher对象实例。

teacher.setId(resultSet.getInt("t_id"));

teacher.setName(resultSet.getString("t_name"));

如果是对象类型,如Student对象类似,原理也非常简单。

<select id="findStudentById" parameterType="int" resultType="Student"> SELECT STUD_ID AS STUDID, NAME, EMAIL, DOB, PHONE FROM STUDENTS WHERE STUD_ID = #{Id} </select>

原理:使用ObjectFactory ,创建一个Student对象实例。

student.setStudId(resultSet.getInt("STUDID"));

student.setName(resultSet.getString("NAME"));

问题:

1、Student对象只有studId属性,根本没有STUDID属性;Student对象只有name属性,根本没有NAME属性;Java是大小写敏感的编程语言,我凭什么说原理是这样的?瞎说的吧?

2、resultSet.getInt("STUDID"),resultSet.getString("NAME"),我怎么知道一个是Integer,一个是String?

好的博客文章,价值就体现在这些地方,下面我们就来解开谜团。

未映射的结果集列名为[STUDID, NAME, EMAIL, DOB, PHONE]。

public class Reflector { private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>(); //... caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); //... }

于是caseInsensitivePropertyMap = {STUDID=studId, DOB=dob, PHONE=phone, EMAIL=email, NAME=name}。

于是,resultSet结果集列名和对象属性名之间,就建立起了一对一对应关系。因此,哪怕你把列名写成NaME、pHoNe,它都可以“智能”找到对象属性名,进行赋值操作,Mybatis不愧是一款伟大的开源产品。

caseInsensitive的含义就是忽略结果集列名大小写。

正确找到对象属性名之后,反射获取属性studId的java类型,得到Integer类型,反射获取属性name的java类型得到String类型,Integer类型对应IntegerTypeHandler,String类型对应StringTypeHandler。

于是,resultSet.getInt("STUDID"),resultSet.getString("NAME")就是这么确定的。

详情请参看DefaultResultSetHandler.java源码。

4、级联查询实现原理

级联查询,主要分为一对一关联查询和一对多集合查询,我们研究一下Mybatis是如何实现的。

1、一对一关联查询实现原理(association)

一对一,一个Studen对应一个班级。

举例:假设一个Student对应一个Teacher,如下:

 <resultMap id="studentResult" type="Student"> <association property="teacher" column="teacher_id" javaType="Teacher" select="selectTeacher" /> </resultMap> <select id="selectStudent" parameterType="int" resultMap="studentResult"> SELECT STUD_ID AS STUDID, NAME, EMAIL, DOB, PHONE, TEACHER_ID FROM STUDENTS WHERE STUD_ID = #{id} </select> <select id="selectTeacher" parameterType="int" resultType="Teacher"> SELECT * FROM TEACHERS WHERE ID = #{id} </select>

首先查询Student对象,想要获得该Student对象的属性Teacher teacher对象,那么需要该Student对象的teacher_id值,作为查询Teacher对象的参数,这个语意是易懂的,所以,上面的一对一关联查询,应该很容易看得懂。

作为resultMap标签,其下面的association标签,也会被解析为一个ResultMapping对象。

public class ResultMapping { private String property; private String column; private String nestedQueryId; //... }

对于xml配置,ResultMapping={property=teacher, column=teacher_id, nestedQueryId=selectTeacher},其属性nestedQueryId就是用来存储另外一个select查询的id值的。

org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getPropertyMappingValue(ResultSet, MetaObject, ResultMapping, ResultLoaderMap, String)源码。

 private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { if (propertyMapping.getNestedQueryId() != null) { // 执行另外一个select查询,把查询结果赋值给属性值,比如Student对象的teacher属性。 return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix); } else if (propertyMapping.getResultSet() != null) { addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK? return DEFERED; } else { final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler(); final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); return typeHandler.getResult(rs, column); } }

org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getNestedQueryMappingValue(ResultSet, MetaObject, ResultMapping, ResultLoaderMap, String)源码。

 private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { final String nestedQueryId = propertyMapping.getNestedQueryId(); final String property = propertyMapping.getProperty(); final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId); final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType(); final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix); Object value = null; if (nestedQueryParameterObject != null) { final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject); final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql); final Class<?> targetType = propertyMapping.getJavaType(); if (executor.isCached(nestedQuery, key)) { executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType); value = DEFERED; } else { // ResultLoader保存了关联查询所需要的所有信息 final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql); if (propertyMapping.isLazy()) { // 执行延迟加载 // 语意:resultLoader的查询结果将赋值给metaResultObject源对象的property属性,resultLoader的查询参数值来自于metaResultObject源对象属性中。 // 举例:查询Teacher,赋值给Student的teacher属性,参数来自于查询Student的ResultSet的teacher_id列的值。 // 由于需要执行延迟加载,将查询相关信息放入缓存,但不执行查询,使用该属性时,自动触发加载操作。 lazyLoader.addLoader(property, metaResultObject, resultLoader); value = DEFERED; } else { // 不执行延迟加载,立即查询并赋值 value = resultLoader.loadResult(); } } } return value; }
public ResultLoader(Configuration config, Executor executor, MappedStatement mappedStatement, Object parameterObject, Class<?> targetType, CacheKey cacheKey, BoundSql boundSql) {}

看看ResultLoader的构造函数,它保存了执行一个select查询所需要的所有信息。

2、延迟加载

mybatis-config.xml内全局配置。

<setting name="lazyLoadingEnabled" value="false|true" />
public class ResultLoaderMap { private final Map<String, LoadPair> loaderMap = new HashMap<String, LoadPair>(); } private LoadPair(final String property, MetaObject metaResultObject, ResultLoader resultLoader) { //... }

ResultLoader保存了一个select查询所需要的所有信息,那么,将查询结果赋值给metaResultObject源对象的property属性,这些基本信息都缓存至loaderMap内,这就是语意。

举例:查询Teacher,赋值给Student的teacher属性。为了实现延迟加载,产生了一个loaderMap缓存,缓存了查询所需要的所有信息,如果lazyLoadingEnabled=true,先不执行查询。如果lazyLoadingEnabled=false,那么立即执行查询。

我们看看lazyLoadingEnabled=true时的工作原理。

 private static class EnhancedResultObjectProxyImpl implements MethodHandler { @Override public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable { final String methodName = method.getName(); try { synchronized (lazyLoader) { if (WRITE_REPLACE_METHOD.equals(methodName)) { Object original = null; if (constructorArgTypes.isEmpty()) { original = objectFactory.create(type); } else { original = objectFactory.create(type, constructorArgTypes, constructorArgs); } PropertyCopier.copyBeanProperties(type, enhanced, original); if (lazyLoader.size() > 0) { return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs); } else { return original; } } else { // 此处完成延迟加载功能 if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) { if (aggressive || lazyLoadTriggerMethods.contains(methodName)) { lazyLoader.loadAll(); } else if (PropertyNamer.isProperty(methodName)) { final String property = PropertyNamer.methodToProperty(methodName); if (lazyLoader.hasLoader(property)) { lazyLoader.load(property); } } } } } return methodProxy.invoke(enhanced, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } }

JavassistProxyFactory会使用CGLib创建一个Student代理对象,所有调用Student对象方法,都会经过EnhancedResultObjectProxyImpl.invoke()方法的拦截。

于是当调用Student.getTeacher()方法时,才真正去执行查询Teacher的动作并把结果赋值给Student的teacher属性。

如果lazyLoadingEnabled=false,压根就不会创建Student代理对象,直接就是Student对象,并立即执行Teacher查询,然后赋值给Student的teacher属性。

延迟加载原理,就是这么简单。

3、一对多查询原理(collection)

 <resultMap type="Teacher" id="TeacherResult"> <collection property="students" javaType="ArrayList" column="id" ofType="Student" select="selectStudents"/> </resultMap> <select id="findTeacherById" parameterType="int" resultMap="TeacherResult"> SELECT * FROM TEACHERS where ID = #{ID} </select> <select id="selectStudents" parameterType="int" resultType="Student"> SELECT STUD_ID AS STUDID, NAME, EMAIL, DOB, PHONE FROM STUDENTS WHERE TEACHER_ID = #{id} </select>

一对多查询原理,和一对一查询原理是一模一样的,都是将结果以List的形式返回,如果是一对一查询,就取List的第0个元素,如果是一对多查询,就直接返回List。

org.apache.ibatis.executor.loader.ResultLoader.loadResult()源码。

 public Object loadResult() throws SQLException { List<Object> list = selectList(); resultObject = resultExtractor.extractObjectFromList(list, targetType); return resultObject; }

org.apache.ibatis.executor.ResultExtractor.extractObjectFromList(List<Object>, Class<?>)源码。

public Object extractObjectFromList(List<Object> list, Class<?> targetType) { Object value = null; if (targetType != null && targetType.isAssignableFrom(list.getClass())) { value = list; } else if (targetType != null && objectFactory.isCollection(targetType)) { value = objectFactory.create(targetType); MetaObject metaObject = configuration.newMetaObject(value); metaObject.addAll(list); } else if (targetType != null && targetType.isArray()) { Class<?> arrayComponentType = targetType.getComponentType(); Object array = Array.newInstance(arrayComponentType, list.size()); if (arrayComponentType.isPrimitive()) { for (int i = 0; i < list.size(); i++) { Array.set(array, i, list.get(i)); } value = array; } else { value = list.toArray((Object[])array); } } else { if (list != null && list.size() > 1) { throw new ExecutorException("Statement returned more than one row, where no more than one was expected."); } else if (list != null && list.size() == 1) { value = list.get(0); } } return value; }

4、嵌套查询原理

上面的一对一、一对多查询,都需要单独发送额外的sql进行关联对象查询操作,那么嵌套查询,解决的是只需要一个sql,就可以将关联对象也查询出来。

 <resultMap id="studentResult" type="Student"> <id property="studId" column="stud_id" /> <association property="teacher" column="teacher_id" javaType="Teacher"> <id property="id" column="teacher_id" /> <result property="name" column="T_NAME" /> </association> </resultMap> <select id="selectStudent" parameterType="int" resultMap="studentResult"> SELECT s.STUD_ID ,s.TEACHER_ID ,t.NAME AS T_NAME FROM STUDENTS s LEFT JOIN TEACHERS t ON s.TEACHER_ID = t.ID WHERE s.STUD_ID = #{id} </select>

 ResultMapping的属性nestedResultMapId就是用来做这个的。

public class ResultMapping { private String nestedResultMapId; //.. }

org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(ResultSetWrapper, ResultMap, CacheKey, String, Object)源码。

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException { final String resultMapId = resultMap.getId(); Object resultObject = partialObject; if (resultObject != null) { final MetaObject metaObject = configuration.newMetaObject(resultObject); putAncestor(resultObject, resultMapId, columnPrefix); applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false); ancestorObjects.remove(resultMapId); } else { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); resultObject = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { final MetaObject metaObject = configuration.newMetaObject(resultObject); boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty(); if (shouldApplyAutomaticMappings(resultMap, true)) { foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; } foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; putAncestor(resultObject, resultMapId, columnPrefix); // 解析NestedResultMappings并封装结果,赋值给源对象的关联查询属性上 foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues; ancestorObjects.remove(resultMapId); foundValues = lazyLoader.size() > 0 || foundValues; resultObject = foundValues ? resultObject : null; } if (combinedKey != CacheKey.NULL_CACHE_KEY) { nestedResultObjects.put(combinedKey, resultObject); } } return resultObject; }

applyNestedResultMappings()方法负责从ResultSet结果集中,封装association映射为指定对象,赋值给metaObject源对象的属性对象上。

一对多嵌套查询。

 <resultMap id="teacherResult" type="Teacher"> <id property="id" column="TEACHER_ID" /> <result property="name" column="TEACHER_NAME" /> <collection property="students" ofType="Student"> <id property="studId" column="STUD_ID" /> </collection> </resultMap> <select id="findTeacherById" parameterType="int" resultMap="teacherResult"> SELECT s.STUD_ID ,t.ID AS TEACHER_ID ,t.NAME AS TEACHER_NAME FROM TEACHERS t LEFT JOIN STUDENTS s ON s.TEACHER_ID = t.ID WHERE t.ID = #{id} </select>

原理和一对一嵌套查询是一样的。

问题:left join查询,一对一没问题,但是,一对多时,返回记录像下面这样,也就是说一的一端其实也是N条记录,但是它代表的是一个Teacher对象,Mybatis是如何去重的呢?下面的记录,代表1个老师有6个学生,而不是6个老师6个学生。

|          1 | teacher      |      38 | |          1 | teacher      |      39 | |          1 | teacher      |      40 | |          1 | teacher      |      41 | |          1 | teacher      |      42 | |          1 | teacher      |      43 |

5、一对多嵌套查询一的一端去重复原理

<id property="studId" column="stud_id" />
public class ResultMap { private List<ResultMapping> idResultMappings; //... }
public class DefaultResultSetHandler implements ResultSetHandler { private final Map<CacheKey, Object> nestedResultObjects = new HashMap<CacheKey, Object>(); //... }

<id>和<result>标签的区别就在于此,<id>表示唯一标识一条记录的属性,可以有多个<id>标签,代表联合主键。Map<CacheKey, Object> nestedResultObjects就是用来缓存嵌套查询中,记录去重复功能的。

对于上面的6条结果记录,根据<id>标签生成的CacheKey是相同的,类似下面的值:

-540526625:-2232742192:com.mybatis3.mappers.TeacherMapper.teacherResult:TEACHER_ID:1 -540526625:-2232742192:com.mybatis3.mappers.TeacherMapper.teacherResult:TEACHER_ID:1

每次遍历结果集ResultSet时,获取到的Teacher对象,都是第一次生成的Teacher对象,所以,Teacher是同一个,Student则是6个。

 private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { //... while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) { //... Object partialObject = nestedResultObjects.get(rowKey); rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); }

详情请参看DefaultResultSetHandler.java。

至此,Mybatis的参数设置、结果封装、级联查询、延迟加载原理就分析结束了。

版权提示:文章出自开源中国社区,若对文章感兴趣,可关注我的开源中国社区博客(http://my.oschina.net/zudajun)。(经过网络爬虫或转载的文章,经常丢失流程图、时序图,格式错乱等,还是看原版的比较好)

© 著作权归作者所有

打赏点赞 (2) 收藏 (9)

分享

打印举报

上一篇:Mybatis3.4.x技术内幕(二十二):Mybatis一级、二级缓存原理分析

下一篇:Mybatis3.4.x技术内幕(二十):PageHelper分页插件源码及原理剖析

Mybatis3.4.x技术内幕(二十一):参数设置、结果封装、级联查询、延迟加载原理分析 – 祖大俊的个人页面 – OSCHINA – 中文开源技术交流社区

祖大俊

粉丝 956

博文 32

码字总数 52477

作品 0

昌平

关注私信提问

此博客有 1 条评论,请先登录后再查看。

插入表情插入软件

发表评论

最新文章相关文章

百度网盘限速问题解决方法(非会员不使用客户端)

百度网盘限速问题解决方法(非会员不使用客户端) 参考文章: (1)百度网盘限速问题解决方法(非会员不使用客户端) (2)https://www.cnblogs.com/fisherpau/p/11301815.html 备忘一下。...

javail

19分钟前

15

0

如何让学习数据结构与算法

学习数据结构与算法一个很重要的前提,就是至少熟练掌握一门编程语言。至于是那种语言就无关紧要了,C 语言、C++、Java、Python 等语言都可以。因为无论是数据结构还是算法,它教会我们的是解...

C语言与CPP编程

24分钟前

13

0

Mybatis3.4.x技术内幕(二十一):参数设置、结果封装、级联查询、延迟加载原理分析 – 祖大俊的个人页面 – OSCHINA – 中文开源技术交流社区

Redis源码剖析之RDB

我们小学三年级的时候就知道,redis是一个纯内存存储的中间件,那它宕机会怎么样?数据会丢失吗?答案是可以不丢。 事实上redis为了保证宕机时数据不丢失,提供了两种数据持久化的机制——r...

xindoo

45分钟前

19

0

HBase一次慢查询请求的问题排查与解决过程

HBase一次慢查询请求的问题排查与解决过程 参考文章: (1)HBase一次慢查询请求的问题排查与解决过程 (2)https://www.cnblogs.com/panfeng412/archive/2013/06/08/hbase-slow-query-trou...

富含淀粉

49分钟前

34

0

数据结构课设计:多叉路口交通灯管理问题

题目描述 通常,在十字交叉路口只需设红、绿两色的交通灯便可保持正常的交通秩序,而在多叉路口需设几种颜色的交通灯才能既使车辆相互之间不碰撞,又能达到车辆的最大流通。假设有一个如图(...

wangxuwei

今天

34

0

Mybatis3.4.x技术内幕(二十一):参数设置、结果封装、级联查询、延迟加载原理分析 – 祖大俊的个人页面 – OSCHINA – 中文开源技术交流社区

加载更多

OSCHINA 社区

关于我们联系我们加入我们合作伙伴Open API

在线工具

Gitee.com企业研发管理CopyCat-代码克隆检测实用在线工具国家反诈中心APP下载

QQ交流群

Mybatis3.4.x技术内幕(二十一):参数设置、结果封装、级联查询、延迟加载原理分析 – 祖大俊的个人页面 – OSCHINA – 中文开源技术交流社区

530688128

微信公众号

Mybatis3.4.x技术内幕(二十一):参数设置、结果封装、级联查询、延迟加载原理分析 – 祖大俊的个人页面 – OSCHINA – 中文开源技术交流社区

OSCHINA APP

聚合全网技术文章,根据你的阅读喜好进行个性推荐

下载 APP

©OSCHINA(OSChina.NET)

工信部

开源软件推进联盟

指定官方社区

深圳市奥思网络科技有限公司版权所有

粤ICP备12009483号

继续阅读
weinxin
资源分享QQ群
本站是一个IT技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
沙海
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: