大牛教你怎么用Mybatis底层代码实现Mybatis功能

粉丝福利在后面java

导语:面试

最近研究了一下Mybatis的底层代码,写了一个操做数据库的小工具,实现了Mybatis的部分功能:sql

1.SQL语句在mapper.xml中配置。数据库

2.支持int,String,自定义数据类型的入参。编程

3.根据mapper.xml动态建立接口的代理实现对象。性能优化

功能有限,目的是搞清楚MyBatis框架的底层思想,多学习研究优秀框架的实现思路,对提高本身的编码能力大有裨益。bash

小工具使用到的核心技术点: xml解析+反射+jdk动态代理mybatis

接下来,一步一步来实现。架构

首先来讲为何要使用jdk动态代理。并发

传统的开发方式:

1.接口定义业务方法。

2.实现类实现业务方法。

3.实例化实现类对象来完成业务操做。

接口:

public interface UserDAO {
 public User get(int id);
}
复制代码

实现类:

public class UserDAOImpl implements UserDAO{
 @Override
 public User get(int id) {
 Connection conn = JDBCTools.getConnection();
 String sql = "select * from user where id = ?";
 PreparedStatement pstmt = null;
 ResultSet rs = null;
 try {
 pstmt = conn.prepareStatement(sql);
 pstmt.setInt(1, id);
 rs = pstmt.executeQuery();
 if(rs.next()){
 int sid = rs.getInt(1);
 String name = rs.getString(2);
 User user = new User(sid,name);
 return user;
 }
 } catch (Exception e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }finally{
 JDBCTools.release(conn, pstmt, rs);
 }
 return null;
 }
}
复制代码

测试:

public static void main(String[] args) {
 UserDAO userDAO = new UserDAOImpl();
 User user = userDAO.get(1);
 System.out.println(user);
 }
复制代码

Mybatis的方式:

1.开发者只须要建立接口,定义业务方法。

2. 不须要建立实现类。

3.具体的业务操做经过配置xml来完成。

接口:

public interface StudentDAO {
 public Student getById(int id);
 public Student getByStudent(Student student);
 public Student getByName(String name);
 public Student getByStudent2(Student student);
}
复制代码

StudentDAO.xml:

<?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="com.southwind.dao.StudentDAO"> 
 <select id="getById" parameterType="int" 
 resultType="com.southwind.entity.Student">
 select * from student where id=#{id}
 </select>
 <select id="getByStudent" parameterType="com.southwind.entity.Student" 
 resultType="com.southwind.entity.Student">
 select * from student where id=#{id} and name=#{name}
 </select>
 <select id="getByStudent2" parameterType="com.southwind.entity.Student" 
 resultType="com.southwind.entity.Student">
 select * from student where name=#{name} and tel=#{tel} 
 </select>
 <select id="getByName" parameterType="java.lang.String" 
 resultType="com.southwind.entity.Student">
 select * from student where name=#{name}
 </select>
</mapper>
复制代码

测试:

public static void main(String[] args) {
 StudentDAO studentDAO = (StudentDAO) new MyInvocationHandler().getInstance(StudentDAO.class);
 Student stu = studentDAO.getById(1);
 System.out.println(stu);
 }
复制代码

经过以上代码能够看到, MyBatis的方式省去了实现类的建立,改成用xml来定义业务方法的具体实现。

那么问题来了。

咱们知道Java是面向对象的编程语言, 程序在运行时执行业务方法,必需要有实例化的对象。 可是,接口是不能被实例化的,并且也没有接口的实现类,那么此时这个对象从哪来呢?

程序在运行时,动态建立代理对象。

即jdk动态代理,运行时结合接口和mapper.xml来动态建立一个代理对象,程序调用该代理对象的方法来完成业务。

如何使用jdk动态代理?

建立一个类,实现InvocationHandler接口,该类就具有了建立动态代理对象的功能。

两个核心方法:

1.自定义getInstance方法:入参为目标对象,经过Proxy.newProxyInstance方法建立代理对象,并返回。

public Object getInstance(Class cls){
 Object newProxyInstance = Proxy.newProxyInstance( 
 cls.getClassLoader(), 
 new Class[] { cls }, 
 this); 
 return (Object)newProxyInstance;
 }
复制代码

2.实现接口的invoke方法,经过反射机制完成业务逻辑代码。

@Override
 public Object invoke(Object proxy, Method method, Object[] args)
 throws Throwable {
 // TODO Auto-generated method stub
 return null;
 }
复制代码

invoke方法是核心代码,在该方法中实现具体的业务需求。接下来咱们来看如何实现。

既然是对数据库进行操做,则必定须要数据库链接对象,数据库相关信息配置在config.xml中。

因此invoke方法第一步,就是要解析config.xml,建立数据库链接对象,使用C3P0数据库链接池。

