有关于Spring整合Mybatis其实一直是一个很具有典型代表性的Spring实际应用,今天就带着大家由浅入深手撸一遍整合的代码
首先准备两个Mapper作为今天演示的操作对象
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
@Select("select username from user where id = 1")
String selectById();
}
import org.apache.ibatis.annotations.Select;
public interface OrderMapper {
@Select("select 'user_login_platfrom")
void selectById();
}
然后是UserService
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class UserService implements BeanNameAware,InitializingBean{
@Autowired
private UserMapper userMapper;
public void test(){
System.out.println(userMapper.selectById());
}
}
紧接着定义Spring配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
@ComponentScan("com.zzy")
@EnableScheduling
public class AppConfig {
}
最后,新建一个测试类,创建Spring容器并启动
import com.zzy.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AppConfig.class);
context.refresh();
UserService userService = (UserService)context.getBean("userService");
userService.test()
}
}
至此,所有的业务类已经创建结束了,这也符合我们日常使用Mybatis的使用,在service层注入mapper的Bean,并执行mapper中的方法执行最终sql
现在摆在我们面前最大的问题就是如何将UserMapper这个接口(interface)注入到userSerivce中,众所周知,Spring在生成BeanDefinition会过滤掉接口类
在Spring中,给我们提供了一个FactoryBean的接口类,通过实现FactoryBean并改写getObject()及getObjectType()方法可以返回任意类型的实例,代码如下:
import com.zzy.mapper.UserMapper;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Component
public class UserMapperFactoryBean implements FactoryBean {
@Override
public Object getObject() throws Exception {
Object proxyInstance = Proxy.newProxyInstance(UserMapperFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName());
// todo完成相关数据库操作
return null;
}
});
return proxyInstance;
}
@Override
public Class<?> getObjectType() {
return UserMapper.class;
}
}
在getObject中使用了JDK的动态代理,生成了代理对象进行返回,并且再代理类中加入了我们需要的逻辑(即执行数据库操作),这里因为不是最终方案,因此就不写完整了。
这种方式实现起来比较简单,比容易理解,不过不能作为mybatis这种组件实现的方式,因为丧失了扩展性,试想一下项目中会有很多的Mapper,那总不能每一个Mapper都去定义一个相应的BeanFactory吧,所以这就引出了下面一种方案
我们是否可以只是用一个FactoryBean,然后将类型作为入参实现呢,当然也是可以的,可以利用属性+构造方法的方式实现,代码如下:
import org.springframework.beans.factory.FactoryBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MapperFactoryBean implements FactoryBean {
private Class mapperInterface;
public MapperFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
public Object getObject() throws Exception {
Object proxy = Proxy.newProxyInstance(MapperFactoryBean.class.getClassLoader(), new Class[]{mapperInterface}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method = " + method.getName());
// todo 执行数据库操作
return null;
}
});
return proxy;
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
注意此处的MapperFactoryBean没有Component注解,因为我们需要多个Bean,因此需要去手动注册BeanDefinition,返回我们的Test类,修改main方法,代码如下:
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AppConfig.class);
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
beanDefinition.setBeanClass(MapperFactoryBean.class);
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
context.registerBeanDefinition("userMapper",beanDefinition);
context.refresh();
UserService userService = (UserService)context.getBean("userService");
userService.test();
}
}
我们利用了AnnotationConfigApplicationContext注册了一个名为userMapper类型为MapperFactoryBean的BD
当然也可以使用BeanDefinitionRegistryPostProcessor来注册BD,代码如下:
@Component
public class BeanDefinitionRegister implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
beanDefinition.setBeanClass(MapperFactoryBean.class);
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
registry.registerBeanDefinition("userMapper",beanDefinition);
}
}
其实代码写到这里,大家肯定又有疑惑了,因为我们还是需要去手动注册,虽然不需要去创建很多个FactoryBean,但是仍然需要去手动创建很多个BD
那么有没有一种方法能够自动将所有的Mapper都解析出来呢?
了解Spring的第一时间肯定会想到 - 扫描
既然要扫描,那肯定需要确定扫描的路径,mybatis中提供了MapperScan接口,那我们自己写一个ZzyMapperScan接口吧
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZzyImportBeanDefinitionRegistrar.class)
public @interface ZzyMapperScan {
String value();
}
在AppConfig中指定扫描路径:
@ComponentScan("com.zzy")
@ZzyMapperScan("com.zzy.mapper")
public class AppConfig {
}
在MapperScan中我们import了一个类ZzyImportBeanDefinitionRegistrar,具体代码如下:
public class ZzyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ZzyMapperScan.class.getName());
String path = (String)annotationAttributes.get("value");
System.out.println("path = " + path);
ZzyClassPathBeanDefintionScan zzyClassPathBeanDefintionScan = new ZzyClassPathBeanDefintionScan(registry);
zzyClassPathBeanDefintionScan.addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
zzyClassPathBeanDefintionScan.scan(path);
}
ZzyImportBeanDefinitionRegistrar实现了ImportBeanDefintionRegsitrar,并重写了registerBeanDefinitions方法,该方法在import类导入时会执行,而方法其实一共就做了两件事:
自定义扫描器继承于ClassPathBeanDefinitionScanner,代码如下
public class ZzyClassPathBeanDefintionScan extends ClassPathBeanDefinitionScanner {
public ZzyClassPathBeanDefintionScan(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
for(BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders){
BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
beanDefinition.setBeanClassName(MapperFactoryBean.class.getName());
}
return beanDefinitionHolders;
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface();
}
}
其中:
在doScan逻辑中,我们将当前类类型(接口)作为构造方法入参传入,而将BD的bean类型改为了上文所写的MapperFactoryBean
至此,我们完成了基于扫描的可扩展的Mapper注入,接下去就是执行接口注解定义的sql了,我们需要进行一些简单的改造,首先是MapperFactoryBean,我们现在直接使用Mybatis生成的代理对象:
public class MapperFactoryBean implements FactoryBean {
private Class mapperInterface;
private SqlSession sqlSession;
@Autowired
public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
sqlSessionFactory.getConfiguration().addMapper(mapperInterface);
this.sqlSession = sqlSessionFactory.openSession();
}
public MapperFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
public Object getObject() throws Exception {
return sqlSession.getMapper(mapperInterface);
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
这里我们利用到了mybatis的sqlSession,而想要注入sqlSession,我们可以利用构造方法自动注入sqlSessionFactory,并且使用openSession的方法返回sqlSession实例
而sqlSessionFactory就需要我们去手动创建该Bean了,我们可以直接写到AppConfig中:
@ComponentScan("com.zzy")
@ZzyMapperScan("com.zzy.mapper")
public class AppConfig {
@Bean
public SqlSessionFactory sqlSessionFactory() throws IOException{
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
return sqlSessionFactory;
}
}
mybaits.xml配置文件如下:
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<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://{ip}:{port}/{datasource}?characterEncoding=utf-8&useSSL=false"/>
<property name="username" value="{username}"/>
<property name="password" value="{password}"/>
dataSource>
environment>
environments>
configuration>
最后运行,控制台输出:
至此手写Spring-mybtias的过程就结束了,我们经过了一步步的推导过程,十分直观详细的向大家展示了其中的核心逻辑,当然mybatis内部实现的代码写的也是十分好的,例如生成代理对象执行sql等,以后有机会的话也可以带大家手撕一下代码~