手写mybatis

分析jdbc操作问题

先抛出一段jdbc处理sql的代码

public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
			//加载数据驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 通过驱动管理类获取数据库链接
            connection =
                    DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis? 
                            characterEncoding = utf - 8", " root", " root");
                            // 定义sql语句 ? 表示占位符
                            String sql = "select * from user where username = ?";
            // ឴获取预处理statement
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
            preparedStatement.setString(1, "tom");
            // 向数据库发出sql执行查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            // 遍历查询结果集
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                // 封装User
                user.setId(id);
                user.setUsername(username);
            }
            System.out.println(user);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 释放资源
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (preparedStatement != null) {
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

我们看看这段代码中的问题,mybatis所要干的实际就是处理以下问题
在这里插入图片描述

问题解决思路

针对以上问题,解决方式在图中也有体现。

  • 数据库配置信息存在硬编码问题

    硬编码使代码不够灵活,更改数据库信息需要改代码,也不能做到配置统一管理。所以我们可以用配置文件抽取出来。

  • 频繁创建释放数据库连接
    频繁创建释放连接,会过多的消耗性能。可以用连接池管理。

  • 手动封装返回结果集,较为繁琐
    如果每一次连接数据库都需要手动封装,开发人员要疯掉了,太多的时间浪费在解析结果集上面。所以我们想到用反射、内省来优化。

手写mybatis设计思路

我们可以分为使用端和自定义框架端。
在这里插入图片描述

  • 使用端(项目):引入自定义mybatis框架的jar包
    提供两部分配置信息:
    数据库配置信息
    sql配置信息:sql语句、参数类型、返回值类型
    使用配置文件来提供这两部分配置信息:
    (1)sqlMapConfig.xml:存放数据库配置信息,存放mapper.xml的全路径
    (2)mapper.xml:存放sql配置信息
    为什么要分两个配置文件呢?一般来讲我们希望静态不常动的配置动态改变的配置文件分开,易于维护。
    sqlMapConfig.xml存放mapper.xml,这样框架层读取时,只需要读取一个配置文件就行了。
  • 自定义mybatis框架(工程):本质就是对JDBC代码进行封装
  1. 读取配置文件
    读取完后以流的形式存在,然后将配置信息转换成javaBean存储在内存中
    创建Resources类 方法:InputStream getResourceAsStream(String path)

  2. 创建两个javaBean(容器对象)
    存放配置文件解析出来的内容
    Configuration:存放sqlMapConfig.xml内容
    MappedStatement:存放mapper.xlm内容

  3. 解析配置文件:dom4j
    创建类:SqlSessionFactoryBuilder 方法:build(INputStream in)
    第一:使用dom4j解析配置文件,将解析出来的内容封装到容器对象中(javaBean)
    第二:创建SqlSessionFactory对象;生产sqlSession(工厂模式)

  4. 创建SqlSessionFactory接口及实现类DefaultSqlSessionFactory
    创建sqlsession工厂,专门生产sqlSession

  5. 创建SqlSession接口及实现类DefaultSession
    这部分就是对数据库的crud操作

  6. 创建Executor接口及实现类SimpleExecutor实现类
    query(Configuration,MappedStatement,Object…params)执行的就是JDBC代码

手写mybatis实现

在使用端项目中创建配置文件

创建sqlMapConfig.xml

<configuration>
<!-- 数据库连接信息 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql:///zdy_mybatis"></property> <property name="user" value="root"></property> <property name="password" value="root"></property>
<!-- 引入sql配置文件 -->
<mapper resource="mapper.xml"></mapper>
</configuration>

创建mapper.xml

<mapper namespace="User">
 <select id="selectOne" paramterType="com.pojo.User"
resultType="com.pojo.User">
 select * from user where id = #{id} and username =#{username}
 </select>
 
 <select id="selectList" resultType="com.pojo.User">
 select * from user
 </select>
</mapper>

User实体

public class User {

    private int id;
    private String userName;
    private String userCode;
    
// getter setter...
}

注意使用端需要依赖框架端的工程

<!-- TODO 引入自定义框架层-->
    <dependencies>
        <dependency>
            <groupId>...</groupId>
            <artifactId>...</artifactId>
            <version>...</version>
        </dependency>
    </dependencies>

创建框架端

创建javaBean
首先创建好两个javaBean,需要解析的xml配置就存放在这两个javaBean中。
Configuration

public class Configuration {

    /**
     * 数据源
     */
    private DataSource dataSource;

    /**
     * key:statementId value:封装好的MannedStatement对象
     */
    Map<String, MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>();
    
// getter setter...
}

MappedStatement

public class MappedStatement {
	// id标识
    private String id;
    // 返回值类型
    private String resultType;
    // 参数值类型
    private String parameterType;
    // sql语句
    private String sql;
// getter setter...
}

读取配置文件

public class Resources {
    // 根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中
    public static InputStream getResourceAsStream(String path) {
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
}

解析配置文件
使用dom4j解析配置文件,需要在maven中引入dom4j的包。将解析出来的内容封装到容器对象中(javaBean)

public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(InputStream inputStream) throws PropertyVetoException, DocumentException {
        // 第一步:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);

        // 第二步:创建sqlSessionFactory对象 工厂类:生产sqlSession:会话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);

        return defaultSqlSessionFactory;
    }
}

XMLConfigBuilder 用于解析数据封装到Confiuration中

public class XMLConfigBuilder {

    private Configuration configuration;

    public XMLConfigBuilder() {
        this.configuration = new Configuration();
    }

    /**
     * 该方法使用dom4j对配置文件进行解析,封装Configuration
     */
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(inputStream);
        // <configuration>
        Element rootElement = document.getRootElement();
        List<Element> list = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        for (Element element : list) {
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name, value);
        }


        // c3p0连接池
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass((String) properties.get("driverClass"));
        comboPooledDataSource.setJdbcUrl((String) properties.get("jdbcUrl"));
        comboPooledDataSource.setUser((String) properties.get("username"));
        comboPooledDataSource.setPassword((String) properties.get("password"));

        configuration.setDataSource(comboPooledDataSource);

        // mapper标签
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath);
            XMLMapBuilder xmlMapBuilder = new XMLMapBuilder(configuration);
            xmlMapBuilder.parse(resourceAsStream);

        }
        return configuration;

    }
}

