iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >怎么在MyBatis中执行SQL语句
  • 797
分享到

怎么在MyBatis中执行SQL语句

2023-06-15 02:06:54 797人浏览 薄情痞子
摘要

这期内容当中小编将会给大家带来有关怎么在mybatis中执行sql语句,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。基础组件我们要理解 Mybatis 的执行过程,就必须先了解 Mybatis 中都有哪一

这期内容当中小编将会给大家带来有关怎么在mybatis中执行sql语句,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

基础组件

我们要理解 Mybatis 的执行过程,就必须先了解 Mybatis 中都有哪一些重要的类,这些类的职责都是什么?

SqlSession

我们都很熟悉,它对外提供用户和数据库之间交互需要使用的方法,隐藏了底层的细节。它默认是实现类是 DefaultSqlSession

Executor

这个是执行器,SqlSession 中对数据库的操作都是委托给它。它有多个实现类,可以使用不同的功能。

怎么在MyBatis中执行SQL语句

Configuration

它是一个很重要的配置类,它包含了 Mybatis 的所有有用信息,包括 xml 配置,动态 sql 语句等等,我们到处都可以看到这个类的身影。

MapperProxy

这是一个很重要的代理类,它代理的就是 Mybatis 中映射 SQL 的接口。也就是我们常写的 Dao 接口。

工作流程

初步使用

首先,我们需要得到一个 SqlSessionFactory 对象,该对象的作用是可以获取 SqlSession  对象。

// 读取配置InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();// 创建一个 SqlSessionFactory 对象SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);

当我们得到一个 SqlSessionFactory 对象之后,就可以通过它的 openSession 方法得到一个 SqlSession 对象。

 SqlSession sqlSession = sqlSessionFactory.openSession(true);

最后,我们通过 SqlSession 对象获取 Mapper ,从而可以从数据库获取数据。

// 获取 Mapper 对象HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);// 执行方法,从数据库中获取数据Hero hero = mapper.selectById(1);

详细流程

获取 MapperProxy 对象

我们现在主要关注的就是 getMapper 方法,该方法为我们创建一个代理对象,该代理对象为我们执行 SQL 语句提供了重要的支持。

// SqlSession 对象@Overridepublic <T> T getMapper(Class<T> type) {    return configuration.getMapper(type, this);}

getMapper  方法里面委托 Configuration 对象去获取对应的 Mapper 代理对象,之前说过 Configuration 对象里面包含了 Mybatis 中所有重要的信息,其中就包括我们需要的 Mapper 代理对象,而这些信息都是在读取配置信息的时候完成的,也就是执行sqlSessionFactoryBuilder.build 方法。

// Configuration 对象public <T> T getMapper(Class<T> type, SqlSession sqlSession) {    return mapperReGIStry.getMapper(type, sqlSession);}

我们可以看到它又将获取 Mapper 代理对象的操作委托给了 MapperRegistry 对象(搁着俄罗斯套娃呢?),这个 MapperRegistry 对象里面就存放了我们想要的 Mapper 代理对象,如果你这么想,就错了,实际上,它存放的并不是我们想要的 Mapper 代理对象,而是 Mapper 代理对象的工厂,Mybatis 这里使用到了工厂模式。

public class MapperRegistry {  private final Configuration config;  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();  public MapperRegistry(Configuration config) {    this.config = config;  }  @SuppressWarnings("unchecked")  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);    }  }  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<>(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);        }      }    }  }}

我只保留了 getMapper 方法和 addMapper 方法。

在 getMapper 方法中,它获取的是 MapperProxyFactory 对象,我们通过名称可以得出这是一个 Mapper 代理对象工厂,但是我们是要得到一个 MapperProxy 对象,而不是一个工厂对象,我们再来看 getMapper 方法,它通过 mapperProxyFactory.newInstance 来创建代理对象。

protected T newInstance(MapperProxy<T> mapperProxy) {    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);    return newInstance(mapperProxy);}

创建了一个 MapperProxy 对象,并且通过 Proxy.newProxyInstance 方法(不会还有人不知道这是 jdk 动态代理吧),创建一个代理对象处理,这个代理对象就是我们想要的结果。这里没有体现出来代理了哪个对象啊?其实 mapperInterface 这是一个成员变量,它引用了需要被代理的对象。而这个成员变量实在创建 MapperProxyFactory 对象的时候赋值的,所以我们每一个需要被代理的接口,在 Mybatis 中都会为它生成一个 MapperProxyFactory 对象,该对象的作用就是为了创建所需要的代理对象。

