Mybatis总结(二)SqlSession创建以及查询过程(源码分析)

版权声明:本文为博主原创文章,转载添加原文链接 https://blog.csdn.net/qq_34190023/article/details/80879344

SqlSessionFactory是通过上个博文Mybatis总结(一)SqlSessionFactory初始化过程(源码分析)初始化创建的。

根据这个名字我们就知道是使用了工厂模式的设计。对于SqlSessionFactoryBuilder来说,解析完xml后,其任务就完成了,我们可以将其销毁。而对于SqlSessionFactory来说,由于创建它会耗费大量系统资源。所以,最好的做法是只创建一次,使用单例模式。

下面来看一下SqlSession是如何创建的

public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
  this.configuration = configuration;
}
@Override
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
@Override
public SqlSession openSession(boolean autoCommit) {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}
@Override
public SqlSession openSession(ExecutorType execType) {
  return openSessionFromDataSource(execType, null, false);
}
@Override
public SqlSession openSession(TransactionIsolationLevel level) {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), level, false);
}
@Override
public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
  return openSessionFromDataSource(execType, level, false);
}
@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
  return openSessionFromDataSource(execType, null, autoCommit);
}
@Override
public SqlSession openSession(Connection connection) {
  return openSessionFromConnection(configuration.getDefaultExecutorType(), connection);
}
@Override
public SqlSession openSession(ExecutorType execType, Connection connection) {
  return openSessionFromConnection(execType, connection);
}
@Override
public Configuration getConfiguration() {
  return configuration;
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
} finally {
      ErrorContext.instance().reset();
}
  }
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
  try {
      boolean autoCommit;
      try {
        autoCommit = connection.getAutoCommit();
      } catch (SQLException e) {
        // Failover to true, as most poor drivers or databases won't support transactions
         autoCommit = true;
      } 
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      final Transaction tx = transactionFactory.newTransaction(connection);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
 } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
      ErrorContext.instance().reset();
  }
}
private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
if (environment == null || environment.getTransactionFactory() == null) {
return new ManagedTransactionFactory();
}
return environment.getTransactionFactory();
}
private void closeTransaction(Transaction tx) {
if (tx != null) {
try {
        tx.close();
} catch (SQLException ignore) {
// Intentionally ignore. Prefer previous error.
}
    }
  }
}

Configuration保存了所有配置文集的详细信息

涉及的步骤:

(1)获取前面我们加载配置文件的环境信息,并且获取环境信息中配置的数据源。

(2)通过数据源获取一个连接,对连接进行包装代理(通过JDK的代理来实现日志功能)。

(3)设置连接的事务信息(是否自动提交、事务级别),从配置环境中获取事务工厂,事务工厂获取一个新的事务。

(4)传入事务对象获取一个新的执行器,并传入执行器、配置信息等获取一个执行会话对象。


public class Configuration {
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
} else {
    executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
    executor = new CachingExecutor(executor);
}
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

执行步骤如下:

(1)判断执行器类型,如果配置文件中没有配置执行器类型,则采用默认执行类型ExecutorType.SIMPLE。

(2)根据执行器类型返回不同类型的执行器(执行器有三种,分别是 BatchExecutor、SimpleExecutor和CachingExecutor)。

(3)跟执行器绑定拦截器插件(这里也是使用代理来实现)。


首先我们看一下SqlSession接口源码:

public interface SqlSession extends Closeable {
<T>T selectOne(String statement);
<T>T selectOne(String statement, Object parameter);
<E> List<E>selectList(String statement);
<E> List<E>selectList(String statement, Object parameter);
<E> List<E>selectList(String statement, Object parameter, RowBounds rowBounds);
<K, V> Map<K, V>selectMap(String statement, String mapKey);
<K, V> Map<K, V>selectMap(String statement, Object parameter, String mapKey);
<K, V> Map<K, V>selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
<T> Cursor<T>selectCursor(String statement);
<T> Cursor<T>selectCursor(String statement, Object parameter);
<T> Cursor<T>selectCursor(String statement, Object parameter, RowBounds rowBounds);
void select(String statement, Object parameter, ResultHandler handler);
void select(String statement, ResultHandler handler);
void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
int insert(String statement);
int insert(String statement, Object parameter);
int update(String statement);
int update(String statement, Object parameter);
int delete(String statement);
int delete(String statement, Object parameter);
void commit();
void commit(boolean force);
void rollback();
void rollback(boolean force);
List<BatchResult>flushStatements();
@Override
void close();
void clearCache();
Configuration getConfiguration();
<T>T getMapper(Class<T> type);
Connection getConnection();
}

