激情久久久_欧美视频区_成人av免费_不卡视频一二三区_欧美精品在欧美一区二区少妇_欧美一区二区三区的

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

香港云服务器
服務器之家 - 編程語言 - Java教程 - 淺談Mybatis SqlSession執(zhí)行流程

淺談Mybatis SqlSession執(zhí)行流程

2021-10-03 16:13Java有貨 Java教程

本文主要介紹了淺談Mybatis SqlSession執(zhí)行流程,文中通過示例代碼介紹的非常詳細,需要的朋友們下面隨著小編來一起學習學習吧

Mybatis執(zhí)行SQL流程

在看源碼之前,我們需要了解一些基本知識,如果您沒有閱讀Mybatis SqlSessionFactory 初始化原理,可以先閱讀Mybatis SqlSessionFactory 初始化原理這篇文章,這用更有助于我們理解接下來的文章
在看源碼之前,我們需要了解一些基本知識

SqlSession

SqlSession是一個接口,它有兩個實現(xiàn)類:
 - DefaultSqlSession:默認實現(xiàn)類
 - SqlSessionManager:已經棄用的實現(xiàn)類,所以我們不需要關注他
SqlSession是與數(shù)據庫交互的頂層類,通常與ThreadLocal綁定,一個會話使用一個SqlSession,SqlSession是線程不安全的,使用完畢需要close()

?
1
2
3
4
public class DefaultSqlSession implements SqlSession {
 private final Configuration configuration;
 private final Executor executor;
}

SqlSession中最重要的兩個變量:
 - Configuration:核心配置類,也是初始化時傳過來的
 - Executor:實際執(zhí)行SQL的執(zhí)行器

Executor

Executor是一個接口,有三個實現(xiàn)類
 - BatchExecutor 重用語句,并執(zhí)行批量更新
 - ReuseExecutor 重用預處理語句prepared statements
 - SimpleExecutor 普通的執(zhí)行器,默認使用

了解完基本知識后,我們接著往下看代碼。

當創(chuàng)建完SqlSessionFactory后,就可以創(chuàng)建SqlSession,然后使用SqlSession進行增刪改查:

?
1
2
3
4
5
6
7
// 1. 讀取配置文件,讀成字節(jié)輸入流,注意:現(xiàn)在還沒解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. 解析配置文件,封裝Configuration對象   創(chuàng)建DefaultSqlSessionFactory對象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
 
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> objects = sqlSession.selectList("namespace.id");

我們先去看openSession()方法,創(chuàng)建了SqlSession

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//6. 進入openSession方法
@Override
public SqlSession openSession() {
    //getDefaultExecutorType()傳遞的是SimpleExecutor
    // level:數(shù)據庫事物級別,null
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
 
//7. 進入openSessionFromDataSource。
//ExecutorType 為Executor的類型,TransactionIsolationLevel為事務隔離級別,autoCommit是否開啟事務
//openSession的多個重載方法可以指定獲得的SeqSession的Executor類型和事務的處理
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 獲得 Environment 對象
        final Environment environment = configuration.getEnvironment();
        // 創(chuàng)建 Transaction 對象
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 創(chuàng)建 Executor 對象
        final Executor executor = configuration.newExecutor(tx, execType);
        // 創(chuàng)建 DefaultSqlSession 對象
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        // 如果發(fā)生異常,則關閉 Transaction 對象
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

通過源碼可以清晰的看到,會話工廠創(chuàng)建了Environment,Transaction,Executor,DefaultSqlSession對象,并且對于會話對象來說,他的autoCommit默認為false,默認不自動提交。

然后我回到原來的代碼,接著就需要使用SqlSession進行增刪改查操作了

所以我們進入selectList()查看

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//8.進入selectList方法,多個重載方法
@Override
public <E> List<E> selectList(String statement) {
    return this.selectList(statement, null);
}
 
@Override
public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
 
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        // 獲得 MappedStatement 對象
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 執(zhí)行查詢
        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();
    }
}

selectList有多個重載方法,進入到最終方法后,我們可以看到它做了兩件事

  • 通過statementId,從Configuration中取MappedStatement對象,就是存放了sql語句,返回值類型,輸入值類型的對象
  • 然后委派Executor執(zhí)行器去執(zhí)行具體的增刪改查方法

所以,對于實際JDBC的操作,我們還需要進入Executor中查看

Mybatis之Executor

我們繼續(xù)從剛剛selectList源碼中,進入Executor查看

?
1
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
?
1
2
3
4
5
6
7
8
9
10
//此方法在SimpleExecutor的父類BaseExecutor中實現(xiàn)
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //根據傳入的參數(shù)動態(tài)獲得SQL語句,最后返回用BoundSql對象表示
    BoundSql boundSql = ms.getBoundSql(parameter);
    //為本次查詢創(chuàng)建緩存的Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 查詢
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

拆分成了三大步:

(1)先調用MappedStatement的getBoundSql方法,獲取解析后的SQL語句,解析工作是由SqlSourceBuilder完成的

什么叫解析后的SQL語句呢?因為Mybatis編寫SQL語句時,會用到動態(tài)SQL,比如#{}占位符,這種占位符JDBC是不認識的,所以需要將其轉換成?占位符,并且將其內部的字段名存儲起來,后面填充參數(shù)的時候好使用反射獲取值。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * 執(zhí)行解析原始 SQL ,成為 SqlSource 對象
 *
 * @param originalSql 原始 SQL
 * @param parameterType 參數(shù)類型
 * @param additionalParameters 附加參數(shù)集合。可能是空集合,也可能是 {@link org.apache.ibatis.scripting.xmltags.DynamicContext#bindings} 集合
 * @return SqlSource 對象
 */
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // 創(chuàng)建 ParameterMappingTokenHandler 對象
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 創(chuàng)建 GenericTokenParser 對象
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 執(zhí)行解析
    String sql = parser.parse(originalSql);
    // 創(chuàng)建 StaticSqlSource 對象
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

上面代碼就可以看到,會將拆分#{和},進行解析

(2)根據查詢條件,創(chuàng)建緩存key,用來接下來去緩存查找是否有已經執(zhí)行過的結果

(3)調用重載query()方法

接著我們進入重載方法查看:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        // 已經關閉,則拋出 ExecutorException 異常
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 清空本地緩存,如果 queryStack 為零,并且要求清空本地緩存。
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            // queryStack + 1
            queryStack++;
            // 從一級緩存中,獲取查詢結果
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            // 獲取到,則進行處理
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            // 獲得不到,則從數(shù)據庫中查詢
            } else {
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            // queryStack - 1
            queryStack--;
        }
        if (queryStack == 0) {
            // 執(zhí)行延遲加載
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            // 清空 deferredLoads
            deferredLoads.clear();
            // 如果緩存級別是 LocalCacheScope.STATEMENT ,則進行清理
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }

主要的邏輯:

  • 從一級緩存取數(shù)據,如果有直接使用緩存的進行接下來的操作
  • 如果沒有,從數(shù)據庫查詢

進入queryFromDatabase()方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 從數(shù)據庫中讀取操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 在緩存中,添加占位對象。此處的占位符,和延遲加載有關,可見 `DeferredLoad#canLoad()` 方法
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 執(zhí)行讀操作
        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;
}
 
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        // 傳入參數(shù)創(chuàng)建StatementHanlder對象來執(zhí)行查詢
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 創(chuàng)建jdbc中的statement對象
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 執(zhí)行 StatementHandler  ,進行讀操作
        return handler.query(stmt, resultHandler);
    } finally {
        // 關閉 StatementHandler 對象
        closeStatement(stmt);
    }
}

通過代碼可以看到,對于實際與JDBC交互的代碼,Executor也懶得搞,又像SqlSession一樣,委派給小弟StatementHandler了。

Mybatis之StatementHandler

我們從剛剛的Executor的代碼查看

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        // 傳入參數(shù)創(chuàng)建StatementHanlder對象來執(zhí)行查詢
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 創(chuàng)建jdbc中的statement對象
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 執(zhí)行 StatementHandler  ,進行讀操作
        return handler.query(stmt, resultHandler);
    } finally {
        // 關閉 StatementHandler 對象
        closeStatement(stmt);
    }
}

可以看到,這里創(chuàng)建完StatementHandler后,回調用prepareStatement()方法,用來創(chuàng)建Statement對象

我們進入prepareStatement方法中查看

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 初始化 StatementHandler 對象
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 獲得 Connection 對象
    Connection connection = getConnection(statementLog);
    // 創(chuàng)建 Statement 或 PrepareStatement 對象
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 設置 SQL 上的參數(shù),例如 PrepareStatement 對象上的占位符
    handler.parameterize(stmt);
    return stmt;
}
 
@Override
public void parameterize(Statement statement) throws SQLException {
    //使用ParameterHandler對象來完成對Statement的設值
    parameterHandler.setParameters((PreparedStatement) statement);
}

這里可以看到,它實際是使用ParameterHandler來設置Statement的參數(shù)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 遍歷 ParameterMapping 數(shù)組
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        for (int i = 0; i < parameterMappings.size(); i++) {
            // 獲得 ParameterMapping 對象
            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 params
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                // 獲得 typeHandler、jdbcType 屬性
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                if (value == null && jdbcType == null) {
                    jdbcType = configuration.getJdbcTypeForNull();
                }
                // 設置 ? 占位符的參數(shù)
                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);
                }
            }
        }
    }
}

