欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時間:2021年07月11日 11:41:52   作者:Java有貨  
本文主要介紹了淺談Mybatis SqlSession執(zhí)行流程,文中通過示例代碼介紹的非常詳細,需要的朋友們下面隨著小編來一起學習學習吧

Mybatis執(zhí)行SQL流程

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

SqlSession

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

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. 讀取配置文件,讀成字節(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

//6. 進入openSession方法
@Override
public SqlSession openSession() {
    //getDefaultExecutorType()傳遞的是SimpleExecutor
    // level:數(shù)據(jù)庫事物級別,null
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

//7. 進入openSessionFromDataSource。
//ExecutorType 為Executor的類型,TransactionIsolationLevel為事務(wù)隔離級別,autoCommit是否開啟事務(wù)
//openSession的多個重載方法可以指定獲得的SeqSession的Executor類型和事務(wù)的處理
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ā)生異常,則關(guān)閉 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()查看

//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查看

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

//此方法在SimpleExecutor的父類BaseExecutor中實現(xiàn)
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //根據(jù)傳入的參數(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)先調(diào)用MappedStatement的getBoundSql方法,獲取解析后的SQL語句,解析工作是由SqlSourceBuilder完成的

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

/**
 * 執(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)根據(jù)查詢條件,創(chuàng)建緩存key,用來接下來去緩存查找是否有已經(jīng)執(zhí)行過的結(jié)果

(3)調(diào)用重載query()方法

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

@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());
        // 已經(jīng)關(guān)閉,則拋出 ExecutorException 異常
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 清空本地緩存,如果 queryStack 為零,并且要求清空本地緩存。
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            // queryStack + 1
            queryStack++;
            // 從一級緩存中,獲取查詢結(jié)果
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            // 獲取到,則進行處理
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            // 獲得不到,則從數(shù)據(jù)庫中查詢
            } 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ù)據(jù),如果有直接使用緩存的進行接下來的操作
  • 如果沒有,從數(shù)據(jù)庫查詢

進入queryFromDatabase()方法:

// 從數(shù)據(jù)庫中讀取操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 在緩存中,添加占位對象。此處的占位符,和延遲加載有關(guān),可見 `DeferredLoad#canLoad()` 方法
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 執(zhí)行讀操作
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 從緩存中,移除占位對象
        localCache.removeObject(key);
    }
    // 添加到緩存中
    localCache.putObject(key, list);
    // 暫時忽略,存儲過程相關(guān)
    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();
        // 傳入?yún)?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 {
        // 關(guān)閉 StatementHandler 對象
        closeStatement(stmt);
    }
}

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

Mybatis之StatementHandler

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

@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();
        // 傳入?yún)?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 {
        // 關(guān)閉 StatementHandler 對象
        closeStatement(stmt);
    }
}

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

我們進入prepareStatement方法中查看

// 初始化 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());
    // 設(shè)置 SQL 上的參數(shù),例如 PrepareStatement 對象上的占位符
    handler.parameterize(stmt);
    return stmt;
}

@Override
public void parameterize(Statement statement) throws SQLException {
    //使用ParameterHandler對象來完成對Statement的設(shè)值
    parameterHandler.setParameters((PreparedStatement) statement);
}

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

@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è)置 ? 占位符的參數(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);
                }
            }
        }
    }
}

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

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

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

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

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 執(zhí)行查詢
    ps.execute();
    // 處理返回結(jié)果
    return resultSetHandler.handleResultSets(ps);
}

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

進入ResultSetHandler

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    // 多 ResultSet 的結(jié)果集合,每個 ResultSet 對應(yīng)一個 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 ,將結(jié)果添加到 multipleResults 中
        handleResultSet(rsw, resultMap, multipleResults, null);
        // 獲得下一個 ResultSet 對象,并封裝成 ResultSetWrapper 對象
        rsw = getNextResultSet(stmt);
        // 清理
        cleanUpAfterHandlingResultSet();
        // resultSetCount ++
        resultSetCount++;
    }

    // 因為 `mappedStatement.resultSets` 只在存儲過程中使用,本系列暫時不考慮,忽略即可
    // ···

    // 如果是 multipleResults 單元素,則取首元素返回
    return collapseSingleResultList(multipleResults);
}

 // 處理 ResultSet ,將結(jié)果添加到 multipleResults 中
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
        // 暫時忽略,因為只有存儲過程的情況,調(diào)用該方法,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 的處理的結(jié)果,到 multipleResults 中
                multipleResults.add(defaultResultHandler.getResultList());
            } else {
                // 處理 ResultSet 返回的每一行 Row
                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
            }
        }
    } finally {
        // issue #228 (close resultsets)
        // 關(guān)閉 ResultSet 對象
        closeResultSet(rsw.getResultSet());
    }
}

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

// 添加 defaultResultHandler 的處理的結(jié)果,到 multipleResults 中
multipleResults.add(defaultResultHandler.getResultList());

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

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

簡單總結(jié)
SqlSessionFactoryBuilder:

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

SqlSessionFactory:

  • openSession():構(gòu)建Executor,SqlSession等

SqlSession:

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

Executor:

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

StatementHandler:

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

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

相關(guān)文章

  • Mybatis常用分頁插件實現(xiàn)快速分頁處理技巧

    Mybatis常用分頁插件實現(xiàn)快速分頁處理技巧

    這篇文章主要介紹了Mybatis常用分頁插件實現(xiàn)快速分頁處理的方法。非常不錯具有參考借鑒價值,感興趣的朋友一起看看
    2016-10-10
  • SpringBoot如何使用內(nèi)嵌Tomcat問題

    SpringBoot如何使用內(nèi)嵌Tomcat問題

    這篇文章主要介紹了SpringBoot如何使用內(nèi)嵌Tomcat問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • java基于jdbc實現(xiàn)簡單學生管理系統(tǒng)

    java基于jdbc實現(xiàn)簡單學生管理系統(tǒng)

    本文主要主要介紹了java連接mysql數(shù)據(jù)庫的一個簡單學生系統(tǒng),通過jdbc連接數(shù)據(jù)庫。文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-10-10
  • Java 動態(tài)代理與CGLIB詳細介紹

    Java 動態(tài)代理與CGLIB詳細介紹

    這篇文章主要介紹了 Java 動態(tài)代理與CGLIB詳細介紹的相關(guān)資料,需要的朋友可以參考下
    2017-02-02
  • java日志門面之JCL和SLF4J詳解

    java日志門面之JCL和SLF4J詳解

    這篇文章主要給大家介紹了關(guān)于java日志門面之JCL和SLF4J的相關(guān)資料,在系統(tǒng)開發(fā)過程中日志框架的選擇與更換是一大挑戰(zhàn),日志門面的概念,如JCL和SLF4J,允許開發(fā)者面向接口編程,文中介紹的非常詳細,需要的朋友可以參考下
    2024-10-10
  • mybatis-plus指定字段模糊查詢的實現(xiàn)方法

    mybatis-plus指定字段模糊查詢的實現(xiàn)方法

    最近項目中使用springboot+mybatis-plus來實現(xiàn),所以下面這篇文章主要給大家介紹了關(guān)于mybatis-plus實現(xiàn)指定字段模糊查詢的相關(guān)資料,需要的朋友可以參考下
    2022-04-04
  • hibernate4快速入門實例詳解

    hibernate4快速入門實例詳解

    Hibernate是一個輕量級的ORMapping框架,本文重點給大家介紹hibernate4 入門實例詳細,需要的朋友參考下吧
    2017-09-09
  • 通過idea創(chuàng)建Spring Boot項目并配置啟動過程圖解

    通過idea創(chuàng)建Spring Boot項目并配置啟動過程圖解

    這篇文章主要介紹了通過idea創(chuàng)建Spring Boot項目并配置啟動過程圖解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2019-11-11
  • 部署springboot打包不打包配置文件,配置文件為外部配置文件使用詳解

    部署springboot打包不打包配置文件,配置文件為外部配置文件使用詳解

    在Spring Boot項目中,將配置文件排除在jar包之外,通過外部配置文件來管理不同環(huán)境的配置,可以實現(xiàn)靈活的配置管理,在pom.xml文件中添加相關(guān)配置,打包時忽略指定文件,運行時在jar包同級目錄下創(chuàng)建config文件夾,將配置文件放入其中即可
    2025-02-02
  • JAVA保證HashMap線程安全的幾種方式

    JAVA保證HashMap線程安全的幾種方式

    HashMap是線程不安全的,這意味著如果多個線程并發(fā)地訪問和修改同一個HashMap實例,可能會導致數(shù)據(jù)不一致和其他線程安全問題,本文主要介紹了JAVA保證HashMap線程安全的幾種方式,具有一定的參考價值,感興趣的可以了解一下
    2025-04-04

最新評論