目录
一、 我是如何看源码的
二、MyBastis架构
1. 预先分析一下MyBatis的骨架
2. 根据这个架构写出一个MyBatis的1.0版本Demo
3. 看1.0版本有哪些不足,该添加些什么,该怎样升级
4. 开始看真实的MyBatis源码, 理出其骨架
5. 查看MyBatis源码中的各模块代码,查看相互联系和代码实现
看框架原码是提高代码设计能力的一个重要的途径。通过学习大牛的优秀代码和设计思想,我们既能够更深地理解框架的底层原理,又能够强化代码的架构能力。
但是开源框架通常都是比较大的项目,盲目地一头扎到代码中去逐行看,可能会摸不着头脑,没有成就感,而一点点地耗尽热情,最终收效甚微。
我的建议是—— 先抓梗概,后看模块,带着问题看代码。
我通常的做法是:
1. 在看源码之前,设想如果自己来设计这个框架的话会怎么写。
根据框架的功能和暴露的接口,写一个小Demo, 可以写的很简单,接口里甚至只打印一句话都行,关键是思考着对框架进行分层和功能模块划分,只实现最核心的逻辑。
这样能让自己提起好奇心,以便看代码时不会倦怠。同时这是自己设计能力的一个答卷,后面将对照着标准答案(真实的框架代码设计情况)进行评判,找到自己的不足和更好的优化方式。
2. 先看框架梗概,不陷入具体细节,列出整个框架的骨架图和各模块之间的关系;之后再按功能看各个模块的代码实现。
过早地陷入代码细节,会导致只见树木不见森林;先搭好骨架再去填充细节要相对容易得多。
3. 看完代码之后按真实的框架再写一个demo,前面写的demo哪个地方不好?该怎样优化?
不懂的地方可以猜测再debug一下。
多问些关键问题,找关键类联系起来。
我们以MyBastis为例来看一下框架源码。
我们先看一下MyBatis的常用例子:
SqlSession session = null;
try{
//创建SessionFactory对象,读取配置信息
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(
Resources.getResourceAsStream("mybatis-config.xml"));
//创建一个session
session = factory.openSession();
PersonMapper personMapper = session.getMapper(PersonMapper.class);
//Mybatis通过mapper代理直接执行查询方法
Person person= personMapper.selectOne(1001);
System.out.println(person);
}
catch(Exception e){
e.printStackTrace();
}
finally{
if(null != session){
session.close();
}
}
从这些应用信息来看,我们能知道MyBatis:
(1)要有一个Configuration类,负责解析xml配置文件, 将相应的配置读进来并存储到Map中,包括DB相应的配置、包名.类名.方法名 与SQL语句之间的转换配置、以及其他的配置等。
(2)Session类, 要建立与DB的连接,根据调用的情况执行相应的SQL语句并返回相应的结果;如果Session类的功能太多会导致耦合,不妨将与DB打交道的功能单独封装到一个Executor类中(至于叫什么无所谓)。
(3)Session类需要提供一个getMapper() 方法, 根据类名生成相应的代理来调用想要的方法,肯定要用到动态代理,再封装一个动态代理类MapperProxy。
至于SessionFactory和SessionFactoryBuilder不属于核心功能,后续再去添加。
这些是最基本的骨架,我们根据这些基本的功能来搭建MyBatis的1.0版本的Demo。
Session类:
package mybatisdemo;
import java.lang.reflect.Proxy;
public class MSession {
private String xmlPath;
public MSession(String xmlPath) {
this.xmlPath = xmlPath;
}
public T getMapper(Class clazz){
return (T) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{clazz},
new MProxyHandler(this.xmlPath));
}
}
ProxyHandler类:
package mybatisdemo;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MProxyHandler implements InvocationHandler {
private MConfiguration config;
private MExecutor executor;
public MProxyHandler(String xmlPath) {
config = new MConfiguration(xmlPath);
executor = new MExecutor(config);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (this.config.getNameSpace().equals(method.getDeclaringClass().getName())){
//这里应该要根据所调用的方法选择不同的调用分支,先写个最简单版本,之后再丰富
return executor.selectOne(method.getName(), args);
}
//不属于配置的方法,则去调用Object原有的方法
return method.invoke(this, args);
}
}
Configuration类
package mybatisdemo;
import java.util.HashMap;
public class MConfiguration{
//存储数据库配置信息
private HashMap DBMap;
//存储类名.方法名 与SQL之间的映射
private HashMap SQLMap;
//包名.类名,本应读xml配置,这里模拟直接写死
private String nameSpace;
public MConfiguration(String xml) {
//直接在这里赋值模拟读xml文件
DBMap = new HashMap<>();
DBMap.put("user", "root");
DBMap.put("password", "passwd");
DBMap.put("driver", "com.mysql.jdbc.Driver");
DBMap.put("url", "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8");
//SQLMap的内部也应该是读xml进来的,这里直接赋值模拟
SQLMap = new HashMap<>();
SQLMap.put("selectOne", "select * from person where id = ?");
this.nameSpace = "mybatisdemo.PersonMapper";
}
public HashMap getDBMap() {
return DBMap;
}
public HashMap getSQLMap() {
return SQLMap;
}
public String getNameSpace() {
return nameSpace;
}
}
Executor类:
package mybatisdemo;
import java.sql.*;
public class MExecutor {
private static MConfiguration config;
private Connection connection = null;
private PreparedStatement statement = null;
public MExecutor(final MConfiguration config) {
this.config = config;
}
public void init(){
//获取连接
try {
Class.forName(config.getDBMap().get("driver"));
if (null == this.connection){
String url = config.getDBMap().get("url");
String user = config.getDBMap().get("user");
String passwd = config.getDBMap().get("password");
connection = DriverManager.getConnection(url, user, passwd);
}
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public T selectOne(String methodName, Object[] args){
String sql = config.getSQLMap().get(methodName);
if (null != sql && 0 != sql.length()){
return query(sql, args);
}
return null;
}
public T query(String sql, Object[] args){
//这里也是用的硬编码,后续需要改进
PersonInfo person = new PersonInfo();
try {
init();
statement = connection.prepareStatement(sql);
statement.setLong(1, (Long) args[0]);
ResultSet rs = statement.executeQuery();
//这里结果集处理用的硬编码,需改进
while (rs.next()){
person.setId(rs.getLong(1));
person.setName(rs.getString(2));
person.setAge(rs.getInt(3));
person.setGender( rs.getByte("gender"));
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
try {
connection.close();
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return (T) person;
}
}
PersonInfo类: 一个信息封装类
package mybatisdemo;
public class PersonInfo {
private long id;
private String name;
private int age;
private byte gender;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public byte getGender() {
return gender;
}
public void setGender(byte gender) {
this.gender = gender;
}
@Override
public String toString() {
return "PersonInfo{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
'}';
}
}
PersonMapper interface: 一个用来测试的接口,不做implements,
package mybatisdemo;
import java.util.List;
public interface PersonMapper {
PersonInfo selectOne(Long id);
List selectAll();
int insert(PersonInfo info);
int delete(Long id);
int update(PersonInfo info);
}
Main函数, 直接session调用PersonMapper.class 生成动态代理,不用写实现直接根据文件就能查出结果
package mybatisdemo;
public class TestMain {
public static void main(String[] args) {
MSession session = new MSession("mybatis.xml");
PersonMapper mapper = session.getMapper(PersonMapper.class);
PersonInfo info = mapper.selectOne(1L);
System.out.println(info);
}
}
运行结果:
1.0版本能够运行了,已经能够像MyBatis那样,不用写实现、直接通过配置文件就能够在调用方法时输出结果。
但这个简陋的版本,也存在着不少的问题,还需要继续优化。我们看看存在哪些问题:
(1)读取xml部分还没有写
(2)ProxyHandler 的invoke() 里只写了一个方法,这里需要根据不同的调用情况选择不同的调用分支
(3)结果集处理写的是硬编码,这里需要由xml配置给出resultSet或 resultMap,并且加上ORM 进行自动处理
(4)Session加上工厂类SessionFactory、 SessionFactoryBuilder
(5)加上一级缓存、二级缓存,提高查询性能, Cache类及Cache过期策略, LRU/FIFO, 以及增/删/改时Cache失效
(6)连接管理、事务管理
(7)日志,插件,注解,异常处理,以及其他的扩展功能
别急着深入看各个类,先粗看整理出枝节,不要陷入细节
这是一个MyBatis的层级架构和功能模块图
根据功能模块,查看各代码。 看细节代码时,时刻记得整体的架构,自己写的与它们的差距在哪里,怎样写的更好。并注意各模块之间的联系。
后面博文,将分别解读各核心模块, 共包括 Configuration、Plugin、 MapperProxy、 SqlSession、 Executor 几个部分。
谢谢!