欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

1 手写Mybatis的简化版框架

程序员文章站 2024-02-29 08:42:52
...

手写JDBC持久层框架

1 传统的JDBC的问题分析

public static void main(String[] args) {
  Connection connection = null;
  PreparedStatement preparedStatement = null;
  ResultSet resultSet = null;
  try {
    ۖ// 1 加载驱动
      Class.forName("com.mysql.jdbc.Driver");
    // 2 设置数据库的配置连接
    connection =
      DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?
                                  characterEncoding=utf-8", "root", "root");
                                  // 设置sql语句配置
                                  String sql = "select * from user where username = ?";
                                  preparedStatement = connection.prepareStatement(sql);
                                  // 封装传参
                                  preparedStatement.setString(1, "tom");
                                  resultSet = preparedStatement.executeQuery();
                                  while (resultSet.next()) {
                                    // 结果集的返回
                                    int id = resultSet.getInt("id");
                                    String username = resultSet.getString("username");
                                    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();
                                         } } 
                                  }
> 传统JDBC 的问题总结
>
> - 数据库驱动加载、sql 语句的存在硬编码的问题
>   - 可以使用配置文件的方式
>
> - 数据库连接存在多次创建和释放的问题
>   - 数据库连接池
> - 传参和结果集手动封装繁琐的问题
>   - 反射和内省

2根据传统问题分析持久层框架的思路

分析:整个框架的使用可以分为使用端、和框架端

使用端

  • 使用端需要提供数据库的配置文件和sql的配置文件
  • 测试类的编写

框架端

  • 根据配置文件的路径读取配置文件为流存放在内存中
  • 创建两个配置文件的容器类 Configuration、MappedStatement
  • dom4j解析配置文件 分别将数据库的配置信息和sql信息存放到容器类的对象中
    • 创建SqlSessionFactoryBuilder的对象去创建SqlSessionFactory,在构建SqlSessionFactory对象的时候进行解析配置文件,并进行存放到 容器类 Configuration、MappedStatement中
    • 创建SqlSessionFactory的对象、创建SqlSession的对象(工厂模式)
  • 创建SqlSessionFactory的实现类DefaultSqlSessionFactory,创建方法openSqlSession()
    • 在SqlSession中封装了增、删、改、查各种方法
      • select、update、delete、insert
  • 创建Executor接口和SimpleExacutor实现类。query(Conguration,MappedStatement,Object… params)

3 根据持久层框架的思路分析实现

1 使用端

sqlConfiguration.xml

<configuration>
  <!--  数据库配置文件  -->
  <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///zdy_mybatis"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </dataSource>
   <!--    存放mapper.xml的位置-->
    <mapper resource="UserMapper.xml"/>
</configuration>

UserMapper.xml

<mapper namespace = "com.lagou.dao.IUserDao">
    <select id="findAll" resultType="com.lagou.pojo.User" >
        select * from user
    </select>

    <select id="findByConfition" resultType="com.lagou.pojo.User" paramType="com.lagou.pojo.User">
        select * from user where id = #{id} and username = #{username}
    </select>
</mapper>

测试端

 @Test
    public void test() throws PropertyVetoException, DocumentException, IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, SQLException, InvocationTargetException, ClassNotFoundException {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);


        SqlSession sqlSession = sqlSessionFactory.openSession();


        User user = new User();
        user.setId(1);
        user.setUsername("zhangsan");
		
        User userResult = sqlSession.selectOne("com.lagou.dao.IUserDao.findByConfition", user);
        System.out.println(userResult.toString());
        List<User> userList = sqlSession.selectList("com.lagou.dao.IUserDao.findAll");
        userList.forEach(ele -> {
            System.out.println(ele.toString());
        });


        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        List<User> userList1 = userDao.findAll();


        userList1.forEach(ele -> {
            System.out.println(ele.toString());
        });

        User user1 = userDao.findByConfition(user);
        System.out.println(user1);


    }

2 框架端的实现

2.1 创建根据配置文件路径读取成流的类

/**
 * 读取配置文件类
 */
public class Resource {


    /**
     * 根据配置 文件路径读取配置 文件成流放置在内存中
     *
     * @param path
     * @return
     */
    public static InputStream getResourceAsStream(String path) {
        /**
         * 类加载器都是从classPath的目录下去获取资源的,
         * 开发目录和编译好的运行目录还是有区别:编译好的文件一般是classes文件,
         * 而resources下的 配置 文件都是 直接存放在classes下的
         */
        InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }

}

类加载器都是从classPath的目录下去获取资源的,
开发目录和编译好的运行目录还是有区别:编译好的文件一般是classes文件,
而resources下的 配置 文件都是 直接存放在classes下

2.2 创建配置文件的容器类

配置文件中包含Configuration、MappedStatement

  • Configuration中包含数据源信息和所有的MappedStatement(Sql的相关信息)
  • MappedStatement 中包含四部分信息:statementId、resultType、paramType、SqlText

注意: 数据源信息是不经常变化的信息,Mapper的Sql文件中是经常修改的文件所以,这两个配置文件是分开放置的

Configuration类

public class Configuration {


    // 数据库配置信息
    private DataSource dataSource;


    Map<String,MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>();
}

MappedStatement类

public class MappedStatement {


    // id标示
    private String id;

    // 返回值类型
    private String resultType;

    // 参数值类型
    private String paramType;

    // sql语句
    private String sql;
}

2.3 dom4j解析xml文件

​ 该解析过程使用的是在 生成SqlSessionFactory的 对象时候进行解析,构建着模式使用SqlSessionFactoryBuilder构建SqlSessionFactory对象,再使用SqlSessionFactory构建SqlSession

SqlSessionFactoryBuilder

    public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {


        // 1. 使用dom4j解析xml文件,并封装到Configuration中
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);

        // 2. 创建SqlSessionFactory 工厂类  产生sqlSession回话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);



        return defaultSqlSessionFactory;

    }

XMLConfigBuilder

public class XMLConfigBuilder {

    private Configuration configuration;


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

    /**
     * 使用dom4j将配置文件解析,封装Configuration
     *
     * @param inputStream
     * @return
     */
    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {

        Document document = new SAXReader().read(inputStream);

        // configuration
        Element rootElement = document.getRootElement();
        // xpath表达式获取所有property
        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);
        }


        // 数据库连接池实现数据库链接
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));
        configuration.setDataSource(comboPooledDataSource);

        // mapper.xml 的解析   拿路径 -- 字节输入流 -- dom4j进行解析
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream resourceAsStream = Resource.getResourceAsStream(mapperPath);


            // 建造者模式的构造参数的应用变形
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            // 解析mapper.xml并封装到configuration中
            xmlMapperBuilder.parse(resourceAsStream);

        }
        return configuration;
    }
}

XMLMapperBuilder

/**
 * 解析Mapper.xml
 */
public class XMLMapperBuilder {

    private Configuration configuration;

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


    /**
     * 解析mapper.xml的文件,封装到MappedStatement
     *
     * @param inputStream
     */
    public void parse(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();

        String namespace = rootElement.attributeValue("namespace");

        List<Element> list = rootElement.selectNodes("//select");
        for (Element element : list) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String paramterType = element.attributeValue("paramType");
            String sqlText = element.getTextTrim();


            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setParamType(paramterType);
            mappedStatement.setResultType(resultType);
            mappedStatement.setSql(sqlText);
            // 执行sql的唯一Id
            String key = namespace + "." + id;
            configuration.getMappedStatementMap().put(key, mappedStatement);
        }
    }
}

创建SqlSessionFactory的相关实现类

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

    public SqlSession openSession() {
        return new DefaultSqlSession(configuration );
    }
}

创建SqlSession的实现类

SqlSession实现类中封装的是增删该查的操作,其中对应的jdbc代码的实际执行进一步抽象出Executor中

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

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

    public <E> List<E> selectList(String statementId, Object... params) throws SQLException, IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, InvocationTargetException, ClassNotFoundException {
        // 完成对SimpleExecutor的query的调用
        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 selectOne(String statementId, Object... params) throws SQLException, IllegalAccessException, IntrospectionException, InstantiationException, ClassNotFoundException, InvocationTargetException, NoSuchFieldException {

        List<Object> objects = selectList(statementId, params);
        if (objects.size() == 1) {
            return (T) objects.get(0);
        } else {
            throw new RuntimeException("查询结果为空或返回多行");
        }
    }

    @Override
    public <T> T getMapper(Class<?> mapperClass) {

        // JDK动态代理  为Dao生成动态代理对象,并返回
        Object newProxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                // 底层代码执行的还是JDBC,所以要有选择的执行selectList或selectOne
                // 准备参数 1 statementId
                // 由于该处没法获取statementID 所以需要将接口的名和xml中配置的statementId统一
                //namespace.id = 接口权限命名.方法名

                // 方法名:findAll
                String methodName = method.getName();
                String interName = method.getDeclaringClass().getName();

                String statementId = interName + "." + methodName;
                // 准备参数2: param
                // 获取返回值类型
                Type genericReturnType = method.getGenericReturnType();
                // 判断是否进行拉 范型类型参数化
                if(genericReturnType instanceof ParameterizedType){
                    List<Object> objects = selectList(statementId, args);
                    return objects;
                }else {
                    Object one = selectOne(statementId, args);
                    return one;

                }


            }
        });


        return (T) newProxyInstance;
    }

Executor

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语句    select * from user where id = #{id} and username = #{username}
        // 转换成           select * from user where id = ? and username = ?
        // 转换过程中要实现一个 对#{}里面值的解析和存储
        String sqlText = mappedStatement.getSql();
        BoundSql boundSql = getBoundSql(sqlText);

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

        // 4. 设置参数
        // 获取入参类型的全路径
        String paramType = mappedStatement.getParamType();
        Class<?> parameterTypeClass = getClassType(paramType);

        // 反射
        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();


        // 6. 封装返回结果
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);
        ArrayList<Object> objects = new ArrayList<Object>();

        while (resultSet.next()) {
            Object o = resultTypeClass.newInstance();

            ResultSetMetaData metaData = resultSet.getMetaData();
            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;
    }

    private Class<?> getClassType(String paramType) throws ClassNotFoundException {
        if (paramType != null) {
            Class<?> aClass = Class.forName(paramType);
            return aClass;
        }
        return null;
    }


    /**
     * 1 将#{}进行 ?的替换
     * 2 解析存储#{}中的值
     *
     * @param sqlText
     * @return
     */
    private BoundSql getBoundSql(String sqlText) {
        // 标记处理类:配置标记解析器来完成对占位符的解析处理工作

        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);

        // 解析出的sql
        String sql = genericTokenParser.parse(sqlText);
        // 解析的#{}值
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();


        BoundSql boundSql = new BoundSql(sql, parameterMappings);
        return boundSql;


    }
}

4 优化JDBC框架

		> 在实际应用中一般都是直接调用接口类实现jdbc的调用
		>
		> 但是仍然存在一些问题
		>
		> - 代码重复
		> - 硬编码的问题
// UserDao接口
public interface IUserDao {


    //查询所有用户
    List<User> findAll() throws PropertyVetoException, DocumentException, IllegalAccessException, ClassNotFoundException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException;


    // 条件查询
    User findByConfition(User user) throws IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, SQLException, InvocationTargetException, ClassNotFoundException, PropertyVetoException, DocumentException;
}


// 实现类
public class UserDaoImpl implements IUserDao {
    @Override
    public List<User> findAll() throws PropertyVetoException, DocumentException, IllegalAccessException, ClassNotFoundException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException {
      // 代码重复
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = new User();
        user.setId(1);
        user.setUsername("zhangsan");
		// 硬编码的问题
        List<User> userResult = sqlSession.selectList("user.selectList");
        return userResult;
    }

    @Override
    public User findByConfition(User user) throws IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, SQLException, InvocationTargetException, ClassNotFoundException, PropertyVetoException, DocumentException {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();

        User userResult = sqlSession.selectOne("user.selectOne", user);
        return userResult;
    }
}

优化方案:

​ 直接去除实现类,通过JDK的动态代理类去创建实现类

 @Override
    public <T> T getMapper(Class<?> mapperClass) {

        // JDK动态代理  为Dao生成动态代理对象,并返回
        Object newProxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                // 底层代码执行的还是JDBC,所以要有选择的执行selectList或selectOne
                // 准备参数 1 statementId
                // 由于该处没法获取statementID 所以需要将接口的名和xml中配置的statementId统一
                //namespace.id = 接口权限命名.方法名

                // 方法名:findAll
                String methodName = method.getName();
                String interName = method.getDeclaringClass().getName();

                String statementId = interName + "." + methodName;
                // 准备参数2: param
                // 获取返回值类型
                Type genericReturnType = method.getGenericReturnType();
                // 判断是否进行拉 范型类型参数化
                if(genericReturnType instanceof ParameterizedType){
                    List<Object> objects = selectList(statementId, args);
                    return objects;
                }else {
                    Object one = selectOne(statementId, args);
                    return one;

                }


            }
        });


        return (T) newProxyInstance;
    }

使用代理对象调用方法就会执行new InvocationHandler()中的代理方法

代理对象

5 总结开发JDBC的整体思路

1 手写Mybatis的简化版框架

6 实现中使用的设计模式

  • 构建者模式

    构建者模式:主要用于构建复杂程度大于表现形式(通俗的讲就是构建一个对象的过程比较复杂,比如Mybatis中构建一个Factory对象的时候还需要将配置文件的xml的解析出来并且存入容器类中)。

  • 工厂模式

    工厂模式:主要用于创建多个对象,该对象可以是同一个类也可以是不同类的。(Mybatis中执行不同的Sql需要在不同的SqlSession中执行,所以需要创建多个SqlSession的对象)

  • 代理模式

    代理模式:主要用于在直接访问类的方法比较复杂或者有其他问题时使用代理对象 去访问指定类的方法。(Mybatis中如果直接去访问Dao层接口的时候会出现代码重复和硬编码的问题,所以使用SqlSession的代理对象去访问)Dao中的各个方法来规避这些问题