Java之手写实现ORM框架

本文介绍如何借鉴Mybatis框架手写一个ORM框架,包括解析配置文件、执行查询等核心步骤,并提供了一个完整的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

借鉴Mybatis框架手写一个ORM框架。mybatis整体架构中的整体思路是,首先解析一下配置文件,一个是框架的全局配置文件,一个是mapper配置文件,定义格式如下

<configuration>
</configuration>

<mapper>
</mapper>

其中,全局配置文件中包含数据源配置信息和mapper配置文件所在位置,如下

<configuration>
    <!-- 数据源配置 -->
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf8"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </dataSource>

    <!-- 记录mapper.xml的路径 -->
    <!--<mapper resource="UserMapper"/>-->
    <mapper resource="com/***/UserMapper.xml"/>
</configuration>

对该配置文件解析后,可以将这些数据封装成一个java实体,该实体包含了所有的配置信息,全局配置文件中可能包含多个mapper文件的配置,可以将其封装成一个map集合,如下

Map<String, MapperStatement>

其中,集合的key为String类型,value为MapperStatement类型,MapperStatement是对mapper配置文件的一个封装,如下

<mapper namespace="com.***.UserMapper">
    <select id="selectList" resultType="com.***.User">
        select * from user
    </select>
</mapper>

框架会将整个工程中的每个mapper配置文件都封装成一个个MapperStatement并保存到map中,这就需要对每个MapperStatement进行区分,区分的关键就是mapper配置文件中的namespace和id,将其拼接起来作为statementId。
然后提供对应的curd方法,方法的作用是对sql语句进行解析并调用jdbc查询数据库,最后封装结果集。

具体实现如下:

  1. 解析配置文件
    新建一个类Resources,该类负责将一个文件转换成输入流,如下
public class Resources {

    /**
     * 根据配置文件的路径将配置文件加载成字节输入流
     *
     * @param path
     * @return
     */
    public static InputStream getResourceAsStream(String path) {
        return Resources.class.getClassLoader().getResourceAsStream(path);
    }
}

接下来新建一个SqlSessionFactoryBuilder类,该类提供一个build方法来生成SqlSessionFactory,如下

public class SqlSessionFactoryBuilder {

    /**
     * 构建
     *
     * @param inputStream 文件配置流
     * @return
     * @throws DocumentException
     * @throws PropertyVetoException
     */
    public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {
        // 使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
        XmlConfigBuilder builder = new XmlConfigBuilder();
        Configuration configuration = builder.parseConfig(inputStream);
        // 创建SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return sqlSessionFactory;
    }
}

其中SqlSessionFactory是一个接口,需要创建它的默认实现类DefaultSqlSessionFactory,该类需要传入一个Configuration类型对象,这个Configuration就是对全局配置的一个封装,如下

public class Configuration {

    private DataSource dataSource;
    /**
     *  key:statementId,namespace + id
     *  value:封装好的MapperStatement对象
     */
    private Map<String, MapperStatement> mapperStatementMap = new HashMap<>();

}

那么现在关键就是对全局配置文件进行解析了,需提供一个类XmlConfigBuilder,该类提供parseConfig方法来将输入流转换为Configuration对象,如下

public class XmlConfigBuilder {

    private Configuration configuration;

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

    public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(inputStream);
        // 全局查找<configuration>标签
        Element rootElement = document.getRootElement();
        // 全局查找<property>标签
        List<Node> list = rootElement.selectNodes("//property");
        List<Element> propertyList = list.stream().map(node -> (Element) node).collect(Collectors.toList());
        Properties properties = new Properties();
        propertyList.forEach(element -> {
            // 获取到标签中的name和value属性
            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文件
        XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
        List<Node> list2 = rootElement.selectNodes("//mapper");
        List<Element> mapperList = list2.stream().map(node -> (Element) node).collect(Collectors.toList());
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream mapperAsStream = Resources.getResourceAsStream(mapperPath);
            //XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
            xmlMapperBuilder.parse(mapperAsStream);
        }
        return configuration;
    }
}

借助dom4j可以很容易的实现解析,将每个标签中的属性和属性值读取处理进行对应的封装即可,对应mapper配置文件的解析也是如此,通过resource属性可以得到mapper文件位置,然后将其转为输入流并解析,如下

public class XmlMapperBuilder {

    private Configuration configuration;

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

