题记
文章内容输出来源:拉勾教育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存在以下几个问题:
-
数据库配置信息存在硬编码问题。
-
需要频繁创建和释放数据库连接。
-
sql语句、设置参数存在硬编码问题。 实际开发中sql语句和参数设置变化较大且数量较多,修改sql还需要修改代码,系统不易维护。
-
对结果集解析存在硬编码(获取列名),数据库表结构变化需要需要修改相关sql,造成代码不易维护;同时我们还需要手动封装返回结果集到对象中。
2. 解决JDBC问题思路
针对以上问题,我们逐个提出解决方案:
-
采用配置文件方式,数据库配置信息置于配置文件中。
-
采用c3p0数据库连接池方式。
-
采用配置文件方式,相关sql信息都写入配置文件中。
-
采用反射/内省自动封装结果集到对应pojo对象中。
3. 自定义持久层框架设计
前面我们针对传统JDBC操作及存在问题做出了分析,并提出了解决方案,现在我们来对照解决方案来设计我们的自定义持久层框架:
从使用端角度:
-
需要提供数据库的核心配置信息:sqlMapConfig.xml,提供数据库配置信息,同时引入mapper.xml。
-
提供sql相关信息(包括sql语句、参数、返回值等):mapper.xml,存放sql相关信息。
-
引入自定义持久层框架jar包,我们定义为IPersistence.jar
从框架端角度:
-
读取使用端提供的相关配置文件的内容,并存放在内存中。这里我们定义两个javaBean来存放相关配置信息:
Configuration:存放数据库连接,以及sql相关信息(即MappedStatement,sql有很多个,所以需要用Map集合存储,方便存取)。
MappedStatement:sql相关配置信息,sql语句、参数类型、返回结果类型等。
-
解析配置文件:
使用dom4j解析读取的配置文件,把解析到的相关配置信息存放到Configuration和MappedStatement容器对象中。
-
创建SqlSessionFactory工厂接口类及其实现类,获取核心数据库配置信息,并生产SqlSession实例对象。
-
创建SqlSession接口类及其实现类,用于封装操作CRUD操作的相关方法。
-
创建Executor接口及其实现类,用于实现CURD相关操作(底层还是使用的JDBC方法)。
至此,自定义持久层框架就设计完成了,以下我们来实现我们自定义的持久层框架。
4. 自定义持久层框架的实现
4.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>
-
在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>
-
在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>
-
创建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 框架端读取并解析配置文件
-
创建另一个maven工程
创建框架端maven工程IPersistence,代码结构如下图所示:
-
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>
-
读取配置文件
这一步我们读取使用端的配置文件,并转换为输入流。在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; } }
这个类的调用方法十分简单,直接传递配置文件的路径名作为参数即可。
-
解析配置文件,并将核心配置信息存放到容器对象中
① 创建容器对象
前面我们提到,需要解析配置文件,并将解析出来的配置信息存放到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实现类中还存在几个问题:
-
存在重复代码:获取文件流、创建sqlSession
-
存在硬编码:调用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高薪训练营映入了我的眼帘。大家都知道拉勾是做招聘的,同时有自己复杂的业务场景,这些都是拉勾教育的资本;在课程中拉勾会结合自身复杂的业务场景给学生授课,同时对于优秀的同学,拉勾还提供大厂的内推。导师授课讲的很仔细,深入浅出;还有班主任随班;几百个学院一起学习讨论,那学习氛围杠杠的~
为避免太过广告,就不写太多了,只是想着看到这篇博文的同学,如果有和本人有同样的烦恼的话,拉勾训练营是个不错的选择。