Mybatis-01 原理

news2024/11/16 17:41:04

一. JDBC式编程

在 jdbc 编程中,我们最常用的是 PreparedStatement 式的编程,我们看下面这个例子;

Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;

try {
    // 1. 注册驱动
    Class.forName("com.mysql.jdbc.Driver");

    // 2. 获取连接
    String url = "jdbc:mysql://localhost:3306/zhuce?serverTimezone=UTC";
    String user = "root";
    String password = "19991121zq";
    conn = DriverManager.getConnection(url, user, password);

    String sql = "select * from t_user order by id ?";

    // 3. 获取数据库连接对象
    ps = conn.prepareStatement(sql);
    ps.setString(1, keyWords);

    // 4. 执行 sql 语句
    rs = ps.executeQuery();

    // 5. 处理查询结果集
    while(rs.next()){
        System.out.println(rs.getInt("id"));
    }
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} catch (SQLException throwables) {
    throwables.printStackTrace();
} finally {
    //6.关闭资源
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
    if (ps != null) {
        try {
            rs.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
    if (conn != null) {
        try {
            rs.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

二. 解析Configuration

如果我们的 mybatis 配置文件使用 xml 的方式,没有使用 Spring yaml 配置项的形式,可以通过 XMLMapperBuilder 来看解析 Configuration 的过程;

//---------------------------------XMLConfigBuilder----------------------------
public Configuration parse() {
    // 确保每个 xml 配置文件只被解析一次
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    
    // 解析 xml 配置文件中的根节点: configuration
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}



//---------------------------------XMLConfigBuilder-----------------------------
private void parseConfiguration(XNode root) {
    try {
		// 可以看出顺序是固定好的
        propertiesElement(root.evalNode("properties"));
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        loadCustomLogImpl(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        
        // 解析 plugins 标签
        pluginElement(root.evalNode("plugins"));
        
        // 解析 MetaObject 中会使用到的几个 Factory,了解
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);
		
        // 解析 environments 标签
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        
        // 解析 mappers 标签
        // 我们主要看此处
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException();
    }
}

1. 解析mappers

我们为什么要重点看解析 mappers 的地方,因为这里有一个很重要的对象 MappedStatement,我们在 Executor 中全程都会使用到该对象;

我们简单看下:

//-----------------------------XMLMapperBuilder-----------------------------
private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        
        // 解析 mapper 标签中的 select|insert|update|delete 标签
        // 会把每个 select、update 等解析成一个个 MappedStatement 对象
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        //...
    }
}



//-----------------------------XMLMapperBuilder-----------------------------
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}


//-----------------------------XMLMapperBuilder-----------------------------
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        XMLStatementBuilder statementParser = 
            new XMLStatementBuilder(configuration, builderAssistant, context);
        try {
            // 开始解析 select、update、insert 成 MappedStatement
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

我们以如下 StudentMapper.xml 为例来看 MappedStatement 的生成;

该 select 标签的 id 为 “selectStudent”,namespace 为 “com.bjpowernode.dao.StudentDao”;

<mapper namespace="com.bjpowernode.dao.StudentDao">

    <select id="selectStudent" resultType="com.bjpowernode.domain.Student">
        select id,name,email,age from student where id=#{id}
    </select>

</mapper>

解析如上的 select 标签如下:

// --------------------------- XMLStatementBuilder -------------------------
public void parseStatementNode() {
    
    // 1. 此时该 id = "selectStudent"
    String id = context.getStringAttribute("id");

    // ...

    
	// 2. 生成 SqlSource 对象
    // 该 sqlSource 包含了原始sql、参数映射等
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    // 3. 调用 builderAssistant 协助创建 MappedStatement 对象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, 
                                        sqlCommandType, fetchSize, timeout, 
                                        parameterMap, parameterTypeClass, 
                                        resultMap, resultTypeClass,
                                        resultSetTypeEnum, flushCache, 
                                        useCache, resultOrdered,
                                        keyGenerator, keyProperty, 
                                        keyColumn, databaseId, 
                                        langDriver, resultSets);
}




// --------------------------- XMLStatementBuilder -------------------------
public MappedStatement addMappedStatement(
    String id, SqlSource sqlSource, StatementType statementType,
    SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout,
    String parameterMap, Class<?> parameterType, String resultMap,
    Class<?> resultType, ResultSetType resultSetType, boolean flushCache,
    boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator,
    String keyProperty, String keyColumn, String databaseId,
    LanguageDriver lang, String resultSets) {

    // 1. 重新生成 id
    // 此时的 id = id + namespace
    // 也就是 id = "com.bjpowernode.dao.StudentDao.selectStudent"
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    // 2. 创建出 MappedStatement 对象
    MappedStatement statement = statementBuilder.build();
    
    // 3. 将当前 MappedStatement 对象加入到 configuration 配置类中
    configuration.addMappedStatement(statement);
    return statement;
}




// ----------------------------- Configuration --------------------------------
public void addMappedStatement(MappedStatement ms) {
    // 将 MappedStatement 对象放入到 configuration 中的 mappedStatements 中
    // mappedStatements 是一个 Map 对象,可以看到 key 是 MappedStatement.id
    mappedStatements.put(ms.getId(), ms);
}

可以把 MappedStatement 认为是一条 select、insert、update 等标签的映射对象;

我们看下 SqlSource 和 MappedStatement 对象,MappedStatement 对象中聚合了 SqlSource 对象;共同点是两个对象中都聚合了 Configuration 对象

SqlSource;

在这里插入图片描述

MappedStatement;

在这里插入图片描述

三. Executor执行器

1. Executor执行器的种类

对于 Executor,我们需要知道一点,SqlSession 只是门面而已,它聚合了一个 Executor 对象,真正执行 sql 的是 Executor 而非 SqlSession;

  • CachingExecutor:处理二级缓存,采用了装饰者模式;CachingExecutor 也实现了 Executor 接口,并且内部聚合了一个 BaseExecutor 的实现类;
  • BaseExecutor:处理一级缓存,它是一个抽象类,其他的 Executor 都是继承自该类;
    • 该类定义了一个抽象方法如 query(),真正执行查询逻辑的是子类的 doQuery();
    • 该类最大的作用就是用来执行一级缓存的查询,该执行器聚合了一个一级缓存 Cache;
  • SimpleExecutor:简单执行器,默认使用的是这个执行器,我们重点关注这个;
  • ReuseExecutor:可重用的执行器;
  • BatchExecutor:批处理的执行器;

类图如下:

在这里插入图片描述

2. Executor和SqlSession的对应关系

一个 SqlSession 对应一个 Executor;每创建一个 SqlSession,都会创建一个对应的 Executor 对象;

这时为啥呢?我们直接看 DefaultSqlSessionFactory.openSession() 创建 SqlSession 对象的方法就知道了;

// ------------------------ DefaultSqlSessionFactory -------------------------
public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
}



// ------------------------ DefaultSqlSessionFactory -------------------------
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Environment environment = configuration.getEnvironment();
    TransactionFactory transactionFactory = 
        getTransactionFactoryFromEnvironment(environment);
    Transaction tx = transactionFactory
        .newTransaction(environment.getDataSource(), level, autoCommit);
    
    // 1. 通过 configuration 创建出 Executor 对象
    Executor executor = configuration.newExecutor(tx, execType);
    
    // 2. 根据 executor 创建出 SqlSession 对象
    return new DefaultSqlSession(configuration, executor, autoCommit);
}



