最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

MyBatis核心源碼解析之從SqlSession到Mapper接口的綁定過程

 更新時間:2026年06月15日 08:32:18   作者:七夜zippoe  
這篇文章主要介紹了MyBatis核心源碼解析之從SqlSession到Mapper接口的綁定過程,從SqlSession創(chuàng)建到Mapper接口綁定,涵蓋動態(tài)代理、執(zhí)行器、緩存策略等關(guān)鍵點,通過實戰(zhàn)案例與性能測試數(shù)據(jù),提供優(yōu)化策略和故障排查指南,需要的朋友可以參考下

先說說我遇到過的真實問題

去年我們團隊接手一個老系統(tǒng),用的是MyBatis,但沒一個人懂原理。有天線上突然報錯:"Too many connections",數(shù)據(jù)庫連接池爆了。我們查了三天,最后發(fā)現(xiàn)是某個Mapper方法里寫了復雜的動態(tài)SQL,每次執(zhí)行都創(chuàng)建新SqlSession,但沒人close。

更絕的是,有次性能優(yōu)化,我們發(fā)現(xiàn)某個查詢方法被調(diào)用了1000次,但實際只需要查一次數(shù)據(jù)庫。后來一查,是MyBatis一級緩存在作怪,但團隊里沒人知道MyBatis還有緩存。

這些事讓我明白:不懂MyBatis源碼的程序員,就像開自動擋車不知道變速箱原理,早晚要出事

摘要

MyBatis作為Java生態(tài)中最流行的ORM框架,其核心在于SqlSession的動態(tài)管理和Mapper接口的綁定機制。本文深度解析MyBatis從配置文件加載、SqlSession創(chuàng)建、Mapper代理生成到SQL執(zhí)行的完整鏈路。通過源碼級分析,揭示動態(tài)代理、反射、JDBC封裝等底層技術(shù)如何協(xié)同工作。結(jié)合性能測試數(shù)據(jù)和實戰(zhàn)案例,提供MyBatis優(yōu)化策略和故障排查指南。

1. MyBatis架構(gòu)總覽:不只是"寫SQL的工具"

1.1 別把MyBatis看簡單了

很多人覺得MyBatis就是"寫SQL的框架",比Hibernate簡單多了。這話對了一半,MyBatis確實讓SQL更直觀,但它的內(nèi)部設(shè)計絕不簡單。

看看這個調(diào)用鏈路:

圖1:MyBatis完整執(zhí)行鏈路

看到?jīng)]?從你的Mapper方法調(diào)用到真正執(zhí)行SQL,中間隔了至少7層!每一層都有它的職責。

1.2 核心組件職責解析

組件

英文

職責

類比

SqlSession

SQL會話

對外API入口

餐廳服務(wù)員

Executor

執(zhí)行器

SQL執(zhí)行調(diào)度

廚師長

StatementHandler

語句處理器

操作Statement

廚師

ParameterHandler

參數(shù)處理器

設(shè)置參數(shù)

配菜員

ResultSetHandler

結(jié)果集處理器

處理結(jié)果

擺盤員

MapperProxy

Mapper代理

接口方法代理

點餐員

2. SqlSession:MyBatis的"大門"

2.1 SqlSession的創(chuàng)建過程

很多人以為SqlSession就是個簡單的數(shù)據(jù)庫連接包裝,大錯特錯!看看它的創(chuàng)建過程:

// 這是SqlSessionFactory創(chuàng)建SqlSession的核心方法
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
    @Override
    public SqlSession openSession() {
        return openSessionFromDataSource(
            configuration.getDefaultExecutorType(),  // 執(zhí)行器類型
            null,  // 事務(wù)隔離級別
            false  // 是否自動提交
        );
    }
    
    private SqlSession openSessionFromDataSource(ExecutorType execType, 
                                                 TransactionIsolationLevel level, 
                                                 boolean autoCommit) {
        Transaction tx = null;
        try {
            // 1. 獲取環(huán)境配置
            final Environment environment = configuration.getEnvironment();
            
            // 2. 創(chuàng)建事務(wù)工廠
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            
            // 3. 創(chuàng)建事務(wù)
            tx = transactionFactory.newTransaction(
                environment.getDataSource(), level, autoCommit
            );
            
            // 4. 創(chuàng)建執(zhí)行器(關(guān)鍵!)
            final Executor executor = configuration.newExecutor(tx, execType);
            
            // 5. 創(chuàng)建SqlSession
            return new DefaultSqlSession(configuration, executor, autoCommit);
            
        } catch (Exception e) {
            // 關(guān)閉事務(wù)
            closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session. ", e);
        }
    }
}

