由 Mybatis 源码畅谈软件设计(三):简单查询 SQL 执行流程
大家好,我是 方圆。SQL 查询是 Mybatis 中的核心流程,本节我们来介绍简单 SQL 的执行流程,过程会比较长,期间会认识很多重要的组件,比如 SqlSession、四大处理器(Executor、StatementHandler、ParameterHandler 和 ResultSetHandler)等等,大家先有个脸熟,到具体环节时需要重点关注。在这个过程中会遇到很多设计模式,比如 SqlSession 使用的 门面模式,需要考虑为什么它会使用该模式呢?模板方法模式 和 策略模式 在这个过程中被使用的尤其的多。此外,在这里能够很好的理解和区分 代理模式 和 装饰器模式 等等。在设计原则上,多用组合,少用继承 的设计原则有很多体现,单一职责 更是随处可见,还有关于方法命名的小细节等等都特别值得关注。不过,一定要记得一点:应用再多原则都是在为 降低复杂性,提高可读性和可扩展性努力。
验证该过程源码逻辑采用的单测为 org.apache.ibatis.session.SqlSessionTest.shouldExecuteSelectOneAuthorUsingMapperClass,如下:
@Testvoid shouldExecuteSelectOneAuthorUsingMapperClass() {try (SqlSession session = sqlMapper.openSession()) {AuthorMapper mapper = session.getMapper(AuthorMapper.class);Author author = mapper.selectAuthor(101);assertEquals(101, author.getId());}}
开篇我们便能看到 SqlSession session = sqlMapper.openSession() 逻辑,那么就以介绍 SqlSession 开始吧:
SqlSession
org.apache.ibatis.session.SqlSession 采用了 门面模式,封装了对数据库的所有操作,包括查询、插入、更新和删除,也对事务进行管理,它是与数据库进行交互的对象,所有执行逻辑都经过该对象去执行:
public interface SqlSession extends Closeable {<E> List<E> selectList(String statement);int insert(String statement);int update(String statement);int delete(String statement);void commit();void rollback();// ...
}
SqlSession 在 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource 方法中创建,有如下逻辑:
public class DefaultSqlSessionFactory implements SqlSessionFactory {private final Configuration 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);// 创建 Executorfinal Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {// may have fetched a connection so lets call close()closeTransaction(tx);throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}// ...
}
根据上述源码可知,在 SqlSession 的构造方法中组合了 Configuration 和 Executor 对象,通过调用它组合的对象来完成 SQL 的执行,这遵循了 多用组合 的设计原则。这段逻辑中,需要重点关注的是 Executor 对象,接下来我们详细介绍一下它。
Executor
org.apache.ibatis.executor.Executor 执行器是 MyBatis 框架中的核心接口,它定义了执行 SQL 语句、管理事务和处理缓存的基本操作。Executor 负责管理 SQL 语句的执行(update 和 query 方法等)、事务的处理(commit 和 rollBack 方法)以及缓存的维护(一级缓存在 BaseExecutor 中,二级缓存由 CachingExecutor 负责)等,如下所示:
public interface Executor {ResultHandler NO_RESULT_HANDLER = null;// 该方法用于执行更新操作(包括插入、更新和删除),它接受一个 `MappedStatement` 对象和更新参数,并返回受影响的行数int update(MappedStatement ms, Object parameter) throws SQLException;// 该方法用于执行查询操作,接受 `MappedStatement` 对象(包含 SQL 语句的映射信息)、查询参数、分页信息、结果处理器等,并返回查询结果的列表<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,CacheKey cacheKey, BoundSql boundSql) throws SQLException;<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)throws SQLException;<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;// 该方法用于刷新批处理语句并返回批处理结果List<BatchResult> flushStatements() throws SQLException;// 该方法用于提交事务,参数 `required` 表示是否必须提交事务void commit(boolean required) throws SQLException;// 该方法用于回滚事务。参数 `required` 表示是否必须回滚事务void rollback(boolean required) throws SQLException;// 该方法用于创建缓存键,缓存键用于标识缓存中的唯一查询结果CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);// 该方法用于检查某个查询结果是否已经缓存在本地boolean isCached(MappedStatement ms, CacheKey key);// 该方法用于清空一级缓存void clearLocalCache();// 该方法用于延迟加载属性void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);// 该方法用于获取当前的事务对象Transaction getTransaction();// 该方法用于关闭执行器。参数 `forceRollback` 表示是否在关闭时强制回滚事务void close(boolean forceRollback);boolean isClosed();// 该方法用于设置执行器的包装器void setExecutorWrapper(Executor executor);}
Executor 在 org.apache.ibatis.session.Configuration#newExecutor 方法中被创建:
public class Configuration {protected boolean cacheEnabled = true;public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;// 创建具体的 Executor 实现类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);}// 默认 cacheEnabled 为 true,所以实际创建类型为 CachingExecutorif (cacheEnabled) {executor = new CachingExecutor(executor);}// 插件相关逻辑return (Executor) interceptorChain.pluginAll(executor);}// ...
}
如上所示,MyBatis 提供了多个 Executor 的实现类,以支持不同的执行策略和性能优化,灵活地应对不同的性能和资源管理需求,它在定义这些实现类时,使用了 模板方法模式和策略模式,在 BaseExecutor 定义了方法的模板,子类负责实现其中的逻辑,类图关系如下:

BaseExecutor 是所有执行器的基类,它主要用来维护事务对象 Transaction、管理一级缓存 PerpetualCache、提供模板方法 query 和 update,具体的执行方法(doQuery 和 doUpdate)由各子类实现,并提供了缓存管理的方法,如 clearLocalCache 和 flushStatements。
继续 Configuration#newExecutor 方法源码逻辑,ExecutorType executorType 类型默认配置为 ExecutorType.SIMPLE,所以创建的执行器类型为 SimpleExecutor。但需要注意 protected boolean cacheEnabled = true; 配置默认为 true,实际创建类型为 CachingExecutor,会在 SimpleExecutor 外包一层:
我们继续看一下 CachingExecutor 的构造方法实现,注意其中的注释信息:
public class CachingExecutor implements Executor {// 采用了静态代理模式,delegate 为被代理对象,在本次样例中,它为 SimpleExecutor 类型private final Executor delegate;public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}// ...
}
CachingExecutor 的实现使用了 静态代理模式,它是代理对象,负责处理 二级缓存 相关的逻辑,实际的查询逻辑由被代理对象 Executor delegate 执行(SimpleExecutor);逻辑 delegate.setExecutorWrapper(this); 会执行 BaseExecutor 中的 setExecutorWrapper 方法,并用 wrapper 字段引用最外层的执行器,Mybatis 将其命名为 wrapper,但是实际上在源码中并没有应用到 装饰器模式,不过这样设计提供了使用装饰器模式的可能。
public abstract class BaseExecutor implements Executor {// 装饰器模式protected Executor wrapper;@Overridepublic void setExecutorWrapper(Executor wrapper) {this.wrapper = wrapper;}// ...
}
现在 SqlSession session = sqlMapper.openSession() 逻辑已经被执行完了,准备进入获取 Mapper 的逻辑:
@Testvoid shouldExecuteSelectOneAuthorUsingMapperClass() {try (SqlSession session = sqlMapper.openSession()) {// 在会话中获取 MapperAuthorMapper mapper = session.getMapper(AuthorMapper.class);Author author = mapper.selectAuthor(101);assertEquals(101, author.getId());}}
它会执行到 org.apache.ibatis.binding.MapperRegistry#getMapper 方法,注意其中的注释信息:
public class MapperRegistry {private final Configuration config;// 所有的 Mapper 对应的 MapperProxyFactory 已经在 Mybatis 配置加载时初始化好了(对应 mybatis.xml 配置文件中的 <mappers> 标签)private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();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 {// 通过 MapperProxyFactory 工厂创建 MapperProxyreturn mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}}}
mapperProxyFactory.newInstance(sqlSession); 方法创建了 MapperProxy 对象,虽然它将类命名中包含 Factory,但是它并没有使用工厂模式,而是采用了 简单工厂的编程风格,将创建 MapperProxy 对象的逻辑封装起来:
public class MapperProxyFactory<T> {// ...public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);}}
MapperProxy 对象使用了 动态代理模式,它实现了 InvocationHandler 接口,主要代理的功能为创建并缓存 MapperMethodInvoker 对象,衔接了 Mapper 接口方法与 SQL 操作的绑定和执行过程:
public class MapperProxy<T> implements InvocationHandler, Serializable {// ...@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 是否为 Object 类的方法,如果是直接执行,不做代理相关逻辑if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);}// 否则缓存和获取 MapperMethodInvoker 实例,再执行return cachedInvoker(method).invoke(proxy, method, args, sqlSession);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}}
}
其中 cachedInvoker 方法值得关注,它为每个 mapper 方法创建并缓存 MapperMethodInvoker:
public class MapperProxy<T> implements InvocationHandler, Serializable {// ...// 缓存 MapperMethodInvokerprivate final Map<Method, MapperMethodInvoker> methodCache;private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {try {return MapUtil.computeIfAbsent(methodCache, method, m -> {// mapper 中声明的 SQL 执行方法均为非default方法if (!m.isDefault()) {return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));}// default 方法:声明在 interface 中,使用 default 标记,并提供了默认实现// default 方法使用 DefaultMethodInvoker 执行,了解即可try {if (privateLookupInMethod == null) {return new DefaultMethodInvoker(getMethodHandleJava8(method));}return new DefaultMethodInvoker(getMethodHandleJava9(method));} catch (IllegalAccessException | InstantiationException | InvocationTargetException| NoSuchMethodException e) {throw new RuntimeException(e);}});} catch (RuntimeException re) {Throwable cause = re.getCause();throw cause == null ? re : cause;}}
}
PlainMethodInvoker 和 DefaultMethodInvoker 对象中的逻辑非常简单,不过是在方法执行对象上套了一层壳,但是如此设计还是很有必要的,它使用到了 策略模式:
public class MapperProxy<T> implements InvocationHandler, Serializable {// ...interface MapperMethodInvoker {Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;}private static class PlainMethodInvoker implements MapperMethodInvoker {private final MapperMethod mapperMethod;public PlainMethodInvoker(MapperMethod mapperMethod) {this.mapperMethod = mapperMethod;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {return mapperMethod.execute(sqlSession, args);}}private static class DefaultMethodInvoker implements MapperMethodInvoker {private final MethodHandle methodHandle;public DefaultMethodInvoker(MethodHandle methodHandle) {this.methodHandle = methodHandle;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {return methodHandle.bindTo(proxy).invokeWithArguments(args);}}
}
这样设计的好处是:
-
单一职责:
PlainMethodInvoker通过封装MapperMethod的调用逻辑,实现了职责分离。这样做可以将方法调用的具体实现与代理的其他逻辑(如缓存处理、事务管理等)分开,保持代码的清晰和可维护性 -
灵活性和扩展性:封装调用逻辑使得以后可以更容易地扩展或修改调用过程,而不需要直接修改
MapperProxy的代码(如果需要在调用前后添加额外的逻辑,可以实现不同的MethodInvoker) -
提供一致的接口:
MapperProxy可以在调用方法时不关心具体的实现细节,只需调用MethodInvoker#invoke方法。MapperMethodInvoker的另一个实现DefaultMethodInvoker内封装的是MethodHandle,显然与MapperMethod对象执行方法的逻辑不一致,但是MethodInvoker只对外暴露invoke方法,外部调用逻辑便无需针对不同的类型做改动了
现在我们已经清楚了 PlainMethodInvoker 是 MapperMethod 的执行器,这便需要我们重点了解下 MapperMethod 的逻辑:
public class MapperMethod {// 主要用于记录 SQL 类型: SELECT、INSERT、DELETE 和 UPDATE 等private final SqlCommand command;// 记录返回值等信息private final MethodSignature method;public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {this.command = new SqlCommand(config, mapperInterface, method);this.method = new MethodSignature(config, mapperInterface, method);}// ...
}
MapperMethod 被实例化时,会创建 SqlCommand 和 MethodSignature 对象,这两个类均定义在 MapperMethod 类内部。在 Mybatis 源码中多处都遵循了这种设计原则:
- 单一职责:这个是经常提到的原则,定义两个不同的对象来分别做不同的功能体现了该原则
- 高内聚:
SqlCommand和MethodSignature与MapperMethod直接相关,封装为静态内部类体现该原则 - 信息隐藏:
SqlCommand和MethodSignature的实现细节都作为内部类,不对外公开,其他类无需关注具体实现,也没有对外公开使用 - 最少知识原则:其思想是“一个对象应当对其他对象有尽可能少的了解,只与直接的朋友通信,而不与陌生对象通信”,此处也遵循了该思想
SqlCommand 定义非常简单,其中字段 name 和 type 分别记录了方法全路径名和 SQL 类型:
public class MapperMethod {// ...public static class SqlCommand {// eg: org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthorprivate final String name;// eg: SELECTprivate final SqlCommandType type;// ...}
}
MethodSignature 中定义的字段内容稍多一些,请关注注释,具体的字段赋值逻辑并不复杂,便不在这里详细解释了:
public class MapperMethod {// ...public static class MethodSignature {// 结果是否返回一个集合private final boolean returnsMany;// 返回值是否使用了 org.apache.ibatis.annotations.MapKey 注解,标记了作为 Map 的 key 的值// 使用方法详见注解的注释内容private final String mapKey;// 结果是否返回 Mapprivate final boolean returnsMap;// 结果是否返回 voidprivate final boolean returnsVoid;// 结果是否返回 cursorprivate final boolean returnsCursor;// 结果是否返回 optionalprivate final boolean returnsOptional;// 返回值类型private final Class<?> returnType;// resultHandler 在参数中的索引值,无则为 nullprivate final Integer resultHandlerIndex;// rowBounds 用于分页的参数对象的索引值,无则为 nullprivate final Integer rowBoundsIndex;// 用于解析方法参数名称的工具,它用于处理方法参数的名称,以便在执行 SQL 语句时正确地将参数传递给 SQL 语句,常见的 @Param 注解便在这里生效// 该工具类中的注释描述很清楚,其中封装了 names 字段来表示参数名和索引值的对应关系,例子如下// aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}// aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}// aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}private final ParamNameResolver paramNameResolver;// ...}}
MethodSignature 中定义的 ParamNameResolver 中有一段比较有意思的代码,在这里稍稍提一下:
public class ParamNameResolver {// key: 索引 value: 参数值private final SortedMap<Integer, String> names;// ...public ParamNameResolver(Configuration config, Method method) {final SortedMap<Integer, String> map = new TreeMap<>();// ...names = Collections.unmodifiableSortedMap(map);}
}
其中使用到了 Collections.unmodifiableSortedMap(map) 方法,表示该 Map 初始化完成后是不能修改的,如果业务中也有不可修改的对象,可以参考使用该逻辑。此外,该 Map 类型使用的是 TreeMap 红黑树,想具体了解经典红黑树可以参考这篇文章 深入理解经典红黑树。
MapperMethod 中的组件已经介绍完了,下面来看一下 execute 方法,因为该方法相对复杂,我们先集中精力看一下 SELECT 相关的流程:
public class MapperMethod {// ...private final SqlCommand command;private final MethodSignature method;public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: {// ...}case UPDATE: {// ...}case DELETE: {// ...}case SELECT:// 返回值为 void 并且在入参中指定了 ResultHandlerif (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;// 结果返回集合} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);// 结果返回 Map} else if (method.returnsMap()) {result = executeForMap(sqlSession, args);// 结果返回 Cursor} 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;}// ...return result;}}
根据单测用例,我们先看一个返回单个对象的分支,MethodSignature#convertArgsToSqlCommandParam 方法见名知意,它会将入参转换成执行 SQL 命令的参数,由此也可以发现 Mybatis 中各个操作的命名不需要注释信息也能表达清楚。接下来就要执行到关键方法 org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne 了,现在又将 SqlSession 的内容接上了:
public class DefaultSqlSession implements SqlSession {// ...@Overridepublic <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);}if (list.size() > 1) {throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());} else {return null;}}}
可以发现 selectOne 方法复用的是 selectList 方法,这种代码风格也值得我们参考:封装通用的方法尽可能的复用,减少开发工作量。其中的注释也蛮有意思,这么写的原因也是经过大家讨论的结果:
Popular vote was to return null on 0 results and throw exception on too many.
受欢迎的设计是无结果时返回 null,多个结果时抛出异常
其中 DefaultSqlSession#selectList 方法的代码风格也值得参考,它使用了 方法的重载,定义了一个私有的(private)接受全量参数的方法,其他公开出的同名方法入参不同,但本质上调用的都是私有方法(Spring 框架中也有类似的代码)。在《软件设计哲学》中提到过相关的观点,它提倡寻找更 通用的设计,即使在不考虑复用的情况下,通用性的代码也更合理。带入到实际开发中,可以尝试思考如下问题来引导自己找出更通用的设计:
- 满足当前需求最简单的接口是什么?
- 这个方法会在多少种情况下被使用?
- 目前通用的 API 使用起来是否简单?
public class DefaultSqlSession implements SqlSession {// ...@Overridepublic <E> List<E> selectList(String statement) {return this.selectList(statement, null);}@Overridepublic <E> List<E> selectList(String statement, Object parameter) {return this.selectList(statement, parameter, RowBounds.DEFAULT);}@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);}private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {try {MappedStatement ms = configuration.getMappedStatement(statement);dirty |= ms.isDirtySelect();return executor.query(ms, wrapCollection(parameter), rowBounds, handler);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}}
上文中我们提到过,Executor 实际创建类型为 CachingExecutor,接下来继续看下它的 org.apache.ibatis.executor.CachingExecutor#query 方法,分为 3 个主要步骤:
public class CachingExecutor implements Executor {// ...@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 1. 获取 BoundSqlBoundSql boundSql = ms.getBoundSql(parameterObject);// 2. 创建缓存 key 用于一级、二级缓存的获取CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 3. 执行查询逻辑return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
}
第一步获取 BoundSql 对象本质上执行的是 org.apache.ibatis.mapping.SqlSource#getBoundSql 方法,上一节中我们提到过,不包含动态标签的 SQL 最终会被解析成 RawSqlSource 并在内部组合 StaticSqlSource,SqlSource#getBoundSql 做的事情是将 SQL 字符串,参数映射,参数和配置信息保存在 BoundSql 中:
public class StaticSqlSource implements SqlSource {// ...@Overridepublic BoundSql getBoundSql(Object parameterObject) {return new BoundSql(configuration, sql, parameterMappings, parameterObject);}}
借此我们也介绍下 BoundSql,它的主要功能是保存 SQL 语句及其参数的详细信息:
public class BoundSql {// 经过 SqlSource#getBoundSql 处理的 SQL,可能包含 ? 占位符private final String sql;// 参数映射private final List<ParameterMapping> parameterMappings;// 实际入参private final Object parameterObject;// 用于存储附加的参数,这些参数可能是在运行时动态添加的,通常用于处理动态 SQL 中的额外需求private final Map<String, Object> additionalParameters;// 用于方便地访问 additionalParameters 中的属性private final MetaObject metaParameters;// ...
}
第二步创建一级、二级缓存的 key 值,具体逻辑相对简单,它会根据 SQL 和参数等信息来创建,具体方法参见 org.apache.ibatis.executor.BaseExecutor#createCacheKey,无需特别关注。
第三步执行查询逻辑,具体逻辑如下,与 二级缓存 相关,如果想详细了解二级缓存的机制,请参考 从根上理解 Mybatis 二级缓存。
public class CachingExecutor implements Executor {// ...@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,CacheKey key, BoundSql boundSql) throws SQLException {// 二级缓存相关逻辑Cache cache = ms.getCache();if (cache != null) {flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}// 查询逻辑return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
}
我们主要关注 delegate.query 方法的逻辑,如下所示,其中涉及了一级缓存相关的内容,详细了解请参考 从根上理解 Mybatis 一级缓存,在此就不再赘述了。
public abstract class BaseExecutor implements Executor {// ...@Overridepublic <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();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}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 {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;}
}
这里我们重点关注 BaseExecutor#queryFromDatabase 方法设计,它使用到了 模板方法模式,定义了方法的模板,具体执行逻辑 doQuery 由具体子类去实现,而在我们测试的样例中,“具体的子类” 是 SimpleExecutor,执行的方法如下:
public class SimpleExecutor extends BaseExecutor {// ...@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();// 创建 StatementHandlerStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,boundSql);// 准备 Statementstmt = prepareStatement(handler, ms.getStatementLog());// 由 StatementHandler 执行 query 方法return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}}}
首先便会创建 StatementHandler,其中逻辑蛮有意思,我们重点看一下:
StatementHandler
StatementHandler 在 Configuration#newStatementHandler 方法中被创建,实际类型为 RoutingStatementHandler:
public class Configuration {public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,rowBounds, resultHandler, boundSql);// 拦截器相关逻辑return (StatementHandler) interceptorChain.pluginAll(statementHandler);}
}
RoutingStatementHandler 使用了 静态代理模式,命名中 Routing 即表示它代理的作用:根据 statementType 创建不同的 StatementHandler,并去执行相关的逻辑,不配置 statementType 参数的话,默认为 PREPARED,如下所示:
public class RoutingStatementHandler implements StatementHandler {// 代理对象private final StatementHandler delegate;public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler, BoundSql boundSql) {// 在调用构造方法时,根据 statementType 字段为代理对象 delegate 赋值,那么这样便实现了复杂度隐藏,只由代理对象去帮忙路由具体的实现即可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());}}@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {return delegate.query(statement, resultHandler);}// ...
}
在这里我们详细介绍下 org.apache.ibatis.executor.statement.StatementHandler SQL 处理器,它是 MyBatis 框架中的一个接口,定义了处理 SQL 语句的核心方法,提供统一的接口供框架调用。它的主要职责是 准备(prepare)、承接封装 SQL 执行参数的逻辑和承接处理结果集的逻辑,这里描述成“承接”的意思是这两部分职责并不是由它处理,而是分别由 ParameterHandler 和 ResultSetHandler 完成。StatementHandler 是 MyBatis 执行 SQL 语句的关键组件之一,Executor 借助它与数据库进行交互。继承关系图如下(BaseStatementHandler 为抽象类,但并没有在命名中添加 Abstract):

继续回到 SimpleExecutor#doQuery 方法 准备 Statement prepareStatement 方法中:
public class SimpleExecutor extends BaseExecutor {// ...private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog);stmt = handler.prepare(connection, transaction.getTimeout());// 封装SQL语句中的参数handler.parameterize(stmt);return stmt;}
}
它会调用 StatementHandler#prepare 方法,该方法使用了 模板方法模式 定义了算法骨架,具体的步骤 instantiateStatement 分别由具体实现类去实现:
public abstract class BaseStatementHandler implements StatementHandler {// ...@Overridepublic 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 (SQLException e) {closeStatement(statement);throw e;} catch (Exception e) {closeStatement(statement);throw new ExecutorException("Error preparing statement. Cause: " + e, e);}}protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
}
在这里实例化 Statement 的方法被命名为 instantiateStatement,instantiate 表示实例化的意思,后续我们在为构造对象的方法命名时也可以采用 instantiateXxx 的形式,一眼便能知道该方法的作用。相应地,为对象的字段赋值的方法可以命名为 initialXxx 表示为已知实例封装了某些字段值。
继续回到源码逻辑中,StatementHandler 实际创建类型为 PreparedStatementHandler,instantiateStatement 方法创建的 Statement 类型为 ClientPreparedStatement,它是 JDBC 相关的内容,就不再多赘述了。
回到 SimpleExecutor#prepareStatement 方法,创建完 Statement 会调用 StatementHandler#parameterize 方法封装参数,其中会使用到 DefaultParameterHandler 完成该操作,它是 ParameterHandler 接口的默认实现类,我们详细介绍下它:
ParameterHandler
ParameterHandler 的核心逻辑如下,它会完成占位符和指定参数值的对应关系:
public class DefaultParameterHandler implements ParameterHandler {// ...@Overridepublic void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());// 获取参数映射,以便处理用户传入的参数List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();if (parameterMappings != null) {MetaObject metaObject = 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)) { // issue #448 ask first for additional paramsvalue = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 已定义类型处理器,这些类型能直接取对应值value = parameterObject;} else {// 使用反射,根据参数映射中定义的字段值获取对应的参数值if (metaObject == null) {metaObject = configuration.newMetaObject(parameterObject);}value = metaObject.getValue(propertyName);}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 | SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}}}
它本身的逻辑并不复杂,其中重要的组件 TypeHandler 我们来介绍下:对于字符串、整数、布尔值等基本数据类型的转换,Mybatis 框架定义了默认 TypeHandler 实现(StringTypeHandler, BooleanTypeHandler…),它其中定义了四个方法:
public interface TypeHandler<T> {// 用于将 Java 类型的数据设置到 JDBC 的 PreparedStatement 中,以便执行 SQL 语句时正确传递参数,替换掉对应顺序的占位符void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;// 用于从 ResultSet 中根据列名获取数据,并将其转换为Java类型T getResult(ResultSet rs, String columnName) throws SQLException;// 用于从 ResultSet 中根据列索引获取数据,并将其转换为Java类型T getResult(ResultSet rs, int columnIndex) throws SQLException;// 用于从 CallableStatement 存储过程中获取存储过程的输出参数,并将其转换为 Java 类型T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
不难发现,它的作用是将 Java 类型的数据设置到正确的占位符索引位置上;根据 SQL 查询结果,将数据库中数据转换为 Java 类型。在 TypeHandlerRegistry 可以看到默认初始化的类型处理器,这些处理器的实现使用了 策略模式和模板方法模式:

如上图所示,TypeHandler 根据不同的 JavaType 来实现不同的策略,由于其中部分逻辑是通用的,所以抽出了抽象层定义方法模板来实现代码的复用。
现在 SimpleExecutor#prepareStatement 方法已经执行完毕了,这时 SQL 已经准备好了,对应的参数已经映射到对应的占位符上了,现在便是执行对应 SQL 的逻辑,它的执行流程在 JDBC 的 PreparedStatement#execute 方法中已经封装好了,感兴趣的同学可以去了解下,在这里我们需要看下 resultSetHandler.handleResultSets(ps) 处理结果的逻辑:
public class PreparedStatementHandler extends BaseStatementHandler {// ...@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute();// 处理从数据库中返回的结果return resultSetHandler.handleResultSets(ps);}
}
结果由 ResultSetHandler 处理,它也是非常重要的组件之一。
ResultSetHandler
它包含以下三个方法,我们关注 ResultSetHandler#handleResultSets 方法即可:
public interface ResultSetHandler {/*** 处理 Statement 对象并返回结果对象** @param stmt SQL 语句执行后返回的 Statement 对象* @return 映射后的结果对象列表*/<E> List<E> handleResultSets(Statement stmt) throws SQLException;/*** 处理 Statement 对象并返回一个 Cursor 对象* 它用于处理从数据库中获取的大量结果集,与传统的 List 或 Collection 不同,Cursor 提供了一种流式处理结果集的方式,* 这在处理大数据量时非常有用,因为它可以避免将所有数据加载到内存中** @param stmt SQL 语句执行后返回的 Statement 对象* @return 游标对象,用于迭代结果集*/<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;/*** 处理存储过程的输出参数** @param cs 存储过程调用的 CallableStatement 对象*/void handleOutputParameters(CallableStatement cs) throws SQLException;
}
它的默认实现类是 DefaultResultSetHandler,将 ResultSet 转换成对应 Java 对象的核心逻辑(根据声明的数据库列和 Java 对象字段的映射关系来赋值),总体上比较简单,关注注释信息即可:
public class DefaultResultSetHandler implements ResultSetHandler {// ... private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {DefaultResultContext<Object> resultContext = new DefaultResultContext<>();ResultSet resultSet = rsw.getResultSet();skipRows(resultSet, rowBounds);// 逐个按行解析成 Java 对象while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);Object rowValue = getRowValue(rsw, discriminatedResultMap, null);storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}}private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {final ResultLoaderMap lazyLoader = new ResultLoaderMap();// 根据返回值对象类型调用其构造方法,该结果中所有字段未生成值Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {// 元对象final MetaObject metaObject = configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;// 根据配置信息是否处理自动字段和列的映射,默认为 tureif (shouldApplyAutomaticMappings(resultMap, false)) {// 在这里处理 result map 中没用定义的字段和列关系的映射// 在 Mybatis 框架下默认情况下,只有字段值和数据库列相同才能完成映射,如果想将数据库列转换成驼峰式的 Java 字段定义,需要配置 mapUnderscoreToCamelCase 为 truefoundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}// 根据 result mapping 中配置的字段和数据库列的映射关系,从 resultSet 中取值后封装给 metaObjectfoundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;foundValues = lazyLoader.size() > 0 || foundValues;rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;}return rowValue;}private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue,ResultMapping parentMapping, ResultSet rs) throws SQLException {if (parentMapping != null) {linkToParents(rs, parentMapping, rowValue);} else {callResultHandler(resultHandler, resultContext, rowValue);}}
}
在这里便完成数据库中数据和 Java 类对象的转换,转换完成后便是不断的方法返回,最终由 Mapper 接口返回结果。这样,一条简单 SQL 的查询便结束了,想要了解该过程非常需要大家 Debug 根据代码代码流程。我们来整理下时序图:
总结

每个声明 SQL 查询语句的 Mapper 接口都会被 MapperProxy 代理,接口中每个方法都会被定义为 MapperMethod 对象,借助 PlainMethodInvoker 执行。当方法被执行时,会先调用 SqlSession 中的查询方法,由 执行器 Executor 去承接,接下来会调用 SQL 处理器 StatementHandler 的方法完成 SQL 准备,而封装参数则由 参数处理器 DefaultParameterHandler 和 TypeHandler 完成,ResultSet 结果的处理:将数据库中数据转换成所需要的 Java 对象由 结果处理器 DefaultResultSetHandler 完成。
相关文章:
由 Mybatis 源码畅谈软件设计(三):简单查询 SQL 执行流程
大家好,我是 方圆。SQL 查询是 Mybatis 中的核心流程,本节我们来介绍简单 SQL 的执行流程,过程会比较长,期间会认识很多重要的组件,比如 SqlSession、四大处理器(Executor、StatementHandler、ParameterHan…...
项目实践 之 pdf简历的解析和填充(若依+vue3)
文章目录 环境背景最终效果前端讲解左侧模块解析右侧上传模块解析前端步骤 后端讲解代码前端 环境背景 若依前后端分离框架 vue最后边附有代码哦 最终效果 前端讲解 左侧模块解析 1、左侧表单使用el-form 注意: 1、prop出现的字段,需要保证是该类所…...
C语言机试编程题
编写版本:vc2022 1.求最大/小值 #include<stdio.h> int main(){int a[50],n;int max, min;printf("请输入您要输入几个数");scanf_s("%d", &n);printf("请输入您要比较的%d个数\n",n);for (int i 0; i<n; i) {scanf_…...
lowagie(itext)老版本手绘PDF,包含页码、水印、图片、复选框、复杂行列合并、行高设置等。
入口类:exportPdf package xcsy.qms.webapi.service;import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.alibaba.nacos.common.utils.StringUtils; import com.ibm.icu.text.RuleBasedNumberFormat; import com.lowagie…...
第002文-kali虚拟机安全与网络配置
1、kali系统介绍 kali是一个基于Linux kernel的操作系统,由BackTrack(简称BT)发展而来。BackTrack是2006年推出的一个用于渗透测试及黑客攻防的专用平台,基于Knoppix(linux的一个发行版)开发。BackTrack版本周期:2006年的起始版本BackTrack …...
软件工程复试专业课-软件生命周期
文章目录 软件过程模型瀑布模型模型图特点优缺点改进后的瀑布模型 快速原型模型模型图优缺点 增量模型(迭代-递增模型)原型图与瀑布和快速原型的区别优缺点风险更大的增量模型 螺旋模型简介模型图优缺点 喷泉模型模型图优缺点 编码修补模型敏捷过程优缺点…...
DILLEMA:扩散模型+大语言模型,多模态数据增强框架
引言:深度学习模型的鲁棒性测试需要全面且多样化的测试数据。现有的方法通常基于简单的数据增强技术或生成对抗网络,但这些方法在生成真实且多样化的测试数据集方面存在局限性。为了克服这些限制,DILLEMA框架应运而生,旨在通过结合…...
OpenBMC:BmcWeb app获取socket
OpenBMC:BmcWeb app.run-CSDN博客 app对象在run函数中调用了setupSocket() static std::vector<Acceptor> setupSocket() {std::vector<Acceptor> acceptors;char** names = nullptr;int listenFdCount = sd_listen_fds_with_names(0, &names);BMCWEB_LOG_DE…...
突破加速度计的精度与量程瓶颈:HEROS-GAN技术
在当今科技飞速发展的背景下,低成本传感器的应用范围日益扩大。然而,低成本加速度计由于其固有的限制——如信号噪声显著和动态范围狭窄——往往难以满足高精度应用场景的需求。哈尔滨工业大学的研究团队最近提出了一项名为HEROS-GAN(Honed-E…...
C++程序员内功修炼——Linux C/C++编程技术汇总
在软件开发的宏大版图中,C 语言宛如一座巍峨的高山,吸引着无数开发者攀登探索。而 Linux 操作系统,以其开源、稳定、高效的特性,成为了众多开发者钟爱的开发平台。将 C 与 Linux 相结合,就如同为开发者配备了一把无坚不…...
苍穹外卖-阿里云OSS文件上传
苍穹外卖-阿里云OSS文件上传 一、阿里云OSS简介**获取AccessKey**获取enpoint 二、代码实现1 引入依赖2 定义OSS相关配置2.1 application-dev.yml2.2 application.yml 3 读取OSS配置3.1 AliOssProperties 4 生成OSS工具类对象4.1 AliOssUtil4.2 OssConfiguration2.5 CommonCont…...
C# 中 Array、ArrayList 和 List 的比较
C# 中 Array、ArrayList 和 List 的比较 在 C# 中,Array、ArrayList 和 List<T> 都用于存储和管理数据集合,但它们在类型安全性、性能、灵活性和使用场景上存在显著差异。以下是对这三种集合的详细比较: 1. 概述 特性ArrayArrayList…...
DeepSeek 提示词:常见指令类型
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编…...
商业AI模型新篇章:Granite 3.2引领行业创新
摘要 Granite 3.2 是一款专为商业环境设计的人工智能模型,旨在提高AI驱动应用的信任度和可扩展性。该模型从零开始构建,采用最新的密集型架构,支持12种语言,覆盖11个不同领域。通过其先进的技术,Granite 3.2 不仅增强了…...
Spring boot中的@ConfigurationProperties注解
Spring boot中的ConfigurationProperties注解 ConfigurationProperties 是 Spring Boot 提供的一个强大注解,用于将配置文件(如 application.properties 或 application.yml)中的配置绑定到一个 Java 对象中。它不仅简化了配置管理ÿ…...
Rust中的异步编程:构建简单的网页爬虫
一、什么是Rust中的Futures和Async? 在Rust中,异步编程基于future(未来)的概念。一个future表示一个当前可能不可用,但将来某个时候可以获得的值。Rust中的Future特征定义了这一概念,任何实现了该特征的类…...
springai系列(二)从0开始搭建和接入azure-openai实现智能问答
文章目录 前言1.从0开始搭建项目2.进入微软openai申请key3.配置application.yaml4.编写controller5.测试源码下载地址总结 前言 之前使用openai的官网的api需要科学上网,但是我们可以使用其他的代理间接实现使用chatgpt的相关模型,解决这个问题。比如:本…...
flutter 局部刷新控件Selector源码实现原理
Flutter 中的 Selector 组件是 provider 包提供的一个优化工具,用于在状态管理中仅选择所需数据片段,避免不必要的 Widget 重建。其实现原理基于以下几个关键点: 1. 核心设计目标 选择性重建:仅当特定数据变化时触发 Widget 重建&…...
Eclipse 编译项目指南
Eclipse 编译项目指南 引言 Eclipse 是一款功能强大的集成开发环境(IDE),广泛用于Java、C/C、Python等多种编程语言的开发。在Eclipse中编译项目是进行软件开发的基础步骤。本文将详细介绍如何在Eclipse中编译项目,包括项目设置…...
Go在1.22版本修复for循环陷阱
记录 前段时间升级Go版本碰到一个大坑,先记录。 先上代码案例: func main() {testClosure() }func testClosure() {for i : 0; i < 5; i {defer func() {fmt.Println(i)}()} }在1.22之下(不包括1.22)版本: 输出的…...
c++_sort函数
sort介绍 在C/C中,要想应用排序算法,可以使用c语言的qsort,也可以使用c的sort 。 1)qsort 是 C 标准库提供的一个通用排序函数,位于 stdlib.h 头文件中。 qsort 适用于 C 语言中的数组。 2)sort 是 C 中STL的泛型算法…...
【Stable Diffusion】AnimatedDiff--AI动画 插件使用技巧分享;文生视频、图生视频、AI生成视频工具;
本专栏主要记录人工智能的应用方面的内容,包括chatGPT、DeepSeek、AI绘画等等; 在当今AI的热潮下,不学习AI,就要被AI淘汰;所以欢迎小伙伴加入本专栏和我一起探索AI的应用,通过AI来帮助自己提升生产力; 本文的目标就是让每一个读者,都能学会并掌握AnimateDiff的使用;成…...
可视化约瑟夫生死环小游戏
这是一个基于Tkinter的图形界面应用程序,用于模拟约瑟夫环问题。约瑟夫环问题是一个经典的数学问题,描述的是N个人围成一圈,从第一个人开始报数,每数到第M个人就将其淘汰,然后从下一个人继续报数,直到剩下最…...
【深入理解JWT】从认证授权到网关安全
最近的项目学习中,在进行登陆模块的用户信息验证这一部分又用到了JWT的一些概念和相关知识,特在此写了这篇文章、方便各位笔者理解JWT相关概念 目录 先来理解JWT是什么? 区分有状态认证和无状态认证 有状态认证 VS 无状态认证 JWT令牌的…...
学习路之PHP --TP6异步执行功能 (无需安装任何框架)
学习路之PHP --异步执行功能 (无需安装任何框架) 简介一、工具类二、调用三、异步任务的操作四、效果: 简介 执行异步任务是一种很常见的需求,如批量发邮箱,短信等等执行耗时任务时,需要程序异步执行&…...
DeepSeek-R1:GPU编程自动化加速的新纪元
摘要 DeepSeek-R1是由斯坦福大学和普林斯顿大学研究者共同开发的项目,其自研的CUDA核心在性能测试中取得了卓越成绩,超越了o1和Claude 3.5 Sonnet,位居榜首。尽管DeepSeek-R1目前仅在约20%的任务中实现了对PyTorch Eager模式的性能超越&#…...
CSS 对齐:深入理解与技巧实践
CSS 对齐:深入理解与技巧实践 引言 在网页设计中,元素的对齐是至关重要的。一个页面中元素的对齐方式直接影响到页面的美观度和用户体验。CSS 提供了丰富的对齐属性,使得开发者可以轻松实现各种对齐效果。本文将深入探讨 CSS 对齐的原理、方法和技巧,帮助开发者更好地掌握…...
vue深拷贝:1、使用JSON.parse()和JSON.stringify();2、使用Lodash库;3、使用深拷贝函数(采用递归的方式)
文章目录 引言三种方法的优缺点在Vue中,实现数组的深拷贝I JSON.stringify和 JSON.parse的小技巧深拷贝步骤缺点:案例1:向后端请求路由数据案例2: 表单数据处理时复制用户输入的数据II 使用Lodash库步骤适用于复杂数据结构和需要处理循环引用的场景III 自定义的深拷贝函数(…...
九、数据治理架构流程
一、总体结构 《数据治理架构流程图》(Data Governance Architecture Flowchart) 水平结构:流程图采用水平组织,显示从数据源到数据应用的进程。 垂直结构:每个水平部分进一步划分为垂直列,代表数据治理的…...
【数据结构】 最大最小堆实现优先队列 python
堆的定义 堆(Heap)是一种特殊的完全二叉树结构,通常分为最大堆和最小堆两种类型。 在最大堆中,父节点的值总是大于或等于其子节点的值; 而在最小堆中,父节点的值总是小于或等于其子节点的值。 堆常用于实…...