// ----------------------------- Configuration -------------------------------
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    
    // 1. 根据 executorType 的类型创建出不同的 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);
    }
    
    // 如果开启了二级缓存的话,还会包装一层,包装为 CachingExecutor
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    
    // 2. 如果需要插件增强的话,给 executor 做一层增强,并返回增强后的 executor 对象
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

四. 一级缓存

1. 一级缓存PerpetualCache

一级缓存是在 BaseExecutor 类中作为成员变量存在的

// ------------------------- BaseExecutor -----------------------------
public abstract class BaseExecutor implements Executor {
    private static final Log log = LogFactory.getLog(BaseExecutor.class);

    protected Transaction transaction;
    protected Executor wrapper;

    protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;

    // localCache 就是我们常说的一级缓存
    // 可以看到它是一个 PerpetualCache 类对象
    protected PerpetualCache localCache;
    protected PerpetualCache localOutputParameterCache;
    protected Configuration configuration;

    protected int queryStack;
    private boolean closed;

    // ...
}

我们看下这个 PerpetualCache 类;

可以看到它内部有一个 HashMap,非常简单,Mybatis 的一级缓存可以看做就是一个简单的 HashMap;

public class PerpetualCache implements Cache {

    private final String id;

    private Map<Object, Object> cache = new HashMap<>();

    public PerpetualCache(String id) {
        this.id = id;
    }

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object getObject(Object key) {
        return cache.get(key);
    }

    @Override
    public Object removeObject(Object key) {
        return cache.remove(key);
    }

    @Override
    public void clear() {
        cache.clear();
    }
}

2. 一级缓存的命中场景

一级缓存生效的几个条件,其实和 CacheKey 有关:

1、mappedStatementId 必须一样;

2、sql 必须一样;

3、RowBounds.offset 分页 offset 必须一样;

4、RowBounds.limit 分页 limit 必须一样;

5、查询参数 param 必须一样;

6、enviroment 环境参数必须一样,Mybatis 配置文件里的 enviroment,一般都是唯一值;

举例如下:

public static void test1(){
    StudentDao mapper = sqlSession.getMapper(StudentDao.class);
    
    // 1. sql和参数相同
    Student student = mapper.selectStudent(1001);
    Student student1 = mapper.selectStudent(1001);
    System.out.println(student == student1);     //true

    
    // 2. 必须是相同的 statementId
    //  com.bjpowernode.dao.StudentDao.selectById
    //  com.bjpowernode.dao.StudentDao.selectById3
    Student student2 = mapper.selectById(1001);
    Student student3 = mapper.selectById3(1001);
    System.out.println(student2 == student3);    //false

    
    // 3. sqlSession 必须一样
    // 严格来说应该是 sqlSession 里的 Executor 必须一样
    // 因为一级缓存 cache 是在 Executor 里的
    // 调用 openSession() 会创建出新的 DefaultSqlSession 和 Executor 对象
    Student s4 = mapper.selectById(1001);
    Student s5 = factory.openSession().getMapper(StudentDao.class).selectById3(1001);
    System.out.println(s4 == s5);                //false

    
    // 4. RowBounds 返回行的范围必须相同
    // RowBounds 的默认值是:RowBounds.DEFAULT
    Student student6 = mapper.selectById(1001);
    String statementId = "com.bjpowernode.dao.StudentDao.selectById";
    RowBounds rowBounds = new RowBounds(0, 10);
    List<Student> stus = sqlSession.selectList(statementId, 1001, rowBounds);
    System.out.println(student6 == stus.get(0));  //false
}

2.1 CacheKey

我们上面讲的缓存命中场景,其实都是和 CacheKey 有关的,下面我们简单看下这个生成 CacheKey 类对象的地方;

// ------------------------------ BaseExecutor --------------------------------
public <E> List<E> query(MappedStatement ms, 
                         Object parameter, 
                         RowBounds rowBounds, 
                         ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    
    // 生成 CacheKey 对象
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}



// ------------------------------ BaseExecutor --------------------------------
public CacheKey createCacheKey(MappedStatement ms, 
                               Object parameterObject, 
                               RowBounds rowBounds, 
                               BoundSql boundSql) {
    CacheKey cacheKey = new CacheKey();
    
    // 1. mappedStatementId
    cacheKey.update(ms.getId());
    
    // 2. RowBounds.offset
    cacheKey.update(rowBounds.getOffset());
    
    // 3. RowBounds.limit
    cacheKey.update(rowBounds.getLimit());
    
    // 4. sql
    cacheKey.update(boundSql.getSql());
    
    // ....
    // 5. 参数 params
    
    if (configuration.getEnvironment() != null) {
        // 6. environment
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}

可以看到 CacheKey 和 6 个变量的值有关,我们举一个例子看看:

在这里插入图片描述

3. 一级缓存的失效场景

一级缓存的失效,大致有以下几种场景:

1、手动清空了一级缓存 cache:sqlSession 的 clearCache()、commit()、rollback() 都会清空一级缓存;

2、执行了 sqlSession.update(),只要执行了 sqlSession.update(),都会清空一级缓存,因为 update() 方法中会做一个清空一级缓存的动作;

3、全局将一级缓存的作用域改为了 STATEMENT,默认是 SESSION 级别的,也就是默认是 SqlSession 级别的;了解即可;

举例如下:

public static void test2(){
    StudentDao mapper = sqlSession.getMapper(StudentDao.class);
    
    // 1. 手动清空一级缓存
    Student student = mapper.selectById(1001);
    sqlSession.clearCache();
    // sqlSession.commit();
    // sqlSession.rollback();
    Student student1 = mapper.selectById(1001);
    System.out.println(student == student1);    // false

    
    // 2.执行了 sqlSession.update()
    // 只要执行过 update 语句,都会清空当前一级缓存
    Student student2 = mapper.selectById3(1001);
    mapper.updateStudent(1002,"zq");
    Student student3 = mapper.selectById3(1001);
    System.out.println(student2 == student3;    // false
}

4. 一级缓存的调用流程

从上面我们知道一级缓存是在 BaseExecutor 中的,我们直接看 BaseExecutor 的 query();

// ------------------------------ BaseExecutor --------------------------------
public <E> List<E> query(MappedStatement ms, 
                         Object parameter, 
                         RowBounds rowBounds, 
                         ResultHandler resultHandler, 
                         CacheKey key, 
                         BoundSql boundSql) throws SQLException {
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        // 如果当前为第一次调用,且当前 MappedStatement 需要清空缓存,则清空一级缓存
        // queryStack 是用于嵌套子查询的,后面会讲
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        
        // 1. 先去一级缓存中查,如果缓存中有值直接返回
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 2. 一级缓存中没有值,去查数据库
            list = 
                queryFromDatabase(ms,parameter,rowBounds,resultHandler,key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // 如果当前的缓存范围是 STATEMENT, 执行完查询后清空缓存
            clearLocalCache();
        }
    }
    return list;
}



// ------------------------------ BaseExecutor --------------------------------
private <E> List<E> queryFromDatabase(MappedStatement ms, 
                                      Object parameter, 
                                      RowBounds rowBounds, 
                                      ResultHandler resultHandler, 
                                      CacheKey key, 
                                      BoundSql boundSql) throws SQLException {
    List<E> list;
    // 1. 将 key 和对应的占位符放入一级缓存
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        
        // 2. 调用 doQuery() 获取查询结果
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 3. 将 key 对应的占位符移除
        localCache.removeObject(key);
    }
    
    // 4. 将查询结果放入一级缓存
    localCache.putObject(key, list);
    
    // 5. 返回查询结果
    return list;
}



