装X神器——定制属于你自己的Mybatis框架~

102 阅读16分钟

手写Mybatis 1.0

需求分析

通过原始的JDBC代码来操作数据库非常的麻烦,里面存在着太多的重复代码和低下的开发效率,针对这种情况我们需要提供一个更加高效的持久层框架。

资源管理

它需要实现对连接资源的自动管理,也就是把创建Connection、创建Statement、关闭Connection、关闭Statement这些操作封装到底层的对象中,不需要在应用层手动调用。

SQL语句

在代码中我们直接将SQL语句和业务代码写在了一起,耦合性太高了,我们需要把SQL语句抽离出来实现集中管理,开发人员不用在业务代码里面写SQL语句。

结果集映射

在上面的代码中我们需要根据字段取出值,然后把值设置到对应对象的属性中,这个操作也是很繁琐的,所以我们也希望框架能够自动帮助我们实现结果集的转换,也就是我们指定了映射规则之后,这个框架会自动帮我们把ResultSet映射成实体类对象。

对外API

实现了上面的功能以后,这个框架需要提供一个API来给我们操作数据库,这里面定义了对数据库的操作的常用的方法。

核心代码

public void queryUser() {
    Connection conn = null;
    Statement stmt = null;
    User user = new User();
    try {
        // 注册 JDBC 驱动
        // Class.forName("com.mysql.cj.jdbc.Driver");
        // 打开连接
        conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&serverTimezone=UTC", "root", "123456");
        // 执行查询
        stmt = conn.createStatement();
        String sql = "SELECT id,user_name,real_name,password,age,d_id from t_user where id = 1";
        ResultSet rs = stmt.executeQuery(sql);
        // 获取结果集
        while (rs.next()) {
            Integer id = rs.getInt("id");
            String userName = rs.getString("user_name");
            String realName = rs.getString("real_name");
            String password = rs.getString("password");
            Integer did = rs.getInt("d_id");
            user.setId(id);
            user.setUserName(userName);
            user.setRealName(realName);
            user.setPassword(password);
            user.setDId(did);

            System.out.println(user);
        }
        rs.close();
        stmt.close();
        conn.close();
    } catch (SQLException se) {
        se.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (stmt != null) stmt.close();
        } catch (SQLException se2) {
        }
        try {
            if (conn != null) conn.close();
        } catch (SQLException se) {
            se.printStackTrace();
        }
    }
}

通过上面的代码,我们可以发现问题还是比较多的,所以需要进行代码的优化。

功能分解

项目的需求我们也已经清楚了,那么我们应该要怎么来解决这些问题呢?我们先来分析下需要哪些核心对象。

核心对象

1、存放参数和结果映射关系、存放SQL语句,我们需要定义一个配置类

2、执行对数据库的操作,处理参数和结果集的映射,创建和释放资源,我们需要定义一个执行器

3、有了这个执行器以后,我们不能直接调用它,而是定义一个给应用层使用的API,它可以根据SQL的id找到SQL语句,交给执行器执行;

4、如果由用户直接使用id查找SQL语句太麻烦了,我们干脆把存放SQL的命名空间定义成一个接口,把SQL的id定义成方法,这样只要调用接口方法就可以找到要执行的SQL。刚好动态代理可以实现这个功能。这个时候我们需要引入一个代理类

核心对象有了,第二个:我们分析一下这个框架操作数据库的主要流程,先从单条查询入手。

操作流程

在这里插入图片描述

1、定义配置类对象Configuration。里面要存放SQL语句,还有查询方法和结果映射的关系。

2、定义应用层的API SqlSession。在SqlSession里面封装增删改查和操作事务的方法(selectOne())。

3、如果直接把Statement ID传给SqlSession去执行SQL,会出现硬编码,我们决定把SQL语句的标识设计成一个接口+方法名(Mapper接口),调用接口的方法就能找到SQL语句。

