长文干货|手写自定义持久层框架!
为何要手写自定义持久层框架?
1.JDBC 编码的弊端
会造成硬编码问题(无法灵活切换数据库驱动) 频繁创建和释放数据库连接造成系统资源浪费 影响系统性能 sql 语句存在硬编码,造成代码不易维护,实际应用中 sql 变化可能较大,变动 sql 需要改 Java 代码 使用 preparedStatement 向占有位符号传参数存在硬编码, 因 sql 语句的 where 条件不确定甚至没有where条件,修改 sql 还要修改代码 系统不易维护 对结果集解析也存在硬编码, sql变化导致解析代码变化2.更有助于读 mybatis 持久层框架源码
JDBC代码
public class jdbcConnection { private static Connection connection = null; private static PreparedStatement preparedStatement = null; private static ResultSet resultSet = null; public static void main(String[] args) { try { // 加载数据库驱动 Class.forName("com.mysql.jdbc.Driver"); // 通过驱动管理类获取数据库连接 connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/huodd", "root", "1234"); // 定义sql语句 ? 表示占位符 String sql = "select id,username from user where id = ?"; // 获取预处理对象 statement PreparedStatement preparedStatement = (PreparedStatement) connection.prepareStatement(sql); // 设置参数 第一个参数为 sql 语句中参数的序号(从1开始) 第二个参数为 设置的参数值 preparedStatement.setInt(1, 1); // 向数据库发出sql执行查询 查询出结果集 resultSet = preparedStatement.executeQuery(); // 遍历查询结果集 while (resultSet.next()) { int id = resultSet.getInt("id"); String username = resultSet.getString("username"); // 封装对象 User user = new User(); user.setId(id); user.setUsername(username); System.out.println(user); } } catch (Exception ex) { ex.printStackTrace(); } finally { try { // 释放资源 if (resultSet != null) { resultSet.close(); } if (preparedStatement != null) { preparedStatement.close(); } if (connection != null) { connection.close(); } } catch (Exception ex) { ex.printStackTrace(); } } } }解决问题的思路
数据库频繁创建连接、释放资源 -> 连接池 sql语句及参数硬编码 -> 配置文件 手动解析封装结果集 -> 反射、内省编码前思路整理
1.创建、读取配置文件
sqlMapConfig.xml 存放数据库配置信息 userMapper.xml :存放sql配置信息 根据配置文件的路径,加载配置文件成字节输入流,存储在内存中Resources#getResourceAsStream(String path) 创建两个JavaBean存储配置文件解析出来的内容 Configuration :核心配置类 ,存放 sqlMapConfig.xml解析出来的内容 MappedStatement:映射配置类:存放mapper.xml解析出来的内容2.解析配置文件(使用dom4j)
创建类:SqlSessionFactoryBuilder#build(InputStream in) -> 设计模式之构建者模式 使用dom4j解析配置文件,将解析出来的内容封装到容器对象(JavaBean)中3.创建 SqlSessionFactory 接口及实现类DefaultSqlSessionFactory
SqlSessionFactory对象,生产sqlSession会话对象 -> 设计模式之工厂模式4.创建 SqlSession接口及实现类DefaultSqlSession
定义对数据库的CRUD操作 selectList() selectOne() update() delete()5.创建Executor接口及实现类SimpleExecutor实现类
query(Configuration configuration, MappedStatement mapStatement, Object... orgs) 执行的就是JDBC代码6.测试代码
用到的设计模式
构建者模式 工厂模式 代理模式进入编码
1.创建、读取配置文件
sqlMapConfig.xml 存放数据库配置信息
<configuration> <dataSource> <!-- 引入数据库连接信息 --> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql:///huodd"></property> <property name="user" value="root"></property> <property name="password" value="1234"></property> </dataSource> <!-- 引入sql配置文件 --> <mapper resource="userMapper.xml"></mapper> </configuration>userMapper.xml 存放sql配置信息
<mapper namespace="user"> <!-- sql 的唯一标识: namespace.id 组成 => statementId 如 当前的为 user.selectList --> <select id="selectList" resultType="com.huodd.pojo.User" paramterType="com.huodd.pojo.User"> select * from user </select> <select id="selectOne" paramterType="com.huodd.pojo.User" resultType="com.huodd.pojo.User"> select * from user where id = #{id} and username =#{username} </select> </mapper>User.java
public class User { private Integer id; private String username; ... 省略getter setter 方法 ... 省略 toString 方法 }pom.xml 中引入依赖
<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>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</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>创建两个JavaBean对象 用于存储解析的配置文件的内容(Configuration.java、MappedStatement.java)
public class Configuration { // 数据源 private DataSource dataSource; //map集合 key:statementId value:MappedStatement private MapmappedStatementMap = new HashMap<>(); ... 省略getter setter 方法 }
public class MappedStatement { // id private String id; // sql 语句 private String sql; // 参数值类型 private Class<?> paramterType; // 返回值类型 private Class<?> resultType; ... 省略getter setter 方法 }创建Resources工具类 并编写静态方法getResourceAsSteam(String path)
public class Resources { /** * 根据配置文件的路径 将配置文件加载成字节输入流 存储在内存中 * @param path * @return InputStream */ public static InputStream getResourceAsStream(String path) { InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }2.解析配置文件(使用dom4j)
创建 SqlSessionFactoryBuilder类 并添加 build 方法
public class SqlSessionFactoryBuilder { public SqlSessionFactory build (InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { // 1. 使用 dom4j 解析配置文件 将解析出来的内容封装到Configuration中 XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder(); // configuration 是已经封装好了sql信息和数据库信息的对象 Configuration configuration = xmlConfigerBuilder.parseConfig(in); // 2. 创建 SqlSessionFactory 对象 工厂类 主要是生产sqlSession会话对象 DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration); return defaultSqlSessionFactory; } } public class XMLConfigerBuilder { private Configuration configuration; public XMLConfigerBuilder() { this.configuration = new Configuration(); } /** * 该方法 使用dom4j对配置文件进行解析 封装Configuration * @param in * @return */ public Configuration parseConfig (InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { Document document = new SAXReader().read(in); // <configuation> Element rootElement = document.getRootElement(); List<Element> propertyElements = rootElement.selectNodes("//property"); Properties properties = new Properties(); for (Element propertyElement : propertyElements) { properties.setProperty(propertyElement.attributeValue("name"), propertyElement.attributeValue("value")); } // 连接池 ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); comboPooledDataSource.setDriverClass(properties.getProperty("driverClass")); comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl")); comboPooledDataSource.setUser(properties.getProperty("user")); comboPooledDataSource.setPassword(properties.getProperty("password")); // 填充 configuration configuration.setDataSource(comboPooledDataSource); // mapper 部分 拿到路径 -> 字节输入流 -> dom4j进行解析 List<Element> mapperElements = rootElement.selectNodes("//mapper"); XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration); for (Element mapperElement : mapperElements) { String mapperPath = mapperElement.attributeValue("resource"); InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath); xmlMapperBuilder.parse(resourceAsStream); } return configuration; } 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); // <mapper> 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"); // 输入参数 class Class<?> paramterTypeClass = getClassType(paramterType); // 返回结果 class Class<?> resultTypeClass = getClassType(resultType); // sql 语句 String sqlStr = element.getTextTrim(); // 封装 mappedStatement MappedStatement mappedStatement = new MappedStatement(); mappedStatement.setId(id); mappedStatement.setParamterType(paramterTypeClass); mappedStatement.setResultType(resultTypeClass); mappedStatement.setSql(sqlStr); // statementId String key = namespace + "." + id; // 填充 configuration configuration.getMappedStatementMap().put(key, mappedStatement); } } private Class<?> getClassType(String paramterType) throws ClassNotFoundException { Class<?> aClass = Class.forName(paramterType); return aClass; } }3.创建 SqlSessionFactory 接口及实现类DefaultSqlSessionFactory
public interface SqlSessionFactory { SqlSession openSession(); }
public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } @Override public SqlSession openSession() { return new DefaultSqlSession(configuration); } }4. 创建 SqlSession接口及实现类DefaultSqlSession
public interface SqlSession { <E> List<E> selectList(String statementId, Object... param) throws Exception; <T> T selectOne(String statementId, Object... params) throws Exception; void close() throws SQLException; } public class DefaultSqlSession implements SqlSession { private Configuration configuration; // 处理器对象 private Executor simpleExcutor = new SimpleExecutor(); public DefaultSqlSession(Configuration configuration) { this.configuration = configuration; } @Override public <E> List<E> selectList(String statementId, Object... param) throws Exception { // 完成对 simpleExcutor里的query方法的调用 MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); List<E> list = simpleExcutor.query(configuration, mappedStatement, param); return list; } @Override public <T> T selectOne(String statementId, Object... params) throws Exception { List<Object> objects = selectList(statementId, params); if (objects.size() == 1) { return (T) objects.get(0); } else { throw new RuntimeException("返回结果过多"); } } @Override public void close() throws SQLException { simpleExcutor.close(); } }5.创建Executor接口及实现类SimpleExecutor实现类
public interface Executor { <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... param) throws Exception; void close() throws SQLException; } public class SimpleExecutor implements Executor { private Connection connection = null; @Override public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... param) throws Exception { // 注册驱动 获取连接 connection = configuration.getDataSource().getConnection(); // select * from user where id = #{id} and username = #{username} String sql = mappedStatement.getSql(); // 对 sql 进行处理 BoundSql boundSql = getBoundSql(sql); // select * from where id = ? and username = ? String finalSql = boundSql.getSqlText(); // 获取传入参数类对象 Class<?> paramterTypeClass = mappedStatement.getParamterType(); // 获取预处理 preparedStatement 对象 PreparedStatement preparedStatement = connection.prepareStatement(finalSql); // 设置参数 List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); for (int i = 0; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String name = parameterMapping.getContent(); // 反射 获取某一个属性对象 Field declaredField = paramterTypeClass.getDeclaredField(name); // 设置暴力访问 declaredField.setAccessible(true); // 参数传递的值 Object o = declaredField.get(param[0]); // 给占位符赋值 preparedStatement.setObject(i + 1, o); } // 执行sql ResultSet resultSet = preparedStatement.executeQuery(); // 封装返回结果集 // 获取返回参数类对象 Class<?> resultTypeClass = mappedStatement.getResultType(); ArrayList<E> results = new ArrayList<>(); while (resultSet.next()) { // 取出 resultSet的元数据 ResultSetMetaData metaData = resultSet.getMetaData(); E o = (E) resultTypeClass.newInstance(); int columnCount = metaData.getColumnCount(); for (int i = 1; i <= columnCount; i++) { // 属性名/字段名 String columnName = metaData.getColumnName(i); // 属性值/字段值 Object value = resultSet.getObject(columnName); // 使用反射或者内省 根据数据库表和实体的对应关系 完成封装 // 创建属性描述器 为属性生成读写方法 PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass); // 获取写方法 Method writeMethod = propertyDescriptor.getWriteMethod(); // 向类中写入值 writeMethod.invoke(o, value); } results.add(o); } return results; } /** * 转换sql语句 完成对 #{} 的解析工作 * 1. 将 #{} 使用?进行代替 * 2. 解析出 #{} 里面的值进行存储 * * @param sql 转换前的原sql * @return */ private BoundSql getBoundSql(String sql) { // 标记处理类: 主要是配合通用解析器 GenericTokenParser 类完成对配置文件等的解析工作 其中TokenHandler 主要完成处理 ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); // GenericTokenParser: 通用的标记解析器 完成了代码片段中的占位符的解析 然后根据给定的标记处理器( TokenHandler ) 来进行表达式的处理 // 三个参数: 分别为 openToken (开始标记)、 closeToken (结束标记)、 handler (标记处理器) GenericTokenParser genericTokenParse = new GenericTokenParser("#{", "}", parameterMappingTokenHandler); // 解析出来的sql String parseSql = genericTokenParse.parse(sql); // #{} 里面解析出来的参数名称 List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BoundSql boundSql = new BoundSql(parseSql, parameterMappings); return boundSql; } @Override public void close() throws SQLException { connection.close(); } } public class BoundSql { // 解析过后的 sql 语句 private String sqlText; // 解析出来的参数 private List<ParameterMapping> parameterMappingList = new ArrayList<>(); // 有参构造方便创建时赋值 public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) { this.sqlText = sqlText; this.parameterMappingList = parameterMappingList; } ... 省略getter setter 方法 }6.测试代码
public class IPersistenceTest { @Test public void test () throws Exception { InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sessionFactory.openSession(); User user = new User(); user.setId(1); user.setUsername("bd2star"); User res = sqlSession.selectOne("user.selectOne", user); System.out.println(res); // 关闭资源 sqlSession.close() } }运行结果如下
User{id=1, username=bd2star}测试通过 调整代码
创建 接口 Dao及实现类
public interface IUserDao { // 查询所有用户 public List<User> selectList() throws Exception; // 根据条件进行用户查询 public User selectOne(User user) throws Exception; } public class UserDaoImpl implements IUserDao { @Override public List<User> findAll() throws Exception { InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sessionFactory.openSession(); List<User> res = sqlSession.selectList("user.selectList"); sqlSession.close(); return res; } @Override public User findByCondition(User user) throws Exception { InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sessionFactory.openSession(); User res = sqlSession.selectOne("user.selectOne", user); sqlSession.close(); return res; } }调整测试方法
public class IPersistenceTest { @Test public void test () throws Exception { User user = new User(); user.setId(1); user.setUsername("bd2star"); IUserDao userDao = new UserDaoImpl(); User res = userDao.findByCondition(user); System.out.println(res); } }运行结果如下
User{id=1, username=bd2star}测试通过
7.补充
huodd.sql
--新建数据库 CREATE DATABASE huodd; --使用数据库 use huodd; --创建表 CREATE TABLE `user` ( `id` int(11) NOT NULL, `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact; -- 插入测试数据 INSERT INTO `user` VALUES (1, bd2star); INSERT INTO `user` VALUES (2, bd3star);用到的工具类
GenericTokenParser.java
public class GenericTokenParser { private final String openToken; //开始标记 private final String closeToken; //结束标记 private final TokenHandler handler; //标记处理器 public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } /** * 解析${}和#{} * @param text * @return * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。 * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现 */ public String parse(String text) { // 验证参数问题,如果是null,就返回空字符串。 if (text == null || text.isEmpty()) { return ""; } // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。 int start = text.indexOf(openToken, 0); if (start == -1) { return text; } // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder, // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码 char[] src = text.toCharArray(); int offset = 0; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null; while (start > -1) { // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理 if (start > 0 && src[start - 1] == \\) { builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); } else { //重置expression变量,避免空指针或者老数据干扰。 if (expression == null) { expression = new StringBuilder(); } else { expression.setLength(0); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1) {////存在结束标记时 if (end > offset && src[end - 1] == \\) {//如果结束标记前面有转义字符时 // this close token is escaped. remove the backslash and continue. expression.append(src, offset, end - offset - 1).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else {//不存在转义字符,即需要作为参数进行处理 expression.append(src, offset, end - offset); offset = end + closeToken.length(); break; } } if (end == -1) { // close token was not found. builder.append(src, start, src.length - start); offset = src.length; } else { //首先根据参数的key(即expression)进行参数处理,返回?作为占位符 builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } }ParameterMapping.java
public class ParameterMapping { private String content; public ParameterMapping(String content) { this.content = content; } ... 省略getter setter 方法 }ParameterMappingTokenHandler.java
public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); // context是参数名称 #{id} #{username} public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; } private ParameterMapping buildParameterMapping(String content) { ParameterMapping parameterMapping = new ParameterMapping(content); return parameterMapping; } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } public void setParameterMappings(List<ParameterMapping> parameterMappings) { this.parameterMappings = parameterMappings; } }TokenHandler.java
public interface TokenHandler { String handleToken(String content); }继续优化自定义框架
通过上述自定义框架,我们解决了JDBC操作数据库带来的一些问题,例如频繁创建释放数据库连接,硬编码,手动封装返回结果等问题
但从测试类可以发现新的问题
dao 的实现类存在重复代码 整个操作的过程模板重复 (如创建 SqlSession 调用 SqlSession方法 关闭 SqlSession) dao 的实现类中存在硬编码,如调用 sqlSession 方法时 参数 statementId 的硬编码解决方案
通过代码模式来创建接口的代理对象1.添加getMapper方法
删除dao的实现类 UserDaoImpl.java 我们通过代码来实现原来由实现类执行的逻辑
在 SqlSession 中添加 getMapper 方法
public interface SqlSession { <T> T getMapper(Class<?> mapperClass); }2. 实现类实现方法
DefaultSqlSession 类中实现 getMapper 方法
@Override public <T> T getMapper(Class<?> mapperClass) { // 使用 JDK 动态代理 来为 Dao 接口生成代理对象 并返回 Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { /** * * @param proxy 当前代理对象的引用 * @param method 当前被调用方法的引用 * @param args 传递的参数 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 底层都还是去执行 JDBC 代码 -> 根据不同情况 调用 selectList() 或者 selectOne() // 准备参数 1. statmentId sql语句的唯一标识 namespace.id = 接口全限定名.方法名 // 2. params -> args // 拿到的是方法名 findAll String methodName = method.getName(); // 拿到该类的全限定类名 com.huodd.dao.IUserDao String className = method.getDeclaringClass().getName(); String statmentId = className + "." + methodName; // 获取被调用方法的返回值类型 Type genericReturnType = method.getGenericReturnType(); // 判断是否进行了 泛型类型参数化 if (genericReturnType instanceof ParameterizedType) { List<Object> list = selectList(statmentId, args); return list; } return selectOne(statmentId, args); } }); return (T) proxyInstance; }3.调整mapper.xml配置文件
这里要注意两点
namespace 与 dao 接口的全限定类名保持一致
id 与 dao 接口中定义的方法名保持一致
<mapper namespace="com.huodd.dao.IUserDao"> <!-- sql 的唯一标识: namespace.id 组成 => statementId 如 当前的为 Userselect.List --> <select id="findAll" resultType="com.huodd.pojo.User" paramterType="com.huodd.pojo.User"> select * from user </select> <select id="findByCondition" paramterType="com.huodd.pojo.User" resultType="com.huodd.pojo.User"> select * from user where id = #{id} and username =#{username} </select> </mapper>4. 进入测试
public class IPersistenceTest { @Test public void test () throws Exception { InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sessionFactory.openSession(); User user = new User(); user.setId(1); user.setUsername("bd2star"); // 此时返回的 userDao 就是代理对象 所以它的类型就是 Proxy IUserDao userDao = sqlSession.getMapper(IUserDao.class); // userDao 是代理对象 调用了接口中的 findAll() 代理对象调用接口中任意方法 都会执行 invoke() List<User> users = userDao.findAll(); System.out.println(users); User res = userDao.findByCondition(user); System.out.println(res); } }运行结果如下
[User{id=1, username=bd2star}, User{id=2, username=bd3star}] User{id=1, username=bd2star}目录结构调整
将代码分为两个模块
提供端(自定义持久层框架-本质就是对JDBC代码的封装) 使用端 (引用持久层框架的jar ) 包含数据库配置信息 包含sql配置信息 包含sql语句 参数类型 返回值类型项目目录结构最终为
提供端

使用端

源码地址 https://gitee.com/bx2star/mybatis-learning.git
扫一扫,关注我们