// ------------------------------ BaseExecutor --------------------------------
public int update(MappedStatement ms, Object parameter) throws SQLException {
    // 1. 先清空一级缓存
    // 为什么说执行了 sqlSession.update() 一级缓存会失效,因为会先清空一级缓存
    clearLocalCache();
    
    // 2. 执行 doUpdate() 并返回值
    return doUpdate(ms, parameter);
}

五. 二级缓存

二级缓存是在 CachingExecutor 中的,二级缓存和一级缓存不一样,二级缓存需要开启才会使用;

我们暂时不去看二级缓存,了解即可;

六. StatementHandler

1. 种类和结构

StatementHandler 的种类和结构大致如下:

我们主要看 PreparedStatementHandler 这个对象:创建 PreparedStatement、设置参数,执行 sql 都是咋 StatementHandler 中执行的;

在这里插入图片描述

2. 分析

我们直接进入到 SimpleExecutor 的 doQuery();

// ------------------------- SimpleExecutor -------------------------------
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();
        
        // 1. 创建出 StatementHandler 对象
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        
        // 2. 得到预编译对象 PreparedStatement
        // 该方法还会进行参数的映射和设置
        stmt = prepareStatement(handler, ms.getStatementLog());
        
        // 3. 通过 statementHandler.query() 执行 sql
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}

下面我们对 doQuery() 中三个步骤做简要分析;

2.1 创建出StatementHandler对象

通过 Configuration 对象创建出 StatementHandler 对象;

// ------------------------- Configuration -------------------------------
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    
    // 1. 创建出 RoutingStatementHandler
    // 这里又是装饰器模式
    // 其实 RoutingStatementHandler 中真正作用的是 PreparedStatementHandler
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    
    // 2. 插件增强
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}



// ------------------------- RoutingStatementHandler -----------------------------
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:
            // 默认创建的就是 PreparedStatementHandler 对象
            // 这里又是装饰器模式
            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");
    }

}

2.2 得到PreparedStatement对象

我们看一下 prepareStatement() 干了啥;

// ------------------------- SimpleExecutor -------------------------------
private Statement prepareStatement(StatementHandler handler, Log statementLog) {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    
    // 1. 执行 PreparedStatementHandler.prepare() 获得 PreparedStatementHandler 对象
    stmt = handler.prepare(connection, transaction.getTimeout());
    
    // 2. 对参数进行映射处理和设值
    // 这里后面我们会重点看!!!
    handler.parameterize(stmt);
    return stmt;
}


// ------------------------- BaseStatementHandler -------------------------------
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
        // 实例化 statement 对象
        statement = instantiateStatement(connection);
        setStatementTimeout(statement, transactionTimeout);
        setFetchSize(statement);
        return statement;
    } catch (Exception e) {
        closeStatement(statement);
        throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
}



// ------------------------- PreparedStatementHandler -----------------------------
protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
        // ...
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
        // 直接执行 connection.prepareStatement(sql)
        // 预编译 sql 得到 PreparedStatement 对象
        // 这里就是我们 jdbc 中预编译得到 PreparedStatement 的方式
        return connection.prepareStatement(sql);
    } else {
        // ...
    }
}

2.3 执行statementHandler.query()

我们简单看下 statementHandler.query() 干了啥;

public <E> List<E> query(Statement statement, ResultHandler resultHandler) {
    PreparedStatement ps = (PreparedStatement) statement;
    
    // 1. 直接执行 ps.execute()
    ps.execute();
    
    // 2. 通过 resultSetHandler 处理结果集
    return resultSetHandler.handleResultSets(ps);
}

七. Mapper代理类对象

1. UserMapper分析

Spring 为我们生成的 UserMapper 是一个代理类;我们在分析 Mybatis 集成 Spring 后一级缓存失效的场景中已经给 UserMapper 做过一个分析了,我们直接看 UserMapper 的结构;

1、这个 UserMapper 是一个代理类,它的 InvocationHandler 是 MapperProxy 类;

2、MapperProxy 内聚合了一个 SqlSessionTemplate 类,SqlSessionTemplate 实现了 SqlSession 类;