怎么在MyBatis中执行SQL语句

缓存执行方法

当我们获取到代理对象 mapper 之后,就可以执行它里面的方法。
这里使用一个例子:

// Myabtis 所需要的接口public interface HeroMapper {    Hero selectById(Integer id);}
// HeroMapper 接口所对应的 xml 文件<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"    "Http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="test.HeroMapper">    <select id="selectById" resultType="test.Hero">        select * from hero where id = #{id}    </select></mapper>

我们执行 selectById 方法,获取一个用户的信息。

// 获取 Mapper 对象HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);// 执行方法,从数据库中获取数据Hero hero = mapper.selectById(1);

通过上面的解析已经知道,这里的 mapper 是一个代理对象的引用,而这个代理类则是 MapperProxy,所以我们主要是去了解 MapperProxy 这个代理类做了什么事情。

public class MapperProxy<T> implements InvocationHandler, Serializable {      private final SqlSession sqlSession;  private final Class<T> mapperInterface;  private final Map<Method, MapperMethodInvoker> methodCache;  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {    this.sqlSession = sqlSession;    this.mapperInterface = mapperInterface;    this.methodCache = methodCache;  }  @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 {        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);      }    } catch (Throwable t) {      throw ExceptionUtil.unwrapThrowable(t);    }  }  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {      return methodCache.computeIfAbsent(method, m -> {           return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));      }  }      private static class PlainMethodInvoker implements MapperMethodInvoker {      private final MapperMethod mapperMethod;      public PlainMethodInvoker(MapperMethod mapperMethod) {          super();          this.mapperMethod = mapperMethod;      }      @Override      public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {          return mapperMethod.execute(sqlSession, args);      }  }}

代理对象执行方法时都是直接执行 invoke() 方法,在这个方法中,我们主要就看一条语句 cachedInvoker(method).invoke(proxy, method, args, sqlSession);

我们首先看 cachedInvoker 方法,它的参数是 Method 类型,所以这个 method 表示的就是我们执行的方法 HeroMapper.selectById,它首先从缓存中获取是否之前已经创建过一个该方法的方法执行器 PlainMethodInvoker 对象,其实这只是一个包装类,可有可无,在工程上来说,有了这个包装类,会更加易于维护。而这个执行器里面只有一个成员对象,这个成员对象就是 MapperMethod,并且这个 MapperMethod 的构造函数中需要传递  HeroMapper、HeroMapper.selectById、Cofiguration 这三个参数。

以上步骤都执行完成之后,接下来我们可以看到执行了 PlainMethodInvoker 的 invoke 方法,而它又将真正的操作委托给了 MapperMethod,执行 MapperMethod 下的 execute 方法,这个方法就是本文章的重点所在。

怎么在MyBatis中执行SQL语句

构造参数

从上面的解析可以知道,最后会执行到这个方法之中。

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);          if (method.returnsOptional()              && (result == null || !method.getReturnType().equals(result.getClass()))) {            result = Optional.ofNullable(result);          }        }        break;      case FLUSH:        result = sqlSession.flushStatements();        break;      default:        throw new BindingException("Unknown execution method for: " + command.getName());    }    return result;  }

这个方法中,我们可以看到熟悉的几个关键字:select、update、delete、insert,这个就是为了找到执行方式,我们因为是 select 语句,所以分支会走向 select,并且最终会执行到 sqlSession.selectOne 方法中,所以最终饶了一大圈,依然还是回到了我们一开始就提到的 SqlSession 对象中。
在这个方法中,首先会构造参数,也就是我们看到的 convertArgsToSqlCommandParam 方法,它的内部执行方式是按照如下方式来转换参数的:

使用 @param 自定义命名
amethod(@Param int a, @Param int b)  则会构造 map  ->  [{"a", a_arg}, {"b", b_arg}, {"param1",  a_arg}, {"param2", b_arg}],a 和 param1 是对参数 a 的命名,a_arg 是传递的实际的值。
虽然只有两个参数,但是最后却会在 Map 存在四个键值对,因为 Mybatis 最后自己会生成以 param 为前缀的参数名称,名称按照参数的位置进行命名。

不使用 @param

amethod(int a, int b),则会构造 map -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}],因为没有对参数进行自定义命名,所以 Myabtis 就对参数取了一个默认的名称,以 arg 为前缀,位置为后缀进行命名。

