序言
hello,大家好,现在放假了,在家闲来无事准备写一个MyBatis框架,现在的进度:刚写完select语句的查询支持,包括注解和配置文件的形式,大家有什么好的意见或者不懂的地方欢迎留言讨论。
Mybatis是什么
相信学习JAVA的小伙伴,都接触过JDBC,JDBC为我们提供了JAVA操作数据库的接口,不过由于JDBC的API太过繁琐,我们每次都要做大量的相同的操作,并且还要对执行sql语句过程中所出现的各种异常和资源释放进行处理,而真正涉及到业务功能的代码其实很少,这明显影响了我们开发的效率。
所以就诞生出了一系列的框架,最开始的有hibernate,然后出现了jdbcTemplate,springDataJpa(基于hibernate),myBatis,等等一系列框架。
我们拿市面上最常用的myBatis和hibernate举例,mybatis是一款半ORM框架,hibernate是一款全ORM框架,所谓的ORM其实就是对象映射,也就是将我们数据库中的表映射成JAVA的实体类,实体类中各个属性和表中各个字段相互一一对应。相比于hibernate来说其实mybatis更适合日常的开发,因为使用mybatis我们可以自己写sql,从而就可以对sql进行调优优化来提升性能。
Mybatis的APi
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder=new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
SqlSession sqlSession = factory.openSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
List<User> all = mapper.findAll();
for (User xx:all){
System.out.println(xx);
}
sqlSession.close();
in.close();
相信大家对上面这一段代码并不陌生,这个就是操作Mybatis的基本APi,那我们就从这里开始手写吧。
获取主配置文件的输入流
需要编写Resources工具类
/**
* 获取核心配置文件的输入流
* */
public class Resources {
public static InputStream getResourceAsStream(String path){
return Resources.class.getClassLoader().getResourceAsStream(path);
}
}
编写SqlSessionFactoryBuilder对象
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in){
//将配置文件中的信息进行封装
Configuration con = XMLConfigBuilder.loadConfiguration(in);
//返回一个带有配置信息的sqlSessionFactory工厂
return new DefalutSqlSessionFactroy(con);
}
}
build方法中传入了主配置类的输入流,我们需要对主配置文件进行解析,然后存放到Configuration对象中。
XMLConfigBuilder
这个类就是用来解析主配置文件的,主配置文件是xml的格式,使用dom4j+xpath的方式来进行解析
public static Configuration loadConfiguration(InputStream config){
try{
//定义封装连接信息的配置对象(mybatis的配置对象)
Configuration cfg = new Configuration();
//1.获取SAXReader对象
SAXReader reader = new SAXReader();
//2.根据字节输入流获取Document对象
Document document = reader.read(config);
//3.获取根节点
Element root = document.getRootElement();
//4.使用xpath中选择指定节点的方式,获取所有property节点
List<Element> propertyElements = root.selectNodes("//property");
//5.遍历节点
for(Element propertyElement : propertyElements){
//判断节点是连接数据库的哪部分信息
//取出name属性的值
String name = propertyElement.attributeValue("name");
if("driver".equals(name)){
//表示驱动
//获取property标签value属性的值
String driver = propertyElement.attributeValue("value");
cfg.setDriver(driver);
}
if("url".equals(name)){
//表示连接字符串
//获取property标签value属性的值
String url = propertyElement.attributeValue("value");
cfg.setUrl(url);
}
if("username".equals(name)){
//表示用户名
//获取property标签value属性的值
String username = propertyElement.attributeValue("value");
cfg.setUsername(username);
}
if("password".equals(name)){
//表示密码
//获取property标签value属性的值
String password = propertyElement.attributeValue("value");
cfg.setPassword(password);
}
}
//判断配置文件中是否配置了别名,如果配置了就添加进去
String packages="";
List<Element> list = root.selectNodes("//typeAliases/package");
for (Element xx:list){
Attribute name = xx.attribute("name");
if (name!=null){
packages = name.getValue()+".";
}
}
//取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
List<Element> mapperElements = root.selectNodes("//mappers/mapper");
//遍历集合
for(Element mapperElement : mapperElements){
//判断mapperElement使用的是哪个属性
Attribute attribute = mapperElement.attribute("resource");
if(attribute != null){
System.out.println("使用的是XML");
//表示有resource属性,用的是XML
//取出属性的值
String mapperPath = attribute.getValue();//获取属性的值"com/itheima/dao/IUserDao.xml"
//把映射配置文件的内容获取出来,封装成一个map
Map<String, Mapper> mappers = loadMapperConfiguration(mapperPath,packages);
//给configuration中的mappers赋值
cfg.setMappers(mappers);
}else{
System.out.println("使用的是注解");
//表示没有resource属性,用的是注解
//获取class属性的值
String daoClassPath = mapperElement.attributeValue("class");
//根据daoClassPath获取封装的必要信息
Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath);
//给configuration中的mappers赋值
cfg.setMappers(mappers);
}
}
//返回Configuration
return cfg;
}catch(Exception e){
throw new RuntimeException(e);
}finally{
try {
config.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
在读取主配置文件时,可以通过mapper标签中的内容来判断采用的是注解形式(class)还是配置文件的形式(resource)。
配置文件形式
private static Map<String,Mapper> loadMapperConfiguration(String mapperPath,String packages)throws IOException {
InputStream in = null;
try{
//定义返回值对象
Map<String,Mapper> mappers = new HashMap<String,Mapper>();
//1.根据路径获取字节输入流
in = Resources.getResourceAsStream(mapperPath);
//2.根据字节输入流获取Document对象
SAXReader reader = new SAXReader();
Document document = reader.read(in);
//3.获取根节点
Element root = document.getRootElement();
//4.获取根节点的namespace属性取值
String namespace = root.attributeValue("namespace");//是组成map中key的部分
//5.获取所有的select节点
List<Element> selectElements = root.selectNodes("//select");
//6.遍历select节点集合
for(Element selectElement : selectElements){
//取出id属性的值组成map中key的部分
String id = selectElement.attributeValue("id");
//取出resultType属性的值 组成map中value的部分
String resultType = selectElement.attributeValue("resultType");
if (null != packages && !"".equals(packages)) {
resultType=packages+resultType;
}
//取出文本内容组成map中value的部分
String queryString = selectElement.getText();
//创建Key
String key = namespace+"."+id;
//创建Value
Mapper mapper = new Mapper();
mapper.setQueryString(queryString);
mapper.setResultType(resultType);
//把key和value存入mappers中
mappers.put(key,mapper);
}
return mappers;
}catch(Exception e){
throw new RuntimeException(e);
}finally{
in.close();
}
}
这个方法就是获取指定配置文件中,所有的方法及其对应的sql语句和返回类型
注解形式
private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{
//定义返回值对象
Map<String,Mapper> mappers = new HashMap<String, Mapper>();
//1.得到dao接口的字节码对象
Class daoClass = Class.forName(daoClassPath);
//2.得到dao接口中的方法数组
Method[] methods = daoClass.getMethods();
//3.遍历Method数组
for(Method method : methods){
//取出每一个方法,判断是否有select注解
boolean isAnnotated = method.isAnnotationPresent(Select.class);
if(isAnnotated){
//创建Mapper对象
Mapper mapper = new Mapper();
//取出注解的value属性值
Select selectAnno = method.getAnnotation(Select.class);
String queryString = selectAnno.value()[0];
mapper.setQueryString(queryString);
//获取当前方法的返回值,还要求必须带有泛型信息
Type type = method.getGenericReturnType();//List<User>
//判断type是不是参数化的类型
if(type instanceof ParameterizedType){
//强转
ParameterizedType ptype = (ParameterizedType)type;
//得到参数化类型中的实际类型参数
Type[] types = ptype.getActualTypeArguments();
//取出第一个
Class domainClass = (Class)types[0];
//获取domainClass的类名
String resultType = domainClass.getName();
//给Mapper赋值
mapper.setResultType(resultType);
}
//组装key的信息
//获取方法的名称
String methodName = method.getName();
String className = method.getDeclaringClass().getName();
String key = className+"."+methodName;
//给map赋值
mappers.put(key,mapper);
}
}
return mappers;
}
获取指定接口中,所有方法对应的sql语句和返回类型。
编写sqlSessionFactory对象
public class DefalutSqlSessionFactroy implements SqlSessionFactory {
private Configuration con;
public DefalutSqlSessionFactroy(Configuration con){
this.con=con;
}
public SqlSession openSession() {
return new DefalutSqlSession(con);
}
}
编写sqlSession对象
这个对象是实现sql查询的核心对象了,在这个对象内部实现了动态代理的机制来生成代理对象,内部封装好了实现的逻辑。
public class DefalutSqlSession implements SqlSession {
private Configuration con;
private Connection connection;
public DefalutSqlSession(Configuration con){
this.con=con;
connection= JDBCUtils.getCon(con);
}
public <E> E getMapper(Class<E> Clazz) {
//利用动态代理增强方法
return (E)Proxy.newProxyInstance(Clazz.getClassLoader(),new Class[]{Clazz}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//key是方法名+类名,value是返回类型+sql语句
Map<String, Mapper> mappers = con.getMappers();
String name = method.getName();
String name1 = method.getDeclaringClass().getName();
String key=name1+"."+name;
Mapper mapper = mappers.get(key);
if(mapper==null){
throw new Exception("传入参数有误");
}
//替换sql语句,将#{}替换为实参
String sql = mapper.getQueryString();
mapper.setQueryString(SqlReplace.getSql(sql,method,args));
//根据方法名和类名找到对应的sql语句和返回类型
return new Executor().selectList(mapper,connection);
}
});
}
public void close() {
if(connection!=null){
try {
connection.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
sql的替换
目前获取到的sql还是原始sql,其中包括了#{}这种参数替换的格式,我们需要把它替换成为有实参的sql,通过SqlReplace类来实现。
public class SqlReplace {
public static String getSql(String sql, Method method,Object[] args){
//获取到参数名对应实参的关系
Map<String,Object> nameArgMap=getNameArgMap(method,args);
StringBuilder sqlSb=new StringBuilder();
char[] chars = sql.toCharArray();
for (int i=0;i<chars.length;i++){
char charI = chars[i];
if (charI=='#'){
if (chars[i+1]=='{'){
StringBuilder sb=new StringBuilder();
int index=i+2;
while (index<chars.length){
if (chars[index]=='}'){
break;
}else {
sb.append(chars[index]);
}
index++;
}
if (index>=chars.length){
throw new RuntimeException("sql格式错误"+sql+"索引为"+index);
}
String string = sb.toString();
Object o = nameArgMap.get(string);
if (o instanceof String){
o="'"+o+"'";
}
sqlSb.append(o.toString());
i=index;
}else {
throw new RuntimeException("sql格式错误"+sql+"索引为"+(i+1));
}
}else {
sqlSb.append(charI);
}
}
System.out.println(String.format("sqlParsing:tt%s",sqlSb.toString()));
return sqlSb.toString();
}
private static Map<String, Object> getNameArgMap(Method method, Object[] args) {
Map<String,Object> nameArgMap=new HashMap<>();
//获取method的参数
Parameter[] parameters = method.getParameters();
int[] index=new int[]{0};
Arrays.asList(parameters).forEach(parameter -> {
nameArgMap.put(parameter.getName(),args[index[0]++]);
});
return nameArgMap;
}
}
实现替换的逻辑主要是:因为sql中#{}的内容和参数名是一一匹配的,我们就可以通过Mehtod来获取到所有的参数名,同时将它们和传入的实参args来进行一一对应,最终将sql中的参数名替换为传入的实参。
调用JDBC实现查询
public class Executor {
public <E> List<E> selectList(Mapper mapper, Connection conn) {
PreparedStatement pstm = null;
ResultSet rs = null;
try {
//1.取出mapper中的数据
String queryString = mapper.getQueryString();//sql语句
String resultType = mapper.getResultType();//返回值类型
Class domainClass = Class.forName(resultType);
//2.获取PreparedStatement对象
pstm = conn.prepareStatement(queryString);
//3.执行SQL语句,获取结果集
rs = pstm.executeQuery();
//4.封装结果集
List<E> list = new ArrayList<E>();//定义返回值
while(rs.next()) {
//实例化要封装的实体类对象
E obj = (E)domainClass.newInstance();
//取出结果集的元信息:ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//取出总列数
int columnCount = rsmd.getColumnCount();
//遍历总列数
for (int i = 1; i <= columnCount; i++) {
//获取每列的名称,列名的序号是从1开始的
String columnName = rsmd.getColumnName(i);
//根据得到列名,获取每列的值
Object columnValue = rs.getObject(columnName);
//给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);//要求:实体类的属性和数据库表的列名保持一种
//获取它的写入方法
Method writeMethod = pd.getWriteMethod();
//把获取的列的值,给对象赋值
writeMethod.invoke(obj,columnValue);
}
//把赋好值的对象加入到集合中
list.add(obj);
}
return list;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
release(pstm,rs);
}
}
private void release(PreparedStatement pstm,ResultSet rs){
if(rs != null){
try {
rs.close();
}catch(Exception e){
e.printStackTrace();
}
}
if(pstm != null){
try {
pstm.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
代理对象主要就在这个方法中做事了,根据sql和返回值,调用JDBC最终获取到结果返回
测试
表结构:
user表
实体类:
@Data
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
}
注解形式的测试
-
在主配置文件中添加
-
sql语句
-
调用方法
-
执行结果
配置文件形式的测试
-
在主配置文件中添加
-
sql语句
-
调用方法
-
执行结果
注解和配置文件的形式都执行成功,大功告成O(∩_∩)O哈哈~
流程图
大概画了一下整体的流程图来帮助理解
总结
希望各位读者有什么问题可以提出,我们共同解决!
原文始发于微信公众号(GuoCoding):我决定写一个简易的Mybatis框架
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/42991.html