您现在的位置是:首页 >技术交流 >mybatis是如何集成到spring的之托管mapper接口网站首页技术交流

mybatis是如何集成到spring的之托管mapper接口

GAMELOFT9 2024-06-17 11:26:18
简介mybatis是如何集成到spring的之托管mapper接口

前言

mybatis集成到spring可以参考spring mvc集成mybatis进行数据库访问 ,其中mybatis集成到spring最重要的两个配置分别是SqlSessionFactoryBean和MapperScannerConfigurer,如下所示:

<!--mybatis sqlSeesionFactory配置-->
		<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
			<property name="dataSource" ref="dataSource" />
			<property name="configLocation" value="classpath:mybatis-config.xml" />
			<property name="mapperLocations" value="classpath:mapper/*Mapper.xml" />
			<property name="typeAliasesPackage" value="com.gameloft9.demo.dataaccess.model" />
		</bean>

		<!--mapper自动扫描配置-->
		<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
			<property name="basePackage" value="com.gameloft9.demo.dataaccess.dao" />
			<!-- 这里不要定义sqlSessionFactory, 定义了会导致properties文件无法加载 -->
		</bean>

在之前的文章mybatis是如何集成到spring的之SqlSessionFactoryBean 中我们已经对SqlSessionFactoryBean有了初步的探讨,接下来我们继续追问mapper接口是如何被spring管理起来的
我们平时使用mapper进行数据库操作时十分简单,首先定义好Dao层的接口:

**
 * 用户Mapper
 * Created by gameloft9 on 2017/11/28.
 */
public interface UserMapper {
    /**
    * 根据主键查询用户信息
    */
    UserTest selectByPrimaryKey(String id);
}

然后在repository层通过@Autowire就可以自动注入并使用了:
在这里插入图片描述
多么神奇的魔法!但我们需要对其一探究竟。我们先根据现有的知识提出几个问题,然后带着问题找答案:

1、我并没有定义任何UserMapper的Bean,它是怎么自动创建的?

MapperScannerConfigurer

MapperScannerConfigurer是专门用来扫描mapper接口并创建对应的spring bean。它的结构如下所示:
在这里插入图片描述
MapperScannerConfigurer实现了两个非常重要的接口BeanDefinitionRegistryPostProcessor和InitializingBean,其中InitializingBean我们已经很熟悉了。BeanDefinitionRegistryPostProcessor用的人比较少:

BeanDefinitionRegistryPostProcessor可以在bean被初始化前,让你修改applicationContext内部的BeanDefinition,加一些你想要的东西。

由此可见,MapperScannerConfigurer里面对某些BeanDefinition动了手脚,我们进去看看它做了什么:
在这里插入图片描述
看来MapperScannerConfigurer比较懒,它把工作丢给了ClassPathMapperScanner来做了。由于我们只配置了basePackage,所以被叉掉的部分不用去管它,重点是这个scan方法(basePackage可以是用分隔符分割的包,这里做了简单的分割。)在看scan方法前,我们先简单整理下流程:
在这里插入图片描述
MapperScannerConfigurer主要是读取需要扫描mapper的包路径,然后对其做一个简单的检查,然后提供一个可以对BeanDefinition做修改的入口,具体的修改工作交给了ClassPathMapperScanner。

ClassPathMapperScanner

好像ClassPathMapperScanner才是真正做苦力的那位老实人,在spring扫描完我们提供的包路径并创建完BeanDefinition后,ClassPathMapperScanner开始对BeanDefinition进行魔改。其中最重要的两点就是把mapper的className传给了构造函数以及篡改了bean的class为MapperFactoryBean。
在这里插入图片描述
在这里插入图片描述
在修改完Bean定义后,根据spring Bean的生命周期,肯定会创建Bean的实例。我们再看看修改这两个东西有什么用?

MapperFactoryBean

MapperFactoryBean的类结构如下:
在这里插入图片描述
实际上看到FactoryBean基本就可以断定mapper的代理bean是通过getObject创建的,实际上也确实如此:
在这里插入图片描述
对FactoryBean不熟悉的同学这里再简单提一下:

Spring 中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean 即 FactoryBean。FactoryBean跟普通Bean不同,其返回的对象不是指定类的一个实例,而是该FactoryBean的getObject方法所返回的对象。
一般情况下,Spring通过反射机制利用的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。

看到getSqlSession().getMapper(this.mapperInterface)是不是很熟悉了?后面就正式进去到mybatis内部创建mapper代理对象的流程里去了,spring和mybatis的连接就建立起来了!
现在可以回答刚才的问题了,BeanDefinition修改了BeanClass为MapperFactoryBean.class,那么创建的时候是MapperFactoryBean的实例,然后因为添加了构造函数入参为原始的接口,所以原始的mapper的class也一并传递了进去,这样mapperFactoryBean的mapperInterface字段就保存着原始的mapper接口名称。
又由于MapperFactoryBean实现了FactoryBean接口,所以获取mapper实例的时候,实际调用的是getObject方法,把真正要创建的mapper接口传给getMapper方法,这样原始的mapper的代理对象就可以通过mybatis创建了。

举一反三

1、MapperFactoryBean继承SqlSessionDaoSupport有什么用?

MapperFactoryBean继承了SqlSessionDaoSupport实际上有好几个作用:
1、最重要的,是可以获取SqlSession对象,这样才可以创建mapper的代理对象
2、做一些简单的检查,例如mapperInterface不能为空

2、篡改BeanDefinition和复杂bean初始化的延伸思考

mapper bean的动态创建,通过扫描包并修改BeanDefinition,然后自定义的复杂的初始化逻辑通过实现InitializingBean+FactoryBean接口来达到目的。
作者曾经做过一个支付项目,支付渠道系统开放出来的远程dubbo服务就一个:

/**
* 微信渠道服务
*/
public interface WxDubboService {
   // 服务调用,所有服务都走它,包括下单,查询,退款等操作
   // jsonStr里面有具体的操作的msgType,根据它组装不通过服务处理链路
   String invoke(String jsonStr);
}

内部具体业务怎么处理全部通过一个msgType来区分,具体的业务服务配置大概长这个样子:

<bean id="refundService" class="com.xx.xx.xx.BusinessService">
		<property name="msgType">
			<util:constant static-field="com.xx.xx.wx.config.WXConstant$MsgType.REFUND" />
		</property>
		<property name="targetSys">
			<util:constant static-field="com.xx.xx.framework.config.Constant$TargetSys.WX" />
		</property>
		<property name="name" value="微信交易退款" />
		<property name="requestClass">
			<bean class="com.xx.xx.wx.jsonbean.RefundRequest" />
		</property>
		<property name="filterTemplate" ref="refundFilterTemplate" />
		<property name="businessHandler" ref="refundServiceHandler" />
	</bean>

然后服务的调用实现实际上是根据不同的msgType,然后通过注册中心拿到具体的BusinessService进行处理。这样的好处显而易见,内部子系统调用接口不需要新增,只需要新增msgType和对应的BusinessService即可,减少了接口数量。坏处也很明显,内部调用的逻辑复杂,必须知道msgType才知道最后走的是哪个调用。排查问题和理解难度都比普通dubbo服务要高。从本篇文章我们能有什么启发呢?
通过对比,我们可以大致看到和mybatis的逻辑是类似的,底层通过统一的服务来处理所有的逻辑,然后需要对外支持不同的接口。因此,我们可以类似的创建不同接口的Bean,然后通过FactoryBean将其实现丢给原来统一过的调用逻辑。
首先类似MapperScannerConfigurer我们配置一个ServiceScannerConfigurer:

     <!--service自动扫描配置-->
	<bean class="com.gameloft9.demo.mgrframework.mapper.ServiceScannerConfigurer">
		<property name="basePackage" value="com.gameloft9.demo.service.api.out" />
	</bean>

basePackage就是我们要提供的服务所在的package,我们在out package里面写一个测试接口:

/**
 * 微信订单服务
 */
public interface WxRefundService {
    // 退款
    String refund(String json);
}

剩下的就是一样的套路,我们在ServiceScannerConfigurer篡改BeanDefinition:

public class ServiceScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean {
    // api包
    private String basePackage;

    public void afterPropertiesSet() throws Exception {
        notNull(this.basePackage, "Property 'basePackage' is required");
    }

    // 扫描api包,并篡改BeanDefitnition
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        ClassPathServiceScanner scanner = new ClassPathServiceScanner(registry);
        scanner.registerFilters();
        scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }

    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }

    public String getBasePackage() {
        return basePackage;
    }

    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
    }
public class ClassPathServiceScanner extends ClassPathBeanDefinitionScanner {
    // Service工厂Bean
    private ServiceFactoryBean<?> serviceFactoryBean = new ServiceFactoryBean<Object>();

