大牛教你怎麼用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性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!。

相關文章
相關標籤/搜索