粉絲福利在後面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性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!。