3、MapperProxy 的 invoke() 最终会调用目标类 sqlSessionTemplate 的方法;

4、SqlSessionTemplate 内聚合了一个 sqlSessionProxy,sqlSessionProxy 也是一个代理类,它实现了 SqlSession 类,它的 invocationHandler 是 SqlSessionInterceptor 类;

在这里插入图片描述

类图分析如下,其实代理的目标类是 SqlSession,也就是说干活的类是 SqlSession;

在这里插入图片描述

需要注意两个 InvocationHandler:MapperProxy 和 SqlSessionInterceptor,SqlSessionInterceptor 我们在一级缓存失效的场景中分析过了,这次我们重点看 MapperProxy;

2. MapperProxy

单看 MapperProxy 这个类名,可能认为它是一个代理类对象,其实它是一个 InvocationHandler 对象,我们看下它的 invoke();

// ------------------------------- MapperProxy ---------------------------------
public class MapperProxy<T> implements InvocationHandler {
    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, 
                       Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else if (isDefaultMethod(method)) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        
        // 1. 创建或从缓存中得到 MapperMethod 对象
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        
        // 2. 执行 mapperMethod.execute()
        return mapperMethod.execute(sqlSession, args);
    }
}

主要是执行 MapperMethod.execute(sqlSession, args);

我们先看下 MapperMethod 的结构,其中 paramNameResolver 用于解析参数对象,后面我们会重点看;

在这里插入图片描述

mapperMethod.execute() 如下:

// ------------------------------- MapperMethod ---------------------------------
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()) {
                // ...
            } else {
                
                // 我们直接看这个地方
                // 1. 处理参数 Object[] args,得到处理后的参数 Object param
                Object param = method.convertArgsToSqlCommandParam(args);
                
                // 2. 调用 sqlSession.selectxxx() 得到 result
                // 可以看到参数已经是我们上面得到的 param,不再是 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();
    }
    if (result == null 
        && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
        throw new BindingException();
    }
    return result;
}



// ------------------------------- MethodSignature ---------------------------------
public Object convertArgsToSqlCommandParam(Object[] args) {
    // 调用 paramNameResolver.getNamedParams(args) 解析得到新的参数对象
    return paramNameResolver.getNamedParams(args);
}

参数解析是在 ParamNameResolver 中做的,我们重点看下 ParamNameResolver;

2.1 参数解析ParamNameResolver

2.1.1 构造函数

我们先看下它的构造函数;

// ------------------------- ParamNameResolver ---------------------------
public ParamNameResolver(Configuration config, Method method) {
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    
    // 轮询解析方法中的参数
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        if (isSpecialParameter(paramTypes[paramIndex])) {
            continue;
        }
        String name = null;
        
        // 1. 如果该参数前有 @Param 注解,name 值为 @Param 注解的 value 值
        for (Annotation annotation : paramAnnotations[paramIndex]) {
            if (annotation instanceof Param) {
                // 方法参数上有 @Param 注解的情况下,hasParamAnnotation 置为 true
                hasParamAnnotation = true;
                name = ((Param) annotation).value();
                break;
            }
        }
        
        // 2. 如果参数前没有 @Param 注解
        if (name == null) {
            // 3. 默认情况下 Mybatis 中 config 的 useActualParamName 默认值为 true
            // 所以默认情况下 name 值为方法参数名
            if (config.isUseActualParamName()) {
                name = getActualParamName(method, paramIndex);
            }
            if (name == null) {
                name = String.valueOf(map.size());
            }
        }
        
        // map 中值的形式如 {{0, "id"}, {1, "name"}}
        map.put(paramIndex, name);
    }
    
    // 4. names 也是一个 Map,所以 names 中值的形式如 {{0, "id"}, {1, "name"}}
    names = Collections.unmodifiableSortedMap(map);
}

names 的值跟有无 @Param 注解有关,我们可以看如下场景中 names 的值;

1、没有 @Param 注解,但是给 Java 加上了 -parameters,names 值为:{{0, “id”}, {1, “username”}}

2、没有 @Param 注解,没有给 Java 加上了 -parameters,names 值为:{{0, “arg0”}, {1, “arg1”}}

User selectByIdAndName(int id, String username);

3、有 @Param 注解,names 值为:{{0, “id”}, {1, “name”}}

User selectByIdAndName(int id, @Param("name") String username);
2.1.2 参数解析

我们再看它的参数解析方法:getNamedParams();

