一、拦截对象和接口实现示例 MyBatis拦截器的作用是在于Dao到DB中间进行额外的处理 。大部分情况下通过mybatis的xml配置sql都可以达到想要的DB操作效果 , 然而存在一些类似或者相同的查询条件或者查询要求 , 这些可以通过拦截器的实现可以提升开发效率 , 比如:分页、插入和更新时间/人、数据权限、SQL监控日志等 。
- Mybatis支持四种对象拦截Executor、StatementHandler、PameterHandler和ResultSetHandler
- Executor:拦截执行器的方法 。
- StatementHandler:拦截Sql语法构建的处理 。
- ParameterHandler:拦截参数的处理 。
- ResultHandler:拦截结果集的处理 。
- MyBatis提供的拦截器接口:
Object plugin方法用于判断执行拦截器的类型;
void setProperties方法用于获取配置项的属性 。
- 拦截对象和拦截器接口的结合 , 自定义的拦截器类需要实现拦截器接口 , 并通过注解@Intercepts和参数@Signature来声明要拦截的对象 。
@Intercepts可以有多个@Signature , 即一个拦截器实现类可以同时拦截多个对象及方法 , 示例如下:
- Executor->intercept
- StatementHandler->intercept
- ParameterHandler->intercept
- ResultHandler->intercept
前面介绍了Mybatis的拦截对象及其接口的实现方式 , 那么在项目中如何注册拦截器呢?本文中给出三种注册方式 。
1.XML注册
xml注册是最基本的方式 , 是通过在Mybatis配置文件中plugins元素来进行注册的 。一个plugin对应着一个拦截器 , 在plugin元素可以指定property子元素 , 在注册定义拦截器时把对应拦截器的所有property通过Interceptor的setProperties方法注入给拦截器 。因此拦截器注册xml方式如下:
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!DOCTYPE configuration 3PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4"http://mybatis.org/dtd/mybatis-3-config.dtd"> 5 <configuration> 6<!-- ...... --> 7<plugins> 8<plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor"> 9<property name="prop1" value="https://tazarkount.com/read/prop1"/>10<property name="prop2" value="https://tazarkount.com/read/prop2"/>11</plugin>12</plugins>13<!-- ...... -->14 </configuration> 2.配置类注册
配置类注册是指通过Mybatis的配置类中声明注册拦截器 , 配置类注册也可以通过Properties类给Interceptor的setProperties方法注入参数 。具体参考如下:
1 @Configuration 2 public class MyBatisConfig { 3@Bean 4public String MyBatisInterceptor(SqlSessionFactory sqlSessionFactory) { 5UpdatePlugin executorInterceptor = new UpdatePlugin(); 6Properties properties = new Properties(); 7properties.setProperty("prop1", "value1"); 8// 给拦截器添加自定义参数 9executorInterceptor.setProperties(properties);10sqlSessionFactory.getConfiguration().addInterceptor(executorInterceptor);11sqlSessionFactory.getConfiguration().addInterceptor(new StatementPlugin());12sqlSessionFactory.getConfiguration().addInterceptor(new ResultPlugin());13sqlSessionFactory.getConfiguration().addInterceptor(new ParameterPlugin());14// sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin());15return "interceptor";16}17 18// 与sqlSessionFactory.getConfiguration().addInterceptor(new SelectPlugin());效果一致19@Bean20public SelectPlugin SelectInterceptor() {21SelectPlugin interceptor = new SelectPlugin();22Properties properties = new Properties();23// 调用properties.setProperty方法给拦截器设置自定义参数24interceptor.setProperties(properties);25return interceptor;26}27 } 3.注解方式
通过@Component注解方式是最简单的方式 , 在不需要转递自定义参数时可以使用 , 方便快捷 。
@Component@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public class SelectPlugin implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof Executor) {System.out.println("SelectPlugin");}return invocation.proceed();}@Overridepublic Object plugin(Object target) {if (target instanceof Executor) {return Plugin.wrap(target, this);}return target;}@Overridepublic void setProperties(Properties properties) {}}三、ParameterHandler参数改写-修改时间和修改人统一插入
针对具体的拦截器实现进行描述 。日常编码需求中会碰到修改时需要插入修改的时间和人员 , 如果要用xml的方式去写非常麻烦 , 而通过拦截器的方式可以快速实现全局的插入修改时间和人员 。先看代码:
1 @Component 2 @Intercepts({ 3@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}), 4 }) 5 public class MyBatisInterceptor implements Interceptor { 6@Override 7public Object intercept(Invocation invocation) throws Throwable { 8// 参数代理 9if (invocation.getTarget() instanceof ParameterHandler) {10System.out.println("ParameterHandler");11// 自动添加操作员信息12autoAddOperatorInfo(invocation);13}14return invocation.proceed();15}16 17@Override18public Object plugin(Object target) {19return Plugin.wrap(target, this);20}21 22@Override23public void setProperties(Properties properties) {24 25}26 27/**28* 自动添加操作员信息29*30* @param invocation 代理对象31* @throws Throwable 异常32*/33private void autoAddOperatorInfo(Invocation invocation) throws Throwable {34System.out.println("autoInsertCreatorInfo");35// 获取代理的参数对象ParameterHandler36ParameterHandler ph = (ParameterHandler) invocation.getTarget();37// 通过MetaObject获取ParameterHandler的反射内容38MetaObject metaObject = MetaObject.forObject(ph,39SystemMetaObject.DEFAULT_OBJECT_FACTORY,40SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,41new DefaultReflectorFactory());42// 通过MetaObject反射的内容获取MappedStatement43MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement");44// 当sql类型为INSERT或UPDATE时 , 自动插入操作员信息45if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT ||46mappedStatement.getSqlCommandType() == SqlCommandType.UPDATE) {47// 获取参数对象48Object obj = ph.getParameterObject();49if (null != obj) {50// 通过反射获取参数对象的属性51Field[] fields = obj.getClass().getDeclaredFields();52// 遍历参数对象的属性53for (Field f : fields) {54// 如果sql是INSERT,且存在createdAt属性55if ("createdAt".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {56// 设置允许访问反射属性57f.setAccessible(true);58// 如果没有设置createdAt属性 , 则自动为createdAt属性添加当前的时间59if (null == f.get(obj)) {60// 设置createdAt属性为当前时间61f.set(obj, LocalDateTime.now());62}63}64// 如果sql是INSERT,且存在createdBy属性65if ("createdBy".equals(f.getName()) && mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {66// 设置允许访问反射属性67f.setAccessible(true);68// 如果没有设置createdBy属性 , 则自动为createdBy属性添加当前登录的人员69if (null == f.get(obj)) {70// 设置createdBy属性为当前登录的人员71f.set(obj, 0);72}73}74// sql为INSERT或UPDATE时均需要设置updatedAt属性75if ("updatedAt".equals(f.getName())) {76f.setAccessible(true);77if (null == f.get(obj)) {78f.set(obj, LocalDateTime.now());79}80}81// sql为INSERT或UPDATE时均需要设置updatedBy属性82if ("updatedBy".equals(f.getName())) {83f.setAccessible(true);84if (null == f.get(obj)) {85f.set(obj, 0);86}87}88}89 90// 通过反射获取ParameterHandler的parameterObject属性91Field parameterObject = ph.getClass().getDeclaredField("parameterObject");92// 设置允许访问parameterObject属性93parameterObject.setAccessible(true);94// 将上面设置的新参数对象设置到ParameterHandler的parameterObject属性95parameterObject.set(ph, obj);96}97}98}99 } 拦截器的接口实现参考前文 , 这里着重介绍autoAddOperatorInfo方法里的相关类 。
1.ParameterHandler
接口源码:
1 public interface ParameterHandler {2Object getParameterObject();3void setParameters(PreparedStatement var1) throws SQLException;4 } 提供两个方法:
getParameterObject是获取参数对象 , 可能存在null , 需要注意null指针 。
setParameters是控制如何设置SQL参数 , 即sql语句中配置的java对象和jdbc类型对应的关系 , 例如#{id,jdbcType=INTEGER} , id默认类型是javaType=class java.lang.Integer 。
该接口有一个默认的实现类 , 源码如下:
1 public class DefaultParameterHandler implements ParameterHandler { 2private final TypeHandlerRegistry typeHandlerRegistry; 3private final MappedStatement mappedStatement; 4private final Object parameterObject; 5private final BoundSql boundSql; 6private final Configuration configuration; 78public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { 9this.mappedStatement = mappedStatement;10this.configuration = mappedStatement.getConfiguration();11this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();12this.parameterObject = parameterObject;13this.boundSql = boundSql;14}15 16public Object getParameterObject() {17return this.parameterObject;18}19 20public void setParameters(PreparedStatement ps) {21ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId());22List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();23if (parameterMappings != null) {24for(int i = 0; i < parameterMappings.size(); ++i) {25ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);26if (parameterMapping.getMode() != ParameterMode.OUT) {27String propertyName = parameterMapping.getProperty();28Object value;29if (this.boundSql.hasAdditionalParameter(propertyName)) {30value = https://tazarkount.com/read/this.boundSql.getAdditionalParameter(propertyName);31} else if (this.parameterObject == null) {32value = null;33} else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {34value = this.parameterObject;35} else {36MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);37value = metaObject.getValue(propertyName);38}39 40TypeHandler typeHandler = parameterMapping.getTypeHandler();41JdbcType jdbcType = parameterMapping.getJdbcType();42if (value == null && jdbcType == null) {43jdbcType = this.configuration.getJdbcTypeForNull();44}45 46try {47typeHandler.setParameter(ps, i + 1, value, jdbcType);48} catch (SQLException | TypeException var10) {49throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10);50}51}52}53}54 55}56 } 通过DefaultParameterHandler实现类我们知道通过ParameterHandler可以获取到哪些属性和方法 , 其中包括我们下面一个重要的类MappedStatement 。
2.MappedStatement
MyBatis的mapper文件中的每个select/update/insert/delete标签会被解析器解析成一个对应的MappedStatement对象 , 也就是一个MappedStatement对象描述一条SQL语句 。MappedStatement对象属性如下:
1// mapper配置文件名 2private String resource; 3// mybatis的全局信息 , 如jdbc 4private Configuration configuration; 5// 节点的id属性加命名空间,如:com.example.mybatis.dao.UserMapper.selectByExample 6private String id; 7private Integer fetchSize; 8private Integer timeout; 9private StatementType statementType;10private ResultSetType resultSetType;11private SqlSource sqlSource;12private Cache cache;13private ParameterMap parameterMap;14private List<ResultMap> resultMaps;15private boolean flushCacheRequired;16private boolean useCache;17private boolean resultOrdered;18// sql语句的类型:select、update、delete、insert19private SqlCommandType sqlCommandType;20private KeyGenerator keyGenerator;21private String[] keyProperties;22private String[] keyColumns;23private boolean hasNestedResultMaps;24private String databaseId;25private Log statementLog;26private LanguageDriver lang;27private 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时不添加删除标志
1 @Mapper2 public interface AdminProjectDao {3@DeletedAt(has = false)4List<AdminProjectPo> selectProjects(AdminProjectPo po);5 } 拦截器通过删除注解标记判断是否添加删除标志
1 @Component 2 @Intercepts({ 3@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}), 4 }) 5 public class MyBatisInterceptor implements Interceptor { 6@Override 7public Object intercept(Invocation invocation) throws Throwable { 8if (invocation.getTarget() instanceof StatementHandler) { 9System.out.println("StatementHandler");10checkHasDeletedAtField(invocation);11}12return invocation.proceed();13}14 15@Override16public Object plugin(Object target) {17return Plugin.wrap(target, this);18}19 20@Override21public void setProperties(Properties properties) {22 23}24 25/**26* 检查查询是否需要添加删除标志字段27*28* @param invocation 代理对象29* @throws Throwable 异常30*/31private void checkHasDeletedAtField(Invocation invocation) throws Throwable {32System.out.println("checkHasDeletedAtField");33StatementHandler statementHandler = (StatementHandler) invocation.getTarget();34// 通过MetaObject访问对象的属性35MetaObject metaObject = MetaObject.forObject(36statementHandler,37SystemMetaObject.DEFAULT_OBJECT_FACTORY,38SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,39new DefaultReflectorFactory());40// 获取成员变量mappedStatement41MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");42// 如果sql类型是查询43if (mappedStatement.getSqlCommandType() == SqlCommandType.SELECT) {44// 获取删除注解标志45DeletedAt annotation = null;46String id = mappedStatement.getId();47String className = id.substring(0, id.lastIndexOf("."));48String methodName = id.substring(id.lastIndexOf(".") + 1);49Class<?> aClass = Class.forName(className);50Method[] declaredMethods = aClass.getDeclaredMethods();51for (Method declaredMethod : declaredMethods) {52declaredMethod.setAccessible(true);53//方法名相同 , 并且注解是DeletedAt54if (methodName.equals(declaredMethod.getName()) && declaredMethod.isAnnotationPresent(DeletedAt.class)) {55annotation = declaredMethod.getAnnotation(DeletedAt.class);56}57}58// 如果注解不存在或者注解为true(默认为true) 则为mysql语句增加删除标志59if (annotation == null || annotation.has()) {60BoundSql boundSql = statementHandler.getBoundSql();61//获取到原始sql语句62String sql = boundSql.getSql();63//通过反射修改sql语句64Field field = boundSql.getClass().getDeclaredField("sql");65field.setAccessible(true);66String newSql = sql.replaceAll("9=9", "9=9 and deleted_at is null ");67field.set(boundSql, newSql);68}69}70}71 } 在SQL语句替换上需要能识别到要被替换的内容 , 因此在xml的sql语句中加入特殊标志"9=9",该标志不影响原来SQL的执行结果 , 不同的过滤条件可以设置不同的标志 , 是一个比较巧妙的替换方式 。
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
