自定义持久层框架设计思路及实现

题记

文章内容输出来源:拉勾教育Java高薪训练营

本篇文章是 开源框架源码剖析 学习课程中的一部分笔记。

前言

说起持久层框架,相信大家第一时间想到的就是Mybatis、Hibernate,它们都是优秀的持久层框架,应用于java后端开发中,为客户端程序提供访问数据库的接口。

我们都知道,JDBC是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。这也就是持久层框架所具备的功能,那既然已经有了JDBC了,为什么还要用持久层框架呢?

原因很简单,因为单纯使用JDBC进行开发会出现效率低下、耗费资源及影响程序拓展性等问题。接下来我们就从JDBC入手,思考如何自定义持久层框架?

1. JDBC基本用法及问题分析

首先,我们来看看传统的JDBC操作代码

public static void main( String[] args ) {
    Connection conn = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    
    String driverClass = "com.mysql.jdbc.Driver";
    String url = "jdbc:mysql://localhost:3306/lagouedu?characterEncoding=utf-8";
    String user = "root";
    String password = "1234";
    
    try {
        // 1.加载数据库驱动
        Class.forName(driverClass);
        // 2.获取数据库连接
        conn = DriverManager.getConnection(url, user, password);
        // 3.编写sql语句  ? 表示占位符
        String sql = "select * from user where username = ?";
        // 4.获取预编译 statement,预编译sql语句
        preparedStatement = conn.prepareStatement(sql);
        // 5.设置sql参数,第一个参数为sql中参数的序号(从1开始),第二个参数为参数值
        preparedStatement.setString(1, "tom");
        // 6.执行sql语句,返回结果集
        resultSet = preparedStatement.executeQuery();
        // 7.遍历结果集,封装结果
        while (resultSet.next()) {
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            
            // TODO:封装结果到User对象
            System.out.println("id="+id+",username="+username);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 8.释放资源
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (preparedStatement != null) {
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

以上即为传统的JDBC操作相关代码,分析以上操作,我们不难发现使用原生的JDBC存在以下几个问题:

  1. 数据库配置信息存在硬编码问题。

  2. 需要频繁创建和释放数据库连接。

  3. sql语句、设置参数存在硬编码问题。 实际开发中sql语句和参数设置变化较大且数量较多,修改sql还需要修改代码,系统不易维护。

  4. 对结果集解析存在硬编码(获取列名),数据库表结构变化需要需要修改相关sql,造成代码不易维护;同时我们还需要手动封装返回结果集到对象中。

2. 解决JDBC问题思路

针对以上问题,我们逐个提出解决方案:

  1. 采用配置文件方式,数据库配置信息置于配置文件中。

  2. 采用c3p0数据库连接池方式。

  3. 采用配置文件方式,相关sql信息都写入配置文件中。

  4. 采用反射/内省自动封装结果集到对应pojo对象中。

3. 自定义持久层框架设计

前面我们针对传统JDBC操作及存在问题做出了分析,并提出了解决方案,现在我们来对照解决方案来设计我们的自定义持久层框架:

从使用端角度:

  1. 需要提供数据库的核心配置信息:sqlMapConfig.xml,提供数据库配置信息,同时引入mapper.xml。

  2. 提供sql相关信息(包括sql语句、参数、返回值等):mapper.xml,存放sql相关信息。

  3. 引入自定义持久层框架jar包,我们定义为IPersistence.jar

从框架端角度:

  1. 读取使用端提供的相关配置文件的内容,并存放在内存中。这里我们定义两个javaBean来存放相关配置信息:

    Configuration:存放数据库连接,以及sql相关信息(即MappedStatement,sql有很多个,所以需要用Map集合存储,方便存取)。

    MappedStatement:sql相关配置信息,sql语句、参数类型、返回结果类型等。

  2. 解析配置文件:

    使用dom4j解析读取的配置文件,把解析到的相关配置信息存放到Configuration和MappedStatement容器对象中。

  3. 创建SqlSessionFactory工厂接口类及其实现类,获取核心数据库配置信息,并生产SqlSession实例对象。

  4. 创建SqlSession接口类及其实现类,用于封装操作CRUD操作的相关方法。

  5. 创建Executor接口及其实现类,用于实现CURD相关操作(底层还是使用的JDBC方法)。

至此,自定义持久层框架就设计完成了,以下我们来实现我们自定义的持久层框架。

4. 自定义持久层框架的实现

4.1 使用端创建配置文件

  1. 创建maven 工程

    首先新建maven工程IPersistence_Test,如下图所示(包名可自行定义):

    完成工程创建后,在pom.xml下引入如下依赖:

    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    ​
        <groupId>com.lagouedu</groupId>
        <artifactId>IPersistence_Test</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
    ​
        <name>IPersistence_Test</name>
        <url>http://maven.apache.org</url>
    ​
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    ​
        <dependencies>
            <!-- 自定义持久层框架jar包 -->
            <dependency>
                <groupId>com.lagouedu</groupId>
                <artifactId>IPersistence</artifactId>
                <version>0.0.1-SNAPSHOT</version>
            </dependency>
            <!-- lombok,可自行选择 -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.18</version>
                <scope>provided</scope>
            </dependency>
            <!-- 日志处理 -->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>1.7.26</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.26</version>
            </dependency>
        </dependencies>
    </project>
  2. 在resources目录下创建sqlMapConfig.xml配置文件,存放数据库连接信息核心配置

    <!-- sqlMapConfig存放数据库连接信息,mapper全路径 -->
    <configration>
        
        <!-- 数据库连接信息 -->
        <dataSource>
            <property name="driverClass" value="com.mysql.jdbc.Driver" />
            <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/lagouedu?characterEncoding=utf-8" />
            <property name="user" value="root" />
            <property name="password" value="1234" />
        </dataSource>
        
        <mapper resource="UserMapper.xml"></mapper>
        
    </configration>
  3. 在resources目录下创建mapper.xml,存放sql相关信息

    <!-- mapper.xml,存放sql相关信息 -->
    <!-- namespace需全局唯一 -->
    <Mapper namespace="user">
        
        <!-- namespace和id组成statemeId,作为sql语句的唯一标识 -->
        <select id="selectAll" paramterType="com.lagouedu.pojo.User" resultType="com.lagouedu.pojo.User">
            select * from user
        </select>
        
        <select id="selectUserById" paramterType="com.lagouedu.pojo.User" resultType="com.lagouedu.pojo.User">
            select * from user where username = #{username}
        </select>
        
        <insert id="insert" paramterType="com.lagouedu.pojo.User">
            insert into user values(#{id}, #{username})
        </insert>
        
        <update id="update" paramterType="com.lagouedu.pojo.User">
            update user set username = #{username} where id = #{id}
        </update>
        
        <delete id="delete" paramterType="java.lang.Integer">
            delete from user where id = #{id}
        </delete>
        
    </Mapper>
  4. 创建User实体

    创建User实体类,对应数据库user表

    package com.lagouedu.pojo;
    ​
    import lombok.Data;
    import lombok.ToString;
    ​
    @Data
    @ToString
    public class User {
        
        private Integer id;
        
        private String username;
    ​
    }
    ​

4.2 框架端读取并解析配置文件

  1. 创建另一个maven工程

    创建框架端maven工程IPersistence,代码结构如下图所示:

  2. pom中引入相关依赖

    以下为本人pom.xml内容:

    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    ​
        <groupId>com.lagouedu</groupId>
        <artifactId>IPersistence</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
    ​
        <name>IPersistence</name>
        <url>http://maven.apache.org</url>
    ​
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    ​
        <dependencies>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.18</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.17</version>
            </dependency>
            <dependency>
                <groupId>c3p0</groupId>
                <artifactId>c3p0</artifactId>
                <version>0.9.1.2</version>
            </dependency>
            
            <dependency>
                <groupId>dom4j</groupId>
                <artifactId>dom4j</artifactId>
                <version>1.6.1</version>
            </dependency>
            <dependency>
                <groupId>jaxen</groupId>
                <artifactId>jaxen</artifactId>
                <version>1.1.6</version>
            </dependency>
    ​
            <!-- 日志处理 -->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>1.7.26</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.26</version>
            </dependency>
        </dependencies>
    </project>
    ​
  3. 读取配置文件

    这一步我们读取使用端的配置文件,并转换为输入流。在io包下新建 Resources 类,编写代码如下:

    /**
     * 读取配置文件
     * @author yz
     *
     */
    public class Resources {
        
        /**
         * 读取配置文件,获取流
         * @param path 文件路径
         * @return InputStream
         */
        public static InputStream getResourceAsSteam(String path) {
            InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
            return resourceAsStream;
        }
    ​
    }

    这个类的调用方法十分简单,直接传递配置文件的路径名作为参数即可。

  4. 解析配置文件,并将核心配置信息存放到容器对象中

    ① 创建容器对象

    前面我们提到,需要解析配置文件,并将解析出来的配置信息存放到Configuration、MappedStatement两个容器对象中,下面我们来创建这两个容器对象:

    Configuration:

    /**
     * 存放数据库信息和mapper信息
     * @author yz
     *
     */
    @Data
    public class Configration {
        
        /**
         * 数据源:数据库信息
         */
        private DataSource dataSource;
        
        /**
         * mapper信息:key:statementId:namespace+id;value:MappedStatement
         */
        private Map<String, MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>();
    ​
    }

    MappedStatement:

    /**
     * 存放sql语句相关信息
     * @author yz
     *
     */
    @Data
    public class MappedStatement {
    	
    	/**
    	 * id
    	 */
    	private String id;
    	
    	/**
    	 * 返回值类型
    	 */
    	private String resultType;
    	
    	/**
    	 * 参数类型
    	 */
    	private String paramterType;
    	
    	/**
    	 * sql语句
    	 */
    	private String sql;
    
    }
    

    ② 创建配置文件解析类

    容器对象我们建完了,接下来我们开始解析配置文件。在config包下分别创建XmlConfigBuilder和XmlMapperBuilder类,代码如下:

    XmlConfigBuilder:用于解析sqlMapConfig.xml

    /**
     * sqlMapConfig.xml解析工具类,内容封装到了configration中
     * @author yz
     *
     */
    public class XmlConfigBuilder {
    	
    	private Configration configration;
    	
    	/**
    	 * 定义无参构造函数
    	 */
    	public XmlConfigBuilder() {
    		this.configration = new Configration();
    	}
    
    	/**
    	 * 使用dom4j解析配置文件,并封装到Configration对象
    	 * @param in 配置文件流
    	 * @return Configration
    	 * @throws DocumentException 
    	 * @throws PropertyVetoException 
    	 */
    	@SuppressWarnings("unchecked")
    	public Configration parseConfig(InputStream in) throws DocumentException, PropertyVetoException {
    		
    		// 1.解析sqlMapConfig.xml
    		// 读取sqlMapConfig.xml配置文件
    		Document document = new SAXReader().read(in);
    		// 获取根标签:即<configration>标签
    		Element rootElement = document.getRootElement();
    		
    		// 获取property标签内容:即数据库配置信息
    		List<Element> list = rootElement.selectNodes("//property");
    		// 封装到properties中
    		Properties properties = new Properties();
    		for (Element ele : list) {
    			String name = ele.attributeValue("name");
    			String value = ele.attributeValue("value");
    			properties.setProperty(name, value);
    		}
    		
    		// 将数据库配置信息设置到c3p0的数据库连接池中
    		ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
    		comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
    		comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
    		comboPooledDataSource.setUser(properties.getProperty("user"));
    		comboPooledDataSource.setPassword(properties.getProperty("password"));
    		configration.setDataSource(comboPooledDataSource);
    		
    		// 2.解析mapper.xml
    		// 获取mapper.xml的全路径--获取文件流--dom4j解析
    		List<Element> mapperList = rootElement.selectNodes("//mapper");
    		for (Element ele : mapperList) {
    			String mapperPath = ele.attributeValue("resource");
    			InputStream inputStream = Resources.getResourceAsSteam(mapperPath);
    			XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configration);
    			xmlMapperBuilder.parse(inputStream);
    		}
    		return configration;
    	}
    }
    

    XmlMapperBuilder:用于解析mapper.xml

    /**
     * mapper.xml解析工具类,封装到configration中
     * @author yz
     *
     */
    public class XmlMapperBuilder {
    	
    	private Configration configration;
    
    	/**
    	 * 有参构造函数
    	 * @param configration
    	 */
    	public XmlMapperBuilder(Configration configration) {
    		this.configration = configration;
    	}
    	
    	/**
    	 * 解析mapper.xml,并将结果封装到configration的mappedStatementMap对象中
    	 * @param in
    	 * @throws DocumentException 
    	 */
    	@SuppressWarnings("unchecked")
    	public void parse(InputStream in) throws DocumentException {
    		// 读取mapper.xml配置文件
    		Document document = new SAXReader().read(in);
    		// 获取根标签:即<mapper>标签
    		Element rootElement = document.getRootElement();
    		// 获取mapper的namespace属性值
    		String namespace = rootElement.attributeValue("namespace");
    		
    		// 获取select、update、insert、delete标签内容
    		List<Element> list = rootElement.selectNodes("//select|//update|//insert|//delete");
    		
    		// 封装MappedStatement对象,并存放到configration中
    		for (Element ele : list) {
    			String id = ele.attributeValue("id");
    			String resultType = ele.attributeValue("resultType");
    			String paramterType = ele.attributeValue("paramterType");
    			String sql = ele.getTextTrim();
    			MappedStatement mappedStatement = new MappedStatement();
    			mappedStatement.setId(id);
    			mappedStatement.setParamterType(paramterType);
    			mappedStatement.setResultType(resultType);
    			mappedStatement.setSql(sql);
    			// key为namepace.id
    			String key = namespace + "." + id;
    			configration.getMappedStatementMap().put(key, mappedStatement);
    		}
    	}
    }
    

    到此,我们已经解析了使用端的配置文件信息,并存放到了Configuration、MappedStatement两个容器对象中,并且Configuration对象中不仅包含有数据库连接信息(DataSource),还包含了sql信息(MappedStatement)。

4.3 创建SqlSessionFactory工厂接口及其实现类,并生产SqlSession

接下来我们使用工厂模式来生产SqlSession(CRUD操作对象)。

在sqlSession包下创建SqlSessionFactoryBuilder类,用于创建SqlSessionFactory工厂对象:

/**
 * SqlSessionFactoryBuilder:生产sqlSessionFactory,并加载configuration对象
 * @author yz
 *
 */
public class SqlSessionFactoryBuilder {
	
	/**
	 * 使用dom4j解析配置文件,并返回SqlSessionFactory
	 * @param in 配置文件流
	 * @return SqlSessionFactory
	 * @throws DocumentException 
	 * @throws PropertyVetoException 
	 */
	public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
		// 1.解析配置文件封装Configration
		XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
		Configration configration = xmlConfigBuilder.parseConfig(in);
		
		// 2.创建SqlSessionFactory,并返回
		DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configration);
		return defaultSqlSessionFactory;
	}
}

在sqlSession包下创建SqlSessionFactory接口类,及其实现实现类DefaultSqlSessionFactory类,用于生产SqlSession对象:

SqlSessionFactory接口类:

/**
 * SqlSessionFactory工厂类接口:生产SqlSession对象
 * SqlSession:会话对象:CRUD操作
 * @author yz
 *
 */
public interface SqlSessionFactory {
	
	/**
	 * 生产SqlSession接口类
	 * @return
	 */
	public SqlSession openSession();
}

DefaultSqlSessionFactory实现类:

/**
 * SqlSessionFactory接口的实现类:生产sqlSeesion对象
 * sqlSeesion:会话对象:CRUD操作
 * @author yz
 *
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {
	
	private Configration configration;

	/**
	 * 有参构造函数
	 * @param configration
	 */
	public DefaultSqlSessionFactory(Configration configration) {
		this.configration = configration;
	}

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

4.4 创建SqlSession接口及其实现类,并封装CRUD操作

前面我们使用工厂模式来生产SqlSession对象,接下来我们来创建它,并封装CRUD操作:

SqlSession接口类:

/**
 * SqlSession:会话对象
 * CRUD操作
 * @author yz
 *
 */
public interface SqlSession {
	
	/**
	 * 根据条件查询
	 * @param <T> 泛型
	 * @param statementId
	 * @param params 查询条件
	 * @return List<T>
	 * @throws Exception 
	 */
	public <E> List<E> selectList(String statementId, Object... params) throws Exception;
	
	/**
	 * 查询单个
	 * @param <T> 泛型
	 * @param statementId 
	 * @param params 查询条件
	 * @return T
	 * @throws Exception 
	 */
	public <T> T selectOne(String statementId, Object... params) throws Exception;
	
	/**
	 * 通用更新操作
	 * @param statementId
	 * @param params 传递的参数
	 * @return
	 * @throws Exception
	 */
	public int update(String statementId, Object... params) throws Exception;
}

DefaultSqlSession接口实现类:

/**
 * SqlSession接口实现类
 * @author yz
 *
 */
public class DefaultSqlSession implements SqlSession {

	private Configration configration;

	/**
	 * 有参构造函数
	 * 
	 * @param configration
	 */
	public DefaultSqlSession(Configration configration) {
		this.configration = configration;
	}

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

	public <T> T selectOne(String statementId, Object... params) throws Exception {
		List<T> list = selectList(statementId, params);
		if (list.size() == 1) {
			return list.get(0);
		} else {
			throw new RuntimeException("查询结果为空或返回结果过多");
		}
	}
	
	public int update(String statementId, Object... params) throws Exception {
		SimpleExecutor simpleExecutor = new SimpleExecutor();
		MappedStatement mappedStatement = configration.getMappedStatementMap().get(statementId);
		int res = simpleExecutor.excuteUpdate(configration, mappedStatement, params);
		return res;
	}
}

其中我们调具体的调用我们写到了SimpleExecutor类中。

4.5 创建Executor接口及其实现类,实现JDBC的增删查改操作

前面我们在SqlSession封装好了CURD的操作,供使用端调用,但是底层实现方法还未实现,现在我们来实现底层对JDBC的操作:

创建Executor接口:

/**
 * Executor接口
 * @author yz
 *
 */
public interface Executor {
	
	/**
	 * 通用查询方法
	 * @param <T> 泛型
	 * @param configration configration对象
	 * @param mappedStatement mappedStatement对象
	 * @param params 查询条件
	 * @return List<T>
	 * @throws Exception 
	 */
	public <E> List<E> query(Configration configration, MappedStatement mappedStatement, Object... params) throws Exception;
	
	/**
	 * 通用更新方法
	 * @param configration configration对象
	 * @param mappedStatement mappedStatement对象
	 * @param params 查询条件
	 * @return 受影响行数
	 * @throws Exception
	 */
	public int excuteUpdate(Configration configration, MappedStatement mappedStatement, Object[] params)  throws Exception;
}

SimpleExecutor实现类:底层调用JDBC方法

/**
 * Executor的实现类:底层还是调用的JDBC方法
 * @author yz
 *
 */
@Slf4j
public class SimpleExecutor implements Executor {

	/**
	 * 通用查询方法的实现
	 * @throws Exception 
	 */
	@SuppressWarnings("unchecked")
	public <E> List<E> query(Configration configration, MappedStatement mappedStatement, Object... params) throws Exception {
		
		PreparedStatement preparedStatement = preparedStatement(configration, mappedStatement, params);
		
		// 执行sql
		ResultSet resultSet = preparedStatement.executeQuery();
		
		// 封装返回结果集:通过反射封装
		//    获取返回值类型对应的class对象
		String resultType = mappedStatement.getResultType();
		Class<?> resultTypeClass = getClassType(resultType);
		
		List<Object> list = new ArrayList<Object>();
		while (resultSet.next()) {
			// 元数据:即结果集的结构信息,比如表名、列数、字段名等
			ResultSetMetaData metaData = resultSet.getMetaData();
			// 获取返回值类型对应对象实体
			Object resultObj = resultTypeClass.newInstance();
			for (int i = 1; i <= metaData.getColumnCount(); i++) {
				// 获取数据库字段名
				String columnName = metaData.getColumnName(i);
				// 获取数据库对应字段的值
				Object value = resultSet.getObject(columnName);
				
				// 使用反射或者内省,根据数据库表和实体的对应关系,完成封装(所以实体的字段名称要和数据库表对应)
				/**
				 * 内省库方法:属性描述器:创建对应class对象目标属性的get、set方法
				 * 参数1:属性名称
				 * 参数2:对应javaBean(对象)的class对象
				 */
				PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
				// 获取写方法
				Method writeMethod = propertyDescriptor.getWriteMethod();
				/**
				 * 对带有指定参数的指定对象调用由此方法,此处相当于调用set方法
				 * 参数1:指定对象
				 * 参数2:指定参数
				 */
				writeMethod.invoke(resultObj, value);
			}
			list.add(resultObj);
		}
		return (List<E>) list;
	}
	
	/**
	 * 通用更新方法
	 * @throws Exception 
	 */
	public int excuteUpdate(Configration configration, MappedStatement mappedStatement, Object[] params) throws Exception {

		PreparedStatement preparedStatement = preparedStatement(configration, mappedStatement, params);
		
		// 执行sql
		int res = preparedStatement.executeUpdate();
		
		return res;
	}
	
	/**
	 * 转换sql,设置sql参数,并返回预处理对象PreparedStatement
	 * @param configration
	 * @param mappedStatement
	 * @param params
	 * @return PreparedStatement
	 * @throws Exception
	 */
	private PreparedStatement preparedStatement(Configration configration, MappedStatement mappedStatement, Object... params) throws Exception {
		// 1.注册驱动,获取数据库连接
		Connection conn = configration.getDataSource().getConnection();
		
		// 2.获取sql:select * from user where username = #{username}
		String sql = mappedStatement.getSql();
		
		// 3.转换sql:select * from user where username = ?;
		// 转换过程中需要对#{}中的值进行解析存储,后续利用反射获取参数值
		BoundSql boundSql = getBoundSql(sql);
		
		// 4.获取预处理对象:PreparedStatement
		log.info("执行sql语句为:{}", boundSql.getSql());
		PreparedStatement preparedStatement = conn.prepareStatement(boundSql.getSql());
		
		// 5.设置参数:通过反射来设置参数值
		//   获取参数类型的全路径
		String paramterType = mappedStatement.getParamterType();
		//    根据全路径获取class对象
		Class<?> paramterTypeClass = getClassType(paramterType);
		
		// 循环遍历设置参数
		List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();
		String paramStr = "";
		for (int i = 0; i < parameterMappingList.size(); i++) {
			ParameterMapping parameterMapping = parameterMappingList.get(i);
			
			// 参数名称
			String content = parameterMapping.getContent();
			
			// 判断参数类型是否为基本类型或其包装类型,是则直接赋值
			if (isCommonDataType(paramterTypeClass) || isWrapClass(paramterTypeClass)) {
				preparedStatement.setObject(i + 1, params[0]);
				paramStr += content+"="+params[0] + ",";
			} else {
				// 否则通过反射:根据参数名称反射设置参数值
				// ①:获取字段
				/*
				 * getField 只能获取public的,包括从父类继承来的字段。
				 * getDeclaredField可以获取本类所有的字段,包括private的,但是不能获取继承来的字段。 
				 *  (注:这里只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true))
				 */
				Field declaredField = paramterTypeClass.getDeclaredField(content);
				// ②:设置暴力访问,方便获取private私有属性的值
				declaredField.setAccessible(true);
				// ③:获取指定对象中此字段的值,这里即查询条件对象
				Object value = declaredField.get(params[0]);
				// ④:设置参数
				preparedStatement.setObject(i + 1, value);
				paramStr += content+"="+value + " ";
			}
		}
		log.info("参数为:{}", paramStr);
		return preparedStatement;
	}
	
	
	/**
	 * 判断是否是基础数据类型,即 int,double,long等类似格式
	 * @param clazz
	 */
	private boolean isCommonDataType(Class<?> clazz) {
		return clazz.isPrimitive();
	}
	
	/**
	 * 判断是否为基本类型的包装类
	 * @param clazz
	 * @return boolean
	 */
	private boolean isWrapClass(Class<?> clazz) {
		try {
			return ((Class<?>) clazz.getField("TYPE").get(null)).isPrimitive();
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * 根据全路径获取Class
	 * @param paramterType
	 * @return
	 * @throws ClassNotFoundException 
	 */
	private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
		if (paramterType != null) {
			Class<?> clazz = Class.forName(paramterType);
			return clazz;
		}
		return null;
	}

	/**
	 * 完成对sql中#{***}的解析工作:1.将#{}用?代替;2.解析出#{}中的值,并进行存储
	 * @param sql xml中sql语句
	 * @return BoundSql
	 */
	private BoundSql getBoundSql(String sql) {
		// 标记处理类:配合标记解析器完成对占位符的解析处理工作
		ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
		// 标记解析器:解析#{}占位符
		GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
		// 解析sql语句,并返回,此时#{***}已经转换成了?
		String parseSql = genericTokenParser.parse(sql);
		// #{***}中解析出来的参数名称
		List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
		
		BoundSql boundSql = new BoundSql(parseSql, parameterMappings);
		return boundSql;
	}
}

我们重点关注preparedStatement方法:

第一步:获取数据库连接,我们将数据库连接信息已经封装到了Configration的dataSource属性中,这里直接取就可以了。不清楚的可以回顾第四节的第二小节内容。

第二步:获取sql。我们在MappedStatement对象中存有sql语句信息,这里直接取就好。

第三步:转换sql。将#{***}转换为?,并保存#{}中的属性名称。解析出来的信息保存到BoundSql实体中。BoundSql属性如下:

@Data
public class BoundSql {
	
	/**
	 * 解析后的sql语句:select * from user where username = ?
	 */
	private String sql;
	
	/**
	 * 原始sql语句#{***}中的参数名称集合
	 */
	private List<ParameterMapping> parameterMappings;

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

}

第四步:获取预处理对象PreparedStatement。

第五步:设置参数。这里我们从mappedStatement中拿到参数的全路径,已经前面处理返回的boundSql对象中的parameterMappings属性(参数变量名称集合);循环遍历parameterMappings设置参数。

第六步:返回preparedStatement。

4.6 使用端测试

在使用端IPersistence_Test项目中编写测试类,代码如下:

private SqlSession sqlSession;
	
@Before
public void loadData() throws Exception {
    InputStream in = Resources.getResourceAsSteam("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
    sqlSession = sqlSessionFactory.openSession();
}

@Test
public void sqlSessionTest() throws Exception {
	// 查询单个
    User params = new User();
    params.setId(1);
    // user:mapper.xml中的namespace;getUserByCondition对应方法名称,即id
    User res = sqlSession.selectOne("user.selectUserById", params);
    log.info("查询单个:{}", res.toString());

    // 查询所有
    List<User> userList = sqlSession.selectList("user.selectAll");
    for (User user : userList) {
        log.info("查询所有:{}", user.toString());
    }

    // 更新
    User user = new User();
    user.setId(1);
    user.setUsername("张三");
    int rows = sqlSession.update("user.update", user);
    log.info("更新执行受影响行数:{}", rows);
}

这样每次使用sqlSession对象调用显然是不合理的,我们定义IUserMppaer接口及其实现类UserMpperImpl

IUserMppaer接口类:

public interface IUserMapper {
	
	/**
	 * 查询所有
	 * @return List<User>
	 * @throws Exception 
	 */
	List<User> findAll() throws Exception;
	
	/**
	 * 查询单个
	 * @param user 查询条件
	 * @return User
	 * @throws Exception 
	 */
	User getUserById(User user) throws Exception;
	
	/**
	 * 新增
	 * @param id
	 * @return
	 * @throws Exception
	 */
	int insertUser(User user) throws Exception;
	
	/**
	 * 更新
	 * @param user
	 * @return
	 * @throws Exception
	 */
	int updateUserById(User user) throws Exception;
	
	/**
	 * 删除
	 * @param id
	 * @return
	 * @throws Exception
	 */
	int deleteUserById(Integer id) throws Exception;
	
}

UserMapperImpl实现类:

public class UserMapperImpl implements IUserMapper {

	public List<User> findAll() throws Exception {
		InputStream in = Resources.getResourceAsSteam("sqlMapConfig.xml");
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
		SqlSession sqlSession = sqlSessionFactory.openSession();
        // findAll方法名需要和mapper.xml里的方法名称对应
		List<User> userList = sqlSession.selectList("user.selectAll");
		return userList;
	}

	public User getUserById(User param) throws Exception {
		InputStream in = Resources.getResourceAsSteam("sqlMapConfig.xml");
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
		SqlSession sqlSession = sqlSessionFactory.openSession();
		
		//调用自定义持久层查询方法
		User user = sqlSession.selectOne("user.selectUserById", param);
		return user;
	}

	public int insertUser(User user) throws Exception {
		InputStream in = Resources.getResourceAsSteam("sqlMapConfig.xml");
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
		SqlSession sqlSession = sqlSessionFactory.openSession();
		return sqlSession.update("user.insert", user);
	}

	public int updateUserById(User user) throws Exception {
		InputStream in = Resources.getResourceAsSteam("sqlMapConfig.xml");
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
		SqlSession sqlSession = sqlSessionFactory.openSession();
		return sqlSession.update("user.update", user);
	}

	public int deleteUserById(Integer id) throws Exception {
		InputStream in = Resources.getResourceAsSteam("sqlMapConfig.xml");
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
		SqlSession sqlSession = sqlSessionFactory.openSession();
		return sqlSession.update("user.delete", id);
	}
}

测试类中调用:

@Test
public void sqlSessionTest() throws Exception {
	// 使用端编写Dao接口和接口实现类来调用
	IUserMapper userMapper = new UserMapperImpl();
	// 查询单个
	User params = new User();
	params.setId(1);
	User res = userMapper.selectUserById(params);
	log.info("查询单个:{}", res.toString());
	
	// 查询所有
	List<User> userList = userMapper.findAll();
	for (User user : userList) {
		log.info("查询所有:{}", user.toString());
	}
	
	// 更新
	User param = new User();
	param.setId(1);
	param.setUsername("王小二");
	int rows1 = userMapper.updateUserById(param);
	log.info("更新执行受影响行数:{}", rows1);
	
	// 删除
	int rows2 = userMapper.deleteUserById(1);
	log.info("更新执行受影响行数:{}", rows2);
	
	// 新增
	User user = new User();
	user.setId(1);
	user.setUsername("王小二");
	int rows3 = userMapper.insertUser(user);
	log.info("更新执行受影响行数:{}", rows3);
}

4.7 优化自定义框架

自定义持久层框架我们基本上算完成了。但是上述代码UserMapperImpl实现类中还存在几个问题:

  1. 存在重复代码:获取文件流、创建sqlSession

  2. 存在硬编码:调用sqlSession方法时statementId存在硬编码

那么怎样去解决这些问题呢?在这里给出解决方案:去掉实现类,使用JDK动态代理生成Dao层接口的代理实现类

我们结合代码来看,首先,我们在框架端SqlSession添加getMapper方法:

/**
 * 为Dao接口动态代理生成实现对象
 * @param <T>
 * @param mapperClass Dao接口方法类
 * @return 代理对象
 */
public <T> T getMapper(Class<?> mapperClass);

并在DefaultSqlSession实现类中添加对应实现方法:

@SuppressWarnings("unchecked")
public <T> T getMapper(Class<?> mapperClass) {
	// 使用jdk动态代理为目标Dao接口生成代理对象,并返回
	// 代理对象调用接口中的任意方法都会执行InvocationHandler中的invoke方法
	// 即:使用端在使用userMapper.findAll()调用时,会执行invoke中的方法
	Object newProxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(),
			new Class[] { mapperClass }, new InvocationHandler() {
				/**
				 * proxy:当前代理对象的引用
				 * method:当前代理对象调用的方法的引用,即findAll()方法
				 * args:调用方法传递的参数
				 */
				public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
					// 其实底层还是执行JDBC方法,所有这里我们可以根据条件调用之前写好的selectList和selectOne方法
					
					// 准备参数1:statementId:namespace+id
					// 因为这里我们只能拿到接口的全限定名和方法名,所以mapper.xml
					// 的namespace应该设置为接口的全限定名,对应方法的sql语句id应该为方法名
					// 接口的全限定名:com.lagouedu.dao.IUserMapper
					String className = method.getDeclaringClass().getName();
					// 方法名:findAll
					String methodName = method.getName();
					String statementId = className + "." + methodName;
					
					// 准备参数2:params:即args
					
					// 根据方法名称判断调用哪个底层方法
					// 从这里可以看出,查询方法应该以select、find、get开头
					if (methodName.startsWith("select") || methodName.startsWith("find")
							|| methodName.startsWith("get")) {
						// 获取被调用方法返回值类型
						Type genericReturnType = method.getGenericReturnType();
						// 判断是否实现 泛型类型参数化
						// 泛型类型参数化: 即返回值类型是否有<***>
						if (genericReturnType instanceof ParameterizedType) {
							return selectList(statementId, args);
						}
						return selectOne(statementId, args);
					} else {
						return update(statementId, args);
					}
				}
			});
	return (T) newProxyInstance;
}

这里我们注意InvocationHandler的invoke方法,其中参数为proxy:当前代理对象的引用、method:当前代理对象调用的方法的引用、args:调用方法传递的参数,分析出通过这三个参数我们并不能拿到statementId。所以我们需要对mapper.xml里的namespace和id进行规范,即:namespace应该设置为接口的全限定名,对应方法的sql语句id应该为方法名。所以对应IUserMapper接口修改UserMapper.xml如下:

<!-- mapper.xml,存放sql相关信息 -->
<!-- namespace需全局唯一 -->
<!-- 以对应接口的全限定名称为namespace -->
<Mapper namespace="com.lagouedu.dao.IUserMapper">
	
	<!-- namespace和id组成statemeId,作为sql语句的唯一标识 -->
	<!-- 以接口对应方法的名称为id -->
	<select id="findAll" paramterType="com.lagouedu.pojo.User" resultType="com.lagouedu.pojo.User">
		select * from user
	</select>
	
	<select id="getUserById" paramterType="com.lagouedu.pojo.User" resultType="com.lagouedu.pojo.User">
		select * from user where id = #{id}
	</select>
	
	<insert id="insertUser" paramterType="com.lagouedu.pojo.User">
		insert into user values(#{id}, #{username})
	</insert>
	
	<update id="updateUserById" paramterType="com.lagouedu.pojo.User">
		update user set username = #{username} where id = #{id}
	</update>
	
	<delete id="deleteUserById" paramterType="java.lang.Integer">
		delete from user where id = #{id}
	</delete>
</Mapper>

优化完成了,接下来我们在测试类中调用:

private SqlSession sqlSession;
private IUserMapper userMapper;

@Before
public void loadData() throws Exception {
	InputStream in = Resources.getResourceAsSteam("sqlMapConfig.xml");
	SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
	sqlSession = sqlSessionFactory.openSession();
	// 返回的是代理对象,调用IUserMapper下的所有方法都会执行invoke()方法
	userMapper = sqlSession.getMapper(IUserMapper.class);
}

@Test
public void sqlSessionTest() throws Exception {
	// 使用代理对象调用接口方法
	// 查询所有
	List<User> userList = userMapper.findAll();
	for (User user : userList) {
		log.info("查询所有:{}", user.toString());
	}
	
	// 查询单个
	User params = new User();
	params.setId(1);
	User result = userMapper.getUserById(params);
	log.info("查询单个:{}", result.toString());
	
	// 更新
	User param = new User();
	param.setId(1);
	param.setUsername("张三");
	int rows1 = userMapper.updateUserById(param);
	log.info("更新执行受影响行数:{}", rows1);
	
	// 删除
	int rows2 = userMapper.deleteUserById(1);
	log.info("更新执行受影响行数:{}", rows2);
	
	// 新增
	User user = new User();
	user.setId(1);
	user.setUsername("王小二");
	int rows3 = userMapper.insertUser(user);
	log.info("更新执行受影响行数:{}", rows3);
}

到此,我们的自定义持久层框架就全部完成了。

总结

我们总结下自定义持久层框架中用到的知识点:jdbc基础、dom4j、xpath、反射、内省、JDK动态代理,涉及到的设计模式有:工厂模式。完整代码请参考:

链接: https://pan.baidu.com/s/1gzf0Sk02lD2X2GumqNbpTQ 提取码: 75ef

写在最后

工作 N 年了,总感觉知道的东西挺多,但一问道“你了解其中的原理吗”等问题时就懵逼了。在工作中我们总是只了解了如何去使用某些技术,而没有去深入的了解其实现原理及技术框架;同时,到了一定的瓶颈,你会发现你不知道如何去提升自己,什么东西都会一点,但就是不精。在考虑跳槽面试时这些问题就会浮出水面。 由此我萌生了找一个培训机构来学习的想法(不是不想自学,百度云资料几十G,只是不知道从哪里学起,同时本人的自制力也比较差~),拉勾Java高薪训练营映入了我的眼帘。大家都知道拉勾是做招聘的,同时有自己复杂的业务场景,这些都是拉勾教育的资本;在课程中拉勾会结合自身复杂的业务场景给学生授课,同时对于优秀的同学,拉勾还提供大厂的内推。导师授课讲的很仔细,深入浅出;还有班主任随班;几百个学院一起学习讨论,那学习氛围杠杠的~

为避免太过广告,就不写太多了,只是想着看到这篇博文的同学,如果有和本人有同样的烦恼的话,拉勾训练营是个不错的选择。

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于JDBC实现自定义持久层框架可以分为以下几个步骤: 1. 设计数据访问接口:首先,你需要定义数据访问接口,包括插入、更新、删除和查询等操作的方法。这些方法应该是通用的,可适用于不同的实体类。 2. 编写实现类:接下来,你需要编写具体的实现类来实现数据访问接口。在这个实现类中,你可以使用JDBC来连接数据库,并执行SQL语句。 3. 定义实体类:为了与数据库中的表进行映射,你需要定义实体类。这些实体类应该与数据库表的结构相对应,并且包含与表中列对应的属性。 4. 建立连接:在进行数据库操作之前,你需要建立与数据库的连接。可以使用JDBC提供的`java.sql.Connection`对象来创建连接。 5. 执行SQL语句:通过使用JDBC提供的`java.sql.Statement`或`java.sql.PreparedStatement`对象,你可以执行SQL语句。可以通过编写动态SQL语句或使用预编译语句来实现各种操作。 6. 处理结果集:对于查询操作,你需要处理返回的结果集。可以使用JDBC提供的`java.sql.ResultSet`对象来遍历结果集并获取数据。 7. 关闭连接:完成数据库操作后,记得关闭连接以释放资源。可以使用`java.sql.Connection`对象的`close()`方法关闭连接。 通过以上步骤,你可以基于JDBC实现自定义持久层框架。当然,在实际应用中,你还可以考虑使用连接池、事务管理等技术来提高性能和可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值