mybatis 是一个优秀的基于 java 的持久层框架,它内部封装了 jdbc,使开发者只需要关注 sql 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。
mybatis 通过 xml或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题,对 jdbc 进行了封装,屏蔽了 jdbc api 底层访问细节,使我们不用与 jdbc api 打交道,就可以完成对数据库的持久化操作。
创建工程之前,我们先新建数据库mybatis,并在数据库中新建一张User表,并加一些数据。表包含:id,username,birthday,sex,address字段
mybatis
mysql驱动
log4j
junit单元测试
4.0.0
com.itheima
mybatis-day01-demo1
1.0-SNAPSHOT
jar
UTF-8
UTF-8
org.mybatis
mybatis
3.4.5
mysql
mysql-connector-java
5.1.6
runtime
log4j
log4j
1.2.12
junit
junit
4.12
test
src/main/java
**/*.xml
#####2.2.2 创建User
创建com.itheima.domain包,在该包下创建User对象,并添加对应的属性。
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
//略 get...set...toString...
}
创建com.itheima.mapper包,并在该包下创建接口,代码如下:
它其实就是dao层的接口
public interface UserMapper {
List findAll();
}
这个xml配置文件的位置,必须和对应的那个Mapper接口的位置一样。而且其文件名也要和接口名一样
在com.itheima.mapper包下创建UserMapper.xml,并在UserMapper.xml中添加一个select查询结点,代码如下:
在main/resources下创建SqlMapConfig.xml,在文件中配置数据源信息和加载映射文件,代码如下:
这个配置文件的名字不是固定的,你可以随便命名
为了方便查看日志,在main/resources下创建log4j.properties文件,代码如下:
log4j.rootLogger=DEBUG,Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
log4j.logger.org.apache=DEBUG
在test包下创建com.itheima.test,再在该包下创建MyBatisTest类,代码如下:
public class MyBatisTest {
@Test
public void testFindAll() throws IOException {
//读取配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//通过SqlSessionBuilder对象构建一个SqlSessionFactory
SqlSessionFactory sqlSessionFactory = builder.build(is);
//通过SqlSessionFactory构建一个SqlSession
SqlSession session = sqlSessionFactory.openSession();
//通过SqlSession实现增删改查
UserMapper userMapper = session.getMapper(UserMapper.class);
List users = userMapper.findAll();
//打印输出
for (User user : users) {
System.out.println(user);
}
//关闭资源
session.close();
is.close();
}
}
本章我们将使用前面所学的基础知识来构建一个属于自己的持久层框架,将会涉及到的一些知识点:工厂模式(Factory 工厂模式)、构造者模式(Builder 模式)、动态代理模式,反射, xml 解析,数据库元数据,数据库元数据等。
我们来看看MyBatis框架使用过程中用到的一些设计模式。
基于上面我们说到的JDBC流程再结合MyBatis流程,我们封装一个持久城框架,达到MyBatis中的增删改查效果。来分析一波:
通过上图分析,我们可以发现:
分别将上一个入门工程中的User.java、UserMapper.java、MyBatisTest.java、UserMapper.xml、log4j.properties、SqlMapConfig.xml都拷贝到该工程中,将XML中引用的DTD文件约束去掉,不然每次解析都会去网上下载。
4.0.0
com.itheima
mybatis-day01-demo2-custom
1.0-SNAPSHOT
jar
log4j
log4j
1.2.12
mysql
mysql-connector-java
5.1.36
dom4j
dom4j
1.6.1
jaxen
jaxen
1.1.3
c3p0
c3p0
0.9.1.2
junit
junit
4.12
src/main/java
**/*.xml
我们看到上面的工程存在错误,我们对他进行改造一下,首先创建对应的文件来去掉错误。
该类的主要作用是读取类路径下的资源文件,所以要求被它读取的文件务必放到classes下,我们创建它的目的主要是模拟加载读取UserMapper.xml和SqlMapConfig.xml文件。
public class Resources {
public static InputStream getResourceAsStream(String path){
InputStream is = Resources.class.getClassLoader().getResourceAsStream(path);
return is;
}
}
此时MyBatisTest中的Resources类就引用上面创建的类就可以去掉一个错误了。
创建该类,并创建一个build方法返回一个SqlSessionFactory对象,但SqlSessionFactory还没创建,所以接着我们需要创建它。
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream is) {
return null;
}
}
创建SqlSessionFactory接口及其实现类,并且创建一个openSession方法。
public interface SqlSessionFactory {
SqlSession openSession();
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
@Override
public SqlSession openSession() {
return null;
}
}
创建SqlSession接口,并在接口里面创建对应方法,然后创建一个DefaultSqlSession实现类
public interface SqlSession {
UserMapper getMapper(Class userMapperClass);
void close();
}
public class DefaultSqlSession implements SqlSession {
@Override
public UserMapper getMapper(Class userMapperClass) {
return null;
}
@Override
public void close() {
}
}
把MyBatisTest类重新导包后,错误就全部消失了。接着我们就要开始对每个模块展开分析和代码实现了。
我们回到刚才我们的分析,首先我们要解析SqlMapConfig.xml,并把信息存储到Configuration对象中,然后通过Configuration对象获取数据库连接对象Connection。我们可以分这么几个步骤完成:
public class Configuration {
private String username;
private String password;
private String url;
private String driver;
//get..set..toString..
}
public class XMLConfigBuilder{
public static Configuration loadConfiguration(InputStream is){
try {
//1)数据库配置信息存储
Configuration cfg = new Configuration();
//创建SAXReader对象读取XML文件字节输入流
SAXReader reader = new SAXReader();
Document document = reader.read(is);
//解析配置文件,获取根节点信息,//property表示获取根节点下所有的property结点对象
List rootList = document.selectNodes("//property");
//循环迭代所有结点对象
for (Element element : rootList) {
//name属性的值
String name = element.attributeValue("name");
//vallue属性的值
String value = element.attributeValue("value");
//2)将解析的数据库连接信息存储到Configuration中
//数据库驱动
if(name.equals("driver")){
cfg.setDriver(value);
}else if(name.equals("url")){
//数据库连接地址
cfg.setUrl(value);
}else if(name.equals("username")){
//数据库账号
cfg.setUsername(value);
}else if(name.equals("password")){
//数据库密码
cfg.setPassword(value);
}
}
//获取需要解析的XML路径
String resource = element.attributeValue("resource");
//拿到映射配置文件的路径
...接下来看下面的分析准备解析映射配置文件
} catch (Exception e) {
e.printStackTrace();
}
}
}
在Configuration对象中创建ComboPooledDataSource对象,并创建获得数据源的方法getDataSource,再创建一个获得Connection的方法getConnection,getConnection通过调用getDataSource获得数据源,然后获得Connection对象。
public class Configuration {
//数据库用户名
private String username;
//数据库用户密码
private String password;
//数据库连接地址
private String url;
//数据库驱动
private String driver;
//创建数据源
private ComboPooledDataSource dataSource = new ComboPooledDataSource();
//get..set..toString..
//获取数据源
private DataSource getDataSource(){
//设置数据源配置
try {
dataSource.setUser(username);
dataSource.setPassword(password);
dataSource.setJdbcUrl(url);
dataSource.setDriverClass(driver);
} catch (PropertyVetoException e) {
e.printStackTrace();
}
return dataSource;
}
public Connection getConnection(){
try {
return getDataSource().getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
UserMapper.xml内容:
接着上面流程,我们需要解析UserMapper.xml获取SQL语句,并获取返回值的类型,这个时候我们可以考虑封装一个Mapper对象,存储对应的SQL语句和返回值类型。针对这个操作实现,我们可以分为下面几个步骤实现:
Mapper对象用于存储SQL语句和返回的JavaBean全限定名,这时候我们可以考虑定义2个属性来接收存储。
public class Mapper {
//执行的SQL语句
private String sql;
//执行SQL语句后要返回的JavaBean全限定名
private String resultType;
//带参构造函数
public Mapper(String sql, String resultType) {
this.sql = sql;
this.resultType = resultType;
}
//get..set..toString
}
我们接着将刚才解析的XML信息存储到Mapper中。我们分析下,目前我们只存在一个select结点,如果以后存在多个怎么操作?如下代码:
我们可以把多个select结点信息封装成多个Mapper,并将多个Mapper存储到Map
/**
* 解析UserMapper.xml,提取SQL语句和返回JavaBean全限定名
* path为UserMapper.xml的路径
*/
public static Map loadMapper(String path){
/****
* 1)定义一个Map mappers
* 用于存储解析的XML封装的Mapper信息
*/
Map mappers = new HashMap();
try {
//获得文件字节输入流
InputStream is = Resources.getResourceAsStream(path);
//创建SAXReader对象,加载文件字节输入流
SAXReader reader = new SAXReader();
Document document = reader.read(is);
//获得根节点
Element rootElement = document.getRootElement();
//获取命名空间的值
String namespace = rootElement.attributeValue("namespace");
//获取所有select结点
List selectList = document.selectNodes("//select");
//循环所有select结点
for (Element element : selectList) {
//获取ID属性值
String id = element.attributeValue("id");
//获取resultType属性值
String resultType = element.attributeValue("resultType");
//获取SQL语句
String sql = element.getText();
//2)构建Mapper对象
Mapper mapper = new Mapper(sql,resultType);
//key = namespace+.+id;
String key = namespace+"."+id;
//存储到Map中
mappers.put(key,mapper);
}
return mappers;
} catch (DocumentException e) {
e.printStackTrace();
}
return mappers;
}
按照JDBC操作流程,最终调用SQL语句的是PreparedStatment对象,而PreparedStatment对象由Connection对象构建,所以我们可以把SQL语句给Configuration对象管理。按照这个思路,可以在获取Configuration对象中创建一个Map
这里主要添加了一个Map
public class Configuration {
//数据库用户名
private String username;
//数据库用户密码
private String password;
//数据库连接地址
private String url;
//数据库驱动
private String driver;
//创建数据源
private ComboPooledDataSource dataSource = new ComboPooledDataSource();
//存储所有SQL语句和返回值全限定名
private Map mappers = new HashMap();
public Map getMappers() {
return mappers;
}
//这里的set方法为了保证每次填充进来的数据不被覆盖,直接调用putAll塞进Map中
public void setMappers(Map mappers) {
this.mappers.putAll(mappers);
}
//...略
}
我们再来看看MyBatis操作的流程,从代码中我们可以看到,通过SqlSession的getMapper方法创建的代理对象是具备查询数据库功能的,也就是说它拥有操作数据库的能力,而操作数据库的能力的前提是能获得连接数据库Connection对象,我们可以基于这个想法,把上面获得的Configuration对象给DefaultSqlSession对象,这样就能通过SqlSession构建一个代理对象,对赋予这个代理对象操作数据库的能力。
@Test
public void testFindAll() throws IOException {
//读取配置文件
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//通过SqlSessionBuilder对象构建一个SqlSessionFactory
SqlSessionFactory sqlSessionFactory = builder.build(is);
//通过SqlSessionFactory构建一个SqlSession
SqlSession session = sqlSessionFactory.openSession();
//通过SqlSession实现增删改查
UserMapper userMapper = session.getMapper(UserMapper.class);
List users = userMapper.findAll();
//打印输出
for (User user : users) {
System.out.println(user);
}
//关闭资源
session.close();
is.close();
}
那么我们会有下面这些疑问:
1)什么时候加载解析配置文件呢?
答:我们可以在SqlSessionFactory.openSqlSession()的时候,初始化加载上面配置文件。
2)加载配置文件会初始化数据库连接信息,这时候需要加载读取SqlMapConfig.xml配置文件,如何通知程序读取这个配置文件?
答:早在我们第一步的时候就加载读取了SqlMapConfig.xml文件获取了文件字节输入流,我们可以在构建SqlSessionFactory对象的时候把它传给build方法,如果这时候DefaultSqlSessionFactory中可以接受这个文件字节输入流,那么在openSqlSession()的时候,就可以把这个字节输入流传给XMLConfigBuilder来解析,并获取对应的配置。
3)上面说到让DefaultSqlSession对象具备操作数据库的能力,需要把Configuration对象给DefaultSqlSession对象,如果做到呢?
答:可以在解析XML对象的时候,直接把DefaultSqlSession的实例传给XMLConfigBuilder.loadConfiguration(DefaultSqlSession session,InputStream is)
在DefaultSqlSession中加上Configuration对象,让他具备操作数据库的能力,创建set方法给Configuration赋值,代码如下:
public class DefaultSqlSession implements SqlSession {
//把Configuration对象给DefaultSqlSession
private Configuration cfg;
//创建一个set方法,给Configuration赋值
public void setCfg(Configuration cfg) {
this.cfg = cfg;
}
@Override
public UserMapper getMapper(Class userMapperClass) {
return null;
}
@Override
public void close() {
}
}
改造DefaultSqlSessionFactory,加入SqlMapConfig.xml配置文件的字节输入流,并创建DefaultSqlSession对象,加入加载解析配置文件的方法loadConfiguration(sqlSession,is)
public class DefaultSqlSessionFactory implements SqlSessionFactory {
//SqlMapConfig.xml的字节输入流
private InputStream is;
public void setIs(InputStream is) {
this.is = is;
}
@Override
public SqlSession openSession() {
//创建一个DefaultSqlSession
DefaultSqlSession sqlSession = new DefaultSqlSession();
//加载解析配置文件
Configuration cfg = XMLConfigBuilder.loadConfiguration(is);
sqlSession.setCfg(cfg);
return sqlSession;
}
}
改造SqlSessionFactoryBuilder的build方法,创建DefaultSqlSessionFactory对象,并将SqlMapConfig.xml的字节输入流传给DefaultSqlSessionFactory。
public class SqlSessionFactoryBuilder {
/***
* 读取并解析配置文件,构建一个SqlSessionFactory对象
* @param is
* @return
*/
public SqlSessionFactory build(InputStream is) {
//创建一个SqlSessionFactory的实例
DefaultSqlSessionFactory sqlSessionFactory =new DefaultSqlSessionFactory();
//给SqlSessionFactory的is属性赋值
sqlSessionFactory.setIs(is);
return sqlSessionFactory;
}
}
把其中getMapper改成通用的方法
/***
* 改造成通用的方法
* @param clazz
* @param
* @return
*/
T getMapper(Class clazz);
@Override
public T getMapper(Class clazz) {
/*****
* 参数:
* 1)被代理对象的类加载器
* 2)字节数组,让代理对象和被代理对象有相同的行为[行为也就是有相同的方法]
* 3)InvocationHandler:增强代码,需要使用提供者增强的代码,改代码是以接口的实现类方式提供的,通常用匿名内部内,但不绝对。
*/
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
}
按照JDBC操作流程,我们得先拿到Connection对象,再拿到SQL语句,再执行获取返回结果集,再将返回结果集封装成要的对象即可。因此我们需要DefaultSqlSession,通过它来实现增删改查,实现增删改查就需要获取当前所需的Mapper,Mapper里面包含需要执行的SQL语句和执行SQL语句后返回的结果集需要转换的JavaBean对象。
我们需要用DefaultSqlSession来实现增删改查,可以直接考虑在SqlSession接口中编写增删改查,让DefaultSqlSession完成增删改查的实现,因此我们这里只需要引入SqlSession即可。
按照上面这个分析,我们可以总结为如下几个步骤实现:
######4.6.3.4 在SqlSession中编写增删改查,在DefaultSqlSession中实现增删改查
修改SqlSession,在SqlSession中增加selectList方法
/***
* 集合查询
* @param
* @return
*/
List selectList(String statement);
修改DefaultSqlSession,在DefaultSqlSession中实现selectList方法,其中集合查询我们用到了一个Converter转换器,转换器的写法紧接着在后面会列出。
@Override
public List selectList(String statement) {
//获取对应的Mapper
Mapper mapper = cfg.getMappers().get(statement);
//JDBC操作流程实现
if(mapper!=null){
//执行查询
Connection conn = null;
PreparedStatement stm = null;
ResultSet resultSet = null;
try {
//获取Connection对象
conn = cfg.getConnection();
//获取PreparedStatment
stm = conn.prepareStatement(mapper.getSql());
//执行查询
resultSet = stm.executeQuery();
//调用Converter实现转换
List list = Converter.list(resultSet,Class.forName(mapper.getResultType()));
return list;
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if(resultSet!=null){
resultSet.close();
}
if(stm!=null){
stm.close();
}
//关闭Connection
this.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return null;
}
Converter转换器主要利用反射机制实现ResultSet转成JavaBean,代码实现如下:
public class Converter {
/**
* 这个方法,是将结果集中的每一条数据封装到一个JavaBean中,多条数据就对应多个JavaBean,再将多个 JavaBean放到一个List集合中
* @param set
* @param clazz
* @param
* @return
*/
public static List converList(ResultSet set, Class clazz){
List beans = new ArrayList();
//1.遍历结果集
try {
//根据结果集元数据,获取结果集中的每一列的列名
ResultSetMetaData metaData = set.getMetaData();
int columnCount = metaData.getColumnCount();//获取总列数
while (set.next()){
//每次遍历,遍历出一条数据,每条数据就对应一个JavaBean对象
E o = (E) clazz.newInstance();
//获取每一列数据,根据列名获取
//for循环遍历出每一列
for(int i=1;i<=columnCount;i++){
String columnName = metaData.getColumnName(i);
Object value = set.getObject(columnName);//获取该列的值
//将该列的值存放到JavaBean中
//也就是调用JavaBean的set方法,使用内省机制
PropertyDescriptor descriptor = new PropertyDescriptor(columnName,clazz);
Method writeMethod = descriptor.getWriteMethod();//获取该属性的set方法
//调用set方法
writeMethod.invoke(o,value);
}
//经过这个for循环,我的JavaBean就设置好了
//把JavaBean添加进list集合
beans.add(o);
}
} catch (Exception e) {
e.printStackTrace();
}
return beans;
}
}
我们定义一个代理实现类,在代理实现类中通过被调用的方法来确定Mapper的key。然后直接调用SqlSession中定义的selectList方法。
public class MapperProxyFactory implements InvocationHandler {
//1)
private SqlSession sqlSession;
public MapperProxyFactory(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//1、获取当前操作锁对应的Mapper信息
String className = method.getDeclaringClass().getName(); //类的名字,和UserMapper.xml中mapper的namespace一致
String methodName = method.getName(); //方法名字,和UserMapper.xml中的id值一致
String key = className+"."+methodName;
//确定当前操作是否是查询所有
Class> returnType = method.getReturnType();
if(returnType== List.class){
//2)执行集合查询操作
return sqlSession.selectList(key);
}else{
return null;
}
}
}
DefaultSqlSession中的getMapper方法
@Override
public T getMapper(Class clazz) {
/*****
* 参数:
* 1)被代理对象的类加载器
* 2)字节数组,让代理对象和被代理对象有相同的行为[行为也就是有相同的方法]
* 3)InvocationHandler:增强代码,需要使用提供者增强的代码,改代码是以接口的实现类方式提供的,通常用匿名内部内,但不绝对。
*/
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new MapperProxyFactory(this));
}
将DefaultSqlSession中selectList的代码封装到一个Executor的工具类中,方便使用。我们新建一个Executor的工具类。
public class Executor {
/***
* 集合查询
* @param conn
* @param mapper
* @param
* @return
*/
public static List list(Connection conn, Mapper mapper) {
//执行查询
PreparedStatement stm = null;
ResultSet resultSet = null;
try {
//获取PreparedStatment
stm = conn.prepareStatement(mapper.getSql());
//执行查询
resultSet = stm.executeQuery();
//调用Converter实现转换
List list = Converter.list(resultSet, Class.forName(mapper.getResultType()));
return list;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
if (resultSet != null) {
resultSet.close();
}
if (stm != null) {
stm.close();
}
if(conn!=null){
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
修改DefaultSqlSession中的selectList方法
@Override
public List selectList(String statement) {
//获取对应的Mapper
Mapper mapper = cfg.getMappers().get(statement);
//JDBC操作流程实现
if(mapper!=null){
return Executor.list(cfg.getConnection(), mapper);
}
return null;
}
通过本文,我们对mybatis源码做了深入的剖析,使用了工厂模式、构建者模式、动态代理模式、DOM4J解析xml,反射、数据库元数据等等知识实现了对mybatis的自定义。相信假以时日,我们都能对框架做到知其然知其所以然。