自定义持久层框架
- 环境准备
- 重温JDBC
- 进入正题: 自定义持久层框架
- 敲代码前的思考
- 创建persistent模块
- 创建persistentConfig.xml文件
- 创建UserMapper.xml文件
- 创建Configuration主配置类
- 创建MapperStatement实体
- 创建Resource类读取配置文件流
- 创建工具类ConvertClass
- 创建SqlSessionFacotryBuilder构建者
- 创建XMLConfigBuilder类
- 创建XMLMapperBuilder
- 创建SqlSessionFactory工厂类
- 创建DefaultSqlSessionFactory工厂实现类
- 创建SqlSession接口
- 创建DefaultSqlSession
- 创建Executor执行器接口
- 创建SimpleExecutor
- 测试框架
- 解析sql
- 使用动态代理实现查询
环境准备
数据库准备
CREATE DATABASE IF NOT EXISTS `persistent`;
USE `persistent`;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`age` int NULL DEFAULT NULL,
`sex` char(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '张三', 22, '男', '广东省广州市');
INSERT INTO `user` VALUES (2, '李四', 23, '女', '湖北省武汉市');
INSERT INTO `user` VALUES (3, '王五', 30, '男', '广州市深圳市');
SET FOREIGN_KEY_CHECKS = 1;
创建实体
package com.xs.persistent.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author xs
* @Description User实体类
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
/**
* 用户id
*/
private Integer id;
/**
* 用户姓名
*/
private String name;
/**
* 用户年龄
*/
private Integer age;
/**
* 用户性别
*/
private Character sex;
/**
* 用户住址
*/
private String address;
}
导入pom依赖
最好创建父工程:mybatis-study 导入到父工程pom中 其他模块需要导入对应的依赖就好
<?xml version="1.0" encoding="UTF-8"?>
<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>org.xs</groupId>
<artifactId>mybatis-study</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>jdbc-study</module>
</modules>
<packaging>pom</packaging>
<description>Mybatis study project!</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<mysql.version>8.0.30</mysql.version>
<c3p0.version>0.9.1.2</c3p0.version>
<junit.version>4.13.2</junit.version>
<dom4j.version>2.1.3</dom4j.version>
<jaxen.version>2.0.0</jaxen.version>
<slf4j.version>2.0.6</slf4j.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.18.24</lombok.version>
</properties>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>${c3p0.version}</version>
</dependency>
<!-- Junit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Dom4j -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>${dom4j.version}</version>
</dependency>
<!-- jaxen -->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>${jaxen.version}</version>
</dependency>
<!-- slf4j日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
<type>pom</type>
<scope>test</scope>
</dependency>
<!-- log4j日志实现 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
重温JDBC
JDBC是我们学习java操作数据库的必回技能, 虽然项目很少使用,但是我们学习的持久层框架底层都是通过JDBC来实现的, 只用掌握JDBC后才能知道框架演变过来时解决了哪些问题方便我们程序员使用,那我们就开始吧!
创建JDBC子模块
创建子模块:jdbc-study 并导入pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<artifactId>mybatis-study</artifactId>
<groupId>org.xs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jdbc-study</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
手敲JDBC
jdbc案例 分析jdbc并思考怎么封装持久化框架
package com.xs.jdbc;
import com.xs.jdbc.entity.User;
import java.sql.*;
/**
* @Author xs
* @Description jdbc案例 分析jdbc并思考怎么封装持久化框架
* 总结: 1.连接数据库的时候参数写死 修改需要重新编译 后期不好维护 解决办法: 提取到配置文件 运行时读取配置文件获取数据源
* 2. 每一次查询都要获取和关闭资源 数据库多次网络IO 降低性能 解决办法: 使用数据库连接池
* 3. 获取结果集后需要手动封装结果集 如果查询很多每次自己封装很费时费力 效率低 解决办法: 通过反射技术拿到要封装的对象class 自动给对象设置属性
**/
public class JdbcDemo {
public static void main(String[] args) {
Connection connection;
PreparedStatement preparedStatement;
ResultSet resultSet;
try {
//加载驱动 META-INF\services\java.sql.Driver文件下已经指定默认驱动 8.0.0及以后版本可以不写
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/persistent?useSSL=false&serverTimezone=UTC";
String user ="root";
String password = "123456";
//获取连接
connection = DriverManager.getConnection(url, user, password);
//获取预处理对象
preparedStatement = connection.prepareStatement("select u.id,u.name,u.age,u.sex,u.address from user u where name = ?");
//设置参数
preparedStatement.setString(1,"xs");
//执行sql
resultSet = preparedStatement.executeQuery();
//创建要封装的实体
User u = null;
//遍历结果集
while (resultSet.next()){
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
String sex = resultSet.getString("sex");
String address = resultSet.getString("address");
u = new User(id,name,age,sex.charAt(0),address);
}
//输出结果
System.out.println(u);
//关闭资源
close(connection,preparedStatement,resultSet);
} catch (Exception e) {
e.printStackTrace();
if(e instanceof ClassNotFoundException){
throw new RuntimeException("获取数据库驱动失败!");
}
}
}
private static void close(Connection connection,PreparedStatement statement,ResultSet resultSet){
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("关闭结果集对象异常!");
}
}
if(statement != null){
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("关闭预处理对象异常!");
}
}
if(connection != null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("关闭数据库连接异常!");
}
}
}
}
总结:
1.连接数据库的时候参数写死 修改需要重新编译 后期不好维护;
解决办法: 提取到配置文件 运行时读取配置文件获取数据源
2. 每一次查询都要获取和关闭资源 数据库多次网络IO 降低性能
解决办法: 使用数据库连接池
3. 获取结果集后需要手动封装结果集 如果查询很多每次自己封装很费时费力 效率低
解决办法: 通过反射技术拿到要封装的对象class 自动给对象设置属性
进入正题: 自定义持久层框架
兄弟们!开始我们自定义框架吧!
敲代码前的思考
我们先整理梳理一下思路 这样写起来就不会毫无头绪,小伙伴们工作的时候最好先理一下思路再开始写会比较顺哦! (推荐使用思维导图这样更加清晰明了)
创建persistent模块
引入模块所需依赖
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<artifactId>mybatis-study</artifactId>
<groupId>org.xs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>persistent-study</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
</dependency>
<!-- Junit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- Dom4j -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
</dependency>
<!-- jaxen -->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
</dependency>
<!-- slf4j日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
<!-- log4j日志实现 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<!-- slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
User对象记得从JDBC模块copy过来
创建persistentConfig.xml文件
注意mapper会有多个,后边会通过namespace定位到是那一个mapper 这也是面试常问的
<configuration>
<!--数据库连接属性-->
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1/persistent?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<!--映射mapper文件集合-->
<mappers>
<mapper resource="user/UserMapper.xml"/>
</mappers>
</configuration>
创建UserMapper.xml文件
这里可以通过namespace.id知道具体是哪个sql语句,后边看代码会有更深刻的理解namespace的作用
<mapper namespace="User">
<select id="selectOne" resultType="com.xs.persistent.entity.User" paramType="com.xs.persistent.entity.User">
select u.id,u.name,u.age,u.sex,u.address from user u where name = #{name}
</select>
<select id="select" resultType="com.xs.persistent.entity.User">
select u.id,u.name,u.age,u.sex,u.address from user u
</select>
</mapper>
目录结构
创建Configuration主配置类
package com.xs.persistent.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @Author xs
* @Description 主配置类
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Configuration {
/**
* 数据源 封装连接数据
*/
private DataSource dataSource;
/**
* 封装多个mapper 装在map里 通过namespace.id获取到对应的执行语句等 key: namespace.id value:MapperStatement
*/
private Map<String,MapperStatement> mapperStatements = new HashMap<>();
}
}
创建MapperStatement实体
封装mapper.xml中一个执行标签的数据 比如select insert update delete
package com.xs.persistent.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author xs
* @Description 封装mapper.xml中一个执行标签的数据 比如select insert update delete
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MapperStatement {
/**
* 命名空间 后期通过动态代理就必需是Mapper接口的全限定类名
*/
private String namespace;
/**
* id 对应方法名 后期通过动态代理就必需和方法名相同这里先不用纠结
*/
private String id;
/**
* 参数类型
*/
private Class<?> paramsType;
/**
* 结果类型
*/
private Class<?> resultType;
/**
* sql语句 这里是带#{param}的
*/
private String sql;
}
创建Resource类读取配置文件流
package com.xs.persistent.utils;
import java.io.InputStream;
/**
* @Author xs
* @Description 读取配置文件资源类
**/
public class Resource {
public static InputStream getResourceAsStream(String path){
return Resource.class.getClassLoader().getResourceAsStream(path);
}
}
创建工具类ConvertClass
package com.xs.persistent.utils;
/**
* @Author xs
* @Description 通过全限定类名获取class对象
**/
public class ConvertClass {
public static Class<?> getClass(String className){
if (className == null){
return null;
}
Class<?> aClass = null;
try {
aClass = Class.forName(className);
return aClass;
} catch (Exception e) {
if(e instanceof ClassNotFoundException){
throw new RuntimeException("获取class对象失败!");
}
e.printStackTrace();
}
return aClass;
}
}
创建SqlSessionFacotryBuilder构建者
package com.xs.persistent.session;
import com.xs.persistent.builder.XMLConfigBuilder;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.session.defaults.DefaultSqlSessionFactory;
import java.io.InputStream;
/**
* @Author xs
* @Description SqlSessionFactory构建者类 这里用到了构建者模式 构建的是Configuration类
**/
public class SqlSessionFactoryBuilder {
private Configuration configuration;
/**
* 初始化的时候创建Configuration对象
*/
public SqlSessionFactoryBuilder(){
this.configuration = new Configuration();
}
/**
* 构建SqlSessionFactory对象的方法
*/
public SqlSessionFactory build(InputStream inputStream){
XMLConfigBuilder configBuilder = new XMLConfigBuilder(configuration);
configuration = configBuilder.parse(inputStream);
return new DefaultSqlSessionFactory(configuration);
}
}
创建XMLConfigBuilder类
解析persistent主配置文件并把数据封装到Configuration对象中
package com.xs.persistent.builder;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.utils.Resource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
/**
* @Author xs
* @Description
**/
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder(Configuration configuration){
this.configuration = configuration;
}
/**
* 解析配置文件 封装数据到Configuration对象中
* @param resourceAsStream
* @return configuration
*/
public Configuration parse(InputStream resourceAsStream){
//使用dom4j的SAXReader读取文件流
try {
//获取配置文件文档对象
Document document = new SAXReader().read(resourceAsStream);
//通过xpath表示式获取property节点
List<Node> nodes = document.selectNodes("//property");
//创建properties对象
Properties properties = new Properties();
//遍历节点
for (Node node : nodes) {
Element element = (Element)node;
//根据属性名获取属性值
String name = element.attributeValue("name");
String value = element.attributeValue("value");
//封装数据到properties中
properties.setProperty(name,value);
}
//创c3p0数据源
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass(properties.getProperty("driverClass"));
dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
dataSource.setUser(properties.getProperty("username"));
dataSource.setPassword(properties.getProperty("password"));
//设置数据源
configuration.setDataSource(dataSource);
//获取mapper文件地址 会有多个
List<Node> mapperNodes = document.selectNodes("//mapper");
//遍历mapper.xml地址
for (Node mapperNode : mapperNodes) {
Element element = (Element) mapperNode;
String mapperPath = element.attributeValue("resource");
XMLMapperBuilder mapperBuilder = new XMLMapperBuilder(configuration);
InputStream in = Resource.getResourceAsStream(mapperPath);
configuration = mapperBuilder.parse(in);
}
} catch (Exception e) {
e.printStackTrace();
if(e instanceof DocumentException){
throw new RuntimeException("读取文件流失败!");
}
}
return configuration;
}
}
创建XMLMapperBuilder
解析XXXMpper.xml文件中的增删改查标签并封装到MapperStatement对象中
这里只实现了查询 增删改少了结果集封装其实很简单,有兴趣的小伙伴可以去试试!
package com.xs.persistent.builder;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import com.xs.persistent.utils.ConvertClass;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;
/**
* @Author xs
* @Description 构建 XXXMapper.xml文件 目前只解析select标签 有兴趣可以自己写一下其他标签的解析 应为少了封装结果集会简单很多
**/
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration){
this.configuration = configuration;
}
public Configuration parse(InputStream inputStream){
try {
Document document = new SAXReader().read(inputStream);
//获取根节点
Element rootElement = document.getRootElement();
//获取命名空间
String namespace = rootElement.attributeValue("namespace");
//获取所有select节点
List<Node> nodes = rootElement.selectNodes("//select");
for (Node node : nodes) {
MapperStatement mapperStatement = new MapperStatement();
Element element = (Element) node;
//获取类型
String paramType = element.attributeValue("paramType");
//获取结果集类型
String resultType = element.attributeValue("resultType");
//获取id
String id = element.attributeValue("id");
//获取sql 这时候的sql是带#{}的
String textTrim = element.getTextTrim();
//设置id
mapperStatement.setId(id);
//设置命名空间
mapperStatement.setNamespace(namespace);
//设置参数类型
mapperStatement.setParamsType(ConvertClass.getClass(paramType));
//设置结果集
mapperStatement.setResultType(ConvertClass.getClass(resultType));
//设置sql
mapperStatement.setSql(textTrim);
//创建key 唯一定位到mapper中的增删改查标签 这里是定位到具体哪一个select
String key = namespace+"."+id;
//设置map map中存了MapperStatement key是namespace.id
configuration.getMapperStatements().put(key,mapperStatement);
}
} catch (Exception e) {
if(e instanceof DocumentException){
throw new RuntimeException("读取XXXMapper.xml文件失败");
}
e.printStackTrace();
}
return configuration;
}
}
创建SqlSessionFactory工厂类
package com.xs.persistent.session;
/**
* @Author xs
* @Description sqlSession工厂接口
**/
public interface SqlSessionFactory {
/**
* 获取sqlSession
*/
SqlSession openSession();
}
创建DefaultSqlSessionFactory工厂实现类
package com.xs.persistent.session.defaults;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.session.SqlSession;
import com.xs.persistent.session.SqlSessionFactory;
/**
* @Author xs
* @Description SqlSession工厂接口实现类
**/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration){
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
创建SqlSession接口
封装了增删改查方法 这里我们先实现查询功能 后边的更新 添加 删除就很简单了
package com.xs.persistent.session;
import java.util.List;
/**
* @Author xs
* @Description sqlSession 封装了增删改查方法 这里我们先实现查询功能 后边的更新 添加 删除就很简单了
**/
public interface SqlSession {
/**
* 查询所有的方法
*/
<T> List<T> select(String statement, Object... param);
/**
* 查询一个的方法
*/
<T> T selectOne(String statement,Object... param);
}
创建DefaultSqlSession
package com.xs.persistent.session.defaults;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import com.xs.persistent.executor.Executor;
import com.xs.persistent.executor.impl.SimpleExecutor;
import com.xs.persistent.session.SqlSession;
import java.util.List;
/**
* @Author xs
* @Description sqlSession接口实现类
**/
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration){
this.configuration = configuration;
this.executor = new SimpleExecutor();
}
@Override
public <T> List<T> select(String statement, Object... param) {
//获取MapperStatement
MapperStatement mapperStatement = configuration.getMapperStatements().get(statement);
List<T> query = executor.query(configuration, mapperStatement, param);
if (query.size() == 0){
throw new RuntimeException("未查到匹配的数据!");
}
return query;
}
@Override
public <T> T selectOne(String statement,Object... param) {
List<Object> select = select(statement, param);
if(select.size() > 1){
throw new RuntimeException("查询出现多个值!");
}
return (T)select.get(0);
}
}
创建Executor执行器接口
package com.xs.persistent.executor;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import java.util.List;
/**
* @Author xs
* @Description 执行器接口
**/
public interface Executor {
<E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... param);
}
}
创建SimpleExecutor
在这里执行了JDBC 并拿到Configuration里的数据再通过反射设置参数,封装结果集 理解了这里 整个持久层框架的就很好理解了
其中解析sql的参考解析sql 我也是参考mybatis源码做了精简
代码里try catch有点多是为了方便输出错误 也可以直接方法抛出去 代码看着就不会看着那么乱了
package com.xs.persistent.executor.impl;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import com.xs.persistent.executor.Executor;
import com.xs.persistent.mapping.BoundSql;
import com.xs.persistent.mapping.ParameterMapping;
import com.xs.persistent.parsing.GenericTokenParser;
import com.xs.persistent.parsing.ParameterMappingTokenHandler;
import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* @Author xs
* @Description 执行器接口实现类 这里通过执行JDBC获取结果并封装 是最关键的地方
**/
public class SimpleExecutor implements Executor {
@Override
public <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... param) {
//获取数据库连接
Connection connection = getConnection(configuration.getDataSource());
//获取BoundSql 里边封装了解析好的sql及参数名
BoundSql boundSql = getBoundSql(mapperStatement.getSql());
//获取预处理对象
PreparedStatement statement = null;
try {
statement = connection.prepareStatement(boundSql.getSql());
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("创建预处理对象失败!");
}
//获取ParameterMapping集合
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//创建结果集合
List<E> lists = new ArrayList<>();
//遍历
for (int i = 0; i < parameterMappings.size(); i++) {
//获取参数名
String parameterName = parameterMappings.get(i).getProperty();
//获取mapper里参数class对象
Class<?> paramsType = mapperStatement.getParamsType();
//获取参数名的字段对象
Field field = null;
try {
field = paramsType.getDeclaredField(parameterName);
//开启暴力反射
field.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
throw new RuntimeException("根据字段名获取字段对象!");
}
//获取方法传进来的参数值
Object o = null;
try {
o = field.get(param[0]);
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException("获取实体参数值异常!");
}
//设置参数
try {
statement.setObject(i+1,o);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("设置参数异常!");
}
}
try {
//执行sql 获取结果集
ResultSet resultSet = statement.executeQuery();
//遍历结果集
while (resultSet.next()){
//获取元数据
ResultSetMetaData metaData = resultSet.getMetaData();
//获取字段名集合统计数
int columnCount = metaData.getColumnCount();
//获取结果class
Class<?> resultType = mapperStatement.getResultType();
//创建结果集类型对象
Object resultObject = resultType.newInstance();
//遍历
for (int i = 1; i <= columnCount; i++) {
//获取字段名
String columnName = metaData.getColumnName(i);
//获取字段值
Object object = resultSet.getObject(columnName);
//获取字段对象
Field field = resultType.getDeclaredField(columnName);
//开启暴力反射
field.setAccessible(true);
//获取字段名称
String typeName = field.getType().getName();
if(typeName.equals("java.lang.Character")){
//给char类型的字段赋值 这里获取到的为String 所以要转成char
field.set(resultObject,object.toString().charAt(0));
}else{
//给对象的字段设置值
field.set(resultObject,object);
}
}
//把封装结果放到集合里
lists.add((E)resultObject);
}
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("获取结果集异常!");
} catch (InstantiationException e) {
e.printStackTrace();
throw new RuntimeException("创建结果集类型对象异常!");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
throw new RuntimeException("获取字段对象异常!");
}
return lists;
}
/**
* 获取连接
* @param dataSource
* @return Connection
*/
private Connection getConnection(DataSource dataSource){
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("获取数据库连接失败!");
}
return connection;
}
private static BoundSql getBoundSql(String sql){
//创建BoundSql
BoundSql boundSql = new BoundSql();
//创建参数标记处理器
ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
//创建通用标记处理器 解析sql 配合参数标记处理器获取解析后的sql和参数名集合
GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",tokenHandler);
//解析
String parse = genericTokenParser.parse(sql);
//获取参数集合
List<ParameterMapping> parameterMappings = tokenHandler.getParameterMappings();
//设置参数集合
boundSql.setParameterMappings(parameterMappings);
//设置解析后的sql 这里就是带#{}替换成?的sql
boundSql.setSql(parse);
return boundSql;
}
}
测试框架
package com.xs.persistent;
import com.xs.persistent.entity.User;
import com.xs.persistent.session.SqlSession;
import com.xs.persistent.session.SqlSessionFactory;
import com.xs.persistent.session.SqlSessionFactoryBuilder;
import com.xs.persistent.utils.Resource;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
/**
* @Author xs
* @Description
**/
public class test {
private static final SqlSession sqlSession;
static {
InputStream resourceAsStream = Resource.getResourceAsStream("persistentConfig.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = build.openSession();
}
/**
* 查询单个User
*/
@Test
public void test01(){
User user = new User();
user.setName("张三");
Object o = sqlSession.selectOne("User.selectOne",user);
System.out.println(o);
}
/**
* 查询全部User
*/
@Test
public void test02(){
List<Object> select = sqlSession.select("User.select");
for (Object o : select) {
System.out.println(o);
}
}
}
输出结果
selectOne
select
解析sql
解析sql这部分代码都是参考源码精简的 源码更加复杂 有兴趣的朋友可以去研究一下
创建BoundSql
部分源码
package com.xs.persistent.mapping;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @Author xs
* @Description 封装解析后的sql及#{}中参数名
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BoundSql {
/**
* 解析后的sql
*/
private String sql;
/**
* 解析后的参数集合
*/
private List<ParameterMapping> parameterMappings;
}
创建ParameterMapping
部分源码
封装了解析后的sql语句
package com.xs.persistent.mapping;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author xs
* @Description 封装解析好的sql
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ParameterMapping {
/**
* 解析好的sql语句
*/
private String property;
}
创建TokenHandler
部分源码
package com.xs.persistent.parsing;
/**
* @Author xs
* @Description 标记处理接口
**/
public interface TokenHandler {
/**
* 处理方法
* @param var1
* @return
*/
String handleToken(String var1);
}
创建ParameterMappingTokenHandler
部分源码 这里是一个内部类 我们只需要用到ParameterMappingTokenHandler
package com.xs.persistent.parsing;
import com.xs.persistent.mapping.ParameterMapping;
import java.util.ArrayList;
import java.util.List;
/**
* @Author xs
* @Description 标记处理接口实现类
**/
public class ParameterMappingTokenHandler implements TokenHandler{
private List<ParameterMapping> parameterMappings;
public ParameterMappingTokenHandler(){
parameterMappings = new ArrayList<>();
}
@Override
public String handleToken(String content) {
//content是参数名 封装到parameterMapping里再添加到集合
this.parameterMappings.add(this.buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content){
ParameterMapping parameterMapping = new ParameterMapping();
parameterMapping.setProperty(content);
return parameterMapping;
}
public List<ParameterMapping> getParameterMappings(){
return parameterMappings;
}
}
创建GenericTokenParser
部分源码 主要解析sql的通用标记解析类 解析出sql和参数名集合
package com.xs.persistent.parsing;
/**
* @Author xs
* @Description 通用标记解析器 主要解析sql 把#{}替换成? 把#{}里的参数封装到ParameterMapping里
* 分别为openToken (开始标记)、closeToken (结束标记)、handler (标记处理器)
**/
public class GenericTokenParser {
/**
* 开始标记
*/
private final String openToken;
/**
* 结束标记
*/
private final String closeToken;
/**
* 标记处理器
*/
private final TokenHandler handler;
/**
* 构造方法 初始化GenericTokenParser
* @param openToken
* @param closeToken
* @param handler
*/
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
/**
* 解析方法 我们可以带着一个单个查询sql来走一遍 比较好理解一点 text: select * from user where name = #{name}
* @param text XXXMapper.xml里的sql 带#{}
* @return
*/
public String parse(String text) {
//判断处理的sql是否为空
if (text != null && !text.isEmpty()) {
//拿到第一个{的下标
int start = text.indexOf(this.openToken);
//判断是否包含{
if (start == -1) {
//不包含直接返回原sql
return text;
} else {
//包含标记继续处理
//把sql转为字符数组
char[] src = text.toCharArray();
//截取开始下标
int offset = 0;
//new一个StringBuilder可变字符串
StringBuilder builder = new StringBuilder();
//用来存放#{}的内容
StringBuilder expression = null;
do {
//判断是否被注释
if (start > 0 && src[start - 1] == '\\') {
//this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(this.openToken);
offset = start + this.openToken.length();
} else {
if (expression == null) {
//new 一个StringBuilder
expression = new StringBuilder();
} else {
//不为空设置长度为0
expression.setLength(0);
}
//获取src字符数组中offset 到start-offset的值包含offset
//第一次返回#{之前的字符串
builder.append(src, offset, start - offset);
//这里返回#{之后的字符的下标
offset = start + this.openToken.length();
int end;
//offset开始拿到结束标记}的下标 等于-1就是不包含
for(end = text.indexOf(this.closeToken, offset); end > -1; end = text.indexOf(this.closeToken, offset)) {
if (end <= offset || src[end - 1] != '\\') {
//返回到#{}中的参数名 这里offset就是#{的后一个字符小标 end是}的下标
expression.append(src, offset, end - offset);
break;
}
expression.append(src, offset, end - offset - 1).append(this.closeToken);
offset = end + this.closeToken.length();
}
if (end == -1) {
//不包含结束标记 连接上开始标记之后的字符串
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//把参数名传递过去 返回?替换掉#{}
builder.append(this.handler.handleToken(expression.toString()));
//当前结束标记的下标加上当前结束下标的位置 src会通过返回的offset拿到下一个开始标记
offset = end + this.closeToken.length();
}
}
start = text.indexOf(this.openToken, offset);
} while(start > -1);
if (offset < src.length) {
//如果最后截取小标小于sql字符数组长度就加上结束标签后边的字符串
builder.append(src, offset, src.length - offset);
}
//返回处理好的sql
return builder.toString();
}
} else {
return "";
}
}
}
使用动态代理实现查询
工作中我们都有一个mapper接口来操作数据库 但是我们会看到并有实现类 可能大家会很困惑 mybaits里其实使用的动态代理来创建我们的mapper接口实现类 具体就是从sqlSession的getMapper()方法获取到的
getMapper()
在SqlSession接口创建getMapper()方法并在DefaultSqlSession中实现
SqlSession (动态代理)
package com.xs.persistent.session;
import java.util.List;
/**
* @Author xs
* @Description sqlSession 封装了增删改查方法 这里我们先实现查询功能 后边的更新 添加 删除就很简单了
**/
public interface SqlSession {
/**
* 查询所有的方法
*/
<T> List<T> select(String statement, Object... param);
/**
* 查询一个的方法
*/
<T> T selectOne(String statement,Object... param);
/**
* 获取mapper接口实现类
*/
<T> T getMapper(Class<T> aClass);
}
DefaultSqlSession (动态代理)
package com.xs.persistent.session.defaults;
import com.xs.persistent.config.Configuration;
import com.xs.persistent.config.MapperStatement;
import com.xs.persistent.executor.Executor;
import com.xs.persistent.executor.impl.SimpleExecutor;
import com.xs.persistent.session.SqlSession;
import java.lang.reflect.*;
import java.util.List;
import java.util.Queue;
/**
* @Author xs
* @Description sqlSession接口实现类
**/
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration){
this.configuration = configuration;
this.executor = new SimpleExecutor();
}
@Override
public <T> List<T> select(String statement, Object... param) {
//获取MapperStatement
MapperStatement mapperStatement = configuration.getMapperStatements().get(statement);
List<T> query = executor.query(configuration, mapperStatement, param);
if (query.size() == 0){
throw new RuntimeException("未查到匹配的数据!");
}
return query;
}
@Override
public <T> T selectOne(String statement,Object... param) {
List<Object> select = select(statement, param);
if(select.size() > 1){
throw new RuntimeException("查询出现多个值!");
}
return (T)select.get(0);
}
@Override
public <T> T getMapper(Class<T> aClass) {
//创建接口实现类
T t = (T)Proxy.newProxyInstance(aClass.getClassLoader(), new Class[]{aClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 获取到namesapce.id namespace设置为接口全限定类名 id设置为方法名
* 这样方法被调用时我们就可以通过namespace.id 拿到MapperStatement 执行具体方法
*/
//获取接口权限定类名
String namespace = aClass.getName();
//获取方法名
String id = method.getName();
String key = namespace+"."+id;
//拿到方法的返回值 这里为了判断是调用select还是selectOne 源码不是这样写的哦 这里为了方便
Type genericReturnType = method.getGenericReturnType();
if(genericReturnType instanceof ParameterizedType){
//有泛型
//调用select
return select(key, args);
}else{
return selectOne(key,args);
}
}
});
return t;
}
}
创建UserMapper
package com.xs.persistent.mapper;
import com.sun.org.apache.bcel.internal.generic.Select;
import com.xs.persistent.entity.User;
import java.util.List;
/**
* @Author xs
* @Description
**/
public interface UserMapper {
List<User> select();
User selectOne(User user);
}
修改UserMapper.xml中的namespace为接口权限定类名
<mapper namespace="com.xs.persistent.mapper.UserMapper">
<select id="selectOne" resultType="com.xs.persistent.entity.User" paramType="com.xs.persistent.entity.User">
select u.id,u.name,u.age,u.sex,u.address from user u where name = #{name}
</select>
<select id="select" resultType="com.xs.persistent.entity.User">
select u.id,u.name,u.age,u.sex,u.address from user u
</select>
</mapper>
测试
package com.xs.persistent;
import com.xs.persistent.entity.User;
import com.xs.persistent.mapper.UserMapper;
import com.xs.persistent.session.SqlSession;
import com.xs.persistent.session.SqlSessionFactory;
import com.xs.persistent.session.SqlSessionFactoryBuilder;
import com.xs.persistent.utils.Resource;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
/**
* @Author xs
* @Description
**/
public class test {
private static final SqlSession sqlSession;
static {
InputStream resourceAsStream = Resource.getResourceAsStream("persistentConfig.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = build.openSession();
}
/**
* 查询单个User
*/
@Test
public void test01(){
User user = new User();
user.setName("张三");
Object o = sqlSession.selectOne("User.selectOne",user);
System.out.println(o);
}
/**
* 查询全部User
*/
@Test
public void test02(){
List<Object> select = sqlSession.select("User.select");
for (Object o : select) {
System.out.println(o);
}
}
/**
* 动态代理创建接口实现类
*/
@Test
public void test03(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setName("张三");
User user01 = mapper.selectOne(user);
System.out.println("user:"+user);
List<User> select = mapper.select();
System.out.println("select:"+select);
}
}
运行结果
探究动态代理原理
提出问题 为什么创建的代理类执行方法的时候就能帮我执行getMapper()呢
看源码 当我Proxy.newInstance的时候穿了三个参数 第一个类加载器,通过累加加载器我们可以在java运行时动态的创建类,第二个参数接口class数组,我们这里是new了一个Object数组把UserMpper.class放了进去,Proxy内部会帮我们让生成的代理对象都是实现这些接口的方法 第三个参数InvocationHandler处理程序 源码里这样解释: 处理代理实例方法调用并返回,其实就是代理实例调用时候就会调用我们实现InvocationHandle.invoke()方法,这里大家可能不好理解, 我们来看看生成的代理对象就知道了;
*全局搜索ProxyGenerator
找到方框里的内容复制
sun.misc.ProxyGenerator.saveGeneratedFiles
创建GetProxy
import com.xs.persistent.entity.User;
import com.xs.persistent.mapper.UserMapper;
import com.xs.persistent.session.SqlSession;
import com.xs.persistent.session.SqlSessionFactory;
import com.xs.persistent.session.SqlSessionFactoryBuilder;
import com.xs.persistent.utils.Resource;
import java.io.InputStream;
import java.util.List;
/**
* @Author xs
* @Description
**/
public class GetProxy {
public static void main(String[] args) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
InputStream resourceAsStream = Resource.getResourceAsStream("persistentConfig.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = build.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setName("张三");
User user01 = mapper.selectOne(user);
System.out.println("user:"+user);
List<User> select = mapper.select();
System.out.println("select:"+select);
}
}
查看代理对象
会生成在项目根目录
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.sun.proxy;
import com.xs.persistent.entity.User;
import com.xs.persistent.mapper.UserMapper;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;
public final class $Proxy2 extends Proxy implements UserMapper {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m4;
private static Method m0;
public $Proxy2(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final User selectOne(User var1) throws {
try {
return (User)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final List select() throws {
try {
return (List)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.xs.persistent.mapper.UserMapper").getMethod("selectOne", Class.forName("com.xs.persistent.entity.User"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("com.xs.persistent.mapper.UserMapper").getMethod("select");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
分析原理
可以看到这个代理类实现了UserMapper接口并实现了接口方法,
继承了Proxy对象, 还记得我们创建代理对象传的第三个参数吗 处理调用接口实现类 他会被赋值到proxy的Invocationhandler属性上
然后代理对象的每个方法里都都用了我们写InvocationHandler的invoke方法
然后传入了三个参数 this 就是当前代理类的引用,第二个参数是当前方法的Method对象(见下方图片),第三个参数就是方法的参数 这时候我们是不是就豁然开朗了,当我们处理invoke方法时也能游刃有余,不会感觉迷迷糊糊了
框架源码更加复杂,有兴趣可以研究一下哦