前言
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的时候,可以采取这种解决方案。