0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

MyBatis的实现原理

lhl545545 来源:电子发烧友网 2018-02-24 11:25 次阅读

MyBatis的实现原理

mybatis底层还是采用原生jdbc来对数据库进行操作的,只是通过 SqlSessionFactory,SqlSession Executor,StatementHandler,ParameterHandler,ResultHandler和TypeHandler等几个处理器封装了这些过程

执行器:Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

参数处理器: ParameterHandler (getParameterObject, setParameters

结构处理器 ResultSetHandler (handleResultSets, handleOutputParameters)

sql查询处理器:StatementHandler (prepare, parameterize, batch, update, query)

其中StatementHandler用通过ParameterHandler与ResultHandler分别进行参数预编译 与结果处理。而ParameterHandler与ResultHandler都使用TypeHandler进行映射。如下图:

MyBatis的实现原理

Mybatis工作过程

通过读mybatis的源码进行分析mybatis的执行操作的整个过程,我们通过debug调试就可以知道Mybatis每一步做了什么事,我先把debug每一步结果 截图,然后在分析这个流程。

第一步:读取配置文件,形成InputStream

1 创建SqlSessionFacotry的过程

MyBatis的实现原理

从debug调试看出 返回的 sqlSessionFactory 是DefaultSesssionFactory类型的,但是configuration此时已经被初始化了。查看源码后画如下创建DefaultSessionFactory的时序图:

MyBatis的实现原理

2 创建SqlSession的过程

MyBatis的实现原理

从debug调试 看出SqlSessinoFactory.openSession() 返回的sqlSession是 DefaultSession类型的,此SqlSession里包含一个Configuration的对象,和一个Executor对象。查看源码后画如下创建DefaultSession的时序图:

MyBatis的实现原理

3 创建Mapper的过程

MyBatis的实现原理

从debug调试可以看出,mapper是一个Mapper代理对象,而且初始化了Configuration对象,Executor的对象。查看源码后画如下创建Mapper的时序图:

MyBatis的实现原理

4 执行CRUD过程

1 以select为例查看各步执行的源码

1.mapper.selectEmployeeList()其实是MapperProxy执行invoke方法,此方法显示是判断Method的方法是不是Object的toString等方法如果不是就执行MapperMethod

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// 判断Method的方法是不是Object的toString等方法

if(Object.class.equals(method.getDeclaringClass())) {

try {

return method.invoke(this, args);

} catch (Throwable var5) {

throw ExceptionUtil.unwrapThrowable(var5);

}

} else {

//判断private final Map

MapperMethod mapperMethod = this.cachedMapperMethod(method);

return mapperMethod.execute(this.sqlSession, args);

}

}

//查询一级缓存和设置一级缓存

private MapperMethod cachedMapperMethod(Method method) {

MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);

if(mapperMethod == null) {

mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());

this.methodCache.put(method, mapperMethod);

}

return mapperMethod;

}

经过上面的调用后进入MapperMethod里面执行

//判断sql命令类型

public Object execute(SqlSession sqlSession, Object[] args) {

Object param;

Object result;

if(SqlCommandType.INSERT == this.command.getType()) {

param = this.method.convertArgsToSqlCommandParam(args);

result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));

} else if(SqlCommandType.UPDATE == this.command.getType()) {

param = this.method.convertArgsToSqlCommandParam(args);

result = this.rowCountResult(sqlSession.update(this.command.getName(), param));

} else if(SqlCommandType.DELETE == this.command.getType()) {

param = this.method.convertArgsToSqlCommandParam(args);

result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));

} else if(SqlCommandType.SELECT == this.command.getType()) {

//我们测试的是select类型,则再判断这个方法的返回类型

if(this.method.returnsVoid() && this.method.hasResultHandler()) {

this.executeWithResultHandler(sqlSession, args);

result = null;

} else if(this.method.returnsMany()) {

//我们是查询列表,此方法执行

result = this.executeForMany(sqlSession, args);

} else if(this.method.returnsMap()) {

result = this.executeForMap(sqlSession, args);

} else {

param = this.method.convertArgsToSqlCommandParam(args);

result = sqlSession.selectOne(this.command.getName(), param);

}

} else {

if(SqlCommandType.FLUSH != this.command.getType()) {

throw new BindingException(“Unknown execution method for: ” + this.command.getName());

}

result = sqlSession.flushStatements();

}

