暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

自定义持久层框架----SimpleMyBatis~~

小二与小七 2021-03-11
185

哈哈,时隔一年,满血复活。

至于为什么又开始更新了,我觉得这是一个重新学习的过程,从这里开始会从更深入的角度去刨析Mybatis。

Mybatis是一个使用非常广泛的持久层框架,我相信任何一个从事或学习java语言的人对它都不会陌生,这里我根据Mybatis的实现原理,自定义了一套简单的持久层框架。里面可能会涉及到一些java的基本知识,比如反射,内省等,还有一些设计模式,比如工厂,构建者,代理等,下面就开始了,不过鉴于水平有限,如果有错误请大家指出。

首先介绍一下概念,什么是持久层?

持久层是javaEE三层结构中与数据库交互的一层,我们习惯称之为dao层。

1. jdbc问题分析

思考一下:早期的持久层可能直接使用jdbc完成数据库的CRUD操作,但是我们想一下既然jdbc能够完成数据库操作为什么还会出现Mybatis?是不是jdbc在操作过程中存在一些问题?

看一下下面的代码,你能找到那些问题~~

public class JDBCUtils {

public static Account getAccount() {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try
{
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理类获取数据库链接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8",
"root", "root");
// 定义sql语句?表示占位符
String sql = "select * from account where name = ?";
// 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第个参数为sql语句中参数的序号(1开始),第个参数为设置的参数值
preparedStatement.setString(1, "tom");
// 向数据库发出sql查询,查询出结果集
resultSet = preparedStatement.executeQuery();
Account account = new Account();
// 遍历查询结果集
while (resultSet.next()) {
String id = resultSet.getString("id");
String username = resultSet.getString("name");
// 封装User
account.setId(id);
account.setName(username);
}
return account;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return null;
}
}
复制


下面这些问题你发现了吗?

解决思路

总结一下:

 原始jdbc开发存在的问题如下: 

1、 数据库连接创建、释放频繁造成系统资源浪费,从⽽影响系统性能。 

2、 Sql语句在代码中硬编码,造成代码不易维护,实际应⽤中sql变化的可能较⼤,sql变动需要改变 java代码。 

3、 使⽤preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不⼀定,可能 多也可能少,修改sql还要修改代码,系统不易维护。

4、 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据 库 记录封装成pojo对象解析⽐较⽅便。

解决思路:

1.使⽤数据库连接池初始化连接资源 

2.将sql语句抽取到xml配置⽂件中 

3.使⽤反射、内省等底层技术,⾃动将实体与表进⾏属性与字段的⾃动映射

2. 自定义持久层框架

2.1 设计思路

首先应该明白,自定义框架的本质还是对jdbc操作的封装,在完成框架的过程中需要利用一些方法来规避原生jdbc操作过程中的问题。

框架的设计需靠考虑两部分因素,第一个是使用者,第二个就是框架本身。

使⽤端: 

提供核⼼配置⽂件: 

SqlConfiguration.xml : 存放数据源信息,并引⼊mapper.xml 的地址,由于现在有两份配置文件,这样做的好处是避免配置文件被加载两次,我们可以在读取核心配置文件的时候就把SQL配置信息也一起读取到。

SqlMapper.xml : sql语句的配置⽂件信息


框架端: 

1.加载配置⽂件

读取完成以后以流的形式存在,我们不能将读取到的配置信息以流的形式存放在内存中,不好操作,可 以创建javaBean来存储

(1)SqlConfiguration : 存放数据库基本信息、

所以它应该有下面这些属性

DataSource:  数据库的配置

Map集合: 用来存放sql配置对象,这里需要注意的是,通常情况下一张表就对应一份Sql配置文件,我们的项目中不可能只有一张表,所以这里用一个map来存放不同的sql配置文件,也就是map集合中value,所以我们还应该要有一个独一无二的key来区分这些配置信息,在这用的是表对应的dao接口的全限定类名。

(2)SqlMapper:sql配置类

应该包含

String sql:sql语句、

String statementId: sql语句的唯一标识,用于获取对应的sql语句、

Class<?> paramsType: 输⼊参数java类型、

Class<?> resultType:输出参数java类型 

2.解析配置⽂件获取配置信息(构建者模式) 

