mybatis框架(七)——插件

引言

            插件——用来解决特定功能需求的配置拦截器的一种方式,起到相当于插件的作用。

概述

            插件定义:在sqlSession四大对象调度的过程中,插入自定义代码执行特殊的功能满足特殊的需求。

内容

           1 接口:mybatis使用插件,必须实现接口Interceptor   

	public interface Interceptor{
		Object intercept(Invocation invocation) throws Throwable;
			
		Object plugin(Object target);
			
		void setProperties(Properties properties);
	}
	说明,方法定义
		1)intercept方法:直接覆盖拦截对象的原有方法,是插件的核心方法;参数Invocation对象,通过它反射调用原来对象的方法。
		2)plugin方法:target是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它
		3)setProperties方法:为plugin元素配置属性参数,插件初始化时调用,将插件对象存入到配置中,便于后面使用。

         2 初始化:MyBatis初始化开始就会对插件进行初始化

	private void pluginElement(XNode parent) throw Exception{
		if(parent != null){
			for(XNode child : parent.getChildren()){
				String interceptor = child.getStringAttribute("interceptor");
				Properties properties = child.getChildrenAsProperties();
				//通过反射生成插件实例
				Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
				//设置配置参数
				interceptorInstance.setProperties(properties);
				//将插件实例保存到配置对象中
				configuration.addInterceptor(interceptorInstance);
			}
		}

        }
         3 保存:插件保存在Configuration对象中

	public void addInterceptor(Interceptor interceptor){
		interceptorChain.addInterceptor(interceptor);
	}
	interceptorChain是Configuration的属性对象,包含一个addInterceptor方法,初始化之后插件就保存在List对象interceptors里面,使用时取出来即可。
	private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
	......
	public void addInterceptor(Interceptor interceptor){
		interceptors.add(interceptor);
	}
         4 常用工具类(MetaObject)

	1)MetaObject forObject(Object object、ObjectFactory objectFactory、ObjectWrapperFactory objectWrapperFactory)方法用于包装对象。
           本方法不再使用,通过MyBatis为我们提供的SystemMetaObject.forObject(Object obj)。
	2)Object getValue(String name)方法用于获取对象属性值,支持OGNL
	3)void setValue(String name、Object value)方法用于修改对象属性值,支持OGNL
         5 插件开发过程

              (1)确定需要拦截的签名:运行插件需要注册签名

                      1)根据功能确定需要拦截的对象

                           Executor:执行SQL的全过程,包括组装参数,组装结果集返回和执行SQL过程,都可以拦截;

                           StatementHandler:执行SQL的过程,可以重写SQL执行过程,常用的拦截对象;

                           ParameterHandler:主要拦截执行SQL的参数组装,可以重写组装参数规则;

                           ResultSetHandler:用于拦截执行结果的组装,也可以重写组装结果的规则;

                           需要拦截的是StatementHandler对象,在预编译SQL之前,修改SQL使得结果返回数量被限制。

                      2)拦截方法和参数:查询的过程是通过Executor调度StatementHandler来完成的,StatementHandler的prepare方法预编译SQL,于是我们需要拦截的方法便是prepare方法,在这之前重写SQL语句。

                          StatementHandler接口的定义:

		public interface StatementHandler{
			Statement prepare(Connection connection) throws SQLException;
				
			void parameterize(Statement statement) throws SQLException;
					
			void batch(Statement statement) throws SQLException;
					
			int update(Statement statement) throws SQLException;
					
			<E> List<E> query(Statement statement,ResultHandler resultHandler) throws SQLException;
				
			BoundSql getBoundSql();
					
			ParameterHandler getParameterHandler();
					
		}
                         以上的任何方法都可以拦截,从接口定义而言,prepare方法有一个参数Connection对象,那么如何设计一个拦截器?
         @Intercepts({ @Signature(type = StatementHandler.class,method = "prepare" , args={Connection.class}) }) 
         public class MyPlugin implements Intercept{ ........ } 

                其中,@Intercepts说明它是一个拦截器。@Signature用来注册拦截器签名,只要签名满足条件才能拦截,type可以是四大对象中的一个,这里是StatementHandlermethod代表要拦截四大对象的某一种接口方法, 而args则表示该方法的参数,需要根据拦截对象的方法参数进行设置。

              (2)实现拦截方法:实现了简单的打印顺序功能

		@Intercepts({@Signature(
			type = Executor.class,      //确定要拦截的对象
			method="update",            //确定要拦截的方法
			args = {MappedStatement.class,Object.class}  //拦截方法的参数
		)})
			
		public class MyPlugin implements Intercept{
			Properties props = null;
			/**
			 * 代替拦截对象方法的内容
			 * @param invocation 责任链对象
			 */
			@Override
			public Object intercept(Invocation invocation) throws Throwable{
				System.err.println("before....");
				//如果当前代理的是一个非代理对象,那么它就会调用真实拦截对象的方法,如果不是它会调用下个插件代理对象的invoke方法
				Object obj = invocation.proceed();
				System.err.println("after......");
				return obj;
			}
			/**
			 * 生成对象的代理,这里常用MyBatis提供的Plugin类的wrap方法
			 * @param target 被代理的对象
			 */
			@Override
			public Object plugin(Object target){
				//使用MyBatis提供的Plugin类生成代理对象
				System.err.println("使用生成代理对象....");
				return Plugin.wrap(target,this);
			}
			/**
			 * 获取插件配置的属性,我们在MyBatis的配置文件里面去配置
			 * @param props 是MyBatis配置的参数
			 */
			public void setProperties(Properties props){
				System.err.println(props.get("dbType"));
				this.props = props;
			}
		}
              (3)配置插件

		<plugins>
			<plugin interceptor = "xxx.MyPlugin">
				<property name="dbType" value="mysql" />
			</plugin>
		</plugins>
              (4)插件实例

                      1)实例场景:大型的互联网系统,假如我们数据库使用的MySQL数据库,想要对数据库查询返回数据量需要限制,以避免数据量过大造成网站瓶颈,配置限制50条数据。

                      2)实现步骤:首先我们先确定需要拦截四大对象中的哪一个,根据功能需要修改SQL的执行。由SqlSession运行原理表明拦截对象是StatementHandler,因为它的prepare方法用来编译SQL语句,我们可以在预编译前修改语句来满足我们的需求。通过StatementHandler的prepare()方法,在它预编译前,需要重写SQL,达到限制数据量的要求,它有一个参数(Connection connection),我们就轻易地得到了签名注解。

                     3)代码实现:                            