4、这个需要代理模式实现,所以要创建一个实现了InvocationHandler的触发管理类MapperProxy。代理类在Configuration中通过JDK动态代理创建。

5、有了代理对象之后,我们调用接口方法,就是调用触发管理器MapperProxy的invoke()方法。

6、代理对象的invoke()方法调用了SqlSession的selectOne()。

7、SqlSession只是一个API,还不是真正的SQL执行者,所以接下来会调用执行器Executor的query()方法。

8、执行器Executor的query()方法里面就是对JDBC底层的Statement的封装,最终实现对数据库的操作,和结果的返回。

V1.0实现

首先创建项目添加依赖

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.11</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
    </dependency>
</dependencies>

SqlSession

针对不用户的请求操作我们可以通过SqlSession来处理,在SqlSession中可以提供基础的操作API,我们定义的名称为DemoSqlSession,我们暂时不需要考虑其他的实现,所以先不用创建接口,直接写类。

根据我们刚才总结的流程图,SqlSession需要有一个获取代理对象的方法,那么这个代理对象是从哪里获取到的呢?是从我们的配置类里面获取到的,因为配置类里面有接口和它要产生的代理类的对应关系。

所以,我们要先持有一个Configuration对象,叫DemoConfiguration,我们也创建这个类。除了获取代理对象之外,它里面还存储了我们的接口方法(也就是statementId)和SQL语句的绑定关系。我们在SqlSession中定义的对外的API,最后都会调用Executor去操作数据库,所以我们还要持有一个Executor对象,叫DemoExecutor,我们也创建它。

private DemoConfiguration configuration;

private DemoExecutor executor;

除了这两个属性之外,我们还要定义SqlSession的行为,也就是它的主要的方法。

第一个方法是查询方法,selectOne(),由于它可以返回任意类型(List、Map、对象类型),我们把返回值定义成 T泛型。selectOne()有两个参数,一个是String类型的statementId,我们会根据它找到SQL语句。一个是Object类型的parameter参数(可以是Integer也可以是String等等,任意类型),用来填充SQL里面的占位符。

public <T> T selectOne(String statementId, Object parameter) {
    String sql = DemoConfiguration.sqlMappings.getString(statementId);
    return executor.query(sql, parameter);
}

它会调用Executor的query()方法,所以我们创建Executor类,传入这两个参数,一样返回一个泛型。Executor里面要传入SQL,但是我们还没拿到,先用statementId代替。

public <T> T query(String sql, Object paramater ) { 
    return null; 
}

SqlSession的第二个方法是获取代理对象的方法,我们通过这种方式去避免了statementId的硬编码。我们在SqlSession中创建一个getMapper()的方法,由于可以返回任意类型的代理类,所以我们把返回值也定义成泛型 T。我们是根据接口类型获取到代理对象的,所以传入参数要用类型Class。

Configuration

代理对象我们不是在SqlSession里面获取到的,要进一步调用Configuration的getMapper()方法。返回值需要强转成(T)。

public <T> T getMapper(Class clazz) {
    return configuration.getMapper(clazz, this);
}

我们先在Configuration创建这个方法,返回类型一样是泛型,先返回空。

public <T> T getMapper(Class clazz, DemoSqlSession sqlSession) {
    return null	;
}

MapperProxy

我们要在Configuration中通过getMapper()方法拿到这个代理对象,必须要有一个实现了InvocationHandler的代理类(触发管理器)。

实现invoke()方法。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    return null;
}

invoke()的实现我们先留着。MapperProxy已经有了,我们回到Configuration.getMapper()完成获取代理对象的逻辑。

返回代理对象,直接使用JDK的动态代理:第一个参数是类加载器,第二个参数是被代理类实现的接口(这里没有被代理类),第三个参数是H(触发管理器)。

把返回结果强转为(T):

public <T> T getMapper(Class clazz, DemoSqlSession sqlSession) {
    return (T) Proxy.newProxyInstance(DemoConfiguration.class.getClassLoader(),
            new Class[]{clazz},
            new MapperProxy());
}