if(result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {

throw new BindingException(“Mapper method ‘” + this.command.getName() + “ attempted to return null from a method with a primitive return type (” + this.method.getReturnType() + “)。”);

} else {

return result;

}

}

private

//将param做处理 自动处理为param1,param2.。

Object param = this.method.convertArgsToSqlCommandParam(args);

List result;

if(this.method.hasRowBounds()) {

RowBounds rowBounds = this.method.extractRowBounds(args);

//调用该对象的DefaultSqlSession的selectList方法

result = sqlSession.selectList(this.command.getName(), param, rowBounds);

} else {

result = sqlSession.selectList(this.command.getName(), param);

}

return !this.method.getReturnType().isAssignableFrom(result.getClass())?(this.method.getReturnType().isArray()?this.convertToArray(result):this.convertToDeclaredCollection(sqlSession.getConfiguration(), result)):result;

}

//处理参数方法

public Object convertArgsToSqlCommandParam(Object[] args) {

int paramCount = this.params.size();

if(args != null && paramCount != 0) {

if(!this.hasNamedParameters && paramCount == 1) {

return args[((Integer)this.params.keySet().iterator().next()).intValue()];

} else {

Map

int i = 0;

for(Iterator i$ = this.params.entrySet().iterator(); i$.hasNext(); ++i) {

Entry

param.put(entry.getValue(), args[((Integer)entry.getKey()).intValue()]);

String genericParamName = “param” + String.valueOf(i + 1);

if(!param.containsKey(genericParamName)) {

param.put(genericParamName, args[((Integer)entry.getKey()).intValue()]);

}

}

return param;

}

} else {

return null;

}

}

调用DefaultSqlSession的selectList的方法

public

List var5;

try {

//获取MappedStatement对象

MappedStatement ms = this.configuration.getMappedStatement(statement);

//调用cachingExecutor执行器的方法

var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

} catch (Exception var9) {

throw ExceptionFactory.wrapException(“Error querying database. Cause: ” + var9, var9);

} finally {

ErrorContext.instance().reset();

}

return var5;

}

//CachingExector的query方法

public

//

BoundSql boundSql = ms.getBoundSql(parameterObject);

CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);

//调用下2代码

return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}

//2代码

public

Cache cache = ms.getCache();

if(cache != null) {

this.flushCacheIfRequired(ms);

if(ms.isUseCache() && resultHandler == null) {

this.ensureNoOutParams(ms, parameterObject, boundSql);

List

if(list == null) {

//这里是调用Executor里的query方法 如果开启了缓存这掉CachingExecutor的 如果没有则是调用BaseExecutor的

list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

this.tcm.putObject(cache, key, list);

}

return list;

}

}

return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}

BaseExecutor的query方法

public

ErrorContext.instance().resource(ms.getResource()).activity(“executing a query”).object(ms.getId());

if(this.closed) {

throw new ExecutorException(“Executor was closed.”);

} else {

if(this.queryStack == 0 && ms.isFlushCacheRequired()) {

this.clearLocalCache();

}

List list;

try {

++this.queryStack;

list = resultHandler == null?(List)this.localCache.getObject(key):null;

if(list != null) {

this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

} else {

//如果缓存中没有就从数据库中查询

list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

} finally {

--this.queryStack;

}

if(this.queryStack == 0) {

Iterator i$ = this.deferredLoads.iterator();

while(i$.hasNext()) {

BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();

deferredLoad.load();

}

this.deferredLoads.clear();

if(this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {

this.clearLocalCache();

}

}

return list;

}

}

//从数据库中查询

private

//放入缓存

this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

List list;

try {

//此处是调用子Executor的方法,ExecutorType默认是使用的SimpleExecutor

list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

} finally {

this.localCache.removeObject(key);

}

this.localCache.putObject(key, list);

if(ms.getStatementType() == StatementType.CALLABLE) {

this.localOutputParameterCache.putObject(key, parameter);

}

return list;

}

SimpleExecutor的doQuery方法

public

Statement stmt = null;

List var9;

