MyBatis-源码解说

归档

  • GitHub: MyBatis-源码解说

总说明

  • 源码仓库: https://github.com/mybatis/mybatis-3
  • 克隆:git clone https://github.com/mybatis/mybatis-3.git
  • 切分支(tag):git checkout master
  • JDK: 17
  • Mapper 测试在 org.apache.ibatis.submitted.* 包下面

单元测试

  • 参考:org.apache.ibatis.submitted.extend.ExtendTest
  @BeforeAllstatic void setUp() throws Exception {try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/extend/ExtendConfig.xml")) {sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); // ref: sign_m_110}}@Testvoid testExtend() {try (SqlSession sqlSession = sqlSessionFactory.openSession()) {     // ref: sign_m_210ExtendMapper mapper = sqlSession.getMapper(ExtendMapper.class);   // ref: sign_m_310Child answer = mapper.selectChild();                              // ref: sign_m_410answer = mapper.selectChild();        // 测试:一级缓存关闭} // sign_demo_010 需要第一次 SqlSession 提交,二级缓存才有数据。ref: sign_m_621try (SqlSession sqlSession = sqlSessionFactory.openSession()) {ExtendMapper mapper = sqlSession.getMapper(ExtendMapper.class);Child answer = mapper.selectChild();  // 测试:二级缓存开启}}
  • extend\ExtendConfig.xml
<configuration><settings><setting name="localCacheScope" value="STATEMENT"/> <!-- sign_demo_110 关闭一级缓存 --><setting name="cacheEnabled" value="true"/>         <!-- sign_demo_120 开启二级缓存 --></settings>...
</configuration>
  • extend\Extend.xml
<mapper namespace="org.apache.ibatis.submitted.extend.ExtendMapper"><cache/>  <!-- sign_demo_210 配合开启二级缓存 -->...
</mapper>

原理

创建 SqlSessionFactory

  • org.apache.ibatis.session.SqlSessionFactoryBuilder
  // sign_m_110 构建工厂public SqlSessionFactory build(Reader reader) {return build(reader, null, null); // ref: sign_m_111}// sign_m_111public SqlSessionFactory build(Reader reader, String environment, Properties properties) {try {XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);return build(parser.parse());   // 使用解析的配置,构建工厂,ref: sign_m_112 | sign_m_120} ... // catch finally}// sign_m_112 创建默认的工厂public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);  // ref: sign_c_210}
  • org.apache.ibatis.builder.xml.XMLConfigBuilder
// 解析 <configuration> xml 配置文件
public class XMLConfigBuilder extends BaseBuilder {// sign_m_120 解析配置public Configuration parse() {...parseConfiguration(parser.evalNode("/configuration"));  // ref: sign_m_121return configuration;}// sign_m_121private void parseConfiguration(XNode root) {try {// 先读取属性propertiesElement(root.evalNode("properties"));   // nopProperties settings = settingsAsProperties(root.evalNode("settings"));  // noploadCustomVfsImpl(settings);                      // noploadCustomLogImpl(settings);                      // noptypeAliasesElement(root.evalNode("typeAliases")); // noppluginsElement(root.evalNode("plugins"));         // nopobjectFactoryElement(root.evalNode("objectFactory"));               // nopobjectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // nopreflectorFactoryElement(root.evalNode("reflectorFactory"));         // nopsettingsElement(settings);                        // nop// 在 objectFactory 和 objectWrapperFactory 之后读取environmentsElement(root.evalNode("environments")); // 配置环境databaseIdProviderElement(root.evalNode("databaseIdProvider"));     // noptypeHandlersElement(root.evalNode("typeHandlers"));                 // nopmappersElement(root.evalNode("mappers"));           // 解析 mapper, ref: sign_m_122} ... // catch}// sign_m_122  解析 mapperprivate void mappersElement(XNode context) throws Exception {...for (XNode child : context.getChildren()) {if ("package".equals(child.getName())) {...} else {String resource = child.getStringAttribute("resource");String url = child.getStringAttribute("url");String mapperClass = child.getStringAttribute("class");if (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource);try (InputStream inputStream = Resources.getResourceAsStream(resource)) {XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,configuration.getSqlFragments());mapperParser.parse(); // ref: sign_m_130}} else if (resource == null && url != null && mapperClass == null) {...} else if (resource == null && url == null && mapperClass != null) {...} ... // else { throw new BuilderException }}}}
}
  • org.apache.ibatis.builder.xml.XMLMapperBuilder
// 解析 <mapper> xml 配置文件
public class XMLMapperBuilder extends BaseBuilder {// sign_m_130public void parse() {if (!configuration.isResourceLoaded(resource)) {configurationElement(parser.evalNode("/mapper")); // ref: sign_m_131configuration.addLoadedResource(resource);bindMapperForNamespace();}configuration.parsePendingResultMaps(false);configuration.parsePendingCacheRefs(false);configuration.parsePendingStatements(false);}// sign_m_131private void configurationElement(XNode context) {try {String namespace = context.getStringAttribute("namespace");...builderAssistant.setCurrentNamespace(namespace);cacheRefElement(context.evalNode("cache-ref"));cacheElement(context.evalNode("cache"));parameterMapElement(context.evalNodes("/mapper/parameterMap"));resultMapElements(context.evalNodes("/mapper/resultMap"));                    // resultMap 映射sqlElement(context.evalNodes("/mapper/sql"));buildStatementFromContext(context.evalNodes("select|insert|update|delete"));  // 具体 SQL 方法处理,ref: sign_m_132} ... // catch}// sign_m_132private void buildStatementFromContext(List<XNode> list) {...buildStatementFromContext(list, null);    // ref: sign_m_133}// sign_m_133private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {for (XNode context : list) {// context 相当于 xml 里面的一个 select 方法声明final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {statementParser.parseStatementNode(); // ref: sign_m_140} ... // catch}}
}
  • org.apache.ibatis.builder.xml.XMLStatementBuilder
// 解析 <select> 等 xml 片段
public class XMLStatementBuilder extends BaseBuilder {// sign_m_140public void parseStatementNode() {...String nodeName = context.getNode().getNodeName();SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));boolean isSelect = sqlCommandType == SqlCommandType.SELECT;...String parameterType = context.getStringAttribute("parameterType");...// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)KeyGenerator keyGenerator;...String parameterMap = context.getStringAttribute("parameterMap");String resultType = context.getStringAttribute("resultType");Class<?> resultTypeClass = resolveClass(resultType);String resultMap = context.getStringAttribute("resultMap");...builderAssistant.addMappedStatement(...); // 将语句块添加到配置,ref: sign_m_150}
}
  • org.apache.ibatis.builder.MapperBuilderAssistant
public class MapperBuilderAssistant extends BaseBuilder {// sign_m_150public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, ...) {...MappedStatement.Builder statementBuilder = ...; // 填充 builder...MappedStatement statement = statementBuilder.build();configuration.addMappedStatement(statement);    // 将解析的语句块添加到配置return statement;}
}

获取 SqlSession

  • org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
// sign_c_210
public class DefaultSqlSessionFactory implements SqlSessionFactory {private final Configuration configuration;// sign_m_210@Overridepublic SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);  // ref: sign_m_211}// sign_m_211private 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);  // ref: sign_m_220return new DefaultSqlSession(configuration, executor, autoCommit);  // 返回默认的 session, ref: sign_c_310} ... // catch finally }
}
  • org.apache.ibatis.session.Configuration
  // sign_m_220public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType; // 最终值为: SIMPLEExecutor 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);}return (Executor) interceptorChain.pluginAll(executor); // 组装‘拦截链’}

获取 Mapper

  • org.apache.ibatis.session.defaults.DefaultSqlSession
// sign_c_310
public class DefaultSqlSession implements SqlSession {private final Configuration configuration;private final Executor executor;private final boolean autoCommit;// sign_m_310@Overridepublic <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this); // ref: sign_m_320}
}
  • org.apache.ibatis.session.Configuration
  // sign_m_320public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);  // ref: sign_m_330}
  • org.apache.ibatis.binding.MapperRegistry
  // sign_m_330public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);...try {return mapperProxyFactory.newInstance(sqlSession);  // 创建 Mapper 动态代理,ref: sign_m_340} ... // catch}
  • org.apache.ibatis.binding.MapperProxyFactory
  // sign_m_340  创建 Mapper 动态代理public T newInstance(SqlSession sqlSession) {// 创建 JDK 代理用的 InvocationHandler, ref: sign_c_410final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);  // 创建 JDK 动态代理,ref: sign_m_341}// sign_m_341  创建 JDK 动态代理protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}

执行 Mapper 方法

  • org.apache.ibatis.binding.MapperProxy
// sign_c_410
public class MapperProxy<T> implements InvocationHandler, Serializable {private final SqlSession sqlSession;private final Class<T> mapperInterface;private final Map<Method, MapperMethodInvoker> methodCache;// sign_m_410  调用 Mapper 方法时,底层操作由此方法完成@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {...return cachedInvoker(method)                      // ref: sign_m_411.invoke(proxy, method, args, sqlSession);  // ref: sign_m_412} ... // catch}// sign_m_411private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {try {return MapUtil.computeIfAbsent(methodCache, method, m -> {if (!m.isDefault()) { // 不是 default 方法,进入此逻辑return new PlainMethodInvoker(  // ref: sign_c_411new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())  // ref: sign_c_420 | sign_cm_420);}... // default 方法处理});} ...   // catch}// sign_c_411private static class PlainMethodInvoker implements MapperMethodInvoker {private final MapperMethod mapperMethod;// sign_m_412@Overridepublic Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {return mapperMethod.execute(sqlSession, args);  // ref: sign_m_420}}
}
  • org.apache.ibatis.binding.MapperMethod
// sign_c_420
public class MapperMethod {private final SqlCommand command;private final MethodSignature method;// sign_cm_420public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {this.command = new SqlCommand(config, mapperInterface, method);this.method = new MethodSignature(config, mapperInterface, method);}// sign_m_420  执行 SQLpublic Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: // sqlSession.insertcase UPDATE: // sqlSession.updatecase DELETE: // sqlSession.deletecase SELECT:if (method.returnsVoid() && method.hasResultHandler()) {  // sqlSession.select} else if (method.returnsMany()) {    // sqlSession.selectList} else if (method.returnsMap()) {     // sqlSession.selectMap} else if (method.returnsCursor()) {  // sqlSession.selectCursor} else {Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);  // ref: sign_m_430...}break;...}...return result;}
}
  • org.apache.ibatis.session.defaults.DefaultSqlSession
    • ref: sign_c_310
  // sign_m_430@Overridepublic <T> T selectOne(String statement, Object parameter) {// 0 个结果上返回 null,多个结果抛出异常。List<T> list = this.selectList(statement, parameter);if (list.size() == 1) {return list.get(0); // ref: sign_m_431}...}// sign_m_431@Overridepublic <E> List<E> selectList(String statement, Object parameter) {return this.selectList(statement, parameter, RowBounds.DEFAULT);  // ref: sign_m_432}  // sign_m_432@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER); // ref: sign_m_433}// sign_m_433private <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); // ref: sign_m_440} ... // catch finally}
  • org.apache.ibatis.executor.CachingExecutor
public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();  // 缓存管理器// sign_m_440@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);           // ref: sign_m_441}// sign_m_441@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,CacheKey key, BoundSql boundSql) throws SQLException {Cache cache = ms.getCache();  // Cache 链参考: sign_cc_010if (cache != null) { // sign_bc_410 有开启二级缓存...if (ms.isUseCache() && resultHandler == null) {...List<E> list = (List<E>) tcm.getObject(cache, key); // sign_bc_411 从二级缓存中取,ref: sign_m_610if (list == null) {list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);  // ref: sign_m_510tcm.putObject(cache, key, list);                  // sign_bc_412 添加(只是暂存)到二级缓存,ref: sign_m_611}return list;    // sign_bc_413 二级缓存数据不为空,直接返回}}// delegate 为 SimpleExecutor 实例 (继承 BaseExecutor)return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);        // ref: sign_m_510}
}
底层查询
  • org.apache.ibatis.executor.BaseExecutor
  // sign_m_510@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,CacheKey key, BoundSql boundSql) throws SQLException {...List<E> list;try {queryStack++;list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {  // sign_bc_510 没有一级缓存进入此逻辑list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // ref: sign_m_511}} finally {queryStack--;}if (queryStack == 0) {...if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache();  // sign_bc_520  默认 SESSION 是启用一级缓存,设置为 STATEMENT 会清空,相当于禁用}}return list;}// sign_m_511private <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);  // ref: sign_m_520} ... // finallylocalCache.putObject(key, list);  // sign_bc_530 添加到一级缓存...return list;}
  • org.apache.ibatis.executor.SimpleExecutor
  // sign_m_520@Overridepublic <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,BoundSql boundSql) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler( // 创建处理器并设置‘拦截链’,ref: sign_m_530wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog()); // ref: sign_m_521return handler.query(stmt, resultHandler);              // ref: sign_m_541} finally {closeStatement(stmt);}}// sign_m_521private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog); // 获取 JDBC 连接stmt = handler.prepare(connection, transaction.getTimeout()); // 获取 JDBC Statement, ref: sign_m_540handler.parameterize(stmt); // 处理参数return stmt;}
  • org.apache.ibatis.session.Configuration
  // sign_m_530 创建处理器并设置‘拦截链’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);  // ref: sign_cm_540return (StatementHandler) interceptorChain.pluginAll(statementHandler); // 设置‘拦截链’}
  • org.apache.ibatis.executor.statement.RoutingStatementHandler