// ------------------------- ParamNameResolver ---------------------------
public Object getNamedParams(Object[] args) {
    int paramCount = names.size();
    if (args == null || paramCount == 0) {
        return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
        // 1. 如果方法参数没有 @Param 注解,且只有一个参数
        // 直接返回该参数,不做任何处理
        return args[names.firstKey()];
    } else {
        
        // 2. 此时,参数前有 @Param 注解 || 参数不止一个
        // 创建一个 Map<String, Object> param,该 Map 的类型为 ParamMap
        final Map<String, Object> param = new ParamMap<>();
        int i = 0;
        
        // 3. 遍历我们构造函数中得到的 names, 构造 param
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            // 3.1 给 param 添加值
            // {names.entry.value0, arg[0]}, {names.entry.value1, arg[1]}
            param.put(entry.getValue(), args[entry.getKey()]);
            
            // 3.2 再往 param 中添加 {"param1":args[0]}, {"param2":args[1]}
            String genericParamName = "param" + String.valueOf(i + 1);
            
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        
        // 4. 该情况下,返回得到的 param 对象,该 param 是一个 Map 对象
        return param;
    }
}

param 的值跟有无 @Param 注解有关,我们可以看如下场景中 param 的值;

我们默认没有给 Java 加上 -parameters 参数,看 param 的值:

1、当参数有 @Param 注解标注;此时 param 的 key 列表为 id、name、param1、param2;

// 此时 param 的 key 列表为 id, name, param1, param2
@Select("select * from student where id=#{id} and name=#{name}")
public Student selectByIdAndName(@Param("id") int id, @Param("name") String name);

2、当参数没有 @Param 注解标注;此时 param 的 key 列表为 arg0、arg1、param1、param2;

// 此时 param 的 key 列表为 arg0, arg1, param1, param2
@Select("select * from student where id=#{param1} and name=#{param2}")
public Student selectByIdAndName(int id, String name);

3、当参数不全有 @Param 注解标注(了解即可,一般我们不会写这么脑瘫的写法);

// 此时 param 的 key 列表为 id, arg1, param1, param2
@Select("select * from student where id=#{id} and name=#{param2}")
public Student selectByIdAndName(@Param("id") int id, String name);

4、当只有一个参数时,且没有 @Param 注解,显然 getNamedParams() 会直接返回该参数;

// 此时参数解析结果就是原参数 Student 对象
// 很显然不能使用 #{stu.id},因为根本没有 stu 这个索引,只能使用 #{id}
@Select("select * from student where id=#{id}")
public Student selectByStudentId2(Student stu);

5、当只有一个参数时,且有 @Param 注解;此时 param 的 key 列表为 stu、param1;

// 此时的参数解析结果是 param 对象:{"stu": student, "param1": student}
// 很显然如果使用 #{id} 来取值的话,Mybatis 不知道该用哪个
// 只有我们明确指出 #{stu.id} 才行,或者也可使用 #{param1.id}
@Select("select * from student where id=#{stu.id}")
public Student selectByStudentId(@Param("stu") Student stu);

此时 param 参数如下:

在这里插入图片描述

至此,参数解析完毕;

八. 参数映射处理

我们在第五大点中 prepareStatement() 创建 PreparedStatement 对象中知道,该方法内还会做参数的映射处理;

// ------------------------- SimpleExecutor -------------------------------
private Statement prepareStatement(StatementHandler handler, Log statementLog) {
    Statement stmt;
    Connection connection = getConnection(statementLog);

    // 1. 执行 PreparedStatementHandler.prepare() 获得 PreparedStatementHandler 对象
    stmt = handler.prepare(connection, transaction.getTimeout());

    // 2. 对参数进行映射处理
    handler.parameterize(stmt);
    return stmt;
}

下面我们对 PreparedStatementHandler.parameterize() 做一个简单的分析;

// ----------------------- PreparedStatementHandler -----------------------------
public void parameterize(Statement statement) throws SQLException {
    // 调用 parameterHandler.setParameters() 对参数进行映射
    parameterHandler.setParameters((PreparedStatement) statement);
}

Mybatis 中只有一个默认的 ParameterHandler,是 DefaultParameterHandler,我们看下这个类的 setParameters();

1. DefaultParameterHandler

DefaultParameterHandler 的 setParameters() 流程不长,我们直接往下看;

// ------------------------ DefaultParameterHandler ---------------------------
public void setParameters(PreparedStatement ps) {
    
    // 1. 获取 sql 中所有的 parameterMapping
    // 其实就是我们 sql 中 #{id} 中对应值
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        
        // 2. 遍历处理 parameterMapping
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            Object value;
            // 获取 parameterMapping 的属性 property
            String propertyName = parameterMapping.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry
                       .hasTypeHandler(parameterObject.getClass())) {
                // 3. parameterObject 就是我们上文解析得到的参数对象
                // 如果参数只有一个且没有 @Param 的情况下,value 直接等于 parameterObject
                // 当然这里是简单参数的情况
                // 如果 parameterObject 是 JavaBean 类型,还是会走到第 4 步去解析参数
                value = parameterObject;
            } else {
                
                // 4. 参数不止一个的情况,parameterObject 是一个 map 对象
                // 此时需要通过辅助的 MetaObject 根据 propertyName 获取参数值
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            
            // 5. 获取 parameterMapping 的 TypeHandler
            // 我们在 sql 中一般不指定 TypeHandler
            // 此处获取的 typeHandler 一般都为 UnknownTypeHandler
            TypeHandler typeHandler = parameterMapping.getTypeHandler();
            JdbcType jdbcType = parameterMapping.getJdbcType();
            if (value == null && jdbcType == null) {
                jdbcType = configuration.getJdbcTypeForNull();
            }
            try {
                
                // 6. 通过 typeHandler.setParameter() 给 sql 中参数进行设值
                typeHandler.setParameter(ps, i + 1, value, jdbcType);
            } catch (TypeException | SQLException e) {
                throw new TypeException("Could not set parameters for mapping");
            }
        }
    }
}



