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.连接数据库
com.mysql.jdbc.Driver jdbc:mysql://localhost:3306/test root root
内容为jdbc连接信息,哦忘了,先把pom文件展示一下,两个jar包:mysql连接和dom4j解析
dom4j dom4j 1.6.1 mysql mysql-connector-java 5.1.29
接下来是通过代码将该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 parseNode(Element element){ //判断根目录名称 if (!element.getName().equals("dataSource")) { throw new RuntimeException("主配置文件根名称必须是dataSource"); } Map map = new HashMap(); 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("格式错误,正确格式为 xxx "); } 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 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 selectObject(Mapping mapping, String param){ return excutor.query(mapping,param); } public T getMapper(Class 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文件:
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 mappingList = new ArrayList(); 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 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 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 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打进去,所以要重新引用。
mysql mysql-connector-java 5.1.29 dom4j dom4j 1.6.1 com.my ybatis 1.0.0
实体类:
public class User implements Serializable { private Integer id; private String username; private String password;
//getter、setter省略
}
接口:
public interface UserMapper { public List getUserById(String id);
}
映射文件:
测试类:
public class TestMybatis { public static void main(String[] args) { MySqlSession sqlsession=new MySqlSession(); UserMapper mapper = sqlsession.getMapper(UserMapper.class); List 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'}]
到这里整个过程就已经结束了,目前只有查询功能,可能后期会增加其他功能以及缓存等。有问题欢迎指正,谢谢。