手写mybatis框架(一)通过动态代理简单实现查询功能

git地址:点我

在手写mybatis简化版框架先了解一下mybatis框架的执行流程。

一、Mybatis框架执行流程

1.配置文件有两种,一个为主配置文件,一个为映射文件。

主配置文件:配置了jdbc等环境信息。

映射文件:配置了接口对应的sql语句映射。

这两个配置文件会被封装到Configuration中。

2.通过mybatis配置文件得到SqlSessionFactory。

3.通过SqlSessionFactory得到SqlSession,一个sqlSession相当于一个request请求。

4.SqlSession调用Executor执行器来操作数据库。

5.解析入参,封装成statement,执行,映射结果返回。

将mybatis主要流程分析了一下,思路明确了,那么设计一下简化版的mybatis框架来完成一个查询。

二、简化版ybatis

同上面过程,毕竟是简化版,就不使用sqlsession工厂了,跳过这一步。

即:1.读取xml->2.调用sqlsession->3.调用executor->4.解析参数执行并返回映射结果

先上结果图:

项目结构图:maven项目

1.连接数据库

<?xml version="1.0" encoding="UTF-8"?>
<dataSource>
    <property name="driverClassName">com.mysql.jdbc.Driver</property>
    <property name="url">jdbc:mysql://localhost:3306/test</property>
    <property name="username">root</property>
    <property name="password">root</property>
</dataSource>

内容为jdbc连接信息,哦忘了,先把pom文件展示一下,两个jar包:mysql连接和dom4j解析

 <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.29</version>
        </dependency>

接下来是通过代码将该xml文件解析出来。

//启动应用程序类加载器
    private static ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    /*
      解析xml文件
     */
    public Element parseXML(String resource) {
        try {
            //返回用于读取指定资源的输入流。
            InputStream resourceAsStream = classLoader.getResourceAsStream(resource);
            //使用dom4j方式解析
            SAXReader saxReader = new SAXReader();
            //使用SAX从给定流中读取文件
            Document document = saxReader.read(resourceAsStream);
            //获得文件的根节点
            Element rootElement = document.getRootElement();
            return rootElement;
        } catch (DocumentException e) {
            throw new RuntimeException("解析文件失败:"+resource);
        }
    }
    /*
    解析主配置xml文件节点
     */
    public Map<String, String> parseNode(Element element){
        //判断根目录名称
        if (!element.getName().equals("dataSource")) {
            throw new RuntimeException("主配置文件根名称必须是dataSource");
        }
        Map<String, String> map = new HashMap<String, String>();
        map.put("driverClassName", null);
        map.put("url", null);
        map.put("username", null);
        map.put("password", null);
        //读取property属性内容
        for (Object obj :element.elements("property")){
            Element ele = (Element) obj;
            String name = ele.attributeValue("name");
            String value =ele.getText();
            if (name==null||value==""){
                throw new RuntimeException("格式错误,正确格式为 <property name=\"xxx\">xxx</property>");
            }
            if (name.equals("driverClassName")){
                map.put("driverClassName",value);
            }else if(name.equals("url")){
                map.put("url",value);
            }else if (name.equals("username")){
                map.put("username",value);
            }else if(name.equals("password")){
                map.put("password",value);
            }else {
                throw new RuntimeException("不能识别的名称:"+name);
            }
        }
        return map;
    }
  public Connection build(String resource){
        Element element = parseXML(resource);
        Map<String,String> jdbcMap =parseNode(element);
        try {
            Class.forName(jdbcMap.get("driverClassName"));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("驱动类未找到!");
        }
        Connection connection=null;
        try {
            connection = DriverManager.getConnection(jdbcMap.get("url"),jdbcMap.get("username"),jdbcMap.get("password"));
        } catch (SQLException e) {
            throw new RuntimeException("jdbc连接错误!");
        }
        return connection;
    }
private Connection getConnection(){
        Connection connection=myParse.build("mybatis-config.xml");
        return connection;
    }

 从代码上看,该简化的ybatis主配置文件必须名为mybatis-config.xml

可以自己写一个测试类,看是否报错,没报错则进行下一步。

2.sqlSession代理

sqlSession肯定不会自己去执行,因为不能写死所以使用动态代理来使代理类去实现具体方法。

public class MySqlSession {

    private MyExcutor excutor = new MyExcutorImpl();   //待会实现
    private MyParse parse=new MyParse();
    public <T> T selectObject(Mapping mapping, String param){
      return  excutor.query(mapping,param);
    }
    public <T> T getMapper(Class<T> cls){  //待会实现
      return (T) Proxy.newProxyInstance(cls.getClassLoader(),new Class[]{cls},new MySqlSessionProxy(parse,this));  
    }

}

然后先写一点代理类,把mapper映射文件解析了。

/*
sqlSession代理类
 */
public class MySqlSessionProxy implements InvocationHandler {
    private MyParse parse;
    private MySqlSession sqlSession ;
    private String PATH="mapper/";

    public MySqlSessionProxy(MyParse parse,MySqlSession sqlSession){
        this.parse=parse;
        this.sqlSession=sqlSession;

    }
    public Object invoke(Object proxy, Method method, Object[] args) {
        String name =method.getDeclaringClass().getName();
        String mapperName=name.substring(name.lastIndexOf(".")+1);
        MappingBean mappingBean=parse.parseMapper(parse.parseXML(PATH+mapperName+".xml"));
       
        return null;
    }
}

从这里可以看到该简化的ybatis的映射文件必须和接口名保持一致,并且在mapper文件夹里。

mapper.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.chuan.mapper.UserMapper">
    <select id="getUserById" resultType="com.chuan.entity.User">
        SELECT * FROM user WHERE id = ?
    </select>
</mapper>

