MyBatis是一个持久层的框架,使用Java语言书写的,它封装了很多jdbc操作的细节,使开发者只用关注SQL语句本身,无需关注注册驱动,创建连接等过程,它使用ORM思想实现结果集的封装。下面我们讨论一下细节。(注:ORM,Object Relational Mapping对象关系映射,含义:将数据库表和Java实体类及实体类的属性对应,使我们操作实体类就可以操作数据库表,做到这点需要实体类中的属性和数据库表的字段名保持一致。)
从MyBatis的使用看
想知道MyBatis内部的工作流程,我们从使用MyBatis进行查询所有的过程看,首先看配置MyBatis的环境,分为以下几步:
创建Maven工程导入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> </dependencies>
|
创建实体类和dao层接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.jinqi.dao;
import com.jinqi.domain.User; import com.jinqi.mybatis.annotations.Select;
import java.util.List;
public interface UserDao {
@Select("select * from user") List<User> findAll(); }
|
创建MyBatis的主配置文件SqlMapConfig.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <?xml version="1.0" encoding="UTF-8" ?>
<configuration> <environments default="mysql"> <environment id="mysql"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/MyBatis"/> <property name="username" value="root"/> <property name="password" value="Jq576163960"/> </dataSource> </environment> </environments>
<mappers> <mapper class="com.jinqi.dao.UserDao"></mapper>
</mappers> </configuration>
|
创建映射配置文件UserDao.xml
1 2 3 4 5 6 7 8
| <?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.jinqi.dao.UserDao"> <select id="findAll" resultType="com.jinqi.domain.User"> select * from user </select> </mapper>
|
下面我们编写测试类具体看执行的过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package com.jinqi.test;
import com.jinqi.dao.UserDao; import com.jinqi.domain.User; import com.jinqi.mybatis.sqlsession.SqlSession; import com.jinqi.mybatis.sqlsession.SqlSessionFactory; import com.jinqi.mybatis.sqlsession.SqlSessionFactoryBuilder; import java.io.InputStream; import java.util.List;
public class MyBatisTest {
public static void main(String[] args) throws Exception{
InputStream in = com.jinqi.mybatis.io.Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(in); SqlSession session = factory.openSession(); UserDao userDao = session.getMapper(UserDao.class); List<User> userList = userDao.findAll(); for (User user: userList) { System.out.println(user); } session.close(); in.close(); } }
|
分析代码执行过程开始自定义MyBatis
1.读取SqlMapConfig.xml配置文件方式:
- 使用类加载器,缺点是只能读取类路径下的文件
- 使用ServletContext对象的getRealPath
2.创建SqlSessionFactory,使用了构建者模式,利用SqlSessionFactoryBuilder创建的builder对象就是构建者。
3.创建SqlSession对象,使用了工厂模式,取缔new关键字,让工厂帮我们创建所需的对象,降低程序间的耦合。
4.创建dao接口实现类采用了代理模式,在不修改源代码的基础上,对已有的方法进行增强。
自定义MyBatis的实现
1.SqlMapConfig.xml:标签用于配置连接信息,创建Connection对象,标签用于映射配置文件UserDao.xml。
2.UserDao.xml:包含执行的select语句,获取PreparedStatement。
resultType:封装的实体类的全限定类名。
3.读取配置文件:这里使用的是解析xml文件的方式,使用的是dom4j进行解析xml,我们需要导入dom4j依赖。
1 2 3 4 5 6
| <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency>
|
我们创建一个Configuration实体类,用来封装连接信息,和有着sql语句与resultType的Map。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package com.jinqi.mybatis.cfg;
import java.util.HashMap; import java.util.Map;
public class Configuration {
private String driver; private String url; private String username; private String password;
private Map<String,Mapper> mappers = new HashMap<String, Mapper>();
public Map<String, Mapper> getMappers() { return mappers; }
public void setMappers(Map<String, Mapper> mappers) { this.mappers.putAll(mappers); }
public String getDriver() { return driver; }
public void setDriver(String driver) { this.driver = driver; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; } }
|
4.首先我们需要创建一个SqlSessionFactoryBuilder类,用于创建一个SqlSessionFactory工厂对象,具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.jinqi.mybatis.sqlsession;
import com.jinqi.mybatis.cfg.Configuration; import com.jinqi.mybatis.sqlsession.defaults.DefaultSqlSessionFactory; import com.jinqi.mybatis.utils.XMLConfigBuilder; import java.io.InputStream;
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream config){ Configuration cfg = XMLConfigBuilder.loadConfiguration(config); return new DefaultSqlSessionFactory(cfg); } }
|
在这边附上xml文件解析的工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
| package com.jinqi.mybatis.utils;
import com.jinqi.mybatis.cfg.Configuration; import com.jinqi.mybatis.cfg.Mapper; import com.jinqi.mybatis.io.Resources; import com.jinqi.mybatis.annotations.Select; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader;
import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.HashMap; import java.util.List; import java.util.Map;
public class XMLConfigBuilder {
public static Configuration loadConfiguration(InputStream config){ try{ Configuration cfg = new Configuration();
SAXReader reader = new SAXReader();
Document document = reader.read(config);
Element root = document.getRootElement();
List<Element> propertyElements = root.selectNodes("//property");
for (Element propertyElement : propertyElements){ String name = propertyElement.attributeValue("name"); if ("driver".equals(name)){ String driver = propertyElement.attributeValue("value"); cfg.setDriver(driver); } if ("url".equals(name)){ String url = propertyElement.attributeValue("value"); cfg.setUrl(url); } if ("username".equals(name)){ String username = propertyElement.attributeValue("value"); cfg.setUsername(username); } if ("password".equals(name)){ String password = propertyElement.attributeValue("value"); cfg.setPassword(password); } } List<Element> mapperElements = root.selectNodes("//mappers/mapper"); for (Element mapperElement:mapperElements){ Attribute attribute = mapperElement.attribute("resource"); if (attribute!=null){ System.out.println("使用的是xml"); String mapperPath = attribute.getValue(); Map<String,Mapper> mappers = loadMapperConfiguration(mapperPath); cfg.setMappers(mappers); }else { System.out.println("使用的是注解"); String daoClassPath = mapperElement.attributeValue("class"); Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath); cfg.setMappers(mappers); } } return cfg; }catch (Exception e){ throw new RuntimeException(e); }finally { try { config.close(); } catch (IOException e) { e.printStackTrace(); } } }
public static Map<String,Mapper> loadMapperConfiguration(String mapperPath) throws IOException{
InputStream in = null; try{ Map<String,Mapper> mappers = new HashMap<String, Mapper>(); in = Resources.getResourceAsStream(mapperPath); SAXReader reader = new SAXReader(); Document document = reader.read(in); Element root = document.getRootElement(); String namespace = root.attributeValue("namespace"); List<Element> selectElements = root.selectNodes("//select"); for (Element selectElement :selectElements){ String id = selectElement.attributeValue("id"); String resultType = selectElement.attributeValue("resultType"); String queryString = selectElement.getText(); String key = namespace+"."+id; Mapper mapper = new Mapper(); mapper.setQueryString(queryString); mapper.setResultType(resultType); mappers.put(key,mapper); } return mappers; }catch (Exception e){ throw new RuntimeException(e); }finally { in.close(); } }
public static Map<String,Mapper> loadMapperAnnotation(String daoClassPath) throws Exception{ Map<String,Mapper> mappers = new HashMap<>();
Class daoClass = Class.forName(daoClassPath); Method[] methods = daoClass.getMethods(); for (Method method: methods) { boolean isAnnotated = method.isAnnotationPresent(Select.class); if (isAnnotated){ Mapper mapper = new Mapper(); Select selectAnno = method.getAnnotation(Select.class); String queryString = selectAnno.value(); mapper.setQueryString(queryString); Type type = method.getGenericReturnType(); if (type instanceof ParameterizedType){ ParameterizedType ptype = (ParameterizedType)type; Type[] types = ptype.getActualTypeArguments(); Class domainClass = (Class) types[0]; String resultType = domainClass.getName(); mapper.setResultType(resultType); } String methodName = method.getName(); String className = method.getDeclaringClass().getName(); String key = className+"."+methodName; mappers.put(key,mapper); } } return mappers; }
}
|
5.由于我们定义的SqlSessionFactory工厂是一个接口,因此我们需要写一个SqlSessionFactory的实现类DefaultSqlSessionFactory用于创建一个新的操作数据库对象SqlSession。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.jinqi.mybatis.sqlsession.defaults;
import com.jinqi.mybatis.cfg.Configuration; import com.jinqi.mybatis.sqlsession.SqlSession; import com.jinqi.mybatis.sqlsession.SqlSessionFactory;
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration cfg;
public DefaultSqlSessionFactory(Configuration cfg) { this.cfg = cfg; }
@Override public SqlSession openSession() { return new DefaultSqlSession(cfg); } }
|
6.SqlSession是自定义MyBatis中和数据库交互的核心接口,它可以创建dao接口的代理对象,我们需要一个SqlSession的实现类,具体思路是根据dao接口的字节码创建dao接口的代理对象,而不是直接写dao接口的实现类,核心方法是getMapper方法,该方法在使用代理创建对象时,要传入三个参数,分别是:类加载器,使用和目标类相同的加载器;代理类要实现的接口和目标类要实现的接口相同;如何代理,即要扩展的功能,此处应该给一个InvocationHandler的实现类,在实现类中利用反射调用selectList方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package com.jinqi.mybatis.sqlsession.defaults;
import com.jinqi.mybatis.cfg.Configuration; import com.jinqi.mybatis.sqlsession.SqlSession; import com.jinqi.mybatis.sqlsession.proxy.MapperProxy; import com.jinqi.mybatis.utils.DataSourceUtil; import java.lang.reflect.Proxy; import java.sql.Connection; import java.sql.SQLException;
public class DefaultSqlSession implements SqlSession {
private Configuration cfg;
private Connection conn;
public DefaultSqlSession(Configuration cfg) { this.cfg = cfg; conn = DataSourceUtil.getConnection(cfg); }
@Override public <T> T getMapper(Class<T> daoInterfaceClass) { return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(),new Class[]{daoInterfaceClass},new MapperProxy(cfg.getMappers(),conn)); }
@Override public void close() { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| package com.jinqi.mybatis.sqlsession.proxy;
import com.jinqi.mybatis.cfg.Mapper; import com.jinqi.mybatis.utils.Executor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.sql.Connection; import java.util.Map;
public class MapperProxy implements InvocationHandler {
private Map<String,Mapper> mappers;
private Connection conn;
public MapperProxy(Map<String, Mapper> mappers,Connection conn) { this.mappers = mappers; this.conn=conn; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); String className = method.getDeclaringClass().getName(); String key = className+"."+methodName; Mapper mapper = mappers.get(key); if (mapper ==null){ throw new IllegalArgumentException("传入的参数有误!"); } return new Executor().selectList(mapper,conn); } }
|
最后我们再次执行测试类代码进行测试,同样能够得到需要的结果。
最后,附上完整源代码的github地址:https://github.com/jqdelove/Customize-MyBatis.git。