代碼清單1:SqlSession創(chuàng)建源碼(MyBatis 3.5+)

關(guān)鍵點:SqlSession不是簡單的Connection包裝,它包含:

  1. 配置信息(Configuration)

  2. 執(zhí)行器(Executor)

  3. 事務(wù)控制

2.2 執(zhí)行器類型:選對模式很重要

MyBatis有三種執(zhí)行器,性能差異很大:

public enum ExecutorType {
    SIMPLE,      // 簡單執(zhí)行器
    REUSE,       // 重用執(zhí)行器
    BATCH        // 批量執(zhí)行器
}

性能測試數(shù)據(jù)(插入1000條記錄):

執(zhí)行器類型

耗時(ms)

內(nèi)存占用

適用場景

SIMPLE

1250

45MB

通用查詢

REUSE

980

42MB

頻繁相同SQL

BATCH

350

50MB

批量插入

測試環(huán)境:MySQL 8.0,1000條記錄插入

代碼示例

// 使用批量執(zhí)行器
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    
    for (int i = 0; i < 1000; i++) {
        User user = new User("user" + i, "email" + i + "@test.com");
        mapper.insert(user);
    }
    
    session.commit();  // 一次性提交
}

3. Mapper接口綁定:動態(tài)代理的魔法

3.1 從接口到SQL:到底發(fā)生了什么?

這是MyBatis最神奇的地方。你寫個接口,MyBatis就能執(zhí)行SQL:

public interface UserMapper {
    @Select("SELECT * FROM users WHERE id = #{id}")
    User findById(Long id);
}

// 使用
User user = userMapper.findById(1L);  // 這里發(fā)生了什么?

答案:動態(tài)代理 + 方法簽名解析??丛创a:

// MapperProxy是核心
public class MapperProxy<T> implements InvocationHandler, Serializable {
    
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 1. 如果是Object的方法,直接調(diào)用
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }
            
            // 2. 默認方法(Java 8+)
            if (method.isDefault()) {
                return invokeDefaultMethod(proxy, method, args);
            }
            
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        
        // 3. 緩存中獲取MapperMethod
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        
        // 4. 執(zhí)行SQL
        return mapperMethod.execute(sqlSession, args);
    }
    
    private MapperMethod cachedMapperMethod(Method method) {
        return methodCache.computeIfAbsent(method, k -> 
            new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())
        );
    }
}

代碼清單2:MapperProxy核心源碼

3.2 MapperMethod:方法執(zhí)行的指揮官

MapperMethod是關(guān)鍵橋梁,它把Java方法調(diào)用翻譯成SQL執(zhí)行:

public class MapperMethod {
    
    private final SqlCommand command;  // SQL命令信息
    private final MethodSignature method;  // 方法簽名
    
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        
        switch (command.getType()) {
            case INSERT: {
                // 轉(zhuǎn)換參數(shù)
                Object param = method.convertArgsToSqlCommandParam(args);
                // 執(zhí)行插入
                result = rowCountResult(
                    sqlSession.insert(command.getName(), param)
                );
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(
                    sqlSession.update(command.getName(), param)
                );
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(
                    sqlSession.delete(command.getName(), param)
                );
                break;
            }
            case SELECT:
                // 查詢邏輯更復雜
                if (method.returnsVoid() && method.hasResultHandler()) {
                    // 有ResultHandler的情況
                    executeWithResultHandler(sqlSession, args);
                    result = null;
                } else if (method.returnsMany()) {
                    // 返回集合
                    result = executeForMany(sqlSession, args);
                } else if (method.returnsMap()) {
                    // 返回Map
                    result = executeForMap(sqlSession, args);
                } else if (method.returnsCursor()) {
                    // 返回游標
                    result = executeForCursor(sqlSession, args);
                } else {
                    // 返回單個對象
                    Object param = method.convertArgsToSqlCommandParam(args);
                    result = sqlSession.selectOne(command.getName(), param);
                }
                break;
            case FLUSH:
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }
        
