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;    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;    <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;    List<BatchResult> 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;    <E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;    <E> Cursor<E> queryCursor(Statement var1) throws SQLException;    BoundSql getBoundSql();    ParameterHandler getParameterHandler();}public interface ParameterHandler {    Object getParameterObject();    void setParameters(PreparedStatement var1) throws SQLException;}public interface ResultHandler<T> {    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方式如下:

<?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>    <!-- ...... -->    <plugins>       <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor">           <property name="prop1" value="prop1"/>           <property name="prop2" value="prop2"/>       </plugin>    </plugins>    <!-- ...... --></configuration>

2.配置类注册

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

@Configurationpublic 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<ParameterMapping> 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<ResultMap> 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<AdminProjectPo> 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拦截器的资料请关注其它相关文章!

第一个青春是上帝给的;第二个的青春是*自己努力的

MyBatis拦截器的原理与使用

相关文章:

你感兴趣的文章:

标签云: