手写一个简化版Mybatis

1、引包

引入dom4j包以及数据库连接包,我用的是mysql数据库,因此引入mysql-connector包

2、数据库创建

数据库比较简单,创建sql如下

CREATE DATABASE db_test;

use db_test;

CREATE TABLE `tb_user` (
  `id` int(11) NOT NULL,
  `name` varchar(20) NOT NULL,
  `age` tinyint(4) DEFAULT '0',
  `addr` varchar(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

然后随便插入几条数据

+----+---------+------+-----------+
| id | name    | age  | addr      |
+----+---------+------+-----------+
|  1 | Kurozaki|   19 | Guangdong |
|  2 | Kanako  |   20 | Japan     |
|  3 | Lee     |   20 | Malaysia  |
|  4 | Kintoki |   28 | Shenzhen  |
+----+---------+------+-----------+

3、模仿Mybatis编写mapper.xml



    

    
        update tb_user set name = ?
        where id = ?
    

    
        insert into tb_user
        values(?, ?, ?, ?);
    

4、声明查询接口与实体类

package test.dao;

import test.entity.User;

/**
 * Created by YotWei on 2018/8/6.
 */
public interface UserDao {

    User getUserInfo(int id);

    int updateUserName(String newName, int id);

    int insertUser(int id, String name, int age, String addr);
}
package test.entity;

/**
 * Created by YotWei on 2018/8/6.
 */
public class User {
    private int id;
    private String name;
    private int age;
    private String addr;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }


    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", addr='" + addr + '\'' +
                '}';
    }
}

5、读取mapper文件

mapper文件作用是管理sql语句与接口方法的映射,在使用Mybatis框架的时候,会先从mapper中读取映射信息,包括接口名,方法名,查询返回的数据类型,SQL语句的内容等等,MapperInfo定义如下

package com.yotwei.core;

/**
 * Created by YotWei on 2018/8/6.
 */
public class MapperInfo {

    private QueryType queryType;
    private String interfaceName;
    private String methodName;
    private String sql;
    private String resultType;

    public QueryType getQueryType() {
        return queryType;
    }

    public void setQueryType(QueryType queryType) {
        this.queryType = queryType;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }

    public String getInterfaceName() {
        return interfaceName;
    }

    public void setInterfaceName(String interfaceName) {
        this.interfaceName = interfaceName;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    @Override
    public String toString() {
        return "MapperInfo{" +
                "queryType=" + queryType +
                ", interfaceName='" + interfaceName + '\'' +
                ", methodName='" + methodName + '\'' +
                ", sql='" + sql + '\'' +
                ", resultType='" + resultType + '\'' +
                '}';
    }
}

其中QueryType是一个枚举类型

package com.yotwei.core;

/**
 * Created by YotWei on 2018/8/6.
 */
public enum QueryType {
    SELECT, UPDATE, INSERT, DELETE;

    public static QueryType value(String v) {
        return valueOf(v.toUpperCase());
    }
}

下面是用一个类来读取mapper的信息,这个类可以用枚举单例实现

package com.yotwei.core;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by YotWei on 2018/8/6.
 */
public enum SqlMappersHolder {

    INSTANCE;

    private Map mi = null;

    SqlMappersHolder() {
        if (mi != null)
            return;
        mi = new HashMap<>();

        File dir = new File(SqlMappersHolder.class
                .getClassLoader()
                .getResource(Config.DEFAULT.getMapperPath())
                .getFile());

        // 用dom4j解析
        SAXReader reader = new SAXReader();
        try {
            for (String file : dir.list()) {
                Document doc = reader.read(new File(dir, file));
                Element root = doc.getRootElement();
                String className = root.attributeValue("namespace");

                for (Object o : root.elements()) {
                    Element e = (Element) o;

                    MapperInfo info = new MapperInfo();
                    info.setQueryType(QueryType.value(e.getName()));
                    info.setInterfaceName(className);
                    info.setMethodName(e.attributeValue("id"));
                    info.setResultType(e.attributeValue("resultType"));
                    info.setSql(e.getText());

                    mi.put(idOf(className, e.attributeValue("id")), info);
                }
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    public MapperInfo getMapperInfo(String className, String methodName) {
        return mi.get(idOf(className, methodName));
    }

    /*
     * 类名+"."+方法名作为唯一id
     */
    private String idOf(String className, String methodName) {
        return className + "." + methodName;
    }
}

6、动态代理创建一个查询接口

SqlSession提供一个getMapper方法来获取一个DAO接口,DAO由代理类动态创建,传入一个核心的Sql执行类SqlExecuteHandler,该类实现InvocationHandler接口

package com.yotwei.core;

import java.lang.reflect.Proxy;

/**
 * Created by YotWei on 2018/8/6.
 */
public class SqlSession {

    @SuppressWarnings("unchecked")
    public  T getMapper(Class cls) {
        return (T) Proxy.newProxyInstance(cls.getClassLoader(),
                new Class[]{cls},
                new SqlExecuteHandler());
    }
}

SqlExecuteHandler的代码如下,它的主要任务有

1、在invoke方法中,根据传入的方法类获取接口名与方法名,进而通过SqlMappersHolder获取MapperInfo

2、根据配置连接数据库,获取到一个PreparedStatement对象

3、结合MapperInfo和参数列表设置PreparedStatement的参数,执行

4、获取执行结果,通过反射技术将查询结果映射到对应的实体类

package com.yotwei.core;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * Created by YotWei on 2018/8/6.
 */
public class SqlExecuteHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // get mapper info
        MapperInfo info = getMapperInfo(method);

        // execute sql
        return executeSql(info, args);
    }

    private MapperInfo getMapperInfo(Method method) throws Exception {
        MapperInfo info = SqlMappersHolder.INSTANCE.getMapperInfo(
                method.getDeclaringClass().getName(),
                method.getName());
        if (info == null) {
            throw new Exception("Mapper not found for method: " +
                    method.getDeclaringClass().getName() + "." + method.getName());
        }
        return info;
    }

    private Object executeSql(MapperInfo info, Object[] params)
            throws SQLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Object result = null;
        PreparedStatement pstat = ConnectionManager.get().prepareStatement(info.getSql());
        for (int i = 0; i < params.length; i++) {
            pstat.setObject(i + 1, params[i]);
        }


        if (info.getQueryType() == QueryType.SELECT) {
            ResultSet rs = pstat.executeQuery();
            rs.first();
            // 将查询结果映射为Java类或基本数据类型)
            // 目前简化版仅支持String和int两种类型
            if (rs.getMetaData().getColumnCount() == 1) {
                switch (info.getResultType()) {
                    case "int":
                        result = rs.getInt(1);
                        break;
                    default:
                        result = rs.getString(1);
                }
            } else {
                Class resTypeClass = Class.forName(info.getResultType());
                Object inst = resTypeClass.newInstance();
                for (Field field : resTypeClass.getDeclaredFields()) {
                    String setterName = "set" +
                            field.getName().substring(0, 1).toUpperCase() +
                            field.getName().substring(1);
                    Method md;

                    switch (field.getType().getSimpleName()) {
                        case "int":
                            md = resTypeClass.getMethod(setterName, new Class[]{int.class});
                            md.invoke(inst, rs.getInt(field.getName()));
                            break;

                        default:
                            md = resTypeClass.getMethod(setterName, new Class[]{String.class});
                            md.invoke(inst, rs.getString(field.getName()));
                    }
                }
                result = inst;
            }
        } else {
            result = pstat.executeUpdate();
        }
        pstat.close();
        return result;
    }

}

 

其中ConnectionManager的逻辑就是获取到一个Connection,我的逻辑比较简单,可以改用更好的方法替代,例如使用c3p0连接池。

package com.yotwei.core;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * Created by YotWei on 2018/8/7.
 */
public class ConnectionManager {

    public static Connection get() throws SQLException {
        return DriverManager.getConnection(
                Config.DEFAULT.getUrl(),
                Config.DEFAULT.getUser(),
                Config.DEFAULT.getPwd()
        );
    }
}

Config也是我自己定义的,主要就是放一些配置,我写死在代码里了,可以改用读取配置文件的方式

package com.yotwei.core;

/**
 * Created by YotWei on 2018/8/6.
 */
public class Config {

    public static final Config DEFAULT = new Config();

    private Config() {

    }

    private String url = "jdbc:mysql://localhost/db_test";
    private String user = "root";
    private String pwd = "root";

    private String mapperPath = "mapper/";

    public String getUrl() {
        return url;
    }

    public String getUser() {
        return user;
    }

    public String getPwd() {
        return pwd;
    }

    public String getMapperPath() {
        return mapperPath;
    }
}

7、测试

测试类如下

package test;

import com.yotwei.core.*;
import test.dao.UserDao;


/**
 * Created by YotWei on 2018/8/6.
 */
public class TestClient {

    public static void main(String[] args) {

        SqlSessionFactory factory = new SqlSessionFactory();
        SqlSession sqlSession = factory.openSession();

        UserDao userDao = sqlSession.getMapper(UserDao.class);

        System.out.println(userDao.getUserInfo(3));
    }
}
User{id=3, name='Lee', age=20, addr='Malaysia'}

可以看到代理类成功创建,并且查询后成功映射了

源码

源码在Github上 

https://github.com/Kurozaki/mybatis-simple

你可能感兴趣的:(Java,设计模式,mybatis,反射,动态代理,单例模式,xml解析)