    public ClassPathServiceScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }

    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

        if (beanDefinitions.isEmpty()) {
            logger.warn("No service was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
        } else {
            processBeanDefinitions(beanDefinitions);
        }

        return beanDefinitions;
    }

    // 篡改BeanDefinition 
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        GenericBeanDefinition definition;
        for (BeanDefinitionHolder holder : beanDefinitions) {
            definition = (GenericBeanDefinition) holder.getBeanDefinition();

            // 保存原始interface
            definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
            // 篡改BeanClass为serviceFactoryBean
            definition.setBeanClass(this.serviceFactoryBean.getClass());
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
    }

    // 注册过滤器
    public void registerFilters() {
        boolean acceptAllInterfaces = true;

         // 支持接口
        if (acceptAllInterfaces) {
            // default include filter that accepts all classes
            addIncludeFilter(new TypeFilter() {
                public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                    return true;
                }
            });
        }

        // 去掉 package-info.java
        addExcludeFilter(new TypeFilter() {
            public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                String className = metadataReader.getClassMetadata().getClassName();
                return className.endsWith("package-info");
            }
        });
    }

    // 保证接口可以注册BeanDefinition
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
        if (super.checkCandidate(beanName, beanDefinition)) {
            return true;
        } else {
            logger.warn("Skipping ServiceFactoryBean with name '" + beanName
                    + "' and '" + beanDefinition.getBeanClassName() + "' serviceInterface"
                    + ". Bean already defined with the same name!");
            return false;
        }
    }
    
    public ServiceFactoryBean<?> getServiceFactoryBean() {
        return serviceFactoryBean;
    }

    public void setServiceFactoryBean(ServiceFactoryBean<?> serviceFactoryBean) {
        this.serviceFactoryBean = serviceFactoryBean;
    }
}

然后在ServiceFactoryBean中创建代理类:

public class ServiceFactoryBean<T> implements FactoryBean<T>, ApplicationContextAware {

    /**
     * 要代理的接口类
     */
    private Class<T> serviceInterface;

    // 可以通过context获取其他spring bean
    private ApplicationContext context;

    public ServiceFactoryBean() {
    }

    public ServiceFactoryBean(Class<T> serviceInterface) {
        this.serviceInterface = serviceInterface;
    }

    // 创建代理对象
    public T getObject() throws Exception {
        // 内部服务注册中心
        ServiceRegister serviceRegister = (ServiceRegister ) context.getBean("serviceRegister");
        
        // 不管是直接走动态代理还是调用其他处理逻辑,背后一定少不了动态代理的东西
        return (T)newProxyInstance(
                ServiceFactoryBean.class.getClassLoader(),
                new Class[] { serviceInterface },
                new ServiceInterceptor(serviceRegister));
    }

    public Class<?> getObjectType() {
        return this.serviceInterface;
    }

    public boolean isSingleton() {
        return true;
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
       context = applicationContext;
    }

    // 代理
    private class ServiceInterceptor implements InvocationHandler{

        private ServiceRegister serviceRegister;

        public ServiceInterceptor(ServiceRegister serviceRegister){
            this.serviceRegister= serviceRegister;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
           // 请求参数
           JsonObject json = JSON.pareObject(args[0]);
          
            // 真正的服务处理
            BusinessService service = serviceRegister.get(json.get("msgType"));
            return service.invoke(args[0]);
        }
    }

    public Class<T> getServiceInterface() {
        return serviceInterface;
    }

    public void setServiceInterface(Class<T> serviceInterface) {
        this.serviceInterface = serviceInterface;
    }
}

ServiceRegister 可以理解为一个HashMap,里面根据msgType注册了具体的业务处理类。我们来写一个测试类:

@Slf4j
@ActiveProfiles("local")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/applicationContext.xml")
public class WxRefundServiceTest {

    // 自动注入
    @Autowired
    WxRefundService wxRefundService;

    @Test
    public void test(){
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setMerOrderId("xxxx");
        orderEntity.setAmount(1L);
        orderEntity.setMemo("付款备注");
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("msgType","refund");
        jsonObject.put("order",orderEntity);
        String result = wxRefundService.refund(jsonObject.toString());
        log.info("{}",result);
    }
}

在这里插入图片描述
可以看到我们只是写了一个接口,却可以通过@Autuwired把bean给注入成功,而且调用wxRefundService.refund比调用wxDubboService .invoke更容易理解了。(这里省略了dubbo服务的发布改造)

总结

mybatis集成到spring最重要的两个配置分别是SqlSessionFactoryBean和MapperScannerConfigurer,其中MapperScannerConfigurer帮助我们把mapper对象托管到spring。使得我们没有定义任何Mapper的Bean,却可以通过@Autowired进行注入,大大简化了我们开发的难度,让我们把重心放在了sql逻辑本身。
这是一种非常好的设计思路,当我们日常工作中想把类托管到spring但又无法定义Bean或者不好定义Bean的时候,可以采取这种解决方案。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。