mybatis第五话 - mybatis情同手足的插件之TypeHandler和Interceptor

22 篇文章 0 订阅
13 篇文章 0 订阅

源码分析完了,其实还有很多附加的插件没有分析了,因为这些给开发者实现用的,所以还是先学会用再看源码了。

今天主要两个点:

  1. TypeHandler入参出参实现使用
  2. Interceptor插件实现使用

1. TypeHandler

顾名思义,类型处理器,看下该接口的类

public interface TypeHandler<T> {
  //入参
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
  //出参 根据列名
  T getResult(ResultSet rs, String columnName) throws SQLException;
  //出参 根据列下标
  T getResult(ResultSet rs, int columnIndex) throws SQLException;
 
  T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}

如果不想关心出参和入参数据的判断,可以直接使用BaseTypeHandler

1.1 为什么要有类型处理器

因为mysql是对N种开发语言的,mysql中的类型需要转义到对应语言的实现,我们来看下java中是怎么映射的,ArrayTypeHandler类中

STANDARD_MAPPING = new ConcurrentHashMap<>();
STANDARD_MAPPING.put(BigDecimal.class, JdbcType.NUMERIC.name());
STANDARD_MAPPING.put(BigInteger.class, JdbcType.BIGINT.name());
STANDARD_MAPPING.put(boolean.class, JdbcType.BOOLEAN.name());
STANDARD_MAPPING.put(Boolean.class, JdbcType.BOOLEAN.name());
STANDARD_MAPPING.put(byte[].class, JdbcType.VARBINARY.name());
STANDARD_MAPPING.put(byte.class, JdbcType.TINYINT.name());
STANDARD_MAPPING.put(Byte.class, JdbcType.TINYINT.name());
STANDARD_MAPPING.put(Calendar.class, JdbcType.TIMESTAMP.name());
STANDARD_MAPPING.put(java.sql.Date.class, JdbcType.DATE.name());
STANDARD_MAPPING.put(java.util.Date.class, JdbcType.TIMESTAMP.name());
STANDARD_MAPPING.put(double.class, JdbcType.DOUBLE.name());
STANDARD_MAPPING.put(Double.class, JdbcType.DOUBLE.name());
STANDARD_MAPPING.put(float.class, JdbcType.REAL.name());
STANDARD_MAPPING.put(Float.class, JdbcType.REAL.name());
STANDARD_MAPPING.put(int.class, JdbcType.INTEGER.name());
STANDARD_MAPPING.put(Integer.class, JdbcType.INTEGER.name());
STANDARD_MAPPING.put(LocalDate.class, JdbcType.DATE.name());
STANDARD_MAPPING.put(LocalDateTime.class, JdbcType.TIMESTAMP.name());
STANDARD_MAPPING.put(LocalTime.class, JdbcType.TIME.name());
STANDARD_MAPPING.put(long.class, JdbcType.BIGINT.name());
STANDARD_MAPPING.put(Long.class, JdbcType.BIGINT.name());
STANDARD_MAPPING.put(OffsetDateTime.class, JdbcType.TIMESTAMP_WITH_TIMEZONE.name());
STANDARD_MAPPING.put(OffsetTime.class, JdbcType.TIME_WITH_TIMEZONE.name());
STANDARD_MAPPING.put(Short.class, JdbcType.SMALLINT.name());
STANDARD_MAPPING.put(String.class, JdbcType.VARCHAR.name());
STANDARD_MAPPING.put(Time.class, JdbcType.TIME.name());
STANDARD_MAPPING.put(Timestamp.class, JdbcType.TIMESTAMP.name());
STANDARD_MAPPING.put(URL.class, JdbcType.DATALINK.name());

从上述映射的内容中,基本上能看到我们日常所用的基本上都有映射了。
但是如果是映射内容之外的呢?

1.2 java中使用TypeHandler

使用由来,在一个小说的系统中,需要记录浏览小说名称的先后顺序,可以供后期大数据分析使用。
表字段以及数据:

