MyBatis Interceptor 深度解析與應用實踐

MyBatis Interceptor 深度解析與應用實踐

一、MyBatis Interceptor概述

1.1 什么是MyBatis Interceptor

MyBatis Interceptor,也稱為MyBatis 插件,是 MyBatis 提供的一種擴展機制,用于在 MyBatis 執行 SQL 的過程中插入自定義邏輯。它類似于 AOP 中的切面或攔截器概念,可在不修改核心 MyBatis 源碼的情況下,對核心對象(如執行器、語句處理器等)的方法調用進行攔截和增強。開發者通過實現 org.apache.ibatis.plugin.Interceptor 接口,并在類上使用 @Intercepts 注解指定需要攔截的方法簽名,MyBatis 會在運行時為目標對象動態生成代理,在對應方法執行前后調用攔截器邏輯,實現諸如日志審計性能監控SQL 改寫等功能。

簡而言之,MyBatis Interceptor 是 MyBatis 提供的插件式攔截機制,允許開發者“插入”代碼到 MyBatis 核心流程中,從而實現對 SQL 執行流程的監控、修改或增強。典型的攔截目標包括 SQL 執行前后的操作、參數綁定、結果處理等環節。與 Spring AOP 不同,MyBatis Interceptor 專注于 MyBatis 的內部執行過程,可看作 MyBatis 專屬的“攔截器”。

1.2 MyBatis Interceptor的核心價值

MyBatis 攔截器的核心價值在于 靈活擴展和解耦關注點。通過攔截器,開發者可以將一些常見的橫切關注點邏輯(如日志記錄、性能統計、安全校驗、動態 SQL 構建等)模塊化,不侵入業務代碼地整合到 MyBatis 層。這帶來以下好處:

  • 增強擴展能力:無需修改 MyBatis 核心源碼,即可在查詢、更新等關鍵時刻插入自定義邏輯。例如,可以記錄每條 SQL 的執行時間、自動為分頁添加限制、對敏感數據進行脫敏等。

  • 關注點分離:將通用功能(如日志、監控、安全)從業務代碼中剝離,使業務邏輯更清晰,同時方便統一維護和測試。

  • 復用性:一個攔截器插件可以在不同項目中復用,只需根據需要注冊即可,提升開發效率。

  • 低侵入:與直接在業務層或 DAO 層寫重復代碼相比,攔截器只需配置一次即可全局生效,降低維護成本。

通過這些優勢,MyBatis Interceptor 成為提升項目可維護性、可擴展性的重要工具,尤其適用于需要統一管控 SQL 行為的場景(如審計、安全、性能調優等)。

1.3 MyBatis Interceptor與其他框架的對比

MyBatis 攔截器與其他常見技術的比較主要體現在其攔截范圍和方式上:

  • 與 Spring AOP 對比:Spring AOP 主要針對普通的 Java Bean 方法調用進行橫切(如服務層或 DAO 層方法)。而 MyBatis 攔截器專注于MyBatis 的內部執行流程,攔截的是 MyBatis 核心接口的方法調用(如 Executor.updateStatementHandler.prepare 等)。Spring AOP 通常需要在 Bean 方法上織入代理,而 MyBatis 插件直接對 MyBatis 框架級別的組件做代理,關注點更底層。二者可以結合使用,但職責不同。

  • 與 Hibernate Interceptor 對比:Hibernate 提供的 Interceptor 接口側重于實體對象的生命周期(如保存、刪除事件等),而 MyBatis 插件攔截的是 SQL 執行流程本身。Hibernate 攔截器更關注 ORM 映射層面,MyBatis 插件則對 SQL 和參數處理過程進行干預。

  • 與 Servlet 過濾器對比:Servlet 過濾器作用于 Web 請求流程,攔截 HTTP 請求和響應;MyBatis 攔截器作用于數據庫層,攔截 SQL 語句的準備和執行。盡管都是責任鏈模式的應用,但層級不同,一個是應用級別,一個是持久層級別。

綜上,MyBatis Interceptor 屬于 MyBatis 專用的插件化機制,定位于數據訪問層的 SQL 執行流程管控,與其他通用 AOP 框架并不沖突,而是可補充和專注于 ORM 層面的需求。

二、MyBatis Interceptor原理詳解

2.1 攔截器的工作機制

MyBatis 攔截器的工作機制基于 Java 動態代理 實現,主要涉及以下幾個部分:

  • Interceptor 接口:所有自定義攔截器需實現 org.apache.ibatis.plugin.Interceptor 接口,其中定義了 intercept(Invocation)plugin(Object)setProperties(Properties) 三個方法。業務邏輯主要寫在 intercept 方法中,plugin 用于包裝目標對象,setProperties 用于接收配置屬性。

  • @Intercepts 注解:自定義攔截器類上必須標注 @Intercepts({@Signature(...)}) 注解,用以聲明攔截哪些類型(Executor、StatementHandler 等)及其對應的方法簽名。MyBatis 在加載攔截器時會讀取注解信息,構建一個 signatureMap(映射要攔截的類到方法集合)。

  • InterceptorChain:配置解析完成后,MyBatis 將所有自定義的攔截器對象注冊到 org.apache.ibatis.plugin.InterceptorChain 中,該鏈表按照配置順序保存攔截器實例。

  • 目標對象動態代理:當 MyBatis 創建核心組件(如 Executor、StatementHandler、ParameterHandler、ResultSetHandler)時,會調用 InterceptorChain.pluginAll(target) 方法。該方法遍歷攔截器鏈,對目標對象逐一執行 interceptor.plugin(target),即對目標對象進行動態代理包裝。實際上,plugin(target) 默認實現為 Plugin.wrap(target, this),其中 Plugin 采用 JDK 代理創建一個代理對象,代理時會攔截實現了指定接口的方法調用。

  • Invocation 觸發:執行 SQL 操作時,實際調用會落到代理對象上。代理對象的 InvocationHandler 會判斷當前方法是否在前面 @Signature 指定的方法列表中,如果是則會構造一個 Invocation 對象并調用 interceptor.intercept(invocation);否則直接調用目標對象的原方法。Invocation 封裝了目標對象(target)、方法(method)和參數(args),并提供 proceed() 方法用于繼續調用下一個攔截器或最終的目標方法。

