MyBatis拦截器的原理与使用

网友投稿 364 2022-10-19


MyBatis拦截器的原理与使用

目录二、拦截器注册的三种方式        1.XML注册        2.配置类注册        3.注解方式三、ParameterHandler参数改写-修改时间和修改人统一插入四、通过StatementHandler改写SQL

一、拦截对象和接口实现示例

MyBatis拦截器的作用是在于Dao到DB中间进行额外的处理。大部分情况下通过mybatis的xml配置sql都可以达到想要的DB操作效果,然而存在一些类似或者相同的查询条件或者查询要求,这些可以通过拦截器的实现可以提升开发效率,比如:分页、插入和更新时间/人、数据权限、SQL监控日志等。

Mybatis支持四种对象拦截Executor、StatementHandler、PameterHandler和ResultSetHandler

Executor:拦截执行器的方法。

StatementHandler:拦截Sql语法构建的处理。

ParameterHandler:拦截参数的处理。

ResultHandler:拦截结果集的处理。

public interface Executor {

ResultHandler NO_RESULT_HANDLER = null;

int update(MappedStatement var1, Object var2) throws SQLException;

List query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;

List query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;

Cursor queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;

List flushStatements() throws SQLException;

void commit(boolean var1) throws SQLException;

void rollback(boolean var1) throws SQLException;

CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);

boolean isCached(MappedStatement var1, CacheKey var2);

void clearLocalCache();

void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class> var5);

Transaction getTransaction();

void close(boolean var1);

boolean isClosed();

void setExecutorWrapper(Executor var1);

}

public interface StatementHandler {

Statement prepare(Connection var1, Integer var2) throws SQLException;

void parameterize(Statement var1) throws SQLException;

void batch(Statement var1) throws SQLException;

int update(Statement var1) throws SQLException;

List query(Shttp://tatement var1, ResultHandler var2) throws SQLException;

Cursor queryCursor(Statement var1) throws SQLException;

BoundSql getBoundSql();

ParameterHandler getParameterHandler();

}

public interface ParameterHandler {

Object getParameterObject();

void setParameters(PreparedStatement var1) throws SQLException;

}

public interface ResultHandler {

void handleResult(ResultContext extends T> var1);

}

拦截的执行顺序是Executor->StatementHandler->ParameterHandler->ResultHandler

MyBatis提供的拦截器接口:

public interface Interceptor {

Object intercept(Invocation var1) throws Throwable;

default Object plugin(Object target) {

return Plugin.wrap(target, this);

}

default void setProperties(Properties properties) {}

}

Object intercept方法用于拦截器的实现;

Object plugin方法用于判断执行拦截器的类型;

void setProperties方法用于获取配置项的属性。

拦截对象和拦截器接口的结合,自定义的拦截器类需要实现拦截器接口,并通过注解@Intercepts和参数@Signature来声明要拦截的对象。

@Signature参数type是拦截对象,method是拦截的方法,即上面的四个类对应的方法,args是拦截方法对应的参数(方法存在重载因此需要指明参数个数和类型)

@Intercepts可以有多个@Signature,即一个拦截器实现类可以同时拦截多个对象及方法,示例如下:

Executor->intercept

StatementHandler->intercept

ParameterHandler->intercept

ResultHandler->intercept

@Intercepts({

@Signature(

type = Executor.class,

method = "query",

args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}

)

})

public class SelectPlugin implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

if (invocation.getTarget() instanceof Executor) {

System.out.println("SelectPlugin");

}

return invocation.proceed();

}

@Override

public Object plugin(Object target) {

if (target instanceof Executor) {

return Plugin.wrap(target, this);

}

return target;

}

@Override

public void setProperties(Properties properties) {}

}

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})

public class StatementPlugin implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

if (invocation.getTarget() instanceof StatementHandler) {

System.out.println("StatementPlugin");

}

return invocation.proceed();

}

@Override

public Object plugin(Object target) {

if (target instanceof StatementHandler) {

return Plugin.wrap(target, this);

}

return target;

}

@Override

public void setProperties(Properties properties) {}

}

@Intercepts({@Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})})

public class ParameterPlugin implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

if (invocation.getTarget() instanceof ParameterHandler) {

System.out.println("ParameterPlugin");

}

return invocation.proceed();

}

@Override

public Object plugin(Object target) {

if (target instanceof ParameterHandler) {

return Plugin.wrap(target, this);

}

return target;

}

@Override