很明显我们的核心配置文件是一个复杂的对象,至少它是由两部分构成,一个是DataSource,另一个是Sql配置信息,对于构建复杂对象,用构建者模式会比较方便,一步一步的构建出这个对象。

最后SqlSession需要一个执行器(Executor)来完成具体的工作,在Executor中封装jdbc操作。

创建XmlSqlConfigurationBuilder类: 用于解析SqlConfiguration.xml文件构建核心配置对象

创建XmlSqlMapperBuilder类: 用于解析SqlMapper.xml获取sql语句的相关信息

创建SqlSessionFactoryBuilder类:用于构建会话工厂

3.创建SqlSessionFactory及其实现类DefaultSqlSessionFactory(工厂模式):  用于创建sql会话

与数据库的交互不可能只有一次,也不可能每次都new一个SqlSession,所以我们使用工厂模式来创建SqlSession。

⽅法:openSession() : 获取sqlSession接⼝的实现类实例对象 

4.创建SqlSession接⼝及实现类DefaultSqlSession:

对于数据库的每一次操作,我们都可以认为是一次会话,所以需要有一个类(SqlSession)去完成这些工作,基于开闭原则这个SqlSession是一个接口,在框架中提供一个默认的实现(DefaultSqlSession)。

该类使用执行器(Executor)完成对数据库的操作。

5. 创建Executor及其实现类SimpleExecutor:封装jdbc操作

2.2 框架实现

根据前面的设计思路我们开始搭建这个框架,使用Idea创建两个maven工程,一个是我们的框架就叫 SimpleMybatis,另一个用来测试SimpleMybatis_test.如下图(创建过程就省略了)

1.加载配置⽂件

首先我们需要定义一个工具类,它的作用是根据传入的路径,将配置文件转为字节输入流对象,如下

package com.fxs.io;
import
java.io.InputStream;
/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public class Resources {
/**
*
根据路径将文件转为字节输入流
* @param path 文件路径
* @return
*/
public static InputStream getResourcesAsStream(String path){
return Resources.class.getClassLoader().getResourceAsStream(path);
}
}
复制

然后创建两个javabean用于封装配置信息,在这之前先看一下配置文件中都有哪些信息。

SqlConfiguration.xml:数据库的配置

SqlConfiguration.java 核心配置类

package com.fxs.domain;
import
javax.sql.DataSource;
import
java.util.HashMap;
import
java.util.Map;
/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public class SqlConfiguration {

// 数据库配置
private DataSource dataSource;
// sql信息
private Map<String,SqlMapper> mapperMap = new HashMap<>();
public
DataSource getDataSource() {
return dataSource;
}

public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}

public Map<String, SqlMapper> getMapperMap() {
return mapperMap;
}

public void setMapperMap(Map<String, SqlMapper> mapperMap) {
this.mapperMap = mapperMap;
}
}
复制

SqlMapper.xml sql配置文件

SqlMapper.java

package com.fxs.domain;
/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public class SqlMapper {

private String id;
private
Class<?> resultType;
private
Class<?> paramsType;
private
String sqlContent;
public
String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public Class<?> getResultType() {
return resultType;
}

public void setResultType(Class<?> resultType) {
this.resultType = resultType;
}

public Class<?> getParamsType() {
return paramsType;
}

public void setParamsType(Class<?> paramsType) {
this.paramsType = paramsType;
}

public String getSqlContent() {
return sqlContent;
}

public void setSqlContent(String sqlContent) {
this.sqlContent = sqlContent;
}
}
复制


2.解析配置文件封装到javabean对象中:

前面已经完成了javabean的定义,接下来就要对xml文件进行解析了,在这里使用了dom4j技术,先在pom文件中引入dom4j的坐标。

<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>
复制

前文中说过要解决jdbc连接对象频繁创建的问题,就需要使用数据库连接池,这里我们使用阿里巴巴的druid连接池,继续引入坐标。

<dependency>
<groupId>
com.alibaba</groupId>
<artifactId>
druid</artifactId>
<version>
1.0.9</version>
</dependency>
复制

然后开始解析xml文件获取配置信息,封装到对应的javaBean对象中。

XmlSqlMapperBuilder.java   解析SqlMapper.xml