CREATE TABLE `user_see_books` (
	`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(50) NULL DEFAULT NULL COMMENT '名称',
	`age` INT(11) NULL DEFAULT NULL COMMENT '年龄',
	`status` INT(11) NULL DEFAULT '1' COMMENT '状态',
	`book_names` VARCHAR(50) NULL DEFAULT NULL COMMENT '阅读书名集合',
	`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
	`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

INSERT INTO `user_see_books` (`name`, `age`, `status`, `book_names`, `create_time`, `update_time`) VALUES ('bkid_1646031712018', 19, 1, '斗罗大陆, 遮天, 斗破苍穹', '2022-02-17 16:45:01', '2022-03-07 17:33:12');

但是再bean中需要返回list数组,于是自定义了一个TypeHandler实现类

//定义处理的泛型为List<String>,这里就处理入参和出参之间的转换了
@Slf4j
@Component
public class MyTypeHandler implements TypeHandler<List<String>> {

    @Override
    public void setParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
        log.info("set ========= " + parameter);
        //这里采用偷懒的写法了
        ps.setString(i,parameter.toString().replaceAll("\\[|\\]",""));
    }
    
    @Override
    public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
        log.info("getResult == String");
        String string = rs.getString(columnName);
        //出参处理
        List<String> list = Arrays.asList(string.split(","));
        return list;
    }

    @Override
    public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
        log.info("getResult == columnIndex");
        return null;
    }

    @Override
    public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        log.info("getResult == columnIndex");
        return null;
    }
}

1.3 测试代码

基于springboot 2.5.6版本,maven就不贴了,查询类

@GetMapping("/data/select")
public Object select() {
    Map<String, Object> map = new HashMap<>();
    map.put("bookNames", Arrays.asList(1, 2, 3));
    List<Map<String, Object>> mapList = testMapper.select(map);
    return mapList;
}

xml文件编写

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
		PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
		"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
	<mapper namespace="com.example.demo.mapper.test.TestMapper">
	<!--出参处理-->
	<resultMap id="userMap" type="Map">
			<result column="book_names" property="bookNames" typeHandler="com.example.demo.plugin.MyTypeHandler"/>
		</resultMap>
	
	<select id="select" parameterType="Map" resultMap="userMap">
		select * from user_see_books
		<where>
			<if test="bookNames != null">
			<!--入参处理-->
				and book_names= #{bookNames,typeHandler=com.example.demo.plugin.MyTypeHandler}
			</if>
		</where>
	</select>
</mapper>

调用/data/select,查看打印日志和输出结果

Preparing: select * from user_see_books WHERE  book_names = ?
set ========= [斗罗大陆, 遮天, 斗破苍穹]
Parameters:  斗罗大陆, 遮天, 斗破苍穹(String)
[{
	"update_time": "2022-03-07T17:42:54",
	"create_time": "2022-02-17T16:45:01",
	"bookNames": ["斗罗大陆", " 遮天", " 斗破苍穹"],
	"name": "bkid_1646031712018",
	"id": 1,
	"age": 19,
	"status": 1
}]

需要注意的点
xml中入参写法别忘记,#{bookNames,typeHandler=com.example.demo.plugin.MyTypeHandler}
xml中出参映射resultMap不能忘,<result column="book_names" property="bookNames" typeHandler="com.example.demo.plugin.MyTypeHandler"/>

上面仅为写文章搭建出来的,真实情况根据需求来实现

2. Interceptor拦截插件

在很多业务中,我们可能需要去拦截执行的sql,达到不修改原有的代码业务去处理一些东西。例如:分页操作,数据权限,sql执行次数和时长等待,这时就可以用到这个Interceptor拦截器了

2.1 再次回顾一下Mybaits的核心对象

Configuration 初始化基础配置,一些重要的类型对象,例如:插件,映射器,factory工厂,typeHandler对象等等。该类贯穿全局
SqlSessionFactory SqlSession工厂
SqlSession 顶层工作API,和数据库交互,完成CRUD功能
Executor 执行器,是mybatis调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler 封装了JDBC Statement操作,负责对应操作,例如:设置参数,结果集转出
ParameterHandler 参数转换处理
MapperedStatement 对执行sql节点的封装
SqlSource 动态生成sql语句,并封装到BoundSql中
BoundSql 动态生成的sql和参数的封装
ResultSetHandler 结果集处理,将JDBC类型转换成Java数据类型
TypeHandler 类型转换器,可自定义实现

2.2 拦截器的作用

Mybatis支持对Executor、StatementHandler、ParmeterHandler和ResultSetHandler接口进行拦截,也就是会对这几个接口进行代理。

2.3 java中实现拦截器

本文主要分析执行器Executor#query拦截

@Slf4j
@Component
@Intercepts(value = {@Signature(type = Executor.class, method = "query",
		//arg对应的参数数组
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MyPagePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("进入插件 ================");

        Object target = invocation.getTarget();
        log.info("原始类来源:{}", target.getClass().getName());
        Object[] args = invocation.getArgs();

        MappedStatement st = (MappedStatement) args[0];
        BoundSql sql = st.getBoundSql(args[1]);
        log.info("执行SQL:" + sql.getSql());

        Object arg = args[1];
        log.info("传入参数:{}", arg);
        
        return invocation.proceed();
    }
}

2.4 拦截器测试

还是上面的调用代码,查看一下打印日志

进入插件 ================
原始类来源:org.apache.ibatis.executor.CachingExecutor
执行SQL:select * from user_see_books WHERE  book_names = ?
传入参数:{bookNames=[斗罗大陆, 遮天, 斗破苍穹]}

以后再有sql报错找不到sql的,自己也可以实现了。
其他的功能就不多分析了,后面会有专门的插件拦截器pagehelper使用以及源码分析
别人已经有了这些支持了,那就好好的去把它用起来,不要造重复的轮子。

以上就是本文的全部内容了

上一篇:mybatis第四话 - 让我们一层一层来剥开mybatis的心,源码分析
下一篇:mybatis第六话 - mybatis插件篇之pagehelper的使用

盛年不重来,一日难再晨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值