第15章:Mybaties的Plugin插件功能实现

本文详细介绍了如何在MyBatis中利用动态代理和依赖倒置原则实现插件扩展,包括XML配置、Interceptor接口、InterceptorChain的使用,以及如何通过注解配置拦截特定方法和参数。
摘要由CSDN通过智能技术生成

1.目标

本章节的目标就是用户在xml配置了类,我们需要实现在某处拦截执行用户配置的实现类,而这部分功能就是mybaties的插件扩展。

插件主要的设计理念就是依赖倒置,插件功能依赖于接口,而不是具体的实现,不依赖具体实现,在这个过程中对抽象进行编程,不对实现进行编程,这样就降低了客户与实现模块间的耦合。

而技术实现就需要用动态代理,我们需要用户配置被代理对象,被代理的方法以及参数,框架进行拦截操作,通过配置的方式把实现添加到Mybaties中(如图一),这样在执行插件代理方法invoke()时,就可以直接实现对应的实现类。看图二,用户注解的配置中,StatementHandler当作代理对象,方法是prepare,参数是Connection,那么当我们执行StatementHandler和其子类时并且方法是prepare的化就会执行代理invoke(),此时就会判断调度到用户的实现类里。

 

2.xml类图

看xml类图说明,

1.由XMLConfigBuilder开始进行解析插件内容,为什么从这里开始,因为我们的配置在mybatis-config-datasource.xml中(如下图),解析完毕将拦截器(用户实现)对象放入Configuration的拦截器链条中(InterceptorChain),

2.在执行对应Sql之前(newStatementHandler方法处),我们在此设置包装了代理方法,代理的是PreparedStatementHandler,是StatementHandler的子类,进入包装代理方法就会读取到用户实现类的注释,如Intercepts以及Signature注释得到被代理的类和方法以及参数,最后返回代理类。

3.此时StatementHandler已经被代理,所以执行prepare()时直接执行Plugin类下的incoke()方法,这时就执行用户的实现操作,用户操作完放行,这时继续执行框架里的操作。

3.代码 

3.1 xml插件解析

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);
    }
    
}

3.2 拦截器链

InterceptorChain:拦截器链,有两个方法,

  • 一个是存储interceptor,
  • 另一个是pluginAll方法,调用interceptor.plugin(target)把被代理变成代理对象方法处理。什么时候用呢,要拦截的地方需要把被代理对象传入过来并调用pluginAll方法。
/**
 * @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;
    }

3.3 拦截类

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
    }
}

3.4 插件代理类

Plugin:此类就是要实现代理模式了,实现InvocationHandler。被代理对象执行某个方法则进入到本类的invoke方法。

  • invoke方法的业务是,判断是否是被代理的方法,是则执行用户实现的方法interceptor.intercept(new Invocation(target, method, args));
  • wrap方法的业务是,取出用户在注释上的要代理的代理类,要代理的方法以及参数,然后返回代理对象。
/**
 * @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()]);
    }

}

3.5 插件注解配置类

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);
    }

}

4.测试准备

 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&amp;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>

执行结果:

我们看到已经打印了拦截的语句。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值