    /**
     * mapper配置文件解析成MappedStatement对象
     *
     * @param inputStream
     * @throws DocumentException
     */
    public void parse(InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        // 得到<mapper>根标签
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        // 得到<select>标签
        List<Node> list = rootElement.selectNodes("//select");
        List<Element> selectList = list.stream().map(node -> (Element) node).collect(Collectors.toList());
        handlerMapperStatement(namespace, selectList);
        // 得到<insert>标签
        List<Node> list2 = rootElement.selectNodes("//insert");
        List<Element> insertList = list2.stream().map(node -> (Element) node).collect(Collectors.toList());
        handlerMapperStatement(namespace,insertList);
    }

    /**
     * 封装MapperStatement对象
     * @param namespace   命名空间
     * @param selectNodes 操作节点
     */
    private void handlerMapperStatement(String namespace, List<Element> selectNodes) {
        if (selectNodes == null || selectNodes.size() == 0) {
            return;
        }
        selectNodes.forEach(element -> {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String parameterType = element.attributeValue("parameterType");
            String sql = element.getTextTrim();
            // 封装MapperStatement对象
            MapperStatement mapperStatement = new MapperStatement();
            mapperStatement.setId(id);
            mapperStatement.setResultType(resultType);
            mapperStatement.setParameterType(parameterType);
            mapperStatement.setSql(sql);
            //key:namespace+id,用于标识哪个命名空间下的哪个方法,不然不同文件可能有重名方法
            String key = namespace + "." + id;
            // 将MapperStatement对象保存到Configuration中
            configuration.getMapperStatementMap().put(key, mapperStatement);
        });
    }
}

同样读取每个配置的属性名和属性值,对应MappedStatementMap的封装,其map的key为namespace+id。

  1. 执行查询
    读取完配置文件之后,就得到了一个SqlSessionFactory接口,其实现类为DefaultSqlSessionFactory对象,该对象提供一个openSession方法来获得SqlSession对象,如下
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

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

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

其中,返回的SqlSession接口的默认实现DefaultSqlSession,如下

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 {
        Executor executor = new SimpleExecutor();
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
        List<Object> objects = executor.query(configuration, mapperStatement, params);
        return (List<E>) objects;
    }

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

    @Override
    public Integer insertOne(String statementId, Object... params) throws Exception {
        Executor executor = new SimpleExecutor();
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
        return executor.save(configuration, mapperStatement, params);
    }

    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        //通过jdk动态代理获取对象
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //方法名(这也是为什么xml中id要和接口方法名一致)
                        String methodName = method.getName();
                        //类全路径名(这也是为什么xml中namespace要和接口全路径一致)
                        String className = method.getDeclaringClass().getName();
                        String statementId = className + "." + methodName;
                        Type genericReturnType = method.getGenericReturnType();
                        if (genericReturnType instanceof ParameterizedType) {
                            return selectList(statementId, args);
                        }
                        if (genericReturnType.getTypeName().contains("Integer")) {
                            return insertOne(statementId, args);
                        }
                        return selectOne(statementId, args);
                    }
                });
        return (T) proxyInstance;
    }

}

在该对象中,实现查询操作时需要借助一个SimpleExecutor类来实现具体的查询,那执行查询操作需要那些参数,包括configuration、MapperStatement、查询参数,其中configuration里面是数据源和MapperStatement对象,这样就可以实现查询了。

  1. 实现查询
    执行查询需要Executor来完成,其实现类如下:
public class SimpleExecutor implements Executor {

