目录
- 1 JDBC操作数据库问题分析
- 2 JDBC问题分析和解决思路
- 3 自定义持久层框架_思路分析
- 3.1 使用JDBC和使用持久层框架区别
- 3.2 核心接口/类重点说明
- 3.3 项目使用端
- 3.4 自定义框架本身
- 3.5 最终手写的持久层框架结构参考
- 4 自定义持久层框架_编码
- 5 自定义持久层框架优化
1 JDBC操作数据库问题分析
JDBC API 允许应用程序访问任何形式的表格数据,特别是存储在关系数据库中的数据
代码示例:
public static void main(String[] args) { Connection connection = null;PreparedStatement preparedStatement = null;ResultSet resultSet = null;try {// 加载数据库驱动Class.forName("com.mysql.jdbc.Driver");// 通过驱动管理类获取数据库链接connection =
DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis? characterEncoding=utf-8", "root", "root");// 定义sql语句?表示占位符String sql = "select * from user where username = ?";// 获取预处理statementpreparedStatement = connection.prepareStatement(sql);// 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值 preparedStatement.setString(1, "tom");// 向数据库发出sql执行查询,查询出结果集resultSet = preparedStatement.executeQuery();// 遍历查询结果集while (resultSet.next()) {int id = resultSet.getInt("id");String username = resultSet.getString("username");// 封装Useruser.setId(id);user.setUsername(username);}System.out.println(user);}} catch (Exception e) {e.printStackTrace();} finally {// 释放资源if (resultSet != null) {try {resultSet.close();} catch (SQLException e) {e.printStackTrace();}}if (preparedStatement != null) {try {preparedStatement.close();} catch (SQLException e) {e.printStackTrace();}}if (connection != null) {try {connection.close();} catch (SQLException e) {e.printStackTrace();}}}
}
2 JDBC问题分析和解决思路
剖开代码,逐个分析:
(1)加载驱动,获取链接:
-
存在问题1:数据库配置信息存在硬编码问题。
优化思路:使用配置文件!
-
存在问题2:频繁创建、释放数据库连接问题。
优化思路:使用数据连接池!
(2)定义sql、设置参数、执行查询:
-
存在问题3:SQL语句、设置参数、获取结果集参数均存在硬编码问题 。
优化思路:使用配置文件!
(2)遍历查询结果集:
-
存在问题4:手动封装返回结果集,较为繁琐
优化思路:使用Java反射、内省!
针对JDBC各个环节中存在的不足,现在,我们整理出对应的优化思路,统一汇总:
存在问题 | 优化思路 |
---|---|
数据库配置信息存在硬编码问题 | 使用配置文件 |
频繁创建、释放数据库连接问题 | 使用数据连接池 |
SQL语句、设置参数、获取结果集参数均存在硬编码问题 | 使用配置文件 |
手动封装返回结果集,较为繁琐 | 使用Java反射、内省 |
3 自定义持久层框架_思路分析
JDBC是个人作战,凡事亲力亲为,低效而高险,自己加载驱动,自己建连接,自己 …
而持久层框架好比是多工种协作,分工明确,执行高效,有专门负责解析注册驱动建立连接的,有专门管理数据连接池的,有专门执行sql语句的,有专门做预处理参数的,有专门装配结果集的 …
优化思路: 框架的作用,就是为了帮助我们减去繁重开发细节与冗余代码,使我们能更加专注于业务应用开发。
3.1 使用JDBC和使用持久层框架区别
是不是发现,拥有这么一套持久层框架是如此舒适,我们仅仅需要干两件事:
- 配置数据源(地址/数据名/用户名/密码)
- 编写SQL与参数准备(SQL语句/参数类型/返回值类型)
框架,除了思考本身的工程设计,还需要考虑到实际项目端的使用场景,干系方涉及两端:
- 使用端(实际项目)
- 持久层框架本身
以上两步,我们通过一张架构图《 手写持久层框架基本思路 》来梳理清楚:
3.2 核心接口/类重点说明
分工协作 | 角色定位 | 类名定义 |
---|---|---|
负责读取配置文件 | 资源辅助类 | Resources |
负责存储数据库连接信息 | 数据库资源类 | Configuration |
负责存储SQL映射定义、存储结果集映射定义 | SQL与结果集资源类 | MappedStatement |
负责解析配置文件,创建会话工厂SqlSessionFactory | 会话工厂构建者 | SqlSessionFactoryBuilder |
负责创建会话SqlSession | 会话工厂 | SqlSessionFactory |
指派执行器Executor | 会话 | SqlSession |
负责执行SQL (配合指定资源Mapped Statement) | 执行器 | Executor |
正常来说项目只对应一套数据库环境,一般对应一个SqlSessionFactory实例对象,我们使用单例模式只创建一个SqlSessionFactory实例。
如果需要配置多套数据库环境,那需要做一些拓展,例如Mybatis中通过environments等配置就可以支持多套测试/生产数据库环境进行切换。
3.3 项目使用端
(1)调用框架API,除了引入自定义持久层框架的jar包
(2)提供两部分配置信息:1.sqlMapConfig.xml : 数据库配置信息(地址/数据名/用户名/密码),以及mapper.xml的全路径
2.mapper.xml : SQL配置信息,存放SQL语句、参数类型、返回值类型相关信息
3.4 自定义框架本身
1、加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中。
2、 创建两个javaBean(容器对象):存放配置文件解析出来的内容
3、解析配置文件(使用dom4j) ,并创建SqlSession会话对象
4、创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory
5、创建SqlSession接口以及实现类DefaultSqlSession
6、创建Executor接口以及实现类SimpleExecutor
基本过程我们已经清晰,我们再细化一下类图,更好的助于我们实际编码:
3.5 最终手写的持久层框架结构参考
4 自定义持久层框架_编码
<properties><!-- Encoding --><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.encoding>UTF-8</maven.compiler.encoding><java.version>11</java.version><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><!--引入ipersistent的依赖-->
在使用端项目中创建配置配置文件
创建 sqlMapConfig.xml
<configuration> <!--1.配置数据库配置信息--><dataSource><property name="driverClassName" value="com.mysql.jdbc.Driver"></property><property name="url" value="jdbc:mysql:///zdy_mybatis?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC"></property><property name="username" value="root"></property><property name="password" value="root"></property></dataSource><!--2.引入映射配置文件--><mappers><mapper resource="mapper/UserMapper.xml"></mapper></mappers></configuration>
mapper.xml
<mapper namespace="User"><!--根据条件查询单个--><select id="selectOne" resultType="com.oldlu.pojo.User" parameterType="com.oldlu.pojo.User">select * from user where id = #{id} and username = #{username}</select><!--查询所有--><select id="selectList" resultType="com.oldlu.pojo.User">select * from user</select>
</mapper>
User实体
public class User {//主键标识private Integer id;//用户名private String username;public Integer getId() { return id;}public void setId(Integer id) { this.id = id;}public String getUsername() { return username;}public void setUsername(String username) { this.username = username;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' + '}';}
}
再创建一个Maven子工程并且导入需要用到的依赖坐标
<properties><!-- Encoding --><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.encoding>UTF-8</maven.compiler.encoding><java.version>11</java.version><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><!-- mysql 依赖--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency><!--junit 依赖--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><!--作用域测试范围--><scope>test</scope></dependency><!--dom4j 依赖--><dependency><groupId>dom4j</groupId><artifactId>dom4j</artifactId><version>1.6.1</version></dependency><!--xpath 依赖--><dependency><groupId>jaxen</groupId><artifactId>jaxen</artifactId><version>1.1.6</version></dependency><!--druid连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.21</version></dependency><!-- log日志 --><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency></dependencies>
Resources
public class Resources {/*** 根据配置文件的路径,加载成字节输入流,存到内存中* @param path* @return*/public static InputStream getResourceAsSteam(String path){InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);return resourceAsStream;}
Configuration
/*** 存放核心配置文件解析的内容*/
public class Configuration {// 数据源对象private DataSource dataSource;// map : key :statementId value : 封装好的MappedStatementprivate 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
/*** 存放解析映射配置文件的内容* <select id="selectOne" resultType="com.oldlu.pojo.User" parameterType="com.oldlu.pojo.User">* select * from user where id = #{id} and username = #{username}* </select>*/
public class MappedStatement {// 1.唯一标识 (statementId namespace.id)private String statementId;// 2.返回结果类型private String resultType;// 3.参数类型private String parameterType;// 4.要执行的sql语句private String sql;// 5.mapper代理:sqlcommandTypeprivate String sqlcommandType;public String getSqlcommandType() {return sqlcommandType;}public void setSqlcommandType(String sqlcommandType) {this.sqlcommandType = sqlcommandType;}public String getStatementId() {return statementId;}public void setStatementId(String statementId) {this.statementId = statementId;}public String getResultType() {return resultType;}public void setResultType(String resultType) {this.resultType = resultType;}public String getParameterType() {return parameterType;}public void setParameterType(String parameterType) {this.parameterType = parameterType;}public String getSql() {return sql;}public void setSql(String sql) {this.sql = sql;}
}
SqlSessionFactoryBuilder
public class SqlSessionFactoryBuilder {/*** 1.解析配置文件,封装Configuration 2.创建SqlSessionFactory工厂对象* @return*/public SqlSessionFactory build(InputStream inputStream) throws DocumentException {// 1.解析配置文件,封装ConfigurationXMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();Configuration configuration = xmlConfigBuilder.parse(inputStream);SqlSessionFactory defatultSqlSessionFactory = new DefatultSqlSessionFactory(configuration);return defatultSqlSessionFactory;}}
XMLConfigerBuilder
public class XMLConfigBuilder {private Configuration configuration;public XMLConfigBuilder() {configuration = new Configuration();}/*** 使用dom4j解析xml文件,封装configuration对象* @param inputStream* @return*/public Configuration parse(InputStream inputStream) throws DocumentException {Document document = new SAXReader().read(inputStream);Element rootElement = document.getRootElement();// 解析核心配置文件中数据源部分List<Element> list = rootElement.selectNodes("//property");// <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>Properties properties = new Properties();for (Element element : list) {String name = element.attributeValue("name");String value = element.attributeValue("value");properties.setProperty(name,value);}// 创建数据源对象(连接池)DruidDataSource druidDataSource = new DruidDataSource();druidDataSource.setDriverClassName(properties.getProperty("driverClassName"));druidDataSource.setUrl(properties.getProperty("url"));druidDataSource.setUsername(properties.getProperty("username"));druidDataSource.setPassword(properties.getProperty("password"));// 创建好的数据源对象封装进configuration中、configuration.setDataSource(druidDataSource);// 解析映射配置文件// 1.获取映射配置文件的路径 2.解析 3.封装好mappedStatementList<Element> mapperList = rootElement.selectNodes("//mapper");for (Element element : mapperList) {String mapperPath = element.attributeValue("resource");InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);xmlMapperBuilder.parse(resourceAsSteam);}return configuration;}
}
XMLMapperBuilder
public class XMLMapperBuilder {private Configuration configuration;public XMLMapperBuilder(Configuration configuration) {this.configuration = configuration;}public void parse(InputStream inputStream) throws DocumentException, ClassNotFoundException {Document document = new SAXReader().read(inputStream);Element rootElement = document.getRootElement();String namespace = rootElement.attributeValue("namespace");List<Element> select = rootElement.selectNodes("select");for (Element element : select) { //id的值String id = element.attributeValue("id");String paramterType = element.attributeValue("paramterType");String resultType = element.attributeValue("resultType"); //输入参数classClass<?> paramterTypeClass = getClassType(paramterType);//返回结果classClass<?> resultTypeClass = getClassType(resultType);//statementIdString key = namespace + "." + id;//sql语句String textTrim = element.getTextTrim();//封装 mappedStatementMappedStatement mappedStatement = new MappedStatement();mappedStatement.setId(id);mappedStatement.setParamterType(paramterTypeClass);mappedStatement.setResultType(resultTypeClass);mappedStatement.setSql(textTrim);//填充 configurationconfiguration.getMappedStatementMap().put(key, mappedStatement); private Class<?> getClassType (String paramterType) throws ClassNotFoundException {Class<?> aClass = Class.forName(paramterType);return aClass;}
}
sqlSessionFactory 接口及D efaultSqlSessionFactory 实现类
public interface SqlSessionFactory {/*** 生产sqlSession :封装着与数据库交互的方法* @return*/public SqlSession openSession();}public class DefatultSqlSessionFactory implements SqlSessionFactory {private Configuration configuration;public DefatultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}@Overridepublic SqlSession openSession() {// 执行器创建出来Executor executor = new SimpleExecutor();DefaultSqlSession defaultSqlSession = new DefaultSqlSession(configuration,executor);return defaultSqlSession;}
}
sqlSession 接口及 DefaultSqlSession 实现类
public interface SqlSession {/*** 查询所有的方法 select * from user where username like '%aaa%' and sex = ''* 参数1:唯一标识* 参数2:入参*/public <E> List<E> selectList(String statementId,Object parameter) throws Exception;/*** 查询单个的方法*/public <T> T selectOne(String statementId,Object parameter) throws Exception;
}public class DefaultSqlSession implements SqlSession {private Configuration configuration;private Executor executor;public DefaultSqlSession(Configuration configuration, Executor executor) {this.configuration = configuration;this.executor = executor;}@Override // user.selectList 1 tom userpublic <E> List<E> selectList(String statementId, Object params) throws Exception {MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);// 将查询操作委派给底层的执行器List<E> list = executor.query(configuration,mappedStatement,params);return list;}@Overridepublic <T> T selectOne(String statementId, Object params) throws Exception {List<Object> list = this.selectList(statementId, params);if(list.size() == 1){return (T) list.get(0);}else if(list.size() > 1){throw new RuntimeException("返回结果过多");}else {return null;}}
}
Executor
public interface Executor {<E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object params) throws Exception;
}
SimpleExecutor
public class SimpleExecutor implements Executor {/*** 执行JDBC操作* @param configuration* @param mappedStatement* @param params* @param <E>* @return*/@Override // user productpublic <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object params) throws Exception {// 1. 加载驱动,获取连接Connection connection = configuration.getDataSource().getConnection();// 2. 获取prepareStatement预编译对象/*select * from user where id = #{id} and username = #{username}select * from user where id = ? and username = ?占位符替换 :#{}替换成? 注意:#{id}里面的id名称要保存*/String sql = mappedStatement.getSql();BoundSql boundSql = getBoundSQL(sql);String finaLSql = boundSql.getFinaLSql();PreparedStatement preparedStatement = connection.prepareStatement(finaLSql);// 3.设置参数// 问题1: Object param(类型不确定 user/product/map/String)// 问题2:该把对象中的哪一个属性赋值给哪一个占位符呢?List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();if(parameterMappings.size() > 0){// com.oldlu.pojo.UserString parameterType = mappedStatement.getParameterType();Class<?> parameterTypeClass = Class.forName(parameterType);for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);// idString content = parameterMapping.getContent();// 反射Field declaredField = parameterTypeClass.getDeclaredField(content);// 暴力访问declaredField.setAccessible(true);Object value = declaredField.get(params);preparedStatement.setObject(i+1 ,value);}}// 4.执行sql,发起查询ResultSet resultSet = preparedStatement.executeQuery();String resultType = mappedStatement.getResultType();Class<?> resultTypeClass = Class.forName(resultType);ArrayList<E> list = new ArrayList<>();// 5.遍历封装while (resultSet.next()){// 元数据信息中包含了字段名 字段的值ResultSetMetaData metaData = resultSet.getMetaData();Object obj = resultTypeClass.newInstance();for (int i = 1; i <= metaData.getColumnCount() ; i++) {// id usernameString columnName = metaData.getColumnName(i);Object value = resultSet.getObject(columnName);// 属性描述器PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultTypeClass);Method writeMethod = propertyDescriptor.getWriteMethod();writeMethod.invoke(obj,value);}list.add((E) obj);}return list;}/*** 1.将sql中#{}替换成? 2.将#{}里面的值保存* @param sql* @return*/private BoundSql getBoundSQL(String sql) {// 标记处理器:配合标记解析器完成标记的解析工作ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();// 标记解析器GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);String finalSql = genericTokenParser.parse(sql);// #{}里面的值的集合List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();BoundSql boundSql = new BoundSql(finalSql, parameterMappings);return boundSql;}
}
BoundSql
public class BoundSql {//解析过后的sql语句private String sqlText;//解析出来的参数private List<ParameterMapping> parameterMappingList = new ArrayList<ParameterMapping>();public BoundSql(String sqlText, List<ParameterMapping>parameterMappingList) {this.sqlText = sqlText;this.parameterMappingList = parameterMappingList;}public String getSqlText() {return sqlText;}public void setSqlText(String sqlText) {this.sqlText = sqlText;}public List<ParameterMapping> getParameterMappingList() {return parameterMappingList;}public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {this.parameterMappingList = parameterMappingList;}
}
5 自定义持久层框架优化
通过上述我们的自定义框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连 接,硬编码,手动封装返回结果集等问题,但是现在我们继续来分析刚刚完成的自定义框架代码,有没 有什么问题?
问题如下:
- dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方 法,关闭 sqlsession)
- dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码
解决:使用代理模式来创建接口的代理对象
@Testpublic void test2() throws Exception {InputStream resourceAsSteam = Resources.getResourceAsSteam(path: "sqlMapConfig.xml")SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsSteam);SqlSession sqlSession = build.openSession();User user = new User();user.setld(l);user.setUsername("tom");//代理对象UserMapper userMapper = sqlSession.getMappper(UserMapper.class);User userl = userMapper.selectOne(user);System・out.println(userl);}
在sqlSession中添加方法
public interface SqlSession {public <T> T getMappper(Class<?> mapperClass);
实现类
package com.oldlu.sqlSession;import com.oldlu.executor.Executor;
import com.oldlu.pojo.Configuration;
import com.oldlu.pojo.MappedStatement;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.List;public class DefaultSqlSession implements SqlSession {private Configuration configuration;private Executor executor;public DefaultSqlSession(Configuration configuration, Executor executor) {this.configuration = configuration;this.executor = executor;}@Overridepublic <E> List<E> selectList(String statementId, Object param) throws Exception {// 要传递什么参数呢?MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);List<E> list = executor.query(configuration,mappedStatement,param);return list;}@Overridepublic <T> T selectOne(String statementId, Object param) throws Exception {// 调用selectList方法List<Object> list = selectList(statementId, param);if(list.size() == 1){return (T) list.get(0);}else if(list.size() > 1){throw new RuntimeException("返回结果过多...");}return null;}/*** 生成代理对象* @param mapperClass* @param <T>* @return*/@Overridepublic <T> T getMapper(Class<?> mapperClass) {// 使用JDK动态代理生成代理对象Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {// 参数1:Object o:代理对象的引用,很少用// 参数2:Method method :当前被调用的方法对象// 参数3:Object[] objects:被调用的方法的参数@Overridepublic Object invoke(Object o, Method method, Object[] objects) throws Throwable {// 具体的逻辑:执行底层的JDBC// 思路:通过调用sqlSession的方法来完成执行// 问题1:如何获取statementId 根据method获取Class<?> declaringClass = method.getDeclaringClass();// 类全路径= namespace的值String className = declaringClass.getName();String methodName = method.getName();String statementId = className + "." + methodName;MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);// 问题2:该调用增删改查什么方法呢? 优化:sqlCommandTypeString sqlCommandType = mappedStatement.getSqlCommandType();switch (sqlCommandType){case "select"://查询操作//问题3:调selectOne还是调selectAll呢?Class<?> returnType = method.getReturnType();boolean assignableFrom = Collection.class.isAssignableFrom(returnType);if(assignableFrom){if(mappedStatement.getParameterType() !=null) {return selectList(statementId, objects[0]);}return selectList(statementId, null);}return selectOne(statementId,objects[0]);case "update":// 更新操作break;case "insert":// 更新操作break;case "delete":// 更新操作break;}return null;}});return (T) proxyInstance;}
}