首先进行Mybatis 拦截器介绍
拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。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配置文件中指定一些属性的。
定义自己的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文件如下:
<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接口后:



可以看到只查询影子表,简单效果实现
下一步优化内容:
能够根据控制层传输过来的是否采用影子表标识来动态的进行影子表的读取和写入,而不是写死在代码中