        return result;
    }
    
    // 處理返回集合的情況
    private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
        List<E> result;
        Object param = method.convertArgsToSqlCommandParam(args);
        
        if (method.hasRowBounds()) {
            RowBounds rowBounds = method.extractRowBounds(args);
            result = sqlSession.selectList(command.getName(), param, rowBounds);
        } else {
            result = sqlSession.selectList(command.getName(), param);
        }
        
        // 處理返回數(shù)組的情況
        if (!method.getReturnType().isAssignableFrom(result.getClass())) {
            if (method.getReturnType().isArray()) {
                return convertToArray(result);
            } else {
                return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
            }
        }
        
        return result;
    }
}

代碼清單3:MapperMethod執(zhí)行邏輯

4. 配置文件加載:MyBatis的"啟動過程"

4.1 配置文件的完整加載流程

很多人以為MyBatis配置就是讀個XML文件,太天真了!看看這流程:

圖2:MyBatis配置文件加載流程

4.2 Configuration:MyBatis的"大腦"

Configuration對象是MyBatis的全局配置中心,它緩存了所有信息:

public class Configuration {
    // 環(huán)境信息
    protected Environment environment;
    
    // 注冊的Mapper
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    
    // 攔截器鏈
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    
    // 類型處理器注冊表
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
    
    // 類型別名注冊表
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    
    // 語言驅(qū)動注冊表
    protected final LanguageDriverRegistry languageDriverRegistry = new LanguageDriverRegistry();
    
    // 映射的SQL語句
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection");
    
    // 緩存
    protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
    
    // 結(jié)果映射
    protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
    
    // 參數(shù)映射
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
    
    // 加載的關(guān)鍵方法
    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }
    
    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }
    
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }
    
    public void addMappedStatement(MappedStatement ms) {
        mappedStatements.put(ms.getId(), ms);
    }
    
    public MappedStatement getMappedStatement(String id) {
        return getMappedStatement(id, true);
    }
}

代碼清單4:Configuration核心屬性

關(guān)鍵點Configuration是單例的,全局唯一。這也是為什么MyBatis的插件(Interceptor)是全局生效的。

4.3 Mapper注冊過程

Mapper接口是怎么注冊到MyBatis中的?看代碼:

public class MapperRegistry {
    private final Configuration config;
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            
            boolean loadCompleted = false;
            try {
                // 1. 添加到knownMappers
                knownMappers.put(type, new MapperProxyFactory<>(type));
                
                // 2. 解析Mapper注解
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
    
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // 獲取Mapper代理工廠
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        
        try {
            // 創(chuàng)建代理實例
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
}

代碼清單5:Mapper注冊過程

5. SQL執(zhí)行過程:從Java方法到數(shù)據(jù)庫

5.1 完整的SQL執(zhí)行鏈路

這是MyBatis最復雜的部分,看這張圖:

圖3:SQL執(zhí)行完整鏈路

5.2 Executor:SQL執(zhí)行的"指揮官"

Executor是執(zhí)行器的核心,它負責調(diào)度整個執(zhí)行過程:

public abstract class BaseExecutor implements Executor {
    
    // 一級緩存(本地緩存)
    protected PerpetualCache localCache;
    
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, 
                            RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        // 1. 獲取BoundSql(包含實際SQL和參數(shù))
        BoundSql boundSql = ms.getBoundSql(parameter);
        
        // 2. 創(chuàng)建緩存Key
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        
        // 3. 執(zhí)行查詢
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, 
                            RowBounds rowBounds, ResultHandler resultHandler,
                            CacheKey key, BoundSql boundSql) throws SQLException {
        
        // 檢查一級緩存
        List<E> list = (List<E>) localCache.getObject(key);
        if (list != null) {
            // 緩存命中
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 緩存未命中,從數(shù)據(jù)庫查詢
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
        
        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);
        
        return list;
    }
}

代碼清單6:BaseExecutor查詢邏輯

5.3 StatementHandler:真正的SQL執(zhí)行者

StatementHandler負責創(chuàng)建和執(zhí)行Statement:

public class PreparedStatementHandler implements StatementHandler {
    
    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        
        // 執(zhí)行查詢
        ps.execute();
        
