深入理解Mybatis原理》MyBatis的sqlSessi

sqlSessionFactory 與 SqlSession

正如其名,Sqlsession對應著一次數據庫會話。由于數據庫會話不是永久的,因此Sqlsession的生命周期也不應該是永久的,相反,在你每次訪問數據庫時都需要創建它(當然并不是說在Sqlsession里只能執行一次sql,你可以執行多次,當一旦關閉了Sqlsession就需要重新創建它)。

那么咱們就先看看是怎么獲取SqlSession的吧:

首先,SqlSessionFactoryBuilder去讀取mybatis的配置文件,然后build一個DefaultSqlSessionFactory。源碼如下:

 /*** 一系列的構造方法最終都會調用本方法(配置文件為Reader時會調用本方法,還有一個InputStream方法與此對應)* @param reader* @param environment* @param properties* @return*/public SqlSessionFactory build(Reader reader, String environment, Properties properties) {try {//通過XMLConfigBuilder解析配置文件,解析的配置相關信息都會封裝為一個Configuration對象XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);//這兒創建DefaultSessionFactory對象return build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);} finally {ErrorContext.instance().reset();try {reader.close();} catch (IOException e) {// Intentionally ignore. Prefer previous error.}}}public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);}

當我們獲取到SqlSessionFactory之后,就可以通過SqlSessionFactory去獲取SqlSession對象。源碼如下:

/*** 通常一系列openSession方法最終都會調用本方法* @param execType * @param level* @param autoCommit* @return*/private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {//通過Confuguration對象去獲取Mybatis相關配置信息, Environment對象包含了數據源和事務的配置final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);//之前說了,從表面上來看,咱們是用sqlSession在執行sql語句, 實際呢,其實是通過excutor執行, excutor是對于Statement的封裝final Executor executor = configuration.newExecutor(tx, execType);//關鍵看這兒,創建了一個DefaultSqlSession對象return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}

通過以上步驟,咱們已經得到SqlSession對象了。接下來就是該干嘛干嘛去了(話說還能干嘛,當然是執行sql語句咯)。看了上面,咱們也回想一下之前寫的Demo:

SqlSessionFactory sessionFactory = null;  
String resource = "mybatis-conf.xml";  
try {//SqlSessionFactoryBuilder讀取配置文件sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(resource));
} catch (IOException e) {  e.printStackTrace();  
}    
//通過SqlSessionFactory獲取SqlSession
SqlSession sqlSession = sessionFactory.openSession();

創建Sqlsession的地方只有一個,那就是SqlsessionFactory的openSession方法:

public SqlSessionopenSession() {  return openSessionFromDataSource(configuration.getDefaultExecutorType(),null, false);  
}

我們可以看到實際創建SqlSession的地方是openSessionFromDataSource,如下:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {  Connection connection = null;  try {  final Environment environment = configuration.getEnvironment();  final DataSource dataSource = getDataSourceFromEnvironment(environment);  // MyBatis對事務的處理相對簡單,TransactionIsolationLevel中定義了幾種隔離級別,并不支持內嵌事務這樣較復雜的場景,同時由于其是持久層的緣故,所以真正在應用開發中會委托Spring來處理事務實現真正的與開發者隔離。分析事務的實現是個入口,借此可以了解不少JDBC規范方面的事情。TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);  connection = dataSource.getConnection();  if (level != null) {  connection.setTransactionIsolation(level.getLevel());}  connection = wrapConnection(connection);  Transaction tx = transactionFactory.newTransaction(connection,autoCommit);  Executorexecutor = configuration.newExecutor(tx, execType);  return newDefaultSqlSession(configuration, executor, autoCommit);  } catch (Exceptione) {  closeConnection(connection);  throwExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);  } finally {ErrorContext.instance().reset();}
}  

可以看出,創建sqlsession經過了以下幾個主要步驟:

  • 從配置中獲取Environment;

  • 從Environment中取得DataSource;

  • 從Environment中取得TransactionFactory;

  • 從DataSource里獲取數據庫連接對象Connection;

  • 在取得的數據庫連接上創建事務對象Transaction;

  • 創建Executor對象(該對象非常重要,事實上sqlsession的所有操作都是通過它完成的);

  • 創建sqlsession對象。

SqlSession咱們也拿到了,咱們可以調用SqlSession中一系列的select..., insert..., update..., delete...方法輕松自如的進行CRUD操作了。就這樣?那咱配置的映射文件去哪兒了?別急,咱們接著往下看。

MapperProxy

在mybatis中,通過MapperProxy動態代理咱們的dao, 也就是說, 當咱們執行自己寫的dao里面的方法的時候,其實是對應的mapperProxy在代理。那么,咱們就看看怎么獲取MapperProxy對象吧:

通過SqlSession從Configuration中獲取。源碼如下:

 /*** 什么都不做,直接去configuration中找*/@Overridepublic <T> T getMapper(Class<T> type) {return configuration.<T>getMapper(type, this);}

SqlSession把包袱甩給了Configuration, 接下來就看看Configuration。源碼如下:

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);}

接著調用了MapperRegistry,源碼如下:

@SuppressWarnings("unchecked")public <T> T getMapper(Class<T> type, SqlSession sqlSession) {//交給MapperProxyFactory去做final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");}try {//關鍵在這兒return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}}

MapperProxyFactory源碼:

  @SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {//動態代理dao接口return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}

通過以上的動態代理,咱們就可以方便地使用dao接口啦, 就像之前咱們寫的demo那樣:

UserDao userMapper = sqlSession.getMapper(UserDao.class);  
User insertUser = new User();

這下方便多了吧, 呵呵, 貌似mybatis的源碼就這么一回事兒啊。具體詳細介紹,請參見MyBatis Mapper 接口如何通過JDK動態代理來包裝SqlSession 源碼分析。別急,還沒完, 咱們還沒看具體是怎么執行sql語句的呢。

Excutor

Executor與Sqlsession的關系就像市長與書記,Sqlsession只是個門面,真正干事的是Executor,Sqlsession對數據庫的操作都是通過Executor來完成的。與Sqlsession一樣,Executor也是動態創建的:

  • Executor創建的源代碼

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

可以看出,

  • 如果不開啟cache的話,創建的Executor是3種基礎類型之一BatchExecutor專門用于執行批量sql操作ReuseExecutor會重用statement執行sql操作SimpleExecutor只是簡單執行sql沒有什么特別的

  • 開啟cache的話(默認是開啟的并且沒有任何理由去關閉它),就會創建CachingExecutor,它以前面創建的Executor作為唯一參數。CachingExecutor在查詢數據庫前先查找緩存,若沒找到的話調用delegate(就是構造時傳入的Executor對象)從數據庫查詢,并將查詢結果存入緩存中。

Executor對象是可以被插件攔截的,如果定義了針對Executor類型的插件,最終生成的Executor對象是被各個插件插入后的代理對象。

接下來,去看sql的執行過程。上面,拿到了MapperProxy, 每個MapperProxy對應一個dao接口, 那么在使用的時候,MapperProxy是怎么做的呢?

  • MapperProxy

我們知道對被代理對象的方法的訪問都會落實到代理者的invoke上來,MapperProxy的invoke如下:

  /*** MapperProxy在執行時會觸發此方法*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (Object.class.equals(method.getDeclaringClass())) {try {return method.invoke(this, args);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}}final MapperMethod mapperMethod = cachedMapperMethod(method);//二話不說,主要交給MapperMethod自己去管return mapperMethod.execute(sqlSession, args);}

  • MapperMethod

就像是一個分發者,他根據參數和返回值類型選擇不同的sqlsession方法來執行。這樣mapper對象與sqlsession就真正的關聯起來了。

  /*** 看著代碼不少,不過其實就是先判斷CRUD類型,然后根據類型去選擇到底執行sqlSession中的哪個方法,繞了一圈,又轉回sqlSession了* @param sqlSession* @param args* @return*/public Object execute(SqlSession sqlSession, Object[] args) {Object result;if (SqlCommandType.INSERT == command.getType()) {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));} else if (SqlCommandType.UPDATE == command.getType()) {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));} else if (SqlCommandType.DELETE == command.getType()) {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));} else if (SqlCommandType.SELECT == command.getType()) {if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);} else if (method.returnsMap()) {result = executeForMap(sqlSession, args);} else {Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);}} else {throw new BindingException("Unknown execution method for: " + command.getName());}if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");}return result;}

