springboot整合mybatis
创始人
2024-04-14 16:51:50

1.pom

        org.mybatis.spring.bootmybatis-spring-boot-starter2.2.2

引入一个这个注解,就有了mybatis的强大功能,神奇
mybaits使用流程:

  • 引入依赖
  • 配置数据库信息
  • 创建数据库对应的实体类
  • 创建mapper接口,定义查询方法 @Mapper
  • 创建mapper对应的xml,编写sql语句
  • 创建service,调用mapper接口的方法 @Service
  • 创建controller,调用service的方法 @Controller

2.自动配置

引入 mybaits starter会引入这个autoconfigure
在这里插入图片描述
MybatisAutoConfiguration引入,项目启动时会扫描到对应的autoconfigure下的spring.factories文件中的配置。
在这里插入图片描述
引入mybaits-starter会自动引入的jar包:
在这里插入图片描述
mybaits自动配置类
在这里插入图片描述

3.扫描mapper接口

也就是扫描到我们定义的mapper接口,变成BeanDefinition。
这里是两个问题:
1.接口默认不会被spring装载成BeanDefinition。所以mybatis框架要处理。
2.接口不会被实例化,需要对接口类型进行封装。

1.全局配置方式

在启动类加注解:@MapperScan(“xxx.xxx.xxx.mapper”)
作用就是会扫描到配置的包下面的所有mapper接口,扫描这些接口的目的是为了创建代理对象,因为接口不能实例化,需要用代理方式执行接口中的方法。这里用的是JDK动态代理。

这个注解能生效是因为引入了MapperScannerRegistrar类
mapper啥啥啥输入注册,应该就是这个意思
在这里插入图片描述
MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,当执行refresh的invokeBeanFactoryPostProcessors会加载所有实现了ImportBeanDefinitionRegistrar接口的类,并执行它的registerBeanDefinitions方法,即执行MapperScannerRegistrar.registerBeanDefinitions

而MapperScannerRegistrar.registerBeanDefinitions会先创建MapperScannerConfigurer类到beanfactory中。然后再通过MapperScannerConfigurer再去扫描我们自己定义的mapper接口
在这里插入图片描述

MapperScannerConfigurer实现的BeanDefinitionRegistryPostProcessor接口,因为在invokeBeanFactoryPostProcessors方法里会执行两个接口,按先后执行顺序为:

第一个是BeanDefinitionRegistryPostProcessor;
第二个是BeanFactoryPostProcessor。

而执行BeanDefinitionRegistryPostProcessor,会执行postProcessBeanDefinitionRegistry方法,在这里就是MapperScannerConfigurer.postProcessBeanDefinitionRegistry方法
在这里插入图片描述
这里新建了一个ClassPathMapperScanner类,执行它的scan方法,它的父类是ClassPathBeanDefinitionScanner
在这里插入图片描述
注意上面最后一行 this.basepackage就是配置的扫描路径
会调用ClassPathBeanDefinitionScanner的Scan方法
在这里插入图片描述
先执行ClassPathMapperScanner中的doScan,这里会先调用了父类的doScan,也就是ClassPathBeanDefinitionScanner类的doScan方法(功能就是根据配置的包路径,扫描自定义mapper接口,转换成BeanDefinition)。调用之后执行processBeanDefinitions
在这里插入图片描述
扫描包路径下的所有mapper
在这里插入图片描述
一般情况下,**mapper是接口类。默认情况下,spring对接口interface是不会生成BeanDefinition 对象。在mybatis里,为了生成BeanDefinition 对象,则重写了isCandidateComponent方法,上面箭头指的方法就会调用,是接口类型也要生成BeanDefinition **。
这里解决了标题3的第一个问题。
在这里插入图片描述

通过这个方法就可以生成我们自己的mapper接口的beandefinition了。

spring是扫描所有基于@Component注解的类,并生成bean定义,然后放到bean定义注册表中。而我们这里的mapper没有加@Component注解,所以在执行上述逻辑之前,实际的bean定义注册表中是没有这些mapper的bean定义的。执行上述逻辑之后,才在bean定义注册表中加入了这些mapper的beanDefinition