try {

Configuration configuration = ms.getConfiguration();

//创建StateMentHandler处理器

StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

//调用下3的方法

stmt = this.prepareStatement(handler, ms.getStatementLog());

//调用no4的方法

var9 = handler.query(stmt, resultHandler);

} finally {

this.closeStatement(stmt);

}

return var9;

}

//下3方法

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {

Connection connection = this.getConnection(statementLog);

Statement stmt = handler.prepare(connection);

//SatementHanlder 采用PreparedStatementHandler来实现此方法,而PreparedStatementHandler调用的是父接口ParameterHandler的方法

handler.parameterize(stmt);

return stmt;

}

ParameterHandler参数处理器的方法

public interface ParameterHandler {

Object getParameterObject();

//此方法是用DefaultParameterHandler实现的

void setParameters(PreparedStatement var1) throws SQLException;

}

DefaultParameterHandler默认参数处理器的方法

public void setParameters(PreparedStatement ps) {

ErrorContext.instance().activity(“setting parameters”).object(this.mappedStatement.getParameterMap().getId());

List

if(parameterMappings != null) {

for(int i = 0; i < parameterMappings.size(); ++i) {

ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);

if(parameterMapping.getMode() != ParameterMode.OUT) {

String propertyName = parameterMapping.getProperty();

Object value;

if(this.boundSql.hasAdditionalParameter(propertyName)) {

value = this.boundSql.getAdditionalParameter(propertyName);

} else if(this.parameterObject == null) {

value = null;

} else if(this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {

value = this.parameterObject;

} else {

MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);

value = metaObject.getValue(propertyName);

}

//这里用调用 TypeHandler类型映射处理器来映射

TypeHandler typeHandler = parameterMapping.getTypeHandler();

JdbcType jdbcType = parameterMapping.getJdbcType();

if(value == null && jdbcType == null) {

jdbcType = this.configuration.getJdbcTypeForNull();

}

try {

//类型处理器设置参数映射

typeHandler.setParameter(ps, i + 1, value, jdbcType);

} catch (TypeException var10) {

throw new TypeException(“Could not set parameters for mapping: ” + parameterMapping + “。 Cause: ” + var10, var10);

} catch (SQLException var11) {

throw new TypeException(“Could not set parameters for mapping: ” + parameterMapping + “。 Cause: ” + var11, var11);

}

}

}

}

}

no4的方法

public

//此处调用原生sql的处理器

PreparedStatement ps = (PreparedStatement)statement;

//发出原生sql命令

ps.execute();

//采用ResultHandler结果处理器对结果集封装

return this.resultSetHandler.handleResultSets(ps);

}12345678

ResultHandler代码

public interface ResultSetHandler {

//此处调用的是DefaultResultSetHandler的方法

void handleOutputParameters(CallableStatement var1) throws SQLException;

}

1234567

DefaultResultSetHandler的方法

public List

ErrorContext.instance().activity(“handling results”).object(this.mappedStatement.getId());

List

int resultSetCount = 0;

ResultSetWrapper rsw = this.getFirstResultSet(stmt);

List

int resultMapCount = resultMaps.size();

this.validateResultMapsCount(rsw, resultMapCount);

while(rsw != null && resultMapCount 》 resultSetCount) {

ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount);

this.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null);

rsw = this.getNextResultSet(stmt);

this.cleanUpAfterHandlingResultSet();

++resultSetCount;

}

String[] resultSets = this.mappedStatement.getResulSets();

if(resultSets != null) {

while(rsw != null && resultSetCount < resultSets.length) {

ResultMapping parentMapping = (ResultMapping)this.nextResultMaps.get(resultSets[resultSetCount]);

if(parentMapping != null) {

String nestedResultMapId = parentMapping.getNestedResultMapId();

ResultMap resultMap = this.configuration.getResultMap(nestedResultMapId);

this.handleResultSet(rsw, resultMap, (List)null, parentMapping);

}

rsw = this.getNextResultSet(stmt);

this.cleanUpAfterHandlingResultSet();

++resultSetCount;

}

}

return this.collapseSingleResultList(multipleResults);

}

//处理结果集

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List

