框架学习笔记:自定义持久层框架的简单实现

一、分析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");
            //3.构造查询SQL
            String sql = "select * from user where userName=?";
            //4.通过连接获取预处理对象
            preparedStatement = connection.prepareStatement(sql);
            //5.设置参数
            preparedStatement.setString(1, "lcg");
            //6.查询数据库
            resultSet = preparedStatement.executeQuery();
            //7.遍历查询结果,封装成对象
            while (resultSet.next()) {
                String id = resultSet.getString("id");
                String userName = resultSet.getString("userName");
                System.out.println("id:" + id + ",userName=" + userName);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭resultSet
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            //关闭preparedStatement
            if (preparedStatement != null) {
                try {
                    preparedStatement.cancel();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            //关闭connection
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

    }

1.1 问题分析
  1. 数据库配置信息存在硬编码
    解决:将参数写到外部配置文件,然后通过读取配置文件来获取连接参数。
  2. 频繁创建、释放数据库连接,造成系统资源的浪费,从而影响系统性能。
    解决:连接池
  3. sql语句preparedStatement传参存在硬编码
    解决:将sql写到外部配置文件,程序加载配置文件,获取sql。
  4. 对结果集解析存在硬编码,sql变化会导致解析结果的代码发生变化
    解决:利用JAVA的反射特性实现sql结果的解析。
1.2 问题解决思路总结
  • 使用数据库连接池 统一管理 数据库连接;
  • 将数据库连接信息、sql语句等抽取到xml配置文件中;
  • 使用反射、内省等底层技术,自动将实体与表进行属性和字段的自动映射。

二、自定义框架设计

2.1 设计思路

使用端(项目)
需要提供核心配置文件,其中包括数据库配置信息以及sql配置信息;此外,还需引入我们后面写的自定义框架的jar包;另外,还需要测试使用的实体类以及dao类。
配置文件:

  • sqlMapConfig.xml:存放数据库连接信息,并且需要存放mapper.xml的所有路径。
  • mapper.xml:存放sql配置信息。

框架端

  1. 读取配置文件(使用端会将配置文件以流的形式传递过来,然后转换成对象)
    A. Configuration类:存放数据库基本信息,并以Map的形式(namespace.id作为key)存储sql语句;
    B. MappedStatement类:sql的基本信息,包括其id、输入类型、输出类型、sql语句。
  2. 解析配置文件
    创建sqlSessionFactoryBuilder类:
    具体方法:public SqlSessionFactory build(InputStream in)
    A. 使用dom4j解析配置文件,将解析出来的内容封装到Configuration中;
    B. 创建sqlSessionFactory对象:生产sqlSession会话对象(工厂模式)
  3. 创建SqlSessionFactory接口及实现类:
    ⽅法:openSession() : 获取sqlSession接⼝的实现类实例对象
  4. 创建sqlSession接⼝及实现类DefaultSqlSession:主要封装crud⽅法
    ⽅法:
    selectList(String statementId,Object param):查询所有
    selectOne(String statementId,Object param):查询单个
  5. 创建Executor接口及其实现类SimpleExecutor实现类
    sqlSessionJDBC代码封装在这里。
  6. 使用JDK动态代理来为Dao接口生成代理对象,通过代理对象调用方法都会执行invoke方法,从而去调用DefaultSqlSession的具体方法。(代理模式)
2.2 自定义持久层框架实现
2.2.1 使用端(项目)

sqlMapConfig.xml

<configuration>
    <!-- 数据库连接信息 -->
    <property name="driver" value="com.mysql.cj.jdbc.Driver"></property>
    <property name="url" value="jdbc:mysql://localhost:3306/simple_db"></property>
    <property name="username" value="root"></property>
    <property name="password" value="admin123"></property>

    <!-- 引入所有的sql配置文件 -->
    <mappers>
        <mapper resource="sysUserMapper.xml"></mapper>
    </mappers>
</configuration>

sysUserMapper.xml

<mapper namespace="com.alex.dao.SysUserDao">
    <select id="selectAll" resultType="com.alex.entity.SysUser">
        select *
        from sys_user
    </select>

    <select id="selectOne" parameterType="com.alex.entity.SysUser" resultType="com.alex.entity.SysUser">
        select *
        from sys_user
        where id = #{id} and name = #{name}
    </select>
</mapper>

SysUser.java

package com.alex.entity;

public class SysUser {
    private String id;

    private String name;

    private Integer age;

    public SysUser() {
    }

    public SysUser(String id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "SysUser{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

SysUserDao.java

package com.alex.dao;

import com.alex.entity.SysUser;

import java.util.List;

public interface SysUserDao {
    List<SysUser> selectAll();

    SysUser selectOne(SysUser sysUser);
}
2.2.2 框架端

Resources.java

package com.alex.io;

import java.io.InputStream;

public class Resources {
    public static InputStream getResourceAsStream(String path) {
        //从classpath的根路径获取配置文件
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }
}

Configuration.java

package com.alex.entity;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

public class Configuration {
    /**
     * 数据源
     */
    private DataSource dataSource;

    /**
     * 所有的sql信息
     */
    private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Map<String, MappedStatement> getMappedStatementMap() {
        return mappedStatementMap;
    }

    public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
        this.mappedStatementMap = mappedStatementMap;
    }
}

MappedStatement.java

package com.alex.entity;

public class MappedStatement {
    private String id;
    private String parameterType;
    private String resultType;
    private String sqlText;

    public MappedStatement() {
    }

    public MappedStatement(String id, String parameterType, String resultType, String sqlText) {
        this.id = id;
        this.parameterType = parameterType;
        this.resultType = resultType;
        this.sqlText = sqlText;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getParameterType() {
        return parameterType;
    }

    public void setParameterType(String parameterType) {
        this.parameterType = parameterType;
    }

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }

    public String getSqlText() {
        return sqlText;
    }

    public void setSqlText(String sqlText) {
        this.sqlText = sqlText;
    }
}

SqlSessionFactoryBuilder.java

package com.alex.sqlSession;

import com.alex.config.SqlMapConfigBuilder;
import com.alex.entity.Configuration;
import org.dom4j.DocumentException;

import java.beans.PropertyVetoException;
import java.io.InputStream;

public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(InputStream in) throws PropertyVetoException, DocumentException {
        // 第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
        SqlMapConfigBuilder sqlMapConfigBuilder = new SqlMapConfigBuilder();
        Configuration configuration = sqlMapConfigBuilder.config(in);

        // 第二:创建sqlSessionFactory对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);

        return defaultSqlSessionFactory;
    }
}

DefaultSqlSession.java

package com.alex.sqlSession;

import com.alex.entity.Configuration;
import com.alex.entity.MappedStatement;

import java.lang.reflect.*;
import java.util.List;

public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;

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

    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws Exception {
        SimpleExecutor executor = new SimpleExecutor();
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        return executor.query(configuration, mappedStatement, params);
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
        List<Object> list = this.selectList(statementId, params);
        if (list.size() == 0) {
            return null;
        }
        if (list.size() == 1) {
            return (T) list.get(0);
        }
        throw new RuntimeException("返回结果过多.");
    }

    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        // 使用JDK动态代理来为Dao接口生成代理对象;通过代理对象调用方法都会执行invoke方法
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 1.准备参数 statementId
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String statementId = className + "." + methodName;
                Type genericReturnType = method.getGenericReturnType();
                // 2.调用方法
                // ParameterizedType 表示参数化类型,例如 Collection<String>。
                if (genericReturnType instanceof ParameterizedType) {
                    return selectList(statementId, args);
                }
                return selectOne(statementId, args);
            }
        });
        return (T) proxyInstance;
    }
}