流程示例:在執行 Executor.update() 時,實際會調用動態代理對象的 invoke 方法,該方法檢測到 update 是攔截目標,就執行自定義攔截器的 intercept 邏輯。在自定義攔截器中可以在 Invocation.proceed() 前后添加自己的處理。最終,當所有攔截器鏈條執行完畢后,才真正調用底層的 Executor 完成數據庫操作。

2.2 攔截器的執行流程(結合源碼分析)

結合源碼,可總結 MyBatis 攔截器的執行流程如下:

  1. 加載注冊攔截器:在解析 MyBatis 配置文件時,識別 <plugins> 節點并實例化配置的攔截器類(自動調用無參構造),然后調用 Configuration.addInterceptor(interceptor) 將其加入 InterceptorChain(此時 addInterceptor 內部其實是調用 InterceptorChain.addInterceptor,見源碼[20])。

  2. 創建核心對象并包裝:當 MyBatis 初始化時創建 ExecutorStatementHandlerParameterHandlerResultSetHandler 等對象時,會調用 configuration.getInterceptorChain().pluginAll(target)。例如在 DefaultSqlSession 中創建 Executor 后即包裝之(偽代碼示例):

    Executor executor = new SimpleExecutor(...);
    executor = (Executor) configuration.getInterceptorChain().pluginAll(executor);
    

    此時,InterceptorChain 會遍歷所有注冊的攔截器,對 executor 對象進行包裝,形成多層動態代理:

    target = interceptor1.plugin(target);
    target = interceptor2.plugin(target);
    // ...
    

    具體源碼中 pluginAll 方法顯示:

    public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;
    }
    
  3. 動態代理攔截Interceptor.plugin(Object target) 的默認實現是 Plugin.wrap(target, this)(見)。Plugin.wrap 方法首先通過 getSignatureMap(interceptor) 讀取攔截器類上的 @Intercepts 注解信息,得到一個映射關系,指明要攔截的類型及方法集合。然后,判斷目標對象是否實現了這些接口中的任意一個:如果有匹配,則通過 Proxy.newProxyInstance 創建代理對象;否則直接返回原對象。

  4. 代理對象的 invoke 邏輯(見源碼):當代理對象的方法被調用時,會進入 Plugin.invoke 方法。該方法從 signatureMap 中取出本次調用的目標方法所在接口的 Method 集合,如果調用的方法在其中,則執行 interceptor.intercept(new Invocation(target, method, args));否則調用 method.invoke(target, args) 直接執行原方法。如下關鍵片段:

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 方法匹配,調用自定義攔截器邏輯return interceptor.intercept(new Invocation(target, method, args));}// 方法未配置攔截,直接調用目標方法return method.invoke(target, args);
    }
    

  5. 執行自定義邏輯并繼續調用:進入 interceptor.intercept(Invocation) 后,即執行用戶在攔截器中實現的邏輯。Invocation 對象封裝了 targetmethodargs,可通過調用 invocation.proceed() 來繼續調用下一個攔截器鏈條或目標方法。在 intercept 方法內部,開發者可以在 invocation.proceed() 前后添加自己的業務邏輯,例如記錄日志、修改參數、性能計時等。Invocation.proceed() 最終會調用原始對象的方法(或下一個攔截器),完成真實的數據庫操作。

以上流程說明了 MyBatis 插件機制的核心原理:通過動態代理將多個攔截器按配置順序“串聯”在目標對象上,調用方法時逐個進入攔截器的 intercept 方法,最后執行目標方法。這樣就形成了一條攔截器鏈(責任鏈模式)的執行路徑,實現了對 MyBatis SQL 執行過程的靈活增強。

2.3 攔截器的生命周期管理

MyBatis 攔截器的生命周期主要由 MyBatis 框架自行管理,特征如下:

  • 單例模式:在配置加載階段,MyBatis 通過反射創建攔截器對象(通常是單例的,可以在 MyBatis 配置或 Spring 容器中配置)。一個 SqlSessionFactory 生成期間,配置的每個攔截器類只會被實例化一次,保存到 InterceptorChain 中。之后執行的所有 SQL 操作都共享這些攔截器實例,因此要求攔截器實現是線程安全的(參見 5.2 小節)。

  • 配置屬性注入:對于 <plugin> 配置中的 <property> 屬性,MyBatis 在創建攔截器實例后會調用 setProperties(Properties) 方法,將配置的屬性傳入攔截器,以便初始化參數或外部配置。可以利用這一機制動態改變攔截器行為。

  • 與 SqlSessionFactory 關聯:攔截器實例存儲在 Configuration 對象中,Configuration 決定了 SqlSessionFactory 的行為。只有在配置了特定攔截器時,才會加入攔截鏈。通常情況下,攔截器生命周期等同于整個 SqlSessionFactory 生命周期。

  • 支持熱重載(可選):在開發環境或動態運行時,部分項目可能通過自定義手段實現攔截器的熱插拔,例如重新加載配置或用 Spring 上下文刷新插件。MyBatis 本身并不內置攔截器的熱重載機制,但由于可編程性強,可以結合 Spring 等框架提供類似功能。

總體上,MyBatis 攔截器由框架自動創建、配置與調用,開發者無需手動管理其創建和銷毀。需要注意的是,由于其單例且跨線程調用的特性,在設計攔截器時應避免使用非線程安全的可變共享狀態。后續5.2節將詳細討論攔截器的線程安全及狀態管理策略。

三、自定義攔截器開發指南

開發自定義 MyBatis 攔截器需要關注注解配置、核心方法實現、注冊方式和測試等方面。以下將逐步介紹具體步驟和注意事項。

3.1 攔截器注解的使用規范

自定義攔截器類必須使用 @Intercepts 注解來聲明要攔截的目標接口和方法。常見使用示例如下:

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExampleInterceptor implements Interceptor {// ...
}

注解說明:

  • @Intercepts:用于標記該類是 MyBatis 攔截器,包含多個 @Signature 條目。

  • @Signature 屬性:

    • type:要攔截的接口類型(常見有 ExecutorStatementHandlerParameterHandlerResultSetHandler 等)。

    • method:接口中的方法名(字符串)。

    • args:方法參數類型數組,對應該方法的參數列表。類型必須準確,否則會找不到對應方法。