在参数只有一个,并且参数为集合的情况下,会存放多个键值对:

  • amethod(Collection<Integer> a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"collection", a_arg}]

  • amethod(List<Integer> a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"collection", a_arg}, {"list", a_arg}]

  • amethod(Integer[] a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"array", a_arg}]

  • 但是,如果有两个参数,那么就不会这么存放,而是按照常规的方式:

  • amethod(List<Integer> a,List<Integer> b)  则会构造 map -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}]

  • amethod(List<Integer> a,int b)  则会构造 map -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}]

不会作为参数的对象
在 Mybatis 中有两个特殊的对象:RowBounds、ResultHandler,这两个对象如果作为参数则不会放入到 map 中,但是会占据位置。

amethod(int a,RowBounds rb, int b),这种情况下,会构造 map -> [{"arg0", a_arg}, {"arg2", b_arg}, {"param1", a_arg}, {"param2", b_arg}]

注意这里的 b 参数的命名分别是 arg2 和 param2,arg2 是因为它的位置在参数的第 3 位,而 param2 则是因为它是第 2 个有效参数。

获取需要执行的 SQL 对象

参数构造完成之后,我们就需要寻找需要执行的 SQL 语句了。

@Override  public <T> T selectOne(String statement, Object parameter) {    // Popular vote was to return null on 0 results and throw exception on too many.    List<T> list = this.selectList(statement, parameter);    if (list.size() == 1) {      return list.get(0);    } else if (list.size() > 1) {      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());    } else {      return null;    }  }

这里的 statement 虽然是 String 类型的,但是它并不是真正的 SQL 语句,它是一个寻找对应 MapperStatement 对象的名称,在我们的例子中,它就是 test.HeroMapper.selectById ,Mybatis 通过这个名称可以寻找到包含了 SQL 语句的对象。

我们跟踪代码的执行,最后会来到下面这个方法,这是一个包含三个参数的重载方法。

@Override  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {    try {      MappedStatement ms = configuration.getMappedStatement(statement);      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);    } catch (Exception e) {      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);    } finally {      ErrorContext.instance().reset();    }  }

在第四行代码中,可以得知它通过 statement 从 Configuration 对象中获取了一个 MapperStatement 对象, MapperStatement 对象包含的信息是由 <select>、<update>、<delete> 、<insert> 元素提供的,我们在这些元素中定义的信息都会保存在该对象中,如:Sql 语句、resultMap、fetchSize 等等。

执行 SQL 语句

获取到包含 SQL 语句信息的对象之后,就会交给 Execute 执行器对象去执行后续的处理,也就是 executor.query 方法。

@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {    BoundSql boundSql = ms.getBoundSql(parameter);    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}

获取需要自行的 Sql 语句,然后创建一个缓存使用的 key,用于二级缓存。

@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {    // ....    // 跟缓存有关,如果缓存中存在数据,则直接从缓存中返回,否则从数据库中查询 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); return list;}

最后会执行到一个 doQuery 方法

@Overridepublic <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {    Statement stmt = null;    try {        Configuration configuration = ms.getConfiguration();        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);        stmt = prepareStatement(handler, ms.getStatementLog());        return handler.query(stmt, resultHandler);    } finally {        closeStatement(stmt);    }}

这段代码创建了一个 Statement 对象的处理器 StatementHandler,这个处理器主要的工作就是完成 JDBC 中 PrepareStatement 对象的一些准备工作,包括:创建 PrepareStatement 对象,设置需要执行的 sql 语句,为 sql 语句中的参数赋值。完成这些工作之后,就开始从数据库获取数据了。

@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {    PreparedStatement ps = (PreparedStatement) statement;    ps.execute();    return resultSetHandler.handleResultSets(ps);}

上述就是小编为大家分享的怎么在MyBatis中执行SQL语句了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注编程网精选频道。

--结束END--

本文标题: 怎么在MyBatis中执行SQL语句