既然又回到SqlSession了,前面提到過,sqlsession只是一個門面,真正發揮作用的是executor,對sqlsession方法的訪問最終都會落到executor的相應方法上去。Executor分成兩大類,一類是CacheExecutor,另一類是普通Executor。Executor的創建前面已經介紹了,那么咱們就看看SqlSession的CRUD方法了,為了省事,還是就選擇其中的一個方法來做分析吧。這兒,咱們選擇了selectList方法:

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement(statement);//CRUD實際上是交給Excetor去處理, excutor其實也只是穿了個馬甲而已,小樣,別以為穿個馬甲我就不認識你嘞!return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}

  • CacheExecutor

CacheExecutor有一個重要屬性delegate,它保存的是某類普通的Executor,值在構照時傳入。執行數據庫update操作時,它直接調用delegate的update方法,執行query方法時先嘗試從cache中取值,取不到再調用delegate的查詢方法,并將查詢結果存入cache中。代碼如下:

public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,ResultHandler resultHandler) throws SQLException {  if (ms != null) {  Cache cache = ms.getCache();  if (cache != null) {  flushCacheIfRequired(ms);  cache.getReadWriteLock().readLock().lock();  try {  if (ms.isUseCache() && resultHandler ==null) {  CacheKey key = createCacheKey(ms, parameterObject, rowBounds);  final List cachedList = (List)cache.getObject(key);  if (cachedList != null) {  return cachedList;  } else {  List list = delegate.query(ms,parameterObject, rowBounds, resultHandler);  tcm.putObject(cache,key, list);  return list;  }  } else {  return delegate.query(ms,parameterObject, rowBounds, resultHandler);  }  } finally {  cache.getReadWriteLock().readLock().unlock();  }}  }  return delegate.query(ms,parameterObject, rowBounds, resultHandler);  
}

  • 普通Executor

