记一次手动实现mybatis框架findAll功能
Mybatis是基于持久层开发的框架,它内部封装了JDBC,开发者不需要将精力放在数据库连接这方面,只需要把关注点放在sql语句的编写上。Mybatis通过xml或者注解的方式将要执行的各种statement对象配置起来,并且通过java对象和statement中的sql的动态参数进行映射生成sql语句。最后由Mybatis框架执行sql语句并将结果集对象通过反射技术封装成javabean对象返回。Mybatis采用ORM思想(object relational mapping)将java实体和数据库映射结合。框架内部封装了jdbc底层技术。
开发前准备
导入依赖jar包
<dependencies>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.36</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.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
表结构
+----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| username | varchar(255) | YES | | NULL | |
| password | varchar(255) | YES | | NULL | |
+----------+--------------+------+-----+---------+-------+
javaBean
public class User{
private String username;
private String password;
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
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;
}
}
接口Mapper和Mapper文件准备
package mapper;
import domain.User;
import java.util.List;
public interface UserMapper {
/**
* 查询所有用户的方法
* @return
*/
List<User> findAll();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.UserMapper">
<select id="findAll" resultType="domain.User">
SELECT * FROM user
</select>
</mapper>
编写测试类
import domain.User;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class CustomerMyBatisTest {
@Test
public void testFindAll() {
//1.将主配置文件转换成字节输入流
SqlSession sqlSession = null;
InputStream is = null;
try {
is = Resources.getResourceAsStream("SqlMapConfig.xml");
System.out.println(is);
//2.创建一个SqlSessionFactoryBuilder的对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.构建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = builder.build(is);//使用了构建者模式
//4.创建SqlSession对象
sqlSession = sqlSessionFactory.openSession();
//5.通过SqlSession对象,创建UserMapper接口的代理对象----->动态代理
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//6.调用mapper对象的findAll()方法
List<User> users = mapper.findAll();
for (User user : users) {
System.out.println(user);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//7.关闭资源
sqlSession.close();
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Mybatis配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--
数据库连接四要素
-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!--
加载映射文件
-->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
测试类中的所有接口类都是空实现,需要我们自己补充。
项目结构如下
SqlSessionFactory接口及其实现类DefaultSqlSessionFactory
SqlSession接口及其实现类DefalutSqlSession
sqlMapConfig Mybatis配置文件
Resources类负责读取类路径下资源文件,具体内容如下:
public class Resources {
public static InputStream getResourceAsStream(String path){
InputStream is = Resources.class.getClassLoader().getResourceAsStream(path);
return is;
}
}
项目准备完毕,接下来就是coding。。
读取sqlMapConfig获取jdbc连接
工具类XmlConfigBuilder
1、解析获取dateSource连接必须数据
public class XmlConfigBuilder {
//加载配置文件,解析相关信息,封装到Configuration对象返回
public static Configuration loadSqlMapConfig(InputStream is) {
SAXReader Reader = new SAXReader();//创建SAXReader对象解析XML
try {
Document document = Reader.read(is);//获取document对象
List<Element> propertyElement = document.selectNodes("//property");//通过Xpath技术解析property对象
解析以后可以获取Element元素集合,对应的name分别是:XML中配置的username,password,url、以及dirver
创建Configuration实体类负责接收
public class Configuration {
private String username;
private String password;
private String driver;
private String url;
//存放每个mapper文件对应的sql语句和resultType
private Map<String, Mapper> mappers = new HashMap<String, Mapper>();
Get/Set....
}
Configuration cfg = new Configuration();//创建Configuration对象
//遍历集合取出对应value
for (Element element : propertyElement) {
String name = element.attributeValue("name");
String value = element.attributeValue("value");
if ("username".equals(name)) {
cfg.setUsername(value);
}
if ("password".equals(name)) {
cfg.setPassword(value);
}
if ("url".equals(name)) {
cfg.setUrl(value);
}
if ("driver".equals(name)) {
cfg.setDirver(value);
}
2、解析获取映射文件路径
for (Element mapperElement : mapperElements) {
String resource = mapperElement.attributeValue("resource");//获取映射文件路径
Map<String, Mapper> mappers = loadMapper(resource);//通过配置文件加载每个映射文件,具体方法后面实现TODO
cfg.setMappers(mappers);//将获取的每一个mapper对象放入Configuration对象中存储,并且补充
}
}
return cfg;
} catch (DocumentException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
3、加载每个映射文件获取对应sql语句、resultType
//加载映射文件获取对应sql语句、resultType封装Mapper对象集合返回Map<namespace.id,Mapper对象>
private static Map<String, Mapper> loadMapper(String resource) {
Map<String, Mapper> mappers = new HashMap<String, Mapper>();
InputStream is = Resources.getResourceAsStream(resource);//通过Resources对象将Mapper.xml文件转为流
SAXReader reader = new SAXReader();//创建SAXReader对象
try {
Document document = reader.read(is);//获取document对象
Element rootElement = document.getRootElement();//获得Mapper.xml文件根标签及<mapper namespace="">标签
String namespace = rootElement.attributeValue("namespace");//获取namespace
List<Element> selectElements = document.selectNodes("//select");//获取select标签集合
//遍历集合取出每一个select标签的id和resultType
for (Element selectElement : selectElements) {
String id = selectElement.attributeValue("id");
String resultType = selectElement.attributeValue("resultType");
String sql = selectElement.getTextTrim();//获取标签中的内容及内部SQL语句
//封装mapper对象
Mapper mapper = new Mapper();
mapper.setSql(sql);
mapper.setResultType(resultType);
mappers.put(namespace + "." + id, mapper);//存入map集合
}
return mappers;
} catch (DocumentException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
4、sql语句返回值类型,及其对应的连接信息全部封装在Configuration对象中,补充Configuration对象中连接方法
public Connection getConnection() throws SQLException {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Connection conn = DriverManager.getConnection(url, username, password);
return conn;
}
SqlSessionFactory、SqlSession具体实现类方法实现
使用过mybatis就会发现,基本上所有的增删改查操作的执行都是交给SqlSession对象实现的。(sqlSession.getMapper())
也就是说获取连接也是SqlSession内部实现的,所有我们需要将Configuration对象给SqlSession实现类DefaultSqlSession类。
还需要解决几个问题
1)到底什么时候加载配置文件?
我们可以在在SqlSessionFactory的openSqlSession方法调用时加载配置文件
2)通过什么方式通知程序读取Mybatis配置文件(SqlMapConfig.xml)。
我们已经将配置文件转为流了,只需要在SqlSessionFactory调用build方法的时候将流传入即可,所以我们只需要将流传给SqlSessionFactory的实现类DefaultSqlSessionFacotory。
首先实现SqlSessionFactoryBuilder类
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream is) {
DefaultSqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory();
//创建SqlSessionFactory对象设置上流并返回
sqlSessionFactory.setIs(is);
return sqlSessionFactory;
}
}
DefaultSqlSessionFactory类实现
public class DefaultSqlSessionFactory implements SqlSessionFactory{
private InputStream is;
//在openSession方法内部创建SqlSession对象
public SqlSession openSession() {
//创建SqlSession对象
DefaultSqlSession sqlSession = new DefaultSqlSession();
//通过loadSqlMapConfig方法
Configuration cfg = XmlConfigBuilder.loadSqlMapConfig(is);
//给SqlSession对象设置Configuration对象
sqlSession.setCfg(cfg);
return sqlSession;
}
public void setIs(InputStream is) {
this.is = is;
}
}
DefalutSqlSession类实现
1、编写selectList()方法
//key:mapper.xml中select的方法的全限定名(namespace.id名)
public <E> List<E> selectList(String key) {
//获取连接对象
Connection conn = null;
try {
conn = cfg.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
//获取SQl语句
Map<String, Mapper> mappers = cfg.getMappers();
Mapper mapper = mappers.get(key);
String sql = mapper.getSql();//获取要执行的SQL语句
String resultType = mapper.getResultType();//JavaBean的全限定名
//预编译SQL语句
try {
PreparedStatement pstm = conn.prepareStatement(sql);
//执行Sql语句
ResultSet resultSet = pstm.executeQuery();
//通过反射技术将结果封装成javaBean对象,具体实现后面总结
List<E> list = Converser.converList(resultSet, Class.forName(resultType));
return list;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}finally {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
2、通过动态代理获取代理对象
public <E> E getMapper(Class<E> mapperClass) {
return (E) Proxy.newProxyInstance(mapperClass.getClassLoader(),new Class[]{mapperClass},new MapperProxyFactory(this));
}
Proxy的newProxyInstance方法中需要三个参数:classLoader类加载器,代理对象实现的接口的字节码对象也就是我们这里的传入的mapperClass对象,InvocationHandler接口的实现类对象。具体实现如下
public class MapperProxyFactory implements InvocationHandler{
private SqlSession sqlSession;
public MapperProxyFactory(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果返回值是一个List集合才执行如下代码,即findAll
Class<?> returnType = method.getReturnType();
if (returnType == List.class) {
String methodName = method.getName();//获取方法名
Class<?> clazz = method.getDeclaringClass();//获取接口字节码对象
String className = clazz.getName();//获取接口的全限定名
//key :mapper接口的全限定名
String key = className+"."+methodName;
List<Object> list = sqlSession.selectList(key);
return list;
}else {
//TODO..
return null;
}
}
}
3、Converser类的具体实现(反射将resultType映射成javaBean对象)
public static <E> List<E> converList(ResultSet set, Class clazz){
List<E> javaBeans = new ArrayList<E>();
//1.遍历结果集
try {
//根据结果集元数据,获取结果集中的每一列的列名
ResultSetMetaData metaData = set.getMetaData();
int columnCount = metaData.getColumnCount();//获取总列数
while (set.next()){
//每次遍历,遍历出一条数据,每条数据就对应一个JavaBean对象
E o = (E) clazz.newInstance();
//根据列名获取每一列数据
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);
}
javaBeans.add(o);
}
} catch (Exception e) {
e.printStackTrace();
}
return javaBeans;
}