Mybatis拦截器
可拦截的目标对象有四个(前面是可被拦截的对象,后面括号中是对象中可被拦截的方法)
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
- ResultSetHandler (handleResultSets, handleOutputParameters)
场景
数据库中每一张表都有如下6个通用字段来记录表的操作日志:
- 创建时间:createTime
- 创建人id:createById
- 创建人姓名:createBy
- 更新时间:updateTime
- 更新人id:updateById
- 更新人姓名:updateBy
在执行insert操作时,需要设置这6个字段的值,执行update操作时只需要更新后3个字段的值,在业务代码中对这六个字段进行设值肯定是可以实现的,但是会导致代码重复,而且容易漏写,非常不方便,如果用到Mybatis则可以通过Mybatis拦截器来实现。
实现步骤
- 实现Interceptor接口
org.apache.ibatis.plugin.Interceptor
拦截器上的@Intercepts注解用于配置需要拦截的对象和方法,
这里我们通过拦截 Executor 的 update 方法来实现数据表的这六个字段的更新。//Intercepts设置拦截的目标对象为 Executor的update方法 @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) public class OperateRecordInterceptor implements Interceptor { private static final Logger log = LoggerFactory.getLogger(OperateRecordInterceptor.class); /** * 执行更新的六个字段名 */ public static String FIELD_CREATE_TIME="createTime"; public static String FIELD_UPDATE_TIME="updateTime"; public static String FIELD_CREATE_ID="createById"; public static String FIELD_CREATE_NAME="createBy"; public static String FIELD_UPDATE_ID="updateById"; public static String FIELD_UPDATE_NAME="updateBy"; @Override public Object intercept(Invocation invocation) throws Throwable { try { //获取当前操作的用户 Operator operator=OperatorHolder.getOperator(); //第一个参数为MappedStatement, Executor的update方法的内置参数 MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; //第二个参数才是我们需要的处理的数据库Entity对象,比如通过updateByExample(parameter)中的参数 Object parameter = invocation.getArgs()[1]; //得到sql类型 SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); Date date=new Date(); //SQL插入动作 if (SqlCommandType.INSERT.equals(sqlCommandType)) { //插入数据库时设置六个字段 setOperateTime(parameter, FIELD_CREATE_TIME,date); setOperateId(parameter,FIELD_CREATE_ID,operator); setOperateBy(parameter,FIELD_CREATE_NAME,operator); setOperateTime(parameter, FIELD_UPDATE_TIME,date); setOperateId(parameter,FIELD_UPDATE_ID,operator); setOperateBy(parameter,FIELD_UPDATE_NAME,operator); } //SQL更新动作 else if (SqlCommandType.UPDATE.equals(sqlCommandType)) { //更新数据库时设置后三个字段 setOperateTime(parameter, FIELD_UPDATE_TIME,date); setOperateId(parameter,FIELD_UPDATE_ID,operator); setOperateBy(parameter,FIELD_UPDATE_NAME,operator); } } catch (Exception e) { //这里需要捕获异常,为了保证主流程不会异常阻断 log.error("Mybatis OperateRecordInterceptor 拦截器异常",e); } //继续执行该执行的方法 return invocation.proceed(); } private void setOperateTime(Object parameter, String operateTimeFieldName,Date date) throws IllegalAccessException { Field field = getField(parameter, operateTimeFieldName); setFieldValue(field,parameter,date); } private void setOperateId(Object parameter, String operateFieldName, Operator operator) throws IllegalAccessException { Field field = getField(parameter, operateFieldName); Object value=operator!=null ? operator.getOperatorId() : null; setFieldValue(field,parameter,value); } private void setOperateBy(Object parameter, String operateFieldName, Operator operator) throws IllegalAccessException { Field field = getField(parameter, operateFieldName); Object value=operator!=null ? operator.getOperatorName() : null; setFieldValue(field, parameter,value); } private void setFieldValue(Field field,Object obj,Object value) throws IllegalAccessException { if(field!=null){ field.setAccessible(true); field.set(obj, value); } } private Field getField(Object object, String fieldName){ Field field= null; try { field = object.getClass().getDeclaredField(fieldName); } catch (NoSuchFieldException e) { log.error("从[{}]中获取[{}]字段发生异常:{}:{}",object.getClass().getName(),fieldName,e.getClass().getName(),e.getMessage()); } return field; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
OperatorHolder用于获取当前操作的用户,比如可以通过SpringSecurity往里面设置当前登录的用户信息,
通过ThreadLocal实现 。
关于ThreadLocal使用需注意的问题详见:
java中的ThreadLocal
ThreadLocal在线程池中被串用
public class OperatorHolder {
private static final ThreadLocal<Operator> operatorThreadLocal = new ThreadLocal();
//调用此方法在SpringSecurity中设置当前登录的用户信息
public static void setOperator(Integer operatorId,String operatorName){
operatorThreadLocal.set(new Operator(operatorId,operatorName));
}
public static Operator getOperator() {
return operatorThreadLocal.get();
}
public static void clearOperator() {
operatorThreadLocal.remove();
}
}
Operator为操作员对象
public class Operator {
//操作人的id
private Integer operatorId;
//操作人姓名
private String operatorName;
public Operator(Integer operatorId, String operatorName) {
this.operatorId = operatorId;
this.operatorName = operatorName;
}
public Integer getOperatorId() {
return operatorId;
}
public void setOperatorId(Integer operatorId) {
this.operatorId = operatorId;
}
public String getOperatorName() {
return operatorName;
}
public void setOperatorName(String operatorName) {
this.operatorName = operatorName;
}
}
- 配置mybatis拦截器
拦截器是以插件的形式配置的,mybatis配置文件增加如下plugin即可:mybatis-config.xml
...
<plugins>
<plugin interceptor="com.xxx.OperateRecordInterceptor"></plugin>
...
</plugins>
...
- 如果使用spring框架,还需要指定mybatis配置文件 “mybatis-config.xml” 的路径,否则不会生效
(1)如果使用的是spring boot:application.preperties
mybatis:
config-location: classpath:mybatis-config.xml
(2)如果使用的是spring: application.xml
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations">
<list>
<value>classpath*:com/topscode/**/mapper/*.xml</value>
<value>classpath*:mapper/**/*.xml</value>
</list>
</property>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>