将beanDefinitions放到beanFactory之后,还有一步重要的操作,就是执行本类ClassPathBeanDefinitionScanner中的processBeanDefinitions
在这里插入图片描述
这里是把每个mapper的Bean定义的BeanClass设置为mapperFactoryBeanClass,这样做是为了让后面创建bean时,可以使用MapperFactoryBean来创建bean。
在这里插入图片描述
在这里插入图片描述
保存了自定义mapper的信息,并将类型变为mapperFactoryBeanClass。这样动态代理的时候就可以找到我们自定义的mapper接口了。
在这里插入图片描述

为什么要把mapper的BeanClass设置为mapperFactoryBeanClass?

因为mapper是接口,但是接口是不能实例化的,所以mybatis中就把mapper的beanDefinition的beanClass定义为mapperFactoryBeanClass,利用mapperFactoryBeanClass是通过getObject()来进行实例化,即通过jdk代理的方式,生成的代理对象。
这里解决了标题3的第二个问题。

总结:
先生成MapperScannerConfigurer的bean定义,并放入bean定义注册表中。然后通过MapperScannerConfigurer.postProcessBeanDefinitionRegistry扫描所有mapper类,创建mapper的bean定义,也是放入bean定义注册表中,并将mapper的bean定义的BeanClass属性值设置为mapperFactoryBeanClass,为后面创建bean做准备。

2.单接口配置方式

@mapper
这样需要在每一个mapper接口添加此注解
这个注解是怎么生效的呢
在这里插入图片描述
加载MybatisAutoConfiguration类后,因为MybatisAutoConfiguration内部类AutoConfiguredMapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,当执行refresh的invokeBeanFactoryPostProcessors会加载所有实现了ImportBeanDefinitionRegistrar接口的类,并执行它的registerBeanDefinitions方法,即执行AutoConfiguredMapperScannerRegistrar.registerBeanDefinitions。这个概念和全局配置那种方式是一样的。
在这里插入图片描述
到这就已经把mapper接口的定义信息存到beanFactory了,等待实例化。

4.实例化

接下来,执行到refresh.finishBeanFactoryInitialization(beanFactory),在这里将会把bean定义注册表中的所有的beanDefinition进行实例化,然后放到bean缓存池中,供应用程序调用。

注意这里是把bean定义注册表中的所有beanDefinition遍历处理挨个进行实例化,那么mapper、SqlSessionFactory、SqlSessionTemplate实例化的先后顺序是怎样的呢? 这里需注意一点,在每个创建bean实例之后,初始化bean之前,会执行populateBean进行属性赋值,如果依赖其他的bean,那么会先创建依赖的bean,所以,即使先实例化的是controller(正常controller依赖的service,service依赖mapper,mapper依赖sqlSessionTemplate,sqlSessionTemplate依赖SqlSessionFactory,但实例化的顺序是反过来的),最终还是先实例化SqlSessionFactory。

1.SqlSessionFactory

在加载mybatis自动配置类时,会先判断SqlSessionFactory(mybatis jar包中)类和SqlSessionFactoryBean(mybatis-spring jar包中)类是否引入了,这里肯定是引入了(在mybatis-starter中),这里是为了注入bean,只是扫描到了SqlSessionFactory类,但是并没有实例化,所以会在这里进行实例化。
在这里插入图片描述
传入的是datasource,这个应该是在配置文件配置好的
最后会调用getObject
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
继续调用buildSqlSessionFactory(),主要就是填充前面创建的configuration对象(在MybatisAutoConfiguration类的sqlSessionFactory方法中创建的)。

在这里插入图片描述

