实现映射器的注册和使用

一、设计

鉴于我们希望把整个工程包下关于数据库操作的 DAO 接口与 Mapper 映射器关联起来,那么就需要包装一个可以扫描包路径的完成映射的注册器类。

当然我们还要把上一章节中简化的 SqlSession 进行完善,由 SqlSession 定义数据库处理接口和获取 Mapper 对象的操作,并把它交给映射器代理类进行使用。

有了 SqlSession 以后,可以把它理解成一种功能服务,有了功能服务以后还需要给这个功能服务提供一个工厂,来对外统一提供这类服务。比如我们在 Mybatis 中非常常见的操作,开启一个 SqlSession。

  • 以包装接口提供映射器代理类为目标,补全映射器注册机 MapperRegistry,自动扫描包下接口并把每个接口类映射的代理类全部存入映射器代理的 HashMap 缓存中。

  • 而 SqlSession、SqlSessionFactory 是在此注册映射器代理的上次层使用标准定义和对外服务提供的封装,便于用户使用。我们把使用方当成用户 经过这样的封装就就可以更加方便我们后续在框架上功能的继续扩展了,这种思想可以帮助我们解决一些业务功能开发过程中的领域服务包装。

  • MapperRegistry 提供包路径的扫描和映射器代理类注册机服务,完成接口对象的代理类注册处理。

  • SqlSession、DefaultSqlSession 用于定义执行 SQL 标准、获取映射器以及将来管理事务等方面的操作。基本我们平常使用 Mybatis 的 API 接口也都是从这个接口类定义的方法进行使用的。

  • SqlSessionFactory 是一个简单工厂模式,用于提供 SqlSession 服务,屏蔽创建细节,延迟创建过程。

映射器注册机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 映射注册机
*
* @author:zzc
* @date: 2022/4/10
*/
public class MapperRegistry {

/**
* 将已添加的映射器代理加到HashMap中
*/
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new RuntimeException("Type" + type + "is not known to the MapperRegistry");
}

try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new RuntimeException("Error getting mapper instance. Cause:" + e, e);
}

}

/**
* 注册mapper
*
* @param type mapper接口
* @param <T>
*/
public <T> void addMapper(Class<T> type) {
/*mapper必须是接口才会注册*/
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重复添加了,报错
throw new RuntimeException("Type " + type + " is already known to MapperRegistry.");
}

// 注册映射器代理工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
}
}

/**
* 扫描mapper接口包名 并注册
*
* @param packageName 包名
*/
public void addMappers(String packageName) {
Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}

private <T> boolean hasMapper(Class<T> type) {
return knownMappers.containsKey(type);
}
}

  • MapperRegistry 映射器注册类的核心主要在于提供了 ClassScanner.scanPackage 扫描包路径,调用 addMapper 方法,给接口类创建 MapperProxyFactory 映射器代理类,并写入到 knownMappers 的 HashMap 缓存中。
  • 另外就是这个类也提供了对应的 getMapper 获取映射器代理类的方法,其实这步就包装了我们上一章节手动操作实例化的过程,更加方便在 DefaultSqlSession 中获取 Mapper 时进行使用。

SqlSession 标准定义和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* sqlSession用来执行SQL,获取映射器,管理事务。
* 通常情况下,我们在应用程序中使用的Mybatis的API就是这个接口定义的方法。
*
* @author:zzc
* @date: 2022/4/10
*/
public interface SqlSession {


/**
* 根据指定的sqlId获取一条记录的封装对象
*
* @param statement sqlId
* @param <T> returned object type 封装之后的对象类型
* @return mapped object 封装之后的对象
*/
<T> T selectOne(String statement);

/**
* Retrieve a single row mapped from the statement key and parameter.
* 根据指定的SqlID获取一条记录的封装对象,只不过这个方法容许我们可以给sql传递一些参数
* 一般在实际使用中,这个参数传递的是pojo,或者Map或者ImmutableMap
*
* @param <T> the returned object type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Mapped object
*/
<T> T selectOne(String statement, Object parameter);

/**
* Retrieves a mapper.
* 得到映射器,这个巧妙的使用了泛型,使得类型安全
*
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);


}
  • 在 SqlSession 中定义用来执行 SQL、获取映射器对象以及后续管理事务操作的标准接口。
  • 目前这个接口中对于数据库的操作仅仅只提供了 selectOne,后续还会有相应其他方法的定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @author:zzc
* @date: 2022/4/10
*/
public class DefaultSqlSession implements SqlSession {

/**
* 映射器注册机
*/
private MapperRegistry mapperRegistry;

public DefaultSqlSession(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}

@Override
public <T> T selectOne(String statement) {

return null;
}

@SuppressWarnings("unchecked")
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你被代理了!" + "方法:" + statement + "入参:" + parameter);
}

@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type,this);

}
}
  • 通过 DefaultSqlSession 实现类对 SqlSession 接口进行实现。
  • getMapper 方法中获取映射器对象是通过 MapperRegistry 类进行获取的,后续这部分会被配置类进行替换。
  • 在 selectOne 中是一段简单的内容返回,目前还没有与数据库进行关联,这部分在我们渐进式的开发过程中逐步实现。

SqlSessionFactory 工厂定义和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author:zzc
* @date: 2022/4/10
*/
public interface SqlSessionFactory {


/**
* 打开一个session
*
* @return
*/
SqlSession openSession();
}
  • 这其实就是一个简单工厂的定义,在工厂中提供接口实现类的能力,也就是 SqlSessionFactory 工厂中提供的开启 SqlSession 的能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author:zzc
* @date: 2022/4/10
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {

private final MapperRegistry mapperRegistry;

public DefaultSqlSessionFactory(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}

@Override
public SqlSession openSession() {
return new DefaultSqlSession(mapperRegistry);
}

}
  • 默认的简单工厂实现,处理开启 SqlSession 时,对 DefaultSqlSession 的创建以及传递 mapperRegistry,这样就可以在使用 SqlSession 时获取每个代理类的映射器对象了。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test_MapperProxyFactory() {
//1、注册mapper
MapperRegistry registry = new MapperRegistry();
registry.addMappers("com.zzc.hankz.test.dao");

//2、从sqlsession工厂获取session
DefaultSqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(registry);
SqlSession sqlSession = sqlSessionFactory.openSession();

//3、 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);

//4、测试验证
String s = userDao.queryUserName("10001");
logger.info("测试结果:" + s);
}
  • 在单元测试中通过注册机扫描包路径注册映射器代理对象,并把注册机传递给 SqlSessionFactory 工厂,这样完成一个链接过程。
  • 之后通过 SqlSession 获取对应 DAO 类型的实现类,并进行方法验证。

总结

  • 首先要从设计结构上了解工厂模式对具体功能结构的封装,屏蔽过程细节,限定上下文关系,把对外的使用减少耦合。
  • 使用 SqlSessionFactory 的工厂实现类包装了 SqlSession 的标准定义实现类,并由 SqlSession 完成对映射器对象的注册和使用。
  • 几个重要的知识点,包括:映射器、代理类、注册机、接口标准、工厂模式、上下文。这些工程开发的技巧都是在手写 Mybatis 的过程中非常重要的部分,了解和熟悉才能更好的在自己的业务中进行使用。