SimpleExecutor.java

package com.alex.sqlSession;

import com.alex.config.BoundSql;
import com.alex.entity.Configuration;
import com.alex.entity.MappedStatement;
import com.alex.utils.GenericTokenParser;
import com.alex.utils.ParameterMapping;
import com.alex.utils.ParameterMappingTokenHandler;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class SimpleExecutor implements Executor {
    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
        // 1.获取连接
        Connection connection = configuration.getDataSource().getConnection();
        // 2.转换sql语句 #{}转占位符?,并获取参数列表
        String sql = mappedStatement.getSqlText();
        BoundSql boundSql = getBoundSql(sql);
        // 3.获取预处理statement
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
        // 4.设置参数
        String parameterTypeName = mappedStatement.getParameterType();
        Class<?> parameterTypeClass = getClassType(parameterTypeName);
        for (int i = 0; i < boundSql.getParameterMappingList().size(); i++) {
            ParameterMapping parameterMapping = boundSql.getParameterMappingList().get(i);
            // 利用反射特性,寻找value
            Field field = parameterTypeClass.getDeclaredField(parameterMapping.getContent());
            // 暴力访问
            field.setAccessible(true);
            Object o = field.get(params[0]);

            preparedStatement.setObject(i + 1, o);
        }
        // 5.执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        // 6.封装结果集
        List<Object> list = new ArrayList<>();
        Class<?> resultTypeClass = getClassType(mappedStatement.getResultType());
        while (resultSet.next()) {
            Object item = resultTypeClass.newInstance();
            ResultSetMetaData metaData = resultSet.getMetaData();
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                // 获取参数名称
                String columnName = metaData.getColumnName(i);
                // 获取参数value,这里只用varchar和int举例
                int columnType = metaData.getColumnType(i);
                Object value = null;
                if (columnType == Types.INTEGER) {
                    value = resultSet.getInt(columnName);
                } else if (columnType == Types.VARCHAR) {
                    value = resultSet.getString(columnName);
                }
                // 利用反射以及内省(属性描述器)完成封装
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                Method write = propertyDescriptor.getWriteMethod();
                write.invoke(item, value);
            }
            list.add(item);
        }

        return (List<E>) list;
    }

    private Class<?> getClassType(String parameterTypeName) throws ClassNotFoundException {
        if (parameterTypeName != null && parameterTypeName.length() > 0) {
            return Class.forName(parameterTypeName);
        }
        return null;
    }

    private BoundSql getBoundSql(String sql) {
        ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser parser = new GenericTokenParser("#{", "}", tokenHandler);
        // sql
        String parseSql = parser.parse(sql);
        // 参数
        List<ParameterMapping> parameterMappingList = tokenHandler.getParameterMappings();
        return new BoundSql(parseSql, parameterMappingList);
    }

}

其他代码略,可以到这里阅读。

2.2.3 测试
package test;

import com.alex.dao.SysUserDao;
import com.alex.entity.SysUser;
import com.alex.io.Resources;
import com.alex.sqlSession.SqlSession;
import com.alex.sqlSession.SqlSessionFactory;
import com.alex.sqlSession.SqlSessionFactoryBuilder;
import org.dom4j.DocumentException;
import org.junit.Test;

import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;

public class IPersistentTest {
    @Test
    public void test() throws PropertyVetoException, DocumentException {
        InputStream resourceAsSteam = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // test
        SysUserDao sysUserDao = sqlSession.getMapper(SysUserDao.class);

        System.out.println("Test: selectOne");
        SysUser user = new SysUser("1", "xx", null);
        SysUser find = sysUserDao.selectOne(user);
        System.out.println(find);

        System.out.println("");
        System.out.println("Test: selectAll");
        List<SysUser> list = sysUserDao.selectAll();
        for (SysUser item : list) {
            System.out.println(item);
        }
    }
}

运行结果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容