    @Override
    public <T> List<T> query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws Exception {
        Connection connection = configuration.getDataSource().getConnection();
        // select * from e_user where id = #{id} and name = #{name}
        String sql = mapperStatement.getSql();
        // 将sql中的 #{} 替换为 ?
        SqlMapping sqlMapping = getSqlMapping(sql);
        PreparedStatement preparedStatement = connection.prepareStatement(sqlMapping.getParseSql());
        // 获取到参数的全限定类名
        String parameterType = mapperStatement.getParameterType();
        Class<?> parameterClass = getClassType(parameterType);
        // 设置参数
        List<ParameterMapping> parameterMappings = sqlMapping.getParameterMappings();
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            String fieldName = parameterMapping.getParam();
            // 反射设置值
            Field field = parameterClass.getDeclaredField(fieldName);
            field.setAccessible(true);
            Object param = field.get(params[0]);
            preparedStatement.setObject(i + 1, param);
        }
        // 执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        String resultType = mapperStatement.getResultType();
        Class<?> resultTypeClass = getClassType(resultType);
        List<Object> list = new ArrayList<>();
        // 封装返回结果集
        while (resultSet.next()) {
            // 获取实体实例
            Object instance = resultTypeClass.newInstance();
            // 获取元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            for (int i = 1; i <= metaData.getColumnCount(); ++i) {
                // 获取字段名
                String columnName = metaData.getColumnName(i);
                // 获取字段值
                Object columnValue = resultSet.getObject(columnName);
                // 内省设置值,映射表和实体的关系
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(instance, columnValue);
            }
            list.add(instance);
        }
        return (List<T>) list;
    }

    @Override
    public Integer save(Configuration configuration, MapperStatement mappedStatement, Object... params) throws Exception {
        Connection connection = configuration.getDataSource().getConnection();

        SqlMapping sqlMapping = getSqlMapping(mappedStatement.getSql());

        PreparedStatement preparedStatement = connection.prepareStatement(sqlMapping.getParseSql());

        List<ParameterMapping> parameterMappings = sqlMapping.getParameterMappings();
        String parameterType = mappedStatement.getParameterType();
        Class<?> parameterTypeClass = getClassType(parameterType);
        for (int i = 0; i < parameterMappings.size(); i++) {
            String fieldName = parameterMappings.get(i).getParam();
            Field field = parameterTypeClass.getDeclaredField(fieldName);
            field.setAccessible(true);
            Object param = field.get(params[0]);
            preparedStatement.setObject(i + 1, param);
        }

        return preparedStatement.executeUpdate();
    }

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

    /**
     * 解析sql
     *
     * @param sql
     * @return
     */
    private SqlMapping getSqlMapping(String sql) {
        // 配合标记解析器完成占位符的解析
        ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", tokenHandler);
        // sql解析
        String parseSql = genericTokenParser.parse(sql);
        // 解析#{}中的参数名称
        List<ParameterMapping> parameterMappings = tokenHandler.getParameterMappings();
        return new SqlMapping(parseSql, parameterMappings);
    }
}

整个框架的核心部分就是这个SimpleExecutor类,我们知道,JDBC中preparedStatement类执行的sql是以?作为占位符的,所以我们把#{}替换成?,并将#{id}里面的属性名取出来,这就是查询的一些参数信息,将参数类型和返回类型均通过反射内省技术进行值的封装后,即可得到查询结果。
其整体的工程结构如下:
在这里插入图片描述

  1. 工程应用
    新建一个java工程,引入手写的ORM框架,如下
	<dependencies>
        <dependency>
            <groupId>com.***</groupId>
            <artifactId>mybatis_***</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

注意其前提是将手写的ORM工程打包到maven的本地仓库,这样就可以通过pom进行引入了。
全局配置文件,如下

<configuration>
    <!-- 数据源配置 -->
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf8"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </dataSource>

    <!-- 记录mapper.xml的路径 -->
    <!--<mapper resource="UserMapper"/>-->
    <mapper resource="com/***/UserMapper.xml"/>
</configuration>

mapper配置文件,如下

<mapper namespace="com.ldc.mapper.UserMapper">

    <select id="selectList" resultType="com.ldc.model.User">
        select * from user
    </select>

    <select id="selectOne" parameterType="com.ldc.model.User" resultType="com.ldc.model.User">
        select * from user where id = #{id} and name = #{name}
    </select>

    <insert id="insertOne" parameterType="com.ldc.model.User" resultType="Integer">
        insert into user(name) values(#{name})
    </insert>

</mapper>

测试,如下

@Test
    public void test() throws Exception {
        InputStream inputStream = Resources.getResourceAsStream("Mybatis-Config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = new User();
        user.setId(2);
        user.setName("caocao");
        List<User> userList = sqlSession.selectList("com.ldc.mapper.UserMapper.selectList");
        System.out.println(userList);
        User user2 = sqlSession.selectOne("com.ldc.mapper.UserMapper.selectOne", user);
        System.out.println(user2);
//        Integer integer = sqlSession.insertOne("com.ldc.mapper.UserMapper.insertOne", user);
//        System.out.println(integer);
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> userList2 = userMapper.selectList();
        System.out.println(userList2);
        User user3 = userMapper.selectOne(user);
        System.out.println(user3);
//        Integer integer2 = userMapper.insertOne(user);
//        System.out.println(integer2);

    }

测试结果,如下
在这里插入图片描述
继续深入理解ORM框架底层,做到知其然,知其所以然。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值