有3類,他們都繼承于BaseExecutor

  • BatchExecutor專門用于執行批量sql操作

  • ReuseExecutor會重用statement執行sql操作

  • SimpleExecutor只是簡單執行sql沒有什么特別的

下面以SimpleExecutor為例:

public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler) throws SQLException {  Statement stmt = null;  try {  Configuration configuration = ms.getConfiguration();  StatementHandler handler = configuration.newStatementHandler(this, ms,parameter, rowBounds,resultHandler);  stmt =prepareStatement(handler);  returnhandler.query(stmt, resultHandler);  } finally {  closeStatement(stmt);  }  
} 

然后,通過一層一層的調用,最終會來到doQuery方法, 這兒咱們就隨便找個Excutor看看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();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog());//StatementHandler封裝了Statement, 讓 StatementHandler 去處理return handler.<E>query(stmt, resultHandler);} finally {closeStatement(stmt);}}

Mybatis內置的ExecutorType有3種,默認的是simple,該模式下它為每個語句的執行創建一個新的預處理語句,單條提交sql;而batch模式重復使用已經預處理的語句, 并且批量執行所有更新語句,顯然batch性能將更優;

但batch模式也有自己的問題,比如在Insert操作時,在事務沒有提交之前,是沒有辦法獲取到自增的id,這在某型情形下是不符合業務要求的;

通過走碼和研讀spring相關文件發現,在同一事務中batch模式和simple模式之間無法轉換,由于本項目一開始選擇了simple模式,所以碰到需要批量更新時,只能在單獨的事務中進行;

在代碼中使用batch模式可以使用以下方式:

//從spring注入原有的sqlSessionTemplate
@Autowired
private SqlSessionTemplate sqlSessionTemplate;public void testInsertBatchByTrue() {//新獲取一個模式為BATCH,自動提交為false的session//如果自動提交設置為true,將無法控制提交的條數,改為最后統一提交,可能導致內存溢出SqlSession session = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);//通過新的session獲取mapperfooMapper = session.getMapper(FooMapper.class);int size = 10000;try {for (int i = 0; i < size; i++) {Foo foo = new Foo();foo.setName(String.valueOf(System.currentTimeMillis()));fooMapper.insert(foo);if (i % 1000 == 0 || i == size - 1) {//手動每1000個一提交,提交后無法回滾session.commit();//清理緩存,防止溢出session.clearCache();}}} catch (Exception e) {//沒有提交的數據可以回滾session.rollback();} finally {session.close();}
}

上述代碼沒有使用spring的事務,改手動控制,如果和原spring事務一起使用,將無法回滾,必須注意,最好單獨使用;

StatementHandler

可以看出,Executor本質上也沒有進行處理,具體的事情原來是StatementHandler來完成的。當Executor將指揮棒交給StatementHandler后,接下來的工作就是StatementHandler的事了。我們先看看StatementHandler是如何創建的:

public StatementHandler newStatementHandler(Executor executor, MappedStatementmappedStatement,  ObjectparameterObject, RowBounds rowBounds, ResultHandler resultHandler) {  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement,parameterObject,rowBounds, resultHandler);  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);  return statementHandler;
}  

可以看到每次創建的StatementHandler都是RoutingStatementHandler,它只是一個分發者,他一個屬性delegate用于指定用哪種具體的StatementHandler。可選的StatementHandler有SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler三種。選用哪種在mapper配置文件的每個statement里指定,默認的是PreparedStatementHandler。同時還要注意到StatementHandler是可以被攔截器攔截的,和Executor一樣,被攔截器攔截后的對像是一個代理對象。由于mybatis沒有實現數據庫的物理分頁,眾多物理分頁的實現都是在這個地方使用攔截器實現的,本文作者也實現了一個分頁攔截器,在后續的章節會分享給大家,敬請期待。

StatementHandler創建后需要執行一些初始操作,比如statement的開啟和參數設置、對于PreparedStatement還需要執行參數的設置操作等。代碼如下:

private Statement prepareStatement(StatementHandler handler) throws SQLException {  Statement stmt;  Connection connection = transaction.getConnection();  stmt =handler.prepare(connection);  handler.parameterize(stmt);  return stmt;  
}

statement的開啟和參數設置沒什么特別的地方,handler.parameterize倒是可以看看是怎么回事。handler.parameterize通過調用ParameterHandler的setParameters完成參數的設置,ParameterHandler隨著StatementHandler的創建而創建,默認的實現是DefaultParameterHandler:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {  ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,parameterObject,boundSql);  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);  return parameterHandler;  
}

同Executor和StatementHandler一樣,ParameterHandler也是可以被攔截的。DefaultParameterHandler里設置參數的代碼如下:

public void setParameters(PreparedStatement ps) throws SQLException {  ErrorContext.instance().activity("settingparameters").object(mappedStatement.getParameterMap().getId());  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  if(parameterMappings != null) {  MetaObject metaObject = parameterObject == null ? null :configuration.newMetaObject(parameterObject);  for (int i = 0; i< parameterMappings.size(); i++) {  ParameterMapping parameterMapping = parameterMappings.get(i);  if(parameterMapping.getMode() != ParameterMode.OUT) {  Object value;  String propertyName = parameterMapping.getProperty();  PropertyTokenizer prop = newPropertyTokenizer(propertyName);  if (parameterObject == null) {  value = null;  } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())){  value = parameterObject;  } else if (boundSql.hasAdditionalParameter(propertyName)){  value = boundSql.getAdditionalParameter(propertyName);  } else if(propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)  && boundSql.hasAdditionalParameter(prop.getName())){  value = boundSql.getAdditionalParameter(prop.getName());  if (value != null) {  value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));  }  } else {  value = metaObject == null ? null :metaObject.getValue(propertyName);  }  TypeHandler typeHandler = parameterMapping.getTypeHandler();  if (typeHandler == null) {  throw new ExecutorException("Therewas no TypeHandler found for parameter " + propertyName  + " of statement " + mappedStatement.getId());  }  typeHandler.setParameter(ps, i + 1, value,parameterMapping.getJdbcType());  }  }  }  
} 