public class RoutingStatementHandler implements StatementHandler {private final StatementHandler delegate;// sign_cm_540public 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);case PREPARED:  // 进入此逻辑,ref: sign_cm_550delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);... // default Exception}}// sign_m_540  获取 JDBC Statement@Overridepublic Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {return delegate.prepare(connection, transactionTimeout);  // ref: sign_m_550}// sign_m_541 查询@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {return delegate.query(statement, resultHandler);  // ref: sign_m_561}
}
  • org.apache.ibatis.executor.statement.BaseStatementHandler
  // sign_cm_550protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, ..., BoundSql boundSql) {this.configuration = mappedStatement.getConfiguration();this.executor = executor;....this.boundSql = boundSql;this.parameterHandler = configuration.newParameterHandler(...); // 创建参数处理器,并设置‘拦截链’this.resultSetHandler = configuration.newResultSetHandler(...); // 创建结果处理器,并设置‘拦截链’}// sign_m_550@Overridepublic Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {...Statement statement = null;try {statement = instantiateStatement(connection); // ref: sign_m_560setStatementTimeout(statement, transactionTimeout); // 设置超时setFetchSize(statement);  // 设置拉取行数return statement;} ... // catch}
  • org.apache.ibatis.executor.statement.PreparedStatementHandler
  // sign_m_560  底层 JDBC 创建 Statement@Overrideprotected Statement instantiateStatement(Connection connection) throws SQLException {String sql = boundSql.getSql();...if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {  // 进入此逻辑return connection.prepareStatement(sql);  // JDBC 接口} else {return connection.prepareStatement(...);  // JDBC 接口}}// sign_m_561  底层 JDBC 执行查询@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute(); // 执行 SQL 查询return resultSetHandler.handleResultSets(ps); // 处理查询结果(封装返回)}