public void setProperties(Properties properties) {}

}

@Intercepts({@Signature(type = ResultHandler.class,method = "handleResult",args = {ResultContext.class})})

public class ResultPlugin implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

if (invocation.getTarget() instanceof ResultHandler) {

System.out.println("ResultPlugin");

}

return invocation.proceed();

}

@Override

public Object plugin(Object target) {

if (target instanceof ResultHandler) {

return Plugin.wrap(target, this);

}

return target;

}

@Override

public void setProperties(Properties properties) {}

}

二、拦截器注册的三种方式

前面介绍了Mybatis的拦截对象及其接口的实现方式,那么在项目中如何注册拦截器呢?本文中给出三种注册方式。

1.XML注册

xml注册是最基本的方式,是通过在Mybatis配置文件中plugins元素来进行注册的。一个plugin对应着一个拦截器,在plugin元素可以指定property子元素,在注册定义拦截器时把对应拦截器的所有property通过Interceptor的setProperties方法注入给拦截器。因此拦截器注册xml方式如下:

PUBLIC "-//mybatis.org//DTD Config 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-config.dtd">

2.配置类注册

配置类注册是指通过Mybatis的配置类中声明注册拦截器,配置类注册也可以通过Properties类给Interceptor的setProperties方法注入参数。具体参考如下:

@Configuration

public class MyBatisConfig {

@Bean

public String MyBatisInterceptor(SqlSessionFactory sqlSessionFactory) {

UpdatePlugin executorInterceptor = new UpdatePlugin();

Properties properties = new Properties();

properties.setProperty("prop1", "value1");

// 给拦截器添加自定义参数

executorInterceptor.setProperties(properties);

sqlSessionFactory.getConfiguration().addInterceptor(executorInterceptor);

sqlSessionFactory.getConfiguration().addInterceptor(new StatementPlugin());

sqlSessionFactory.getConfiguration().addInterceptor(new ResultPlugin());

sqlSessionFactory.getConfiguration().addInterceptor(new ParameterPlugin());

// sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin());

return "interceptor";

}

// 与sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin());效果一致

@Bean

public SelectPlugin SelectInterceptor() {

SelectPlugin interceptor = new SelectPlugin();

Properties properties = new Properties();

// 调用properties.setProperty方法给拦截器设置自定义参数

interceptor.setProperties(properties);

return interceptor;

}

}

3.注解方式

通过@Component注解方式是最简单的方式,在不需要转递自定义参数时可以使用,方便快捷。

@Component

@Intercepts({

@Signature(

type = Executor.class,

method = "query",

args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}

)

})

public class SelectPlugin implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

if (invocation.getTarget() instanceof Executor) {

System.out.println("SelectPlugin");

}

return invocation.proceed();

}

@Override

public Object plugin(Object target) {

if (target instanceof Executor) {

return Plugin.wrap(target, this);

}

return target;

}

@Override

public void setProperties(Properties properties) {

}

}

三、ParameterHandler参数改写-修改时间和修改人统一插入

针对具体的拦截器实现进行描述。日常编码需求中会碰到修改时需要插入修改的时间和人员,如果要用xml的方式去写非常麻烦,而通过拦截器的方式可以快速实现全局的插入修改时间和人员。先看代码:

@Component

@Intercepts({

@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),

})

public class MyBatisInterceptor implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

// 参数代理

if (invocation.getTarget() instanceof ParameterHandler) {

System.out.println("ParameterHandler");

// 自动添加操作员信息

autoAddOperatorInfo(invocation);

}

return invocation.proceed();

}

@Override

public Object plugin(Object target) {

return Plugin.wrap(target, this);

}

@Override

public void setProperties(Properties properties) {

}

/**

* 自动添加操作员信息

*

* @param invocation 代理对象

* @throws Throwable 异常

*/