package com.fxs.builder;
import
com.fxs.domain.SqlConfiguration;
import
com.fxs.domain.SqlMapper;
import
org.dom4j.Document;
import
org.dom4j.DocumentException;
import
org.dom4j.Element;
import
org.dom4j.io.SAXReader;
import
java.io.InputStream;
import
java.util.HashMap;

import java.util.List;

/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public class XmlSqlMapperBuilder {

private SqlConfiguration configuration;
public
XmlSqlMapperBuilder(SqlConfiguration configuration) {
this.configuration = configuration;
}

/**
*
解析sql配置文件
* @param inputStream
* @throws DocumentException
*/
public void parse(InputStream inputStream) throws DocumentException, ClassNotFoundException {
// 读取内存中的字节流获取dom对象,
Document dom = new SAXReader().read(inputStream);
// 获取xml的根标签
Element rootElement = dom.getRootElement();
// 获取标签中namespace属性的值,将来用于组成sql的唯一标识符
String namespace = rootElement.attributeValue("namespace");
// 获取根标签下的所有子标签selectupdatedeleteinsert
List<Element> elements = rootElement.elements();
// 用于获取配置文件中所有的sql信息
HashMap<String, SqlMapper> mapperHashMap = new HashMap<>();
for
(Element element : elements) {
SqlMapper sqlMapper = new SqlMapper();
String id = element.attributeValue("id");
// sql的唯一标识符,核心配置文件中的map集合的Key
String statementId = namespace +"."+ id;
sqlMapper.setId(id);
String paramsType = element.attributeValue("paramsType");
// 查询时有可能不需要参数,比如查询所有
if (paramsType != null && paramsType != ""){
// Class.forName()用于获取传入参数的实例
sqlMapper.setParamsType(Class.forName(paramsType));
}
String resultType = element.attributeValue("resultType");
// 实际上除了查询,增删改操作是不需要封装结果的
if (resultType != null && resultType != ""){
sqlMapper.setResultType(Class.forName(resultType));
}
// 获取sql语句
sqlMapper.setSqlContent(element.getTextTrim());
mapperHashMap.put(statementId,sqlMapper);
}
// 将获取到的配置信息装载到核心配置文件中
configuration.setMapperMap(mapperHashMap);
}
}
复制

解析核心配置文件

package com.fxs.builder;
import
com.alibaba.druid.pool.DruidDataSourceFactory;
import
com.fxs.domain.SqlConfiguration;
import
com.fxs.io.Resources;
import
org.dom4j.Document;
import
org.dom4j.Element;
import
org.dom4j.io.SAXReader;
import
javax.sql.DataSource;
import
java.io.InputStream;
import
java.util.List;
import
java.util.Properties;
/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public class XmlSqlConfigurationBuilder {

private SqlConfiguration configuration;
public
XmlSqlConfigurationBuilder(SqlConfiguration configuration) {
this.configuration = configuration;
}

/**
*
解析SqlConfiguration.xml文件
* @param inputStream
* @return
* @throws Exception
*/
public SqlConfiguration parseSqlConfigurationXml(InputStream inputStream) throws Exception {
Document dom = new SAXReader().read(inputStream);
Element rootElement = dom.getRootElement();
//读取数据库配置封装为一个properties对象
List<Element> property = rootElement.selectNodes("//property");
Properties properties = new Properties();
for
(Element element : property) {
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.setProperty(name,value);
}

//获取datasource加载数据库配置信息
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
configuration.setDataSource(dataSource);
//读取映射配置文件
List<Element> mappers = rootElement.selectNodes("//mapper");
for
(Element mapper : mappers) {
//获取sql配置文件的路径
String mapperPath = mapper.attributeValue("resources");
InputStream resourcesAsStream = Resources.getResourcesAsStream(mapperPath);
XmlSqlMapperBuilder xmlSqlMapperBuilder = new XmlSqlMapperBuilder(configuration);
// 解析sql配置文件并装载到核心配置文件中
xmlSqlMapperBuilder.parse(resourcesAsStream);
}
return configuration;
}
}
复制


构建SqlSessionFactory工厂