try {

if(parentMapping != null) {

this.handleRowValues(rsw, resultMap, (ResultHandler)null, RowBounds.DEFAULT, parentMapping);

} else if(this.resultHandler == null) {

DefaultResultHandler defaultResultHandler = new DefaultResultHandler(this.objectFactory);

this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null);

multipleResults.add(defaultResultHandler.getResultList());

} else {

this.handleRowValues(rsw, resultMap, this.resultHandler, this.rowBounds, (ResultMapping)null);

}

} finally {

this.closeResultSet(rsw.getResultSet());

}

}

private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?》 resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {

if(resultMap.hasNestedResultMaps()) {

this.ensureNoRowBounds();

this.checkResultHandler();

this.handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);

} else {

this.handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);

}

}

private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?》 resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {

DefaultResultContext

this.skipRows(rsw.getResultSet(), rowBounds);

Object rowValue = null;

while(this.shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {

ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, (String)null);

CacheKey rowKey = this.createRowKey(discriminatedResultMap, rsw, (String)null);

Object partialObject = this.nestedResultObjects.get(rowKey);

if(this.mappedStatement.isResultOrdered()) {

if(partialObject == null && rowValue != null) {

this.nestedResultObjects.clear();

this.storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());

}

//获取行的值

rowValue = this.getRowValue(rsw, discriminatedResultMap, rowKey, (String)null, partialObject);

} else {

rowValue = this.getRowValue(rsw, discriminatedResultMap, rowKey, (String)null, partialObject);

if(partialObject == null) {

this.storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());

}

}

}

if(rowValue != null && this.mappedStatement.isResultOrdered() && this.shouldProcessMoreRows(resultContext, rowBounds)) {

this.storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());

}

}

String resultMapId = resultMap.getId();

Object resultObject = partialObject;

if(partialObject != null) {

MetaObject metaObject = this.configuration.newMetaObject(partialObject);

this.putAncestor(partialObject, resultMapId, columnPrefix);

this.applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);

this.ancestorObjects.remove(resultMapId);

} else {

ResultLoaderMap lazyLoader = new ResultLoaderMap();

resultObject = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);

if(resultObject != null && !this.typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {

MetaObject metaObject = this.configuration.newMetaObject(resultObject);

boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty();

if(this.shouldApplyAutomaticMappings(resultMap, true)) {

foundValues = this.applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;

}

foundValues = this.applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;

this.putAncestor(resultObject, resultMapId, columnPrefix);

foundValues = this.applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;

this.ancestorObjects.remove(resultMapId);

foundValues = lazyLoader.size() 》 0 || foundValues;

resultObject = foundValues?resultObject:null;

}

if(combinedKey != CacheKey.NULL_CACHE_KEY) {

this.nestedResultObjects.put(combinedKey, resultObject);

}

}

return resultObject;

}

private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) {

return resultMap.getAutoMapping() != null?resultMap.getAutoMapping().booleanValue():(isNested?AutoMappingBehavior.FULL == this.configuration.getAutoMappingBehavior():AutoMappingBehavior.NONE != this.configuration.getAutoMappingBehavior());

}

private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {

List

boolean foundValues = false;

if(autoMapping.size() 》 0) {

Iterator i$ = autoMapping.iterator();

while(true) {

//这里使用了内部类对参数和结果集进行映射

DefaultResultSetHandler.UnMappedColumAutoMapping mapping;

Object value;

do {

if(!i$.hasNext()) {

return foundValues;

}

mapping = (DefaultResultSetHandler.UnMappedColumAutoMapping)i$.next();

value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);

} while(value == null && !this.configuration.isCallSettersOnNulls());

if(value != null || !mapping.primitive) {

metaObject.setValue(mapping.property, value);

}

foundValues = true;

}

} else {

return foundValues;

}

}

private static class UnMappedColumAutoMapping {

private final String column;

private final String property;

private final TypeHandler<?》 typeHandler;

private final boolean primitive;

//此处才类型器对结果进行映射

public UnMappedColumAutoMapping(String column, String property, TypeHandler<?》 typeHandler, boolean primitive) {

this.column = column;

this.property = property;

this.typeHandler = typeHandler;

this.primitive = primitive;

}

}

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • mybatis
    +关注

    关注

    0

    文章

    60

    浏览量

    6711