缓存

一级缓存
  • 默认开启;关闭参考:
    • sign_demo_110
  • 原理参考:
    • sign_bc_510 | sign_bc_520 | sign_bc_530
二级缓存
  • 开启参考:

    • sign_demo_120 | sign_demo_210
  • 使用注意点:

    • sign_demo_010
  • 原理参考:

    • sign_bc_410 | sign_bc_411 | sign_bc_412 | sign_bc_413
  • org.apache.ibatis.cache.TransactionalCacheManager

public class TransactionalCacheManager {private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();// sign_m_610public Object getObject(Cache cache, CacheKey key) {return getTransactionalCache(cache).getObject(key);}// sign_m_611 暂存public void putObject(Cache cache, CacheKey key, Object value) {getTransactionalCache(cache).putObject(key, value);     // 暂存,ref: sign_m_620}
}
  • org.apache.ibatis.cache.decorators.TransactionalCache
public class TransactionalCache implements Cache {private final Cache delegate; // Cache 链参考: sign_cc_010private final Map<Object, Object> entriesToAddOnCommit;   // 中间暂存// sign_m_620 暂存@Overridepublic void putObject(Object key, Object object) {entriesToAddOnCommit.put(key, object);}// sign_m_621 提交后,刷入最终的缓存public void commit() {...flushPendingEntries();  // ref: sign_m_622...}// sign_m_622private void flushPendingEntries() {for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {delegate.putObject(entry.getKey(), entry.getValue()); // 加入到最终的缓存}...}
}
  • Cache 链