private void autoAddOperatorInfo(Invocation invocation) throws Throwable {

System.out.println("autoInsertCreatorInfo");

// 获取代理的参数对象ParameterHandler

ParameterHandler ph = (ParameterHandler) invocation.getTarget();

// 通过MetaObject获取ParameterHandler的反射内容

MetaObject metaObject = MetaObject.forObject(ph,

SystemMetaObject.DEFAULT_OBJECT_FACTORY,

SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,

new DefaultReflectorFactory());

// 通过MetaObject反射的内容获取MappedStatement

MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement");

// 当sql类型为INSERT或UPDATE时,自动插入操作员信息

if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT ||

mappedStatement.getSqlCommandType() == SqlCommandType.UPDATE) {

// 获取参数对象

Object obj = ph.getParameterObject();

if (null != obj) {

// 通过反射获取参数对象的属性

Field[] fields = obj.getClass().getDeclaredFields();

// 遍历参数对象的属性

for (Field f : fields) {

// 如果sql是INSERT,且存在createdAt属性

if ("createdAt".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {

// 设置允许访问反射属性

f.setAccessible(true);

// 如果没有设置createdAt属性,则自动为createdAt属性添加当前的时间

if (null == f.get(obj)) {

// 设置createdAt属性为当前时间

f.set(obj, LocalDateTime.now());

}

}

// 如果sql是INSERT,且存在createdBy属性

if ("createdBy".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {

// 设置允许访问反射属性

f.setAccessible(true);

// 如果没有设置createdBy属性,则自动为createdBy属性添加当前登录的人员

if (null == f.get(obj)) {

// 设置createdBy属性为当前登录的人员

f.set(obj, 0);

}

}

// sql为INSERT或UPDATE时均需要设置updatedAt属性

if ("updatedAt".equals(f.getName())) {

f.setAccessible(true);

if (null == f.get(obj)) {

f.set(obj, LocalDateTime.now());

}

}

// sql为INSERT或UPDATE时均需要设置updatedBy属性

if ("updatedBy".equals(f.getName())) {

f.setAccessible(true);

if (null == f.get(obj)) {

f.set(obj, 0);

}

}

}

// 通过反射获取ParameterHandler的parameterObject属性

Field parameterObject = ph.getClass().getDeclaredField("parameterObject");

// 设置允许访问parameterObject属性

parameterObject.setAccessible(true);

// 将上面设置的新参数对象设置到ParameterHandler的parameterObject属性

parameterObject.set(ph, obj);

}

}

}

}

拦截器的接口实现参考前文,这里着重介绍autoAddOperatorInfo方法里的相关类。

1.ParameterHandler

接口源码:

public interface ParameterHandler {

Object getParameterObject();

void setParameters(PreparedStatement var1) throws SQLException;

}

提供两个方法:

getParameterObject是获取参数对象,可能存在null,需要注意null指针。

setParameters是控制如何设置SQL参数,即sql语句中配置的java对象和jdbc类型对应的关系,例如#{id,jdbcType=INTEGER},id默认类型是javaType=class java.lang.Integer。

该接口有一个默认的实现类,源码如下:

public class DefaultParameterHandler implements ParameterHandler {

private final TypeHandlerRegistry typeHandlerRegistry;

private final MappedStatement mappedStatement;

private final Object parameterObject;

private final BoundSql boundSql;

private final Configuration configuration;

public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {

this.mappedStatement = mappedStatement;

this.configuration = mappedStatement.getConfiguration();

this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();

this.parameterObject = parameterObject;

this.boundSql = boundSql;

}

public Object getParameterObject() {

return this.parameterObject;

}

public void setParameters(PreparedStatement ps) {

ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId());

List parameterMappings = this.boundSql.getParameterMappings();

if (parameterMappings != null) {

for(int i = 0; i < parameterMappings.size(); ++i) {

ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);

if (parameterMapping.getMode() != ParameterMode.OUT) {

String propertyName = parameterMapping.getProperty();

Object value;

if (this.boundSql.hasAdditionalParameter(propertyName)) {

value = this.boundSql.getAdditionalParameter(propertyName);

} else if (this.parameterObject == null) {

value = null;

} else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {

value = this.parameterObject;

} else {

MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);

value = metaObject.getValue(propertyName);

}

TypeHandler typeHandler = parameterMapping.getTypeHandler();

JdbcType jdbcType = parameterMapping.getJdbcType();

if (value == null && jdbcType == null) {

jdbcType = this.configuration.getJdbcTypeForNull();

}

try {

typeHandler.setParameter(ps, i + 1, value, jdbcType);

} catch (SQLException | TypeException var10) {

throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10);

}

}

}

}

}

}

通过DefaultParameterHandler实现类我们知道通过ParameterHandler可以获取到哪些属性和方法,其中包括我们下面一个重要的类MappedStatement。

2.MappedStatement

MyBatis的mapper文件中的每个select/update/insert/delete标签会被解析器解析成一个对应的MappedStatement对象,也就是一个MappedStatement对象描述一条SQL语句。MappedStatement对象属性如下:

// mapper配置文件名

private String resource;

// mybatis的全局信息,如jdbc

private Configuration configuration;

// 节点的id属性加命名空间,如:com.example.mybatis.dao.UserMapper.selectByExample

private String id;

private Integer fetchSize;

private Integer timeout;

private StatementType statementType;

private ResultSetType resultSetType;

private SqlSource sqlSource;

private Cache cache;

private ParameterMap parameterMap;

private List resultMaps;

private boolean flushCacheRequired;

private boolean useCache;

private boolean resultOrdered;

// sql语句的类型:select、update、delete、insert

private SqlCommandType sqlCommandType;

private KeyGenerator keyGenerator;

private String[] keyProperties;

private String[] keyColumns;

private boolean hasNestedResultMaps;

private String databaseId;

private Log statementLog;

private LanguageDriver lang;

private String[] resultSets;

在本例中通过MappedStatement对象的sqlCommandType来判断当前的sql类型是insert、update来进行下一步的操作。

四、通过StatementHandler改写SQL

StatementHandler是用于封装JDBC Statement操作,负责对JDBC Statement的操作,如设置参数,并将Statement结果集转换成List集合。

实现代码如下:

删除注解标记

@Target({ElementType.METHOD}) //表示注解的使用范围

@Retention(RetentionPolicy.RUNTIME) //注解的保存时间

@Documented //文档显示

public @interface DeletedAt {

boolean has() default true;

}

Dao层添加删除注解,为false时不添加删除标志

@Mapper

public interface AdminProjectDao {

@DeletedAt(has = false)

List selectProjects(AdminProjectPo po);

}

拦截器通过删除注解标记判断是否添加删除标志

@Component

@Intercepts({

@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),

})

public class MyBatisInterceptor implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

if (invocation.getTarget() instanceof StatementHandler) {

System.out.println("StatementHandler");

checkHasDeletedAtField(invocation);

}

return invocation.proceed();

}

@Override

public Object plugin(Object target) {

return Plugin.wrap(target, this);

}

@Override

public void setProperties(Properties properties) {

}

/**

* 检查查询是否需要添加删除标志字段

*

* @param invocation 代理对象

* @throws Throwable 异常

*/

private void checkHasDeletedAtField(Invocation invocation) throws Throwable {

System.out.println("checkHasDeletedAtField");

StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

// 通过MetaObject访问对象的属性

MetaObject metaObject = MetaObject.forObject(

statementHandler,

SystemMetaObject.DEFAULT_OBJECT_FACTORY,

SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,

new DefaultReflectorFactory());

// 获取成员变量mappedStatement

MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

// 如果sql类型是查询

if (mappedStatement.getSqlCommandType() == SqlCommandType.SELECT) {

// 获取删除注解标志

DeletedAt annotation = null;

String id = mappedStatement.getId();

String className = id.substring(0, id.lastIndexOf("."));

String methodName = id.substring(id.lastIndexOf(".") + 1);

Class> aClass = Class.forName(className);

Method[] declaredMethods = aClass.getDeclaredMethods();

for (Method declaredMethod : declaredMethods) {

declaredMethod.setAccessible(true);

//方法名相同,并且注解是DeletedAt

if (methodName.equals(declaredMethod.getName()) && declaredMethod.isAnnotationPresent(DeletedAt.class)) {

annotation = declaredMethod.getAnnotation(DeletedAt.class);

}

}

// 如果注解不存在或者注解为true(默认为true) 则为mysql语句增加删除标志

if (annotation == null || annotation.has()) {

BoundSql boundSql = statementHandler.getBoundSql();

//获取到原始sql语句

String sql = boundSql.getSql();

//通过反射修改sql语句

Field field = boundSql.getClass().getDeclaredField("sql");

field.setAccessible(true);

String newSql = sql.replaceAll("9=9", "9=9 and deleted_at is null ");

field.set(boundSql, newSql);

}

}

}

}

在SQL语句替换上需要能识别到要被替换的内容,因此在xml的sql语句中加入特殊标志"9=9",该标志不影响原来SQL的执行结果,不同的过滤条件可以设置不同的标志,是一个比较巧妙的替换方式。

以上就是MyBatis拦截器的原理与使用的详细内容,更多关于MyBatis拦截器的资料请关注我们其它相关文章!


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:数据中心安全性与可靠性研讨会上海举办
下一篇:论数据中心融合趋势-WANGQIB@dc
相关文章

 发表评论

暂时没有评论,来抢沙发吧~