手撕代码带你领略Spring如何集成Mybatis

引言

有关于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会过滤掉接口类

思路1:FactoryBean

在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吧,所以这就引出了下面一种方案

思路2:可扩展的FactoryBean

我们是否可以只是用一个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的第一时间肯定会想到 - 扫描

思路3:扫描

既然要扫描,那肯定需要确定扫描的路径,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类导入时会执行,而方法其实一共就做了两件事:

  1. 首先获取了导入类的ZzyMapperScan注解,并取得了扫描路径
  2. 利用自定义的扫描器对包进行扫描

自定义扫描器继承于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();
	}
}

其中:

  • 实现isCandidateComponent接口是为了让扫描器只去扫描接口类
  • 实现doScan接口可以完成自定义BD的注入

在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等,以后有机会的话也可以带大家手撕一下代码~

你可能感兴趣的:(mybatis,spring,java)