多組 @Signature 表明一個攔截器可以攔截多個目標方法。使用時需確保簽名對應的方法在指定接口中存在。例如 Executor.update(MappedStatement, Object) 表示攔截 Executor 的 update 方法。MyBatis 在構建 signatureMap 時會通過反射獲取指定類型的 Method 對象(見源碼mybatis.org),找不到會報錯。

常用攔截類型與方法

  • Executor(執行器):update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 等。

  • StatementHandler(語句處理器):prepare, parameterize, batch, update, query 等。

  • ParameterHandler(參數處理器):getParameterObject, setParameters

  • ResultSetHandler(結果集處理器):handleResultSets, handleOutputParameters

可根據需要選擇合適的類型和方法。以下示例展示了注解聲明及攔截器類定義的典型結構:

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlLogInterceptor implements Interceptor {// 實現攔截邏輯
}

3.2 攔截器核心方法的實現要點

自定義攔截器需要實現 Interceptor 接口,其核心方法包括:

  1. intercept(Invocation invocation):攔截邏輯的入口。Invocation 對象封裝了當前攔截的目標對象(target)、方法(method)和參數(args)。開發者應在此方法中編寫自定義處理邏輯,然后通過 invocation.proceed() 繼續調用下一個攔截器或目標方法。示例實現:

    @Override
    public Object intercept(Invocation invocation) throws Throwable {// 前置處理邏輯(例如日志、參數檢查等)System.out.println("==> 執行 SQL 之前");// 執行下一個攔截器或實際方法Object result = invocation.proceed();// 后置處理邏輯(例如結果處理、性能統計等)System.out.println("==> 執行 SQL 之后");return result;
    }
    

    關鍵點:

    • 調用 proceed():必須調用 invocation.proceed() 以繼續執行鏈條,否則會阻斷 SQL 的執行。可以在 proceed() 前后分別實現前置和后置邏輯。

    • 返回值:通常將 invocation.proceed() 的返回值原樣返回,或在其基礎上進行修改(例如對查詢結果進行包裝、過濾等)。

    • 異常處理intercept 方法簽名允許拋出 Throwable,出現異常時可捕獲并根據需要處理或包裝后拋出。異常會傳遞給調用者,并可在 MyBatis 層進行統一處理。

  2. plugin(Object target):用于將攔截器應用于目標對象。通常實現只需調用 Plugin.wrap(target, this)。該方法會根據前述簽名決定是否生成代理對象。示例:

    @Override
    public Object plugin(Object target) {// 將攔截器與目標對象通過動態代理綁定return Plugin.wrap(target, this);
    }
    

    一般無需修改此邏輯,但在特殊場景下可以增加對代理創建過程的自定義控制。Plugin.wrap 方法內部會檢查目標對象是否實現了簽名中指定的接口,若符合條件則生成代理,否則返回原對象。

  3. setProperties(Properties properties):接收 <plugin> 配置中的屬性,用于配置攔截器。例如可以通過 properties.getProperty("key") 獲取指定屬性值并初始化攔截器。示例:

    private Properties properties;@Override
    public void setProperties(Properties properties) {this.properties = properties;// 可讀取屬性進行初始化String logLevel = properties.getProperty("logLevel", "INFO");System.out.println("設置攔截器屬性:logLevel=" + logLevel);
    }
    

    在 MyBatis 配置文件中使用時,可為每個 <plugin> 標簽添加 <property name="..." value="..."/> 來傳遞參數。

3.3 攔截器配置與注冊方法

自定義攔截器需在 MyBatis 配置中注冊,常見方式有兩種:

  • MyBatis XML 配置(適用于無 Spring 場景):在 mybatis-config.xml<plugins> 節點中添加 <plugin> 配置。例如:

    <plugins><plugin interceptor="com.example.MyInterceptor"><property name="threshold" value="1000"/><property name="logSql" value="true"/></plugin>
    </plugins>
    

    上例將 com.example.MyInterceptor 添加到攔截鏈,同時傳入兩個配置屬性(可以在 setProperties 方法中讀取)。注意 interceptor 屬性填寫攔截器類的完整類名。

  • Spring Boot 或 Spring 集成:在 Spring 環境中可以通過 Java 配置或 Bean 注冊攔截器。常用方式包括:

    1. SqlSessionFactoryBean/SqlSessionTemplate 設置:在 Spring 配置類中,獲取 SqlSessionFactoryBeanSqlSessionFactory,然后調用 setPlugins 方法注入攔截器數組。例如:

      @Configuration
      public class MyBatisConfig {@Beanpublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();factoryBean.setDataSource(dataSource);factoryBean.setPlugins(new Interceptor[]{ new MyInterceptor() });return factoryBean.getObject();}
      }
      

      這樣 MyInterceptor 將被注冊為 MyBatis 的插件。

    2. MyBatis-Spring-Boot-Starter 方式(自動掃描 Bean):若使用 MyBatis Spring Boot Starter,可簡單地將攔截器聲明為 Spring Bean,例如在配置類中:

      @Bean
      public MyInterceptor myInterceptor() {MyInterceptor interceptor = new MyInterceptor();Properties props = new Properties();props.setProperty("threshold", "1000");interceptor.setProperties(props);return interceptor;
      }
      

      MyBatis-Spring 將自動將所有實現了 Interceptor 接口的 Bean 加入攔截器鏈。部分新版 Starter 也支持通過 application.yml 指定插件,但更常見的還是通過 Bean 注入完成。

攔截器調用鏈測試用例:為了驗證攔截器的功能,通常會編寫單元測試。例如,以下示例測試創建一個簡單目標接口,并在調用時確保攔截器被觸發:

public interface Foo {void bar();
}
public class FooImpl implements Foo {@Overridepublic void bar() {System.out.println("原始業務方法 bar() 執行");}
}
@Intercepts({@Signature(type = Foo.class, method = "bar", args = {})
})
public class FooInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("==> FooInterceptor:方法調用之前");Object result = invocation.proceed();System.out.println("==> FooInterceptor:方法調用之后");return result;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
// JUnit 測試代碼
public class InterceptorTest {@Testpublic void testFooInterceptor() {Foo target = new FooImpl();Foo proxy = (Foo) Plugin.wrap(target, new FooInterceptor());proxy.bar();// 預期輸出:// ==> FooInterceptor:方法調用之前// 原始業務方法 bar() 執行// ==> FooInterceptor:方法調用之后}
}

上述測試中,FooImpl.bar() 的調用被 FooInterceptor 成功攔截,從而在方法前后輸出了額外日志,驗證了攔截器調用鏈的正確性。

四、MyBatis Interceptor應用場景

MyBatis 攔截器在實際項目中有多種應用場景,以下介紹常見幾類,并提供示例代碼。

4.1 SQL分頁插件開發

場景需求:在查詢時自動為 SQL 添加分頁參數,無需在 XML 或注解中手動拼接分頁條件。常見做法是攔截語句處理器,在 StatementHandler.prepare() 階段修改即將執行的 SQL。

實現思路:攔截 StatementHandler.prepare(Connection, Integer) 方法,獲取原始 SQL 并在其后添加分頁語句(以 MySQL 為例為 LIMIT offset, size)。可以通過反射修改 BoundSql 中的 SQL 字符串。示例:

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PaginationInterceptor implements Interceptor {private static final String MYSQL = "mysql";// 從配置或上下文中獲取當前分頁參數private int offset;private int limit;@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler handler = (StatementHandler) invocation.getTarget();// 使用反射獲取實際的RoutingStatementHandler和BoundSqlBoundSql boundSql = handler.getBoundSql();String originalSql = boundSql.getSql().trim();// 根據數據庫方言構建分頁 SQLString pageSql = originalSql + " LIMIT " + offset + ", " + limit;// 通過 MetaObject 修改原 BoundSql 的 SQLMetaObject metaStatementHandler = SystemMetaObject.forObject(handler);metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);System.out.println("分頁攔截器修改SQL: " + pageSql);return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {this.offset = Integer.parseInt(properties.getProperty("offset", "0"));this.limit = Integer.parseInt(properties.getProperty("limit", "10"));}
}

注冊配置示例(mybatis-config.xml)

<plugins><plugin interceptor="com.example.PaginationInterceptor"><property name="offset" value="0"/><property name="limit" value="20"/></plugin>
</plugins>

上述攔截器在每次 prepare 時自動為 SQL 追加 LIMIT 分頁語句,實現了簡易的服務器端分頁功能。實際使用中還可結合 RowBounds 或其它分頁參數動態設置 offsetlimit

4.2 SQL日志記錄與審計

場景需求:記錄所有執行的 SQL 語句及其參數,方便調試和審計。可以在每次查詢或更新前后打印 SQL。

實現思路:攔截 StatementHandlerprepare 方法獲取 SQL,也可以攔截 Executor.query/update 方法獲取參數或計時。示例如下,在 prepare 階段日志輸出 SQL:

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlLogInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler handler = (StatementHandler) invocation.getTarget();BoundSql boundSql = handler.getBoundSql();String sql = boundSql.getSql().replaceAll("\\s+", " ");System.out.println("[SQL日志] 執行 SQL: " + sql);return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}

使用示例

<plugins><plugin interceptor="com.example.SqlLogInterceptor"/>
</plugins>

每次執行 SQL 時,上述攔截器都會打印標準化后的 SQL 語句到控制臺或日志文件。結合日志框架(如 Log4j、SLF4J)可將輸出記錄到文件或監控系統中。

4.3 性能監控與調優

場景需求:監控 SQL 執行時間、慢查詢告警或統計接口響應時間,以便性能優化。

實現思路:攔截執行語句的環節(如 StatementHandler.query/updateExecutor.query/update),在 intercept 方法中記錄開始時間和結束時間,計算耗時,超過閾值時打印警告。示例代碼:

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PerformanceInterceptor implements Interceptor {private long threshold = 500; // 毫秒@Overridepublic Object intercept(Invocation invocation) throws Throwable {long start = System.currentTimeMillis();Object result = invocation.proceed();long end = System.currentTimeMillis();long time = end - start;if (time > threshold) {MappedStatement ms = (MappedStatement) invocation.getArgs()[0];System.err.println("[性能警告] SQL 執行超時 " + time + " ms - " + ms.getId());}return result;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {this.threshold = Long.parseLong(properties.getProperty("threshold", "500"));}
}

注冊示例

<plugins><plugin interceptor="com.example.PerformanceInterceptor"><property name="threshold" value="300"/></plugin>
</plugins>

該攔截器對所有 Executor.updateExecutor.query 進行監控,記錄每次調用耗時。當耗時超過配置的閾值(如300毫秒)時,輸出警告信息。這樣可用于統計慢查詢,并在開發或生產環境中給予提示,幫助定位性能瓶頸。

4.4 數據脫敏與安全校驗

場景需求:在從數據庫查詢并返回結果前,對敏感數據進行脫敏處理(如隱去身份證號中間位、手機號后幾位),或對輸入參數做安全校驗。

實現思路:可以攔截 ResultSetHandler.handleResultSets(ResultSet) 方法,對返回的結果集進行遍歷和處理;也可以攔截 ParameterHandler.setParameters 方法,對 SQL 參數進行校驗或修改。下面演示對返回結果進行脫敏的攔截器示例:

@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DataMaskInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {List<Object> results = (List<Object>) invocation.proceed();if (results != null) {for (Object obj : results) {if (obj instanceof User) { // 假設返回的是 User 對象列表User user = (User) obj;String email = user.getEmail();// 簡單示例:只顯示郵箱前3位,其余替換為星號if (email != null && email.length() > 3) {user.setEmail(email.substring(0, 3) + "****");}}}}return results;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}

在這個示例中,對返回的 List<Object> 進行遍歷,如果元素是 User 對象,就對其 email 字段進行脫敏處理。注冊該攔截器后,所有查詢結果中的 email 字段都會在返回給調用方之前經過該邏輯脫敏。類似地,可以在插入或更新前校驗參數或在查詢前校驗用戶權限等。

4.5 動態SQL構建與參數處理

場景需求:在不修改原 SQL 的情況下,根據某些條件動態更改 SQL 或參數。例如在執行前自動為 SQL 追加查詢條件,或修改輸入參數。

實現思路:可攔截 Executor.updateExecutor.query,分析 MappedStatement 和參數對象,構造新的 SQL 或參數,然后通過 MappedStatementSqlSourceBoundSql 生成新的 MappedStatement。示例思路(簡化版):

@Intercepts({@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DynamicSqlInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {MappedStatement ms = (MappedStatement) invocation.getArgs()[0];Object parameter = invocation.getArgs()[1];// 根據業務需求動態生成或修改 SQLBoundSql boundSql = ms.getBoundSql(parameter);String originalSql = boundSql.getSql();// 例如如果參數中包含某個標志,則追加條件if (parameter instanceof Map && ((Map) parameter).containsKey("activeOnly")) {String newSql = originalSql + " AND active = 1";// 這里為了簡單示例,不做 MappedStatement 完全復制// 在實際中可使用 MetaObject 修改boundSql.sql字段System.out.println("[動態SQL攔截] 新SQL: " + newSql);}return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}

在實際生產場景,完全復制并替換 MappedStatement 及其 SqlSource 需要較多代碼(可參考 MyBatis 源碼中的插件實現邏輯),上述示例僅演示概念。在需要動態 SQL 時,常見做法是結合 MyBatis 提供的 <if> 標簽或 SqlSessionFactory 監聽等,但攔截器也可實現更靈活的邏輯。

五、MyBatis Interceptor性能優化

雖然攔截器功能強大,但錯誤或過多使用會帶來性能負擔。以下優化技巧可供參考。

5.1 攔截器鏈的性能瓶頸分析

  • 動態代理開銷:每個攔截目標都會被多重代理包裝,方法調用需經過多次 InvocationHandler.invoke 調用,增加了額外的反射開銷。隨著攔截器數量增多,這種鏈式包裝帶來的開銷成倍增長。

  • 頻繁構造對象:如果在攔截器中頻繁創建對象(例如每次調用都 new 復雜對象),會增加垃圾回收壓力。特別是處理大量 SQL 時,應盡量重用可復用對象或使用局部緩沖區。

  • 數據結構復雜性:如果攔截器內部邏輯復雜,如使用了深度反射或大規模集合遍歷,會加長單次調用時間。

優化建議

  • 僅攔截必要方法:注解中盡量精確指定需要攔截的方法,避免對不需要的接口進行攔截。

  • 合并相似攔截器:如果多個攔截器作用相似,可合并為一個攔截器,在 intercept 內部分支處理,減少代理層數。

  • 提前過濾:在 intercept 中盡早檢查條件,不滿足時盡快返回 invocation.proceed(),減少不必要的處理。

  • 懶加載:對于某些復雜計算,可考慮僅在滿足特定條件時才執行,避免每次都運行所有代碼。

5.2 攔截器的線程安全設計

攔截器實例通常是單例并在多線程環境中被復用,因此需保證線程安全:

  • 無狀態設計:盡量不在攔截器中定義可變成員變量,或將其限定為 final 常量(例如數據庫方言常量)。避免使用非線程安全的數據結構(如非同步的 ArrayList 等)作為字段。

  • 局部變量:在 intercept 方法中使用局部變量存儲臨時數據,局部變量天然線程隔離。

  • 線程局部變量:如果需要在線程間隔離數據,可使用 ThreadLocal,但需注意可能導致內存泄漏(一定要在請求結束時清理)。

  • 同步鎖避免:盡量避免在攔截器中引入鎖(如 synchronized)會嚴重影響并發性能。如需共享資源,可考慮使用無鎖并發容器或外部緩存系統。

5.3 攔截器的緩存策略

攔截器自身可以引入合理的緩存策略來提升性能:

  • 方法/元數據緩存:對于反射獲取的方法或配置,可以在攔截器加載時緩存 Method 對象。MyBatis Plugin 已經將匹配的方法緩存到 signatureMap 中,無需重復查找。

  • SQL 模板緩存:若動態修改 SQL 的邏輯復雜,可以將常用的 SQL 模板或修改后 SQL 保存到緩存,減少每次重組 SQL 的開銷。

  • 結果/參數緩存:如果某些攔截邏輯依賴于計算結果(如脫敏規則表、加密密鑰等),可將此類靜態信息緩存到攔截器實例中,避免頻繁讀取外部資源。

需要注意避免緩存失效帶來的一致性問題。任何緩存必須與底層數據更新機制兼容,例如在數據表變化后清空相關緩存或使用緩存穿透策略。

5.4 攔截器的異步執行方案

對于一些非關鍵路徑的耗時操作,如日志記錄、審計信息存儲等,可考慮異步執行以減少對主線程的阻塞:

  • 日志異步化:在攔截器 intercept 中生成要記錄的日志內容后,使用異步框架(如異步隊列、獨立線程、消息系統)將日志寫入任務推送出去,不在當前線程寫文件或數據庫。

  • 審計信息異步化:類似日志,將審計數據封裝后放入線程池或消息中間件,讓后臺進程處理。

  • 定時任務:對于周期性或批量統計,也可在攔截器中簡單收集數據(如計數、時間等),而真正的匯總和告警由定時任務完成。

異步策略需注意線程安全異常處理,確保異步任務失敗不會影響主流程,以及及時處理失敗的任務。

六、MyBatis Interceptor的局限性與替代方案

6.1 攔截器的局限性分析

雖然 MyBatis 插件非常靈活,但也存在一些局限,需要合理評估:

  • 作用范圍有限:MyBatis 攔截器只能攔截 ExecutorStatementHandlerParameterHandlerResultSetHandler 這四類接口的方法。對于其他自定義的業務方法、非 MyBatis 的流程,攔截器無能為力。

  • 簽名硬編碼:攔截方法需要在注解中硬編碼指定方法名和參數類型,不能模糊匹配或使用表達式。一旦對應方法重命名或參數改變,攔截配置就會失效或拋出異常。

  • 與插件沖突:多個插件如果攔截了同一目標,執行順序取決于配置順序(默認后配置的先執行,見第8.1節),容易造成難以預測的交互問題。不同插件間的相互影響需人工管理。

  • 調試困難:由于攔截器通過代理隱式工作,對鏈條調試不直觀。排查問題時需依賴日志或斷點,復雜場景下分析鏈路較為繁瑣。

  • 性能開銷:如前所述,每個攔截器會帶來額外方法調用開銷,過多插件會明顯拖慢執行速度,需要謹慎使用。

6.2 MyBatis Interceptor與Spring AOP的對比

對比 MyBatis 插件與 Spring AOP,可從下面幾個方面進行分析:

特性MyBatis InterceptorSpring AOP
織入對象MyBatis 核心接口(Executor、StatementHandler 等)Spring 管理的 Bean 方法
攔截點粒度SQL 執行前后、參數綁定、結果處理等數據庫層面方法調用層面,可攔截任何 public 方法(默認)
配置方式MyBatis 配置文件或 MyBatis-Spring 插件方式注解或 XML 配置切面(@Aspect、XML)
實現方式動態代理/反射(不依賴 AOP 框架)代理(JDK/CGLIB)或字節碼增強
依賴環境必須在 MyBatis 環境中使用任意 Spring 應用,不依賴數據庫
執行時機數據庫操作時(SQL 執行鏈)方法執行時
使用場景日志、SQL 重寫、性能監控、數據過濾等事務、權限、安全、通用日志等

兩者并不沖突,在一個項目中可以同時使用:Spring AOP 用于業務層、服務層的切面邏輯,而 MyBatis 插件專注于數據訪問層面。例如,在業務方法前可以用 AOP 進行權限檢查,用 MyBatis 插件記錄 SQL 日志。

6.3 替代方案:基于責任鏈模式的自定義實現

當 MyBatis 插件的局限無法滿足需求時,可以考慮自定義實現責任鏈模式來達到類似攔截的效果。例如:

  • 手動調用鏈:在 DAO 層或服務層封裝一個調用流程,將多個“處理器”串聯,每個處理器實現某個前置或后置邏輯。通過在執行數據庫操作前后手動調用這些處理器,實現與攔截器類似的效果。

  • 使用 Spring AOP:如果業務方法正好位于 Spring 管理的 Bean 中,也可以使用 Spring AOP 切面攔截 DAO 層的方法,并在切面中操作 SqlSession 或參數。Spring AOP 的切面靈活度高,但無法像 MyBatis 插件那樣直接操作 SQL。

  • 代理包裝 SqlSession:自行對 SqlSession 或 Mapper 接口進行代理,插入攔截邏輯。例如使用 JDK 動態代理或 CGLIB 對 Mapper 接口生成代理類,在調用 select/update 方法時執行預處理或后處理。

這些方案相比 MyBatis 插件更為應用層,需要自行維護鏈條調用邏輯和順序。雖然開發成本可能更高,但在某些特殊場合(如需要插入無法通過 MyBatis API 訪問的步驟)提供了替代可能。

七、MyBatis Interceptor源碼深度解析

進一步深入 MyBatis 源碼,可了解攔截器機制的實現細節。以下重點解析關鍵類:InterceptorChainPluginInvocation

7.1 InterceptorChain源碼解析

org.apache.ibatis.plugin.InterceptorChain 維護了所有攔截器實例列表,并負責對目標對象進行包裝。其源碼核心如下:

public class InterceptorChain {private final List<Interceptor> interceptors = new ArrayList<>();public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;}public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);}public List<Interceptor> getInterceptors() {return Collections.unmodifiableList(interceptors);}
}
  • interceptors 列表:存儲所有已注冊的攔截器,添加順序即配置順序。

  • addInterceptor():在 Configuration 解析 <plugin> 時調用,將新攔截器加入列表。

  • pluginAll(Object target):將給定對象(如 Executor)依次傳遞給每個攔截器的 plugin 方法。plugin 返回一個可能的代理對象,因此最終得到的是多層嵌套的代理。

該設計確保了多個攔截器可以組合使用。順序上,第一個攔截器的 plugin 先被應用(最里層代理),最后一個攔截器包裹在最外層,導致配置文件中后注冊的攔截器先執行的效果(詳見第8.1節)。

7.2 Plugin類的動態代理實現

Plugin 類實現了 InvocationHandler,負責創建并處理代理對象的調用。源碼關鍵部分:

public class Plugin implements InvocationHandler {private final Object target;private final Interceptor interceptor;private final Map<Class<?>, Set<Method>> signatureMap;public static Object wrap(Object target, Interceptor interceptor) {Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);Class<?> type = target.getClass();Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));}return target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {return interceptor.intercept(new Invocation(target, method, args));}return method.invoke(target, args);}// ... 獲取signatureMap和getAllInterfaces方法 ...
}
  • wrap() 方法:獲取攔截器的 @Signature 信息構造 signatureMap,再查找目標類實現的接口,將匹配接口傳入 Proxy.newProxyInstance 構造代理。只有當目標類實現了注解中指定的接口時,才會生成代理;否則返回原始對象。

  • invoke() 方法:每次代理對象調用方法時都會進入此處。根據 method.getDeclaringClass()signatureMap 查找對應的方法集合,如果當前方法匹配,則構造 Invocation 調用攔截器的 intercept;否則直接調用目標對象的方法。

  • getSignatureMap():解析攔截器類上的 @Intercepts 注解,將每個 @Signaturetypemethodargs 轉為 java.lang.reflect.Method 并存入 Map<Class<?>, Set<Method>> 中。這樣每個目標接口對應一個需要攔截的方法列表。

  • getAllInterfaces():收集目標類及其父類實現的所有接口,篩選出包含在 signatureMap 中的接口,用于代理實現。

