公共SQL
-- 用户表和记录 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `birthday` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES (1, 'lucy', '123', '2019-12-12'); INSERT INTO `user` VALUES (2, 'tom', '123', '2019-12-12'); INSERT INTO `user` VALUES (3, 'jack', '123456', '2020-12-02'); SET FOREIGN_KEY_CHECKS = 1;JDBC 开发存在的问题分析
public class QuickStart { 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/lagou_mybatis?characterEncoding=utf-8", "root", "root"); // 从数据库连接中获取Statement对象 preparedStatement = connection.prepareStatement("select * from user where id = ?"); // 设置sql执行参数 preparedStatement.setLong(1, 2); // 执行sql,获取返回结果 resultSet = preparedStatement.executeQuery(); // 封装结果返回 while (resultSet.next()) { int id = resultSet.getInt("id"); String username = resultSet.getString("username"); String password = resultSet.getString("password"); System.out.println("id: " + id + ", username: " + username + ", password: " + password); } } catch (Exception e) { e.printStackTrace(); } finally { // 关闭连接 if (resultSet != null) { try { resultSet.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } if (preparedStatement != null) { try { preparedStatement.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } } }存在的问题
问题解决思路
数据库连接信息硬编码、连接资源频繁创建连接、释放资源?(问题一、问题五) 解决思路:数据库连接信息硬编码,使用XML配置文件。解决思路:使用连接池保存数据库连接,重复使用。 sql语句和参数硬编码?(问题二、问题三) 解决思路:使用配置文件映射sql语句,调用时候动态传递参数。 手动封装结果集解析?(问题四) 解决思路:使用反射、内省技术动态封装。自定义持久层框架设计思路
设计目的
自定义持久层框架的目的是为了解决以上JDBC编程存在的弊端。具体的弊端解决方法如上分析。
架构设计
客户端
对于客户端使用者来说,需要提供两个配置文件 数据库配置信息、SQL的定义。
规定:
数据库信息配置文件名为:SqlMapConfig.xml
SQL语句配置文件名为:实体名称Mapper.xml(如:UserMapper.xml、OrderMapper.xml)
持久层框架端
读取配置文件(SqlMapConfig.xml、Mapper.xml)解析配置文件(将配置文件信息解析成JavaBean对象)创建SqlSessionFactory工厂类创建SqlSession主要封装了CRUD等数据库操作基本方法持久层框架类图
包接口说明
config包
类名作用BoundSql封装解析后的sql和参数XMLConfigBuilderSqlMapConfig.xml配置文件解析工具类XMLMapperBuilderMapper.xml配置文件解析工具类io包
类名作用Resources读取SqlMapConfig.xml和Mapper.xml资源工具类pojo包
类名作用Configuration封装SqlMapConfig.xml配置参数MappedStatement封装Mapper.xml配置的sql参数sqlSession包
类名作用SqlSessionSqlSession接口定义数据库基本的CRUD方法DefaultSqlSessionSqlSession的实现类SqlSessionFactorySqlSessionFactory接口SqlSession工程类DefaultSqlSessionFactorySqlSessionFactory的实现类ExecutorExecutor接口sql的真正执行者,使用JDBC操作数据库SimpleExecutorExecutor的实现类SqlSessionFactoryBuilderSqlSessionFactory构建者类utils包:这个包的内容从MyBatis源码拷贝,主要是解析sql中的#{}类型参数
类名作用GenericTokenParserParameterMappingParameterMappingTokenHandlerTokenHandler框架层具体实现
1)依赖jar引入
<dependencies> <!--需要操作数据库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!--数据库连接池--> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <!--解析XML配置文件--> <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> </dependencies>2)SqlMapConfig.xml配置文件规范示例(resources目录下)
<configuration> <!--数据库连接池--> <dataSource> <properties name="driverClass" value="com.mysql.jdbc.Driver"></properties> <properties name="jdbcUrl" value="jdbc:mysql://localhost:3306/lagou_mybatis?characterEncoding=utf-8"></properties> <properties name="username" value="root"></properties> <properties name="password" value="root"></properties> </dataSource> <!--引入其他Mapper.xml配置信息--> <mappers> <mapper resource="UserMapper.xml"></mapper> </mappers> </configuration>3)Mapper.xml配置文件规范示例(resources目录下)
<mapper namespace="com.zyj.dao.UserDao"> <!--Sql语句--> <!-- 约定:namespace.id 作为SQL语句的唯一标志,statementId --> <select id="selectAll" resultType="com.zyj.pojo.User"> select * from user </select> <select id="selectOne" resultType="com.zyj.pojo.User" paramterType="com.zyj.pojo.User"> select * from user where id = #{id} </select> <insert id="insertUser" paramterType="com.zyj.pojo.User" resultType="java.lang.Integer"> insert into user values(#{id}, #{username}, #{password}, #{birthday}) </insert> <update id="updateUser" paramterType="com.zyj.pojo.User" resultType="java.lang.Integer"> update user set username = #{username}, password = #{password}, birthday = #{birthday} where id = #{id} </update> <delete id="deleteUser" paramterType="com.zyj.pojo.User" resultType="java.lang.Integer"> delete from user where id = #{id} </delete> </mapper>4)工具类(utils包)
这个包中的工具类都是从mybatis源码拷贝,用于解析 insert into user values(#{id}, #{username}, #{password}, #{birthday}) sql语句。解析后的sql语句如 insert into user values(?, ?, ?, ?) 并且会保存对于的参数名称。
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(); } } public class ParameterMapping { // #{id} 中的id字段 private String content; public ParameterMapping(String content) { this.content = content; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return "ParameterMapping{" + "content='" + content + '\'' + '}'; } } import java.util.ArrayList; import java.util.List; public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); // context是参数名称 #{id} #{username} @Override 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; } } public interface TokenHandler { String handleToken(String content); }5)资源读取设计(io包)
根据分析思路,自定义持久层框架需要用户提供2个配置文件SqlMapConfig.xml和Mapper.xml。所以封装一个统一的工具类,根据路径读取配置文件信息并返回输入流。
import java.io.InputStream; /** * 读取配置文件信息 * * Author: zhi chun qiu * date: 2020/5/24 9:50 */ public class Resources { /** * 读取配置文件,以流的形式存储在内存中 * * @param path * @return */ public static InputStream getResourcesAsStream(String path) { InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }6)读取到XML配置文件后,解析成JavaBean(pojo包)
根据面向对象的编程思想,我们需要封装实体。解析XML配置文件,主要的目的就是为了获取到里面的参数信息,提供给我们使用。以此实现动态的功能。
那么我们可以将SqlMapConfig.xml和Mapper.xml配置文件的内容分别封装为2个实体类,这样就可以实现在代码中传递了。
/** * Mapper.xml配置文件信息 * 这里一个查询、修改、删除、新增都分别是一个MappedStatement对象 * * Author: zhi chun qiu * date: 2020/5/24 9:55 */ public class MappedStatement { // id 如:selectAll private String id; // 参数返回值类型 如:com.zyj.pojo.User private String resultType; // 请求参数类型 如:com.zyj.pojo.User 这里参数封装为对象传递 private String paramterType; // 自定义的 sql语句 private String sql; public MappedStatement(String id, String resultType, String paramterType, String sql) { this.id = id; this.resultType = resultType; this.paramterType = paramterType; this.sql = sql; } // getter setter @Override public String toString() { return "MappedStatement{" + "id='" + id + '\'' + ", resultType='" + resultType + '\'' + ", paramterType='" + paramterType + '\'' + ", sql='" + sql + '\'' + '}'; } } import javax.sql.DataSource; import java.util.HashMap; import java.util.List; import java.util.Map; /** * SqlMapConfig.xml配置文件信息 * * Author: zhi chun qiu * date: 2020/5/24 9:53 */ public class Configuration { /* 数据库的配置信息直接封装为一个连接池对象,这样后续就可以直接使用了 */ private DataSource dataSource; /* Mapper.xml文件信息,key: namespace.id 如:com.zyj.dao.UserDao.selectAll value就是一条自定义的语句如下: <select id="selectAll" resultType="com.zyj.pojo.User"> select * from user </select> */ private Map<String, MappedStatement> mappedStatementMap = new HashMap<>(); // getter setter }7)解析SqlMapConfig.xml和Mapper.xml配置文件,封装对象(config包)
第六步我们已经定义好了SqlMapConfig.xml和Mapper.xml配置文件的实体类Configuration和MappedStatement。那么接下来我们就可以使用dom4j和xpath技术结合解析配置文件封装对象了。
思考: 就是关于SqlMapConfig.xml和Mapper.xml配置文件如何解析。
A:分开步骤解析SqlMapConfig.xml和Mapper.xml。
B:既然SqlMapConfig.xml内部的Mappers标签已经引入了Mapper.xml。那么可以在解析SqlMapConfig.xml的时候同步解析Mapper.xml。
答案我选择B方案,因为这样可以减少代码逻辑,而且Configuration内部的mappedStatementMap属性是对Mapper的封装。
import com.zyj.pojo.Configuration; import com.zyj.pojo.MappedStatement; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; /** * 解析Mapper.xml配置文件 * * Author: zhi chun qiu * date: 2020/5/24 11:54 */ public class XMLMapperBuilder { private Configuration configuration; // 构造函数传递Configuration是为了将解析的Mapper封装到内部 public XMLMapperBuilder(Configuration configuration) { this.configuration = configuration; } public void parseMapper(InputStream inputStream) throws DocumentException { Document document = new SAXReader().read(inputStream); Element rootElement = document.getRootElement(); String namespace = rootElement.attributeValue("namespace"); // 筛选 select insert update delete标签的语句 List<Element> elementList = document.selectNodes("//select | //insert | //update | //delete"); // 遍历解析所有的自定义sql配置 for (Element selectNode : elementList) { String id = selectNode.attributeValue("id"); String resultType = selectNode.attributeValue("resultType"); String paramterType = selectNode.attributeValue("paramterType"); String sql = selectNode.getTextTrim(); MappedStatement mappedStatement = new MappedStatement(id, resultType, paramterType, sql); configuration.getMappedStatementMap().put(namespace + "." + id, mappedStatement); } } } import com.mchange.v2.c3p0.ComboPooledDataSource; import com.zyj.io.Resources; import com.zyj.pojo.Configuration; import com.zyj.pojo.MappedStatement; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.beans.PropertyVetoException; import java.io.InputStream; import java.util.List; import java.util.Properties; /** * 解析SqlMapConfig.XML配置文件信息 * * Author: zhi chun qiu * date: 2020/5/24 10:45 */ public class XMLConfigBuilder { private Configuration configuration; public XMLConfigBuilder() { this.configuration = new Configuration(); } public Configuration parseConfiguration(InputStream inputStream) throws DocumentException, PropertyVetoException { // 使用dom4j技术去解析SqlMapConfig.xml配置文件 SAXReader saxReader = new SAXReader(); // 读取到SqlMapConfig.xml文档内容 Document document = saxReader.read(inputStream); // 读取到根节点 Element rootElement = document.getRootElement(); // 使用XPath获取Properties配置文件信息 List<Element> list = rootElement.selectNodes("//dataSource/properties"); Properties properties = new Properties(); for (Element element : list) { String name = element.attributeValue("name"); String value = element.attributeValue("value"); // 将解读到的name和value属性保存到Properties中 properties.setProperty(name, value); } // 使用C3P0技术创建数据库连接池 ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); comboPooledDataSource.setDriverClass(properties.getProperty("driverClass")); comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl")); comboPooledDataSource.setUser(properties.getProperty("username")); comboPooledDataSource.setPassword(properties.getProperty("password")); configuration.setDataSource(comboPooledDataSource); // 解析Mapper.xml配置文件 List<Element> mapperList = rootElement.selectNodes("//mappers/mapper"); XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration); for (Element element : mapperList) { String mapperResource = element.attributeValue("resource"); InputStream mapperInputStream = Resources.getResourcesAsStream(mapperResource); xmlMapperBuilder.parseMapper(mapperInputStream); } return configuration; } }8)核心逻辑(sqlSession包)
截止目前,我们已经将客户端提供的2个配置文件以及解析了。那么我们就获取到了数据库的配置信息,已经客户端自定义的sql语句信息。
这个步骤的所以流程都是围绕用户的配置展开的。因为如何操作数据库,操作那个数据库,操作数据库的语句是什么等,都是根据用户的配置文件处理。所以根据SqlMapConfig.xml解析得到的Configuration对象贯穿整个流程。也就是需要传递Configuration对象。
接下来的任务
执行sql语句(参数解析、设置参数)封装sql执行的结果并返回SqlSessionFactoryBuilder类 (sqlSession包)
这个类只有一个方法SqlSessionFactory build(InputStream inputStream),就是构建SqlSessionFactory。这是构建者模式。
import com.zyj.config.XMLConfigBuilder; import com.zyj.pojo.Configuration; import org.dom4j.DocumentException; import java.beans.PropertyVetoException; import java.io.InputStream; /** * SqlSessionFactory建造者类 * * Author: zhi chun qiu * date: 2020/5/24 10:00 */ public class SqlSessionFactoryBuilder { /** * 解析SqlMapConfig.xml配置文件信息,创建SqlSessionFactory类并返回 */ public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException { XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(); Configuration configuration = xmlConfigBuilder.parseConfiguration(inputStream); DefaultSqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration); return sqlSessionFactory; } }SqlSessionFactory接口及实现类(sqlSession包)
这是一个工厂类对象,主要作用就是创建SqlSession。我们提供一个默认实现DefaultSqlSessionFactory。
public interface SqlSessionFactory { SqlSession openSession(); } import com.zyj.pojo.Configuration; /** * Author: zhi chun qiu * date: 2020/5/24 12:01 */ public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } @Override public SqlSession openSession() { return new DefaultSqlSession(configuration); } }SqlSession接口及实现(sqlSession包)
上个步骤我们实现了SqlSession工厂类,接下来我们就实现SqlSession。SqlSession的作用就是实现数据库的操作。
主要的流程就是根据用户传递的id从Configuration的mappedStatementMap中查询到对应的sql并执行。对于数据库的操作总体就有新增、删除、修改、查询4个大类。所以只需要定义4个方法接口完成操作数据库功能。但是查询可以分为查询一个对象和查询多个对象。
import java.util.List; /** * 定义数据库CRUD操作 * * Author: zhi chun qiu * date: 2020/5/24 11:59 */ public interface SqlSession { int delete(String statementId, Object... params) throws Exception; int update(String statementId, Object... params) throws Exception; int insert(String statementId, Object... params) throws Exception; <E> List<E> selectAll(String statementId, Object... params) throws Exception; <T> T selectOne(String statementId, Object... params) throws Exception; } import com.zyj.pojo.Configuration; import com.zyj.pojo.MappedStatement; import java.lang.reflect.*; import java.util.List; /** * Author: zhi chun qiu * date: 2020/5/24 12:03 */ public class DefaultSqlSession implements SqlSession { private Configuration configuration; public DefaultSqlSession(Configuration configuration) { this.configuration = configuration; } @Override public int delete(String statementId, Object... params) throws Exception { Executor executor = new SimpleExecutor(); MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); int delete = executor.update(configuration, mappedStatement, params); return delete; } @Override public int update(String statementId, Object... params) throws Exception { Executor executor = new SimpleExecutor(); MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); int update = executor.update(configuration, mappedStatement, params); return update; } @Override public int insert(String statementId, Object... params) throws Exception { Executor executor = new SimpleExecutor(); MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); int insert = executor.update(configuration, mappedStatement, params); return insert; } @Override public <E> List<E> selectAll(String statementId, Object... params) throws Exception { Executor executor = new SimpleExecutor(); MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); List<Object> query = executor.query(configuration, mappedStatement, params); return (List<E>) query; } @Override public <T> T selectOne(String statementId, Object... params) throws Exception { List<Object> list = selectAll(statementId, params); if (list != null && list.size() == 1) { return (T) list.get(0); } if (list == null) { return null; } else { throw new RuntimeException("查询结果存在多个记录"); } } }Executor接口及实现(sqlSession包)
我们上面定义了sql的执行操作方法,但是具体的数据库操作(执行sql,封装返回结果)不应该放在SqlSession当中。所以定义一个接口专门处理这些操作。这就是Executor。
Executor最终其实是使用JDBC操作数据库,那么JDBC其实只有2个方法,一个是针对查询的,另一个是针对新增、删除、修改的。
import com.zyj.pojo.Configuration; import com.zyj.pojo.MappedStatement; import java.util.List; /** * Author: zhi chun qiu * date: 2020/5/24 12:14 */ public interface Executor { // 查询 <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object[] params) throws Exception; // 新增、删除、修改 int update(Configuration configuration, MappedStatement mappedStatement, Object[] params) throws Exception; } import com.zyj.config.BoundSql; import com.zyj.pojo.Configuration; import com.zyj.pojo.MappedStatement; import com.zyj.utils.GenericTokenParser; import com.zyj.utils.ParameterMapping; import com.zyj.utils.ParameterMappingTokenHandler; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.List; /** * 真正的连接数据库执行sql * Author: zhi chun qiu * date: 2020/5/24 12:14 */ public class SimpleExecutor implements Executor { @Override public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object[] params) throws Exception { List<E> result = new ArrayList(); // 1.获取连接 Connection connection = configuration.getDataSource().getConnection(); // 2.对SQL预处理 select * from user where id = #{id} => select * from user where id = ? BoundSql boundSql = getBoundSql(mappedStatement.getSql()); // 3.获取sql预处理对象 PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql()); // 4.设置参数 // 4.1获取参数值类型,得到Class String paramterType = mappedStatement.getParamterType(); Class<?> aClass = getClassType(paramterType); // 解析的参数字段名称集 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); String content = parameterMapping.getContent(); // 反射技术获取参数的类型 Field field = aClass.getDeclaredField(content); // 暴力访问 field.setAccessible(true); // 获取参数值 Object o = field.get(params[0]); // 给占位符赋值 preparedStatement.setObject(i + 1, o); } String resultType = mappedStatement.getResultType(); Class<?> resultTypeClass = Class.forName(resultType); // 5.执行sql ResultSet resultSet = preparedStatement.executeQuery(); // 6.解析封装结果集,内省 while (resultSet.next()) { // 创建结果参数封装对象 Object o = resultTypeClass.newInstance(); ResultSetMetaData metaData = resultSet.getMetaData(); for (int i = 1; i < metaData.getColumnCount(); i++) { String columnName = metaData.getColumnName(i); Object columnValue = resultSet.getObject(columnName); // 内省技术,创建属性描述器,为属性生成读写方法(内省技术属性必须有getter setter方法) PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass); // 获取对象的写方法 Method writeMethod = propertyDescriptor.getWriteMethod(); // 参数赋值 writeMethod.invoke(o, columnValue); } result.add((E) o); } return result; } // 新增、删除、修改都是通用这个方法 @Override public int update(Configuration configuration, MappedStatement mappedStatement, Object[] params) throws Exception { // 1.获取连接 Connection connection = configuration.getDataSource().getConnection(); // 2.对SQL预处理 select * from user where id = #{id} => select * from user where id = ? BoundSql boundSql = getBoundSql(mappedStatement.getSql()); // 3.获取sql预处理对象 PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql()); // 4.设置参数 // 4.1获取参数值类型,得到Class String paramterType = mappedStatement.getParamterType(); Class<?> aClass = getClassType(paramterType); // 解析的参数字段名称集 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); String content = parameterMapping.getContent(); // 反射技术获取参数的类型 Field field = aClass.getDeclaredField(content); // 暴力访问 field.setAccessible(true); // 获取参数值,从对象中获取字段的值,params[0]就是User对象 Object o = field.get(params[0]); // 给占位符赋值 preparedStatement.setObject(i + 1, o); } return preparedStatement.executeUpdate(); } /** * 获取参数返回值类型 * * @param paramterType * @return * @throws ClassNotFoundException */ private Class<?> getClassType(String paramterType) throws ClassNotFoundException { if (paramterType != null) { Class<?> aClass = Class.forName(paramterType); return aClass; } return null; } private BoundSql getBoundSql(String sql) { ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser tokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler); // 处理后的带着?号的SQL, String sqlText = tokenParser.parse(sql); // 获取参数列表 List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BoundSql boundSql = new BoundSql(sqlText, parameterMappings); return boundSql; } public static void main(String[] args) { BoundSql boundSql = new SimpleExecutor().getBoundSql("select * from user where id = #{id}"); System.out.println(boundSql); } }BoundSql(config包)
我们在最开始的时候引入了Mybatis的sql解析工具类,那么解析出来的sql语句封装在哪里返回给我们呢?这里我们自定义一个BoundSql类来承接返回的结果。
import com.zyj.utils.ParameterMapping; import java.util.ArrayList; import java.util.List; /** * Author: zhi chun qiu * date: 2020/5/24 12:39 */ public class BoundSql { // 解析过后的sql语句 private String sql; // 解析sql语句时候的参数 #{id} 则表示为id private List<ParameterMapping> parameterMappings = new ArrayList<>(); public BoundSql(String sql, List<ParameterMapping> parameterMappings) { this.sql = sql; this.parameterMappings = parameterMappings; } public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } public void setParameterMappings(List<ParameterMapping> parameterMappings) { this.parameterMappings = parameterMappings; } @Override public String toString() { return "BoundSql{" + "sql='" + sql + '\'' + ", parameterMappings=" + parameterMappings.toString() + '}'; } }总结
我们已经完成了自定义持久层框架,这个其实就是MyBatis框架的雏形。当然可以将这个框架引入到项目中去测试。截止目前已经完善了JDBC中的缺点,但是我们这个持久层框架还不够完善,所以我们需要学习MyBatis框架的知识。
挑战
上诉的自定义持久层框架是基于XML配置方式实现的,你可以挑战一下将其改造成基于注解或者Java API的方式实现。你是否可以基于上诉的自定义框架思想去剖析MyBatis源码呢?虽然很想给大家剖析,但是没有视频讲解很难用文字描述,如果想系统掌握可以加入拉勾教育Java高薪训练营好好学习。 知春秋 认证博客专家 博客专家 Java高级研发 不忘初心,方得始终。初心易得,始终难守。