// -------------------------- UnknownTypeHandler ----------------------------
public void setNonNullParameter(PreparedStatement ps, int i, 
                                Object parameter, JdbcType jdbcType) {
    // 1. 根据 parameter、jdbcType 获取一个合适的 TypeHandler
    // 一般 int 对应 IntegerTypeHandler、String 对应 StringTypeHandler
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    
    // 2. 再次调用 typeHandler.setParameter()
    // 我们以 IntegerTypeHandler 为例来看
    handler.setParameter(ps, i, parameter, jdbcType);
}



// -------------------------- IntegerTypeHandler ----------------------------
public class IntegerTypeHandler extends BaseTypeHandler<Integer> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                    Integer parameter, JdbcType jdbcType) {
        // 调用我们熟悉的 jdbc 中的 ps.setInt()
        // TypeHandlerRegistry 中有所有注册的 TypeHandler
        ps.setInt(i, parameter);
    }
}

我们举两个例子来看 DefaultParameterHandler.setParameters();

1.1 参数只有一个且没有@Param

parameterObject 是简单类型,此时 value = parameterObject;parameterMappings 只有一个参数;

在这里插入图片描述

1.2 参数有两个

此时 parameterObject 是一个 Map 对象,parameterMappings 也有两个参数,需要轮询进行处理;

处理逻辑其实就是取 parameterObject 中对应 key 的 value 值;

在这里插入图片描述

至此,参数映射完毕;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1892243.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL】数据类型{tinyint/bit/float/decimal/char/varchar/date/enum/set}

文章目录 1.数据类型分类2.数值类型2.1tinyint 1字节2.2bit 0-64位2.3浮点类型float 4个字节decimal 3.字符串类型char开多少空间为多大varchar开多少是上限 存多少占多大空间日期和时间类型enum和setenum&#xff1a;枚举&#xff0c;“单选”类型&#xff1b;set&#xff1a;…

从一次 SQL 查询的全过程了解 DolphinDB 线程模型

1. 前言 DolphinDB 的线程模型较为复杂&#xff0c;写入与查询分布式表都可能需要多个类型的线程。通过了解 SQL 查询的全过程&#xff0c;可以帮助我们了解 DolphinDB 的线程模型&#xff0c;掌握 DolpinDB 的配置&#xff0c;以及优化系统性能的方法。 本教程以一个分布式 …

Python基于卷积神经网络分类模型(CNN分类算法)实现时装类别识别项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 在深度学习领域&#xff0c;卷积神经网络&#xff08;Convolutional Neural Networks, CNNs&#xff0…

python拉取gitlab项目以及拉取报错处理

问题解决 问题1、unable to access https://gitlab.dome.com/web-dome/dome.git/: SSL certificate problem: self signed certificate 解决&#xff1a;打开本地git bash输入下面代码 git config --global http.sslVerify false; 问题2、Authentication failed for https:…

G1 垃圾收集器

从 JDK1.9 开始默认 G1&#xff0c;应用在多处理器和大容量内存环境中。 基础概念 Region G1 给整一块Heap内存区域均匀等分了N个 Region&#xff0c;N 默认情况下是 2048。 Region的大小只能是1M、2M、4M、8M、16M或32M (1-32M,并且为2的指数)&#xff0c;比如-Xmx16g -Xms…

JAVA 发送短信信息工具类(腾讯云)

发送短信信息工具类 import cn.hutool.core.collection.CollUtil; import com.tencentcloudapi.common.Credential; import com.tencentcloudapi.common.exception.TencentCloudSDKException; import com.tencentcloudapi.sms.v20210111.SmsClient; import com.tencentcloudapi…

Maven:下载配置教学(2024版 最简)

文章目录 一、Maven下载1.1 下载官网1.2 下载压缩包1.3 解压1.4 创建repo文件夹 二、Maven配置2.1 环境变量2.1.1 新建系统变量2.1.2 添加Path 2.2 阿里云镜像2.3 JDK2.4 本地仓库2.5 conf文件的全部内容2.6 测试安装配置是否成功 三、IDEA中配置Maven3.1 新配置3.2 推荐配置 四…