通過 Plugin.wrap,MyBatis 能夠在運行時動態為目標對象生成代理對象,實現對指定方法的攔截。代理對象與目標對象實現了相同接口,調用時透明地進入 Plugin.invoke,從而觸發開發者編寫的 intercept 邏輯。

7.3 Invocation調用鏈的執行機制

Invocation 是攔截器執行過程中的上下文對象,源碼(見 MyBatis Javadoc)簡單描述如下:

public class Invocation {private final Object target;private final Method method;private final Object[] args;public Invocation(Object target, Method method, Object[] args) { ... }public Object proceed() throws InvocationTargetException, IllegalAccessException {return method.invoke(target, args);}// 還有 getTarget(), getMethod(), getArgs() 等輔助方法
}
  • 作用:封裝了攔截點的信息,包含目標對象、目標方法和調用參數。

  • proceed() 方法:當開發者在 intercept 方法中調用 invocation.proceed() 時,會繼續調用目標方法或下一個攔截器。實際上,Invocation 持有的 target 是原始對象或下一個代理實例,因此調用 method.invoke(target, args) 會進入攔截鏈的下一層,最終到達最里層目標實現。

  • 調用鏈:如果有多個攔截器鏈在一個方法上,則每次 proceed() 都會觸發鏈中下一個攔截器的 intercept;最后一個攔截器的 proceed() 會調用原始目標的方法并返回結果。

在實際代碼中,我們通常不需要直接操作 Invocation 以外的部分,只要遵循**在攔截方法內適時調用 invocation.proceed()**即可實現鏈式調用。例如,一個簡單的 intercept 中可能是:

public Object intercept(Invocation invocation) throws Throwable {// 攔截前邏輯Object result = invocation.proceed(); // 執行下一個攔截器或原方法// 攔截后邏輯return result;
}

Invocation 的作用機制確保了 責任鏈模式 的串聯執行特性:多個攔截器按順序包裹目標,并逐個執行前置和后置邏輯。

八、進階:攔截器鏈的執行順序控制

8.1 攔截器優先級的配置方法

MyBatis 不提供顯式的優先級配置注解。攔截器的執行順序僅通過配置順序控制:在 mybatis-config.xml<plugins> 節點配置多個 <plugin> 時,后聲明的攔截器會先執行,這與 InterceptorChain.pluginAll() 的實現順序相符(后加入的攔截器處于列表后端,調用時代理層數最外層,先被觸發)。例如:

<plugins><plugin interceptor="com.example.InterceptorA"/><plugin interceptor="com.example.InterceptorB"/>
</plugins>

在這個配置中,InterceptorB 會在 InterceptorA 之前執行(因為 B 被包裝在 A 的外層)。如果需要調整順序,只需調整 <plugin> 標簽的順序即可。MyBatis 官方論壇和文檔也說明了這一點。

8.2 攔截器鏈的調試技巧

調試攔截器鏈可以采取以下方法:

  • 日志輸出:在每個攔截器的 intercept 方法中添加日志打印,標識進入和退出方法的位置,或者打印攔截器名稱和方法名。通過日志可了解鏈條中各攔截器執行的先后順序以及方法調用情況。

  • 使用斷點:在 IDE 中為每個攔截器類設置斷點,調試時觀察調用棧和攔截鏈情況。由于鏈式調用較復雜,可在 Plugin.invoke 中斷點查看 Invocation 對象內容,確認目標方法的真正執行過程。

  • MyBatis 調試日志:啟用 MyBatis SQL 日志(在 log4j.propertiesapplication.yml 中設置 log level 為 DEBUG),可以看到代理生成的 SQL 語句執行日志,間接反映攔截器是否修改了 SQL。

  • 測試覆蓋:編寫單元測試對每個攔截器方法進行覆蓋測試,確保鏈路中各攔截器都按預期運行。可以使用 Mockito 等框架模擬 Invocation 對象來測試 intercept 方法的邏輯。

8.3 攔截器鏈的異常處理機制

  • 異常傳播:如果攔截器的 intercept 方法中拋出異常,MyBatis 會將該異常向上拋出,最終由調用方捕獲或傳播。開發者應根據需要捕獲并處理攔截器中的異常,或者將其封裝為自定義異常拋出。

  • 攔截器內部處理:可以在 intercept 方法內部使用 try-catch 捕獲異常,對可預見的錯誤進行處理,并決定是否重新拋出。例如,日志攔截器遇到打印錯誤時一般應捕獲異常避免影響業務流程。

  • 鏈中斷:一旦鏈中某個攔截器出現未捕獲異常,后續攔截器將不會執行,直接中斷整個 SQL 調用。需要注意配置順序和異常可能造成的影響范圍。

  • MyBatis 異常轉化:在 Plugin.invoke 中(源碼)會捕獲底層方法拋出的異常并使用 ExceptionUtil.unwrapThrowable 進行解包拋出,以保證拋出的是可理解的異常類型。因此,在攔截器中如果使用反射或代理調用目標方法而產生 InvocationTargetException,最終調用方看到的將是其根本原因。

合理的異常處理策略可以保證攔截器出現問題時,系統能快速定位并采取措施,而不會因為攔截器問題導致更嚴重的后果。

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

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

相關文章

【自動化測試】Web自動化測試 Selenium

&#x1f525;個人主頁&#xff1a; 中草藥 &#x1f525;專欄&#xff1a;【Java】登神長階 史詩般的Java成神之路 測試分類 了解各種各樣的測試方法分類&#xff0c;不是為了墨守成規按照既定方法區測試&#xff0c;而是已了解思維為核心&#xff0c;并了解一些專業名詞 根…

2025 電賽 C 題完整通關攻略:從單目標定到 2 cm 測距精度的全流程實戰

摘要 2025 年全國大學生電子設計競賽 C 題要求“僅用一顆固定攝像頭”在 5 s 內完成 100 cm~200 cm 距離、誤差 ≤2 cm 的單目測距&#xff0c;并實時顯示功耗。本文整合國一選手方案、CSDN 高分博文、B 站實測視頻及官方說明&#xff0c;給出從硬件選型→離線標定→在線算法→…

Day 10: Mini-GPT完整手寫實戰 - 從組件組裝到文本生成的端到端實現

Day 10-2: Mini-GPT完整手寫實戰 - 從組件組裝到文本生成的端到端實現 ?? 今日學習目標 掌握GPT架構組裝:將Transformer組件組裝成完整的生成模型 理解生成式預訓練:掌握自回歸語言建模的核心機制 端到端代碼實現:從數據預處理到模型訓練的完整流程 文本生成實戰:訓練Mi…

深入解析Prompt緩存機制:原理、優化與實踐經驗

深入解析Prompt緩存機制&#xff1a;原理、優化與實踐經驗 概述 在大型語言模型應用中&#xff0c;API請求的延遲和成本始終是開發者關注的核心問題。Prompt緩存&#xff08;Prompt Caching&#xff09;技術通過智能地復用重復內容&#xff0c;有效減少了API響應時間和運行成本…

CV 醫學影像分類、分割、目標檢測,之【3D肝臟分割】項目拆解

CV 醫學影像分類、分割、目標檢測&#xff0c;之【3D肝臟分割】項目拆解第1行&#xff1a;from posixpath import join第2行&#xff1a;from torch.utils.data import DataLoader第3行&#xff1a;import os第4行&#xff1a;import sys第5行&#xff1a;import random第6行&a…