其实现类:


DefaultSqlSession实现了SqlSession接口,里面有各种各样的SQL执行方法,主要用于SQL操作的对外接口,它会的调用执行器来执行实际的SQL语句。


public class DefaultSqlSession implements SqlSession {
@Override
public <E> List<E>selectList(String statement) {
return this.selectList(statement, null);
}
@Override
public <E> List<E>selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E>selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//1.根据Statement Id(缓存相关用到),在mybatis 配置对象Configuration中查找和配置文件相对应的MappedStatement
    MappedStatement ms = configuration.getMappedStatement(statement);
//2. 将查询任务委托给MyBatis 的执行器 Executor  
    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();
}
}

(1)根据SQL的ID到配置信息中找对应的MappedStatement,在之前配置被加载初始化的时候我们看到了系统会把配置文件中的SQL块解析并放到一个MappedStatement里面,并将MappedStatement对象放到一个Map里面进行存放,Map的key值是该SQL块的ID。

(2)调用执行器的query方法,传入MappedStatement对象、SQL参数对象、范围对象(此处为空)和结果处理方式。



MyBatis执行器Executor根据SqlSession传递的参数执行query()方法:

public abstract class BaseExecutor implements Executor {
protected Executor wrapper;
protected Transaction transaction;
@Override
public <E> List<E>query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1.根据具体传入的参数,动态地生成需要执行的SQL语句,用BoundSql对象表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2.为当前的查询创建一个缓存Key  
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
   return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E>query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) { throw new ExecutorException("Executor was closed."); }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++; // 避免缓存中没有数据,而清理缓存造成的浪费
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); // 从缓存中找
   } else {
     // 缓存中没有值,直接从数据库中读取数据
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
   }
  } finally {
  queryStack--;
  }
  if (queryStack == 0) { // 缓存之前没有值才执行
  for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
  }
  deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
              clearLocalCache();
     }
  }
  return list;
}
private <E> List<E>queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
//4. 执行查询,返回List 结果,然后将查询的结果放入缓存之中
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list); // 将数据真正放在缓存中
  if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
protected abstract <E> List<E>doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)  throws SQLException;
protected Connection getConnection(Log statementLog) throws SQLException {
  Connection connection = transaction.getConnection(); // 单例
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  } else {
    return connection;
  }
}
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
final Object cachedParameter = localOutputParameterCache.getObject(key);
    if (cachedParameter != null && parameter != null) {
      final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
      final MetaObject metaParameter = configuration.newMetaObject(parameter);
      for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
         if (parameterMapping.getMode() != ParameterMode.IN) {
          final String parameterName = parameterMapping.getProperty();
          final Object cachedValue = metaCachedParameter.getValue(parameterName);
          metaParameter.setValue(parameterName, cachedValue);
         }
      }
    }
  }
}
// 内部类
private static class DeferredLoad {
private final MetaObject resultObject;
  private final String property;
  private final Class<?>targetType;
  private final CacheKey key;
  private final PerpetualCache localCache;
  private final ObjectFactory objectFactory;
  private final ResultExtractor resultExtractor;

public DeferredLoad(MetaObject resultObject, String property,CacheKey key, PerpetualCache localCache,
Configuration configuration, Class<?> targetType) {
this.resultObject = resultObject;
    this.property = property;
    this.key = key;
    this.localCache = localCache;
    this.objectFactory = configuration.getObjectFactory();
    this.resultExtractor = new ResultExtractor(configuration, objectFactory);
    this.targetType = targetType;
}
public boolean canLoad() {
return localCache.getObject(key) != null &&localCache.getObject(key) != EXECUTION_PLACEHOLDER;
}
public void load() {
@SuppressWarnings( "unchecked" )
// we suppose we get back a List
List<Object> list = (List<Object>) localCache.getObject(key);
Object value = resultExtractor.extractObjectFromList(list, targetType);
resultObject.setValue(property, value);
}
}
。。。

默认情况下是采用SimpleExecutor执行的


public class SimpleExecutor extends BaseExecutor {
@Override
public <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对象来执行查询操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//6. 创建java.Sql.Statement对象,传递给StatementHandler对象
stmt = prepareStatement(handler, ms.getStatementLog());
//7. 调用StatementHandler.query()方法,返回List结果集
    return handler.<E>query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
//对创建的Statement对象设置参数,即设置SQL 语句中 ? 设置为指定的参数
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);// 设置SQL查询中的参数值
  return stmt;
}

上述的Executor.query()方法几经转折,最后会创建一个StatementHandler对象,然后将必要的参数传递给StatementHandler,使用StatementHandler来完成对数据库的查询,最终返回List结果集