获取代理类的逻辑已经实现完了,我们可以在SqlSession中通过getMapper()拿到代理对象了,也就是可以调用invoke()方法了。接下来去完成MapperProxy 的invoke()方法。

在MapperProxy的invoke()方法里面又调用了SqlSession的selectOne()方法。一个问题出现了:在MapperProxy里面根本没有SqlSession对象?

这两个对象的关系怎么建立起来?MapperProxy怎么拿到一个SqlSession对象?很简单,我们可通过构造函数传入它。

先定义一个属性,然后在MapperProxy的构造函数里面赋值

private DemoSqlSession sqlSession;

public MapperProxy(DemoSqlSession sqlSession) {
    this.sqlSession = sqlSession;
}

因为修改了代理类的构造函数,这个时候Configuration创建代理类的方法getMapper()也要修改。

问题:Configuration也没有SqlSession,没办法传入MapperProxy的构造函数。怎么拿到SqlSession呢?是直接new一个吗?

不需要,可以在SqlSession调用它的时候直接把自己传进来(修改的地方:MapperProxy的构造函数添加了sqlSession,getMapper()方法也添加了SqlSession):

public <T> T getMapper(Class clazz, DemoSqlSession sqlSession) {
    return (T) Proxy.newProxyInstance(DemoConfiguration.class.getClassLoader(),
            new Class[]{clazz},
            new MapperProxy(sqlSession));
}

那么SqlSession的getMapper()方法也要修改(红色是修改的地方):

public <T> T getMapper(Class clazz) {
    return configuration.getMapper(clazz, this);
}

现在在MapperProxy里面已经就可以拿到SqlSession对象了,在invoke()方法里面我们会调用

SqlSession的selectOne()方法。我们继续来完成invoke()方法。

selectOne()方法有两个参数, statementId和paramater,这两个我们怎么拿到呢?

statementId其实就是接口的全路径+方法名,中间加一个英文的点。

paramater可以从方法参数中拿到(args[0])。因为我们定义的是String,还要把拿到的Object强转一下。

把statementId和parameter传给SqlSession:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    String statementId = method.getDeclaringClass().getName() + "." + method.getName();

    return sqlSession.selectOne(statementId, args[0]);
}

好,到了sqlSession的selectOne()方法,这里我们要去调用Executor的query方法,这个时候我们必须传入SQL语句和parameter(根据statementId获取)。

问题来了,我们怎么根据StatementId找到我们要执行的SQL语句呢?他们之间的绑定关系我们配置在哪里?

为了简便,免去读取文件流和解析XML标签的麻烦,我们把我们的SQL语句放在Properties文件里面。我们在resources目录下创建一个sql.properties文件。key就是接口全路径+方法名称,SQL是我们的查询SQL。

参数这里,因为我们要传入一个整数,所以先用一个%d的占位符代替:

com.demo.mapper.UserMapper.selectOne=select * from t_user where id = %d

在sqlSession的selectOne()方法里面,我们要根据StatementId获取到SQL,然后传给Executor。这个绑定关系是放在Configuration里面的。

public <T> T selectOne(String statementId, Object parameter) {
    String sql = DemoConfiguration.sqlMappings.getString(statementId);
    return executor.query(sql, parameter);
}

怎么快速地解析Properties文件?

为了避免重复解析,我们在Configuration创建一个静态属性和静态方法,直接解析sql.properties文件里面的所有KV键值对:

// 保存属性文件信息
public static final ResourceBundle sqlMappings;

static {
    sqlMappings = ResourceBundle.getBundle("sql");
}

在SqlSession中,SQL语句已经拿到了,接下来就是Executor类的query()方法,Executor是数据库操作的真正执行者。怎么做?

我们干脆直接把JDBC的代码全部复制过来,职责先不用细分。

参数用传入的参数替换%d占位符,需要format一下

Executor