package com.fxs.builder;
import
com.fxs.domain.SqlConfiguration;
import
com.fxs.factory.DefaultSqlSessionFactory;
import
com.fxs.factory.SqlSessionFactory;
import
java.io.InputStream;
/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public class SqlSessionFactoryBuilder {

private SqlConfiguration configuration;
public
SqlSessionFactoryBuilder() {
this.configuration = new SqlConfiguration();
}

/**
*
构建SqlSessionFactory工厂
* @param inputStream
* @return
* @throws Exception
*/
public SqlSessionFactory build(InputStream inputStream) throws Exception {

XmlSqlConfigurationBuilder xmlSqlConfigurationBuilder = new XmlSqlConfigurationBuilder(configuration);
// 获取核心配置对象
configuration = xmlSqlConfigurationBuilder.parseSqlConfigurationXml(inputStream);
// 创建SqlSessionFactory工厂对象,并传入配置信息
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return
sqlSessionFactory;
}
}
复制


3. 创建工厂类

SqlSessionFactory.java (接口) 只有一个方法用来生产sqlSession

package com.fxs.factory;

import com.fxs.session.SqlSession;


/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public interface SqlSessionFactory {

public SqlSession openSession();
}
复制

DefaultSqlSessionFactory.java(默认实现)

复制
package com.fxs.factory;
import
com.fxs.domain.SqlConfiguration;
import
com.fxs.session.DefaultSqlSession;

import com.fxs.session.SqlSession;


/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory{

private SqlConfiguration configuration;
public
DefaultSqlSessionFactory(SqlConfiguration configuration) {
this.configuration = configuration;
}

@Override
public SqlSession openSession(){
return new DefaultSqlSession(configuration);
}
}
复制


4.创建SqlSession类

SqlSession.java(接口)

package com.fxs.session;
import
java.beans.IntrospectionException;
import
java.lang.reflect.InvocationTargetException;
import
java.sql.SQLException;

import java.util.List;


/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public interface SqlSession {

/**
*
查询所有
* @param statementId sql唯一标识符
* @param params 查询传入的参数
* @param <T> 查询结果的类型
* @return
* @throws IllegalAccessException
*
@throws IntrospectionException
*
@throws InstantiationException
*
@throws SQLException
*
@throws InvocationTargetException
*
@throws NoSuchFieldException
*/
public <T> List<T> findAll(String statementId,Object... params) throws IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException;
/**
*
查询一个
* @param statementId sql唯一标识符
* @param params
* @param <T>
* @return
* @throws IllegalAccessException
*
@throws IntrospectionException
*
@throws InstantiationException
*
@throws SQLException
*
@throws InvocationTargetException
*
@throws NoSuchFieldException
*/
public <T> T findOne(String statementId,Object... params) throws IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException;
/**
*
更新
* @param statementId sql唯一标识符
* @param params
* @return
* @throws IllegalAccessException
*
@throws NoSuchFieldException
*
@throws SQLException
*/
public int update(String statementId,Object... params) throws IllegalAccessException, NoSuchFieldException, SQLException;
public int
insert(String statementId,Object... params) throws IllegalAccessException, NoSuchFieldException, SQLException;
public int
delete(String statementId,Object... params) throws IllegalAccessException, NoSuchFieldException, SQLException;
}
复制

DefaultSqlSession.java(实现类)

在实现增删改操作的时候需要注意,增删改在jdbc中的操作是相同的,不同的只有sql语句,所以在实现的时候可以共用一套方法,只是传入不同的标识符用来获取对应的sql。

package com.fxs.session;
import
com.fxs.domain.SqlConfiguration;
import
com.fxs.domain.SqlMapper;
import
java.beans.IntrospectionException;
import
java.lang.reflect.*;
import
java.sql.SQLException;

import java.util.List;


/**
*
@author 笔墨画诗
* @version 1.0.0
*/
public class DefaultSqlSession implements SqlSession {

private SqlConfiguration configuration;
public
DefaultSqlSession(SqlConfiguration configuration) {
this.configuration = configuration;
}

@Override
public <T> List<T> findAll(String statementId,Object... params) throws IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException {
SqlMapper sqlMapper = configuration.getMapperMap().get(statementId);
SimpleExecutor simpleExecutor = new SimpleExecutor();
List<Object> query = simpleExecutor.query(configuration, sqlMapper, params);
return
(List<T>) query;
}

@Override
public <T> T findOne(String statementId,Object... params) throws IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException {
List<Object> list = findAll(statementId, params);
if
(list.size() == 1){
return (T) list.get(0);
}else {
throw new RuntimeException("呀,查询出错了!");
}
}

@Override
public int update(String statementId,Object... params) throws IllegalAccessException, NoSuchFieldException, SQLException {
SqlMapper sqlMapper = configuration.getMapperMap().get(statementId);
SimpleExecutor simpleExecutor = new SimpleExecutor();
return
simpleExecutor.update(configuration, sqlMapper, params);
}

@Override
public int insert(String statementId,Object... params) throws IllegalAccessException, NoSuchFieldException, SQLException {
return update(statementId,params);
}

@Override
public int delete(String statementId,Object... params) throws IllegalAccessException, NoSuchFieldException, SQLException {
return update(statementId,params);

}

}
复制

