基于XML文件方式自定义简单的MyBoptis
这里用到的技术
-
代理模式(JDK动态代理)
-
工厂模式
-
C3P0连接池
-
DOM4J
-
XPATH
竟然要自定义MyBatis,那就要把pom.xml里MyBatis的做标干掉,添加C3P0,DOM4J,XPATH坐标
<!-- C3P0 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<!-- 解析 xml 的 dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- dom4j 的依赖包 jaxen -->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
去掉mybatis坐标后,发现单元测试里面报错了. 那么下面我们就来开始自定义MyBatis框架!!!
思路分析
// 1.把配置文件转成流
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2.根据构建SqlSessionFactory,把流传给SqlSessionFactory
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = builder.build(is);
// 3.在SessionFactory根据流解析xml,读取配置文件的内容,
//给sqlSession赋值(a.连接数据库的基本信息 b.映射文件相关信息[为代理对象做准备])
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4.通过动态代理创建UserDao的代理对象,提供InvocationHandler实现,对它实现时候,
// 需要提供sql语句,执行sql封装结果集(会用到低3步中的b的部分)
UserDao userDao = sqlSession.getMapper(UserDao.class);
第一步入手
1、创建Resources.java
public class Resources {
/**
* 读取classpath下面的文件,转成字节输入流
*/
public static InputStream getResourceAsStream(String path) {
InputStream is = Resources.class.getClassLoader().getResourceAsStream(path);
return is;
}
}
完成第一步
第二步
2、使用建造者模式创建SqlSessionFactoryBuilder
2.1 创建SqlSessionFactory接口
2.2 创建SqlSessionFactoryBuilder类里添加build(InputStream is)方法,该方法根据(主配置文件)字节输入流, 构建SqlSessionFactory,在里创建工厂实现类返回,要问为什么?我们来看一下源码,我们发现源码都是抄我们的(我们抄源码的)
2.3 接下来创建SqlSessionFactory的实现类,如果直接new SqlSessionFactory返回,和主配置文件无瓜,我们要加载(配置文件流)所有在DefautlSqlSessionFactory添加一个加载配置文件流的方法
public class SqlSessionFactoryBuilder {
/**
* 根据(主配置文件)字节输入流, 构建SqlSessionFactory
* @param is
* @return
*/
public SqlSessionFactory build(InputStream is) {
DefaultSqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory();
// 加载资源(流)
sqlSessionFactory.setConfiguration(is);
return sqlSessionFactory;
}
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
//主配置文件输入流
private InputStream config;
public void setConfiguration(InputStream config) {
this.config = config;
}
}
第二步搞定(不报错了)
第三步
在SqlSessionFactory里创建openSession返回SqlSession获得SqlSession
我们先来看SqlSession(抄一下)源码
[外链图片转存失败(img-bO1qfGn2-1566818707943)(sqlSession源码.png)]
1、创建SqlSession接口
2、创建SqlSessiond的实现类DefaultSqlSession(封装了Connection)
显然这样返回是不够的,SqlSession需要根据config解析config,给SqlSession赋值(SqlSession就好比跟数据库连接的通道。。【需要连接数据库,封装SQL语句等…】);这个我们等下再做
第三步不报错了(表面完成)
第四步
在SqlSession里创建getMapper(xxxx.calss)方法(得到接口的代理对象,也就是说这个方法内部用的就是动态代理)
public interface SqlSession {
/**
* @param daoClass 接口类的字节码对象(class)
* @author lys
* @Description 生成代理对象
* @return T
* @date 2019/8/26 12:40
*/
<T>T getMapper(Class<T> daoClass);
}
4.2、DefaultSqlSession再实现这个方法
似乎还有一个地方报错!!!不能忍
4.3再给SqlSession添加一个close方法,DefaultSqlSession再实现一下
不报错了 完成!!!跑一个???
???
想啥呢 兄dei ,我们只搭建了一个整体的架构,才刚刚开始;
接
着
来
吧
。
。
。
。
。
。
五
第一第二步我们已经完成了
从第三步开始
5.1我们先创建一个工具类,该类用来解析配置文件封装到pojo里
public class XmlConfigUtils {
public static Configuration parseXml(InputStream config) {
try {
//0 创建Configuration对象
Configuration configuration = new Configuration();
//1. 创建解析器对象
SAXReader saxReader = new SAXReader();
//2. 读取配置文件, 获得document
Document document = saxReader.read(config);
//3. 获得所有的property对象集合
List<Element> propertyEles = document.selectNodes("//dataSource/property");
//4. 遍历property对象集合(解析连接数据库四个基本项)
for (Element propertyEle : propertyEles) {
String name = propertyEle.attributeValue("name");
String value = propertyEle.attributeValue("value");
if ("driver".equals(name)) {
configuration.setDriver(value);
}
if ("url".equals(name)) {
configuration.setUrl(value);
}
if ("username".equals(name)) {
configuration.setUsername(value);
}
if ("password".equals(name)) {
configuration.setPassword(value);
}
}
//5. 判断是否使用连接池
Element dataSourceEle = (Element) document.selectSingleNode("//dataSource");
String type = dataSourceEle.attributeValue("type");
if ("POOLED".equals(type)) {
//使用连接池
configuration.setUsePool(true);
} else {
//不使用连接池
configuration.setUsePool(false);
}
//6. 解析所有的mapper节点
List<Element> mapperEles = document.selectNodes("//mapper");
//7. 遍历
for (Element mapperEle : mapperEles) {
//8 判断是xml方式还是注解方式
Attribute resourceAttr = mapperEle.attribute("resource");
if (resourceAttr != null) {
//9.xml方式, 解析映射文件,获得Map<String,Mapper> mapperMap
String mapPath = resourceAttr.getValue();
Map<String, Mapper> mapperMap = loadXmlMapperConfig(mapPath);
configuration.setMappers(mapperMap);
} else {
//10 注解方式
}
}
return configuration;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 加载映射文件,生成Map<String,Mapper>
*
* @param mapPath
* @return
*/
private static Map<String, Mapper> loadXmlMapperConfig(String mapPath) throws Exception {
InputStream is = null;
try {
Map<String, Mapper> mapperMap = new HashMap<String, Mapper>();
//1. 根据路径获得流对象
is = Resources.getResourceAsStream(mapPath);
//2. 创建解析器
SAXReader saxReader = new SAXReader();
//3. 读取流, 获得document对象
Document document = saxReader.read(is);
//4. 获得根元素 mapper
Element mapperElement = document.getRootElement();
//5. 获得namespace
String namespace = mapperElement.attributeValue("namespace");
//6. 获得所有的select节点
List<Element> selectEles = mapperElement.elements("select");
//7. 遍历(没遍历一次对应一个mapper)
for (Element selectEle : selectEles) {
String id = selectEle.attributeValue("id");
String resultType = selectEle.attributeValue("resultType");
String sql = selectEle.getText();
Mapper mapper = new Mapper();
mapper.setSql(sql);
mapper.setResultType(resultType);
String key = namespace + "." + id;
mapperMap.put(key, mapper);
}
return mapperMap;
} finally {
is.close();
}
}
}
5.2 居然要封装到pojo我们也需要一个pojo,创建一个配置类
public class Configuration {
private String driver;
private String url;
private String username;
private String password;
private boolean usePool; // POOLED:使用连接池(mybatis内置的); UNPOOLED:不使用连接池
private Map<String,Mapper> mappers; // 映射文件对应的对象(sql语句,返回结果类型....)
get set.....
5.3 这里的mappers是什么? 我们先来看看,映射文件、数据库和pojo
[外链图片转存失败(img-5fjPdGVP-1566818707953)(分析.png)]
执行SQL语句,根据结果类型(resutType),封装结果集到pojo对象里
根据已知的id(调用的方法名)来获取sql语句和resutType,所有Map来作为数据结构,仅仅把id作为map的key行不行?当然是不行的,因为可能有很多的接口和接口映射文件(eg:UserDao.xml,ProductDao.xml…),可能id(方法名)会重复,所有我们不能仅仅把id作为Key
我们把namespace(接口的全限定名)+id(方法名)作为Key
因为只就两个(Sql语句和resutType),我们可以封装成一个对象(Mapper类),map的value就是Mapper对象
public class Mapper {
private String sql;
private String resultType;
get set...
}
Configuration里mappers的结构
key | value |
---|---|
接口的全限定名.方法名() | Mapper对象 |
5.4 分析完成,调用该工具类获得Configuration
六、使用代理模式编写 ProxyMethodInvocationHandler类
代理模式分为静态和动态代理。 静态代理,我们通常都很熟悉。有一个写好的代理类,实现与要代理的类的一个共同的接口,目的是为了约束也为了安全。具体不再多说。
今天我们就会使用 JDK 动态代理方式来编写MapperProxyFactory 类
我们先来完成getMapper方法,该方法就是通过JDK的动态代理产生接口的对象
@Override
public <T> T getMapper(Class <T> daoClass) {
return (T) Proxy.newProxyInstance(daoClass.getClassLoader(),
new Class[]{daoClass},
new MethodProxyInvocationHandler
(getConnection(),configuration.getMappers()));
}
new MethodProxyInvocationHandler(getConnection(),configuration.getMappers());增强方法类,该类控制了代理对象的执行方法逻辑。操作数据;(通过构造方法传参)
参数1:连接对象
参数2:刚才我们创建的类,封装了sql语句
我们现在来完成增强方法
-
1.获得方法名(id)
-
2.获得当前方法所在类的全限定名(namespace)
-
3.获得mapper对象
-
4.获得sql语句和结果类型
-
5.创建预编译sql语句对象
-
6.执行
-
6.1我们需要一个工具类,该通过泛型的反射获得结果集
开搞!!
public class MethodProxyInvocationHandler implements InvocationHandler {
private Connection connection;
private Map <String, Mapper> mappers;
public MethodProxyInvocationHandler(Connection connection, Map <String, Mapper> mappers) {
this.connection = connection;
this.mappers = mappers;
}
/**
* 控制代理对象的执行方法逻辑 增强,操作数据
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//1. 获得方法名(id)
String methodName = method.getName();
//2. 获得当前方法所在类的全限定名(namespace)
String className = method.getDeclaringClass().getName();
//3. 获得mapper对象
Mapper mapper = mappers.get(className + "." + methodName);
//4.获得sql语句和结果类型
String sql = mapper.getSql();
String resultType = mapper.getResultType();
//5创建预编译sql语句对象
preparedStatement = connection.prepareStatement(sql);
//6.执行
resultSet = preparedStatement.executeQuery();
// 6.1 把resultSet封装成List集合,list里面的对象类型就是resultType
List list = ResultUtils.handleResult(resultSet, resultType);
return list;
} finally {
// 释放资源。。。。。。。。。。。
if (resultSet != null) {
resultSet.close();
}
if (preparedStatement != null){
preparedStatement.close();
}
}
}
}
结果集工具类
public class ResultUtils {
/**
* 把结果集,封装成List
* @param resultSet 结果集
* @param className 类的全限定名
* @return
* @throws Exception
*/
public static List handleResult(ResultSet resultSet, String className) throws Exception {
List list = new ArrayList();
Class clazz = Class.forName(className);
//1.遍历结果集, 没遍历一次创建一个对象
while(resultSet.next()){
Object obj = clazz.newInstance();
// 2. 获得元数据
ResultSetMetaData metaData = resultSet.getMetaData();
//3. 获得列的个数
int columnCount = metaData.getColumnCount();
for(int i = 1; i <= columnCount;i++){
//4. 获得列名(其实就是对象的属性名)
String columnName = metaData.getColumnName(i);
//5. 反射得到字段对象
Field declaredField = clazz.getDeclaredField(columnName);
declaredField.setAccessible(true);
//6. 获得当前列的数据
Object value = resultSet.getObject(columnName);
//7. 赋值
declaredField.set(obj,value);
}
list.add(obj);
}
return list;
}
}
对了释放资源方法还没完成,完成以下
@Override
public void close() {
if(connection != null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
大功告成!!!跑起来
这个简单的自定义MyBatis希望可以帮助到大家更好理解MyBatis
当然真正的MyBatis比这个要复杂很多…