doQuery方法的内部执行步骤:

(1)获取配置信息对象。

(2)通过配置对象获取一个新的StatementHandler,该类主要用来处理一次SQL操作。

(3)预处理StatementHandler对象,得到Statement对象。

(4)传入Statement和结果处理对象,通过StatementHandler的query方法来执行SQL,并对执行结果进行处理。 

public class Configuration {
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,ResultHandler resultHandler, BoundSql boundSql) {
  ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
  resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
  return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
/**
(1)根据相关的参数获取对应的StatementHandler对象。
(2)为StatementHandler对象绑定拦截器插件
*/
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}
public Executor newExecutor(Transaction transaction) {
  return newExecutor(transaction, defaultExecutorType);
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
} else {
    executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
    executor = new CachingExecutor(executor);
}
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

public class RoutingStatementHandler implements StatementHandler {
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}

根据 MappedStatement对象的StatementType来创建不同的StatementHandler,这个跟前面执行器的方式类似。StatementType有STATEMENT、PREPARED和CALLABLE三种类型,跟JDBC里面的Statement类型一一对应。

Executor的功能和作用是:

(1、根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用;

(2、为查询创建缓存,以提高性能

(3、创建JDBC的Statement连接对象,传递给StatementHandler对象,返回List查询结果。

StatementHandler对象负责设置Statement对象中的查询参数、处理JDBC返回的resultSet,将resultSet加工为List 集合返回:

StatementHandler对象主要完成两个工作:

(1. 对于JDBC的PreparedStatement类型的对象,创建的过程中,使用的是SQL语句字符串会包含若干个? 占位符,其后再对占位符进行设值。StatementHandler通过parameterize(statement)方法对Statement进行设值;       

(2.StatementHandler通过List<E> query(Statement statement,ResultHandler resultHandler)方法来完成执行Statement,和将Statement对象返回的resultSet封装成List;

public interface StatementHandler {
  Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
  void parameterize(Statement statement) throws SQLException;
  void batch(Statement statement) throws SQLException;
  int update(Statement statement)throws SQLException;
<E> List<E>query(Statement statement, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E>queryCursor(Statement statement) throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}

public abstract class BaseStatementHandler implements StatementHandler {
protected final ParameterHandler parameterHandler;

public class PreparedStatementHandler extends BaseStatementHandler {
@Override
public void parameterize(Statement statement) throws SQLException {
  //使用ParameterHandler对象来完成对Statement的设值
parameterHandler.setParameters((PreparedStatement) statement);
}

  Object getParameterObject();
  void setParameters(PreparedStatement ps) throws SQLException;
}

public class DefaultParameterHandler implements ParameterHandler {
//  实现对某一个Statement进行设置参数
@Override
public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) { 
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
          value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
} else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 每一个Mapping都有一个TypeHandler,根据TypeHandler来对preparedStatement进行设置参数
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 设置参数
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
      }
    }
  }
}
…….

StatementHandler 的parameterize(Statement) 方法调用了 ParameterHandler的setParameters(statement) 方法,

ParameterHandler的setParameters(Statement)方法负责根据我们输入的参数,对statement对象的 ? 占位符处进行赋值。


public class PreparedStatementHandler extends BaseStatementHandler {
@Override
public <E> List<E>query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;// 使用的是底层的jdbc
ps.execute();
// 结果交给ResultHandler来处理
  return resultSetHandler.<E>handleResultSets(ps);
}


public class DefaultResultSetHandler implements ResultSetHandler {
@Override
public List<Object>handleResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
  final List<Object> multipleResults = new ArrayList<Object>();
  int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
  String[] resultSets = mappedStatement.getResultSets();
  if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
      ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
      if (parentMapping != null) {
        String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
      rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
  }
return collapseSingleResultList(multipleResults);
}
。。。。

  ResultSetWrapper是ResultSet的包装类,调用getFirstResultSet方法获取第一个ResultSet,同时获取数据库的MetaData数据,包括数据表列名、列的类型、类序号等,这些信息都存储在ResultSetWrapper类中了。然后调用handleResultSet方法来来进行结果集的封装。

StatementHandler 的List<E> query(Statement statement,ResultHandler resultHandler)方法的实现,是调用了ResultSetHandler的handleResultSets(Statement) 方法。ResultSetHandler的handleResultSets(Statement) 方法会将Statement语句执行后生成的resultSet 结果集转换成List<E> 结果集



猜你喜欢

转载自blog.csdn.net/qq_34190023/article/details/80879344