本章节的目标就是用户在xml配置了类,我们需要实现在某处拦截执行用户配置的实现类,而这部分功能就是mybaties的插件扩展。
插件主要的设计理念就是依赖倒置,插件功能依赖于接口,而不是具体的实现,不依赖具体实现,在这个过程中对抽象进行编程,不对实现进行编程,这样就降低了客户与实现模块间的耦合。
而技术实现就需要用动态代理,我们需要用户配置被代理对象,被代理的方法以及参数,框架进行拦截操作,通过配置的方式把实现添加到Mybaties中(如图一),这样在执行插件代理方法invoke()时,就可以直接实现对应的实现类。看图二,用户注解的配置中,StatementHandler当作代理对象,方法是prepare,参数是Connection,那么当我们执行StatementHandler和其子类时并且方法是prepare的化就会执行代理invoke(),此时就会判断调度到用户的实现类里。
?
看xml类图说明,
1.由XMLConfigBuilder开始进行解析插件内容,为什么从这里开始,因为我们的配置在mybatis-config-datasource.xml中(如下图),解析完毕将拦截器(用户实现)对象放入Configuration的拦截器链条中(InterceptorChain),
2.在执行对应Sql之前(newStatementHandler方法处),我们在此设置包装了代理方法,代理的是PreparedStatementHandler,是StatementHandler的子类,进入包装代理方法就会读取到用户实现类的注释,如Intercepts以及Signature注释得到被代理的类和方法以及参数,最后返回代理类。
3.此时StatementHandler已经被代理,所以执行prepare()时直接执行Plugin类下的incoke()方法,这时就执行用户的实现操作,用户操作完放行,这时继续执行框架里的操作。
XMLConfigBuilder类里:此类添加了解析Plugin的方法,拿到了interceptor属性的插件实现类,以及对应的属性名称和值,最后将interceptor放入到拦截链条里(interceptorChain)
public class XMLConfigBuilder extends BaseBuilder {
// 省略其他方法...
public Configuration parse() {
try {
// 插件 step-16 添加
pluginElement(root.element("plugins"));
// 环境
environmentsElement(root.element("environments"));
// 解析映射器
mapperElement(root.element("mappers"));
} catch (Exception e) {
throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
return configuration;
}
// 解析插件
private void pluginElement(Element parent) throws Exception {
if (parent == null) return;
List<Element> elements = parent.elements();
for (Element element : elements) {
String interceptor = element.attributeValue("interceptor");
Properties properties = new Properties();
List<Element> propertyElementList = element.elements("property");
for (Element property : propertyElementList) {
properties.setProperty(property.attributeValue("name"), property.attributeValue("value"));
}
// 获取插件实现类并实例化:cn.bugstack.mybatis.test.plugin.TestPlugin
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
// 存储到拦截器链条里
configuration.addInterceptor(interceptorInstance);
}
}
}
Configuration:此类添加存储interceptor操作。
public class Configuration {
// 省略其他....
// 插件拦截器链
protected final InterceptorChain interceptorChain = new InterceptorChain();
public void addInterceptor(Interceptor interceptorInstance) {
interceptorChain.addInterceptor(interceptorInstance);
}
}
InterceptorChain:拦截器链,有两个方法,
/**
* @Author df
* @Description: 拦截器链
* @Date 2024/1/4 14:05
*/
// step-16 添加
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
// 拦截的目标
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
}
Configuration:在已有的方法newStatementHandler里添加调用pluginAll方法,StatementHandler为被代理对象传入过来。
/**
* 创建语句处理器
*/
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 创建语句处理器,Mybatis 这里加了路由 STATEMENT、PREPARED、CALLABLE 我们默认只根据预处理进行实例化
StatementHandler statementHandler = new PreparedStatementHandler(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
// 嵌入插件,代理对象,新添加
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
Interceptor:拦截类,定义方法,由用户实现该接口,pligin和setproperties可以不用用户实现,用户主要实现intercept方法,这样框架执行某个地方就会调用用户的intercept方法了。
/**
* @Author df
* @Description: 拦截器接口
* 面向依赖倒置的入口,插件只定义标准,具体调用处理结果交由使用方决定,
* @Date 2024/1/4 11:05
*/
// step-16 添加
public interface Interceptor {
/**
* 1.intercept需要由使用方实现。
* 2.plugin和setProperties使用方不做实现
* 3.setProperties用户设置的属性通过此方法传递过来
* 4.plugin方法,一个 Interceptor 的实现类就都通过解析的方式,注册到拦截器链中,
* 在后续需要基于 StatementHandler 语句处理器创建时,就可以使用通过代理的方式,把自定义插件包装到代理方法中。
*/
// 拦截,目的:使用方实现,然后调用到使用方
Object intercept(Invocation invocation) throws Throwable;
// 代理
default Object plugin(Object target) {
// target=PrepareStatement
// 把目标类包装成代理类。
return Plugin.wrap(target, this);
}
// 设置属性
default void setProperties(Properties properties) {
// NOP
}
}
Plugin:此类就是要实现代理模式了,实现InvocationHandler。被代理对象执行某个方法则进入到本类的invoke方法。
/**
* @Author df
* @Description: 代理模式插件
* @Date 2024/1/4 11:27
*/
// step-16 添加
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private 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;
}
/**
* 代理调用了目标方法则访问此方法--》再流转到用户实现方法。
*
* 拦截
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取声明的方法列表
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 过滤需要拦截的方法
if (methods != null && methods.contains(method)) {
// 调用 Interceptor#intercept 插入自己的反射逻辑
// 调用用户实现的方法
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
}
/**
* 用代理把自定义插件行为包裹到目标方法中,也就是 Plugin.invoke 的过滤调用
* 1.把要拦截的类包装处理成代理类并返回代理类,(然后执行了目标方法(prepare)后就会调用我们的plugin的invoke()方法。)
*
* 代理动作
*/
public static Object wrap(Object target, Interceptor interceptor) {
// 取得签名Map
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 取得要改变行为的类(ParameterHandler|ResultSetHandler|StatementHandler|Executor),目前只添加了 StatementHandler
Class<?> type = target.getClass();
// 取得接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 创建代理(StatementHandler)
if (interfaces.length > 0) {
// Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
return Proxy.newProxyInstance(type.getClassLoader(), interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
/**
* 获取要代理的方法签名组 Map的信息
*/
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 取Intercepts 注解,例子可参见 TestPlugin.java
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// 必须得有 Intercepts 注解,没有报错
if (interceptsAnnotation == null) {
throw new RuntimeException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
// value是数组型,Signature的数组
Signature[] sigs = interceptsAnnotation.value();
// 每个 class 类有多个可能有多个 Method 需要被拦截
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
/**
* 取得接口
* 获取给的目标类,以及父类,如果有父类返回父类。
*/
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
// 拦截 ParameterHandler|ResultSetHandler|StatementHandler|Executor
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
Intercepts:拦截注解类,由用户plugin实现类使用,如
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
/**
* @Author df
* @Description: 拦截注解
* @Date 2024/1/4 11:36
*/
// step-16 添加
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
?Signature:方法签名注解类,由用户plugin实现类使用,被拦截类的设置以及方法和参数的设置。
/**
* @Author df
* @Description: 方法签名
* @Date 2024/1/4 11:37
*/
// step-16 添加
public @interface Signature {
/**
* 被拦截类
*/
Class<?> type();
/**
* 被拦截类的方法
*/
String method();
/**
* 被拦截类的方法的参数
*/
Class<?>[] args();
}
Invocation:调用信息的封装,并添加方法proceed(),用来执行完代理方法返回到框架执行的地方。
/**
* @Author df
* @Description: 调用信息
* @Date 2024/1/4 11:22
*/
// step-16 添加
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;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
// 放行;调用执行
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
?TestPlugin:这个类名叫什么都可以,这里测试就叫TestPlugin。
拦截操作进来以后就调用的intercept方法,我们拦截到SQL进行打印,然后调用放行操作。
/**
* @Author df
* @Description: TODO
* @Date 2024/1/4 14:14
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
public class TestPlugin implements Interceptor {
/***
* 执行完拦截操作以后放行操作。
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 获取SQL信息
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
// 输出SQL
System.out.println("拦截SQL:" + sql);
// 放行
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
System.out.println("参数输出:" + properties.getProperty("test00"));
}
}
单元测试:
@Test
public void test_queryActivityById() throws IOException {
// 2. 获取映射器对象
IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
// 3. 测试验证
Activity req = new Activity();
req.setActivityId(100001L);
Activity res = dao.queryActivityById(req);
logger.info("测试结果:{}", JSON.toJSONString(res));
}
mybatis-config-datasource.xml添加插件标签
<configuration>
<plugins>
<plugin interceptor="cn.bugstack.mybatis.test.plugin.TestPlugin">
<property name="test00" value="100"/>
<property name="test01" value="200"/>
</plugin>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.5.17:3306/mybatis?useUnicode=true&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- XML 配置 -->
<mapper resource="mapper/Activity_Mapper.xml"/>
</mappers>
</configuration>
执行结果:
我们看到已经打印了拦截的语句。