收藏 人收藏

    评论

    相关推荐

    一文了解MyBatis的查询原理

    本文通过MyBatis一个低版本的bug(3.4.5之前的版本)入手,分析MyBatis的一次完整的查询流程,从配置文件的解析到一个查询的完整执行过程详细解读MyBatis的一次查询流程,通过本文
    的头像 发表于 10-10 11:42 1425次阅读

    Mybatis自动生成增删改查代码

    使用 mybatis generator 自动生成代码,实现数据库的增删改查。 1 配置Mybatis插件 在pom文件添加依赖: pluginsplugin
    的头像 发表于 01-13 15:43 1097次阅读
    <b class='flag-5'>Mybatis</b>自动生成增删改查代码

    Mybatis的内部设计介绍

    Mybatis源码分析-整体设计(一)
    发表于 06-06 09:43

    基于SpringBoot mybatis方式的增删改查实现

    SpringBoot mybatis方式实现增删改查
    发表于 06-18 16:56

    MyBatis的整合

    SpringBoot-15-之整合MyBatis-注解篇+分页
    发表于 10-28 08:09

    Mybatis是什么

    Mybatis第一讲
    发表于 06-04 15:33

    SSM框架在Web应用开发中的设计与实现 pdf下载

    表现层的功能,比如响应请求。Spring 框架主要起到容器的功能,整合了 SpringMVC 和 Mybatis实现层 与层之间的
    发表于 01-29 09:47 2次下载

    mybatis快速入门

    本文详细介绍了mybatis相关知识,以及mybatis快速入门步骤详解。
    的头像 发表于 02-24 09:41 3522次阅读
    <b class='flag-5'>mybatis</b>快速入门

    easy-mybatis Mybatis的增强框架

    ./oschina_soft/gitee-easy-mybatis.zip
    发表于 06-14 09:45 1次下载
    easy-<b class='flag-5'>mybatis</b> <b class='flag-5'>Mybatis</b>的增强框架

    Fluent Mybatis、原生MybatisMybatis Plus对比

    使用fluent mybatis可以不用写具体的xml文件,通过java api可以构造出比较复杂的业务sql语句,做到代码逻辑和sql逻辑的合一。不再需要在Dao中组装查询或更新操作,在xml或
    的头像 发表于 09-15 15:41 1432次阅读

    MyBatis-Plus为什么不支持联表

    `的所有功能`MyBatis Plus Join`同样拥有;框架的使用方式和`MyBatis Plus`一样简单,几行代码就能实现联表查询的功能
    的头像 发表于 02-28 15:19 2455次阅读
    <b class='flag-5'>MyBatis</b>-Plus为什么不支持联表

    MyBatis、JDBC等做大数据量数据插入的案例和结果

    30万条数据插入插入数据库验证 实体类、mapper和配置文件定义 不分批次直接梭哈 循环逐条插入 MyBatis实现插入30万条数据 JDBC实现插入30万条数据 总结   本文主要讲述通过
    的头像 发表于 05-22 11:23 1078次阅读
    <b class='flag-5'>MyBatis</b>、JDBC等做大数据量数据插入的案例和结果

    SpringBoot+Mybatis如何实现流式查询?

    使用mybatis作为持久层的框架时,通过mybatis执行查询数据的请求执行成功后,mybatis返回的结果集不是一个集合或对象,而是一个迭代器,可以通过遍历迭代器来取出结果集
    的头像 发表于 06-12 09:57 1271次阅读

    mybatis的dao能重载吗

    不同需求的数据操作。 重载是指在同一个类中定义了多个方法,它们具有相同的名称但具有不同的参数。重载允许使用相同的方法名来处理不同类型和数量的参数,以提供更加灵活的操作。 在MyBatis的DAO中,我们可以通过重载方法来实现不同类型和数量的参数。例如,可以定义一个根
    的头像 发表于 12-03 11:51 1282次阅读

    mybatis接口动态代理原理

    MyBatis是一款轻量级的Java持久化框架,它通过XML或注解配置的方式,将数据库操作与SQL语句解耦,提供了一种简单、灵活的数据访问方式。在MyBatis中,使用动态代理技术来实现接口的代理
    的头像 发表于 12-03 11:52 940次阅读