parseMapper()方法:

 /*
     解析mapper映射xml文件
     */
    public MappingBean parseMapper(Element element){
        //判断根目录名称
        if (!element.getName().equals("mapper")) {
            throw new RuntimeException("mapper映射文件根名称必须是mapper");
        }
        MappingBean mappingBean=new MappingBean();
        String namespace=element.attributeValue("namespace");
        if (namespace==null){
            throw new RuntimeException("mapper映射文件namespace不存在");
        }
        mappingBean.setInterfaceName(namespace);
        List<Mapping> mappingList = new ArrayList<Mapping>();
        Iterator it=element.elementIterator();
        while (it.hasNext()){
            Element ele=(Element) it.next();
            Mapping mapping =new Mapping();
            String funcName =ele.attributeValue("id");
            if (funcName==null){
                throw new RuntimeException("mapper映射文件中id不存在");
            }
            String sqlType = ele.getName();
            String paramType = ele.attributeValue("parameterType");
            String resultType=ele.attributeValue("resultType");
            String sql=ele.getText().trim();
            mapping.setFuncName(funcName);
            mapping.setSqlType(sqlType);
            mapping.setParamType(paramType);
            mapping.setSql(sql);
            Object object=null;
            try {
                object=Class.forName(resultType).newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            mapping.setResultType(object);
            mappingList.add(mapping);
        }
        mappingBean.setMappingList(mappingList);
        return mappingBean;
    }

这里就完成了对mapper映射文件的解析,接下来到下一步,进入executor执行。

3.executor执行

在写executor之前先创建两个实体类,一个是对应接口实体类,一个是xml文件sql实体类。

sql实体类:

public class Mapping {
    /*
    sql语句
     */
    private String sql;
    /*
    sql语句参数类型
     */
    private String sqlType;
    /*
    入参类型
     */
    private String paramType;
    /*
    返回参数类型
     */
    private Object resultType;
    /*
    方法名
     */
    private String funcName;
//getter、setter省略

}

接口实体类:

public class MappingBean {
    /*
    接口名
     */
    private String interfaceName;
    /*
    接口名下所有方法
     */
    private List<Mapping> mappingList;
//getter、setter省略
}

补齐代理类:

 public Object invoke(Object proxy, Method method, Object[] args) {
        String name =method.getDeclaringClass().getName();
        String mapperName=name.substring(name.lastIndexOf(".")+1);
        MappingBean mappingBean=parse.parseMapper(parse.parseXML(PATH+mapperName+".xml"));
        if (mappingBean!=null&&(mappingBean.getMappingList()!=null&&mappingBean.getMappingList().size()>0)){
            for (Mapping mapping :mappingBean.getMappingList()){
                //进入查询逻辑
                if (mapping.getSqlType().equals("select")){
                    if (mapping.getFuncName().equals(method.getName())){
                        System.out.println("执行查询方法:"+mapping.getSql());
                        System.out.println("参数:"+args[0]);
                        return sqlSession.selectObject(mapping,String.valueOf(args[0]));
                    }
                }
            }
        }
        return null;
    }

通过sqlSession.selectObject调用到了executor方法,通过这里看到该简化的ybatis传参只接收一个,并且是字符串。

executor方法:这里通过反射将结果转换成对象,但只做了整型和字符串的转换。

public class MyExcutorImpl implements MyExcutor {

    private MyParse myParse = new MyParse();

    public <T> T query(Mapping mapping, Object param) {
        Connection connection=getConnection();
        PreparedStatement preparedStatement=null;
        ResultSet resultSet=null;
        Object obj=null;
        List list=new ArrayList();
        try {
            preparedStatement=connection.prepareStatement(mapping.getSql());
            preparedStatement.setString(1,param.toString());
            if (mapping.getResultType()==null){
                throw new RuntimeException("返回的映射结果不能为空!");
            }
            resultSet = preparedStatement.executeQuery();
            int row = 0;
            ResultSetMetaData rd = resultSet.getMetaData();
            while (resultSet.next()){
                obj=resultToObject(resultSet,mapping.getResultType());
                row++;
                list.add(obj);
            }
            System.out.println("记录行数:"+row);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return (T)list;
    }

    private Connection getConnection(){
        Connection connection=myParse.build("mybatis-config.xml");
        return connection;
    }
    /*
    * 把得到的一列数据存入到一个对象中
	 */
    @SuppressWarnings("unchecked")
    private <T> T resultToObject(ResultSet rs,Object object) {
        Object obj=null;
        try {
        Class  cls=object.getClass();
        /*
        这里为什么要通过class再new一个对象,因为如果不new一个新的对象,每次返回的都是形参上的object,
        而这个object都是同一个,会导致list列表后面覆盖前面值。
         */
        obj=cls.newInstance();
        //获取结果集元数据(获取此 ResultSet 对象的列的编号、类型和属性。)
        ResultSetMetaData rd=rs.getMetaData();
        for (int i = 0; i < rd.getColumnCount(); i++) {
            //获取列名
            String columnName=rd.getColumnLabel(i+1);
            //组合方法名
            String methodName="set"+columnName.substring(0, 1).toUpperCase()+columnName.substring(1);
            //获取列类型
            int columnType=rd.getColumnType(i+1);
            Method method=null;
            switch(columnType) {
                case java.sql.Types.VARCHAR:
                case java.sql.Types.CHAR:
                    method=cls.getMethod(methodName, String.class);
                    if(method!=null) {
                        method.invoke(obj, rs.getString(columnName));
                    }
                    break;
                case java.sql.Types.INTEGER:
                    method=cls.getMethod(methodName, Integer.class);
                    if(method!=null) {
                        method.invoke(obj, rs.getInt(columnName));
                    }
                    break;
                default:
                    break;
            }
        }
        }  catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return (T) obj;
    }

}

4.测试

将该简化版的ybatis打成jar包。

将导出的jar包引入到本地仓库。

命令:根据自己实际路径和名称修改。

mvn install:install-file -DgroupId=com.my -DartifactId=ybatis -Dversion=1.0.0 -Dpackaging=jar -Dfile=C:\Users\chuan\Desktop\glzxCode\ybatis\target\ybatis-1.0-SNAPSHOT.jar

新建一个maven项目。

添加pom文件:引入jar包,因为打包并没有将引入的jar打进去,所以要重新引用。

 <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.29</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>com.my</groupId>
            <artifactId>ybatis</artifactId>
            <version>1.0.0</version>
        </dependency>

实体类:

public class User implements Serializable {
    private Integer id;
    private String username;
    private String password;
//getter、setter省略
}

接口:

public interface UserMapper {
    public List<User> getUserById(String id);
}

映射文件:

<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.chuan.mapper.UserMapper">
  
    <select id="getUserById" resultType="com.chuan.entity.User">
        SELECT * FROM user WHERE id = ?
    </select>
</mapper>

测试类:

public class TestMybatis {

    public static void main(String[] args) {
        MySqlSession sqlsession=new MySqlSession();
        UserMapper mapper = sqlsession.getMapper(UserMapper.class);
        List<User> user = mapper.getUserById("1");
        System.out.println(user);
    }
}

结果:

执行查询方法:SELECT * FROM user WHERE id = ?
参数:1
记录行数:2
[User{id='1', username='aaa', password='bbb'}, User{id='1', username='c', password='c'}]

到这里整个过程就已经结束了,目前只有查询功能,可能后期会增加其他功能以及缓存等。有问题欢迎指正,谢谢。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值