mybaits拦截器+自定义注解
实现目的:为了存储了公共字典表主键的其他表在查询的时候不用关联查询(所以拦截位置位于mybaits语句查询得出结果集后)
项目环境 :springboot+mybaits
实现步骤:自定义注解——自定义实现mybaits拦截器——注册mybaits拦截器
一、自定义注解
1.1 代码示例
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.FIELD})// @Retention(RetentionPolicy.RUNTIME)//该注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在; @Inherited//允许子类继承父类的注解。 (子类中可以获取并使用父类注解) @Documented//指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。 /** * 该自定义注解用于所查询语句中字段包含字典表主键 并需要将主键同时对照成字典表对应的名称 * 将该注解放置在名称列,参数为字典表主键存储列的名字 * @ClassName: DictReplace * 描述: TODO 用于字典名称字段默认为空,则空则认为字典id字段名为 字典名称字典.substring(0,length()-4) 若不为空则认定字典id字段名称为参数值 * 作者cy * 时间 2019年3月26日 上午9:02:47 * */ public @interface DictReplace { String dictIdFieldName() default ""; }
@Target 注解
功能:指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里。
ElementType的取值包含以下几种:
- TYPE:类,接口或者枚举
- FIELD:域,包含枚举常量
- METHOD:方法
- PARAMETER:参数
- CONSTRUCTOR:构造方法
- LOCAL_VARIABLE:局部变量
- ANNOTATION_TYPE:注解类型
- PACKAGE:包
@Retention 注解
功能:指明修饰的注解的生存周期,即会保留到哪个阶段。
RetentionPolicy的取值包含以下三种:
- SOURCE:源码级别保留,编译后即丢弃。
- CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值。
- RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用。
@Documented 注解
功能:指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。
@Inherited注解
功能:允许子类继承父类中的注解。
1.2 使用场景
@TableField("runtime_platform") private Integer runtimePlatform; @DictReplace//字典替换注解 @TableField(exist = false) private String runtimePlatformName;
二、自定义mybaits拦截器并注册
2.1 代码示例
import java.util.List; import java.util.Properties; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import com.msunsoft.base.common.factory.ConstantFactory; import com.msunsoft.base.common.interceptor.annotation.DictReplace; import com.msunsoft.base.spring.SpringContextHolder; import com.msunsoft.base.util.ToolUtil; import org.apache.ibatis.executor.resultset.ResultSetHandler; import java.lang.reflect.Field; import java.sql.Statement; /** * 字典替换拦截器,当注解方法被执行后拦截并修改查询后的结果 * @ClassName: DictReplaceInteceptor * 描述: TODO * 作者 * 时间 2019年3月25日 下午7:23:41 * */ @Intercepts({ @Signature(type = ResultSetHandler.class,method = "handleResultSets", args = { Statement.class }) }) public class DictReplaceInteceptor implements Interceptor{ private Properties properties; private SpringContextHolder spring;//实现 ApplicationContextAware 接口的类包含获取spring容器中的bean的静态方法 @Override @SuppressWarnings(value = {"all"}) public Object intercept(Invocation invocation) throws Throwable { //因为 handleResultSets 方法执行结束后可以收到一个list类型的数据结果集,所以虽然该方法的目的是用于结束本次拦截,执行预定方法(handleResultSets)方便下次拦截 List<Object> results = (List<Object>)invocation.proceed(); try{
//自定义方法用于判断对象是否为空 if(ToolUtil.isNotEmpty(results)){
//ConstantFactory 是自定义的包含常用方法的一个类,现在用到的是它包含在其中的通过字典主键获取字典名称的方法 ConstantFactory constantFactory = spring.getBean(ConstantFactory.class); Class<?> cls = results.get(0).getClass(); Field[] fields = cls.getDeclaredFields();// 获取private修饰的成员变量 获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段。 for(Object result:results){ for (Field field : fields) { //获取我们自定义的注解 DictReplace dictReplace = field.getAnnotation(DictReplace.class); if(dictReplace!=null){//如果存在这个注解 我们在执行后续方法 String dictIdFieldName = dictReplace.dictIdFieldName();//获取注解属性值 Field idField = null; if(ToolUtil.isNotEmpty(dictIdFieldName)){ idField = cls.getDeclaredField(dictIdFieldName);//获取实体类对应字段 }else{ String fieldName = field.getName();//获取实体类字段名称 String idFieldName = fieldName.substring(0,fieldName.length()-4); idField = cls.getDeclaredField(idFieldName); } idField.setAccessible(true);//允许我们在用反射时访问私有变量 Object dictId = idField.get(result);//从返回值中获得字段对应的 值 field.setAccessible(true); if(ToolUtil.isNotEmpty(dictId)){ field.set(result, constantFactory.getDictName( Long.valueOf(new String(dictId.toString())) ) ); //用字典id查询出字典名称 并替换结果集中的值 } } } } } }catch (Exception e) { e.printStackTrace(); }finally{ return results; } } @Override public Object plugin(Object target) { // 读取@Signature中的配置,判断是否需要生成代理类 if (target instanceof ResultSetHandler) { return Plugin.wrap(target, this);//返回代理 } else { return target; } } @Override public void setProperties(Properties properties) { this.properties = properties; } }
2019年4月16日更新,为了使用mybaits缓存机制减少数据库负担,将部分代码改写
@Intercepts({ @Signature(type = ResultSetHandler.class,method = "handleResultSets", args = { Statement.class }) }) public class DictReplaceInteceptor implements Interceptor{ private Properties properties; private SpringContextHolder spring; @Override @SuppressWarnings(value = {"all"}) public Object intercept(Invocation invocation) throws Throwable { // List<Object> results = (List<Object>)invocation.proceed(); SqlSessionFactory sqlSessionFactory = spring.getBean(SqlSessionFactory.class); SqlSession sqlSession = sqlSessionFactory.openSession(); try{ if(ToolUtil.isNotEmpty(results)){ ConstantFactory constantFactory = spring.getBean(ConstantFactory.class); Class<?> cls = results.get(0).getClass(); Field[] fields = cls.getDeclaredFields();// 暴力获取private修饰的成员变量 获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段。 for(Object result:results){ for (Field field : fields) { DictReplace dictReplace = field.getAnnotation(DictReplace.class); if(dictReplace!=null){ String dictIdFieldName = dictReplace.dictIdFieldName(); Field idField = null; if(ToolUtil.isNotEmpty(dictIdFieldName)){ idField = cls.getDeclaredField(dictIdFieldName); }else{ String fieldName = field.getName(); String idFieldName = fieldName.substring(0,fieldName.length()-4); idField = cls.getDeclaredField(idFieldName); } idField.setAccessible(true); Object dictId = idField.get(result); field.setAccessible(true); if(ToolUtil.isNotEmpty(dictId)){ if (ToolUtil.isEmpty(dictId)) { return ""; } else {
//以前是直接调用方法,每次调用调用都会创建,现在通过sqlSession获取对应的mapper 避免每次都创建 DictMapper dictMapper = sqlSession.getMapper(DictMapper.class); Dict dict = dictMapper.selectById(new String(dictId.toString())); if (dict == null) { } else { field.set(result, dict.getName()); } } } } } } } }catch (Exception e) { e.printStackTrace(); }finally{ sqlSession.close(); return results; } } @Override public Object plugin(Object target) { // 读取@Signature中的配置,判断是否需要生成代理类 if (target instanceof ResultSetHandler) { return Plugin.wrap(target, this); } else { return target; } } @Override public void setProperties(Properties properties) { this.properties = properties; } }
再举一个例子
注解对象
@Target({ElementType.FIELD})// @Retention(RetentionPolicy.RUNTIME)//该注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在; @Inherited//允许子类继承父类的注解。 (子类中可以获取并使用父类注解) @Documented//指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。 public @interface One2One { String byField(); Class resultType(); Class mapper(); String methodName(); }
拦截器
import com.msunsoft.base.common.factory.ConstantFactory; import com.msunsoft.base.common.interceptor.annotation.One2One; import com.msunsoft.base.spring.SpringContextHolder; import com.msunsoft.base.util.ToolUtil; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.Statement; import java.util.List; import java.util.Properties; @Intercepts({ @Signature(type = ResultSetHandler.class,method = "handleResultSets", args = { Statement.class }) }) public class One2OneInteceptor implements Interceptor { private Properties properties; private SpringContextHolder spring; @Override @SuppressWarnings(value = {"all"}) public Object intercept(Invocation invocation) throws Throwable { // List<Object> results = (List<Object>)invocation.proceed(); SqlSessionFactory sqlSessionFactory = spring.getBean(SqlSessionFactory.class); SqlSession sqlSession = sqlSessionFactory.openSession(); try{ if(ToolUtil.isNotEmpty(results)){ ConstantFactory constantFactory = spring.getBean(ConstantFactory.class); Class<?> cls = results.get(0).getClass(); Field[] fields = cls.getDeclaredFields();// 暴力获取private修饰的成员变量 获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段。 for(Object result:results){ for (Field field : fields) { field.setAccessible(true); One2One one2One = field.getAnnotation(One2One.class); if(one2One!=null){ String byFieldString = one2One.byField(); Class resultType = one2One.resultType(); Class mapper = one2One.mapper(); String methodName = one2One.methodName(); Object objMaper = sqlSession.getMapper(mapper); Method method = mapper.getMethod(methodName, Serializable.class); Field byField = cls.getDeclaredField(byFieldString); byField.setAccessible(true); field.set(result, method.invoke(objMaper,byField.get(result))); } } } } }catch (Exception e) { e.printStackTrace(); }finally{ sqlSession.close(); return results; } } @Override public Object plugin(Object target) { // 读取@Signature中的配置,判断是否需要生成代理类 if (target instanceof ResultSetHandler) { return Plugin.wrap(target, this); } else { return target; } } @Override public void setProperties(Properties properties) { this.properties = properties; } }
使用实例(实体类中)
//项目编号 @TableField("project_id") private Long projectId; //项目信息 @TableField(exist = false) @One2One(byField = "projectId",resultType = Project.class,mapper= ProjectMapper.class,methodName = "selectById") private Project project;
2.2 拦截器部分知识点
2.1.1 MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
2.1.2 MyBatis拦截器的接口定义
一共有三个方法intercept
、plugin
、setProperties
setProperties()
方法主要是用来从配置中获取属性。
plugin()
方法用于指定哪些方法可以被此拦截器拦截。
intercept()
方法是用来对拦截的sql
进行具体的操作。
注解实现
MyBatis
拦截器用到了两个注解:@Intercepts
和@Signature
@Intercepts( { @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), } )
type
的值与类名相同,method
与方法名相同,为了避免方法重载,args
中指定了各个参数的类型和个数,可通过invocation.getArgs()
获取参数数组。
2.1.3 Spring Boot整合
方法一
如果是使用xml式配置拦截器,可在Mybatis配置文件中添加如下节点,属性可以以如下方式传递
<plugins> <plugin interceptor="tk.mybatis.simple.plugin.XXXInterceptor"> <property name="propl" value="valuel" /> <property name="prop2" value="value2" /> </plugin> </plugins>
方法二
如果在Spring boot
中使用,则需要单独写一个配置类,如下:
@Configuration public class MybatisInterceptorConfig { @Bean public String myInterceptor(SqlSessionFactory sqlSessionFactory) { ExecutorInterceptor executorInterceptor = new ExecutorInterceptor(); Properties properties = new Properties(); properties.setProperty("prop1","value1"); executorInterceptor.setProperties(properties); return "interceptor"; } }
OR
import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.msunsoft.base.common.interceptor.mybaits.DictReplaceInteceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @EnableTransactionManagement @MapperScan("com.msunsoft.**.mapper")//Mapper接口扫描 public class DataSourceConfig { /** * 乐观锁mybatis插件 */ @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } /** * mybatis-plus分页插件 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } @Bean public DictReplaceInteceptor dictReplaceInteceptor(){ return new DictReplaceInteceptor(); } }
方法三
在拦截器上加@Component注解
ps:
一、引用并参考
1.《深入理解mybatis原理》 MyBatis的架构设计以及实例分析
https://blog.csdn.net/luanlouis/article/details/40422941
2.关于mybatis拦截器,对结果集进行拦截
https://www.cnblogs.com/SmallHan/articles/8127327.html
3.Springboot2(22)Mybatis拦截器实现
https://blog.csdn.net/cowbin2012/article/details/85256360
二、涉及技术点
spring(注解、AOP) ,java反射与动态代理,mybaits(以上代码示例用的是mybaits-Plus 3.0.6.jar),