Mybatis學習筆記(七)

Spring Boot集成 簡要描述&#xff1a;MyBatis-Plus與Spring Boot的深度集成&#xff0c;提供了自動配置、啟動器等特性&#xff0c;大大簡化了配置和使用。 核心概念&#xff1a; 自動配置&#xff1a;基于條件的自動配置機制啟動器&#xff1a;簡化依賴管理的starter配置屬性…

機器人伴侶的智能升級:Deepoc具身智能模型如何重塑成人伴侶體驗

引言&#xff1a;機器人伴侶市場的技術變革需求隨著人工智能技術的飛速發展和人們情感需求的多元化&#xff0c;機器人成人伴侶市場正在經歷前所未有的增長。傳統機器人伴侶已經能夠滿足基礎的交互需求&#xff0c;但在智能化、情感化和個性化方面仍存在明顯不足。這正是深算紀…

metabase基礎使用技巧 (dashboard, filter)

這是metabase系列分享文章的第2部分。本文將介紹metabase的基礎概念和使用介紹 question question是metabase中提供的通過UI化操作就能實現簡單的 快捷 直接的BI查詢。 點擊右側的New -> Question即可創建Question&#xff0c;可以理解為一個格式化的查詢&#xff1a; 這里…

機器人成人伴侶的智能化升級:Deepoc具身模型賦能沉浸式體驗

引言&#xff1a;成人機器人市場的技術革新需求隨著人工智能和機器人技術的快速發展&#xff0c;成人陪伴機器人行業正經歷從簡單機械運動到智能化交互的轉型。據市場研究數據顯示&#xff0c;全球成人機器人市場規模預計將在2026年突破100億美元&#xff0c;年復合增長率保持在…

Go語言企業級權限管理系統設計與實現

最近跟著學長再寫河南師范大學附屬中學圖書館的項目&#xff0c;學長交給了我一個任務&#xff0c;把本項目的權限管理給吃透&#xff0c;然后應用到下一個項目上。 我當然是偷著樂吶&#xff0c;因為讀代碼的時候&#xff0c;總是莫名給我一種公費旅游的感覺。 本來就想去了解…

Java應用快速部署Tomcat指南

將Java應用部署到Apache Tomcat服務器是開發Web應用過程中常見的任務。Tomcat是一個免費且開源的Servlet容器,它為Java應用提供了運行環境。本文將介紹如何準備你的Java應用,并將其部署到Tomcat服務器上。 Java 應用部署 tomcat 的根目錄結構 Tomcat中默認網站根目錄是$CAT…

Java 學習筆記(基礎篇2)

1. 分支結構① if 語句&#xff1a;(1) 雙分支&#xff1a;if (條件) {// 語句體1 } else {// 語句體2 }(2) 多分支if (條件1) {// 語句體1 } else if (條件2) {// 語句體2 } else {// 語句體N }② switch 語句&#xff1a;(1) 語法&#xff1a;如果都不是&#xff08;default&…

谷歌云代理商:用 AI 啟航,Gemini 重塑旅游酒店行業新體驗

本文由谷歌云谷歌地圖官方授權代理商、高級合作伙伴 CloudAce云一 整理發布。谷歌云谷歌地圖在中國授權代理商名單&#xff1a;Cloud Ace云一&#xff0c;全球20分公司&#xff0c;國內核心城市多個據點&#xff0c;谷歌云與谷歌地圖代理商、頂級合作伙伴&#xff08;Premier P…

springboot+vue實現通過poi完成excel

前端1、按鈕<el-buttontype"text"size"mini"click"handleExport">導出</el-button>2、方法//導出async handleExport() {if (!this.activityId) {this.$message.warning(活動ID不存在);return;}try {this.loading true;const res …

JMeter性能測試詳細版(適合0基礎小白學習--非常詳細)

01性能測試的概念 02性能測試的概念 基準測試 負載測試 穩定性測試 其他&#xff1a;并發測試、壓力測試、回歸測試等 壓力測試就是在系統強負載的情況下&#xff0c;是否會出現功能隱患問題&#xff0c;出現問題后是否可以盡快恢復 負載測試和壓力測試的區別: 1,核心目標不…

QT6(創建第一個QT項目)

編寫第一個QT項目 QT官網 安裝完QT后的界面 創建第一個項目 這里我們選擇第一個就好 下一步 下一步 選擇CMake&#xff0c;QMake是QT的CMAKE&#xff08;現在官方自己都不推薦了&#xff09; 下一步 選擇QWidget我們先創建一個最簡單的窗口程序 QMainWindow&#xff1a;主窗…

Golang指針操作

在 Go 語言&#xff08;Golang&#xff09;中&#xff0c;* 和 & 是與指針相關的兩個重要操作符。 理解它們對于掌握 Go 的內存管理和函數參數傳遞機制非常關鍵。 文章目錄一、& 操作符&#xff1a;取地址&#xff08;Address-of&#xff09;示例&#xff1a;二、* 操…

微服務從0到1

微服務從0到1實施步驟與注意事項一、核心實施步驟??需求分析與架構設計??明確業務邊界?&#xff1a;根據業務模塊&#xff08;如用戶管理、訂單系統&#xff09;劃分服務職責&#xff0c;避免服務職責重疊或耦合?。?定義接口契約?&#xff1a;通過 OpenAPI/Swagger 規范…

小程序排名優化:功能迭代如何助力排名攀升

小程序的功能不是一成不變的&#xff0c;持續的功能迭代不僅能滿足用戶不斷變化的需求&#xff0c;也是提升排名的重要途徑。平臺更傾向于推薦那些不斷更新、功能完善的小程序&#xff0c;因為它們能為用戶提供更優質的服務。合理規劃功能迭代方向和節奏&#xff0c;能讓小程序…

Unity TextMeshPro(二)優化

文章目錄前言一、字體打包優化二、ab打包冗余1、問題1、解決方法三、字體靜態優化四、擴展總結前言 優化TextMeshPro包體大小的方法記錄。 一、字體打包優化 游戲開發階段通常使用Fast打包方式&#xff0c;在正式項目發布的時候需要切換一下打包方式&#xff0c;重新將字體打…