插件机制
mybatis采用责任链模式,通过动态代理组织多个插件,通过插件改变默认的sql的行为,myabtis允许通过插件来拦截四大对象:Executor、ParameterHandler、ResultSetHandler以及StatementHandler。
1、插件机制源码
//创建参数处理器
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
//创建ParameterHandler
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//插件在这里插入
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
//创建结果集处理器
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
//创建DefaultResultSetHandler(稍老一点的版本3.1是创建NestedResultSetHandler或者FastResultSetHandler)
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
//插件在这里插入
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
//创建语句处理器
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//创建路由选择语句处理器
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
//插件在这里插入
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
//产生执行器
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//判断使用的执行器类型
executorType = executorType == null ? defaultExecutorType : executorType;
//这句再做一下保护,囧,防止粗心大意的人将defaultExecutorType设成null?
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//然后就是简单的3个分支,产生3种执行器BatchExecutor/ReuseExecutor/SimpleExecutor
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//如果要求缓存,生成另一种CachingExecutor(默认就是有缓存),装饰者模式,所以默认都是返回CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//此处调用插件,通过插件可以改变Executor行为
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
每一个拦截器对目标类都进行一次代理
/**
*@target
*@return 层层代理后的对象
*/
public Object pluginAll(Object target) {
//循环调用每个Interceptor.plugin方法
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
Interceptor 接口说明
/**
* 拦截器接口
*
* @author Clinton Begin
*/
public interface Interceptor {
/**
* 执行拦截逻辑的方法
*
* @param invocation 调用信息
* @return 调用结果
* @throws Throwable 异常
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 代理类
*
* @param target
* @return
*/
Object plugin(Object target);
/**
* 根据配置来初始化 Interceptor 方法
* @param properties
*/
void setProperties(Properties properties);
}
注解拦截器并签名
@Intercepts(@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
))
参数说明:
type 要拦截四大对象的类型
method 拦截对象中哪个方法
args 需要传入的参数
2、手写分页插件
基于ThreadLocal传递分页参数,拦截StatementHandler
@Intercepts(@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
))
public class PagePlugin implements Interceptor {
// 插件的核心业务
@Override
public Object intercept(Invocation invocation) throws Throwable {
/**
* 1、拿到原始的sql语句
* 2、修改原始sql,增加分页 select * from t_user limit 0,3
* 3、执行jdbc去查询总数
*/
// 从invocation拿到我们StatementHandler对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 拿到原始的sql语句
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
// statementHandler 转成 metaObject
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// spring context.getBean("userBean")
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 获取mapper接口中的方法名称 selectUserByPage
String mapperMethodName = mappedStatement.getId();
if (mapperMethodName.matches(".*ByPage")) {
Page page = PageUtil.getPaingParam();
// select * from user;
String countSql = "select count(0) from (" + sql + ") a";
System.out.println("查询总数的sql : " + countSql);
// 执行jdbc操作
Connection connection = (Connection) invocation.getArgs()[0];
PreparedStatement countStatement = connection.prepareStatement(countSql);
ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
parameterHandler.setParameters(countStatement);
ResultSet rs = countStatement.executeQuery();
if (rs.next()) {
page.setTotalNumber(rs.getInt(1));
}
rs.close();
countStatement.close();
// 改造sql limit
String pageSql = this.generaterPageSql(sql, page);
System.out.println("分页sql:" + pageSql);
//将改造后的sql设置回去
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
// 把执行流程交给mybatis
return invocation.proceed();
}
// 把自定义的插件加入到mybatis中去执行
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 设置属性
@Override
public void setProperties(Properties properties) {
}
// 根据原始sql 生成 带limit sql
public String generaterPageSql(String sql, Page page) {
StringBuffer sb = new StringBuffer();
sb.append(sql);
sb.append(" limit " + page.getStartIndex() + " , " + page.getTotalSelect());
return sb.toString();
}
}
Data
@NoArgsConstructor
public class Page {
public Page(int currentPage,int pageSize){
this.currentPage=currentPage;
this.pageSize=pageSize;
}
private int totalNumber;// 当前表中总条目数量
private int currentPage;// 当前页的位置
private int totalPage; // 总页数
private int pageSize = 3;// 页面大小
private int startIndex; // 检索的起始位置
private int totalSelect;// 检索的总数目
public void setTotalNumber(int totalNumber) {
this.totalNumber = totalNumber;
// 计算
this.count();
}
public void count() {
int totalPageTemp = this.totalNumber / this.pageSize;
int plus = (this.totalNumber % this.pageSize) == 0 ? 0 : 1;
totalPageTemp = totalPageTemp + plus;
if (totalPageTemp <= 0) {
totalPageTemp = 1;
}
this.totalPage = totalPageTemp;// 总页数
if (this.totalPage < this.currentPage) {
this.currentPage = this.totalPage;
}
if (this.currentPage < 1) {
this.currentPage = 1;
}
this.startIndex = (this.currentPage - 1) * this.pageSize;// 起始位置等于之前所有页面输乘以页面大小
this.totalSelect = this.pageSize;// 检索数量等于页面大小
}
}
@Data
public class PageResponse<T> {
private int totalNumber;
private int currentPage;
private int totalPage;
private int pageSize = 3;
private T data;
}
public class PageUtil {
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
public static void setPagingParam(int offset, int limit) {
Page page = new Page(offset, limit);
LOCAL_PAGE.set(page);
}
public static void removePagingParam() {
LOCAL_PAGE.remove();
}
public static Page getPaingParam() {
return LOCAL_PAGE.get();
}
}
使用demo
PageUtil.setPagingParam(page,size);
List<TUser> tUsers = tUserDao.queryByPage();
Page pageInfo = PageUtil.getPaingParam();
PageResponse<List<TUser>> pageResponse = new PageResponse();
pageResponse.setData(tUsers);
pageResponse.setCurrentPage(pageInfo.getCurrentPage());
pageResponse.setPageSize(pageInfo.getPageSize());
pageResponse.setTotalNumber(pageInfo.getTotalNumber());
pageResponse.setTotalPage(pageInfo.getTotalPage());
PageUtil.removePagingParam();
return pageResponse;
相关配置
server:
port: 8085
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3339/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
hikari:
auto-commit: true #自动提交
connection-timeout: 30000 #等待连接池分配连接的最大时长
minimum-idle: 5 #最小连接数
maximum-pool-size: 20 #最大连接数
idle-timeout: 600000 #连接超时的最大时长(毫秒)
pool-name: DateSourceHikariCP
max-lifetime: 1800000 #连接的生命时长
connection-test-query: select 1
mybatis:
type-aliases-package: com.example.entity
mapper-locations: classpath:mapper/*.xml
config-location: classpath:mybatis.xml
3、手写插件实现读写分离
基于spring动态数据源和Theadlocal,拦截Executor
@Intercepts({// mybatis 执行流程
@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 })
})
@Slf4j
public class DynamicPlugin implements Interceptor {
private static final Map<String, String> cacheMap = new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
String dynamicDataSource = null;
if ((dynamicDataSource = cacheMap.get(ms.getId())) == null) {
// 读方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { // select * from user; update insert
// !selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
dynamicDataSource = "write";
} else {
// 负载均衡,针对多个读库
dynamicDataSource = "read";
}
} else {
dynamicDataSource = "write";
}
log.info("方法[{"+ms.getId()+"}] 使用了 [{"+dynamicDataSource+"}] 数据源, SqlCommandType [{"+ms.getSqlCommandType().name()+"}]..");
// 把id(方法名)和数据源存入map,下次命中后就直接执行
cacheMap.put(ms.getId(), dynamicDataSource);
}
// 设置当前线程使用的数据源
DynamicDataSourceHolder.putDataSource(dynamicDataSource);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
public final class DynamicDataSourceHolder {
// 使用ThreadLocal记录当前线程的数据源key
private static final ThreadLocal<String> holder = new ThreadLocal<String>();
public static void putDataSource(String name){
holder.set(name);
}
public static String getDataSource(){
return holder.get();
}
/**
* 清理数据源
*/
public static void clearDataSource() {
holder.remove();
}
}
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDataSource();
}
}
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.db01.jdbcUrl}")
private String db01Url;
@Value("${spring.datasource.db01.username}")
private String db01Username;
@Value("${spring.datasource.db01.password}")
private String db01Password;
@Value("${spring.datasource.db01.driverClassName}")
private String db01DiverClassName;
@Bean("dataSource01")
public DataSource dataSource01(){
HikariDataSource dataSource01 = new HikariDataSource();
dataSource01.setJdbcUrl(db01Url);
dataSource01.setDriverClassName(db01DiverClassName);
dataSource01.setUsername(db01Username);
dataSource01.setPassword(db01Password);
return dataSource01;
}
@Value("${spring.datasource.db02.jdbcUrl}")
private String db02Url;
@Value("${spring.datasource.db02.username}")
private String db02Username;
@Value("${spring.datasource.db02.password}")
private String db02Password;
@Value("${spring.datasource.db02.driverClassName}")
private String db02DiverClassName;
@Bean("dataSource02")
public DataSource dataSource02(){
HikariDataSource dataSource02 = new HikariDataSource();
dataSource02.setJdbcUrl(db02Url);
dataSource02.setDriverClassName(db02DiverClassName);
dataSource02.setUsername(db02Username);
dataSource02.setPassword(db02Password);
return dataSource02;
}
@Bean("multipleDataSource")
public DataSource multipleDataSource(@Qualifier("dataSource01") DataSource dataSource01,
@Qualifier("dataSource02") DataSource dataSource02) {
Map<Object, Object> datasources = new HashMap<Object, Object>();
datasources.put("write", dataSource01);
datasources.put("read", dataSource02);
DynamicDataSource multipleDataSource = new DynamicDataSource();
multipleDataSource.setDefaultTargetDataSource(dataSource01);
multipleDataSource.setTargetDataSources(datasources);
return multipleDataSource;
}
}
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDataSource();
}
}
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {
private static final long serialVersionUID = 1L;
public DynamicDataSourceTransactionManager(DataSource dataSource){
super(dataSource);
}
/**
* 只读事务到读库,读写事务到写库
*
* @param transaction
* @param definition
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
// 设置数据源
boolean readOnly = definition.isReadOnly();
if (readOnly) {
DynamicDataSourceHolder.putDataSource("read");
} else {
DynamicDataSourceHolder.putDataSource("write");
}
super.doBegin(transaction, definition);
}
/**
* 清理本地线程的数据源
*
* @param transaction
*/
@Override
protected void doCleanupAfterCompletion(Object transaction) {
super.doCleanupAfterCompletion(transaction);
DynamicDataSourceHolder.clearDataSource();
}
}
Configuration
@MapperScan("com.example.dao")
@EnableTransactionManagement
public class MybatisConfig implements TransactionManagementConfigurer {
private static String mybatisConfigPath = "mybatis-config.xml";
@Autowired
@Qualifier("multipleDataSource")
private DataSource multipleDataSource;
@Bean("sqlSessionFactoryBean")
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(multipleDataSource);
bean.setTypeAliasesPackage("com.example.entity");
bean.setConfigLocation(new ClassPathResource(mybatisConfigPath));
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
return bean.getObject();
}
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DynamicDataSourceTransactionManager(multipleDataSource);
}
}
mybatis二级缓存
mybatis默认开启一级缓存,一级缓存是sqlsession级别的,所以在实际场景中并没有什么用,Mybatis二级缓存默认关闭,使用方式如下: 1、在全局配置文件中加入
<settings>
<setting name="cacheEnabled" value="true" />
</settings>
2、在使用二级缓存的mapper.xml中加入
<mapper namespace="com.study.mybatis.mapper.UserMapper">
<!--开启本mapper的namespace下的二级缓存-->
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
</mapper>
<!--eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。
(1) LRU,最近最少使用的,一处最长时间不用的对象
(2) FIFO,先进先出,按对象进入缓存的顺序来移除他们
(3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象
(4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,移除最长时间不用的对形象
flushInterval:刷新间隔时间,单位为毫秒,这里配置的是100秒刷新,如果你不配置它,那么当
SQL被执行的时候才会去刷新缓存。
size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。
这里配置的是1024个对象
readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有办法修改缓存,他的默认值是false,不允许我们修改
-->
这样我们就实现了基于单机jvm内存的myabtis二级缓存,如果是分布式应用,可以引入myabtis-redis相关依赖,实现基于redis的分布式缓存
cache type="org.mybatis.caches.redis.RedisCache" />
也可以自定义缓存,myabtis为我们预留了Cache接口
mybatis自定义类型转换器
通过用于特殊字段的统一转换、敏感字段加密等,使用方式如下:
public class MyTypeHandler implements TypeHandler {
//private static String KEY = "123456";
/**
* 通过preparedStatement对象设置参数,将T类型的数据存入数据库。
*
* @param ps
* @param i
* @param parameter
* @param jdbcType
* @throws SQLException
*/
@Override
public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
try {
String encrypt = EncryptUtil.encode(((String) parameter).getBytes());
ps.setString(i, encrypt);
} catch (Exception e) {
e.printStackTrace();
}
}
// 通过列名或者下标来获取结果数据,也可以通过CallableStatement获取数据。
@Override
public Object getResult(ResultSet rs, String columnName) throws SQLException {
String result = rs.getString(columnName);
if (result != null && result != "") {
try {
return EncryptUtil.decode(result.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
@Override
public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
String result = rs.getString(columnIndex);
if (result != null && result != "") {
try {
return EncryptUtil.decode(result.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
@Override
public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
String result = cs.getString(columnIndex);
if (result != null && result != "") {
try {
return EncryptUtil.decode(result.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
}
public class EncryptUtil {
//base64 解码
public static String decode(byte[] bytes) {
return new String(Base64.decodeBase64(bytes));
}
//base64 编码
public static String encode(byte[] bytes) {
return new String(Base64.encodeBase64(bytes));
}
}
mybatis配置文件中引入类型转换器
<plugins>
<plugin interceptor="com.example.plugin.PagePlugin" >
<property name="type" value="mysql"/>
</plugin>
</plugins>
在需要使用的字段中指定类型转换器
<resultMap id="resultListUser" type="com.example.entity.User" >
<result column="password" property="password" typeHandler="com.example.typehandler.MyTypeHandler" />
</resultMap>
<update id="updateUser" parameterType="com.example.entity.User">
UPDATE user userName=#{userName typeHandler="com.example.typehandler.MyTypeHandler"} WHERE id=#{id}
</update>
完整mybatis配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局配置 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" />
<!--这个配置使全局的映射器(二级缓存)启用或禁用缓存-->
<setting name="cacheEnabled" value="true" />
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 自定义类型处理器-->
<typeHandlers>
<typeHandler handler="com.study.mybatis.handler.AllenTypeHandle" />
</typeHandlers>
<!-- 插件 -->
<plugins>
<plugin interceptor="com.example.plugin.PagePlugin" >
<property name="type" value="mysql"/>
</plugin>
</plugins>
</configuration>