這里面最重要的一句其實就是最后一句代碼,它的作用是用合適的TypeHandler完成參數的設置。那么什么是合適的TypeHandler呢,它又是如何決斷出來的呢?BaseStatementHandler的構造方法里有這么一句:

this.boundSql= mappedStatement.getBoundSql(parameterObject);

它觸發了sql 的解析,在解析sql的過程中,TypeHandler也被決斷出來了,決斷的原則就是根據參數的類型和參數對應的JDBC類型決定使用哪個TypeHandler。比如:參數類型是String的話就用StringTypeHandler,參數類型是整數的話就用IntegerTypeHandler等。

參數設置完畢后,執行數據庫操作(update或query)。如果是query最后還有個查詢結果的處理過程。

接下來,咱們看看StatementHandler 的一個實現類 PreparedStatementHandler(這也是我們最常用的,封裝的是PreparedStatement), 看看它使怎么去處理的:

  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {// 到此,原形畢露, PreparedStatement, 這個大家都已經滾瓜爛熟了吧PreparedStatement ps = (PreparedStatement) statement;ps.execute();// 結果交給了ResultSetHandler 去處理return resultSetHandler.<E> handleResultSets(ps);}

結果處理使用ResultSetHandler來完成,默認的ResultSetHandler是FastResultSetHandler,它在創建StatementHandler時一起創建,代碼如下:

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement,  
RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {  ResultSetHandler resultSetHandler = mappedStatement.hasNestedResultMaps() ? newNestedResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds): new FastResultSetHandler(executor,mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);  resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);  return resultSetHandler;  
} 

可以看出ResultSetHandler也是可以被攔截的,可以編寫自己的攔截器改變ResultSetHandler的默認行為。ResultSetHandler內部一條記錄一條記錄的處理,在處理每條記錄的每一列時會調用TypeHandler轉換結果,如下:

protected boolean applyAutomaticMappings(ResultSet rs, List<String> unmappedColumnNames,MetaObject metaObject) throws SQLException {  boolean foundValues = false;  for (String columnName : unmappedColumnNames) {  final String property = metaObject.findProperty(columnName);  if (property!= null) {  final ClasspropertyType =metaObject.getSetterType(property);  if (typeHandlerRegistry.hasTypeHandler(propertyType)) {  final TypeHandler typeHandler = typeHandlerRegistry.getTypeHandler(propertyType);  final Object value = typeHandler.getResult(rs,columnName);  if (value != null) {  metaObject.setValue(property, value);  foundValues = true;  }  }  }  }  return foundValues;  
}

從代碼里可以看到,決斷TypeHandler使用的是結果參數的屬性類型。因此我們在定義作為結果的對象的屬性時一定要考慮與數據庫字段類型的兼容性。到此, 一次sql的執行流程就完了。

文章轉載自:Seven

原文鏈接:《深入理解Mybatis原理》MyBatis的sqlSession執行流程 - seven97_top - 博客園

體驗地址:引邁 - JNPF快速開發平臺_低代碼開發平臺_零代碼開發平臺_流程設計器_表單引擎_工作流引擎_軟件架構

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/65274.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/65274.shtml
英文地址,請注明出處:http://en.pswp.cn/web/65274.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

《HarmonyOS第一課》煥新升級,賦能開發者快速掌握鴻蒙應用開發

隨著HarmonyOS NEXT發布&#xff0c;鴻蒙生態日益壯大&#xff0c;廣大開發者對于系統化學習平臺和課程的需求愈發強烈。近日&#xff0c;華為精心打造的《HarmonyOS第一課》全新上線&#xff0c;集“學、練、考”于一體&#xff0c;憑借多維融合的教學模式與系統課程設置&…

springboot集成整合工作流,activiti審批流,整合實際案例,流程圖設計,流程自定義,表單配置自定義,代碼demo流程