在这里插入图片描述
这个configuration里有很多属性,
在这里插入图片描述
在这里插入图片描述
对应的xml
在这里插入图片描述
buildSqlSessionFactory就是通过XMLConfigBuilder解析mybatis配置,通过XMLMapperBuilder解析mapper.xml的配置(重点是生成mappedStatements、resultMaps、sqlFragments等),以及其他的配置,最终放到Configuration里,供后面使用。
在XMLMapperBuilder.parse()里通过mapperRegistry.addMapper方法,会把mapper接口添加到knownMappers中(knownMappers结构为HashMap,每个mapper的value为MapperProxyFactory对象),那么后面调用getMapper的时候就可以直接获取了。所以XMLMapperBuilder.parse()方法,完成了mapper接口和xml映射文件的绑定。

  • configuration创建
    在这里插入图片描述
    在这里插入图片描述
    yml配置
    在这里插入图片描述
    自定义配置类
    在这里插入图片描述
  • buildSqlSessionFactory方法
  protected SqlSessionFactory buildSqlSessionFactory() throws Exception {final Configuration targetConfiguration;XMLConfigBuilder xmlConfigBuilder = null;if (this.configuration != null) {//就是上面创建的configuration对象targetConfiguration = this.configuration;if (targetConfiguration.getVariables() == null) {targetConfiguration.setVariables(this.configurationProperties);} else if (this.configurationProperties != null) {targetConfiguration.getVariables().putAll(this.configurationProperties);}} else if (this.configLocation != null) {xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);targetConfiguration = xmlConfigBuilder.getConfiguration();} else {LOGGER.debug(() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");targetConfiguration = new Configuration();Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);}Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);if (hasLength(this.typeAliasesPackage)) {scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream().filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface()).filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);}if (!isEmpty(this.typeAliases)) {Stream.of(this.typeAliases).forEach(typeAlias -> {targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");});}if (!isEmpty(this.plugins)) {Stream.of(this.plugins).forEach(plugin -> {targetConfiguration.addInterceptor(plugin);LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");});}if (hasLength(this.typeHandlersPackage)) {scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers())).forEach(targetConfiguration.getTypeHandlerRegistry()::register);}if (!isEmpty(this.typeHandlers)) {Stream.of(this.typeHandlers).forEach(typeHandler -> {targetConfiguration.getTypeHandlerRegistry().register(typeHandler);LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");});}targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler);if (!isEmpty(this.scriptingLanguageDrivers)) {Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {targetConfiguration.getLanguageRegistry().register(languageDriver);LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");});}Optional.ofNullable(this.defaultScriptingLanguageDriver).ifPresent(targetConfiguration::setDefaultScriptingLanguage);if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmlstry {targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));} catch (SQLException e) {throw new NestedIOException("Failed getting a databaseId", e);}}Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);if (xmlConfigBuilder != null) {try {xmlConfigBuilder.parse();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();}}targetConfiguration.setEnvironment(new Environment(this.environment,this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,this.dataSource));//重要1 在这儿解析所有的mapper.xml文件if (this.mapperLocations != null) {if (this.mapperLocations.length == 0) {LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");} else {//遍历mapper.xml文件进行解析for (Resource mapperLocation : this.mapperLocations) {if (mapperLocation == null) {continue;}try {//创建xml的构造器XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());xmlMapperBuilder.parse();} catch (Exception e) {throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);} finally {ErrorContext.instance().reset();}LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");}}} else {LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");}return this.sqlSessionFactoryBuilder.build(targetConfiguration);}

重要1:
在这里插入图片描述

  • parse方法
  public void parse() {if (!configuration.isResourceLoaded(resource)) {//1.解析mapper标签以及内部的标签configurationElement(parser.evalNode("/mapper"));configuration.addLoadedResource(resource);//2.构建mapper与dao的关系bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();}

解析mapper标签以及内部的标签

  • configurationElement方法
  private void configurationElement(XNode context) {try {//获取namespace 也就是mapper接口String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.isEmpty()) {throw new BuilderException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);cacheRefElement(context.evalNode("cache-ref"));cacheElement(context.evalNode("cache"));//解析parameterMap标签parameterMapElement(context.evalNodes("/mapper/parameterMap"));//解析resultMap标签resultMapElements(context.evalNodes("/mapper/resultMap"));//解析sql标签sqlElement(context.evalNodes("/mapper/sql"));//解析定义的sql语句buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);}}

在这里插入图片描述
list是mapper.xml中的sql语句
在这里插入图片描述
parseStatementNode方法做具体工作,就是解析标签上的所有的属性,然后封装成MappedStatement对象最终放到配置类Configuration的mappedStatements属性上。key 为 id ,mapper接口对应方法的全限定名,value 为 MappedStatement。
执行完parse方法中的 configurationElement(parser.evalNode(“/mapper”))方法:
在这里插入图片描述
这里能获取到两个,早期是通过方法名方式进行查询的。现在是根据全限定名

在这里插入图片描述
构建mapper与dao的映射关系

  • bindMapperForNamespace();
    在这里插入图片描述
    addmapper方法
    在这里插入图片描述

在这里插入图片描述
将mapper接口封装成MapperProxyFactory然后放到knownMappers属性中。
在这里插入图片描述

2.sqlSessionTemplate

在这里插入图片描述
通过动态代理
在这里插入图片描述

  • 上面最后一行代码使用jdk动态代理生成SqlSession代理对象
  • 第一个参数表示生成代理对象的类加载器,第二个参数表示被代理类对象,第三个参数表示代理实现类(里面主要是执行代理对象时要做的事情)

3.mapper

在这里插入图片描述
mapper接口在前面被封装成了MapperFactoryBean,实现了InitializingBean接口,所以应该重写该接口的方法afterPropertiesSet,但是在MapperFactoryBean类中没有重新这个方法,而他的父类SqlSessionDaoSupport也没重写,所以是在SqlSessionDaoSupport的父类DaoSupport类中重写的,所以初始化MapperFactoryBean时会先调用DaoSupport中的afterPropertiesSet方法。
在这里插入图片描述
然后执行MapperFactoryBean的checkDaoConfig方法
在这里插入图片描述
在这里插入图片描述

mapper是什么时候放到Configuration中的呢,请看上面的checkDaoConfig(也可能是在实例化sqlSessionFactory过程中,通过xmlMapperBuilder加载进来的,两种方式最终都是调用mapperRegistry.addMapper方法加载进来)
初始化之后,因为MapperFactoryBean是FactoryBean,所以会执行MapperFactoryBean.getObject,

在这里插入图片描述
上面的getSqlSession()获取的就是上面创建的SqlSessionTemplate对象,这是mapper的SqlSession,然后继续执行SqlSessionTemplate.getMapper,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过jdk代理方式创建的mapper代理对象,代理实现类为MapperProxy,以后有人调用mapper方法,就会调用代理对象的invoke方法了。也就是MapperProxy的invoke方法了。
最后把实例化mapper的bean放到bean缓存池中,至此,实例化mapper结束。

然后就是前端发请求调用controller,到service,到mapper,到数据库,具体是怎么执行查询了。

5.执行mapper操作

执行mapper是调用的MapperProxy.invoke,最终调用的MapperMethod的execute方法
在这里插入图片描述
执行测试会执行MapperProxy类的invoke方法
在这里插入图片描述
在这里插入图片描述
测试为查询,且没有返回list之类的,所以查询一条
在这里插入图片描述

method和command来自MapperMethod的有参构造函数,去config中拿到sql类型和mapper中方法的全限定名
在这里插入图片描述
这里的sqlSession其实就是上面生成的sqlSessionTemplate,在初始化MapperMethod时候,通过SqlCommand从Configuration中的MappedStatement获取限定名和SQL类型(select|insert|update|delete)
因为sqlSessionTemplate是SqlSessionInterceptor代理创建的,所以,接下来走SqlSessionInterceptor.invoke方法,通过上面实例化sqlSessionTemplate可以看出,实际用的是反射机制,执行sqlSession方法。
在这里插入图片描述
继续执行selectone
在这里插入图片描述
在这里插入图片描述
这里创建的sqlSession是与数据库交互的会话sqlSession,注意与sqlSessionTemplate这个sqlSession的区别,然后通过method.invoke反射调用到具体的 DefaultSqlSession.selectList 方法。
在这里插入图片描述
在这里插入图片描述

最终调用Executor方法执行,Executor有两个方法:BaseExecutor 和 CachingExecutor,分别代表两种缓存机制,BaseExecutor 是一级缓存机制使用,CachingExecutor是二级缓存机制使用(如果查询二级缓存中没有结果,则会调用BaseExecutor的方法查询一级缓存,然后把查询结果放到二级缓存)。
所以先来到二级缓存查询
在这里插入图片描述
在这里插入图片描述
继续执行SimpleExecutor的query方法
在这里插入图片描述
在这里插入图片描述
然后执行doQuery
在这里插入图片描述

  @Overridepublic  List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {//获取到configuration Configuration configuration = ms.getConfiguration();//创建statementHandlerStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);//调用 prepareStatement() 方法 获取到 Statement 对象 (真正执行静态SQl的接口)stmt = prepareStatement(handler, ms.getStatementLog());//调用 StatementHandler.query() 方法执行return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}}

prepareStatement方法
在这里插入图片描述

在这里插入图片描述
这几个子类的意义:

SimpleStatementHandler ,这个对应的 就是JDBC 中常用到的 Statement 接口,用于简单SQL的处理

PreparedStatementHandler , 这个对应的就是JDBC中的 PreparedStatement,用于预编译SQL的处理

CallableStatementHandler , 这个对应JDBC中 CallableStatement ,用于执行存储过程相关的处理

RoutingStatementHandler,这个接口是以上三个接口的路由,没有实际操作,只是负责上面三个StatementHandler的创建及调用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后获取结果
在这里插入图片描述
在这里插入图片描述
这里为了测试改了下sql语句
在这里插入图片描述
数据库对应数据
在这里插入图片描述
这样一次完整的查询就走完了。
流程:
执行mapper方法,实际就是通过jdk代理机制,执行MapperMethod的execute方法。然后通过sqlSessionTemplate的jdk代理机制,执行method.invoke方法。然后通过反射机制,执行DefaultSqlSession.selectList方法。通过configuration.getMappedStatement获取MappedStatement,这时找到了实际的sql了,然后到了Executor模块。执行SimpleExecutor的doQuery方法,然后执行PreparedStatementHandler的query方法,最终调用jdbc操作执行sql,最后解析结果并返回。

6.总结

需要处理的问题应该就是:mapper是一个接口,接口不能实例化,也就是不能干活,想让他干活的话就要用动态代理,找一个代理类帮他干活。mapper.xml有具体的sql,想让它执行就要把mapper接口的方法和xml中的sql关联起来
springboot整合mybaits的问题

  • mapper怎么被spring注册为beanDefinition
  • mapper怎么被实例化
  • mapper方法怎么被执行

1.mapper怎么被spring注册为beanDefinition:

mapperscan注解或mapper注解,并且mybatis要重写isCandidateComponent方法,保证是接口也要生成beanDefinition

2.mapper是接口怎么被实例化

注册的时候将mapper接口的类型转换为MapperFactoryBean,然后通过MapperFactoryBean的getObject方法实现实例化(通过jdk代理生成了bean的代理对象)。

3.相关属性初始化

初始化的过程,创建SqlSessionFactory、SqlSessionTemplate、MapperScannerConfigurer的bean定义,放到IOC容器(beanFactory)中,这是基础。在此过程,通过MapperScannerConfigurer扫描指定包下的所有mapper接口生成beanDefinition,并放到bean定义注册表中(类型为:MapperFactoryBean)。

  • SqlSessionFactory
    初始化SqlSessionFactory主要是填充Configuration属性。这里有两个重要参数:MappedStatement和MapperRegistry。
    MappedStatement是解析mapper.xml后封装成的对象。通过XMLMapperBuilder实现。
    在这里也完成了mapper接口与mapper.xml的绑定。MapperRegistry类内的knownMappers缓存: key为namespace对应的dao的class,value为MapperProxyFactory。
  • SqlSessionTemplate
    使用SqlSessionTemplate构造器创建SqlSessionTemplate对象,其中用了jdk代理方式创建了SqlSession代理对象。也就是执行查询的时候调用SqlSessionTemplate的方法实际上调用的是SqlSession的方法。所有访问数据库的操作都是通过SqlSession来的。这么做是为了解耦Mapper和SqlSession。

4.执行mapper

执行mapper方法的过程,主要是先通过两个代理类,即先执行mapper代理实现类MapperProxy的invoke方法,然后执行SqlSessionTemplate代理实现类的invoke方法,然后进入DefaultSqlSession相应方法中,这里会根据mapper的限定名获取MappedStatement,然后调用Executor相应方法,而Executor是封装了jdbc的操作,所以最终是通过jdbc执行sql,最后再把执行的结果解析返回。

整个SpringBoot整合Mybatis的过程,就是在spring容器初始化的过程中生成mapper的代理对象,然后在执行mapper方法的过程,利用代理机制,执行目标方法,最终底层通过jdbc执行sql。

参考链接
https://blog.csdn.net/zhengguofeng0328/article/details/125945926
https://blog.csdn.net/u013521882/article/details/120624374

相关内容

热门资讯

长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
tag是什么意思 tab是什么... 一、B端基础控件的认识控件一词,直译的话可以翻译成 “用来控制的元件”,是我们对 B 端系统进行信息...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
tag是什么意思 tab是什么... 一、B端基础控件的认识控件一词,直译的话可以翻译成 “用来控制的元件”,是我们对 B 端系统进行信息...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...