前面我们编写了一个正常的项目,他需要使用到我们的框架,但是我们还没有开始编写我们的MyMybatis框架,我们现在已经学会了使用mybatis框架,已经学会了使用jdbc连接mysql,并且已经搭好了一个引用MyMybatis框架的正常项目,所以这次我们开始真正的开始编写我们的MyMybatis框架,开始“抄袭”之路。
首先我们要做的准备一个maven项目,名字叫做my-mybatis-core
,之后就是在pom文件下面引入以下的jar包
<dependencies>
<!-- mysql 依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>
前面我们已经知道了如何使用MyMybatis框架,所以我们从引用我们框架的第一行代码开始入手
第一行代码长这样
InputStream resourceAsSteam = Resources.getResourceAsStream("myMybatisConfig.xml");
我们再来回顾一下myMybatisConfig.xml
文件
<configuration>
<!--1.配置数据库信息-->
<dataSource>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://masiyi.obmtj0gc1rgs0ho0-mi.oceanbase.aliyuncs.com:3306/test_ob"></property>
<property name="username" value="rootmsy"></property>
<property name="password" value="Msy18719255298"></property>
</dataSource>
<!--2.引入映射配置文件-->
<mappers>
<mapper resource="mapper/UserMapper.xml"></mapper>
<mapper resource="mapper/UserMapperCopy.xml"></mapper>
</mappers>
</configuration>
根据第一行代码的内容,我们需要创建一个Resources
类,里面有一个getResourceAsStream
方法传入一个字符串返回一个InputStream
的方法,类似这样:
package com.masiyi.io;
import java.io.InputStream;
/**
* 解析配置文件
*/
public class Resources {
/**
* 加载配置文件
* @param path
* @return
*/
public static InputStream getResourceAsStream(String path) {
return Resources.class.getClassLoader().getResourceAsStream(path);
}
}
这个方法的作用就是根据xml文件的路径转换为一个输入流,目的是加载配置文件
我们再看看第二行代码:
Configuration configuration = new ConfigParse().parse(resourceAsSteam);
这一行的代码是解析为一个Configuration对象,首先我们来创建一个Configuration类用来存储数据库的信息:
package com.masiyi.entity;
import lombok.Data;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 存放核心配置文件解析出来的内容UserMapper.xml
*/
@Data
public class Configuration {
// 数据源对象
private DataSource dataSource;
// key:statementId:namespace.id MappedStatement:封装好的MappedStatement对象
private Map<String, MappedStatement> mappedStatementMap = new HashMap();
}
里面的DataSource
属性是javax.sql
包里面的,而MappedStatement
类是我们自定义的类,用于存放mapper.xml解析内容,他是长这样:
package com.masiyi.entity;
import lombok.Data;
/**
* 映射配置类:存放mapper.xml解析内容,如UserMapper.xml
*/
@Data
public class MappedStatement {
// 唯一标识 statementId:namespace.id
private String statementId;
// 返回值类型
private String resultType;
// 参数值类型
private String parameterType;
// sql语句
private String sql;
}
这个类里面的属性用来解析xml中自定义的属性,一一对应于xml文件中的
<select id="findById" resultType="com.masiyi.entity.User" parameterType="java.lang.Integer">
select * from user where id = #{id}
</select>
通过这段代码,Configuration
类中的dataSource
被赋值
最后通过MapperParse
的mapperParse
方法,Configuration
类中的mappedStatementMap
被赋值,所以最终解析出来的configuration
内容如下:
里面包含了数据库的属性和各个sql解析出来的mappedStatementMap
SimpleSqlSession simpleSqlSession = new SimpleSqlSession(configuration);
这行代码的作用是创建一个sqlSession,用于连接数据库,SimpleSqlSession
类长这样,里面有一个configuration
属性,用来存第二步解析出来的configuration,第二个属性是一个执行类,里面放的就是我们之前用jdbc写的代码,只不过多了一个封装返回成一个实体类的步骤罢了。
package com.masiyi.executor;
import com.masiyi.entity.Configuration;
import com.masiyi.entity.MappedStatement;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
/**
* @Author masiyi
* @Date 2023/11/10
* @PackageName:com.masiyi.sqlSession
* @ClassName: SqlSession
* @Description: 放操作(查询)的地方
* @Version 1.0
*/
@Data
public class SimpleSqlSession {
private Configuration configuration;
private SimpleExecutor simpleExecutor;
public SimpleSqlSession(Configuration configuration) {
this.configuration = configuration;
this.simpleExecutor = new SimpleExecutor();
}
/**
* 查询列表
*
* @param param
* @param <E>
* @return
*/
public <E> List<E> selectList(MappedStatement mappedStatement, Object param) {
//拿到 MappedStatement 对象,例如
// <select id="findById" resultType="com.masiyi.entity.User" parameterType="java.lang.Integer">
// select * from user where id = #{id}
// </select>
return simpleExecutor.query(configuration, mappedStatement, param);
}
public <T> T newProxyClass(Class<T> targetClass) {
return (T) Proxy.newProxyInstance(SimpleSqlSession.class.getClassLoader(), new Class[]{targetClass}, new InvocationHandler() {
/**
*
* @param proxy 调用该方法的代理实例
*
* @param method {@code Method}实例对应于在代理实例上调用的接口方法。{@code Method}对象的声明类将是该方法被声明的接口,
* 该接口可能是代理类继承该方法所通过的代理接口的超接口。
*
* @param args 一个对象数组,包含在代理实例上的方法调用中传递的参数值,或者如果接口方法不接受参数,则{@code null}。
* 基本类型的参数被包装在适当的基本包装器类的实例中,例如{@code java.lang。Integer}或{@code java.lang.Boolean}。
*
* @return 方法的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
//拿到statementId
String statementId = method.getDeclaringClass().getName() + "." + method.getName();
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
//todo 添加其他的增删改查方法,渲染传递的参数值
return selectList(mappedStatement, args == null ? null : args[0]);
}
});
}
/**
* 关闭资源
*/
public void close() {
simpleExecutor.close();
}
}
SimpleExecutor类长这样:
package com.masiyi.executor;
import com.masiyi.entity.Configuration;
import com.masiyi.entity.MappedStatement;
import com.masiyi.util.MyMybatisUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
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.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
* @Author masiyi
* @Date 2023/11/10
* @PackageName:com.masiyi.executor
* @ClassName: SimpleExecutor
* @Description: 执行器,把jdbc里面的代码拿过来
* Connection conn = null;
* Statement stmt = null;
* ResultSet rs = null;
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SimpleExecutor {
private Connection conn = null;
private Statement stmt = null;
private ResultSet rs = null;
@SneakyThrows
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) {
// 获取数据库连接
conn = configuration.getDataSource().getConnection();
// 获取 SQL 语句
String sql = mappedStatement.getSql();
// 创建 PreparedStatement 对象,并将 SQL 语句中的占位符替换为实际的参数值
PreparedStatement preparedStatement = conn.prepareStatement(MyMybatisUtil.replacePlaceholders(sql,param));
// 执行查询操作,获取结果集
rs = preparedStatement.executeQuery();
// 处理返回结果集
ArrayList<E> list = new ArrayList<>();
// 遍历结果集的每一行
while (rs.next()){
// 获取结果集的元数据信息,包含字段名和字段值的信息
ResultSetMetaData metaData = rs.getMetaData();
// 获取结果类型
String resultType = mappedStatement.getResultType();
// 根据结果类型获取对应的类对象
Class<?> resultTypeClass = Class.forName(resultType);
// 创建结果对象的实例
Object o = resultTypeClass.newInstance();
// 遍历结果集的每一列
for (int i = 1; i <= metaData.getColumnCount() ; i++) {
// 获取字段名
String columnName = metaData.getColumnName(i);
// 获取字段的值
Object value = rs.getObject(columnName);
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
// 创建属性描述器,用于获取属性的读写方法
Method writeMethod = propertyDescriptor.getWriteMethod();
// 调用属性的写方法,将字段值设置到结果对象中
writeMethod.invoke(o,value);
}
// 将结果对象添加到列表中
list.add((E) o);
}
return list;
}
/**
* 关闭资源
*/
public void close() {
// 关闭资源
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这一部分代码就是封装返回集为一个实体类:
UserDao userDao = simpleSqlSession.newProxyClass(UserDao.class);
这行代码的目的就是使用第三步创建的SimpleSqlSession
类创建一个代理类UserDao
,从而实现代理每个方法,在调用每个方法之前都会调用SimpleSqlSession
类里面的这个方法从而实现代理模式的应用。
//findById
User user = new User();
user.setId(1);
userDao.findById(user).forEach(System.out::println);
System.out.println("===================");
//findAll
userDao.findAll().forEach(System.out::println);
userDao对应的xml文件内容如下:
<mapper namespace="com.masiyi.dao.UserDao">
<select id="findAll" resultType="com.masiyi.entity.User">
select * from user
</select>
<select id="findById" resultType="com.masiyi.entity.User" parameterType="java.lang.Integer">
select * from user where id = #{id}
</select>
</mapper>
最后便通过上面代理模式的方法执行得到结果:
simpleSqlSession.close();
执行SimpleExecutor
类里面的close
方法,将
编写成功,至此我们的MyMybatis框架就完全地“抄袭”mybatis成功,我们自己的框架里面成功实现了通过封装sql成xml文件,最后进行解析,成功实现了sql的select的功能,但是我们没能实现增删改
方法,这里给大家留一个课后作业,大家可以根据现有的基础自己完成剩余功能的编写,而这个项目已经完全开源,仅供大家参考,代码地址为:https://gitee.com/WangFuGui-Ma/my-mybatis
最后附上文章里面工具类:
package com.masiyi.util;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author masiyi
* @Date 2023/11/10
* @PackageName:com.masiyi.util
* @ClassName: MyMybatisUtil
* @Description: 工具类
* @Version 1.0
*/
public class MyMybatisUtil {
static final Pattern pattern = Pattern.compile("#\\{([^}]+)}");
/**
* 将 SQL 查询语句中的占位符替换为对象的属性值
*
* @param sql 原始的 SQL 查询语句
* @param obj 包含属性值的对象
* @return 替换占位符后的 SQL 查询语句
*/
public static String replacePlaceholders(String sql, Object obj) {
// 创建匹配器,用于匹配占位符
Matcher matcher = pattern.matcher(sql);
// 创建字符串缓冲区,用于存储替换后的结果
StringBuffer sb = new StringBuffer();
// 循环查找匹配的占位符
while (matcher.find()) {
// 获取占位符的名称
String placeholder = matcher.group(1);
// 获取占位符对应的属性值
Object replacement = getPropertyValue(obj, placeholder);
// 如果属性值不为空
if (replacement != null) {
if (replacement.getClass() == String.class && !((String) replacement).isEmpty()) {
// 如果属性值是字符串类型且非空,添加单引号
replacement = "'" + replacement + "'";
} else {
// 将属性值转换为字符串
replacement = replacement.toString();
}
// 将匹配到的占位符替换为属性值,并将结果添加到字符串缓冲区
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement.toString()));
}
}
// 将剩余的部分添加到字符串缓冲区
matcher.appendTail(sb);
return sb.toString();
}
/**
* 获取对象的属性值
*
* @param obj 包含属性值的对象
* @param propertyName 属性名
* @return 属性值的字符串表示
*/
public static Object getPropertyValue(Object obj, String propertyName) {
try {
// 根据属性名使用反射获取属性值
Field field = obj.getClass().getDeclaredField(propertyName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将字符串的首字母大写
*
* @param str 输入字符串
* @return 首字母大写后的字符串
*/
public static String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
}
另外如果对Elastic Search感兴趣的话,推荐一下我的专栏,这篇专栏介绍了Elasticsearch的Restful API的入门指南。学习如何使用API进行索引、搜索和分析,包括创建索引、定义映射、添加文档、执行查询等。通过实例和代码片段,快速上手Elasticsearch的Restful API,构建强大的搜索功能。感谢大家支持: