Mybatis通过Interceptor来简单实现影子表进行动态sql读取和写入

首先进行Mybatis 拦截器介绍

    拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候如果你觉得这几种实现对于Executor接口的query方法都不能满足你的要求,那怎么办呢?是要去改源码吗?当然不。我们可以建立一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截之后实现自己的query方法逻辑,之后可以选择是否继续执行原来的query方法。
对于拦截器Mybatis为我们提供了一个Interceptor接口,通过实现该接口就可以定义我们自己的拦截器。我们先来看一下这个接口的定义:

1package org.apache.ibatis.plugin; 2 3import java.util.Properties; 4 5public interface Interceptor { 6 7 Object intercept(Invocation invocation) throws Throwable; 8 9 Object plugin(Object target); 10 11 void setProperties(Properties properties); 12 13} 14 org.apache.ibatis.plugin; 15 16import java.util.Properties; 17 18public interface Interceptor { 19 20 Object intercept(Invocation invocation) throws Throwable; 21 22 Object plugin(Object target); 23 24 void setProperties(Properties properties); 25 26}

我们可以看到在该接口中一共定义有三个方法,intercept、plugin和setProperties。plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法,这点将在后文讲解。setProperties方法是用于在Mybatis配置文件中指定一些属性的。

       定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。

       对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。

    我们先看一下Plugin的wrap方法,它根据当前的Interceptor上面的注解定义哪些接口需要拦截,然后判断当前目标对象是否有实现对应需要拦截的接口,如果没有则返回目标对象本身,如果有则返回一个代理对象。而这个代理对象的InvocationHandler正是一个Plugin。所以当目标对象在执行接口方法时,如果是通过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。所以接着我们来看一下该invoke方法的内容。这里invoke方法的逻辑是:如果当前执行的方法是定义好的需要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数传递给当前拦截器的intercept方法。如果不需要拦截,则直接调用当前的方法。Invocation中定义了定义了一个proceed方法,其逻辑就是调用当前方法,所以如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。

       这就是Mybatis中实现Interceptor拦截的一个思想,如果用户觉得这个思想有问题或者不能完全满足你的要求的话可以通过实现自己的Plugin来决定什么时候需要代理什么时候需要拦截。以下讲解的内容都是基于Mybatis的默认实现即通过Plugin来管理Interceptor来讲解的。

       对于实现自己的Interceptor而言有两个很重要的注解,一个是@Intercepts,其值是一个@Signature数组。@Intercepts用于表明当前的对象是一个Interceptor,而@Signature则表明要拦截的接口、方法以及对应的参数类型。来看一个自定义的简单Interceptor:
       首先看setProperties方法,这个方法在Configuration初始化当前的Interceptor时就会执行,这里只是简单的取两个属性进行打印。

       其次看plugin方法中我们是用的Plugin的逻辑来实现Mybatis的逻辑的。

       接着看MyInterceptor类上我们用@Intercepts标记了这是一个Interceptor,然后在@Intercepts中定义了两个@Signature,即两个拦截点。第一个@Signature我们定义了该Interceptor将拦截Executor接口中参数类型为MappedStatement、Object、RowBounds和ResultHandler的query方法;第二个@Signature我们定义了该Interceptor将拦截StatementHandler中参数类型为Connection的prepare方法。

       最后再来看一下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文件如下:    

1 <dependency> 2 <groupId>mysql</groupId> 3 <artifactId>mysql-connector-java</artifactId> 4 <version>8.0.12</version> 5 </dependency> 6 7 <dependency> 8 <groupId>org.mybatis.spring.boot</groupId> 9 <artifactId>mybatis-spring-boot-starter</artifactId> 10 <version>1.3.2</version> 11 <exclusions> 12 <exclusion> 13 <groupId>org.mybatis</groupId> 14 <artifactId>mybatis</artifactId> 15 </exclusion> 16 <exclusion> 17 <groupId>org.mybatis</groupId> 18 <artifactId>mybatis-spring</artifactId> 19 </exclusion> 20 </exclusions> 21 </dependency> 22 23 <dependency> 24 <groupId>com.alibaba</groupId> 25 <artifactId>druid</artifactId> 26 <version>1.1.10</version> 27 </dependency> 28 29 <dependency> 30 <groupId>org.springframework.boot</groupId> 31 <artifactId>spring-boot-starter-jdbc</artifactId> 32 </dependency> 33 34 <dependency> 35 <groupId>com.baomidou</groupId> 36 <artifactId>mybatisplus-spring-boot-starter</artifactId> 37 <version>1.0.5</version> 38 </dependency> 39 40 <dependency> 41 <groupId>com.baomidou</groupId> 42 <artifactId>mybatis-plus-generator</artifactId> 43 <version>3.0.7.1</version> 44 </dependency> 45 46 <dependency> 47 <groupId>org.mybatis.spring.boot</groupId> 48 <artifactId>mybatis-spring-boot-autoconfigure</artifactId> 49 <version>1.3.2</version> 50 </dependency> 51 52 <!-- velocity 模板引擎, 默认 --> 53 <dependency> 54 <groupId>org.apache.velocity</groupId> 55 <artifactId>velocity-engine-core</artifactId> 56 <version>2.0</version> 57 </dependency> 58 59 <!-- freemarker 模板引擎 --> 60 <dependency> 61 <groupId>org.freemarker</groupId> 62 <artifactId>freemarker</artifactId> 63 <version>2.3.28</version> 64 </dependency> 65 66 67 <!-- beetl 模板引擎 --> 68 <dependency> 69 <groupId>com.ibeetl</groupId> 70 <artifactId>beetl</artifactId> 71 <version>2.9.8</version> 72 </dependency>

    mybatis配置如下:

1########################## mysql ########################## 2spring.datasource.url: "jdbc:mysql://localhost:3306/chinotan?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true" 3spring.datasource.username: root 4spring.datasource.password: 5spring.datasource.driver-class-name: "com.mysql.cj.jdbc.Driver" 6 7logging.level.com.shyroke.mapper: debug 8 9 10########################## mybatis ########################## 11mybatis.mapper-locations: classpath:mybatis/*.xml 12 13########################## druid配置 ########################## 14spring.datasource.type: com.alibaba.druid.pool.DruidDataSource 15 16# 初始化大小,最小,最大 17spring.datasource.initialSize: 5 18spring.datasource.minIdle: 5 19spring.datasource.maxActive: 20 20# 配置获取连接等待超时的时间 21spring.datasource.maxWait: 60000 22# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 23spring.datasource.timeBetweenEvictionRunsMillis: 60000 24# 配置一个连接在池中最小生存的时间,单位是毫秒 25spring.datasource.minEvictableIdleTimeMillis: 300000 26# 校验SQL,Oracle配置 spring.datasource.validationQuery: SELECT 1 FROM DUAL,如果不配validationQuery项,则下面三项配置无用 27spring.datasource.validationQuery: SELECT 'x' 28spring.datasource.testWhileIdle: true 29spring.datasource.testOnBorrow: false 30spring.datasource.testOnReturn: false 31# 打开PSCache,并且指定每个连接上PSCache的大小 32spring.datasource.poolPreparedStatements: true 33spring.datasource.maxPoolPreparedStatementPerConnectionSize: 20 34# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 35spring.datasource.filters: stat,wall,log4j 36# 通过connectProperties属性来打开mergeSql功能;慢SQL记录 37spring.datasource.connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 38# 合并多个DruidDataSource的监控数据 39spring.datasource.useGlobalDataSourceStat: true 40 41# 配置mybatis-plus 42mybatis-plus: 43 mapper-locations: classpath:/mapper/*.xml 44 #实体扫描,多个package用逗号或者分号分隔 45 typeAliasesPackage: cn.chinotan.entity 46 global-config: 47 #主键类型 0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID"; 48 id-type: 0 49 #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断" 50 field-strategy: 2 51 #驼峰下划线转换 52 db-column-underline: true 53 #刷新mapper 调试神器 54 refresh-mapper: true 55 #数据库大写下划线转换 56 #capital-mode: true 57 #序列接口实现类配置 58 #key-generator: com.baomidou.springboot.xxx 59 #逻辑删除配置(下面3个配置) 60 logic-delete-value: 0 61 logic-not-delete-value: 1 62 #自定义SQL注入器 63 #sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector 64 #自定义填充策略接口实现 65 #meta-object-handler: com.baomidou.springboot.xxx 66 configuration: 67 map-underscore-to-camel-case: true 68 cache-enabled: false

    Druid和mybatis的配置文件:

1package cn.chinotan.config; 2 3import com.alibaba.druid.pool.DruidDataSource; 4import com.alibaba.druid.support.http.StatViewServlet; 5import com.alibaba.druid.support.http.WebStatFilter; 6import org.springframework.beans.factory.annotation.Value; 7import org.springframework.boot.context.properties.ConfigurationProperties; 8import org.springframework.boot.web.servlet.FilterRegistrationBean; 9import org.springframework.boot.web.servlet.ServletRegistrationBean; 10import org.springframework.context.annotation.Bean; 11import org.springframework.context.annotation.Configuration; 12import org.springframework.context.annotation.Primary; 13import org.springframework.context.annotation.PropertySource; 14 15import javax.sql.DataSource; 16import java.sql.SQLException; 17 18/** 19 * @program: test 20 * @description: 导入增加的jdbc配置文件 21 * @author: xingcheng 22 * @create: 2019-02-16 17:43 23 **/ 24@Configuration 25@PropertySource(value = "classpath:application.yml") 26public class DataSourceConfiguration { 27 28 @Value("${spring.datasource.url}") 29 private String dbUrl; 30 @Value("${spring.datasource.username}") 31 private String username; 32 @Value("${spring.datasource.password}") 33 private String password; 34 @Value("${spring.datasource.driver-class-name}") 35 private String driverClassName; 36 @Value("${spring.datasource.initialSize}") 37 private int initialSize; 38 @Value("${spring.datasource.minIdle}") 39 private int minIdle; 40 @Value("${spring.datasource.maxActive}") 41 private int maxActive; 42 @Value("${spring.datasource.maxWait}") 43 private int maxWait; 44 @Value("${spring.datasource.timeBetweenEvictionRunsMillis}") 45 private int timeBetweenEvictionRunsMillis; 46 @Value("${spring.datasource.minEvictableIdleTimeMillis}") 47 private int minEvictableIdleTimeMillis; 48 @Value("${spring.datasource.validationQuery}") 49 private String validationQuery; 50 @Value("${spring.datasource.testWhileIdle}") 51 private boolean testWhileIdle; 52 @Value("${spring.datasource.testOnBorrow}") 53 private boolean testOnBorrow; 54 @Value("${spring.datasource.testOnReturn}") 55 private boolean testOnReturn; 56 @Value("${spring.datasource.poolPreparedStatements}") 57 private boolean poolPreparedStatements; 58 @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}") 59 private int maxPoolPreparedStatementPerConnectionSize; 60 @Value("${spring.datasource.filters}") 61 private String filters; 62 @Value("${spring.datasource.connectionProperties}") 63 private String connectionProperties; 64 @Value("${spring.datasource.useGlobalDataSourceStat}") 65 private boolean useGlobalDataSourceStat; 66 67 @Bean //声明其为Bean实例 68 @Primary //在同样的DataSource中,首先使用被标注的DataSource 69 public DataSource druidDataSource() { 70 DruidDataSource datasource = new DruidDataSource(); 71 datasource.setUrl(this.dbUrl); 72 datasource.setUsername(username); 73 datasource.setPassword(password); 74 datasource.setDriverClassName(driverClassName); 75 76 //configuration 77 datasource.setInitialSize(initialSize); 78 datasource.setMinIdle(minIdle); 79 datasource.setMaxActive(maxActive); 80 datasource.setMaxWait(maxWait); 81 datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); 82 datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); 83 datasource.setValidationQuery(validationQuery); 84 datasource.setTestWhileIdle(testWhileIdle); 85 datasource.setTestOnBorrow(testOnBorrow); 86 datasource.setTestOnReturn(testOnReturn); 87 datasource.setPoolPreparedStatements(poolPreparedStatements); 88 datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); 89 datasource.setUseGlobalDataSourceStat(useGlobalDataSourceStat); 90 try { 91 datasource.setFilters(filters); 92 } catch (SQLException e) { 93 System.err.println("druid configuration initialization filter: "+ e); 94 } 95 datasource.setConnectionProperties(connectionProperties); 96 return datasource; 97 } 98 99 /** 100 * 注册一个StatViewServlet 101 * @return 102 */ 103 @Bean 104 public ServletRegistrationBean druidStatViewServlet(){ 105 //org.springframework.boot.context.embedded.ServletRegistrationBean提供类的进行注册. 106 ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*"); 107 108 //添加初始化参数:initParams 109 //白名单: 110 servletRegistrationBean.addInitParameter("allow","127.0.0.1"); 111 //IP黑名单 (存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page. 112 servletRegistrationBean.addInitParameter("deny","192.168.1.73"); 113 //登录查看信息的账号密码. 114 servletRegistrationBean.addInitParameter("loginUsername","admin"); 115 servletRegistrationBean.addInitParameter("loginPassword","123456"); 116 //是否能够重置数据. 117 servletRegistrationBean.addInitParameter("resetEnable","false"); 118 return servletRegistrationBean; 119 } 120 121 /** 122 * 注册一个:filterRegistrationBean 123 * @return 124 */ 125 @Bean 126 public FilterRegistrationBean druidStatFilter(){ 127 128 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter()); 129 130 //添加过滤规则. 131 filterRegistrationBean.addUrlPatterns("/*"); 132 133 //添加不需要忽略的格式信息. 134 filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); 135 return filterRegistrationBean; 136 } 137} 138 139 140package cn.chinotan.config; 141 142import cn.chinotan.interceptor.ShareStatementPlugin; 143import com.baomidou.mybatisplus.core.MybatisConfiguration; 144import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver; 145import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 146import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor; 147import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; 148import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; 149import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; 150import org.apache.ibatis.plugin.Interceptor; 151import org.apache.ibatis.session.SqlSessionFactory; 152import org.apache.ibatis.type.JdbcType; 153import org.mybatis.spring.mapper.MapperScannerConfigurer; 154import org.springframework.context.annotation.Bean; 155import org.springframework.context.annotation.Configuration; 156import org.springframework.core.io.ResourceLoader; 157 158import javax.sql.DataSource; 159 160/** 161 * @program: test 162 * @description: 163 * @author: xingcheng 164 * @create: 2019-02-16 20:39 165 **/ 166@Configuration 167public class MybatisPlusConfig { 168 169 @Bean 170 public MapperScannerConfigurer mapperScannerConfigurer() { 171 MapperScannerConfigurer scannerConfigurer = new MapperScannerConfigurer(); 172 //可以通过环境变量获取你的mapper路径,这样mapper扫描可以通过配置文件配置了 173 scannerConfigurer.setBasePackage("cn.chinotan.dao"); 174 return scannerConfigurer; 175 } 176 177 @Bean("mybatisSqlSession") 178 public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ResourceLoader resourceLoader) throws Exception { 179 MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); 180 sqlSessionFactory.setDataSource(dataSource); 181 sqlSessionFactory.setTypeAliasesPackage("cn.chinotan.entity"); 182 MybatisConfiguration configuration = new MybatisConfiguration(); 183 configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class); 184 configuration.setJdbcTypeForNull(JdbcType.NULL); 185 sqlSessionFactory.setConfiguration(configuration); 186 sqlSessionFactory.setPlugins(new Interceptor[]{ 187 new PaginationInterceptor(), 188 new PerformanceInterceptor(), 189 new OptimisticLockerInterceptor() 190 }); 191 return sqlSessionFactory.getObject(); 192 } 193 194 /*** 195 * plus 的性能优化 196 * @return 197 */ 198 @Bean 199 public PerformanceInterceptor performanceInterceptor() { 200 PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor(); 201 /*<!-- SQL 执行性能分析,开发环境使用,线上不推荐。 maxTime 指的是 sql 最大执行时长 -->*/ 202 performanceInterceptor.setMaxTime(500000000); 203 /*<!--SQL是否格式化 默认false-->*/ 204 performanceInterceptor.setFormat(true); 205 return performanceInterceptor; 206 } 207 208 /** 209 * @Description : mybatis-plus分页插件 210 */ 211 @Bean 212 public PaginationInterceptor paginationInterceptor() { 213 return new PaginationInterceptor(); 214 } 215 216 /** 217 * @Description : mybatis插件 218 */ 219 @Bean 220 public ShareStatementPlugin examplePlugin(SqlSessionFactory sqlSessionFactory) { 221 ShareStatementPlugin plugin = new ShareStatementPlugin(); 222 sqlSessionFactory.getConfiguration().addInterceptor(plugin); 223 return plugin; 224 } 225 226}

    自动文件生成:

1package cn.chinotan.codeGenerator; 2 3import com.baomidou.mybatisplus.annotation.DbType; 4import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; 5import com.baomidou.mybatisplus.core.toolkit.StringPool; 6import com.baomidou.mybatisplus.generator.AutoGenerator; 7import com.baomidou.mybatisplus.generator.InjectionConfig; 8import com.baomidou.mybatisplus.generator.config.*; 9import com.baomidou.mybatisplus.generator.config.po.TableInfo; 10import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; 11import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; 12import org.apache.commons.lang3.StringUtils; 13 14import java.util.ArrayList; 15import java.util.List; 16import java.util.Scanner; 17 18/** 19 * @program: test 20 * @description: 代码生成 21 * @author: xingcheng 22 * @create: 2019-02-16 19:52 23 **/ 24public class CodeGenerator { 25 26 /** 27 * 28 * @Title: main 29 * @Description: 生成 30 * @param args 31 */ 32 public static void main(String[] args) { 33 AutoGenerator mpg = new AutoGenerator(); 34 35 // 全局配置 36 GlobalConfig gc = new GlobalConfig(); 37 gc.setOutputDir("/Users/xingcheng/Documents/IdeaProjects/test/start_test/out"); //输出文件路径 38 gc.setFileOverride(true); 39 gc.setActiveRecord(false);// 不需要ActiveRecord特性的请改为false 40 gc.setEnableCache(false);// XML 二级缓存 41 gc.setBaseResultMap(true);// XML ResultMap 42 gc.setBaseColumnList(false);// XML columList 43 gc.setAuthor("xingcheng");// 作者 44 45 // 自定义文件命名,注意 %s 会自动填充表实体属性! 46 gc.setControllerName("%sController"); 47 gc.setServiceName("%sService"); 48 gc.setServiceImplName("%sServiceImpl"); 49 gc.setMapperName("%sMapper"); 50 gc.setXmlName("%sMapper"); 51 mpg.setGlobalConfig(gc); 52 53 // 数据源配置 54 DataSourceConfig dsc = new DataSourceConfig(); 55 dsc.setDbType(DbType.MYSQL); 56 dsc.setDriverName("com.mysql.cj.jdbc.Driver"); 57 dsc.setUsername("root"); 58 dsc.setPassword(""); 59 dsc.setUrl("jdbc:mysql://localhost:3306/chinotan?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"); 60 mpg.setDataSource(dsc); 61 62 // 策略配置 63 StrategyConfig strategy = new StrategyConfig(); 64 // strategy.setTablePrefix(new String[] { "sys_" });// 此处可以修改为您的表前缀 65 strategy.setNaming(NamingStrategy.underline_to_camel);// 表名生成策略 66 strategy.setInclude(new String[] { "user", "user_bak" }); // 需要生成的表 67 68 strategy.setSuperServiceClass(null); 69 strategy.setSuperServiceImplClass(null); 70 strategy.setSuperMapperClass(null); 71 72 mpg.setStrategy(strategy); 73 74 // 包配置 75 PackageConfig pc = new PackageConfig(); 76 pc.setParent("cn.chinotan"); 77 pc.setController("controller"); 78 pc.setService("service"); 79 pc.setServiceImpl("service.impl"); 80 pc.setMapper("dao"); 81 pc.setEntity("entity"); 82 pc.setXml("xml"); 83 mpg.setPackageInfo(pc); 84 85 // 执行生成 86 mpg.execute(); 87 88 } 89 90}

    数据实体:

1package cn.chinotan.entity; 2 3import com.baomidou.mybatisplus.annotation.IdType; 4import com.baomidou.mybatisplus.annotation.TableId; 5import com.baomidou.mybatisplus.annotation.TableName; 6 7import java.time.LocalDateTime; 8import java.io.Serializable; 9 10/** 11 * <p> 12 * 用户表 13 * </p> 14 * 15 * @author xingcheng 16 * @since 2019-02-16 17 */ 18public class User { 19 20 /** 21 * 主键 22 */ 23 @TableId(value = "id", type = IdType.AUTO) 24 private Long id; 25 26 /** 27 * 姓名 28 */ 29 private String name; 30 31 /** 32 * 创建时间 33 */ 34 private LocalDateTime createTime; 35 36 /** 37 * 更新时间 38 */ 39 private LocalDateTime updateTime; 40 41 42 public Long getId() { 43 return id; 44 } 45 46 public void setId(Long id) { 47 this.id = id; 48 } 49 50 public String getName() { 51 return name; 52 } 53 54 public void setName(String name) { 55 this.name = name; 56 } 57 58 public LocalDateTime getCreateTime() { 59 return createTime; 60 } 61 62 public void setCreateTime(LocalDateTime createTime) { 63 this.createTime = createTime; 64 } 65 66 public LocalDateTime getUpdateTime() { 67 return updateTime; 68 } 69 70 public void setUpdateTime(LocalDateTime updateTime) { 71 this.updateTime = updateTime; 72 } 73 74 @Override 75 public String toString() { 76 return "User{" + 77 "id=" + id + 78 ", name=" + name + 79 ", createTime=" + createTime + 80 ", updateTime=" + updateTime + 81 "}"; 82 } 83}

    建立一个注解,用来配置路由策略    

1package cn.chinotan.aop; 2 3import java.lang.annotation.ElementType; 4import java.lang.annotation.Retention; 5import java.lang.annotation.RetentionPolicy; 6import java.lang.annotation.Target; 7import java.text.SimpleDateFormat; 8import java.util.Date; 9 10/** 11 * @program: test 12 * @description: 路由策略 13 * @author: xingcheng 14 * @create: 2019-02-23 16:44 15 **/ 16@Retention(RetentionPolicy.RUNTIME) 17@Target({ElementType.TYPE}) 18public @interface TableConfig { 19 //是否影子表 20 boolean split() default true; 21 22 //表名 23 String value() default ""; 24 25 //获取分表策略 26 String strategy(); 27}

    mapper接口:

1package cn.chinotan.dao; 2 3import cn.chinotan.aop.TableConfig; 4import cn.chinotan.entity.User; 5import com.baomidou.mybatisplus.core.mapper.BaseMapper; 6 7/** 8 * <p> 9 * 用户表 Mapper 接口 10 * </p> 11 * 12 * @author xingcheng 13 * @since 2019-02-16 14 */ 15@TableConfig(strategy="bak") 16public interface UserMapper extends BaseMapper<User> { 17 18}

    mapperXML路径:

    Controller层控制:

1package cn.chinotan.controller; 2 3 4import cn.chinotan.entity.User; 5import cn.chinotan.service.UserService; 6import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; 7import org.springframework.beans.factory.annotation.Autowired; 8import org.springframework.web.bind.annotation.GetMapping; 9import org.springframework.web.bind.annotation.PathVariable; 10import org.springframework.web.bind.annotation.RequestMapping; 11import org.springframework.web.bind.annotation.RestController; 12 13import java.time.LocalDateTime; 14import java.util.Date; 15import java.util.List; 16 17/** 18 * <p> 19 * 用户表 前端控制器 20 * </p> 21 * 22 * @author xingcheng 23 * @since 2019-02-16 24 */ 25@RestController 26@RequestMapping("/user") 27public class UserController { 28 29 @Autowired 30 UserService userService; 31 32 33 @GetMapping("/list") 34 public Object list() { 35 List<User> list = userService.list(); 36 return list; 37 } 38 39 @GetMapping("/save/{name}") 40 public Object save(@PathVariable("name") String name) { 41 User user = new User(); 42 user.setCreateTime(LocalDateTime.now()); 43 user.setUpdateTime(LocalDateTime.now()); 44 user.setName(name); 45 boolean save = userService.save(user); 46 return save; 47 } 48 49 @GetMapping("/update/{name}") 50 public Object update(@PathVariable("name") String name) { 51 User user = new User(); 52 user.setId(1L); 53 user.setCreateTime(LocalDateTime.now()); 54 user.setUpdateTime(LocalDateTime.now()); 55 user.setName(name); 56 boolean update = userService.updateById(user); 57 return update; 58 } 59} 60

    路由策略接口和实现:

1package cn.chinotan.service; 2 3/** 4 * @program: test 5 * @description: 6 * @author: xingcheng 7 * @create: 2019-02-23 17:52 8 **/ 9public interface Strategy { 10 /** 11 * 传入一个需要分表的表名,返回一个处理后的表名 12 * Strategy必须包含一个无参构造器 13 * @param tableName 14 * @return 15 */ 16 String convert(String tableName); 17} 18 19 20package cn.chinotan.service.impl; 21 22import cn.chinotan.service.Strategy; 23import org.springframework.stereotype.Component; 24 25import java.text.SimpleDateFormat; 26import java.util.Date; 27 28/** 29 * @program: test 30 * @description: 31 * @author: xingcheng 32 * @create: 2019-02-23 17:52 33 **/ 34@Component("bak") 35public class BakStrategy implements Strategy { 36 37 @Override 38 public String convert(String tableName) { 39 StringBuilder sb=new StringBuilder(tableName); 40 sb.append("_bak"); 41 return sb.toString(); 42 } 43}

    接下来最重要的Interceptor实现:

1package cn.chinotan.interceptor; 2 3import cn.chinotan.aop.TableConfig; 4import cn.chinotan.service.Strategy; 5import org.apache.commons.lang3.StringUtils; 6import org.apache.ibatis.executor.statement.StatementHandler; 7import org.apache.ibatis.mapping.BoundSql; 8import org.apache.ibatis.mapping.MappedStatement; 9import org.apache.ibatis.plugin.*; 10import org.apache.ibatis.reflection.MetaObject; 11import org.apache.ibatis.reflection.SystemMetaObject; 12import org.slf4j.Logger; 13import org.slf4j.LoggerFactory; 14import org.springframework.beans.factory.annotation.Autowired; 15import org.springframework.stereotype.Component; 16 17import java.lang.reflect.ParameterizedType; 18import java.lang.reflect.Proxy; 19import java.lang.reflect.Type; 20import java.sql.Connection; 21import java.util.Map; 22import java.util.Properties; 23 24/** 25 * 完成插件签名: 26 * 告诉MyBatis当前插件用来拦截哪个对象的哪个方法 27 * type 指四大对象拦截哪个对象, 28 * method : 代表拦截哪个方法 ,在StatementHandler 中查看,需要拦截的方法 29 * args :代表参数 30 */ 31@Component 32@Intercepts({ 33 @Signature(type = StatementHandler.class, method = "prepare", args = { 34 Connection.class, Integer.class})}) 35public class ShareStatementPlugin implements Interceptor { 36 37 private static final Logger LOG = LoggerFactory.getLogger(ShareStatementPlugin.class); 38 39 @Autowired 40 private Map<String, Strategy> strategyMap; 41 42 @Override 43 public Object intercept(Invocation invocation) throws Throwable { 44 StatementHandler statementHandler = realTarget(invocation.getTarget()); 45 MetaObject metaObject = SystemMetaObject.forObject(statementHandler); 46 doTable(statementHandler, metaObject); 47 return invocation.proceed(); 48 } 49 50 private void doTable(StatementHandler handler, MetaObject metaStatementHandler) throws ClassNotFoundException { 51 BoundSql boundSql = handler.getBoundSql(); 52 String originalSql = boundSql.getSql(); 53 54 if (originalSql != null && !originalSql.equals("")) { 55 LOG.info("分表前的SQL:{}", originalSql); 56 MappedStatement mappedStatement = (MappedStatement) metaStatementHandler 57 .getValue("delegate.mappedStatement"); 58 String id = mappedStatement.getId(); 59 String className = id.substring(0, id.lastIndexOf(".")); 60 Class<?> classObj = Class.forName(className); 61 Class baseEntity = null; 62 Type[] interfacesTypes = classObj.getGenericInterfaces(); 63 for (Type type : interfacesTypes) { 64 if (type instanceof ParameterizedType) { 65 ParameterizedType interfacesType = (ParameterizedType) interfacesTypes[0]; 66 Type t = interfacesType.getActualTypeArguments()[0]; 67 baseEntity = (Class) t; 68 } 69 } 70 // 根据配置自动生成分表SQL 71 TableConfig tableConfig = classObj.getAnnotation(TableConfig.class); 72 // 获取表名 并进行相应转化 73 String tableName = baseEntity.getSimpleName().toLowerCase(); 74 if (StringUtils.isNotBlank(tableConfig.value())) { 75 tableName = tableConfig.value(); 76 } 77 78 if (tableConfig != null && tableConfig.split()) { 79 // 获取策略来处理 80 Strategy strategy = strategyMap.get(tableConfig.strategy()); 81 String convertedSql = originalSql.replaceAll(tableName, strategy.convert(tableName)); 82 metaStatementHandler.setValue("delegate.boundSql.sql", convertedSql); 83 LOG.info("分表后的SQL:{}", convertedSql); 84 } 85 } 86 } 87 88 @Override 89 public Object plugin(Object target) { 90 if (target instanceof StatementHandler) { 91 return Plugin.wrap(target, this); 92 } 93 return target; 94 } 95 96 @Override 97 public void setProperties(Properties properties) { 98 } 99 100 /** 101 * 获得真正的处理对象,可能多层代理 102 * 103 * @param target 104 * @param <T> 105 * @return 106 */ 107 public static <T> T realTarget(Object target) { 108 if (Proxy.isProxyClass(target.getClass())) { 109 MetaObject metaObject = SystemMetaObject.forObject(target); 110 return realTarget(metaObject.getValue("h.target")); 111 } 112 return (T) target; 113 } 114}

    效果:变更前两张表数据

执行save接口后:

可以看到只查询影子表,简单效果实现

下一步优化内容:

能够根据控制层传输过来的是否采用影子表标识来动态的进行影子表的读取和写入,而不是写死在代码中

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

MyBatis 面试题(附答案解析)

目录MyBatis的实现逻辑MyBatis的缓存实现逻辑{}和${}的区别是什么?MyBatis中自定义标签的执行原理简述Mapper接口的工作原理在Spring中Mapper接口是如何被注入的?在Mapper接口中是否可以有重载方法?当实体类中的属性名和表中的字

MyBatis拦截器实现分页

MyBatis分页拦截器实现前言    拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比