        // 處理結(jié)果集
        return resultSetHandler.handleResultSets(ps);
    }
    
    @Override
    public void parameterize(Statement statement) throws SQLException {
        // 設(shè)置參數(shù)
        parameterHandler.setParameters((PreparedStatement) statement);
    }
    
    @Override
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
        // 準備Statement
        Statement statement = null;
        try {
            // 創(chuàng)建PreparedStatement
            statement = instantiateStatement(connection);
            
            // 設(shè)置超時
            setStatementTimeout(statement, transactionTimeout);
            
            // 設(shè)置FetchSize
            setFetchSize(statement);
            
            return statement;
        } catch (SQLException e) {
            closeStatement(statement);
            throw e;
        }
    }
    
    protected Statement instantiateStatement(Connection connection) throws SQLException {
        String sql = boundSql.getSql();
        
        // 處理返回主鍵的情況
        if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
            String[] keyColumnNames = mappedStatement.getKeyColumns();
            if (keyColumnNames == null) {
                return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
            } else {
                return connection.prepareStatement(sql, keyColumnNames);
            }
        } else if (mappedStatement.getResultSetType() != null) {
            // 設(shè)置ResultSet類型
            return connection.prepareStatement(sql,
                mappedStatement.getResultSetType().getValue(),
                ResultSet.CONCUR_READ_ONLY);
        } else {
            // 普通的PreparedStatement
            return connection.prepareStatement(sql);
        }
    }
}

代碼清單7:StatementHandler執(zhí)行SQL

6. 緩存機制:性能提升的關(guān)鍵

6.1 一級緩存 vs 二級緩存

很多人分不清MyBatis的兩種緩存,這很危險!

緩存類型

作用范圍

生命周期

默認開啟

注意點

一級緩存

SqlSession級別

會話期間

同一個SqlSession有效

二級緩存

Mapper級別

應(yīng)用級別

需要手動開啟

6.2 一級緩存的坑

我踩過最深的坑就是一級緩存??催@個例子:

// 錯誤示例
try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    
    // 第一次查詢
    User user1 = mapper.findById(1L);  // 查數(shù)據(jù)庫
    System.out.println(user1);
    
    // 第二次查詢
    User user2 = mapper.findById(1L);  // 從一級緩存獲取
    System.out.println(user2);  // 和user1是同一個對象!
    
    // 修改user1
    user1.setName("修改后");
    
    // 第三次查詢
    User user3 = mapper.findById(1L);  // 還是從緩存獲取
    System.out.println(user3.getName());  // 輸出"修改后"!數(shù)據(jù)不一致!
}

問題:一級緩存導致數(shù)據(jù)不一致,而且同一個對象被修改會影響緩存。

解決方案

// 方案1:清除緩存
session.clearCache();

// 方案2:創(chuàng)建新的SqlSession
// 方案3:關(guān)閉一級緩存(不推薦)

6.3 二級緩存的正確使用

二級緩存更復雜,但用好了性能提升明顯:

<!-- 開啟二級緩存 -->
<mapper namespace="com.example.UserMapper">
    <cache
        eviction="LRU"
        flushInterval="60000"
        size="512"
        readOnly="true"/>
    
    <select id="findById" resultType="User" useCache="true">
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>

性能測試(查詢同一數(shù)據(jù)1000次):

緩存類型

