最近看了一个讲如何面试的视频,里面说到不要只写自己阅读过某某源码,要把它体现在项目中,就算自己做的项目中没有,也可以说自己看到别人的项目中利用了某个框架的某些特性,于是我就准备自己动手试试,学习一下优秀框架的精髓,我手动整合了spring和mybatis,视图体会mybatis的优秀之处。
要开始整合spring和mybatis,自然是要先搭建一个maven的项目,我在整合spring和mybatis的同时还整合了log4j方便查看日志,整合了阿里的druid作为mysql的数据库连接池,由于我只是要整合spring和mybatis并找到mybatis基于spring的扩展点及整合的原理,没有必要使用web项目,所以我只引入了spring-context包。项目pom文件如下
4.0.0
com.ww
mybatis-spring
1.0-SNAPSHOT
org.mybatis
mybatis
3.4.6
org.springframework
spring-context
4.3.25.RELEASE
mysql
mysql-connector-java
5.1.47
org.springframework
spring-jdbc
4.3.25.RELEASE
log4j
log4j
1.2.17
org.mybatis
mybatis-spring
1.3.2
org.projectlombok
lombok
1.18.10
compile
com.alibaba
druid
1.1.10
由于我不喜欢使用xml,所以本次搭建我是用了纯注解的形式,在项目的resource目录下,我建立了application.properties文件
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/testmybatis
spring.datasource.driver=com.mysql.jdbc.Driver
编写配置类,获取application.properties中配置的数据库信息
@Configuration
@PropertySource("classpath:application.properties")
public class PropertiesConfig {
@Value("${spring.datasource.url}")
public String url;
@Value("${spring.datasource.username}")
public String username;
@Value("${spring.datasource.password}")
public String password;
@Value("${spring.datasource.driver}")
public String driver;
public String getUrl() {
return url;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getDriver() {
return driver;
}
}
mapper文件如下
public interface UserMapper {
@Select("select id,name,height,weight from user where id=#{id}")
public User selectUser(Integer id);
}
实体类
@Data
public class User {
private int id;
private String name;
private String height;
private String weight;
}
相对应的,在mysql中我建了一个数据库,表名为user
service类
@Service
public class UserService{
@Autowired
UserMapper mapper;
public User getUser(int id) {
//一开始log4j并没有输出日志,在官网上查了之后说加上这句话就可以打印日志了
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
return mapper.selectUser(id);
}
}
让spring来启动的主配置类
@Configuration
@ComponentScan("com.ww")
@MapperScan("com.ww.mapper")
@PropertySource("classpath:application.properties")
public class MybatisConfig {
//这些都是mybatis-spring官网上的例子,照着改改就行
@Bean
public DataSource dataSource(PropertiesConfig config) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(config.getDriver());
dataSource.setUrl(config.getUrl());
dataSource.setPassword(config.getPassword());
dataSource.setUsername(config.getUsername());
return dataSource;
}
//这些都是mybatis-spring官网上的例子,照着改改就行
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
}
到这里项目算搭建完成了,接下来我们运行项目,来观察mybatis整合spring之后的运行过程,以及mybatis整合spring和不整合spring究竟有什么不同
首先我们看到主配置类上有一行@MapperScan的注解,表示扫描mapper到spring容器中,把mapper交给spring管理,那么我想知道mapper是什么时候被spring扫描并注入的呢,我们点进这个注解,看到这个注解是一个组合注解,其中有这么一行注解引起了我的注意
@Import(MapperScannerRegistrar.class)
这个注解是什么意思呢,Import注解的意思是导入资源,那么我们看看它导入的是个什么资源,看类的名字,我猜想这可能是一个mapper扫描器的注册器,于是我就点进去看看
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private ResourceLoader resourceLoader;
/**
* {@inheritDoc}
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// this check is needed in Spring 3.1
if (resourceLoader != null) {
scanner.setResourceLoader(resourceLoader);
}
Class extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
scanner.setAnnotationClass(annotationClass);
}
Class> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
scanner.setMarkerInterface(markerInterface);
}
Class extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
}
Class extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
}
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
List basePackages = new ArrayList();
for (String pkg : annoAttrs.getStringArray("value")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (String pkg : annoAttrs.getStringArray("basePackages")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (Class> clazz : annoAttrs.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
scanner.registerFilters();
//调用ClassPathMapperScanner中的doScan方法,来扫描mapper并组装成beanDefinition
scanner.doScan(StringUtils.toStringArray(basePackages));
}
/**
* {@inheritDoc}
*/
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
可以看到这个类实现了ImportBeanDefinitionRegistrar这个接口,这就引出了spring的第一个扩展点,ImportBeanDefinitionRegistrar接口可以用来动态注册bean,它可以支持我们自己写的代码封装成BeanDefinition对象。在这里,mybatis作为一个第三方框架因为没有办法像第一方组件那样使用@Component或者@Service来表示这是一个需要注入的bean,所以只能扩展这个接口来动态的注入bean。
我们再来看registerBeanDefinitions这个方法,这个方法的含义就是注册bd(为了方便起见,以下beanDefinition都简称bd),我们看到方法最后调用了ClassPathMapperScanner的doScan()方法,我们看看ClassPathMapperScanner这个类
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
...
}
这个类属于spring-mybatis包下,继承了spring的ClassPathBeanDefinitionScanner类
@Override
public Set doScan(String... basePackages) {
//调用spring的doScan方法来扫描bd
Set beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
//扫描完成后执行bd处理
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
由于spring先执行componentScan,我把断点打在ClassPathBeanDefinitionScanner的doScan方法中的时候,首先扫描我自己的bean的时候也会进入这个断点,本次由于不分析componentScan,这一部分略过。跳过这个断点,此时控制台上输出
Registering bean definition for @Bean method com.ww.config.MybatisConfig.dataSource()
Registering bean definition for @Bean method com.ww.config.MybatisConfig.sqlSessionFactory()
表示我自己的bean已经注册完成了,接下来就应该进入mapper扫描了,我把断点打在MapperScannerRegistrar类中registerBeanDefinitions()方法的第一行,果然断点跳了进来,一路执行下去,当执行完ClassPathBeanDefinitionScanner的doScan的方法之后,控制台输出一句:
Identified candidate component class: file [E:\mycode\gitclone\mybatis-spring\target\classes\com\ww\mapper\UserMapper.class]
确认了候选的组件类,也就是说明spring已经扫描到mapper了,同时spring已经注册了bd,接下来再执行,则会进入processBeanDefinitions()方法
//处理注册好的bd
private void processBeanDefinitions(Set beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
if (logger.isDebugEnabled()) {
logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName()
+ "' and '" + definition.getBeanClassName() + "' mapperInterface");
}
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
//bd中加入mapperFactoryBean类,由此可见一个mapper对应的beanClass就是mapperFactoryBean,这是mybatis核心类之一,会详细说
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
if (logger.isDebugEnabled()) {
logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
//自动注入类型为byType
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
等这个方法处理完成之后,整个bd算是建立完了,接下来mybatis就开始初始化的过程了,首先我们来看之前被添加进bd中的MapperFactoryBean类
public class MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean {
private Class mapperInterface;
private boolean addToConfig = true;
public MapperFactoryBean() {
//intentionally empty
}
public MapperFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
/**
* {@inheritDoc}
*/
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
/**
* {@inheritDoc}
*/
@Override
public Class getObjectType() {
return this.mapperInterface;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSingleton() {
return true;
}
//------------- mutators --------------
/**
* Sets the mapper interface of the MyBatis mapper
*
* @param mapperInterface class of the interface
*/
public void setMapperInterface(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
/**
* Return the mapper interface of the MyBatis mapper
*
* @return class of the interface
*/
public Class getMapperInterface() {
return mapperInterface;
}
/**
* If addToConfig is false the mapper will not be added to MyBatis. This means
* it must have been included in mybatis-config.xml.
*
* If it is true, the mapper will be added to MyBatis in the case it is not already
* registered.
*
* By default addToCofig is true.
*
* @param addToConfig
*/
public void setAddToConfig(boolean addToConfig) {
this.addToConfig = addToConfig;
}
/**
* Return the flag for addition into MyBatis config.
*
* @return true if the mapper will be added to MyBatis in the case it is not already
* registered.
*/
public boolean isAddToConfig() {
return addToConfig;
}
}
MapperFactoryBean扩展了Spring的FactoryBean接口,FactoryBean作为Spring的扩展点,FactoryBean的功能是可以让我们自定义Bean的创建过程
//返回的对象实例
T getObject() throws Exception;
//Bean的类型
Class> getObjectType();
//true是单例,false是非单例 在Spring5.0中此方法利用了JDK1.8的新特性变成了default方法,返回true
boolean isSingleton();
同时MapperFactory还继承了SqlSessionDaoSupport类,SqlSessionDaoSupport类又继承了Spring的DaoSupport类,利用了Spring中InitializingBean这个扩展点,在属性设置之后对bean进行操作,我们来看SqlSessionDaoSupport类和DaoSupport类都是做什么的
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSession sqlSession;
private boolean externalSqlSession;
//设置sqlSessionFactory,由于mapperScan最后执行了definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE),所以这个set方法会自动注入进spring容器中
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
//设置sqlSessionTemplate,spring-mybatis的核心类之一,替代了普通mybatis中的DefaultSqlSession类,这个类控制sqlSession,包含一个内部类用来执行动态代理,同时sqlSessionTemplate类线程安全,可以在spring中作为单例bean使用
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSession = sqlSessionTemplate;
this.externalSqlSession = true;
}
/**
* Users should use this method to get a SqlSession to call its statement methods
* This is SqlSession is managed by spring. Users should not commit/rollback/close it
* because it will be automatically done.
*
* @return Spring managed thread safe SqlSession
*/
public SqlSession getSqlSession() {
return this.sqlSession;
}
/**
* {@inheritDoc}
*/
@Override
protected void checkDaoConfig() {
notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
}
//实现InitializingBean接口,会执行afterPropertiesSet()方法,在mybatis整合了spring后会执行MapperFactoryBean类中的checkDaoConfig()方法
public abstract class DaoSupport implements InitializingBean {
/** Logger available to subclasses */
protected final Log logger = LogFactory.getLog(getClass());
@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
// Let abstract subclasses check their configuration.
checkDaoConfig();
// Let concrete implementations initialize themselves.
try {
initDao();
}
catch (Exception ex) {
throw new BeanInitializationException("Initialization of DAO failed", ex);
}
}
/**
* Abstract subclasses must override this to check their configuration.
* Implementors should be marked as {@code final} if concrete subclasses
* are not supposed to override this template method themselves.
* @throws IllegalArgumentException in case of illegal configuration
*/
protected abstract void checkDaoConfig() throws IllegalArgumentException;
/**
* Concrete subclasses can override this for custom initialization behavior.
* Gets called after population of this instance's bean properties.
* @throws Exception if DAO initialization fails
* (will be rethrown as a BeanInitializationException)
* @see org.springframework.beans.factory.BeanInitializationException
*/
protected void initDao() throws Exception {
}
}
把断点打在MapperFactoryBean类中的checkDaoConfig()方法上,继续执行,我们看到执行了configuration.addMapper(this.mapperInterface),点进去,我们看到跳入了org.apache.ibatis.session.Configuration类中的addMapper方法
//mapper注册器,添加和获取mapper的实际类
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public void addMapper(Class type) {
mapperRegistry.addMapper(type);
}
于是乎再进入一层
//mapper被加入到MapperProxyFactory类中
private final Map, MapperProxyFactory>> knownMappers = new HashMap, MapperProxyFactory>>();
public void addMapper(Class type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory(type));
//加入到map中后立即解析mapper中的注解,目的是拿到mapper中的注解sql语句
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
public class MapperProxyFactory {
private final Class mapperInterface;
private final Map methodCache = new ConcurrentHashMap();
public MapperProxyFactory(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class getMapperInterface() {
return mapperInterface;
}
public Map getMethodCache() {
return methodCache;
}
//动态代理回调方法
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
//mapper代理类的实例化方法
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
继续往下执行,同时注意控制台的输出情况,看到控制台输出一句Finished creating instance of bean 'userMapper',此时mapper实例化完成,既然实例化完成了,那么就要返回实例化好的mapper,由于bd中放的beanClass是mapperFactoryBean,所以mapper实例要从mapperFactoryBean的getObject方法中来获得,来看代码
//org.mybatis.spring.mapper.MapperFactoryBean#getObject
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
//org.mybatis.spring.SqlSessionTemplate#getMapper
//sqlSessionTemplate作为sqlSession
@Override
public T getMapper(Class type) {
return getConfiguration().getMapper(type, this);
}
//org.apache.ibatis.session.Configuration#getMapper
public T getMapper(Class type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
//org.apache.ibatis.binding.MapperRegistry#getMapper
public T getMapper(Class type, SqlSession sqlSession) {
final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//之前说过,这里返回的是个代理的对象,也就是经过动态代理的mapper
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
我们观察变量的变化,发现最终返回的object被MapperProxy所代理,到此为止,mapper已经被spring所管理,spring将通过动态代理技术使用代理类来执行mapper中的各种操作。
经过自己动手用纯注解的方式整合spring和mybatis,我们看到了mybatis基于spring做的许多扩展,同时也看到了spring的很多扩展点,比如ImportBeanDefinitionRegistrar、InitializingBean、FactoryBean,下面总结一下mybatis整合spring过程中所用到的几个关键的类和这些类的基本功能