@Intercepts({@Signature(
			type = Executor.class,      //确定要拦截的对象
			method="update",            //确定要拦截的方法
			args = {MappedStatement.class,Object.class}  //拦截方法的参数
		)})
				
		public class MyPlugin implements Intercept{
			Properties props = null;
			/**
			 * 代替拦截对象方法的内容
			 * @param invocation 责任链对象
			 */
			@Override
			public Object intercept(Invocation invocation) throws Throwable{
				System.err.println("before....");
				//如果当前代理的是一个非代理对象,那么它就会调用真实拦截对象的方法,如果不是它会调用下个插件代理对象的invoke方法
				Object obj = invocation.proceed();
				System.err.println("after......");
				return obj;
			}
			/**
			 * 生成对象的代理,这里常用MyBatis提供的Plugin类的wrap方法
			 * @param target 被代理的对象
			 */
			@Override
			public Object plugin(Object target){
				//使用MyBatis提供的Plugin类生成代理对象
				System.err.println("使用生成代理对象....");
				return Plugin.wrap(target,this);
			}
			/**
			 * 获取插件配置的属性,我们在MyBatis的配置文件里面去配置
			 * @param props 是MyBatis配置的参数
			 */
			public void setProperties(Properties props){
				System.err.println(props.get("dbType"));
				this.props = props;
			}
		}
				
		<plugins>
			<plugin interceptor = "xxx.MyPlugin">
				<property name="dbType" value="mysql" />
			</plugin>
		</plugins>
				
		@Intercepts({ @Signature(type = StatementHandler.class, //确定要拦截的对象
		method = "prepare",  //确定要拦截的方法
		args = { Connection.class}) //拦截方法的参数
		})
				
		public class QueryLimitPlugin implements Interceptor{
			//默认限制查询返回行数
			private int limit;
					
			private String dbType;
					
			//限制表中间别名,避免表重名起得比较特殊
			private static final String LMT_TABLE_NAME = "limit_Table_Name_person";
					
			@Override
			public Object intercept(Invocation invocation) throws Throwable{
				//取出被拦截对象
				StatementHandler stmtHandler = (StatementHandler) invocation.getTarget();
				MetaObject metaStmtHandler = SystemMetaObject.forObject(stmtHandler);
						
				// 分离代理对象,从而形成多次代理,通过俩次循环最原始的被代理类,MyBatis使用的是JDK代理
				while (metaStmtHandler.hasGetter("h")){
					Object object = metaStmtHandler.getValue("h");
					metaStmtHandler = SystemMetaObject.forObject(object);
				}
						
				// 分离最后一个代理对象的目标类
				while (metaStmtHandler.hasGetter("target")){
					Object object = metaStmtHandler.getValue("target");
					metaStmtHandler = SystemMetaObject.forObject(object);
				}
						
				// 取出即将要执行的SQL
				String sql = (String)metaStmtHandler.getValue("delegate.boundSql.sql");
				String limitSql;
						
				//判断参数是不是MySQL数据库且SQL有没有被插件重写过
				if ("mysql".equals(this.dbType) && sql.indexOf(LMT_TABLE_NAME) == -1){
					//去掉前后空格
					sql = sql.trim();
					//将参数写入SQL
					limitSql = "select * from (" + sql +") " + LMT_TABLE_NAME + " limit " + limit;
					//重写要执行的SQL
					metaStmtHandler.setValue("delegate.boundSql.sql", limitSql);
				}
					//调用原来对象的方法,进入责任链的下一层级
					return invocation.proceed();
				}
					
				@Override
				public Object plugin(Object target){
					//使用默认的MyBatis提供的类生成代理对象
					return Plugin.wrap(target, this);
				}
					
				@Override
				public void setProperties(Properties props){
					String strLimit = (String)props.getProperty("limit", "50");
					this.limit = Integer.parseInt(strLimit);
					//读取设置的数据库类型
					this.dbType = (String) props.getProperty("dbType", "mysql");
				}	
		}
                说明:在setProperties方法中可以读入配置给插件的参数,“limit”是数据库的名称,“50”是限制记录数;
                      在MyBatis初始化的时候就已经被设置好,需要的时候直接使用即可;
                      在plugin方法里,使用MyBatis提供的类target来生成代理对象,插件进入plugin的invoke方法;
                  最后使用到拦截器的intercept方法;
                      插件QueryLimitPlugin的intercept方法就会覆盖掉StatementHandler的prepare方法,先从代理对象
                  分离出真实对象,然后根据需要修改SQL,来达到限制返回行数的目的;
                      再然后使用invocation.proceed()来调度真实StatementHandler的prepare方法完成SQL预编译;
                      最后需要在MyBatis配置文件里才能运行这个插件

			<plugins>
				<plugin interceptor="com.learn.chapter7.plugin.QueryLimitPlugin">
					<property name="dbType" value="mysql"/>
					<property name="limit" value="50"/>
				</plugin>
			</plugins>

 总结

          (1)插件修改MyBatis的底层设计,尽量少用插件;

        (2)插件生成的原理是层层代理对象的责任链模式,通过反射方法运行,性能不高,减少插件就能减少代理,提高系统性能;

        (3)编写插件需要了解MyBatis的运行原理,了解四大对象及其方法的作用,准备判断需要拦截什么对象,什么方法,参数是什么,才能确定如何编写签名;

        (4)插件需要读取和修改MyBatis映射器中的对象属性

        (5)多个插件层层代理,保证逻辑的正确性

        (6)尽量少改动MyBatis底层,减少错误


            

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值