MyBatis 使用—反射工厂

一般来讲,使用 MyBatis 默认的 setter 方法规则获取对应属性值来构造动态 SQL 在日常使用中都是可行的。然而,在某些情况下,可能不得不改变这个默认的行为。比如说:希望优先使用接口中定义的方法名直接获取对应的属性值,这在设计通用接口的时候可能会遇到

结合 MyBatis 执行流程对应的源代码:

public class DefaultParameterHandler implements ParameterHandler {
    // 省略部分代码

    @Override
    public void setParameters(PreparedStatement ps) {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    // 省略部分其它的处理逻辑
                    /*
                    	通过 getter 方法获取参数值的处理是在这里进行的,具体的处理也是交给 MyBatis 内置的 ReflctFactory 进行
                    */
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                    // 省略部分代码
                }
            }
        }
    }
}

对于 MetaObject 来讲,一般获取对应的参数属性是通过 MyBatis 内置的反射工具类存储对应的方法,在获取时通过 Getter 规则匹配对应的方法,最后通过反射的方式调用对应的 Getter 方法获取属性。其中,缓存 Getter 方法对应的源代码如下:

public class Reflector {
    public Reflector(Class<?> clazz) {
        Method[] classMethods = getClassMethods(clazz);
        addGetMethods(classMethods);
        // 省略部分代码
    }

    // 通过属性名获取对应的 Getter 方法,这里的 Invoker 是对 Method 的封装
    public Invoker getGetInvoker(String propertyName) {
        Invoker method = getMethods.get(propertyName);
        if (method == null) {
            throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
        }
        return method;
    }
}

为了能够使得方法名能够对应特定的属性,我们需要自己进行相关参数值的获取,结合上文提到的对于 Getter 方法的处理,我们可以从这个角度入手,替换掉获取属性值的默认行为。为了简化问题,我们只需要拓展需要的部分即可,因此我们定义自己的 Reflector

import org.apache.ibatis.reflection.Reflector;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.reflection.invoker.MethodInvoker;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.TreeMap;

/**
 * @author xhliu
 */
public class TaskInfoReflector
    extends Reflector {

    private final static Map<String, Method> map = new TreeMap<>();

    public static interface TaskInfo {
        long userId();
        String userName();
    }

    static {
        /*
        	使用合适的反射工具获取对应类型的所有方法即可
        */
        Method[] methods = ReflectionUtils.getAllDeclaredMethods(TaskInfo.class);
        for (Method m : methods) {
            map.put(m.getName(), m);
        }
    }

    public TaskInfoReflector(Class<?> clazz) {
        super(clazz);
    }

    public Invoker getGetInvoker(String propName) {
        /*
        	这里我们将属性名直接和方法名进行匹配,如果能够匹配则直接封装对应的 Method 作为 Invoker 返回给客户端
        */
        for (Map.Entry<String, Method> entry : map.entrySet()) {
            String name = entry.getKey();
            Method method = entry.getValue();
            int ps = method.getParameterCount();
            if (ps > 0) continue;
            if (propName.equals(name) && (String.class == method.getReturnType()
                                          || method.getReturnType().isPrimitive())) {
                return new MethodInvoker(method);
            }
        }
        /*
        	对于不能处理的属性名,委托给父类进行原有逻辑的处理,这也是装饰器模式的常见使用方式
        */
        return super.getGetInvoker(propName);
    }
}

为了使得我们自定义的反射工具能够在 MyBatis 中使用,我们需要将其注册到 MyBatis 中,以替换原有的内置反射工具。结合对应的源码可知,MyBatis 中获取 Reflector 是通过工厂方法模式来实现的,在 Configuration 类中持有对应的 ReflectorFactory,以此来创建对应的 Reflector

因此我们需要定义自己的 ReflectorFactory,时期能够替换默认的 ReflectorFactory,对应代码如下:

import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.Reflector;

/**
 * @author xhliu
 */
public class TaskInfoReflectorFactory
        extends DefaultReflectorFactory {

    public Reflector findForClass(Class<?> type) {
        if (!(TaskInfo.class.isAssignableFrom(type))) {
            return super.findForClass(type);
        }
        return new TaskInfoReflector(type);
    }
}

再结合 Configuration 配置类的解析过程,对于 XML 格式的配置,我们可以直接指定这个属性:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd" >
<configuration>
    <reflectorFactory type="com.example.demo.reflect.TaskInfoReflectorFactory"/>
    <!-- 省略部分其它配置属性 -->
</configuration>

而对于整合到 Spring 的 MyBatis 来讲,情况要稍微复杂一点。MyBatis 在 Spring 中进行配置是通过 org.mybatis.spring.boot.autoconfigure.MybatisProperties 完成的,因此如果我们希望配置 Configuration 中的相关属性,那么可以在 Spring 的配置文件(以 yaml 文件为例)做对应配置:

mybatis:
  configuration:
    cache-enabled: true
    local-cache-scope: statement
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    reflectorFactory: com.example.demo.reflect.TaskInfoReflectorFactory # 自定义反射工厂的全限定名

然而这样配置无法生效,甚至会得到类似以下的异常:

Failed to bind properties under 'mybatis.configuration.reflector-factory' to org.apache.ibatis.reflection.ReflectorFactory:

    Property: mybatis.configuration.reflector-factory
    Value: "com.example.demo.reflect.TaskInfoReflectorFactory"
    Origin: class path resource [application.yml] - 22:23
    Reason: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [org.apache.ibatis.reflection.ReflectorFactory]

这是因为 Spring 的配置属性为字符串类型,而我们需要的是实际的对象,并且在 Spring 中不存在这样的转换关系,因此出现了这样的问题

为了解决这个问题,我们需要自定义转换类,实现类型名称到实际对象的转换:

import org.apache.ibatis.reflection.ReflectorFactory;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.lang.reflect.Constructor;

/**
 * @author xhliu
 */
@Component
@ConfigurationPropertiesBinding
public class StringToReflectFactoryConvert
        implements Converter<String, ReflectorFactory> {

    public ReflectorFactory convert(@Nonnull String source) {
        try {
            Class<?> clazz = Class.forName(source);
            if (!ReflectorFactory.class.isAssignableFrom(clazz)) {
                throw new IllegalArgumentException("非法的反射工厂类型: " + clazz.getName());
            }
            // 这里假定反射工厂类都存在默认的无参构造函数
            Constructor<?> constructor = clazz.getConstructor();
            return (ReflectorFactory) constructor.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

这样配置后,由于 Spring 会扫描 @Component 注解的类作为 Bean 注册到容器中,同时会将其放入到转换链,因此最终能够实现 String —> ReflectorFactory 的转换

值得一提的是,在配置转换类时,如果使用 @Configuration 替换 @Component 用于标记 Bean,可能同样会出现找不到转换类的问题,这是因为 @Configuration 的目的是为了标识配置组件,Spring 默认会对其创建对应的代理对象,这会导致 Spring 无法检测到 @ConfigurationPropertiesBinding 注解,使得配置的转换类失效。关于 Spring 对于 @Configuration 的特殊处理,可以查看:https://www.cnblogs.com/daihang2366/p/15125874.html

如果要求必须使用 @Configuration,那么必须设置为 @Configuration(proxyBeanMethods = false) 来禁用默认的代理行为

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值