這段代碼的主要目的,就是獲取入參,然后根據值,來設置?占位符的參數(shù)

TypeHandler是具體進行參數(shù)設置的對象

所以handler.prepare(connection, transaction.getTimeout());方法,就是使用ParameterHandler來對占位符位置的參數(shù)進行值設置

然后我們回到Executor,查看handler.query()方法

?
1
2
3
4
5
6
7
8
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 執(zhí)行查詢
    ps.execute();
    // 處理返回結果
    return resultSetHandler.handleResultSets(ps);
}

代碼很簡單,這里直接使用JDBC的PreparedStatement來進行SQL執(zhí)行,然后使用ResultSetHandler進行結果數(shù)據封裝處理。

進入ResultSetHandler

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
 
    // 多 ResultSet 的結果集合,每個 ResultSet 對應一個 Object 對象。而實際上,每個 Object 是 List<Object> 對象。
    // 在不考慮存儲過程的多 ResultSet 的情況,普通的查詢,實際就一個 ResultSet ,也就是說,multipleResults 最多就一個元素。
    final List<Object> multipleResults = new ArrayList<>();
 
    int resultSetCount = 0;
    // 獲得首個 ResultSet 對象,并封裝成 ResultSetWrapper 對象
    ResultSetWrapper rsw = getFirstResultSet(stmt);
 
    // 獲得 ResultMap 數(shù)組
    // 在不考慮存儲過程的多 ResultSet 的情況,普通的查詢,實際就一個 ResultSet ,也就是說,resultMaps 就一個元素。
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount); // 校驗
    while (rsw != null && resultMapCount > resultSetCount) {
        // 獲得 ResultMap 對象
        ResultMap resultMap = resultMaps.get(resultSetCount);
        // 處理 ResultSet ,將結果添加到 multipleResults 中
        handleResultSet(rsw, resultMap, multipleResults, null);
        // 獲得下一個 ResultSet 對象,并封裝成 ResultSetWrapper 對象
        rsw = getNextResultSet(stmt);
        // 清理
        cleanUpAfterHandlingResultSet();
        // resultSetCount ++
        resultSetCount++;
    }
 
    // 因為 `mappedStatement.resultSets` 只在存儲過程中使用,本系列暫時不考慮,忽略即可
    // ···
 
    // 如果是 multipleResults 單元素,則取首元素返回
    return collapseSingleResultList(multipleResults);
}
 
 // 處理 ResultSet ,將結果添加到 multipleResults 中
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
        // 暫時忽略,因為只有存儲過程的情況,調用該方法,parentMapping 為非空
        if (parentMapping != null) {
            handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
        } else {
            // 如果沒有自定義的 resultHandler ,則創(chuàng)建默認的 DefaultResultHandler 對象
            if (resultHandler == null) {
                // 創(chuàng)建 DefaultResultHandler 對象
                DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
                // 處理 ResultSet 返回的每一行 Row
                handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
                // 添加 defaultResultHandler 的處理的結果,到 multipleResults 中
                multipleResults.add(defaultResultHandler.getResultList());
            } else {
                // 處理 ResultSet 返回的每一行 Row
                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
            }
        }
    } finally {
        // issue #228 (close resultsets)
        // 關閉 ResultSet 對象
        closeResultSet(rsw.getResultSet());
    }
}

代碼比較多,實際最重要的代碼就是

?
1
2
// 添加 defaultResultHandler 的處理的結果,到 multipleResults 中
multipleResults.add(defaultResultHandler.getResultList());

將處理后的結果封裝到集合中返回,這樣基本Mybatis邏輯就走完了.

我們來回顧一下,都用到了哪些類

淺談Mybatis SqlSession執(zhí)行流程

簡單總結
SqlSessionFactoryBuilder:

  • 解析核心配置文件,創(chuàng)建Configuration
    • XMLConfigBuilder.parse():解析核心配置文件
    • XMLMapperBuilder.parse():解析映射配置文件MappedStatement
  • 創(chuàng)建SqlSessionFactory,默認創(chuàng)建DefaultSqlSessionFactory

SqlSessionFactory:

  • openSession():構建Executor,SqlSession等

SqlSession:

  • 根據statementId獲取MappedStatement
  • 委派給Executor執(zhí)行器執(zhí)行

Executor:

  • 使用SqlSourceBuilder,將SQL解析成JDBC認可的
  • 查詢緩存,是否存在結果
  • 結果不存在,委派給StatementHandler處理器

StatementHandler:

  • PreparedStatement:處理參數(shù),將參數(shù)賦值到占位符上
    • TypeHandler:具體設置值的類
  • ResultSetHandler:封裝結果集,封裝成設置的返回值類型
    • TypeHandler:根據結果集,取出對應列

