深入 Mybatis 插件机制

在使用 Mybatis 查询时,通常会使用 PageHelper 分页插件分页,这样就可以不在 SQL 中写 limitcount ,比较方便。
除此之外, Mybatis 插件还常用于数据脱敏,自动填充字段(全局唯一Id,数据生成时间,数据创建者,数据修改时间,数据修改者等等),数据库字段加密,水平分表,菜单权限控制,黑白名单等。
Mybatis 插件用法和 Spring AOP 环绕通知很像,但是定义切点方式不同, 具体使用方法,可参考官方文档
Spring AOP 是通过代理实现,那 Mybatis 插件呢?

一、JDK 动态代理实现 Mybatis 插件链

其实,Mybatis 插件是通过 JDK 动态代理实现,回顾下 JDK 动态代理实现方式,假设有 Aircraft 接口:

public interface Aircraft {
    void fly();
}

并有其实现类 Airplane

public class Airplane implements Aircraft {
    @Override
    public void fly() {
        System.err.println("I'm flying");
    }
}

如果需要在 Aircraftfly() 方法前后执行额外逻辑,如果记录日志,可以使用 JDK 动态代理:

public class Plugin implements InvocationHandler {

    /**
     * 目标对象
     */
    private Object target;

    public Plugin (Object target) {

        super();
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.err.println("Started"); // 记录前置日志
        Object invoked = method.invoke(target, args); // 执行目标对象 fly()
        System.err.println("Stopped"); // 记录后置日志

        return invoked;
    }

    public static Object wrap(Object target) {

        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
                new Plugin (target));
    }

    public static void main(String[] args) {

        Aircraft aircraft = (Aircraft) Plugin.wrap(new Airplane());
        aircraft.fly();
        // Started
		// I'm flying
		// Stopped
    }
}

1. 优化 —— 单一职责

JDK 动态代理可以在不修改源代码基础上增强原有功能,解决了开闭原则问题(直接修改目标对象 fly() 增加额外功能违反开闭原则),但是违反了单一职责原则,动态代理功能和日志记录都在 Plugin 中实现, Plugin 可以理解为动态代理工具类,不应该包含业务逻辑。
为了解决这个问题,可以将拦截处理逻辑抽象出来,定义如下接口:

public interface Interceptor {
    /**
     * 拦截处理逻辑
     */
    void intercept();
}

定义前置,后置拦截日志 Interceptor 实现类:

public class StartedInterceptor implements Interceptor {
    @Override
    public void intercept() {
        System.err.println("Started");
    }
}
public class StoppedInterceptor implements Interceptor{
    @Override
    public void intercept() {
        System.err.println("Stopped");        
    }
}

Plugin 需要作出相应调整:

public class Plugin implements InvocationHandler {

    /**
     * 目标对象
     */
    private Object target;

    private List<Interceptor> interceptors = Lists.newArrayList();

    public Plugin(Object target, List<Interceptor> interceptors) {

        super();
        this.target = target;
        this.interceptors = interceptors;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		
        for (Interceptor interceptor : interceptors) {
            interceptor.intercept(); // 拦截增强逻辑
        }
        Object invoked = method.invoke(target, args); // 执行目标对象 fly()

        return invoked;
    }

    public static Object wrap(Object target, List<Interceptor> interceptors) {

        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
                new Plugin(target, interceptors));
    }

    public static void main(String[] args) {

        Aircraft aircraft = (Aircraft) Plugin.wrap(new Airplane(),
                Lists.newArrayList(new StartedInterceptor(), new StoppedInterceptor()));
        aircraft.fly();
        // Started
		// Stopped
		// I'm flying
    }

}

2. 再优化 —— “环绕通知”

从上面代码可以看出,这样做是有缺陷的 —— 将拦截处理逻辑抽象后,只能全部在目标对象方法前或者后执行,无法实现“环绕通知”,因为在 Interceptor 接口 intercept() 内部无法感知目标对象方法 method.invoke(target, args) 执行时机。要在 Interceptor 中实现“环绕通知”,需要将目标对象方法执行放到 intercept() 内部,所以对目标对象方法执行参数进行封装:

public class Invocation {

    /**
     * 目标对象
     */
    private Object target;

    /**
     * 目标对象需要执行的方法
     */
    private Method method;

    /**
     * 方法参数
     */
    private Object[] args;