// sign_cc_010
// (delegate)
TransactionalCache ->
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache// 上面相关实现原理较简单
//   SynchronizedCache 使用 ReentrantLock 加锁
//   LoggingCache 只是在 getObject() 方法记录和打印命中率
//   SerializedCache 使用 JDK 序列化
//   LruCache 使用 LinkedHashMap 实现 LRU
//   PerpetualCache 使用 HashMap 记录

Spring 集成

  • 注解扫描:
    • 示例 @MapperScan("com.baomidou.mybatisplus.test.h2.mapper")
    • org.mybatis.spring.annotation.MapperScan
      • 会引入注册类 org.mybatis.spring.annotation.MapperScannerRegistrar

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

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

相关文章

钉钉二次开发-企业内部系统集成官方OA审批流程(三)

书接上回&#xff0c;本文主要分享 企业内部系统集成钉钉官方OA审批流程的步骤 的第二部分。 前端代码集成钉钉免登JSAPI: 前端通过corpid 获得钉钉临时访问码code&#xff0c;再通过临时访问码code调用此接口返回当前用户的姓名、userid、 钉钉用户id、 系统工号、 钉钉部门…

从0开发一个Chrome插件:核心功能开发——弹出页面

前言 这是《从0开发一个Chrome插件》系列的第十一篇文章,本系列教你如何从0去开发一个Chrome插件,每篇文章都会好好打磨,写清楚我在开发过程遇到的问题,还有开发经验和技巧。 专栏: 从0开发一个Chrome插件:什么是Chrome插件?从0开发一个Chrome插件:开发Chrome插件的必…