是不是以为到这就结束了,还没有呢,还有结果集的自动封装,jdbc的封装还没有做,但是为啥停下了,因为有点困,,,,

但是在睡觉之前,还有其他东西要说,前面我们使用了设计模式中的构建者模式和工厂模式其实都比较简单,没啥好说的,后面会用到代理模式,可能稍稍难一丢丢~~~所以提前说一下啥是代理模式。

话说小二刚开始的时候是一个小演员,他唯一的工作就是演戏,非常的开心也很快乐(忽略这个病句),小儿呢演的非常认真,所以名气开始变大,戏约呢也就变得很多,这时候他的工作开始忙碌于和各个剧组谈合约,演戏的时间就变少了,他是一个非常热爱演戏的人,这时候怎么办呢,他找来了一个经纪人,经纪人去谈那些杂七杂八的事,小二又可以愉快的演戏了,用一张图来形容一下

像上面这种经纪人和小二之间的关系就可以称之为代理,从图中我们可以发现,小二和经纪人是之间交互的,经纪人知道我为谁代理,小二知道谁为我代理的模式又叫做静态代理,虽然能在一时之间解燃煤之急但是缺陷也很明显,故事继续往下走,小二越来越红,他现在除了演戏之外还要拍广告,上综艺,唱歌,,,一个经纪人已经忙不过来了,所以他开始增加经纪人的数量,1个,2个,3个,然后我们就会发现小二的工作变成了跟各个经纪人打交道,正事又落下了,怎么办呢?找一家经济公司,小二只需要告诉经济公司我要代理什么,经济公司指定经济人完成,这样小二不直接和经纪人打交道,经纪人直到公司指定的时候才知道要为谁代理的模式,在代理之前双方互不接触的模式叫做动态代理。

在java中动态代理有两种方式,一个是jdk动态代理另一个是cjlib动态代理,区别在于是否需要提供实例化对象,jdk动态代理需要,cjlib动态代理不需要,所以cjlib动态代理的功能更强大一些,但是需要引入额外的依赖,在这个框架中使用的是jdk的动态代理。


概念

代理模式(Proxy Pattern):给某⼀个对象提供⼀个代理,并由代理对象控制对原对象的引⽤。代理模式 的英⽂叫做Proxy,它是⼀种对象结构型模式,代理模式分为静态代理和动态代理。

例子

创建⼀个Person接⼝,使其拥有⼀个没有返回值的doSomething⽅法。

/**
* 抽象类
*/
public interface Person {
void doSomething();
}
复制

创建⼀个名为Bob的Person接⼝的实现类,使其实现doSomething⽅法

/**
* 创建个名为Bob的实现类
*/
public class Bob implements Person {
public void doSomething() {
System.out.println("Bob doing something!");
}
}
复制

创建JDK动态代理类,使其实现InvocationHandler接⼝。拥有⼀个名为target的变量,并创建 getTa rget获取代理对象⽅法

public class JDKDynamicProxy implements InvocationHandler {
//被代理的对象
Person target;
// JDKDynamicProxy 构造函数
public JDKDynamicProxy(Person person) { this.target = person;
}
//获取代理对象
public Person getTarget() { return (Person)
Proxy.newProxylnstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), this);
}
//动态代理invoke
public Person invoke(Object proxy, Method method, Object[] args) throws
Throwable {
//被代理法前执
System.out.println("JDKDynamicProxy do something before!");
//被代理的
Person result = (Person) method.invoke(target, args);
//被代理法后执
System.out.println("JDKDynamicProxy do something after!"); return
result;
}
}
复制

大概就是这么回事~~~

文章转载自小二与小七,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论