前言 activiti工作流引擎項目&#xff0c;企業erp、oa、hr、crm等企事業辦公系統輕松落地&#xff0c;一套完整并且實際運用在多套項目中的案例&#xff0c;滿足日常業務流程審批需求。 一、項目形式 springbootvueactiviti集成了activiti在線編輯器&#xff0c;流行的前后端…

《探秘計算機視覺與深度學習:開啟智能視覺新時代》

《探秘計算機視覺與深度學習&#xff1a;開啟智能視覺新時代》 一、追溯起源&#xff1a;從萌芽到嶄露頭角二、核心技術&#xff1a;解鎖智能視覺的密碼&#xff08;一&#xff09;卷積神經網絡&#xff08;CNN&#xff09;&#xff1a;圖像識別的利器&#xff08;二&#xff0…

設計模式-結構型-適配器模式

在軟件開發中&#xff0c;隨著系統的不斷擴展和模塊的不斷增加&#xff0c;往往會遇到不同模塊之間接口不兼容的情況。此時&#xff0c;如果我們能通過某種方式將一個接口轉化為另一個接口&#xff0c;那么開發工作將變得更加靈活和高效。適配器模式&#xff08;Adapter Patter…

Vmware安裝centos

用來記錄自己安裝的過程 一、創建虛擬機安裝centos鏡像 點擊完成后&#xff0c;等待一會會進入centos的系統初始化界面 二、centos初始化配置 三、配置網絡 1、虛擬網絡編輯器&#xff0c;開啟VMnet1、VMnet8的DHCP vmware左上角工具欄&#xff0c;點擊【編輯】->【虛擬網…

Unity-Mirror網絡框架-從入門到精通之Chat示例

文章目錄 前言Chat聊天室Authentication授權ChatAuthenticatorChat示例中的授權流程聊天Chat最后 前言 在現代游戲開發中&#xff0c;網絡功能日益成為提升游戲體驗的關鍵組成部分。Mirror是一個用于Unity的開源網絡框架&#xff0c;專為多人游戲開發設計。它使得開發者能夠輕…

知識問答系統

文章目錄 早期的問答系統基于信息檢索的問答系統基于知識庫的問答系統CommunityQA/FAQ-QA:基于問答對匹配的問答系統Hybrid QA Framework混合問答系統框架早期的問答系統 20世紀六七十年代,早期的NLIDB(Natural Language Interface toData bBase)伴隨著人工智能的研發逐步興起…

第3章:Go語言復合數據類型

第3章&#xff1a;Go語言復合數據類型 1. 數組 1.1 數組聲明和初始化 // 方式1&#xff1a;聲明固定長度數組 var numbers [5]int // 聲明一個包含5個整數的數組&#xff0c;默認零值// 方式2&#xff1a;初始化數組 arr1 : [5]int{1, 2, 3, 4, 5} // 完全初始化// 方式3&…

uniapp-vue3 實現, 一款帶有絲滑動畫效果的單選框組件,支持微信小程序、H5等多端

采用 uniapp-vue3 實現, 是一款帶有絲滑動畫效果的單選框組件&#xff0c;提供點狀、條狀的動畫過渡效果&#xff0c;支持多項自定義配置&#xff0c;適配 web、H5、微信小程序&#xff08;其他平臺小程序未測試過&#xff0c;可自行嘗試&#xff09; 可到插件市場下載嘗試&…

深度學習GPU服務器推薦:打造高效運算平臺

文章來源于百家號&#xff1a;GPU服務器廠家 在深度學習和人工智能領域&#xff0c;一個高性能的GPU服務器是研究和開發工作的關鍵。今天&#xff0c;我們將為大家推薦一款基于詳細硬件配置表的深度學習GPU服務器&#xff0c;它專為高效運算和數據處理而設計。 一、機箱設計 …

2025第1周 | JavaScript中的正則表達式

