手写Mybatis,源码初体验

本次目标:手写Mybatis,理解运行流程

前置知识

  1. JDBC的相关操作
  2. XML文件的解析
  3. Mybatis的基本使用
  4. 反射基本知识

Mybatis基本运行流程

  • 读取解析配置文件
    –db.properties
    –mybatis-config.xml
    –mapper.xml
  • 执行SQL
    – 参数的解析,生成动态SQL
    – SQL执行
    – 结果集映射(封装成实体对象/集合)

运行流程代码分析

	/**
	 * 解析配置文件,生成SqlSessionFactory
	 */
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
	
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        /**
         * 执行SQL阶段
         */
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //创建StudentDao的代理对象
        StudentDao studentDao = sqlSession.getMapper(StudentDao.class);

        Student student = studentDao.queryById(902);

        System.out.println(student);

重要对象


Java是面向对象的语言,将xml中的数据信息解析封装成一个个对象也是合情合理的。

  • Configuration:全局配置类,包含了很多重要的信息,如数据源、mappedStatement对象等。
  • MappedStatement:封装了每一个INSERT|SELECT|UPDATE|DELETE标签中信息,保存了SQL的全部信息,最后存存储在Configuration的mappedStatements(Map)中,namespace+"."+SQL标签的id作为key,MappedStatement对象作为value。
  • Executor:真正执行SQL的类的接口。

基本环境搭建

导入依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

配置文件
db.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/studentdb
jdbc.username=root
jdbc.password=123456

mapper.xml

<mapper namespace="com.customer.mybatis.StudentDao">
    <select id="selectOne" resultType="com.customer.mybatis.bean.Student" parameterType="java.lang.Integer">
        select * from student where id = ?
    </select>
</mapper>

具体实现代码

根据Mybatis源码的设计,定义相关主要的类,保留本次测试能用到的属性

Configuration.java

Data
public class Configuration {
    
    
    //数据源
    private String jdbcDriver;
    private String jdbcUrl;
    private String jdbcUsername;
    private String jdbcPassword;
    private Map<String,MappedStatement> mappedStatements = new HashMap<String, MappedStatement>();

}

MappedStatement.java

@Data
public class MappedStatement {
    
    
    private String namespace; 
    private String sourceId; // namespace+"."+id(key值)
    private String resultType; // 返回结果类型
    private String sql; //sql语句
}
  • 根据mybatis运行流程以及源码支持,我们知道首先需要读取配置文件,创建SqlSessionFactory。
  • 如下是源码中创建SqlSessinFactory的build方法,在经过一系列解析配置后,将Configuration对象以构造方法的形式注入到实现类DefaultSqlSessionFactory中。
  • public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
    }

SqlSessionFactory.java

/**
 * SqlSessinFactory两个主要任务
 * 1、实例化时加载Configuration
 * 2、生成SqlSession
 */
public class SqlSessionFactory {

    private final Configuration configuration = new Configuration();

    public static final String DB_CONFIG = "db.properties";

    public static final String MAPPER_CONFIG = "com/customer/mybatis/mapper";
    
    //初始化加载配置文件并解析
    public SqlSessionFactory() {
        loadDbInfo();
        loadMappersInfo();
    }
    //openSession方法加载Configuration对象
    public SqlSession openSqlSession(){
        return new DefaultSqlSession(configuration);
    }
    
    //加载mapper.xml的信息。然后将存储在Configuration对象中
    private void loadMappersInfo() {
        URL resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_CONFIG);
        //mapper文件夹下的全部mapper.xml文件
        File mapperXmls = new File(resource.getFile());
        if(mapperXmls.isDirectory()){
            File[] mapperXmlList = mapperXmls.listFiles();
            for (File file : mapperXmlList) {
                if(file.getName().indexOf(".xml") > 0){
                    loadMappersInfo(file);
                }
            }
        }
    }
    //Dom4j解析Xml文件,获取namespace、id、resultType、sql
    private void loadMappersInfo(File file) {
        SAXReader saxReader = new SAXReader();
        Document document = null;
        try {
            //读取xml文件,转换成Doucment对象
            document = saxReader.read(file);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        //获取根节点元素<mapper></mapper>
        Element rootElement = document.getRootElement();
        //获取命名空间
        String namespace = rootElement.attribute("namespace").getValue();
        //获取根节点下的全部子节点<INSERT/SELECT/UPDATE/DELETE>
        //本次测试只读取<select></select>节点
        List<Element> elements = rootElement.elements("select");
        //遍历select节点,读取id、resultType、sql,写入MappedStatement,然后以键值对的方式存储在Configuration中
        for (Element element : elements) {
            MappedStatement mappedStatement = new MappedStatement();
            String id = element.attribute("id").getValue();
            String resultType = element.attribute("resultType").getValue();
            String sql = element.getStringValue();
            String sourceId = namespace+"."+id;
            //向MappedStatement中添加属性
            mappedStatement.setNamespace(namespace);
            mappedStatement.setResultType(resultType);
            mappedStatement.setSql(sql);
            mappedStatement.setSourceId(sourceId);
            configuration.getMappedStatements().put(sourceId,mappedStatement);
        }
    }
    //加载数据库配置信息,最后加载到Configuration对象中
    private void loadDbInfo() {
        //加载数据库配置文件
        InputStream inputStream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG);
        Properties properties = new Properties();
        try {
            //将配置配置信息写入properties
            properties.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //将数据库配置信息写入configruation对象
        configuration.setJdbcDriver(properties.get("jdbc.driver").toString());
        configuration.setJdbcUrl(properties.get("jdbc.url").toString());
        configuration.setJdbcUsername(properties.get("jdbc.username").toString());
        configuration.setJdbcPassword(properties.get("jdbc.password").toString());
    }
}

在生成了SqlSessionFactory后,便可以生成Sqlsession,SqlSession对外提供了访问数据库的API,而且将数据库的操作分配给了Executor,因此SqlSession中必须包含CRUD方法以及Executor对象以及创建Mapper代理对象的getMapper方法

SqlSession.java

public interface SqlSession {
    
    

    <T> T selectOne(String statement, Object parameter);

    <T> List<T> selectList(String statement,Object parameter);

    <T> T getMapper(Class<T> clazz);
}

DefaultSqlSession.java

public class DefaultSqlSession implements SqlSession {
    
    

    private final Configuration configuration;

    private Executor executor;

    public DefaultSqlSession(Configuration configuration) {
    
    

        this.configuration = configuration;
        this.executor = new DefaultExecutor(configuration);
    }
    //查询单个对象方法
    public <T> T selectOne(String statement, Object parameter) {
    
    
        List<Object> objects = this.selectList(statement, parameter);
        if(objects.size() == 0){
    
    
            return null;
        }
        if (objects.size() == 1){
    
    
            return (T) objects.get(0);
        }else{
    
    
            throw new RuntimeException("except one,but found "+objects.size());
        }
    }

    public <T> List<T> selectList(String statement, Object parameter) {
    
    
        return executor.query(configuration.getMappedStatements().get(statement), parameter);
    }

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

MapperProxy.java

public class MapperProxy implements InvocationHandler {
    
    

    private SqlSession sqlSession;

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

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
        String namespace = method.getDeclaringClass().getName();
        String id = method.getName();
        //判断返回类型是否是集合
       if(Collection.class.isAssignableFrom(method.getReturnType())) {
    
    
           return sqlSession.selectList(namespace + "." + id, args == null ? null : args[0]);
}
           return sqlSession.selectOne(namespace + "." + id, args == null ? null : args[0]);
    }
}

有了SqlSession,就可以通过Mapper接口创建代理对象调用方法操作数据库了。

mapper.java

public interface StudentDao {
    
    
    Student selectOne(Integer id);
}

但是最终的CURD方法还是要交给Executor执行器。在Mybatis中,一共有三种执行器,本次测试只实现一个DefaultExecutor

Executor.java

public interface Executor {
    
    
    <T> List<T> query(MappedStatement ms, Object parameter);

    <T> T queryOne(MappedStatement ms, Object parameter);
}
public class DefaultExecutor implements Executor {
    
    
    private final Configuration configuration;

    public DefaultExecutor(Configuration configuration) {
    
    
        this.configuration = configuration;
    }
    //查询集合方法
    public <T> List<T> query(MappedStatement ms, Object parameter) {
    
    
        //定义一个最终的结果集List
        List<T> resultList = new ArrayList<T>();
        try {
    
    
            Class.forName(configuration.getJdbcDriver());
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
    
    
            //获取数据库连接
            connection = DriverManager.getConnection(configuration.getJdbcUrl(), configuration.getJdbcUsername(), configuration.getJdbcPassword());
            //获取preparedStatement对象
            preparedStatement = connection.prepareStatement(ms.getSql());
            //参数赋值
            parameterize(preparedStatement, parameter);
            //获取结果集
            resultSet = preparedStatement.executeQuery();
            //结果集映射
            handleResultSet(resultSet, resultList, ms.getResultType());
        } catch (SQLException throwables) {
    
    
            throwables.printStackTrace();
        } finally {
    
    
            try {
    
    
                resultSet.close();
                preparedStatement.close();
                connection.close();
            } catch (SQLException throwables) {
    
    
                throwables.printStackTrace();
            }
        }
        return resultList;
    }
    //结果集映射,利用反射简单实现结果集到对象的转化
    private <T> void handleResultSet(ResultSet resultSet, List<T> resultList, String resultType) {
    
    
        try {
    
    
            Class<?> clazz = Class.forName(resultType);
            //获取声明的全部属性
            Field[] fileds = clazz.getDeclaredFields();
            while (resultSet.next()){
    
    
                T object = (T) clazz.newInstance();
                for (Field filed : fileds) {
    
    
                    //属性名称
                    String name = filed.getName();
                    //结果集中的数据
                    Object value = resultSet.getObject(name);
                    if (value != null) {
    
    
                        filed.setAccessible(true);
                        filed.set(object, value);
                    }
                }
                resultList.add(object);
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
    //参数赋值,动态SQL的生成,参数赋值是关键,也挺复杂,本次测试就简单做做样子
    private void parameterize(PreparedStatement preparedStatement, Object parameter) {
    
    
        try {
    
    
            preparedStatement.setObject(1, parameter);
        } catch (SQLException throwables) {
    
    
            throwables.printStackTrace();
        }
    }

    public <T> T queryOne(MappedStatement ms, Object parameter) {
    
    
        System.out.println(ms.getSql());
        return null;
    }
}

测试

public class Test {
    
    
    public static void main(String[] args) {
    
    
        //创建sqlSessionFactory 
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
        //获取sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSqlSession();
        //创建mapper代理对象
        StudentDao studentDao = sqlSession.getMapper(StudentDao.class);
        //执行sql
        Student student = studentDao.selectOne(914);
        //打印结果
        System.out.println(student);
    }
}

结果

Student(id=903, name=张三, password=123456, sex=女, birth=1989-03-12, address=湖南省永州市)

猜你喜欢

转载自blog.csdn.net/zh137289/article/details/107731128