首先進行Mybatis 攔截器介紹css
攔截器的一個做用就是咱們能夠攔截某些方法的調用,咱們能夠選擇在這些被攔截的方法執行先後加上某些邏輯,也能夠在執行這些被攔截的方法時執行本身的邏輯而再也不執行被攔截的方法。Mybatis攔截器設計的一個初衷就是爲了供用戶在某些時候能夠實現本身的邏輯而沒必要去動Mybatis固有的邏輯。打個比方,對於Executor,Mybatis中有幾種實現:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。這個時候若是你以爲這幾種實現對於Executor接口的query方法都不能知足你的要求,那怎麼辦呢?是要去改源碼嗎?固然不。咱們能夠創建一個Mybatis攔截器用於攔截Executor接口的query方法,在攔截以後實現本身的query方法邏輯,以後能夠選擇是否繼續執行原來的query方法。
對於攔截器Mybatis爲咱們提供了一個Interceptor接口,經過實現該接口就能夠定義咱們本身的攔截器。咱們先來看一下這個接口的定義:前端
package org.apache.ibatis.plugin; import java.util.Properties; public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); } org.apache.ibatis.plugin; import java.util.Properties; public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
咱們能夠看到在該接口中一共定義有三個方法,intercept、plugin和setProperties。plugin方法是攔截器用於封裝目標對象的,經過該方法咱們能夠返回目標對象自己,也能夠返回一個它的代理。當返回的是代理的時候咱們能夠對其中的方法進行攔截來調用intercept方法,固然也能夠調用其餘方法,這點將在後文講解。setProperties方法是用於在Mybatis配置文件中指定一些屬性的。java
定義本身的Interceptor最重要的是要實現plugin方法和intercept方法,在plugin方法中咱們能夠決定是否要進行攔截進而決定要返回一個什麼樣的目標對象。而intercept方法就是要進行攔截的時候要執行的方法。mysql
對於plugin方法而言,其實Mybatis已經爲咱們提供了一個實現。Mybatis中有一個叫作Plugin的類,裏面有一個靜態方法wrap(Object target,Interceptor interceptor),經過該方法能夠決定要返回的對象是目標對象仍是對應的代理。web
咱們先看一下Plugin的wrap方法,它根據當前的Interceptor上面的註解定義哪些接口須要攔截,而後判斷當前目標對象是否有實現對應須要攔截的接口,若是沒有則返回目標對象自己,若是有則返回一個代理對象。而這個代理對象的InvocationHandler正是一個Plugin。因此當目標對象在執行接口方法時,若是是經過代理對象執行的,則會調用對應InvocationHandler的invoke方法,也就是Plugin的invoke方法。因此接着咱們來看一下該invoke方法的內容。這裏invoke方法的邏輯是:若是當前執行的方法是定義好的須要攔截的方法,則把目標對象、要執行的方法以及方法參數封裝成一個Invocation對象,再把封裝好的Invocation做爲參數傳遞給當前攔截器的intercept方法。若是不須要攔截,則直接調用當前的方法。Invocation中定義了定義了一個proceed方法,其邏輯就是調用當前方法,因此若是在intercept中須要繼續調用當前方法的話能夠調用invocation的procced方法。spring
這就是Mybatis中實現Interceptor攔截的一個思想,若是用戶以爲這個思想有問題或者不能徹底知足你的要求的話能夠經過實現本身的Plugin來決定何時須要代理何時須要攔截。如下講解的內容都是基於Mybatis的默認實現即經過Plugin來管理Interceptor來說解的。sql
對於實現本身的Interceptor而言有兩個很重要的註解,一個是@Intercepts,其值是一個@Signature數組。@Intercepts用於代表當前的對象是一個Interceptor,而@Signature則代表要攔截的接口、方法以及對應的參數類型。來看一個自定義的簡單Interceptor:
首先看setProperties方法,這個方法在Configuration初始化當前的Interceptor時就會執行,這裏只是簡單的取兩個屬性進行打印。數據庫
其次看plugin方法中咱們是用的Plugin的邏輯來實現Mybatis的邏輯的。apache
接着看MyInterceptor類上咱們用@Intercepts標記了這是一個Interceptor,而後在@Intercepts中定義了兩個@Signature,即兩個攔截點。第一個@Signature咱們定義了該Interceptor將攔截Executor接口中參數類型爲MappedStatement、Object、RowBounds和ResultHandler的query方法;第二個@Signature咱們定義了該Interceptor將攔截StatementHandler中參數類型爲Connection的prepare方法。api
最後再來看一下intercept方法,這裏咱們只是簡單的打印了一句話,而後調用invocation的proceed方法,使當前方法正常的調用。
對於這個攔截器,Mybatis在註冊該攔截器的時候就會利用定義好的n個property做爲參數調用該攔截器的setProperties方法。以後在新建可攔截對象的時候會調用該攔截器的plugin方法來決定是返回目標對象自己仍是代理對象。對於這個攔截器而言,當Mybatis是要Executor或StatementHandler對象的時候就會返回一個代理對象,其餘都是原目標對象自己。而後當Executor代理對象在執行參數類型爲MappedStatement、Object、RowBounds和ResultHandler的query方法或StatementHandler代理對象在執行參數類型爲Connection的prepare方法時就會觸發當前的攔截器的intercept方法進行攔截,而執行這兩個接口對象的其餘方法時都只是作一個簡單的代理。
冊攔截器是經過在Mybatis配置文件中plugins元素下的plugin元素來進行的。一個plugin對應着一個攔截器,在plugin元素下面咱們能夠指定若干個property子元素。Mybatis在註冊定義的攔截器時會先把對應攔截器下面的全部property經過Interceptor的setProperties方法注入給對應的攔截器。因此,咱們能夠這樣來註冊咱們在前面定義的MyInterceptor:
Mybatis攔截器只能攔截四種類型的接口:Executor、StatementHandler、ParameterHandler和ResultSetHandler。這是在Mybatis的Configuration中寫死了的,若是要支持攔截其餘接口就須要咱們重寫Mybatis的Configuration。Mybatis能夠對這四個接口中全部的方法進行攔截。
下面將介紹一個Mybatis攔截器的實際應用。Mybatis攔截器經常會被用來進行分頁處理。咱們知道要利用JDBC對數據庫進行操做就必需要有一個對應的Statement對象,Mybatis在執行Sql語句前也會產生一個包含Sql語句的Statement對象,並且對應的Sql語句是在Statement以前產生的,因此咱們就能夠在它成Statement以前對用來生成Statement的Sql語句下手。在Mybatis中Statement語句是經過RoutingStatementHandler對象的prepare方法生成的。因此利用攔截器實現Mybatis分頁的一個思路就是攔截StatementHandler接口的prepare方法,而後在攔截器方法中把Sql語句改爲對應的分頁查詢Sql語句,以後再調用StatementHandler對象的prepare方法,即調用invocation.proceed()。更改Sql語句這個看起來很簡單,而事實上來講的話就沒那麼直觀,由於包括sql等其餘屬性在內的多個屬性都沒有對應的方法能夠直接取到,它們對外部都是封閉的,是對象的私有屬性,因此這裏就須要引入反射機制來獲取或者更改對象的私有屬性的值了。對於分頁而言,在攔截器裏面咱們經常還須要作的一個操做就是統計知足當前條件的記錄一共有多少,這是經過獲取到了原始的Sql語句後,把它改成對應的統計語句再利用Mybatis封裝好的參數和設置參數的功能把Sql語句中的參數進行替換,以後再執行查詢記錄數的Sql語句進行總記錄數的統計。
下面是一個簡單的實現影子表切換的功能:數據庫mysql8.0.12,數據庫鏈接池:Druid1.1.10,mybatis版本3.4.6,springboot版本2.0.3,使用mybatis-plus插件版本3.0.7.1
主要數據庫相關pom文件以下:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.12</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> <exclusions> <exclusion> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> </exclusion> <exclusion> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatisplus-spring-boot-starter</artifactId> <version>1.0.5</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.0.7.1</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-autoconfigure</artifactId> <version>1.3.2</version> </dependency> <!-- velocity 模板引擎, 默認 --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.0</version> </dependency> <!-- freemarker 模板引擎 --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.28</version> </dependency> <!-- beetl 模板引擎 --> <dependency> <groupId>com.ibeetl</groupId> <artifactId>beetl</artifactId> <version>2.9.8</version> </dependency>
mybatis配置以下:
########################## mysql ########################## spring.datasource.url: "jdbc:mysql://localhost:3306/chinotan?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true" spring.datasource.username: root spring.datasource.password: spring.datasource.driver-class-name: "com.mysql.cj.jdbc.Driver" logging.level.com.shyroke.mapper: debug ########################## mybatis ########################## mybatis.mapper-locations: classpath:mybatis/*.xml ########################## druid配置 ########################## spring.datasource.type: com.alibaba.druid.pool.DruidDataSource # 初始化大小,最小,最大 spring.datasource.initialSize: 5 spring.datasource.minIdle: 5 spring.datasource.maxActive: 20 # 配置獲取鏈接等待超時的時間 spring.datasource.maxWait: 60000 # 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 spring.datasource.timeBetweenEvictionRunsMillis: 60000 # 配置一個鏈接在池中最小生存的時間,單位是毫秒 spring.datasource.minEvictableIdleTimeMillis: 300000 # 校驗SQL,Oracle配置 spring.datasource.validationQuery: SELECT 1 FROM DUAL,若是不配validationQuery項,則下面三項配置無用 spring.datasource.validationQuery: SELECT 'x' spring.datasource.testWhileIdle: true spring.datasource.testOnBorrow: false spring.datasource.testOnReturn: false # 打開PSCache,而且指定每一個鏈接上PSCache的大小 spring.datasource.poolPreparedStatements: true spring.datasource.maxPoolPreparedStatementPerConnectionSize: 20 # 配置監控統計攔截的filters,去掉後監控界面sql沒法統計,'wall'用於防火牆 spring.datasource.filters: stat,wall,log4j # 經過connectProperties屬性來打開mergeSql功能;慢SQL記錄 spring.datasource.connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 合併多個DruidDataSource的監控數據 spring.datasource.useGlobalDataSourceStat: true # 配置mybatis-plus mybatis-plus: mapper-locations: classpath:/mapper/*.xml #實體掃描,多個package用逗號或者分號分隔 typeAliasesPackage: cn.chinotan.entity global-config: #主鍵類型 0:"數據庫ID自增", 1:"用戶輸入ID",2:"全局惟一ID (數字類型惟一ID)", 3:"全局惟一ID UUID"; id-type: 0 #字段策略 0:"忽略判斷",1:"非 NULL 判斷"),2:"非空判斷" field-strategy: 2 #駝峯下劃線轉換 db-column-underline: true #刷新mapper 調試神器 refresh-mapper: true #數據庫大寫下劃線轉換 #capital-mode: true #序列接口實現類配置 #key-generator: com.baomidou.springboot.xxx #邏輯刪除配置(下面3個配置) logic-delete-value: 0 logic-not-delete-value: 1 #自定義SQL注入器 #sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector #自定義填充策略接口實現 #meta-object-handler: com.baomidou.springboot.xxx configuration: map-underscore-to-camel-case: true cache-enabled: false
Druid和mybatis的配置文件:
package cn.chinotan.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.PropertySource; import javax.sql.DataSource; import java.sql.SQLException; /** * @program: test * @description: 導入增長的jdbc配置文件 * @author: xingcheng * @create: 2019-02-16 17:43 **/ @Configuration @PropertySource(value = "classpath:application.yml") public class DataSourceConfiguration { @Value("${spring.datasource.url}") private String dbUrl; @Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; @Value("${spring.datasource.driver-class-name}") private String driverClassName; @Value("${spring.datasource.initialSize}") private int initialSize; @Value("${spring.datasource.minIdle}") private int minIdle; @Value("${spring.datasource.maxActive}") private int maxActive; @Value("${spring.datasource.maxWait}") private int maxWait; @Value("${spring.datasource.timeBetweenEvictionRunsMillis}") private int timeBetweenEvictionRunsMillis; @Value("${spring.datasource.minEvictableIdleTimeMillis}") private int minEvictableIdleTimeMillis; @Value("${spring.datasource.validationQuery}") private String validationQuery; @Value("${spring.datasource.testWhileIdle}") private boolean testWhileIdle; @Value("${spring.datasource.testOnBorrow}") private boolean testOnBorrow; @Value("${spring.datasource.testOnReturn}") private boolean testOnReturn; @Value("${spring.datasource.poolPreparedStatements}") private boolean poolPreparedStatements; @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}") private int maxPoolPreparedStatementPerConnectionSize; @Value("${spring.datasource.filters}") private String filters; @Value("${spring.datasource.connectionProperties}") private String connectionProperties; @Value("${spring.datasource.useGlobalDataSourceStat}") private boolean useGlobalDataSourceStat; @Bean //聲明其爲Bean實例 @Primary //在一樣的DataSource中,首先使用被標註的DataSource public DataSource druidDataSource() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(this.dbUrl); datasource.setUsername(username); datasource.setPassword(password); datasource.setDriverClassName(driverClassName); //configuration datasource.setInitialSize(initialSize); datasource.setMinIdle(minIdle); datasource.setMaxActive(maxActive); datasource.setMaxWait(maxWait); datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setValidationQuery(validationQuery); datasource.setTestWhileIdle(testWhileIdle); datasource.setTestOnBorrow(testOnBorrow); datasource.setTestOnReturn(testOnReturn); datasource.setPoolPreparedStatements(poolPreparedStatements); datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); datasource.setUseGlobalDataSourceStat(useGlobalDataSourceStat); try { datasource.setFilters(filters); } catch (SQLException e) { System.err.println("druid configuration initialization filter: "+ e); } datasource.setConnectionProperties(connectionProperties); return datasource; } /** * 註冊一個StatViewServlet * @return */ @Bean public ServletRegistrationBean druidStatViewServlet(){ //org.springframework.boot.context.embedded.ServletRegistrationBean提供類的進行註冊. ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*"); //添加初始化參數:initParams //白名單: servletRegistrationBean.addInitParameter("allow","127.0.0.1"); //IP黑名單 (存在共同時,deny優先於allow) : 若是知足deny的話提示:Sorry, you are not permitted to view this page. servletRegistrationBean.addInitParameter("deny","192.168.1.73"); //登陸查看信息的帳號密碼. servletRegistrationBean.addInitParameter("loginUsername","admin"); servletRegistrationBean.addInitParameter("loginPassword","123456"); //是否可以重置數據. servletRegistrationBean.addInitParameter("resetEnable","false"); return servletRegistrationBean; } /** * 註冊一個:filterRegistrationBean * @return */ @Bean public FilterRegistrationBean druidStatFilter(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter()); //添加過濾規則. filterRegistrationBean.addUrlPatterns("/*"); //添加不須要忽略的格式信息. filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } }
package cn.chinotan.config; import cn.chinotan.interceptor.ShareStatementPlugin; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.type.JdbcType; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ResourceLoader; import javax.sql.DataSource; /** * @program: test * @description: * @author: xingcheng * @create: 2019-02-16 20:39 **/ @Configuration public class MybatisPlusConfig { @Bean public MapperScannerConfigurer mapperScannerConfigurer() { MapperScannerConfigurer scannerConfigurer = new MapperScannerConfigurer(); //能夠經過環境變量獲取你的mapper路徑,這樣mapper掃描能夠經過配置文件配置了 scannerConfigurer.setBasePackage("cn.chinotan.dao"); return scannerConfigurer; } @Bean("mybatisSqlSession") public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ResourceLoader resourceLoader) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); sqlSessionFactory.setTypeAliasesPackage("cn.chinotan.entity"); MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class); configuration.setJdbcTypeForNull(JdbcType.NULL); sqlSessionFactory.setConfiguration(configuration); sqlSessionFactory.setPlugins(new Interceptor[]{ new PaginationInterceptor(), new PerformanceInterceptor(), new OptimisticLockerInterceptor() }); return sqlSessionFactory.getObject(); } /*** * plus 的性能優化 * @return */ @Bean public PerformanceInterceptor performanceInterceptor() { PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor(); /*<!-- SQL 執行性能分析,開發環境使用,線上不推薦。 maxTime 指的是 sql 最大執行時長 -->*/ performanceInterceptor.setMaxTime(500000000); /*<!--SQL是否格式化 默認false-->*/ performanceInterceptor.setFormat(true); return performanceInterceptor; } /** * @Description : mybatis-plus分頁插件 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } /** * @Description : mybatis插件 */ @Bean public ShareStatementPlugin examplePlugin(SqlSessionFactory sqlSessionFactory) { ShareStatementPlugin plugin = new ShareStatementPlugin(); sqlSessionFactory.getConfiguration().addInterceptor(plugin); return plugin; } }
自動文件生成:
package cn.chinotan.codeGenerator; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Scanner; /** * @program: test * @description: 代碼生成 * @author: xingcheng * @create: 2019-02-16 19:52 **/ public class CodeGenerator { /** * * @Title: main * @Description: 生成 * @param args */ public static void main(String[] args) { AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir("/Users/xingcheng/Documents/IdeaProjects/test/start_test/out"); //輸出文件路徑 gc.setFileOverride(true); gc.setActiveRecord(false);// 不須要ActiveRecord特性的請改成false gc.setEnableCache(false);// XML 二級緩存 gc.setBaseResultMap(true);// XML ResultMap gc.setBaseColumnList(false);// XML columList gc.setAuthor("xingcheng");// 做者 // 自定義文件命名,注意 %s 會自動填充表實體屬性! gc.setControllerName("%sController"); gc.setServiceName("%sService"); gc.setServiceImplName("%sServiceImpl"); gc.setMapperName("%sMapper"); gc.setXmlName("%sMapper"); mpg.setGlobalConfig(gc); // 數據源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setDbType(DbType.MYSQL); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword(""); dsc.setUrl("jdbc:mysql://localhost:3306/chinotan?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"); mpg.setDataSource(dsc); // 策略配置 StrategyConfig strategy = new StrategyConfig(); // strategy.setTablePrefix(new String[] { "sys_" });// 此處能夠修改成您的表前綴 strategy.setNaming(NamingStrategy.underline_to_camel);// 表名生成策略 strategy.setInclude(new String[] { "user", "user_bak" }); // 須要生成的表 strategy.setSuperServiceClass(null); strategy.setSuperServiceImplClass(null); strategy.setSuperMapperClass(null); mpg.setStrategy(strategy); // 包配置 PackageConfig pc = new PackageConfig(); pc.setParent("cn.chinotan"); pc.setController("controller"); pc.setService("service"); pc.setServiceImpl("service.impl"); pc.setMapper("dao"); pc.setEntity("entity"); pc.setXml("xml"); mpg.setPackageInfo(pc); // 執行生成 mpg.execute(); } }
數據實體:
package cn.chinotan.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDateTime; import java.io.Serializable; /** * <p> * 用戶表 * </p> * * @author xingcheng * @since 2019-02-16 */ public class User { /** * 主鍵 */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 姓名 */ private String name; /** * 建立時間 */ private LocalDateTime createTime; /** * 更新時間 */ private LocalDateTime updateTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } public LocalDateTime getUpdateTime() { return updateTime; } public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; } @Override public String toString() { return "User{" + "id=" + id + ", name=" + name + ", createTime=" + createTime + ", updateTime=" + updateTime + "}"; } }
創建一個註解,用來配置路由策略
package cn.chinotan.aop; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.text.SimpleDateFormat; import java.util.Date; /** * @program: test * @description: 路由策略 * @author: xingcheng * @create: 2019-02-23 16:44 **/ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface TableConfig { //是否影子表 boolean split() default true; //表名 String value() default ""; //獲取分表策略 String strategy(); }
mapper接口:
package cn.chinotan.dao; import cn.chinotan.aop.TableConfig; import cn.chinotan.entity.User; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /** * <p> * 用戶表 Mapper 接口 * </p> * * @author xingcheng * @since 2019-02-16 */ @TableConfig(strategy="bak") public interface UserMapper extends BaseMapper<User> { }
mapperXML路徑:
Controller層控制:
package cn.chinotan.controller; import cn.chinotan.entity.User; import cn.chinotan.service.UserService; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.util.Date; import java.util.List; /** * <p> * 用戶表 前端控制器 * </p> * * @author xingcheng * @since 2019-02-16 */ @RestController @RequestMapping("/user") public class UserController { @Autowired UserService userService; @GetMapping("/list") public Object list() { List<User> list = userService.list(); return list; } @GetMapping("/save/{name}") public Object save(@PathVariable("name") String name) { User user = new User(); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); user.setName(name); boolean save = userService.save(user); return save; } @GetMapping("/update/{name}") public Object update(@PathVariable("name") String name) { User user = new User(); user.setId(1L); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); user.setName(name); boolean update = userService.updateById(user); return update; } }
路由策略接口和實現:
package cn.chinotan.service; /** * @program: test * @description: * @author: xingcheng * @create: 2019-02-23 17:52 **/ public interface Strategy { /** * 傳入一個須要分表的表名,返回一個處理後的表名 * Strategy必須包含一個無參構造器 * @param tableName * @return */ String convert(String tableName); }
package cn.chinotan.service.impl; import cn.chinotan.service.Strategy; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Date; /** * @program: test * @description: * @author: xingcheng * @create: 2019-02-23 17:52 **/ @Component("bak") public class BakStrategy implements Strategy { @Override public String convert(String tableName) { StringBuilder sb=new StringBuilder(tableName); sb.append("_bak"); return sb.toString(); } }
接下來最重要的Interceptor實現:
package cn.chinotan.interceptor; import cn.chinotan.aop.TableConfig; import cn.chinotan.service.Strategy; import org.apache.commons.lang3.StringUtils; 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.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.sql.Connection; import java.util.Map; import java.util.Properties; /** * 完成插件簽名: * 告訴MyBatis當前插件用來攔截哪一個對象的哪一個方法 * type 指四大對象攔截哪一個對象, * method : 表明攔截哪一個方法 ,在StatementHandler 中查看,須要攔截的方法 * args :表明參數 */ @Component @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class})}) public class ShareStatementPlugin implements Interceptor { private static final Logger LOG = LoggerFactory.getLogger(ShareStatementPlugin.class); @Autowired private Map<String, Strategy> strategyMap; @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); doTable(statementHandler, metaObject); return invocation.proceed(); } private void doTable(StatementHandler handler, MetaObject metaStatementHandler) throws ClassNotFoundException { BoundSql boundSql = handler.getBoundSql(); String originalSql = boundSql.getSql(); if (originalSql != null && !originalSql.equals("")) { LOG.info("分表前的SQL:{}", originalSql); MappedStatement mappedStatement = (MappedStatement) metaStatementHandler .getValue("delegate.mappedStatement"); String id = mappedStatement.getId(); String className = id.substring(0, id.lastIndexOf(".")); Class<?> classObj = Class.forName(className); Class baseEntity = null; Type[] interfacesTypes = classObj.getGenericInterfaces(); for (Type type : interfacesTypes) { if (type instanceof ParameterizedType) { ParameterizedType interfacesType = (ParameterizedType) interfacesTypes[0]; Type t = interfacesType.getActualTypeArguments()[0]; baseEntity = (Class) t; } } // 根據配置自動生成分表SQL TableConfig tableConfig = classObj.getAnnotation(TableConfig.class); // 獲取表名 並進行相應轉化 String tableName = baseEntity.getSimpleName().toLowerCase(); if (StringUtils.isNotBlank(tableConfig.value())) { tableName = tableConfig.value(); } if (tableConfig != null && tableConfig.split()) { // 獲取策略來處理 Strategy strategy = strategyMap.get(tableConfig.strategy()); String convertedSql = originalSql.replaceAll(tableName, strategy.convert(tableName)); metaStatementHandler.setValue("delegate.boundSql.sql", convertedSql); LOG.info("分表後的SQL:{}", convertedSql); } } } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) { } /** * 得到真正的處理對象,可能多層代理 * * @param target * @param <T> * @return */ public static <T> T realTarget(Object target) { if (Proxy.isProxyClass(target.getClass())) { MetaObject metaObject = SystemMetaObject.forObject(target); return realTarget(metaObject.getValue("h.target")); } return (T) target; } }
效果:變動前兩張表數據
執行save接口後:
能夠看到只查詢影子表,簡單效果實現
下一步優化內容:
可以根據控制層傳輸過來的是否採用影子表標識來動態的進行影子表的讀取和寫入,而不是寫死在代碼中