平均耗時(ms)

數(shù)據(jù)庫查詢次數(shù)

內(nèi)存占用

無緩存

1250

1000

一級緩存

45

1

二級緩存

15

1

注意:二級緩存是跨SqlSession的,要確保緩存的實體類實現(xiàn)Serializable。

7. 插件機制:MyBatis的"鉤子"

7.1 插件原理:責任鏈模式

MyBatis插件基于責任鏈模式實現(xiàn),可以攔截4大對象:

  1. Executor

  2. StatementHandler

  3. ParameterHandler

  4. ResultSetHandler

@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class SlowSqlInterceptor implements Interceptor {
    
    private static final Logger log = LoggerFactory.getLogger(SlowSqlInterceptor.class);
    private long slowSqlThreshold = 1000;  // 1秒
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        try {
            // 繼續(xù)執(zhí)行
            return invocation.proceed();
        } finally {
            long costTime = System.currentTimeMillis() - startTime;
            
            if (costTime > slowSqlThreshold) {
                StatementHandler handler = (StatementHandler) invocation.getTarget();
                BoundSql boundSql = handler.getBoundSql();
                
                log.warn("慢SQL檢測: {}, 耗時: {}ms, 參數(shù): {}", 
                    boundSql.getSql(), costTime, boundSql.getParameterObject());
                
                // 可以發(fā)送告警
                AlertManager.sendSlowSqlAlert(boundSql.getSql(), costTime);
            }
        }
    }
    
    @Override
    public Object plugin(Object target) {
        // 創(chuàng)建代理
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 讀取配置
        String threshold = properties.getProperty("slowSqlThreshold");
        if (threshold != null) {
            this.slowSqlThreshold = Long.parseLong(threshold);
        }
    }
}

代碼清單8:慢SQL監(jiān)控插件

7.2 插件的執(zhí)行順序

插件是按配置順序執(zhí)行的,這很重要:

<plugins>
    <!-- 先執(zhí)行插件1 -->
    <plugin interceptor="com.example.plugin1">
        <property name="property1" value="value1"/>
    </plugin>
    
    <!-- 再執(zhí)行插件2 -->
    <plugin interceptor="com.example.plugin2">
        <property name="property2" value="value2"/>
    </plugin>
</plugins>

執(zhí)行順序:

原始對象
    ↓
插件2代理
    ↓  
插件1代理
    ↓
目標方法

8. 類型處理器:Java與數(shù)據(jù)庫的橋梁

8.1 內(nèi)置類型處理器

MyBatis內(nèi)置了豐富的類型處理器,但很多人不知道:

Java類型

JDBC類型

處理器

說明

String

VARCHAR

StringTypeHandler

字符串

Integer

INTEGER

IntegerTypeHandler

整數(shù)

Date

TIMESTAMP

DateTypeHandler

日期

Enum

VARCHAR

EnumTypeHandler

枚舉(存名字)

Enum

INTEGER

EnumOrdinalTypeHandler

枚舉(存序號)

boolean

BOOLEAN

BooleanTypeHandler

布爾

8.2 自定義類型處理器

我做過一個需求:把JSON字符串存到數(shù)據(jù)庫。用自定義類型處理器很方便:

// 1. 定義處理器
@MappedTypes(MyData.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<MyData> {
    
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                   MyData parameter, JdbcType jdbcType) throws SQLException {
        try {
            String json = objectMapper.writeValueAsString(parameter);
            ps.setString(i, json);
        } catch (JsonProcessingException e) {
            throw new SQLException("對象轉(zhuǎn)JSON失敗", e);
        }
    }
    
    @Override
    public MyData getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        return parseJson(json);
    }
    
    @Override
    public MyData getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String json = rs.getString(columnIndex);
        return parseJson(json);
    }
    
    @Override
    public MyData getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String json = cs.getString(columnIndex);
        return parseJson(json);
    }
    
    private MyData parseJson(String json) {
        if (json == null || json.isEmpty()) {
            return null;
        }
        
        try {
            return objectMapper.readValue(json, MyData.class);
        } catch (IOException e) {
            throw new RuntimeException("JSON解析失敗: " + json, e);
        }
    }
}

