本次实现是非常基础的模拟实现,大佬们可以不用看了,如果对mybatis不怎么了解的同学可以看一看。
先看一下整个实现的结构目录,如下图:
第一步,加载配置的 mapper 文件
public class ChampionConfiguration {
private static List<MapperBean> mappers = new ArrayList<>();
private Logger log = Logger.getLogger(ChampionConfiguration.class);
public ChampionConfiguration(String packagePath) {
readMapperXML(packagePath);
}
public ChampionConfiguration() {
}
public List<MapperBean> getMappers() {
return mappers;
}
/**
* 加载数据库propertis文件,并返回数据库连接对象
* @param resource
* @return
*/
public Connection loadProperties(String resource) {
try {
InputStream is = ClassLoader.getSystemResourceAsStream(resource);
Properties prop = new Properties();
prop.load(is);
//获取配置文件中的数据库连接信息
String driverClassName = prop.getProperty("datasource.driverClassName");
String url = prop.getProperty("datasource.url");
String username = prop.getProperty("datasource.username");
String password = prop.getProperty("datasource.password");
log.info("driver:" + driverClassName);
log.info("url:" + url);
log.info("username:" + username);
log.info("password:" + password);
//加载jdbc驱动类
Class.forName(driverClassName);
Connection connection = DriverManager.getConnection(url, username, password);
return connection;
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
/**某路径下的Mapper的xml文件
* 读取
* @param packagePath
* @return
*/
public List<MapperBean> readMapperXML(String packagePath) {
StringBuilder path = new StringBuilder();
//获取到项目路径
path.append(System.getProperty("user.dir").replaceAll("\\\\" , "/"));
//拼接资源路径
path.append("/src/main/java/");
packagePath = packagePath.replaceAll("\\.", "/");
//拼接包路径
path.append(packagePath);
File directory = new File(path.toString());
log.info(path.toString());
//遍历包下所有文件,不能有子包
File[] files = directory.listFiles();
for (File file : files) {
String name = file.getName();
//过滤以.xml结尾的文件,进行操作
if (name.endsWith(".xml")) {
try {
FileInputStream fis = new FileInputStream(file);
SAXReader reader = new SAXReader();
Document document = reader.read(fis);
Element root = document.getRootElement();
//创建mapper文件对应的存储对象类
MapperBean mapper = new MapperBean();
//获取根节点命名空间的值
String interfaceName = root.attributeValue("namespace").trim();
//将命名空间的值设置到MapperBean中
mapper.setInterfaceName(interfaceName);
//创建mapper文件中每一个sql对应的sql对象类
List<SQLFunction> functions = new ArrayList<>();
//通过迭代器遍历根节点下元素
for (Iterator iterator = root.elementIterator() ; iterator.hasNext();) {
//获取节点
Element e = (Element) iterator.next();
//获取sql的类型,select、insert、update、delete
String sqlType = e.getName();
//获取方法名
String functionName = e.attributeValue("id").trim();
//获取参数类型
String parameterType = e.attributeValue("parameterType");
//不为空时将空格去掉
if (parameterType != null) parameterType = parameterType.trim();
//获取返回的类型
String resultType = e.attributeValue("resultType");
Object resultTypeClass = null;
//不为空时将空格去掉
if (resultType != null) {
resultType = resultType.trim();
//获取返回类型的一个类对象
resultTypeClass = Class.forName(resultType).newInstance();
}
//获取sql语句
String sql = e.getText();
//创建一个方法类对象并存入数据
SQLFunction function = new SQLFunction(sqlType, functionName, sql, resultTypeClass, parameterType);
//将方法类对象添加到方法列表中
functions.add(function);
}
mapper.setFunctions(functions);
mappers.add(mapper);
} catch (DocumentException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
return mappers;
}
}
该类中一共有两个方法,分别是用来加载数据库 properties 文件和 mapper 配置文件。
先看 loadProperties 方法,通过传入的 resource 路径,获取到数据库配置信息并获取到数据库连接。
然后是 readMapperXML 方法,通过传入的包名,解析路径后,依此将路径下所有后缀为 .xml 的文件进行解析。解析所有节点的信息后存入到 List mappers = new ArrayList<>() 中,先看一下 AccountInfoMapper.xml 配置文件内容:
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.champion.mybatis.mapper.AccountInfoMapper" >
<select id="selectByPrimaryKey" resultType="com.champion.mybatis.pojo.AccountInfo" parameterType="java.lang.Integer" >
select id,username,password,money
from account_info
where id = #{id}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
delete from account_info
where id = #{id}
</delete>
<insert id="insert" parameterType="com.champion.mybatis.pojo.AccountInfo" >
insert into account_info (id, username, password, money)
values (#{id}, '#{username}', '#{password}', #{money})
</insert>
<update id="updateByPrimaryKey" parameterType="com.champion.mybatis.pojo.AccountInfo" >
update account_info
set username = '#{username}',
password = '#{password}',
money = #{money}
where id = #{id}
</update>
</mapper>
通过 mapper 文件,可以看到一个 mapper 文件中存在多个 sql 查询语句,而一个 sql 语句,又包含了多条信息,比如id,sql类型,入参,sql语句等等,所以对于一个 mapper 文件,需要创建一个合适的数据结构来存储,就是 MapperBean 这个Java类。
public class MapperBean {
/** 接口包名,用来做反射 */
private String interfaceName;
/** 所有 sql 集合,存储每一个 sql 的信息 */
private List<SQLFunction> functions;
public MapperBean() {
}
public MapperBean(String interfaceName, List<SQLFunction> functions) {
this.interfaceName = interfaceName;
this.functions = functions;
}
public String getInterfaceName() {
return interfaceName;
}
public void setInterfaceName(String interfaceName) {
this.interfaceName = interfaceName;
}
public List<SQLFunction> getFunctions() {
return functions;
}
public void setFunctions(List<SQLFunction> functions) {
this.functions = functions;
}
}
MapperBean 类中还有一个 SQLFunction 类,如下:
public class SQLFunction {
/** sql 类型(select,insert,update,delete) */
private String sqlType;
/** sql 查询对应接口的方法名 */
private String functionName;
/** sql 语句 */
private String sql;
/** sql 的返回类型 */
private Object resultType;
/** sql 方法传入的参数类型 */
private String parameterType;
public SQLFunction() {
}
public SQLFunction(String sqlType, String functionName, String sql, Object resultType, String parameterType) {
this.sqlType = sqlType;
this.functionName = functionName;
this.sql = sql;
this.resultType = resultType;
this.parameterType = parameterType;
}
public String getSqlType() {
return sqlType;
}
public void setSqlType(String sqlType) {
this.sqlType = sqlType;
}
public String getFunctionName() {
return functionName;
}
public void setFunctionName(String functionName) {
this.functionName = functionName;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public Object getResultType() {
return resultType;
}
public void setResultType(Object resultType) {
this.resultType = resultType;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
@Override
public String toString() {
return "SQLFunction{" +
"sqlType='" + sqlType + '\'' +
", functionName='" + functionName + '\'' +
", sql='" + sql + '\'' +
", resultType=" + resultType +
", parameterType='" + parameterType + '\'' +
'}';
}
}
Mapper 文件存储的结构就是这样,
包扫描后,存在 ChampionConfiguration 类中的 List 中。
第二步,初始化执行器 ChampionExecutor 类
public class ChampionExecutor implements Executor {
private ChampionConfiguration configuration;
/** 数据库连接对象 */
private static Connection connection;
private Logger log = Logger.getLogger(ChampionExecutor.class);
/**
* 获取数据库连接对象
* @return
*/
private Connection getConnection() {
if (connection == null) { // 多线程环境下可能会有问题,可能会产生多个 connectioon
connection = configuration.loadProperties("properties/db.properties");
log.info("调用一次connection");
}
return connection;
}
public ChampionExecutor(ChampionConfiguration configuration) {
this.configuration = configuration;
}
/**
* 查找方法
* @param sql 传入的sql语句
* @param resultType 返回结果类对象
* @param param 方法参数值
* @param <T> 结果对象的类型
* @return 返回查询的结果对象
*/
@Override
public <T> T query(String sql,Object resultType, Object param) throws Exception {
Object newInstance = resultType.getClass().newInstance();
//判断sql语句中时否包含了#字符,#字符表示一个占位符,#{XXX},匹配一个参数值
if (sql.contains("#")) {
//判断参数是否为空
if (param == null) {
//如果为空,则未设置参数值,将异常抛出
throw new Exception("该查找sql中没有设置对应参数");
}
sql = sql.replaceFirst("#\\{.+}", String.valueOf(param));
}
log.info("\nSQL:" + sql);
Connection connection = getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = connection.prepareStatement(sql);
rs = ps.executeQuery();
ReflectHelper helper = new ReflectHelper(newInstance);
//获取全部成员变量
Field[] fields = helper.getFields();
while (rs.next()) {
for (Field field : fields) {
String name = field.getName();
//给对象设置值
helper.setFieldValue(name, rs.getObject(name));
}
}
return (T) newInstance;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rs != null) {
rs.close();
}
if (ps != null) {
ps.close();
}
}
return null;
}
/**
* 更新方法
* @param sql 传入的sql语句
* @param param 方法参数值
* @return
* @throws Exception
*/
@Override
public Integer update(String sql, String parameterType, Object param) throws Exception {
//创建传入的参数的反射工具类
ReflectHelper helper = new ReflectHelper(param);
List<Object> params = new ArrayList<>();
//获取成员变量
Field[] fields = helper.getFields();
//遍历成员变量,获取每一个值,并存入变量值列表
for (int i=0 ; i<fields.length ; i++) {
//根据变量名获取变量值
Object value = helper.getFieldValue(fields[i].getName());
params.add(value);
}
//遍历成员遍历名,替换对应的占位符
for (int i=0 ; i<fields.length ; i++) {
sql = sql.replaceFirst("#\\{" + fields[i].getName() + "}", String.valueOf(params.get(i)));
}
log.info("\nSQL:" + sql);
//获取数据库连接
Connection connection = getConnection();
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
//执行更新操作
int i = ps.executeUpdate();
return i;
} catch (Exception e) {
e.printStackTrace();
return 0;
} finally {
if (ps != null) {
ps.close();
}
}
}
/**
* 删除方法
* @param sql 传入的sql语句
* @param param 方法参数值
* @return
* @throws Exception
*/
@Override
public Integer delete(String sql, Object param) throws Exception {
//判断sql语句中时否包含了#字符,#字符表示一个占位符,#{XXX},匹配一个参数值
if (sql.contains("#")) {
//判断参数是否为空
if (param == null) {
//如果为空,则未设置参数值,将异常抛出
throw new Exception("该查找sql中没有设置对应参数");
}
sql = sql.replaceFirst("#\\{.+}", String.valueOf(param));
}
log.info("\nSQL:" + sql);
Connection connection = getConnection();
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
int i = ps.executeUpdate();
return i;
} catch (Exception e) {
return 0;
} finally {
if (ps != null) {
ps.close();
}
}
}
}
因为是根据我的 mapper 配置文件写的,所以扩展性不是很好,你们可以进一步修改。
在查询和更新方法中有如下代码
ReflectHelper helper = new ReflectHelper(newInstance);
//获取全部成员变量
Field[] fields = helper.getFields();
//给对象设置值
helper.setFieldValue(name, rs.getObject(name));
//根据变量名获取变量值
Object value = helper.getFieldValue(fields[i].getName());
这些操作都是在 ReflectHelper 类中封装好的,不过也是刚好满足我这次的需求而已,依然可以继续改进。下面看一下该类
public class ReflectHelper {
/** 传入 Object 对象的 class 对象 */
private Class clazz;
/** 传入 Object 对象 */
private Object object;
/** 传入 Object 对象的所有成员变量 */
private Field[] fields;
/** 传入 Object 对象的所有成员变量的 get 方法 */
private Hashtable<String, Method> getMethods = null;
/** 传入 Object 对象的所有成员变量的 set 方法 */
private Hashtable<String, Method> setMethods = null;
public Field[] getFields() {
return fields;
}
public Hashtable<String, Method> getGetMethods() {
return getMethods;
}
public Hashtable<String, Method> getSetMethods() {
return setMethods;
}
public ReflectHelper() {
}
public ReflectHelper(Object object) {
this.object = object;
initMethod();
}
/**
* 初始化该对象的变量和setter,getter方法
*/
private void initMethod() {
//创建getter,setter集合
getMethods = new Hashtable<>();
setMethods = new Hashtable<>();
//获取该对象的class
clazz = object.getClass();
//获取到成员变量列表并保存
fields = clazz.getDeclaredFields();
//获取到全部方法
Method[] methods = clazz.getMethods();
String gs = "get(\\w+)";
String ss = "set(\\w+)";
Pattern getM = Pattern.compile(gs);
Pattern setM = Pattern.compile(ss);
//遍历方法集合
for (Method m : methods) {
String name = m.getName();
//判断方法名是否以get或者set开头
if (Pattern.matches(gs, name)) {
//如果get开头,则将get去掉并且将去掉后的字符串首字母小写
String s = toLowerFirst(getM.matcher(name).replaceAll("$1"));
//存入get方法集合中
getMethods.put(s, m);
} else if (Pattern.matches(ss, name)) {
//如果set开头,则将set去掉并且将去掉后的字符串首字母小写
String s = toLowerFirst(setM.matcher(name).replaceAll("$1"));
//存入set方法集合中
setMethods.put(s, m);
} else {
//如果开头get,set都不是,则不处理
}
}
}
/**
* 给对象设置值
* @param fieldName
* @param value
* @return
*/
public boolean setFieldValue(String fieldName, Object value) {
Method method = setMethods.get(fieldName);
if (method != null) {
boolean flag = false;
try {
method.invoke(object, value);
flag = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
return flag;
}
} else {
return false;
}
}
public Object getFieldValue(String fieldName) {
Method method = getMethods.get(fieldName);
if (method != null) {
try {
Object invoke = method.invoke(object, null);
return invoke;
} catch (Exception e) {
e.printStackTrace();
return null;
}
} else {
return null;
}
}
/**
* 转换首字母变为小写
* @param s
* @return
*/
private String toLowerFirst(String s) {
char[] chars = s.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
}
该类就是将传入的 Object 对象解析出所有的成员变量,以及他们的 get,set 方法,并存储起来方便使用,并且提供了,对成员变量设第置值和获取值的方法。
第三步,就是ChampionSqlSession类,该类是将之前的 mapper 加载,以及执行器串起来形成完整的流程的一个类。
public class ChampionSqlSession {
private ChampionConfiguration configuration;
private ChampionExecutor executor;
public ChampionSqlSession(String packagePath) {
configuration = new ChampionConfiguration(packagePath);
executor = new ChampionExecutor(configuration);
}
public Object selectOne(SQLFunction function, Object param) throws Exception {
String sqlType = function.getSqlType();
String parameterType = function.getParameterType();
Object resultType = function.getResultType();
String sql = function.getSql();
switch (sqlType) {
case "select" :
try {
Object newInstance = executor.query(sql, resultType, param);
return newInstance;
} catch (Exception e) {
e.printStackTrace();
return null;
}
case "insert" :
case "update" :
try {
Integer i = executor.update(sql, parameterType, param);
return i;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
case "delete" :
try {
Integer i = executor.delete(sql, param);
return i;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
default:
throw new Exception("mapper文件配置sql配置错误......");
}
}
@SuppressWarnings("unchecked")
public <T> T getMapper(Class clazz) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
new ChampionMapperProxy(configuration, this, clazz));
}
该类中有两个方法,第一个是 selectOne 方法,它用来判断要执行方法中的 sql 是什么类型,然后根据类型的不同,调用执行器中不同的方法。第二个方法是 getMapper ,他通过动态代理获取到一个动态代理对象,这个动态代理对象每次调用方法都会执行 ChampionMapperProxy 这个动态代理类的 invoke 方法,动态代理类都必须实现 InvocationHandler 接口。如下所示:
public class ChampionMapperProxy implements InvocationHandler {
private ChampionSqlSession sqlSession;
private ChampionConfiguration configuration;
private Class clazz;
public ChampionMapperProxy(ChampionConfiguration configuration, ChampionSqlSession sqlSession, Class clazz) {
this.configuration = configuration;
this.sqlSession = sqlSession;
this.clazz = clazz;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//获取configuration中的mappers集合
List<MapperBean> mappers = configuration.getMappers();
//如果集合中没有值则返回空
if (mappers == null || mappers.size() == 0) {
return null;
} else {
//获取该方法的接口名称
String interfaceName = method.getDeclaringClass().getName();
boolean exists = false;
MapperBean mapper = null;
//遍历集合
for (MapperBean bean : mappers) {
//如果集合中存在该接口,则将该mapperBean保存下来,并结束循环
if (bean.getInterfaceName().equals(interfaceName)) {
exists = true;
mapper = bean;
break;
}
}
//判断标识是否已获取到该接口的mapperBean
if (exists) {
//有则取其中的方法
List<SQLFunction> functions = mapper.getFunctions();
//判断方法集合是否为空
if (functions != null && functions.size() > 0) {
//遍历方法集合
for (SQLFunction function : functions) {
//如果方法名跟调用方法名相等
if (function.getFunctionName().equals(method.getName())) {
//将调用sqlSession的selectOne方法,传入方法对象以及参数值
return sqlSession.selectOne(function, args[0]);
}
}
}
}
return null;
}
}
}
在 invoke 方法中我们可以看到,他根本就没有执行外部调用的那个方法,而是去执行 sqlSession 的 selectOne 方法,传入获取的 mapper 信息,和外部调用时的实际参数,最后返回这个结果。也就是说在外部通过 ChampionSqlSession 类获取的接口,在调用过程中,实际上执行的是 ChampionExecutor 类中的方法,通过 mapper 文件的配置信息,操作数据库并返回。
最后,看一下测试类,以及结果。
public class ChampionBatisTest {
@Test
public void run() throws InterruptedException {
Logger log = Logger.getLogger(ChampionBatisTest.class);
ChampionSqlSession sqlSession = new ChampionSqlSession("com.champion.mybatis.mapper");
AccountInfoMapper mapper = sqlSession.getMapper(AccountInfoMapper.class);
AccountInfo accountInfo = mapper.selectByPrimaryKey(1);
log.info(accountInfo.toString());
AccountInfo acc = new AccountInfo(2, "李四", "123", 17.00);
mapper.insert(acc);
AccountInfo acc2 = mapper.selectByPrimaryKey(2);
log.info(acc2.toString());
acc.setMoney(27.00);
mapper.updateByPrimaryKey(acc);
AccountInfo acc3 = mapper.selectByPrimaryKey(2);
log.info(acc3.toString());
mapper.deleteByPrimaryKey(2);
AccountInfo acc5 = mapper.selectByPrimaryKey(2);
log.info(acc5.toString());
}
}
结果:
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.readMapperXML(ChampionConfiguration.java:91)] - C:/IDEA/workplace/champion-mybatis/src/main/java/com/champion/mybatis/mapper
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.query(ChampionExecutor.java:64)] -
SQL:
select id,username,password,money
from account_info
where id = 1
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.loadProperties(ChampionConfiguration.java:58)] - driver:com.mysql.jdbc.Driver
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.loadProperties(ChampionConfiguration.java:59)] - url:jdbc:mysql://127.0.0.1:3306/spring_day03?useUnicode=true&characterEncoding=utf-8
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.loadProperties(ChampionConfiguration.java:60)] - username:root
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.loadProperties(ChampionConfiguration.java:61)] - password:123456
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.getConnection(ChampionExecutor.java:34)] - 调用一次connection
[INFO] [com.championBatis.test.ChampionBatisTest.run(ChampionBatisTest.java:24)] - AccountInfo{id=1, username='张三', password='123', money=12.0}
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.update(ChampionExecutor.java:119)] -
SQL:
insert into account_info (id, username, password, money)
values (2, '李四', '123', 17.0)
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.query(ChampionExecutor.java:64)] -
SQL:
select id,username,password,money
from account_info
where id = 2
[INFO] [com.championBatis.test.ChampionBatisTest.run(ChampionBatisTest.java:29)] - AccountInfo{id=2, username='李四', password='123', money=17.0}
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.update(ChampionExecutor.java:119)] -
SQL:
update account_info
set username = '李四',
password = '123',
money = 27.0
where id = 2
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.query(ChampionExecutor.java:64)] -
SQL:
select id,username,password,money
from account_info
where id = 2
[INFO] [com.championBatis.test.ChampionBatisTest.run(ChampionBatisTest.java:34)] - AccountInfo{id=2, username='李四', password='123', money=27.0}
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.delete(ChampionExecutor.java:156)] -
SQL:
delete from account_info
where id = 2
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.query(ChampionExecutor.java:64)] -
SQL:
select id,username,password,money
from account_info
where id = 2
[INFO] [com.championBatis.test.ChampionBatisTest.run(ChampionBatisTest.java:38)] - AccountInfo{id=0, username='null', password='null', money=0.0}
Process finished with exit code 0
给自己一些学习的记录,也希望能帮助看到这里的你。