    public Invocation(Object target, Method method, Object[] args) {

        this.target = target;
        this.method = method;
        this.args = args;
    }

    /**
     * 执行目标对象 fly()
     */
    public Object proceed() throws Exception {

        return method.invoke(target, args);
    }
	// 省略getter/setter
}

Interceptor拦截逻辑处理接口修改为:

public interface Interceptor {
    /**
     * 拦截处理逻辑
     */
    Object intercept(Invocation invocation) throws Exception;
}

其实现类 AroundInterceptor

public class AroundInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Exception {

        System.err.println("Started"); // 记录前置日志
        Object proceeded = invocation.proceed();
        System.err.println("Stopped"); // 记录后置日志

        return proceeded;
    }
}

Plugin 做相应调整:

public class Plugin implements InvocationHandler {

    /**
     * 目标对象
     */
    private Object target;

    private Interceptor interceptor;

    public Plugin(Object target, Interceptor interceptor) {

        super();
        this.target = target;
        this.interceptor = interceptor;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        return interceptor.intercept(new Invocation(target, method, args));
    }

    public static Object wrap(Object target, Interceptor interceptor) {

        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
                new Plugin(target, interceptor));
    }

    public static void main(String[] args) {

        Aircraft aircraft = (Aircraft) Plugin.wrap(new Airplane(), new AroundInterceptor());
        aircraft.fly();
        // Started
		// I'm flying
		// Stopped
    }
}

3. 再再优化 —— 插件“挂载”

优化后已经可以实现“环绕通知”了,但是 main 函数中这样去调用看上去有点别扭,拦截处理逻辑好比装饰品一样,我们希望像在房间里挂挂件一样将 Interceptor 挂载到目标对象上, 而不是 Plugin 对象中 wrap 方法将插件和目标对象一起处理。
为了实现这个功能,需要在 Interceptor 中增加方法 plugin ,表示挂载到目标对象:

public interface Interceptor {

    /**
     * 拦截处理逻辑
     */
    Object intercept(Invocation invocation) throws Exception;

    /**
     *  挂载到目标对象
     */
    Object plugin(Object target);
}

其实现类修改为:

public class AroundInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Exception {

        System.err.println("Started"); // 记录前置日志
        Object proceeded = invocation.proceed();
        System.err.println("Stopped"); // 记录后置日志

        return proceeded;

    }

    @Override
    public Object plugin(Object target) {

        return Plugin.wrap(target, this);
    }
}

测试方法可以修改为如下:

public static void main(String[] args) {
	Aircraft aircraft = (Aircraft) new AroundInterceptor().plugin(new Airplane());
	aircraft.fly();
}

这样就好理解多了,新建一个插件类 new AroundInterceptor() 然后将其挂载到目标对象 new Airplane() 上。但是,如果在目标对象上挂载多个插件呢?
注意观察,返回挂载后返回对象不还是 Aircraft 吗,可以继续将其余插件挂载到 aircraft 上。
新建时间积累拦截器:

public class RecordedInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Exception {

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        System.err.println("Current time「 " + LocalDateTime.now().format(dateTimeFormatter) + "」");
        Object proceeded = invocation.proceed();
        System.err.println("Current time「 " + LocalDateTime.now().format(dateTimeFormatter) + "」");
        return proceeded;

    }

    @Override
    public Object plugin(Object target) {

        return Plugin.wrap(target, this);
    }

}

AroundInterceptorRecordedInterceptor 一起挂载到目标对象 Airplane

public static void main(String[] args) {

    Aircraft aircraft = (Aircraft) new AroundInterceptor().plugin(new Airplane());
    aircraft = (Aircraft) new RecordedInterceptor().plugin(aircraft);
    aircraft.fly();
    // Current time「 2021-12-08 21:56:53」
	// Started
	// I'm flying
	// Stopped
	// Current time「 2021-12-08 21:56:53」
}

如果还有其他拦截器,还可以通过 (Aircraft) new RecordedInterceptor().plugin(aircraft) 返回的 aircraft 继续挂载。

4. 最终版 —— 插件链“挂载”

这已经近乎完美,但是如果有很多插件,这样依次挂载显得很麻烦,冗余代码较多,对此,可以定义 InterceptorChain 封装所有插件,并定义方法 pluginAll() 将所有插件挂载到目标对象上:

public class InterceptorChain {

    private final List<Interceptor> interceptors = Lists.newArrayList();

    /**
     * 挂载所有插件 
     */
    public Object pluginAll(Object target) {

        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    public void addInterceptor(Interceptor interceptor) {

        interceptors.add(interceptor);
    }

    public List<Interceptor> getInterceptors() {

        return Collections.unmodifiableList(interceptors);
    }

}

测试类做如下调整:

public static void main(String[] args) {

    InterceptorChain chain = new InterceptorChain();
    chain.addInterceptor(new AroundInterceptor());
    chain.addInterceptor(new RecordedInterceptor());
    Aircraft aircraft = (Aircraft) chain.pluginAll(new Airplane());
    aircraft.fly();
    // Current time「 2021-12-08 22:54:33」
	// Started
	// I'm flying
	// Stopped
	// Current time「 2021-12-08 22:54:33」
}

二、Mybatis 插件原理

我们最终版其实和 Mybatis 插件很像了,我们抽象出的类可以和 Mybatis 如下类对比:

  • 插件, org.apache.ibatis.plugin.Interceptor
  • 插件链,org.apache.ibatis.plugin.InterceptorChain
  • 动态代理工具类,org.apache.ibatis.plugin.Plugin
  • 目标对象执行方法及其参数封装,org.apache.ibatis.plugin.Invocation

1. 插件 Interceptor

Mybatis 插接口 Interceptor 不仅定义了执行增强逻辑方法 intercept(Invocation invocation) 和将插件挂载到目标对象方法 plugin(Object target),还定义了设置配置属性方法 setProperties(Properties properties) ,这些配置属性在执行增强逻辑时可能用到。

public interface Interceptor {
	Object intercept(Invocation invocation) throws Throwable;
	Object plugin(Object target);
	void setProperties(Properties properties);
}

2. 插件链 InterceptorChain

org.apache.ibatis.session.Configuration 是 Mybatis 及其重要的一个类,其中包含了几乎所有 Mybatis 配置信息,其中就包含插件链 interceptorChain注意,用户注册的所有插件(ParameterHandler 插件,ResultSetHandler 插件,StatementHandler 插件,Executor 插件)都保存在interceptorChain 中。

public class InterceptorChain {

	private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
	
	public Object pluginAll(Object target) {
		for (Interceptor interceptor : interceptors) {
		target = interceptor.plugin(target);
	}
	return target;
	}
	
	public void addInterceptor(Interceptor interceptor) {
		interceptors.add(interceptor);
	}
	
	public List<Interceptor> getInterceptors() {
		return Collections.unmodifiableList(interceptors);
	}
}

并且在 Configuration 中还提供了创建目标对象工厂方法 newParameterHandler()newResultSetHandler()newStatementHandler()newExecutor(),这些工厂方法在生成目标对象同时还向目标方法挂载了插件。

public class Configuration {
	// ...
	protected final InterceptorChain interceptorChain = new InterceptorChain();
	// ...
	public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
		ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
		parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
		return parameterHandler;
	}

	public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
		ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
		resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
		return resultSetHandler;
	}

	public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
		StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
		statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
		return statementHandler;
	}

	public Executor newExecutor(Transaction transaction) {
		return newExecutor(transaction, defaultExecutorType);
	}

	public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
		executorType = executorType == null ? defaultExecutorType : executorType;
		executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
		Executor executor;
		if (ExecutorType.BATCH == executorType) {
			executor = new BatchExecutor(this, transaction);
		} else if (ExecutorType.REUSE == executorType) {
			executor = new ReuseExecutor(this, transaction);
		} else {
			executor = new SimpleExecutor(this, transaction);
		}
		if (cacheEnabled) {
			executor = new CachingExecutor(executor);
		}
		executor = (Executor) interceptorChain.pluginAll(executor);
		return executor;
	}
	// ...
}

3. 动态代理工具类 Plugin

Configuration 中只提供了 ParameterHandlerResultSetHandlerStatementHandlerExecutor工厂方法,这就意味着 Mybatis 插件只能对这几个类进行拦截,但是在这些工厂方法中,均是调用 interceptorChainpluginAll 将所有插件向新建对象上挂载,那我们插件拦截最小粒度就是这四个目标类所有方法吗?
其实不是的,在 Interceptor 插件中,还可以使用 @Intercepts 注解标识该插件具体拦截哪个类的哪个方法,最小粒度可以控制到方法级别。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
	Signature[] value();
}

参数 Signature[] 数组表明该插件拦截所有方法签名:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  Class<?> type();  // 目标类
  String method();  // 目标类方法
  Class<?>[] args();  // 目标方法参数
}

比如分页插件 PageHelper 该注解为:

@Intercepts({@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class PageHelper extends BasePageHelper implements Interceptor {
	// ...
}

表示 PageHelper 插件只拦截 Executor 类中 query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException 方法。

public interface Executor {

	ResultHandler NO_RESULT_HANDLER = null;
	
	int update(MappedStatement ms, Object parameter) throws SQLException;
	
	<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
	
	<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
	
	<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
	// ...
}

Plugin 是动态代理工具方法,在各个插件 plugin(Object target) 方法将该插件挂载到时,通常是调用 Pluginwrap(Object target, Interceptor interceptor) 方法,比如 PageHelperplugin(Object target) 方法:

public Object plugin(Object target) {
	return Plugin.wrap(target, this);
}

Configuration 将插件链 interceptorChain 所有插件都向目标对象挂载,那么Pluginwrap(Object target, Interceptor interceptor) 在将插件挂载到目标类是就需要根据该插件 @Intercepts 注解确定能否挂载,能挂载时才挂载:

public class Plugin implements InvocationHandler {

	private final Object target;
	private final Interceptor interceptor;
	private final Map<Class<?>, Set<Method>> signatureMap;
	
	private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
		this.target = target;
		this.interceptor = interceptor;
		this.signatureMap = signatureMap;
	}
	
	public static Object wrap(Object target, Interceptor interceptor) {
		Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
		Class<?> type = target.getClass();
		Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
		// 如果 interceptor 拦截了 target 继承体系中任意类,则将 interceptor 挂载到 target 上
		if (interfaces.length > 0) { 
			return Proxy.newProxyInstance(
			type.getClassLoader(),
			interfaces,
			new Plugin(target, interceptor, signatureMap));
		}
		return target;
	}
	
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		try {
			// 插件挂载只能在类层面过滤, 在执行目标方法时需要判断插件是否是否拦截了该方法
			Set<Method> methods = signatureMap.get(method.getDeclaringClass());
		if (methods != null && methods.contains(method)) {
			return interceptor.intercept(new Invocation(target, method, args));
		}
		return method.invoke(target, args);
		} catch (Exception e) {
			throw ExceptionUtil.unwrapThrowable(e);
		}
	}
	// 获取该插件拦截了哪些方法
	// key : 拦截的目标类, 可选值[ParameterHandler, ResultSetHandler, StatementHandler, Executor]
	// value : 拦截目标类里面那些方法
	private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
		Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // 插件 Intercepts 注解
		// issue #251
		if (interceptsAnnotation == null) {
			throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
		}
		// 拦截方法签名
		Signature[] sigs = interceptsAnnotation.value();
		Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
		for (Signature sig : sigs) {
			Set<Method> methods = signatureMap.get(sig.type());
			if (methods == null) {
				methods = new HashSet<Method>();
				signatureMap.put(sig.type(), methods);
			}
			try {
				Method method = sig.type().getMethod(sig.method(), sig.args()); // 通过方法名和参数获取目标方法
				methods.add(method);
			} catch (NoSuchMethodException e) {
				throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
			}
		}
		return signatureMap;
	}
	
	//  查询插件接口签名 signatureMap 拦截了哪些目标类 type 继承结构中的类
	private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
		Set<Class<?>> interfaces = new HashSet<Class<?>>();
		while (type != null) { // 从 type 出发自下而上遍历所有父类
			for (Class<?> c : type.getInterfaces()) {
				if (signatureMap.containsKey(c)) { // 如果被插件拦截则加r
					interfaces.add(c);
				}
			}
			type = type.getSuperclass();
		}
		return interfaces.toArray(new Class<?>[interfaces.size()]);
	}

}

4. Invocation

Invocation 需要将目标类,目标对象执行方法以及方法参数封装给插件,这样插件就可以实现“环绕通知”。

public class Invocation {

	private final Object target;
	private final Method method;
	private final Object[] args;
	
	// 省略构造方法及getter/setter	
}
  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值