Mybatis 拦截器 说明和使用 (二)
Mybatis 拦截器使用
- 一.处理器类简介
- 1.主要类梳理
- 2.处理器接口
- 3.简要调用流程(仅做参考,不标准)
- 二.拦截器应用
- 1.环境准备
- 1.依赖包(多环境打包可忽略)
- 2.配置文件(访问的本地 Postgres 数据库)
- 3.启动类
- 4.数据库连接池配置
- 5.MVC 层代码
- 6.Map 接口和实现
- 7.表结构和数据
- 2.拦截器配置方式
- 1.Spring注解
- 2.手动注册
- 3.顺序问题
- 3.拦截器处理Map
- 4.为不同SQL动态设置超时
一.处理器类简介
1.主要类梳理
类 | 作用 |
---|---|
org.apache.ibatis.session.Configuration | 数据源配置信息、插件、映射器等 |
org.apache.ibatis.session.SqlSession | 顶层API,数据库会话类 |
org.apache.ibatis.session.SqlSessionFactory | SqlSession工厂 |
org.apache.ibatis.type.TypeHandler | JdbcType 和 JavaType 映射 |
org.apache.ibatis.mapping.BoundSql | 动态生成 SQL 的封装类 |
org.apache.ibatis.mapping.MappedStatement | xml 标签配置信息封装对象 |
2.处理器接口
处理器接口 | 默认实现 | 方法 |
---|---|---|
ParameterHandler | DefaultParameterHandler | getParameterObject() setParameters(PreparedStatement ps) |
ResultSetHandler | DefaultResultSetHandler | handleResultSets(Statement stmt) handleCursorResultSets(Statement stmt) handleOutputParameters(CallableStatement cs) |
StatementHandler | BaseStatementHandler CallableStatementHandler PreparedStatementHandler RoutingStatementHandler SimpleStatementHandler | prepare(Connection connection, Integer transactionTimeout) parameterize(Statement statement) batch(Statement statement) update(Statement statement) query(Statement statement, ResultHandler resultHandler) queryCursor(Statement statement) getBoundSql() getParameterHandler() |
Executor | BaseExecutor BatchExecutor CachingExecutor ClosedExecutor ReuseExecutor SimpleExecutor | update(MappedStatement ms, Object parameter) query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) flushStatements() commit(boolean required) rollback(boolean required) createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) isCached(MappedStatement ms, CacheKey key) clearLocalCache() deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) getTransaction() close(boolean forceRollback) isClosed() setExecutorWrapper(Executor executor) |
3.简要调用流程(仅做参考,不标准)
SimpleExecutor -> newStatementHandler -> RoutingStatementHandler -> SimpleStatementHandler -> newParameterHandler -> newResultSetHandler
二.拦截器应用
1.环境准备
1.依赖包(多环境打包可忽略)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!--配置阿里云依赖包和插件仓库-->
<repositories>
<repository>
<id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>aliyun-plugin</id>
<url>https://maven.aliyun.com/nexus/content/groups/public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.11</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
</dependencies>
<!-- 多环境配置 -->
<profiles>
<profile>
<id>test</id>
<properties>
<properties.active>test</properties.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>dev</id>
<properties>
<properties.active>dev</properties.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<properties.active>prod</properties.active>
</properties>
</profile>
</profiles>
<build>
<finalName>demo</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.4</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<!--打包的时候先去掉所有的配置文件-->
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>application*.yml</exclude>
</excludes>
</resource>
<!--添加需要包含的文件-->
<resource>
<directory>src/main/resources</directory>
<!-- 是否替换yml或者properties里@xx@表示的maven properties属性值 -->
<filtering>true</filtering>
<!--在打包的时候,根据-P参数,加上需要的yml配置文件-->
<includes>
<include>application.yml</include>
<include>application-${properties.active}.yml</include>
</includes>
</resource>
</resources>
</build>
</project>
2.配置文件(访问的本地 Postgres 数据库)
server:
port: 8093
spring:
application:
name: moon
datasource:
druid:
username: postgres
password:
url: jdbc:postgresql://127.0.0.1:5432/wiki_animal_db
driverClassName: org.postgresql.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
validation-query: select version()
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,wall用于防火墙
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall
use-global-data-source-stat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#mybatis配置
mybatis:
mapper-locations: classpath:/mapper/*.xml #修改为对应的mapper文件路径
#驼峰命名
configuration:
map-underscore-to-camel-case: true
#打印sql
logging:
level:
com.demo: info
3.启动类
package com.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author
* @date 2022-10-13 19:22
* @since 1.8
*/
@MapperScan("com.demo.mapper")
@SpringBootApplication
public class DemoApplication {
/**
* 主类
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class,args);
}
}
4.数据库连接池配置
package com.demo.confin;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
import java.util.Properties;
/**
* @author
* @date 2022-10-14 16:29
* @since 1.8
*/
@Slf4j
@Component
public class AnimalDataSource {
/**
* 获取配置
* @return
*/
@Bean(name = "druidProperties")
@ConfigurationProperties(prefix = "spring.datasource")
public Properties druidProperties(){
return new Properties();
}
/**
* @description: postgres dataSource
* @params: [properties]
* @return: com.alibaba.druid.pool.DruidDataSource
* @create: 2022-08-18
*/
@Primary
@Bean(name = "druidDataSource")
public DruidDataSource druidDataSource(@Qualifier("druidProperties")Properties properties){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.configFromPropety(properties);
try {
druidDataSource.init();
} catch (SQLException e) {
log.error("Animal Datasource Init Exception:",e);
}
return druidDataSource;
}
}
5.MVC 层代码
package com.demo.controller;
import com.demo.entity.AnimalEntity;
import com.demo.service.AnimalService;
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;
/**
* @author
* @date 2022-10-17 19:48
* @since 1.8
*/
@RestController
@RequestMapping("/animal")
public class AnimalController {
@Autowired
AnimalService animalService;
@GetMapping("/query")
public List<AnimalEntity>query() {
return animalService.query();
}
@GetMapping("/queryByName/{name}")
public Map<Integer, String> query(@PathVariable("name") String name) {
return animalService.queryByName(name);
}
@GetMapping("/queryByCountryAndName")
public AnimalEntity queryByCountryAndName(String name,String country) {
return animalService.queryByCountryAndName(name,country);
}
}
package com.demo.service;
import com.demo.entity.AnimalEntity;
import java.util.List;
import java.util.Map;
/**
* @author
* @date 2022-10-17 19:48
* @since 1.8
*/
public interface AnimalService {
/**
* 查询
* @return
*/
List<AnimalEntity> query();
/**
* 按名字查询
* @param name
* @return
*/
Map<Integer, String> queryByName(String name);
/**
* 按国家和名字查询
* @param name
* @param country
* @return
*/
AnimalEntity queryByCountryAndName(String name,String country);
}
package com.demo.service.impl;
import com.demo.entity.AnimalEntity;
import com.demo.mapper.AnimalMapper;
import com.demo.service.AnimalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* @author
* @date 2022-10-17 19:50
* @since 1.8
*/
@Service
public class AnimalServiceImpl implements AnimalService {
@Autowired
AnimalMapper animalMapper;
@Override
public List<AnimalEntity> query() {
return animalMapper.query();
}
@Override
public Map<Integer, String> queryByName(String name) {
return animalMapper.queryByName(name).get(0);
}
@Override
public AnimalEntity queryByCountryAndName(String name,String country){
return animalMapper.queryByCountryAndName(name,country);
}
}
6.Map 接口和实现
package com.demo.mapper;
import com.demo.entity.AnimalEntity;
import java.util.List;
import java.util.Map;
/**
* @author
* @date 2022-10-17 19:50
* @since 1.8
*/
public interface AnimalMapper {
/**
* 查询
* @return
*/
List<AnimalEntity> query();
/**
* 查名字
* @param name
* @return
*/
List<Map<Integer, String>> queryByName(String name);
/**
* 按国家和名字
* @param name
* @param country
* @return
*/
AnimalEntity queryByCountryAndName(String name,String country);
}
<?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.demo.mapper.AnimalMapper">
<resultMap id="rabbit" type="com.demo.entity.AnimalEntity">
<result column="id" property="id"/>
<result column="animal_name" property="name"/>
<result column="country" property="country"/>
</resultMap>
<select id="query" resultMap="rabbit">
select id,animal_name,country from public.tb_rabbit_basic
</select>
<select id="queryByName" resultMap="rabbit">
select id,animal_name,country from public.tb_rabbit_basic where animal_name like '%'||#{name}||'%'
</select>
<select id="queryByCountryAndName" resultMap="rabbit">
select id,animal_name,country from public.tb_rabbit_basic where animal_name = #{name} and country = #{country}
</select>
</mapper>
7.表结构和数据
-- public.tb_rabbit_basic definition
-- Drop table
-- DROP TABLE public.tb_rabbit_basic;
CREATE TABLE public.tb_rabbit_basic (
id int4 NULL,
animal_name varchar NULL,
country varchar NULL
);
INSERT INTO public.tb_rabbit_basic (id,animal_name,country) VALUES
(1,'安哥拉兔子','安哥拉'),
(2,'道奇兔','荷兰'),
(3,'多瓦夫兔','德国'),
(4,'巨型花明兔','比利时');
2.拦截器配置方式
1.Spring注解
定义拦截器类,在 Executor 阶段拦截了 query 方法
package com.demo.confin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import java.util.Properties;
/**
* @author
* @date 2022-10-21 23:09
* @since 1.8
*/
@Slf4j
@Component
@Intercepts({
@Signature(method = "query",type = Executor.class,args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class AnimalInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
log.info("进入拦截器 Mapper.xml Method ID Is:{}",mappedStatement.getId());
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
启动服务并调用
日志:
2022-10-21 23:14:25.008 DEBUG 15240 --- [ main] com.demo.DemoApplication : Running with Spring Boot v2.7.4, Spring v5.3.23
2022-10-21 23:14:25.009 INFO 15240 --- [ main] com.demo.DemoApplication : The following 1 profile is active: "dev"
2022-10-21 23:14:26.297 INFO 15240 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8093 (http)
2022-10-21 23:14:26.308 INFO 15240 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-10-21 23:14:26.308 INFO 15240 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-10-21 23:14:26.480 INFO 15240 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-10-21 23:14:26.481 INFO 15240 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1420 ms
2022-10-21 23:14:26.764 INFO 15240 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2022-10-21 23:14:27.298 INFO 15240 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8093 (http) with context path ''
2022-10-21 23:14:27.308 INFO 15240 --- [ main] com.demo.DemoApplication : Started DemoApplication in 2.796 seconds (JVM running for 3.222)
2022-10-21 23:14:38.102 INFO 15240 --- [nio-8093-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-10-21 23:14:38.103 INFO 15240 --- [nio-8093-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-10-21 23:14:38.103 INFO 15240 --- [nio-8093-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
2022-10-21 23:14:38.141 INFO 15240 --- [nio-8093-exec-2] com.demo.confin.AnimalInterceptor : 进入拦截器 Mapper.xml Method ID Is:com.demo.mapper.AnimalMapper.query
2022-10-21 23:14:38.581 DEBUG 15240 --- [nio-8093-exec-2] com.demo.mapper.AnimalMapper.query : ==> Preparing: select id,animal_name,country from public.tb_rabbit_basic
2022-10-21 23:14:38.712 DEBUG 15240 --- [nio-8093-exec-2] com.demo.mapper.AnimalMapper.query : ==> Parameters:
2022-10-21 23:14:38.728 DEBUG 15240 --- [nio-8093-exec-2] com.demo.mapper.AnimalMapper.query : <== Total: 4
2.手动注册
取消上面自定义拦截器的初始化注解:@Component;并通过程序手动注册,如果多拦截器的情况下,可以取消所有自动注入,并统一手动处理,PageHelper 分页插件失效或者自定义拦截器不生效,多数情况即为注入顺序导致功能相互覆盖引起的
新增手动注入拦截器类
package com.demo.confin;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author
* @date 2022-10-21 23:19
* @since 1.8
*/
@Component
public class MybatisInterceptorListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
/**
* 控制注册自定义拦截器顺序
* 1.避免被PageHelper覆盖(暂不涉及)
* 2.避免循环依赖
* @param event the event to respond to
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
for (SqlSessionFactory factory : sqlSessionFactoryList) {
factory.getConfiguration().addInterceptor(new AnimalInterceptor());
}
}
}
重启服务并调用接口
拦截器生效
3.顺序问题
同一类型的拦截类,处理时才会有顺序问题,Mybatis 四种类型直接的顺序关系是固定的,也即,如果多个拦截器都是拦截 Executor 的同一 query 方法,才会有注册覆盖现象;不会出现Executor 的拦截覆盖了 StatementHandler 的拦截,但是在应用中还是要谨慎注意处理时是否受到"顺序"影响,或者其他处理冲突
实际观察一下这四个类的处理顺序:Executor、ParameterHandler、StatementHandler、ResultSetHandler
分别定义四个拦截器,并乱序注入
package com.demo.confin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
/**
* @author
* @date 2022-10-21 23:35
* @since 1.8
*/
@Slf4j
@Intercepts({
@Signature(method = "query",type = Executor.class,args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class OneInterceptor implements Interceptor {
/**
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("Proxy {} Target {}",this.getClass().getName(),invocation.getTarget().getClass());
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
package com.demo.confin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import java.sql.PreparedStatement;
import java.util.Properties;
/**
* @author
* @date 2022-10-21 23:13
* @since 1.8
*/
@Slf4j
@Intercepts({
@Signature(method = "setParameters",type = ParameterHandler.class,args = {PreparedStatement.class})
})
public class TwoInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("Proxy {} Target {}",this.getClass().getName(),invocation.getTarget().getClass());
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
package com.demo.confin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import java.sql.Statement;
import java.util.Properties;
/**
* @author
* @date 2022-10-21 23:53
* @since 1.8
*/
@Slf4j
@Intercepts({
@Signature(method = "query", type = StatementHandler.class, args = {Statement.class, ResultHandler.class})
})
public class ThreeInterceptor implements Interceptor {
/**
* mapper.xml 不可以重载,会取包加方法名作为
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("Proxy {} Target {}",this.getClass().getName(),invocation.getTarget().getClass());
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
package com.demo.confin;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import java.sql.Statement;
import java.util.Properties;
/**
* @author
* @date 2022-10-21 23:53
* @since 1.8
*/
@Slf4j
@Intercepts({
@Signature(method = "handleResultSets", type = ResultSetHandler.class,args = {Statement.class})
})
public class FourInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("Proxy {} Target {}",this.getClass().getName(),invocation.getTarget().getClass());
Object result = invocation.proceed();
//TODO 对结果对象进行加工
return result;
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
注册顺序:Executor、ResultSetHandler、ParameterHandler、StatementHandler
package com.demo.confin;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author
* @date 2022-10-21 23:19
* @since 1.8
*/
@Component
public class MybatisInterceptorListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
/**
* @param event the event to respond to
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
for (SqlSessionFactory factory : sqlSessionFactoryList) {
factory.getConfiguration().addInterceptor(new OneInterceptor());
factory.getConfiguration().addInterceptor(new FourInterceptor());
factory.getConfiguration().addInterceptor(new TwoInterceptor());
factory.getConfiguration().addInterceptor(new ThreeInterceptor());
}
}
}
启动服务并调用接口,查看打印结果:127.0.0.1:8093/animal/query
是按1,2,3,4调用的:
日志内打印了被代理类:CachingExecutor、DefaultParameterHandler、RoutingStatementHandler、DefaultResultSetHandler
2022-10-21 23:36:33.909 INFO 26032 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8093 (http) with context path ''
2022-10-21 23:36:33.924 INFO 26032 --- [ main] com.demo.DemoApplication : Started DemoApplication in 2.657 seconds (JVM running for 3.071)
2022-10-21 23:36:41.437 INFO 26032 --- [nio-8093-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-10-21 23:36:41.437 INFO 26032 --- [nio-8093-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-10-21 23:36:41.437 INFO 26032 --- [nio-8093-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
2022-10-21 23:36:41.475 INFO 26032 --- [nio-8093-exec-2] com.demo.confin.OneInterceptor : Proxy com.demo.confin.OneInterceptor Target class org.apache.ibatis.executor.CachingExecutor
2022-10-21 23:36:41.930 DEBUG 26032 --- [nio-8093-exec-2] com.demo.mapper.AnimalMapper.query : ==> Preparing: select id,animal_name,country from public.tb_rabbit_basic
2022-10-21 23:36:42.070 INFO 26032 --- [nio-8093-exec-2] com.demo.confin.TwoInterceptor : Proxy com.demo.confin.TwoInterceptor Target class org.apache.ibatis.scripting.defaults.DefaultParameterHandler
2022-10-21 23:36:42.071 INFO 26032 --- [nio-8093-exec-2] com.demo.confin.ThreeInterceptor : Proxy com.demo.confin.ThreeInterceptor Target class org.apache.ibatis.executor.statement.RoutingStatementHandler
2022-10-21 23:36:42.071 DEBUG 26032 --- [nio-8093-exec-2] com.demo.mapper.AnimalMapper.query : ==> Parameters:
2022-10-21 23:36:42.075 INFO 26032 --- [nio-8093-exec-2] com.demo.confin.FourInterceptor : Proxy com.demo.confin.FourInterceptor Target class org.apache.ibatis.executor.resultset.DefaultResultSetHandler
2022-10-21 23:36:42.088 DEBUG 26032 --- [nio-8093-exec-2] com.demo.mapper.AnimalMapper.query : <== Total: 4
3.拦截器处理Map
从上面 Executor 类的 query 方法我们可以看到一个 ResultHandler 结果处理类,实现此接口就能自定义数据处理方式,我们尝试一下将查询结果转为 Map 处理
自定义Handler,就是取出所需属性值填充到Map,再用List封装一下
package com.demo.confin.result;
import com.demo.entity.AnimalEntity;
import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author
* @date 2022-10-21 23:24
* @since 1.8
*/
public class MapResultHandler implements ResultHandler<AnimalEntity> {
/**
* 定义一个结果:因为如果返回数据为多条,DefaultSqlSession 会自动按List处理,避免报错需要包装一下
*/
List<Map<Integer, String>> list = new ArrayList<>(1);
/**
* 初始化MAP
*/
Map<Integer, String> map = new HashMap<>(16);
@Override
public void handleResult(ResultContext<? extends AnimalEntity> resultContext) {
map.put(resultContext.getResultObject().getId(),resultContext.getResultObject().getName());
}
public Object getResult(){
list.add(map);
return list;
}
}
改造一下 OneInterceptor 仅对指定查询处理,并取消其他三个拦截器的注册
package com.demo.confin;
import com.demo.confin.result.MapResultHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
/**
* @author
* @date 2022-10-21 23:35
* @since 1.8
*/
@Slf4j
@Intercepts({
@Signature(method = "query",type = Executor.class,args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class OneInterceptor implements Interceptor {
/**
* 可以设为有参构造,并在舒适化时传入一些参数;
* 如果用依赖诸如的方式则要注意注入顺序
*/
public OneInterceptor(){
}
/**
*
* 取出 MappedStatement 并根据 ID 过滤我们要处理的查询方法(全部处理则无需过滤)
* mapper.xml 不可重载,ID 即包名.类名.方法名,MappedStatement 即 XML 配置文件内某个 SQL 标签的封装类
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("Proxy {} Target {}",this.getClass().getName(),invocation.getTarget().getClass());
//我们签名的方法第一个参数为MappedStatement
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String id = mappedStatement.getId();
//比如只处理按名字查询
String queryByName = "com.demo.mapper.AnimalMapper.queryByName";
if (queryByName.equals(id)){
//修改参数
invocation.getArgs()[1] = "兔";
//添加自定义处理器
MapResultHandler mapResultHandler = new MapResultHandler();
invocation.getArgs()[3] = mapResultHandler;
//调用
invocation.proceed();
//返回
return mapResultHandler.getResult();
}
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
启动服务,并调用接口(顺便测试参数修改):127.0.0.1:8093/animal/queryByName/测试
4.为不同SQL动态设置超时
package com.demo.confin;
import com.alibaba.druid.pool.DruidPooledPreparedStatement;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import java.sql.Statement;
import java.util.Map;
import java.util.Properties;
/**
* @author
* @date 2022-10-21 23:49
* @since 1.8
*/
@Slf4j
@Intercepts({
@Signature(method = "query", type = StatementHandler.class, args = {Statement.class, ResultHandler.class})
})
public class DynamicSetSqlTimeOutInterceptor implements Interceptor {
/**
* TODO
* 1.定义 Filed,如超时值
* 2.通过依赖注入、有参构造初始化 Filed
*/
/**
* 附加超时
*/
private int addedTime;
/**
* 构造初始化
* @param addedTime
*/
public DynamicSetSqlTimeOutInterceptor(int addedTime){
this.addedTime = addedTime;
}
/**
* 调用存储过程时获取参数的超时配置并重置到SQL配置里
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取连接信息
DruidPooledPreparedStatement statement = (DruidPooledPreparedStatement) invocation.getArgs()[0];
//TODO 通过SQL内是否包含某些特殊字符判断是否需要动态设置超时
if (statement.getSql().startsWith("")){
//取参数
RoutingStatementHandler target = (RoutingStatementHandler)invocation.getTarget();
BoundSql boundSql = target.getBoundSql();
Map param = (Map) boundSql.getParameterObject();
//TODO 获取超时参数(通过SQL参数传递)
try {
int timeOut = (int) param.get("sqlTimeOut");
//设置超时
statement.setQueryTimeout(timeOut + addedTime);
//TODO 缓存超时信息并通过 close 或 cancel 方法主动取消查询 statement statement.close(); statement.cancel();
Object proceed = invocation.proceed();
return proceed;
} finally {
//TODO 收尾处理
log.info("处理完成...");
}
}
//非存储过程调用直接返回
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
此处不在演示效果,可通过慢查询或调用存储过程设置超时,测试效果