到此這篇關于淺談Mybatis SqlSession執(zhí)行流程的文章就介紹到這了,更多相關Mybatis SqlSession執(zhí)行流程內容請搜索服務器之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持服務器之家!

原文鏈接:https://blog.csdn.net/weixin_38937840/article/details/118601796

延伸 · 閱讀

精彩推薦
  • Java教程Java8中Stream使用的一個注意事項

    Java8中Stream使用的一個注意事項

    最近在工作中發(fā)現(xiàn)了對于集合操作轉換的神器,java8新特性 stream,但在使用中遇到了一個非常重要的注意點,所以這篇文章主要給大家介紹了關于Java8中S...

    阿杜7472021-02-04
  • Java教程20個非常實用的Java程序代碼片段

    20個非常實用的Java程序代碼片段

    這篇文章主要為大家分享了20個非常實用的Java程序片段,對java開發(fā)項目有所幫助,感興趣的小伙伴們可以參考一下 ...

    lijiao5352020-04-06
  • Java教程xml與Java對象的轉換詳解

    xml與Java對象的轉換詳解

    這篇文章主要介紹了xml與Java對象的轉換詳解的相關資料,需要的朋友可以參考下...

    Java教程網2942020-09-17
  • Java教程小米推送Java代碼

    小米推送Java代碼

    今天小編就為大家分享一篇關于小米推送Java代碼,小編覺得內容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧...

    富貴穩(wěn)中求8032021-07-12
  • Java教程Java使用SAX解析xml的示例

    Java使用SAX解析xml的示例

    這篇文章主要介紹了Java使用SAX解析xml的示例,幫助大家更好的理解和學習使用Java,感興趣的朋友可以了解下...

    大行者10067412021-08-30
  • Java教程Java實現(xiàn)搶紅包功能

    Java實現(xiàn)搶紅包功能

    這篇文章主要為大家詳細介紹了Java實現(xiàn)搶紅包功能,采用多線程模擬多人同時搶紅包,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙...

    littleschemer13532021-05-16
  • Java教程升級IDEA后Lombok不能使用的解決方法

    升級IDEA后Lombok不能使用的解決方法

    最近看到提示IDEA提示升級,尋思已經有好久沒有升過級了。升級完畢重啟之后,突然發(fā)現(xiàn)好多錯誤,本文就來介紹一下如何解決,感興趣的可以了解一下...

    程序猿DD9332021-10-08
  • Java教程Java BufferWriter寫文件寫不進去或缺失數(shù)據的解決

    Java BufferWriter寫文件寫不進去或缺失數(shù)據的解決

    這篇文章主要介紹了Java BufferWriter寫文件寫不進去或缺失數(shù)據的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望...

    spcoder14552021-10-18
766
Weibo Article 1 Weibo Article 2 Weibo Article 3 Weibo Article 4 Weibo Article 5 Weibo Article 6 Weibo Article 7 Weibo Article 8 Weibo Article 9 Weibo Article 10 Weibo Article 11 Weibo Article 12 Weibo Article 13 Weibo Article 14 Weibo Article 15 Weibo Article 16 Weibo Article 17 Weibo Article 18 Weibo Article 19 Weibo Article 20 Weibo Article 21 Weibo Article 22 Weibo Article 23 Weibo Article 24 Weibo Article 25
主站蜘蛛池模板: 久久艹精品 | 久久久免费观看完整版 | 一区二区久久电影 | 青青草国产在线视频 | 国产精品视频一区二区噜噜 | 在线视频 欧美日韩 | 青久草视频 | 激情黄页 | 一级黄色免费观看 | 一级网站| 91精品国产福利尤物免费 | 一区二区三区日韩电影 | 亚洲无线看 | 一级看片免费视频 | 色妞欧美 | 欧美激情视频一区二区免费 | 中文在线观看视频 | jizzjizz中国人少妇中文 | 免费在线成人网 | 日韩免费黄色 | 55夜色66夜色国产精品视频 | 中文字幕 亚洲一区 | 欧美成人黄色小视频 | 在线影院av | 欧美三级欧美成人高清www | 久久久久亚洲视频 | av在线浏览| 一级做a爰性色毛片免费 | 福利在线免费视频 | 免费一级高清毛片 | 91精品国产九九九久久久亚洲 | 成人男女视频 | 精品久久久久99 | 亚洲精品成人久久 | 日本黄免费 | 亚洲成人自拍电影 | 男女一边摸一边做羞羞视频免费 | 国产成人自拍av | 91成人一区二区三区 | 国产成年人在线观看 | 欧洲成人综合网 |