// 2. 注冊處理器
@MappedTypes(MyData.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<MyData> {
    // 實現(xiàn)...
}

// 3. 在配置中注冊
<typeHandlers>
    <typeHandler handler="com.example.JsonTypeHandler"/>
</typeHandlers>

代碼清單9:自定義JSON類型處理器

9. 實戰(zhàn):性能優(yōu)化與問題排查

9.1 性能優(yōu)化技巧

技巧1:合理使用批量操作

// 錯誤:每次insert都提交
for (User user : userList) {
    userMapper.insert(user);  // 每次都有commit
}

// 正確:批量提交
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    
    for (User user : userList) {
        mapper.insert(user);
    }
    
    session.commit();  // 一次性提交
}

性能對比:插入1000條記錄

  • 逐條提交:1250ms

  • 批量提交:350ms

  • 提升:72%

技巧2:使用分頁查詢

// 錯誤:一次性查詢所有
List<User> users = userMapper.findAll();  // 如果數(shù)據(jù)量大,內(nèi)存爆炸

// 正確:分頁查詢
PageHelper.startPage(1, 100);  // 第1頁,每頁100條
List<User> users = userMapper.findByCondition(condition);

技巧3:合理使用緩存

<!-- 只緩存不經(jīng)常變的配置數(shù)據(jù) -->
<select id="findConfig" resultType="Config" useCache="true" flushCache="false">
    SELECT * FROM config WHERE type = #{type}
</select>

<!-- 經(jīng)常變的數(shù)據(jù)不要緩存 -->
<select id="findLatestOrders" resultType="Order" useCache="false">
    SELECT * FROM orders ORDER BY create_time DESC LIMIT 100
</select>

9.2 常見問題排查

問題1:N+1查詢問題

現(xiàn)象:查詢列表,然后循環(huán)查詢詳情,產(chǎn)生大量SQL。

解決方案

<!-- 使用關(guān)聯(lián)查詢 -->
<select id="findUserWithOrders" resultMap="userWithOrders">
    SELECT u.*, o.* 
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = #{id}
</select>

<resultMap id="userWithOrders" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <collection property="orders" ofType="Order">
        <id property="id" column="order_id"/>
        <result property="amount" column="amount"/>
    </collection>
</resultMap>

問題2:參數(shù)綁定錯誤

現(xiàn)象#{}${}用錯。

區(qū)別

  • #{}:預(yù)編譯,防SQL注入

  • ${}:字符串替換,不安全

-- 安全
WHERE name = #{name}  -- 變成 WHERE name = ?

-- 不安全,但有特殊用途
ORDER BY ${orderBy}  -- 動態(tài)排序字段

問題3:延遲加載問題

現(xiàn)象:關(guān)閉Session后訪問懶加載屬性報錯。

解決方案

<!-- 開啟后需要管理Session生命周期 -->
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

或者使用FetchType.EAGER:

public class User {
    @OneToMany(fetch = FetchType.EAGER)  // 立即加載
    private List<Order> orders;
}

10. 企業(yè)級最佳實踐

10.1 我的"MyBatis軍規(guī)"

經(jīng)過多年實踐,我總結(jié)了一套MyBatis最佳實踐:

第一條:明確SQL寫在XML還是注解

  • 簡單SQL用注解

  • 復雜SQL、動態(tài)SQL用XML

  • 不要混用

第二條:合理使用緩存

  • 配置類數(shù)據(jù)用二級緩存

  • 事務(wù)性數(shù)據(jù)不用緩存

  • 緩存要設(shè)置合理的超時時間

第三條:批量操作要用批量模式

  • 插入/更新大量數(shù)據(jù)用ExecutorType.BATCH

  • 記得手動提交事務(wù)

第四條:監(jiān)控SQL性能

  • 用插件監(jiān)控慢SQL

  • 定期分析執(zhí)行計劃

  • 設(shè)置合理的超時時間