ACE Studio的成功经验:从国内到全球的市场拓展

在AI技术飞速发展的今天&#xff0c;音乐创作也正经历着一场前所未有的变革。作为这一变革的前沿代表&#xff0c;ACE Studio无疑引起了广泛关注。本文将通过对时域科技创始人Joe与曲凯的对话&#xff0c;深入探讨ACE Studio的创新理念、市场定位、技术优势以及未来发展方向。 …

20240704 每日AI必读资讯

Runway Gen-3 Alpha 详细使用教程 - 以及提示词指南大全&#xff0c;包括摄像机风格、灯光效果、运动状态类型以及风格美学、文本风格等。 - Gen-3 Alpha是Runway推出的新—代视频生成模型&#xff0c;它在保真度、一致性、运动和速度方面都比以前的模型有所改进&#xff0c;…

HTML总结2

什么是HTML HTML&#xff08;Hypertext Markup Language&#xff09;&#xff0c;超文本标记语言&#xff0c;&#xff08;是一套标记标签&#xff0c;一般用来描述网页&#xff09;。 HTML标签 HTML标记标签&#xff0c;通常被称为HTML标签&#xff0c;或者HTML标记。 标签…

Qt篇——QLabel固定尺寸的情况下让字体大小自适应并自动换行以完整显示

当文字较少时&#xff0c;默认字体大小为16&#xff1b;当文字内容较多时&#xff0c;自动换行并缩小字体。 举例&#xff1a; 字体较少时 字体较多时 思路&#xff1a; 设置自动换行属性 setWordWrap&#xff1b;通过QFontMetrics计算文字字体要多大、显示多少行才不会超过…

是霍尼韦尔还是柏曼啊??书客、霍尼、柏曼三款护眼大路灯横向PK!

是霍尼韦尔还是柏曼啊&#xff1f;&#xff1f;近年来市面上的劣质护眼大路灯越来越多&#xff0c;很容易使我们选到劣质产品。为了解决这一问题&#xff0c;我自费购买了多个品牌的护眼大路灯进行测评。经过深入研究&#xff0c;我发现市面上确实存在一些光线不稳定、选材做工…

【postgresql初级使用】用户与角色的关系,搭建数据库安全体系中的分权管理

用户角色管理 ​专栏内容&#xff1a; postgresql使用入门基础手写数据库toadb并发编程 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 文章目录 用户角色管…

使用 Smart-doc 记录 Spring REST API

如果您正在使用 Spring Boot 开发 RESTful API&#xff0c;您希望让其他开发人员尽可能容易地理解和使用您的 API。文档是必不可少的&#xff0c;因为它为将来的更新提供了参考&#xff0c;并帮助其他开发人员与您的 API 集成。很长一段时间以来&#xff0c;记录 REST API 的方…

Toshiba东芝TB67S109AFNAG:步进电机控制的强大解决方案

与我司合作的工程师客户不断寻求稳健、高效且精确的元件来提升他们的产品设计。东芝的TB67S109AFNAG步进电机驱动IC具备这些优点&#xff0c;是从工业机械到消费电子等广泛应用的理想选择。本文将深入探讨TB67S109AFNAG的特性、优势和应用。 主要特性 TB67S109AFNAG是一款采用…

《人生苦短,我用python·七》各种报错问题解决及C++调用python的接口

1、VS的debug版本正常可以调用python的release版本&#xff08;python安装完只有release版本的dll和lib&#xff09;&#xff0c;在项目——附加依赖项中加入python39.lib然后编译debug版本报错&#xff0c;无法打开python39_d.lib&#xff0c;我在项目属性配置的是调用release…

少儿编程 2024年6月电子学会图形化编程等级考试Scratch一级真题解析(选择题)

2024年6月scratch编程等级考试一级真题 选择题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 1、音乐Video Game1的时长将近8秒&#xff0c;点击一次角色&#xff0c;下列哪个程序不能完整地播放音乐两次 A、 B、 C、 D、 答案&#xff1a;D 考…

Redis基础教程(十一):Redis 发布订阅

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

【FFmpeg】avcodec_open2函数

目录 1. avcodec_open21.1 编解码器的预初始化&#xff08;ff_encode_preinit & ff_decode_preinit&#xff09;1.2 编解码器的初始化&#xff08;init&#xff09;1.3 释放编解码器&#xff08;ff_codec_close&#xff09; FFmpeg相关记录&#xff1a; 示例工程&#xff…

vue H5页面video 视频流自动播放, 解决ios不能自动播放问题

视频组件 <videostyle"width: 100%; height: 100%;object-fit: fill"class"player"refplayer_big_boxcontrolspreloadautoplay //自动播放muted //是否静音playsinline"true"x5-playsinline""webkit-playsinline"tru…