在Executor中我们就可以直接来执行SQL的执行了:

public <T> T query(String sql, Object parameter) {
    Connection conn = null;
    Statement stmt = null;
    User user = new User();
    try {
        // 注册 JDBC 驱动
        // Class.forName("com.mysql.cj.jdbc.Driver");
        // 打开连接
        conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&serverTimezone=UTC", "root", "123456");
        // 执行查询
        stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(String.format(sql, parameter));
        // 获取结果集
        while (rs.next()) {
            Integer id = rs.getInt("id");
            String userName = rs.getString("user_name");
            String realName = rs.getString("real_name");
            String password = rs.getString("password");
            Integer did = rs.getInt("d_id");
            user.setId(id);
            user.setUserName(userName);
            user.setRealName(realName);
            user.setPassword(password);
            user.setDId(did);
        }
        rs.close();
        stmt.close();
        conn.close();
        return (T) user;
    } catch (SQLException se) {
        se.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (stmt != null) stmt.close();
        } catch (SQLException se2) {
        }
        try {
            if (conn != null) conn.close();
        } catch (SQLException se) {
            se.printStackTrace();
        }
    }
    return null;
}

到这儿我们就可以来写个测试类来跑下程序了:

public class Test {
    public static void main(String[] args) {
        DemoSqlSession sqlSession = new DemoSqlSession();
        //User user = sqlSession.selectOne("com.demo.domain.User.selectOne", 1);

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(mapper.selectOne(1));
    }
}

在这里插入图片描述

V1.0不足

1、在Executor中,对参数、语句和结果集的处理是耦合的,没有实现职责分离;

2、参数:没有实现对语句的预编译,只有简单的格式化(format),效率不高,还存在SQL注入的风险;

3、语句执行:数据库连接硬编码;

4、结果集:还只能处理Blog类型,没有实现根据实体类自动映射。

手写Mybatis 2.0

V1.0的代码优化目标

对Executor的职责进行细化;

支持参数预编译;

支持结果集的自动处理(通过反射)。

V1.0的功能增强目标

在方法上使用注解配置SQL;

查询带缓存功能;

支持自定义插件。

优化实现

建立全局的数据库配置文件

建立全局的数据库配置文

Executor职责划分

在这里插入图片描述

拆分成三个对象:ParameterHandler、StatementHandler、ResultSetHandler。

ParameterHandler

ParameterHandler负责参数处理,原来用的是Statement,不能对参数进行预编译,只能用字符串占位符的方式传参。修改为PreparedStatement。

怎么处理参数?调用psmt的set方法;把传入的多个参数填充到SQL的?处

/**
 * 参数处理器
 */
public class ParameterHandler {
    private PreparedStatement psmt;

    public ParameterHandler(PreparedStatement statement) {
        this.psmt = statement;
    }

    /**
     * 从方法中获取参数,遍历设置SQL中的?占位符
     *
     * @param parameters
     */
    public void setParameters(Object[] parameters) {
        try {
            // PreparedStatement的序号是从1开始的
            for (int i = 0; i < parameters.length; i++) {
                int k = i + 1;
                if (parameters[i] instanceof Integer) {
                    psmt.setInt(k, (Integer) parameters[i]);
                } else if (parameters[i] instanceof Long) {
                    psmt.setLong(k, (Long) parameters[i]);
                } else if (parameters[i] instanceof String) {
                    psmt.setString(k, String.valueOf(parameters[i]));
                } else if (parameters[i] instanceof Boolean) {
                    psmt.setBoolean(k, (Boolean) parameters[i]);
                } else {
                    psmt.setString(k, String.valueOf(parameters[i]));
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
ResultSetHandler

通过ResultSetHandler实现结果集的处理

/**
 * 结果集处理器
 */
public class ResultSetHandler {

    /**
     * @param resultSet 结果集
     * @param type      需要转换的目标类型
     * @param <T>
     * @return
     */
    public <T> T handle(ResultSet resultSet, Class type) {
        // 直接调用Class的newInstance方法产生一个实例
        Object pojo = null;
        try {
            pojo = type.newInstance();
            // 遍历结果集
            if (resultSet.next()) {
                // 循环赋值
                for (Field field : pojo.getClass().getDeclaredFields()) {
                    setValue(pojo, field, resultSet);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return (T) pojo;
    }

    /**
     * 通过反射给属性赋值
     */
    private void setValue(Object pojo, Field field, ResultSet rs) {
        try {
            // 获取 pojo 的 set 方法
            Method setMethod = pojo.getClass().getMethod("set" + firstWordCapital(field.getName()), field.getType());
            // 调用 pojo 的set 方法,使用结果集给属性赋值
            // 赋值先从resultSet取出值 setter
            setMethod.invoke(pojo, getResult(rs, field));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据反射判断类型,从ResultSet中取对应类型参数
     */
    private Object getResult(ResultSet rs, Field field) throws SQLException {
        // TODO TypeHandler
        Class type = field.getType();
        String dataName = HumpToUnderline(field.getName()); // 驼峰转下划线
        // TODO 类型判断不够全
        if (Integer.class == type) {
            return rs.getInt(dataName);
        } else if (String.class == type) {
            return rs.getString(dataName);
        } else if (Long.class == type) {
            return rs.getLong(dataName);
        } else if (Boolean.class == type) {
            return rs.getBoolean(dataName);
        } else if (Double.class == type) {
            return rs.getDouble(dataName);
        } else {
            return rs.getString(dataName);
        }
    }

    // 数据库下划线转Java驼峰命名
    public static String HumpToUnderline(String para) {
        StringBuilder sb = new StringBuilder(para);
        int temp = 0;
        if (!para.contains("_")) {
            for (int i = 0; i < para.length(); i++) {
                if (Character.isUpperCase(para.charAt(i))) {
                    sb.insert(i + temp, "_");
                    temp += 1;
                }
            }
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 单词首字母大写
     */
    private String firstWordCapital(String word) {
        String first = word.substring(0, 1);
        String tail = word.substring(1);
        return first.toUpperCase() + tail;
    }
}
StatementHandler

StatementHandler 封装获取连接的方法、处理参数的ParameterHandler,执行查询的PreparedStatement,处理结果集的ResultSetHandler,起到了承前启后的作用。

public class StatementHandler {
    private ResultSetHandler resultSetHandler = new ResultSetHandler();

    public <T> T query(String statement, Object[] parameter, Class pojo) {
        Connection conn = null;
        PreparedStatement preparedStatement = null;
        Object result = null;

        try {
            conn = getConnection();
            preparedStatement = conn.prepareStatement(statement);
            ParameterHandler parameterHandler = new ParameterHandler(preparedStatement);
            parameterHandler.setParameters(parameter);
            preparedStatement.execute();
            try {
                result = resultSetHandler.handle(preparedStatement.getResultSet(), pojo);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return (T) result;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                conn = null;
            }
        }
        // 只在try里面return会报错
        return null;
    }

    /**
     * 获取连接
     *
     * @return
     * @throws SQLException
     */
    private Connection getConnection() {
        String driver = Configuration.properties.getString("jdbc.driver");
        String url = Configuration.properties.getString("jdbc.url");
        String username = Configuration.properties.getString("jdbc.username");
        String password = Configuration.properties.getString("jdbc.password");
        Connection conn = null;
        try {
            Class.forName(driver);
            conn = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
}

Configuration细化

创建:MapperProxyFactory,用来创建代理对象。

创建:MapperRegistry,维护接口和工厂类映射关系。

创建:SqlSessionFactory,用来创建SqlSession。

定义了Executor接口和基本实现SimpleExecutor。

注解

定义了一个类的注解@Entity。如果有@Entity注解,说明是查询数据库的接口。

/**
 * 用于注解接口,以映射返回的实体类
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Entity {
    Class<?> value();
}
/**
 * 注解方法,配置SQL语句
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
    String value();
}

注解在Configuration构造函数中的parsingClass()中解析,保存在MapperRegistry中。

注意在properties中和注解上同时配置SQL会覆盖。

properties中对表达三个对象的映射关系并不适合,所以暂时用–分隔。注意类型前面不能有空格。

支持缓存

当全局配置中的cacheEnabled=true时,Configuration的newExecutor()方法会对SimpleExecutor进行装饰,返回被代理过的Executor。

if (properties.getString("cache.enabled").equals("true")) {
    executor = new CachingExecutor(new SimpleExecutor());
} else {
    executor = new SimpleExecutor();
}

定义了一个CachingExecutor:

/**
 * 带缓存的执行器,用于装饰基本执行器
 */
public class CachingExecutor implements Executor {

    private Executor delegate;
    private static final Map<Integer, Object> cache = new HashMap<>();

    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
    }

    @Override
    public <T> T query(String statement, Object[] parameter, Class pojo) {
        // 计算CacheKey
        CacheKey cacheKey = new CacheKey();
        cacheKey.update(statement);
        cacheKey.update(joinStr(parameter));
        // 是否拿到缓存
        if (cache.containsKey(cacheKey.getCode())) {
            // 命中缓存
            System.out.println("【命中缓存】");
            return (T) cache.get(cacheKey.getCode());
        } else {
            // 没有的话调用被装饰的SimpleExecutor从数据库查询
            Object obj = delegate.query(statement, parameter, pojo);
            cache.put(cacheKey.getCode(), obj);
            return (T) obj;
        }
    }

    // 为了命中缓存,把Object[]转换成逗号拼接的字符串,因为对象的HashCode都不一样
    public String joinStr(Object[] objs) {
        StringBuffer sb = new StringBuffer();
        if (objs != null && objs.length > 0) {
            for (Object objStr : objs) {
                sb.append(objStr.toString() + ",");
            }
        }
        int len = sb.length();
        if (len > 0) {
            sb.deleteCharAt(len - 1);
        }
        return sb.toString();
    }
}

在DefaultSqlSession调用Executor时,会先走到CachingExecutor。

定义了一个CacheKey用于计算缓存Key。

/**
 * 缓存Key
 */
public class CacheKey {
    // MyBatis抄袭了我的设计 手动滑稽
    private static final int DEFAULT_HASHCODE = 17; // 默认哈希值
    private static final int DEFAULT_MULTIPLIER = 37; // 倍数

    private int hashCode;
    private int count;
    private int multiplier;

    /**
     * 构造函数
     */
    public CacheKey() {
        this.hashCode = DEFAULT_HASHCODE;
        this.count = 0;
        this.multiplier = DEFAULT_MULTIPLIER;
    }

    /**
     * 返回CacheKey的值
     *
     * @return
     */
    public int getCode() {
        return hashCode;
    }

    /**
     * 计算CacheKey中的HashCode
     *
     * @param object
     */
    public void update(Object object) {
        int baseHashCode = object == null ? 1 : object.hashCode();
        count++;
        baseHashCode *= count;
        hashCode = multiplier * hashCode + baseHashCode;
    }
}

支持插件

插件需要几个必要的类:

Interceptor接口规范插件格式;

@Intercepts注解指定拦截的对象和方法;

InterceptorChain容纳解析的插件类;

Plugin可以产生代理对象,也是触发管理器;

Invocation包装类,用来调用被拦截对象的方法。

工作流程分析

启动解析

SqlSessionFactory的build()方法,调用了Configuration的构造函数进行解析。

静态代码块解析Properties文件。

static {
    sqlMappings = ResourceBundle.getBundle("v2sql");
    properties = ResourceBundle.getBundle("mybatis");
}

首先解析解析sql.properties,放到mappedStatements中,把接口和工厂类也绑定起来。

然后解析Mapper接口上的注解,不能重复配置。

最后解析插件,添加到interceptorChain中。

/**
 * 初始化时解析全局配置文件
 */
public Configuration() {
    // Note:在properties和注解中重复配置SQL会覆盖
    // 1.解析sql.properties
    for (String key : sqlMappings.keySet()) {
        Class mapper = null;
        String statement = null;
        String pojoStr = null;
        Class pojo = null;
        // properties中的value用--隔开,第一个是SQL语句
        statement = sqlMappings.getString(key).split("--")[0];
        // properties中的value用--隔开,第二个是需要转换的POJO类型
        pojoStr = sqlMappings.getString(key).split("--")[1];
        try {
            // properties中的key是接口类型+方法
            // 从接口类型+方法中截取接口类型
            mapper = Class.forName(key.substring(0, key.lastIndexOf(".")));
            pojo = Class.forName(pojoStr);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        MAPPER_REGISTRY.addMapper(mapper, pojo); // 接口与返回的实体类关系
        mappedStatements.put(key, statement); // 接口方法与SQL关系
    }

    // 2.解析接口上的注解(会覆盖properties中的接口与实体类的关系)
    String mapperPath = properties.getString("mapper.path");
    scanPackage(mapperPath);
    for (Class<?> mapper : mapperList) {
        parsingClass(mapper);
    }
    // 3.解析插件,可配置多个插件
    String pluginPathValue = properties.getString("plugin.path");
    String[] pluginPaths = pluginPathValue.split(",");
    if (pluginPaths != null) {
        // 将插件添加到interceptorChain中
        for (String plugin : pluginPaths) {
            Interceptor interceptor = null;
            try {
                interceptor = (Interceptor) Class.forName(plugin).newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
            interceptorChain.addInterceptor(interceptor);
        }
    }
}
获取SqlSession

在这里面也创建了一个executor。

如果开启了缓存,用CachingExecutor对SimpleExecutor进行装饰。

如果配置了插件,对Executor创建代理。

/**
 * 创建执行器,当开启缓存时使用缓存装饰
 * 当配置插件时,使用插件代理
 *
 * @return
 */
public Executor newExecutor() {
    Executor executor = null;
    if (properties.getString("cache.enabled").equals("true")) {
        executor = new CachingExecutor(new SimpleExecutor());
    } else {
        executor = new SimpleExecutor();
    }

    // 目前只拦截了Executor,所有的插件都对Executor进行代理,没有对拦截类和方法签名进行判断
    if (interceptorChain.hasPlugin()) {
        return (Executor) interceptorChain.pluginAll(executor);
    }
    return executor;
}
Mapper接口调用

因为返回的是一个代理对象,所以会先走到MapperProxy的invoke()方法。

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectOne(1);

它会根据把接口类型和方法名组成statementId,传给SqlSession。SqlSession里面会从Configuration中拿到SQL,传给Executor。Executor会调用StatementHandler执行,这个里面又包括了ParameterHandler、PrepareStatement、ResultSetHandler。

测试

public class TestMybatis {

    public static void main(String[] args) {
        SqlSessionFactory factory = new SqlSessionFactory();
        DefaultSqlSession sqlSession = factory.build().openSqlSession();
        // 获取包含了h MapperProxy代理
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.selectOne(1);

        System.out.println("第一次查询: " + user);
        System.out.println();
        user = mapper.selectOne(1);
        System.out.println("第二次查询: " + user);
    }
}

在这里插入图片描述

V2.0不足

  • 不能返回List、Map;
  • TypeHandler只能处理部分类型,如果能够处理所有类型的转换关系,和自定义类型就好了。
  • 缓存只有一级,只有一个全局开关,不能在单个方法上关闭(配置不灵活,properties不够用了);
  • 插入、删除、修改的注解;
  • 插件对其他对象、指定方法的拦截,插件支持参数配置;
  • 细节考虑不足,异常处理有点粗暴;

项目地址

orm-mini