第五條:代碼可讀性

  • SQL要格式化

  • 復雜的SQL要加注釋

  • 參數(shù)要有明確的命名

10.2 生產(chǎn)環(huán)境配置模板

<!-- mybatis-config.xml -->
<configuration>
    <!-- 設(shè)置 -->
    <settings>
        <!-- 開啟下劃線轉(zhuǎn)駝峰 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        
        <!-- 開啟二級緩存 -->
        <setting name="cacheEnabled" value="true"/>
        
        <!-- 查詢超時時間 -->
        <setting name="defaultStatementTimeout" value="30"/>
        
        <!-- 返回null時調(diào)用setter方法 -->
        <setting name="callSettersOnNulls" value="true"/>
        
        <!-- 日志實現(xiàn) -->
        <setting name="logImpl" value="SLF4J"/>
    </settings>
    
    <!-- 類型別名 -->
    <typeAliases>
        <package name="com.example.model"/>
    </typeAliases>
    
    <!-- 插件 -->
    <plugins>
        <!-- 分頁插件 -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <property name="reasonable" value="true"/>
        </plugin>
        
        <!-- 慢SQL監(jiān)控 -->
        <plugin interceptor="com.example.SlowSqlInterceptor">
            <property name="slowSqlThreshold" value="1000"/>
        </plugin>
    </plugins>
    
    <!-- 環(huán)境 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
                <!-- 連接池配置 -->
                <property name="poolMaximumActiveConnections" value="20"/>
                <property name="poolMaximumIdleConnections" value="10"/>
                <property name="poolMaximumCheckoutTime" value="20000"/>
            </dataSource>
        </environment>
    </environments>
    
    <!-- 映射文件 -->
    <mappers>
        <package name="com.example.mapper"/>
    </mappers>
</configuration>

11. 最后的話

MyBatis就像一把瑞士軍刀,功能多但要用對地方。用好了,它能讓你如虎添翼;用不好,它就是定時炸彈。

我見過太多團隊在MyBatis上栽跟頭:有的因為緩存問題導致數(shù)據(jù)不一致,有的因為SQL注入導致安全漏洞,有的因為N+1查詢拖垮數(shù)據(jù)庫。

記住:框架是工具,不是黑盒子。理解原理,掌握細節(jié),才能在關(guān)鍵時刻解決問題。

?? 推薦閱讀

官方文檔

  1. MyBatis官方文檔? - 最權(quán)威的參考

  2. MyBatis-Spring整合文檔? - Spring整合指南

源碼學習

  1. MyBatis GitHub倉庫? - 直接看源碼

  2. MyBatis Spring Boot Starter? - Spring Boot整合

實踐指南

  1. 阿里巴巴Java開發(fā)手冊? - MyBatis章節(jié)必看

  2. MyBatis最佳實踐? - 官方博客

性能優(yōu)化

  1. MySQL性能優(yōu)化? - 數(shù)據(jù)庫層面優(yōu)化

  2. Arthas診斷工具? - MyBatis性能診斷

最后建議:打開IDE,創(chuàng)建一個簡單的MyBatis項目,然后打斷點跟一遍源碼。從SqlSessionFactoryBuilder.build()開始,一步步看MyBatis是怎么啟動的,Mapper是怎么代理的,SQL是怎么執(zhí)行的。親手跟一遍,勝過看十篇文章。