//读取C3P0数据源配置信息
 public static Map<String,String> getC3P0Properties(){
 Map<String,String> map = new HashMap<String,String>();
 SAXReader reader = new SAXReader();
 try {
 Document document = reader.read("src/config.xml");
 //获取根节点
 Element root = document.getRootElement();
 Iterator iter = root.elementIterator();
 while(iter.hasNext()){
 Element e = (Element) iter.next();
 //解析environments节点
 if("environments".equals(e.getName())){
 Iterator iter2 = e.elementIterator();
 while(iter2.hasNext()){
 //解析environment节点
 Element e2 = (Element) iter2.next();
 Iterator iter3 = e2.elementIterator();
 while(iter3.hasNext()){
 Element e3 = (Element) iter3.next();
 //解析dataSource节点
 if("dataSource".equals(e3.getName())){
 if("POOLED".equals(e3.attributeValue("type"))){
 Iterator iter4 = e3.elementIterator();
 //获取数据库链接信息
 while(iter4.hasNext()){
 Element e4 = (Element) iter4.next();
 map.put(e4.attributeValue("name"),e4.attributeValue("value"));
 }
 }
 }
 }
 }
 }
 }
 } catch (Exception e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 return map; 
 }
//获取C3P0信息,建立数据源对象
Map<String,String> map = ParseXML.getC3P0Properties();
ComboPooledDataSource datasource = new ComboPooledDataSource();
datasource.setDriverClass(map.get("driver"));
datasource.setJdbcUrl(map.get("url"));
datasource.setUser(map.get("username"));
datasource.setPassword(map.get("password"));
datasource.setInitialPoolSize(20);
datasource.setMaxPoolSize(40);
datasource.setMinPoolSize(2);
datasource.setAcquireIncrement(5);
Connection conn = datasource.getConnection();
复制代码

有了数据库链接,接下来就须要获取待执行的SQL语句,SQL的定义所有写在StudentDAO.xml中,继续解析xml,执行SQL语句。

SQL执行完毕,查询结果会保存在ResultSet中,还须要将ResultSet对象中的数据进行解析,封装到JavaBean中返回。

两步完成:

1.反射机制建立Student对象。

2.经过反射动态执行类中全部属性的setter方法,完成赋值。

这样就将ResultSet中的数据封装到JavaBean中了。

//获取sql语句
String sql = element.getText();
//获取参数类型
String parameterType = element.attributeValue("parameterType");
//建立pstmt
PreparedStatement pstmt = createPstmt(sql,parameterType,conn,args);
ResultSet rs = pstmt.executeQuery();
if(rs.next()){
 //读取返回数据类型
 String resultType = element.attributeValue("resultType"); 
 //反射建立对象
 Class clazz = Class.forName(resultType);
 obj = clazz.newInstance();
 //获取ResultSet数据
 ResultSetMetaData rsmd = rs.getMetaData();
 //遍历实体类属性集合,依次将结果集中的值赋给属性
 Field[] fields = clazz.getDeclaredFields();
 for(int i = 0; i < fields.length; i++){
 Object value = setFieldValueByResultSet(fields[i],rsmd,rs);
 //经过属性名找到对应的setter方法
 String name = fields[i].getName();
 name = name.substring(0, 1).toUpperCase() + name.substring(1);
 String MethodName = "set"+name;
 Method methodObj = clazz.getMethod(MethodName,fields[i].getType());
 //调用setter方法完成赋值
 methodObj.invoke(obj, value);
 }
}
复制代码

代码的实现大体思路如上所述,具体实现起来有不少细节须要处理。 使用到两个自定义工具类:ParseXML,MyInvocationHandler。

完整代码:

ParseXML

public class ParseXML {
 //读取C3P0数据源配置信息
 public static Map<String,String> getC3P0Properties(){
 Map<String,String> map = new HashMap<String,String>();
 SAXReader reader = new SAXReader();
 try {
 Document document = reader.read("src/config.xml");
 //获取根节点
 Element root = document.getRootElement();
 Iterator iter = root.elementIterator();
 while(iter.hasNext()){
 Element e = (Element) iter.next();
 //解析environments节点
 if("environments".equals(e.getName())){
 Iterator iter2 = e.elementIterator();
 while(iter2.hasNext()){
 //解析environment节点
 Element e2 = (Element) iter2.next();
 Iterator iter3 = e2.elementIterator();
 while(iter3.hasNext()){
 Element e3 = (Element) iter3.next();
 //解析dataSource节点
 if("dataSource".equals(e3.getName())){
 if("POOLED".equals(e3.attributeValue("type"))){
 Iterator iter4 = e3.elementIterator();
 //获取数据库链接信息
 while(iter4.hasNext()){
 Element e4 = (Element) iter4.next();
 map.put(e4.attributeValue("name"),e4.attributeValue("value"));
 }
 }
 }
 }
 }
 }
 }
 } catch (Exception e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 return map; 
 }
 //根据接口查找对应的mapper.xml
 public static String getMapperXML(String className){
 //保存xml路径
 String xml = "";
 SAXReader reader = new SAXReader();
 Document document;
 try {
 document = reader.read("src/config.xml");
 Element root = document.getRootElement();
 Iterator iter = root.elementIterator();
 while(iter.hasNext()){
 Element mappersElement = (Element) iter.next();
 if("mappers".equals(mappersElement.getName())){
 Iterator iter2 = mappersElement.elementIterator();
 while(iter2.hasNext()){
 Element mapperElement = (Element) iter2.next();
 //com.southwin.dao.UserDAO . 替换 #
 className = className.replace(".", "#");
 //获取接口结尾名
 String classNameEnd = className.split("#")[className.split("#").length-1];
 String resourceName = mapperElement.attributeValue("resource");
 //获取resource结尾名
 String resourceName2 = resourceName.split("/")[resourceName.split("/").length-1];
 //UserDAO.xml . 替换 #
 resourceName2 = resourceName2.replace(".", "#");
 String resourceNameEnd = resourceName2.split("#")[0];
 if(classNameEnd.equals(resourceNameEnd)){
 xml="src/"+resourceName;
 }
 }
 }
 }
 } catch (DocumentException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 return xml;
 }
}
复制代码

MyInvocationHandler:

public class MyInvocationHandler implements InvocationHandler{
 private String className;
 public Object getInstance(Class cls){
 //保存接口类型
 className = cls.getName();
 Object newProxyInstance = Proxy.newProxyInstance( 
 cls.getClassLoader(), 
 new Class[] { cls }, 
 this); 
 return (Object)newProxyInstance;
 }
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
 SAXReader reader = new SAXReader();
 //返回结果
 Object obj = null;
 try {
 //获取对应的mapper.xml
 String xml = ParseXML.getMapperXML(className);
 Document document = reader.read(xml);
 Element root = document.getRootElement();
 Iterator iter = root.elementIterator();
 while(iter.hasNext()){
 Element element = (Element) iter.next();
 String id = element.attributeValue("id");
 if(method.getName().equals(id)){
 //获取C3P0信息,建立数据源对象
 Map<String,String> map = ParseXML.getC3P0Properties();
 ComboPooledDataSource datasource = new ComboPooledDataSource();
 datasource.setDriverClass(map.get("driver"));
 datasource.setJdbcUrl(map.get("url"));
 datasource.setUser(map.get("username"));
 datasource.setPassword(map.get("password"));
 datasource.setInitialPoolSize(20);
 datasource.setMaxPoolSize(40);
 datasource.setMinPoolSize(2);
 datasource.setAcquireIncrement(5);
 Connection conn = datasource.getConnection();
 //获取sql语句
 String sql = element.getText();
 //获取参数类型
 String parameterType = element.attributeValue("parameterType");
 //建立pstmt
 PreparedStatement pstmt = createPstmt(sql,parameterType,conn,args);
 ResultSet rs = pstmt.executeQuery();
 if(rs.next()){
 //读取返回数据类型
 String resultType = element.attributeValue("resultType"); 
 //反射建立对象
 Class clazz = Class.forName(resultType);
 obj = clazz.newInstance();
 //获取ResultSet数据
 ResultSetMetaData rsmd = rs.getMetaData();
 //遍历实体类属性集合,依次将结果集中的值赋给属性
 Field[] fields = clazz.getDeclaredFields();
 for(int i = 0; i < fields.length; i++){
 Object value = setFieldValueByResultSet(fields[i],rsmd,rs);
 //经过属性名找到对应的setter方法
 String name = fields[i].getName();
 name = name.substring(0, 1).toUpperCase() + name.substring(1);
 String MethodName = "set"+name;
 Method methodObj = clazz.getMethod(MethodName,fields[i].getType());
 //调用setter方法完成赋值
 methodObj.invoke(obj, value);
 }
 }
 conn.close();
 }
 }
 } catch (Exception e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 return obj;
 }
 /**
 * 根据条件建立pstmt
 * @param sql
 * @param parameterType
 * @param conn
 * @param args
 * @return
 * @throws Exception
 */
 public PreparedStatement createPstmt(String sql,String parameterType,Connection conn,Object[] args) throws Exception{
 PreparedStatement pstmt = null;
 try {
 switch(parameterType){
 case "int":
 int start = sql.indexOf("#{");
 int end = sql.indexOf("}");
 //获取参数占位符 #{name}
 String target = sql.substring(start, end+1);
 //将参数占位符替换为?
 sql = sql.replace(target, "?");
 pstmt = conn.prepareStatement(sql);
 int num = Integer.parseInt(args[0].toString());
 pstmt.setInt(1, num);
 break;
 case "java.lang.String":
 int start2 = sql.indexOf("#{");
 int end2 = sql.indexOf("}");
 String target2 = sql.substring(start2, end2+1);
 sql = sql.replace(target2, "?");
 pstmt = conn.prepareStatement(sql);
 String str = args[0].toString();
 pstmt.setString(1, str);
 break;
 default:
 Class clazz = Class.forName(parameterType);
 Object obj = args[0];
 boolean flag = true;
 //存储参数
 List<Object> values = new ArrayList<Object>();
 //保存带#的sql
 String sql2 = "";
 while(flag){
 int start3 = sql.indexOf("#{");
 //判断#{}是否替换完成
 if(start3<0){
 flag = false;
 break;
 }
 int end3 = sql.indexOf("}");
 String target3 = sql.substring(start3, end3+1);
 //获取#{}的值 如#{name}拿到name
 String name = sql.substring(start3+2, end3);
 //经过反射获取对应的getter方法
 name = name.substring(0, 1).toUpperCase() + name.substring(1);
 String MethodName = "get"+name;
 Method methodObj = clazz.getMethod(MethodName);
 //调用getter方法完成赋值
 Object value = methodObj.invoke(obj);
 values.add(value);
 sql = sql.replace(target3, "?");
 sql2 = sql.replace("?", "#");
 }
 //截取sql2,替换参数
 String[] sqls = sql2.split("#");
 pstmt = conn.prepareStatement(sql);
 for(int i = 0; i < sqls.length-1; i++){
 Object value = values.get(i);
 if("java.lang.String".equals(value.getClass().getName())){
 pstmt.setString(i+1, (String)value);
 }
 if("java.lang.Integer".equals(value.getClass().getName())){
 pstmt.setInt(i+1, (Integer)value);
 }
 }
 break;
 }
 } catch (SQLException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 return pstmt;
 }
 /**
 * 根据将结果集中的值赋给对应的属性
 * @param field
 * @param rsmd
 * @param rs
 * @return
 */
 public Object setFieldValueByResultSet(Field field,ResultSetMetaData rsmd,ResultSet rs){
 Object result = null;
 try {
 int count = rsmd.getColumnCount();
 for(int i=1;i<=count;i++){
 if(field.getName().equals(rsmd.getColumnName(i))){
 String type = field.getType().getName();
 switch (type) {
 case "int":
 result = rs.getInt(field.getName());
 break;
 case "java.lang.String":
 result = rs.getString(field.getName());
 break;
 default:
 break;
 }
 }
 }
 } catch (SQLException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 return result;
 }
}
复制代码

代码测试:

StudnetDAO.getById

public static void main(String[] args) {
 StudentDAO studentDAO = (StudentDAO) new MyInvocationHandler().getInstance(StudentDAO.class);
 Student stu = studentDAO.getById(1);
 System.out.println(stu);
 }
复制代码

代码中的studentDAO为动态代理对象,此对象经过 MyInvocationHandler().getInstance(StudentDAO.class)方法动态建立, 而且结合StudentDAO.xml实现了StudentDAO接口的所有方法,直接调用studentDAO对象的方法便可完成业务需求。

StudnetDAO.getByName

public static void main(String[] args) {
 StudentDAO studentDAO = (StudentDAO) new MyInvocationHandler().getInstance(StudentDAO.class);
 Student stu = studentDAO.getByName("李四");
 System.out.println(stu);
 }
复制代码

StudnetDAO.getByStudent(根据id和name查询)

public static void main(String[] args) {
 StudentDAO studentDAO = (StudentDAO) new MyInvocationHandler().getInstance(StudentDAO.class);
 Student student = new Student();
 student.setId(1);
 student.setName("张三");
 Student stu = studentDAO.getByStudent(student);
 System.out.println(stu);
 }
复制代码

StudnetDAO.getByStudent2(根据name和tel查询)

public static void main(String[] args) {
 StudentDAO studentDAO = (StudentDAO) new MyInvocationHandler().getInstance(StudentDAO.class);
 Student student = new Student();
 student.setName("李四");
 student.setTel("18367895678");
 Student stu = studentDAO.getByStudent2(student);
 System.out.println(stu);
 }
复制代码

以上就是仿MyBatis实现自定义小工具的大体思路,细节之处还需具体查看源码,最后附上小工具源码连接。

源码:

连接: https://pan.baidu.com/s/ 1pMz0FDh

密码: fnjb

粉丝福利

分布式架构资料

面试资料

源码分析

架构师进阶

Java书籍

领取资料 转发+收藏+关注我+Java技术交流群:710373545里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多!。

相关文章
相关标签/搜索