XMLMapBuilder 用于解析sql语句封装到MappedStatement中

public class XMLMapBuilder {
    private Configuration configuration;
    public XMLMapBuilder(Configuration configuration) {
        this.configuration = configuration;
    }

    public void parse(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");

        buildMappedStatement(rootElement, namespace, "//select");
    
    }

    private void buildMappedStatement(Element rootElement, String namespace, String node) {
        List<Element> elementList = rootElement.selectNodes(node);
        for (Element element : elementList) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String parameterType = element.attributeValue("parameterType");
            String sqlText = element.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setResultType(resultType);
            mappedStatement.setSql(sqlText);
            mappedStatement.setStatementType(element.getName());

            String statementId = namespace + "." + id;
            configuration.getMappedStatementMap().put(statementId, mappedStatement);
        }
    }
}

创建SqlSessionFactory,生产sqlSession

public interface SqlSessionFactory {
    public SqlSession openSession();
}

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

	// 生产sqlSession
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }
}

创建SqlSession接口及实现类DefaltsqlSesson
实现crud操作,此处只实现query方法

public interface SqlSession {

    public <E> List<E> queryList(String statementId, Object... params) throws IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, SQLException, InvocationTargetException, ClassNotFoundException;
    
    public <T> T queryOne(String statementId, Object... params) throws IllegalAccessException, ClassNotFoundException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException;
}


public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }

    public <E> List<E> queryList(String statementId, Object... params) throws IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, SQLException, InvocationTargetException, ClassNotFoundException {
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        List<Object> list = simpleExecutor.query(configuration, mappedStatement, params);
        return (List<E>) list;
    }

    public <T> T queryOne(String statementId, Object... params) throws IllegalAccessException, ClassNotFoundException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException {
        List<Object> list = queryList(statementId, params);
        if (list.size() == 1) {
            return (T) list.get(0);
        } else {
            throw new RuntimeException("查询结果为空或者返回结果过多");
        }
    }
   
}

创建Executor接口及实现类SimpleExecutor
封装JDBC代码

public interface Executor {
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IntrospectionException, InstantiationException, InvocationTargetException;

}
public class SimpleExecutor implements  Executor{
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IntrospectionException, InstantiationException, InvocationTargetException {
        // 1.注册驱动,获取连接
        Connection connection = configuration.getDataSource().getConnection();

        // 2.将sql语句转换
        String sql = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sql);

        // 3.获取预处理对象 preparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());

        // 4.设置参数
            //获取参数全路径
        String parameterType = mappedStatement.getParameterType();
        Class<?> parameterTypeClass = getClassType(parameterType);
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();

            // 反射
            Field declaredField = parameterTypeClass.getDeclaredField(content);
            // 暴力访问
            declaredField.setAccessible(true);
            Object o = declaredField.get(params[0]);

            preparedStatement.setObject(i+1,o);

        }

        // 5.执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);

        ArrayList<Object> objects = new ArrayList<>();

        // 6.封装返回结果集
        while (resultSet.next()) {
            ResultSetMetaData metaData = resultSet.getMetaData();
            Object o = resultTypeClass.newInstance();
            for (int i = 1; i <= metaData.getColumnCount() ; i++) {
                // 字段名
                String columnName = metaData.getColumnName(i);
                // 字段值
                Object value = resultSet.getObject(columnName);

                // 使用反射或内省,根据数据库表和实体对应关系,完成封装
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o,value);

            }
            objects.add(o);
        }


        return (List<E>) objects;
    }
  }

BoundSql存放转换过后的sql语句

public class BoundSql {
    private String sqlText;
    private List<ParameterMapping> parameterMappingList = new ArrayList<>();

    public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
        this.sqlText = sqlText;
        this.parameterMappingList = parameterMappingList;
    }

   // getter setter...
}

编写测试类

在使用端中编写Test测试类

@Test
    public void test() throws PropertyVetoException, DocumentException, IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, SQLException, InvocationTargetException, ClassNotFoundException {
        // 读取配置文件到内存
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        // 创建sqlSession工厂,并解析内存中的数据到容器中
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        //打开sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        User zhangsan = sqlSession.queryOne("com.custom.dao.IUserDao.findByOne", "zhangsan");
        System.out.println(zhangsan);
 }

手写mybatis优化

通过上述手写mybatis框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连接、硬编码,手动封装返回结果集等问题,但刚刚自定义的mybatis框架还是有一些缺点。
问题如下:

  • dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlSession,调用sqlSession方法,关闭sqlSession)

  • dao的实现类中存在硬编码,调用sqlSession的方法时,参数statement的id还是硬编码

解决:使用代理模式来创建接口的代理对象
在sqlSession中添加getMapper方法

public interface SqlSession {
 // other method ...

    // 为DAO接口生成代理实现类
    public <T> T getMapper(Class<?> mapperClass);
}

实现类

@Override
public <T> T getMappper(Class<?> mapperClass) {
// 使用JDK动态代理,来为DAO接口生成代理对象并返回
 T o = (T) Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[]
{mapperClass}, new InvocationHandler() {
 @Override
 public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
 // selectOne
 String methodName = method.getName();
 // className:namespace
 String className = method.getDeclaringClass().getName();
 
 //statementid
 String key = className+"."+methodName;
 MappedStatement mappedStatement =
configuration.getMappedStatementMap().get(key);
 Type genericReturnType = method.getGenericReturnType();
 ArrayList arrayList = new ArrayList<> ();
//判断是否进行泛型类型参数化
 if(genericReturnType instanceof ParameterizedType){
 return selectList(key,args);
 }
 return selectOne(key,args);
 }
 });
 return o; }

再测试一下

    @Test
    public void test() throws PropertyVetoException, DocumentException, IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, SQLException, InvocationTargetException, ClassNotFoundException {
        // 读取配置文件到内存
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        // 创建sqlSession工厂,并解析内存中的数据到容器中
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        //打开sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

//        User zhangsan = sqlSession.queryOne("com.custom.dao.IUserDao.findByOne", "zhangsan");
//        System.out.println(zhangsan);


        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        List<User> all = userDao.findAll();
        for (User user : all) {
            System.out.println(user);
        }
    }

总结

每个框架都有它出现的理由,我们先弄清楚它为什么出现,能解决什么问题,然后再去尝试自己解决这些问题。
本篇手写mybatis,实现起来简单,着重要抓住其思路,掌握这个思路之后再去翻Mybatis源码会事半功倍。
最后附上我的代码
自定义mybatis使用端:https://github.com/ZWcrab/IPresistence_test
自定义mybatis框架端:https://github.com/ZWcrab/customBatis

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值