目錄 1. 正則表達式是個什么東東&#xff1f;1.1 怎么定義正則1.2 對象字面量方式1.3 類創建方式 2. 怎么使用2.1 實例方法2.1.1 exec方法2.1.2 test方法 2.2 字符串中的方法2.2.1 match/matchAll2.2.2 replace/replaceAll2.2.3 split2.2.4 search 3. 規則3.1 修飾符3.2 字符類…

大模型LLM-Prompt-OPTIMAL

1 OPTIMAL OPTIMAL 具體每項內容解釋如下&#xff1a; Objective Clarity&#xff08;目標清晰&#xff09;&#xff1a;明確定義任務的最終目標和預期成果。 Purpose Definition&#xff08;目的定義&#xff09;&#xff1a;闡述任務的目的和它的重要性。 Information Gat…

78、使用愛芯派2_AX630C開發板 3.2T高有效算力 低功耗 支持AI-ISP真黑光實驗

基本思想:使用愛心元智最新的版本開發板進行實驗 AX630C、AX620Q 都是 620E 這一代 一、參考這個官方教程,先把代碼在本地交叉編譯完成 https://github.com/AXERA-TECH/ax620e_bsp_sdk 然后在拷貝到620c設備上 root@ax630c:~/ax620e_bsp_sdk/msp/out/arm64_glibc/bin# ./…

C語言 掃雷程序設計

目錄 1.main函數 2.菜單打印menu函數 3.游戲game函數 4.宏定義 5.界面初始化 6.打印界面 7.設置雷 8.統計排查坐標周圍雷的個數 9.排查雷 10.總代碼 test.c代碼 game.h代碼 game.c代碼 結語&#xff1a; 一個簡單的掃雷游戲&#xff0c;通過宏定義可以修改行列的…

《高速公路警察模擬器》

一個引人入勝的警察故事在等著你&#xff0c;你可以選擇扮演男警官或女警官。公路警察模擬器》擁有休閑和模擬兩種游戲模式&#xff0c;將兩個世界的精華結合在一起&#xff1a;在身臨其境的虛擬環境中自由駕駛和行走&#xff0c;在故事驅動的游戲中解決各種令人興奮的案件。探…

EasyGBS小知識:如何確保攝像機的網絡連接穩定?

在當今數字化時代&#xff0c;視頻監控系統已成為保障安全和提高效率的重要工具。然而&#xff0c;攝像機的網絡連接穩定性直接關系到監控系統的可靠性和有效性。為了確保視頻監控系統能夠持續穩定地運行&#xff0c;我們需要從硬件、網絡設置、軟件與監控以及安裝與維護等多個…

微服務-Eureka

Eureka的作用 使用RestTemplate完成遠程調用需要被調用者的ip和端口&#xff0c;從而能夠發起http請求&#xff0c;但是如果有很多個實例也更加不能有效的處理&#xff0c;而且我們又該如何知道這些實例是否健康呢。所以就有了很多的注冊中心比如Eureka、Nacos等等。 服務注…

LabVIEW軟件侵權分析與應對

問&#xff1a;如果涉及到LabVIEW軟件的仿制或模仿&#xff0c;特別是在功能、界面等方面&#xff0c;如何判斷是否構成侵權&#xff1f;該如何應對&#xff1f; 答&#xff1a;LabVIEW軟件的侵權問題&#xff0c;尤其是在涉及到仿制或模仿其功能、界面、設計等方面&#xff0…

MATLAB仿真:基于GS算法的經大氣湍流畸變渦旋光束波前校正仿真

GS算法流程 GS&#xff08;Gerchberg-Saxton&#xff09;相位恢復算法是一種基于傅里葉變換的最速下降算法&#xff0c;可以通過輸出平面和輸入平面上光束的光強分布計算出光束的相位分布。圖1是基于GS算法的渦旋光束畸變波前校正系統框圖&#xff0c;在該框圖中&#xff0c;已…

數樹數(中等難度)

題目&#xff1a; 解題代碼&#xff1a; n,qmap(int,input().split())#分別輸入層數和路徑數量 for i in range(q):sinput()#輸入“L”或“R”x1for j in s:if j "L":xx*2-1 #&#xff01;&#xff01;&#xff01;規律else:xx*2print(x)