1. 程式人生 > >SpringBoot整合Mybatis自定義攔截器,實現拼接sql和修改

SpringBoot整合Mybatis自定義攔截器,實現拼接sql和修改

一、應用場景

1.分頁,如com.github.pagehelper的分頁外掛實現;

2.攔截sql做日誌監控;

3.統一對某些sql進行統一條件拼接,類似於分頁。

二、MyBatis的攔截器簡介

然後我們要知道攔截器攔截什麼樣的物件,攔截物件的什麼行為,什麼時候攔截?

       在Mybatis框架中,已經給我們提供了攔截器介面,防止我們改動原始碼來新增行為實現攔截。說到攔截器,不得不提一下,攔截器是通過動態代理對Mybatis加入一些自己的行為。

攔截物件

       確立攔截物件範圍:要攔對人,既要保證攔對人,又要保證對正確的人執行正確的攔截動作

攔截地點

       在Mybatis的原始碼中

攔截時機

        如果mybatis為我們提供了攔截的功能,我們應該在Mybatis執行過程的哪一步攔截更加合適呢?

攔截某個物件幹某件事的時候,攔截的時機要對,過早的攔截會耽誤別人做自己的工作,攔截太晚達不到目的。

    Mybatis實際也是一個JDBC執行的過程,只不過被包裝起來了而已,我們要攔截Mybatis的執行,最遲也要在獲取PreparedStatement時攔截:

PreparedStatementstatement=conn.prepareStatement(sql.toString());

在此處偷偷的將sql語句換掉,就可以改變mybatis的執行,加入自己想要的執行行為。

 而獲取Mybatis的Statement是在StatementHandler中進行的。

三、程式碼示例

第一步、引入依賴

        <dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>

 第二步、配置application.propertities,指定好好對映檔案和實體類的目錄

### mybatis
mybatis.mapperLocations: classpath:mapping/*.xml  
###classpath就是應用程式resources的路徑
mybatis.type-aliases-package: com.pingan.yc.demo.model

第三步、配置程式碼生成器,引入配置在pom中新增一下程式碼

<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<!--<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-resources-plugin</artifactId>
				<configuration>
					<encoding>${encoding}</encoding>
				</configuration>
			</plugin>-->
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<!-- mybatis generator 自動生成程式碼外掛 -->
			<plugin>
				<groupId>org.mybatis.generator</groupId>
				<artifactId>mybatis-generator-maven-plugin</artifactId>
				<version>1.3.2</version>
				<configuration>
					<!-- 自動生成程式碼的配置檔案地址 -->
					<configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
					<verbose>true</verbose>
					<overwrite>true</overwrite>
				</configuration>
			</plugin>
		</plugins>
	</build>

在resource資料夾下新建generator.xml檔案


內容如下:需要注意配置資料庫連線驅動和資料庫連線,指定生成實體類和對映檔案的路徑,最後按格式配置要生成的資料表

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!-- 資料庫驅動:選擇你的本地硬碟上面的資料庫驅動包-->
    <classPathEntry  location="D:\Users\admin\.m2\repository\mysql\mysql-connector-java\5.1.46\mysql-connector-java-5.1.46.jar"/>
    <context id="DB2Tables"  targetRuntime="MyBatis3">
        <commentGenerator>
            <property name="suppressDate" value="true"/>
            <!-- 是否去除自動生成的註釋 true:是 : false:否 -->
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>
        <!--資料庫連結URL,使用者名稱、密碼 -->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:4433/standard_policy_db" userId="dev" password="dev">
        </jdbcConnection>
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>
        <!-- 生成模型的包名和位置-->
        <javaModelGenerator targetPackage="com.pingan.yc.policy.model" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>
        <!-- 生成對映檔案的包名和位置-->
        <sqlMapGenerator targetPackage="mapping" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!-- 生成DAO的包名和位置-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.pingan.yc.policy.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>
        <!-- 要生成的表 tableName是資料庫中的表名或檢視名 domainObjectName是實體類名-->
        <table tableName="t_user_yc" domainObjectName="User" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false"  />
        <table tableName="t_userinfo_yc" domainObjectName="UserInfo" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false"  selectByExampleQueryId="true"/>
        

    </context>
</generatorConfiguration>

最後:在maven命令視窗或如下介面中執行mybatis-generator:generate命令

最後生成如下檔案:

第四步、攔截器 

(生成對應檔案後,mybatis環境算是整合好了,可以執行一個測試類試試能否從資料庫讀取資料。)

定義一個類實現Mybatis的Interceptor介面,@Component註解必須要新增,不然可能出現攔截器無效的情況!!!

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Properties;


@Component
@Intercepts({
        @Signature(
                type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class
        })
})

public class MySqlInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {


        // 方法一
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
        //先攔截到RoutingStatementHandler,裡面有個StatementHandler型別的delegate變數,其實現類是BaseStatementHandler,然後就到BaseStatementHandler的成員變數mappedStatement
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        //id為執行的mapper方法的全路徑名,如com.uv.dao.UserMapper.insertUser
        String id = mappedStatement.getId();
        //sql語句型別 select、delete、insert、update
        String sqlCommandType = mappedStatement.getSqlCommandType().toString();
        BoundSql boundSql = statementHandler.getBoundSql();

        //獲取到原始sql語句
        String sql = boundSql.getSql();
        String mSql = sql;
        //TODO 修改位置

        //註解邏輯判斷  添加註解了才攔截
        Class<?> classType = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf(".")));
        String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, mappedStatement.getId().length());
        for (Method method : classType.getDeclaredMethods()) {
            if (method.isAnnotationPresent(InterceptAnnotation.class) && mName.equals(method.getName())) {
                InterceptAnnotation interceptorAnnotation = method.getAnnotation(InterceptAnnotation.class);
                if (interceptorAnnotation.flag()) {
                    mSql = sql + " limit 2";
                }
            }
        }

        //通過反射修改sql語句
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, mSql);
        return invocation.proceed();

    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }

    }

        @Override
    public void setProperties(Properties properties) {

    }
}

此外,定義了一個方法層面的註解,實現區域性指定攔截


@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface InterceptAnnotation {
    boolean flag() default  true;
}

最後只需要在指定的方法出添加註解就可以實現區域性攔截

最後執行看結果就好了。(按我的邏輯下來會只查到2條資料)

在@Interceptor的註解中也有以下的註解方式,究竟有什麼不同和差異,請大家自己研究咯,我就在此拋磚引玉了,請各位大牛指導了。

@Intercepts(value = {
        @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,
                        CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})