从0开发一个Chrome插件:内容脚本实战——用户访问任何网页时,在页面顶部插入一条通知信息

前言 这是《从0开发一个Chrome插件》系列的第十篇文章,本系列教你如何从0去开发一个Chrome插件,每篇文章都会好好打磨,写清楚我在开发过程遇到的问题,还有开发经验和技巧。 专栏: 从0开发一个Chrome插件:什么是Chrome插件?从0开发一个Chrome插件:开发Chrome插件的必要…

如何使用Python中的random模块生成随机数

在Python中&#xff0c;random模块提供了多种用于生成随机数的函数。以下是一些基本示例&#xff1a; 生成随机整数&#xff1a; 使用random.randint(a, b)函数生成一个介于a和b之间的随机整数&#xff08;包括a和b&#xff09;。 python复制代码 import random random_int …

50etf期权怎么开户?期权懂有几种方式?

今天带你了解50etf期权怎么开户&#xff1f;期权懂有几种方式&#xff1f;50ETF期权开户可以通过证券公司、期权交易平台或期权交易应用进行。投资者需填写开户申请表格&#xff0c;提供身份证明和其他资料&#xff0c;完成开户手续。 50etf期权怎么开户&#xff1f; 满足资金…

欢乐钓鱼大师辅助:哪家云手机自动钓鱼更好操作!

在探索《欢乐钓鱼大师》的世界时&#xff0c;我们不得不提到一个强大的游戏辅助工具——VMOS云手机。通过VMOS云手机&#xff0c;你可以轻松实现自动钓鱼&#xff0c;让游戏体验更加便捷高效。 什么是VMOS云手机&#xff1f; VMOS云手机是一款基于虚拟机技术的云端工具&#…

【每日一函数】uname 函数介绍及代码演示

Linux uname 函数介绍及代码演示 引言 Linux 系统中&#xff0c;uname 是一个常用的命令行工具&#xff0c;用于显示系统信息。然而&#xff0c;在编程过程中&#xff0c;我们有时需要在程序中获取这些信息&#xff0c;此时就可以使用 uname 函数。本文将对 uname 函数进行详…

ubuntu20.04中设置包含ros节点的文件自启动