本文链接: https://www.lsjlt.com/news/276806.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 怎么在MyBatis中执行SQL语句
    这期内容当中小编将会给大家带来有关怎么在MyBatis中执行SQL语句,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。基础组件我们要理解 Mybatis 的执行过程,就必须先了解 Mybatis 中都有哪一...
    99+
    2023-06-15
  • MyBatis中怎么执行SQL语句
    在MyBatis中执行SQL语句可以通过Mapper接口和映射文件来实现。以下是一般步骤: 1、创建Mapper接口:在Mapper...
    99+
    2024-03-07
    MyBatis
  • MyBatis中怎么执行动态SQL语句
    MyBatis中执行动态SQL语句的方式是通过使用动态SQL语句的标签来实现。在MyBatis中,有以下几种常用的动态SQL标签: ...
    99+
    2024-04-09
    MyBatis
  • MyBatis中SQL语句是怎么执行的
    MyBatis中的SQL语句执行流程大致如下: 配置文件:首先在配置文件(通常是mybatis-config.xml)中配置数据...
    99+
    2024-04-23
    MyBatis
  • MyBatis中怎么自定义SQL语句执行顺序
    在MyBatis中,无法直接控制SQL语句的执行顺序,因为MyBatis是一个持久层框架,它的执行顺序是由其内部的执行流程控制的。但...
    99+
    2024-04-23
    MyBatis SQL
  • db2中怎么执行sql语句
    在DB2中执行SQL语句有多种方法,以下是其中一种常用的方法: 打开DB2命令行终端或者连接到DB2数据库的客户端工具; 输入以下...
    99+
    2024-05-10
    DB2 sql
  • sql语句在mysql中是怎么执行的
    这篇文章主要介绍了sql语句在mysql中是怎么执行的,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一、mysql架构分析下面是mysql的...
    99+
    2024-04-02
  • 在PL/SQL块中怎么执行select语句
    这篇文章主要介绍“在PL/SQL块中怎么执行select语句”,在日常操作中,相信很多人在在PL/SQL块中怎么执行select语句问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希...
    99+
    2024-04-02
  • Java MyBatis是怎么执行一条SQL语句的
    今天小编给大家分享一下Java MyBatis是怎么执行一条SQL语句的的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下...
    99+
    2023-07-02
  • 一条SQL语句在MySQL中怎么执行的
    小编给大家分享一下一条SQL语句在MySQL中怎么执行的,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!一 MySQL 基础架构分...
    99+
    2024-04-02
  • MyBatis的SQL语句执行过程是什么
    这篇文章主要介绍“MyBatis的SQL语句执行过程是什么”,在日常操作中,相信很多人在MyBatis的SQL语句执行过程是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”MyBatis的SQL语句执行过程...
    99+
    2023-06-30
  • dbvisualizer怎么执行sql语句
    要在DBVisualizer中执行SQL语句,可以按照以下步骤操作:1. 连接到数据库:在DBVisualizer中,选择菜单栏的"...
    99+
    2023-10-19
    sql
  • psql怎么执行sql语句
    在psql中执行SQL语句有以下几种方式: 在命令行中直接输入SQL语句: psql -c "SELECT * FROM...
    99+
    2023-10-26
    psql sql
  • EntityFramework中执行sql语句
    一、为什么要在EF中执行SQL语句 使用EF操作数据库,可以避免写SQL语句,完成使用Linq实现,但为什么还要在EF中执行SQL语句呢。如果要写SQL语句,完全可以使用ADO.NE...
    99+
    2024-04-02
  • PostgreSql中怎么kill掉正在执行的sql语句
    这篇文章主要介绍“PostgreSql中怎么kill掉正在执行的sql语句”,在日常操作中,相信很多人在PostgreSql中怎么kill掉正在执行的sql语句问题上存在疑惑,小编查阅了各式资料,整理出简单...
    99+
    2024-04-02
  • 怎么在windows中批量执行Mysql的sql语句
    这篇文章给大家介绍怎么在windows中批量执行Mysql的sql语句,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。@ECHO OFF SET dbhost=主机名(例如:127.0.0....
    99+
    2023-06-14
  • Shell脚本中怎么执行sql语句
    本篇文章为大家展示了Shell脚本中怎么执行sql语句,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。1、将SQL语句直接嵌入到shell脚本文件中代码如下:--演示环境  [root@SZ...
    99+
    2023-06-09
  • mysql中怎么执行外部sql语句
    这篇文章将为大家详细讲解有关mysql中怎么执行外部sql语句,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。 mysql执行外部sql   ...
    99+
    2024-04-02
  • java中怎么用jdbc执行sql语句
    在Java中使用JDBC执行SQL语句的一般步骤如下:1. 加载数据库驱动程序(一般在应用程序的入口处执行):javaClass.f...
    99+
    2023-10-23
    java jdbc sql
  • 在mybatis执行SQL语句之前进行拦击处理实例
    比较适用于在分页时候进行拦截。对分页的SQL语句通过封装处理,处理成不同的分页sql。实用性比较强。import java.sql.Connection; import java.sql.PreparedStatement; import ...
    99+
    2023-05-31
    mybatis 拦击 batis
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作