到此這篇關(guān)于MyBatis核心源碼解析之從SqlSession到Mapper接口的綁定過程的文章就介紹到這了,更多相關(guān)MyBatis SqlSession到Mapper接口綁定內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • java程序員常見的sql錯誤

    java程序員常見的sql錯誤

    當Java程序員在SQL中要寫個查詢語句是很簡單的。但在Java里類似的語句卻不容易,因為程序員不僅要反復考慮編程范式,而且也要考慮算法的問題。下面我們來看看這幾個常見的錯誤吧
    2019-06-06
  • SpringBoot注冊Filter的兩種實現(xiàn)方式

    SpringBoot注冊Filter的兩種實現(xiàn)方式

    這篇文章主要介紹了SpringBoot注冊Filter的兩種實現(xiàn)方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-08-08
  • Java實現(xiàn)二叉查找樹的增刪查詳解

    Java實現(xiàn)二叉查找樹的增刪查詳解

    二叉查找樹(ADT)是一個具有對于樹種的某個節(jié)點X,它的左節(jié)點都比X小,它的右節(jié)點都比X大的二叉樹。本文將用Java實現(xiàn)二叉查找樹的增刪查,需要的可以參考一下
    2022-06-06
  • IDEA離線安裝maven helper插件的圖文教程

    IDEA離線安裝maven helper插件的圖文教程

    本文通過圖文并茂的形式給大家介紹IDEA離線安裝maven helper插件,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2021-08-08
  • Mybatis select記錄封裝的實現(xiàn)

    Mybatis select記錄封裝的實現(xiàn)

    這篇文章主要介紹了Mybatis select記錄封裝的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-10-10
  • 一文掌握Spring中循環(huán)依賴與三級緩存

    一文掌握Spring中循環(huán)依賴與三級緩存

    這篇文章主要介紹了Spring中循環(huán)依賴與三級緩存,Spring通過三級緩存解決了循環(huán)依賴,其中一級緩存為單例池,二級緩存為早期曝光對象earlySingletonObjects,三級緩存為早期曝光對象工廠(singletonFactories),本文結(jié)合實例代碼介紹的非常詳細,需要的朋友參考下吧
    2023-09-09
  • Java實現(xiàn)ftp上傳下載、刪除文件及在ftp服務(wù)器上傳文件夾的方法

    Java實現(xiàn)ftp上傳下載、刪除文件及在ftp服務(wù)器上傳文件夾的方法

    這篇文章主要介紹了Java實現(xiàn)ftp上傳下載、刪除文件及在ftp服務(wù)器上傳文件夾的方法,需要的朋友可以參考下
    2015-11-11
  • Spring?Boot對接Oracle數(shù)據(jù)庫具體流程

    Spring?Boot對接Oracle數(shù)據(jù)庫具體流程

    這篇文章主要給大家介紹了關(guān)于Spring?Boot對接Oracle數(shù)據(jù)庫的具體流程,本文將介紹如何在Spring Boot中連接Oracle數(shù)據(jù)庫的基本配置,包括添加依賴、配置數(shù)據(jù)源、配置JPA等,需要的朋友可以參考下
    2023-11-11
  • SpringBoot讀取配置的常用方式總結(jié)

    SpringBoot讀取配置的常用方式總結(jié)

    在SpringBoot應(yīng)用開發(fā)中,配置文件是不可或缺的一部分,它們幫助我們管理應(yīng)用的運行時參數(shù),使得應(yīng)用的部署和維護變得更加靈活,本文將介紹六種常用的SpringBoot讀取配置方式,需要的朋友跟著小編一起來看看吧
    2024-07-07
  • SpringBoot算法實現(xiàn)數(shù)據(jù)加密傳輸

    SpringBoot算法實現(xiàn)數(shù)據(jù)加密傳輸

    這篇文章主要為大家詳細介紹了SpringBoot算法實現(xiàn)數(shù)據(jù)加密傳輸?shù)南嚓P(guān)知識,本文主要是混合加密,前端 SM2 + SM4,后端 Spring Boot + Hutool 解密,希望對大家有所幫助
    2026-03-03

最新評論

西乌珠穆沁旗| 堆龙德庆县| 田林县| 富民县| 湛江市| 新竹县| 盘锦市| 阿尔山市| 临桂县| 临夏市| 外汇| 荣成市| 牙克石市| 黄大仙区| 莎车县| 兰溪市| 镇宁| 惠来县| 阳高县| 绍兴市| 久治县| 天津市| 桂林市| 洪洞县| 儋州市| 朔州市| 长白| 北海市| 东光县| 福海县| 福州市| 象山县| 江津市| 湖口县| 喜德县| 赤峰市| 长沙市| 荔波县| 怀来县| 南川市| 元谋县|