若文件里包含了ros话题的发布和接收&#xff0c;那么设置自启动时&#xff0c;应该首先将roscore设置为自启动。 首先确保roscore有一个systemd服务文件。如果还没有&#xff0c;需要在/etc/systemd/system/下创建一个。例如&#xff0c;一个基本的roscore.service文件可能如下…

安徽代理记账公司的专业服务和创新理念

在当今竞争激烈的市场环境中&#xff0c;为了提升企业的运营效率&#xff0c;许多企业开始寻找专业的代理记账公司进行财务管理和记账&#xff0c;本文将介绍一家名为安徽代理记账公司的专业服务和创新理念。 安徽代理记账公司是一家专注于为企业提供全方位会计服务的公司&…

Java异步处理:不使用线程池实现异步任务

在现代应用程序中,异步处理是一项重要的技术,它允许程序在执行耗时操作时不会阻塞主线程。尽管线程池是管理和调度线程的常用工具,但有时我们可能需要其他方法来实现异步处理。本文将介绍在Java中如何不使用线程池来处理异步任务,并提供详细的代码示例和解释。 一、什么是…

SwiftUI中Mask修饰符的理解与使用

Mask是一种用于控制图形元素可见性的图形技术&#xff0c;使用给定视图的alpha通道掩码该视图。在SwiftUI中&#xff0c;它类似于创建一个只显示视图的特定部分的模板。 Mask修饰符的定义&#xff1a; func mask<Mask>(alignment: Alignment .center,ViewBuilder _ ma…

大屏可视化建设方案(word)

1.系统概述 1.1.需求分析 1.2.重难点分析 1.3.重难点解决措施 2.系统架构设计 2.1.系统架构图 2.2.关键技术 2.3.接口及要求 3.系统功能设计 3.1.功能清单列表 3.2.数据源管理 3.3.数据集管理 3.4.视图管理 3.5.仪表盘管理 3.6.移动端设计 3.1.系统权限设计 3.…

9.1 Go 接口的定义

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

算法训练营day03--203.移除链表元素+707.设计链表+206.反转链表

一、203.移除链表元素 题目链接&#xff1a;https://leetcode.cn/problems/remove-linked-list-elements/ 文章讲解&#xff1a;https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html 视频讲解&#xff1a;https://www.bilibili.com…

如何使用Java进行安全的网络通信

在当今日益数字化的世界中&#xff0c;网络通信的安全性成为了至关重要的议题。Java作为一种广泛使用的编程语言&#xff0c;提供了多种工具和库来实现安全的网络通信。下面&#xff0c;我将从技术难点、面试官关注点、回答吸引力和代码举例四个方面&#xff0c;详细阐述如何使…

Python报错:IndentationError: unexpected indent问题的解决办法及原因

解决Python报错&#xff1a;IndentationError: unexpected indent问题的解决办法及原因 Python是一种注重可读性的编程语言&#xff0c;它使用缩进来定义代码块。如果你遇到了IndentationError: unexpected indent的错误&#xff0c;这意味着Python解释器在代码中遇到了意外的缩…

腾讯元宝APP上线:国内大模型产品的新篇章

近日&#xff0c;腾讯元宝APP的正式上线标志着国内大模型产品领域又迎来了一位强有力的竞争者。随着人工智能技术的飞速发展&#xff0c;我们见证了越来越多的“全能”大模型AIGC产品涌现&#xff0c;它们以其卓越的性能和广泛的应用场景&#xff0c;逐渐渗透到我们生活的各个角…

阿里云(域名解析) certbot 证书配置

1、安装 certbot ubuntu 系统&#xff1a; sudo apt install certbot 2、申请certbot 域名证书&#xff0c;如申请二级域名aa.example.com 的ssl证书&#xff0c;同时需要让 bb.aa.example.com 也可以使用此证书 1、命令&#xff1a;sudo certbot certonly -d “域名” -d “…

使用亮数据代理IP爬取PubMed文章链接和邮箱地址

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【工具大全】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

如何进行单元测试以及使用过的测试框架

在进行软件开发的过程中&#xff0c;单元测试是一个至关重要的环节&#xff0c;它确保代码的各个部分能够按照预期工作&#xff0c;从而提高软件的整体质量。下面我将从技术难点、面试官关注点、回答吸引力和代码举例四个方面&#xff0c;详细描述如何进行单元测试以及我所使用…