手写一个简单的mybatis
创始人
2024-03-26 04:38:50
0

1.写个简单的mybatis

今天写个简单版V1.0版本的mybatis,可以其实就是在jdbc的基础上一步步去优化的,网上各种帖子都是照着源码写,各种抄袭,没有自己的一点想法,写代码前要先思考,如果是你,你该怎么写?怎么去实现,为什么要这样写?而不是照着源码依葫芦画瓢。

2.思考

在手写mybatis之前,我们先来手写个jdbc,看看jdbc和mybatis有哪些不同,mybatis能解决哪些jdbc不能解决的问题?如果让你写,你应该从哪里开始写呢?

3.准备工作

CREATE TABLE `user` (`id` bigint(32) NOT NULL AUTO_INCREMENT,`name` varchar(40) DEFAULT NULL COMMENT '用户名',`age` tinyint(3) DEFAULT NULL COMMENT '年龄',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='用户表';
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('1', '张三', '12');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('2', '李四', '33');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('3', '王五', '44');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('4', '陈贺', '88');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('5', '刘磊', '34');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('6', '刘磊', '86');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('7', '22', '22');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('8', '王子睿', '22');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('9', '陈陈陈', '22');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('10', '刘德华', '66');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('11', '周杰伦', '77');

新建一个web工程,里面只要引入mysql,不需要spring

    mysqlmysql-connector-java8.0.18

在这里插入图片描述

4.jdbc

    public static void main(String[] args) {Connection conn = null;PreparedStatement stmt = null;ResultSet resultSet = null;try {//加载驱动Class.forName("com.mysql.cj.jdbc.Driver");// 获取数据库连接conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/eom?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=UTC","root","password");//定义SQL语句String sql = "select * from user where name = ?";//获取执行SQL对象stmt = conn.prepareStatement(sql);//参数赋值,注意jdbc的下标 都是从1开始stmt.setString(1,"刘磊");//执行SQL语句stmt.execute();//获取结果resultSet = stmt.getResultSet();//结果集List list = new ArrayList<>();//遍历结果while (resultSet.next()){//封装对象User user = new User();user.setId(resultSet.getInt("id"));user.setName(resultSet.getString("name"));user.setAge(resultSet.getInt("age"));list.add(user);}System.out.println("-------------" + list);}catch (Exception e){}finally {//释放资源if(conn != null){try {conn.close();} catch (SQLException e) {e.printStackTrace();}}if(stmt != null){try {stmt.close();} catch (SQLException e) {e.printStackTrace();}}if(resultSet != null){try {resultSet.close();} catch (SQLException e) {e.printStackTrace();}}}}

执行结果如下,可以获取查询结果
在这里插入图片描述

可以看到jdbc的执行过程就是:

  • 加载驱动
  • 获取连接
  • 定义SQL
  • 预编译SQL
  • 赋值参数替换?
  • 执行sql
  • 获取结果集
  • 释放资源

jdbc的不足

  1. 频繁创建连接释放资源,性能差
  2. sql不灵活,参数也不灵活
  3. 结果集返回的类型是写死的,不能动态的返回
  4. 可读性差,扩展性差

带着这几个问题,我们来一步一步解决这些问题,看看mybatis是如何解决的

5.DAO

首先我们准备一个接口,里面有一些增删改查方法,而且返回类型有List,有User对象,也有返回单个值的例如String,int这些,基本包含的大部分场景,这个写法就是按照mybatis的方式写的接口,往下看如何一步步实现这些方法


public interface UserMapper {//这里为什么要搞2个name 参数呢?这是因为在处理获取参数,并赋值的时候,需要考虑这种情况public List getUserNameAndAge(String name, Integer age);public List getUserName(String name);public User getUserById(Integer id);public String getUserNameById(Integer id);public int insertUser(String name,Integer age);public int updateUserById(String name,Integer age,Integer id);public int deleteUserById(Integer id);
}

那么有没有想过,在spring中我们是通过注入的方式来实现接口的调用
例如通过注解@Resource或者@Autowired来实现注入,因为这些接口都是依赖于spring,被spring统一管理的,但是我们这里是没有spring环境的,那么应该怎么去调用这些接口呢?

6.动态代理

通过jdk的动态代理,来生成一个UserMapper 的代理对象,通过代理对象来执行接口里面的方法

public class MapperProxyFactory {static {//注册驱动器try {Class.forName("com.mysql.cj.jdbc.Driver");} catch (ClassNotFoundException e) {e.printStackTrace();}}public static  T getMapper(Class mapper){//JDK动态代理,生成代理对象,也就是usermapper这个对象Object proxyInstance = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{mapper}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {}});return (T) proxyInstance;
}

有了这个代理工厂就可以拿到UserMapper这个代理对象,然后调用他里面的方法了

public class MybatisApplication {public static void main(String[] args) {UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class);List list = userMapper.getUserName("刘磊");System.out.println("--------sql查询1返回--------" + list);User user = userMapper.getUserById(2);System.out.println("--------sql查询2返回--------" + user);String name = userMapper.getUserNameById(2);System.out.println("--------sql查询3返回--------" + name);int insertResult = userMapper.insertUser("周杰伦",77);System.out.println("--------sql新增1返回--------" + insertResult);int updateResult = userMapper.updateUserById("陈陈陈",22,9);System.out.println("--------sql修改1返回--------" + updateResult);int deleteResult = userMapper.deleteUserById(18);System.out.println("--------sql删除1返回--------" + deleteResult);}
}

现在去执行这些方法,是无法实现的,因为这个代理对象里面执行方法还是空的,所有也就无法执行这些方法,那现在我们要怎么办呢?我们需要在代理对象中的invoke方法里面去实现我们的具体业务逻辑代码。

在getMapper的invoke方法中来封装一个方法叫做 doInvoke

    public static  T getMapper(Class mapper){//JDK动态代理,生成代理对象,也就是usermapper这个对象Object proxyInstance = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{mapper}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return doInvoke(proxy, method, args);}});return (T) proxyInstance;}

7.开始写最最核心的逻辑

我们在doInvoke方法里面来一步一步的实现业务逻辑,首先我们还是安装JDBC的方法来把整个流程先写出来,前面几步跟jdbc一样注册驱动,获取连接,但是这里的sql就不是我们手动写死了,而是需要获取接口上面注解@Select(只实现简单的注解方式,xml配置方式其实逻辑类似,但是过于复杂这里就按注解的方式来实现),以及@Param来实现参数的复制

8.@Select和@Param注解

由于时间关系这里只考虑select注解的方式,其他注解类似

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {String value();
}

有了这2个注解,我们就可以在接口方法上加上注解了

public interface UserMapper {//这里为什么要搞2个name 参数呢?这是因为在处理获取参数,并赋值的时候,需要考虑这种情况@Select("SELECT * FROM `user` WHERE name = #{name} and age = #{age}")public List getUserNameAndAge(@Param("name") String name, @Param("age") Integer age);@Select("SELECT * FROM `user` WHERE name = #{name}")public List getUserName(@Param("name") String name);@Select("SELECT * FROM `user` WHERE id = #{id}")public User getUserById(@Param("id") Integer id);@Select("SELECT name from `user` WHERE id = #{id}")public String getUserNameById(@Param("id") Integer id);@Select("INSERT INTO `user` (NAME, age) VALUES (#{name}, #{age})")public int insertUser(@Param("name") String name,@Param("age") Integer age);@Select("UPDATE USER SET `name` = #{name}, age = #{age} WHERE id = #{id}")public int updateUserById(@Param("name") String name,@Param("age") Integer age,@Param("id") Integer id);@Select("DELETE FROM `user` WHERE id = #{id}")public int deleteUserById(@Param("id") Integer id);}

9.获取sql

那么怎么拿到方法上的sql呢?

可以通过Method对象来获取注解上的值,注意下面代码都在doInvoke方法中

//通过method参数拿到select注解上的 sqlSelect annotation = method.getAnnotation(Select.class);//sql = select * from user where name = #{name} and age = #{age} and id = #{name}String sql = annotation.value();

此时拿到的sql是这样的

select * from user where name = #{name} and age = #{age} and id = #{name}

很明显,这样的sql是无法执行的,我们需要把#{xxx}这样的变量替换成?,statement才能进行预编译执行sql,也就是变成下面这样的sql

select * from user where name = ? and age = ? and id = ?

那么应该怎么搞?大家想一想,如果是你写,你会怎么入手呢?

10.把#{}替换成?并赋值

我们可以通过method 来获取参数,然后遍历赋值,可以定义一个map,里面存放参数名和参数值

例如:
key=name,value=刘磊
key=age,value=66
key=id,value=刘磊

这里我为什么要搞2个#{name}呢?有没有想过呢?

//用来存放参数名和参数值
Map paramValueMapping = new HashMap<>();
//通过方法拿到入参
Parameter[] parameters = method.getParameters();
//遍历参数
for (int i = 0; i < parameters.length; i++) {Parameter parameter = parameters[i];//注意这里parameter.getName()拿到的 是arg0,arg1,并不是真正的参数名,这个时候就需要@param注解了paramValueMapping.put(parameter.getName(),args[i]);//通过@param注解的方式拿到参数名称,这里拿到的就是name,age,id的真正参数名String paramName = parameter.getAnnotation(Param.class).value();paramValueMapping.put(paramName,args[i]);
}

现在要做的就是如何把#{xxx}替换成?,来我们一步步实现
我们写个参数处理器接口,然后去实现他

TokenHandler 接口

public interface TokenHandler {String handleToken(String content);
}

ParameterMapping对象,用来存放sql中#{xxx}对应的值

public class ParameterMapping {//sql中 #{name} 的这个值private String property;public ParameterMapping(String property) {this.property = property;}public String getProperty() {return property;}public void setProperty(String property) {this.property = property;}
}

ParameterMappingTokenHandler实现类

public class ParameterMappingTokenHandler implements TokenHandler {//存放sql 中#{} 变量的名称private List parameterMappings = new ArrayList();@Overridepublic String handleToken(String content) {//把变量名称存到list中,然后返回?,这样即拿到了参数名称,也替换了?parameterMappings.add(new ParameterMapping(content));return "?";}public List getParameterMappings() {return parameterMappings;}
}

好,那这个替换的逻辑写好了,我们是不是应该先解析这个sql,把这些#{}的内容找出来,这里节约时间之间把mybatis源码中的解析器拿过来之间用,有个叫GenericTokenParser的解析器,这个类里面逻辑并不复杂就是找到符合条件的字符串,然后替换成?

public class GenericTokenParser {private final String openToken;private final String closeToken;//这里就是刚刚我们定义的处理器接口private final TokenHandler handler;public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {this.openToken = openToken;this.closeToken = closeToken;this.handler = handler;}public String parse(String text) {final StringBuilder builder = new StringBuilder();final StringBuilder expression = new StringBuilder();if (text != null && text.length() > 0) {char[] src = text.toCharArray();int offset = 0;// search open tokenint start = text.indexOf(openToken, offset);while (start > -1) {if (start > 0 && src[start - 1] == '\\') {// this open token is escaped. remove the backslash and continue.builder.append(src, offset, start - offset - 1).append(openToken);offset = start + openToken.length();} else {// found open token. let's search close token.expression.setLength(0);builder.append(src, offset, start - offset);offset = start + openToken.length();int end = text.indexOf(closeToken, offset);while (end > -1) {if (end > offset && src[end - 1] == '\\') {// this close token is escaped. remove the backslash and continue.expression.append(src, offset, end - offset - 1).append(closeToken);offset = end + closeToken.length();end = text.indexOf(closeToken, offset);} else {expression.append(src, offset, end - offset);offset = end + closeToken.length();break;}}if (end == -1) {// close token was not found.builder.append(src, start, src.length - start);offset = src.length;} else {//最最关键的地方是这里,通过刚刚我们定义的方法,把符合条件的替换成?builder.append(handler.handleToken(expression.toString()));offset = end + closeToken.length();}}start = text.indexOf(openToken, offset);}if (offset < src.length) {builder.append(src, offset, src.length - offset);}}return builder.toString();}
}

最关键的就是这一行代码

//最最关键的地方是这里,通过刚刚我们定义的方法,把符合条件的替换成?
builder.append(handler.handleToken(expression.toString()));

好了,解析器也有了,替换?的逻辑也有了,我们来看下 如何去使用?

//生成解析器-就是把#{} 改成?
ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser parse = new GenericTokenParser("#{","}",tokenHandler);
//解析之后的sql=select * from user where name = ? and age = ? and id = ?
String parseSql = parse.parse(sql);

GenericTokenParser这个方法有3个参数,分别是开始,结束,和要替换的处理器,不难理解
此时解析后的parseSql = select * from user where name = ? and age = ? and id = ?,正是我们想要的结果,好了sql解析完成,那么如何去赋值呢?想一想?

我们知道,statement的赋值方式 是setString,或者setInt,或者set其他类型来实现参数赋值的,那么这里我们是不是需要先拿到参数的类型,我才能知道 到底是setString,还是setInt呢?

刚刚上面的那个ParameterMappingTokenHandler里面的那个list其实已经拿到了参数名,我们来把这个list来遍历,这里面需要考虑一个问题,就是怎么获取参数的类型,到底是string还是int或者其他类型

//这里面就是存放的替换?的 那些变量名name,age,name
List parameterMappings = tokenHandler.getParameterMappings();//遍历赋值
for (int i = 0; i < parameterMappings.size(); i++) {String property = parameterMappings.get(i).getProperty();//注意这里赋值的时候,会有个类型的问题,有可能是字符串,也有可能是数字类型,所以需要知道参数的类型,然后才能给参数赋值,不然会报错//stmt.setString(); ???//stmt.setInt(); ???//通过变量名获取变量的类型,刚刚上面的那个map里面存放的就是参数名和参数值,那么通过获取这个参数值,我们就能拿到这个参数值的类型Object value = paramValueMapping.get(property);Class type = value.getClass();}

此时这个type就是我们具体的参数类型了,那么拿到这个参数类型了,我是不是就可以赋值了?我们可以根据type来判断,if或者switch都可以实现,但是有个问题,java类型那么多,写那么多if else 可读性可扩展性是不是很差,而且也不符合java的设计模式,这个时候,我们可以使用策略模式来处理这里的逻辑

if(type.equals(String.class)){stmt.setString();
}else if(type.equals(Integer.class)){stmt.setInt();
}

首先我们定义个带泛型的类型处理器接口,里面有个赋值(setParameter)的方法,3个参数分别是statement,第几个参数赋值,要赋值的参数类型

还有个方法getResult是一会处理结果集时,找到对应字段的值

public interface TypeHandler {/*** @param statement st* @param i 第几个参数赋值* @param value 参数值* @return void* @author WangPan* @date 2022/12/7 15:37*/void setParameter(PreparedStatement statement,int i, T value) throws SQLException;/*** @description 在结果集里面获取对应字段的值* @param resultSet* @param columnName* @return T* @author WangPan* @date 2022/12/7 17:45*/T getResult(ResultSet resultSet,String columnName) throws SQLException;
}

然后我们需要写string和int 的2个实现类,去实现这2个方法,这里节约时间只考虑string和int 类型,其他类型类似

IntegerTypeHandler 里面的赋值方法就可以写死setInt,泛型也是Integer

public class IntegerTypeHandler implements TypeHandler{/*** @param statement st* @param i         第几个参数赋值* @param value     参数值* @return void* @author WangPan* @date 2022/12/7 15:37*/@Overridepublic void setParameter(PreparedStatement statement, int i, Integer value) throws SQLException {statement.setInt(i,value);}/*** @param resultSet* @param columnName* @return T* @description 在结果集里面获取对应字段的值* @author WangPan* @date 2022/12/7 17:45*/@Overridepublic Integer getResult(ResultSet resultSet, String columnName) throws SQLException {return resultSet.getInt(columnName);}
}

StringTypeHandler 里面的赋值方法就可以写死setString,泛型也是String

public class StringTypeHandler implements TypeHandler{/*** @param statement st* @param i         第几个参数赋值* @param value     参数值* @return void* @author WangPan* @date 2022/12/7 15:37*/@Overridepublic void setParameter(PreparedStatement statement, int i, String value) throws SQLException {statement.setString(i,value);}/*** @param resultSet* @param columnName* @return T* @description 在结果集里面获取对应字段的值* @author WangPan* @date 2022/12/7 17:45*/@Overridepublic String getResult(ResultSet resultSet, String columnName) throws SQLException {return resultSet.getString(columnName);}
}

好了,然后我们把这个放在static静态块里面去初始化,一会就可以直接使用typeHandlerMap来赋值

    //类型处理器,sql给变量赋值的时候,到底是setString,还是setInt,还是别的类型private static Map typeHandlerMap = new HashMap<>();static {typeHandlerMap.put(String.class, new StringTypeHandler());typeHandlerMap.put(Integer.class,new IntegerTypeHandler());}

接下来我们看看如何使用这个,接着上面的for循环写

for (int i = 0; i < parameterMappings.size(); i++) {String property = parameterMappings.get(i).getProperty();//通过变量名获取变量的类型Object value = paramValueMapping.get(property);Class type = value.getClass();//利用类型处理器,根据字段类型来给变量赋值//通过type类型来获取对应的处理器,如果是string就执行StringTypeHandler这个处理器里面的赋值方法//如果type=Integer,就执行IntegerTypeHandler 这个处理器里面的赋值方法typeHandlerMap.get(type).setParameter(stmt,i+1,value);
}

注意这里jdbc的赋值要从1开始
这样写是不是要优雅的多,逼格一下子就上来了了,不必if else 一目了然,可读性更高吗?

执行sql获取结果集

这里有来了个问题,有的结果是返回List,有的是返回String,有的是返回User对象,有的是返回int,那么应该怎么办呢?

首先我们要明白一点的就是只有select查询语句是有结果集的,insert update delete是没有结果集
那么如果是insert update delete就简单了,只需要返回更新的条数就行
那么如果是select怎么办?
我们要考虑这下面3种情况

//查询多条数据返回list
List
//查询单条数据返回对象
User
//查询具体某个字段返回string
String

还有一种情况需要考虑,就是如果不是User对象,例如返回Person对象,里面的字段不一样,那我们应该怎么把结果集转换成对应的java类型返回呢?又怎么去调用User对象里面set方法赋值呢?

这里是本次手写mybatis的最最复杂的地方,如果是你,你有没有思路呢?

//执行SQL语句
stmt.execute();//获取结果
ResultSet resultSet = stmt.getResultSet();//先定义一个要返回的对象,不管是User,或者list或者string最后都赋值给Object返回
Object result = null;
//返回的对象如果是list 就先存到list里面然后在放在Object里面返回
List list = new ArrayList<>();//这里要判断返回结果是否为空,如果不是select,是insert或者update,delete的话就没有 返回结果集
if(resultSet != null){//这里需要对返回的结果进行处理,要获取返回的类型到底是集合,还是的那个对象,还是别的其他类型//定义一个返回对象类型Class resultType = null;//获取方法的返回类型,来判断是否是泛型Type genericReturnType = method.getGenericReturnType();if(genericReturnType instanceof Class){//不是泛型resultType = (Class) genericReturnType;}else if(genericReturnType instanceof ParameterizedType){//是泛型,这里只考虑list 这种简单泛型,不考虑 List 这种Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();//取泛型的第一个ListresultType = (Class) actualTypeArguments[0];}//结果集的元数据ResultSetMetaData metaData = resultSet.getMetaData();//存放sql查询的 有哪些字段,例如 select id,name,age那这个list 里面就是对应的 字段名List columnList = new ArrayList<>();for (int i = 0; i < metaData.getColumnCount(); i++) {columnList.add(metaData.getColumnName(i+1));}//记录User对象里面有哪些set方法Map setterMethodMapping = new HashMap<>();//获取User对象中所有的方法for(Method declaredMethod : resultType.getDeclaredMethods()){//找到set方法if(declaredMethod.getName().startsWith("set")){//获取set方法后截取set之后的字符串就是 对应的字段名String propertyName = declaredMethod.getName().substring(3);//然后把首字母改成小写propertyName = propertyName.substring(0,1).toLowerCase(Locale.ROOT) + propertyName.substring(1);setterMethodMapping.put(propertyName,declaredMethod);}}while (resultSet.next()){//反射创建返回对象Object instance = resultType.newInstance();//创建出user对象之后,需要调用set方法赋值,但是又不能调用所有的set方法,是根据sql里面查询的结果来set ,//如果是select * 就要set所有,如果只查询了3个字段就只调用这3个字段的set方法for (int i = 0; i < columnList.size(); i++) {String column = columnList.get(i);if(setterMethodMapping.size() > 0){//如果有setter方法就说明返回类型 是对象类型//获取这个字段对应的setter方法Method setterMethod = setterMethodMapping.get(column);//然后通过setter方法找到入参的类型,因为setter方法只有1个参数,所以取0个,渠道的结果是String,Int或者其他类型Class clazz = setterMethod.getParameterTypes()[0];//然后根据入参的类型来调用具体的处理器,意思就是到底是调用setString,还是setInt,还是别的类型TypeHandler typeHandler = typeHandlerMap.get(clazz);//获取结果集里面字段对应的值Object resultValue = typeHandler.getResult(resultSet, column);//setter方法执行(2个参数,一个是对象,一个是执行setter方法的参数值),这样User对象里的属性就有值了setterMethod.invoke(instance,resultValue);}else{//如果没有setter方法,就说明返回类型是 数据类型//获取结果集里面字段对应的值Class clazz  = (Class)method.getGenericReturnType();//然后根据入参的类型来调用具体的处理器,意思就是到底是调用setString,还是setInt,还是别的类型TypeHandler typeHandler = typeHandlerMap.get(clazz);instance = typeHandler.getResult(resultSet, column);}}list.add(instance);}
}else{//如果是insert或者update,delete,就获取更新条数result = stmt.getUpdateCount();
}
 

这段结果集处理的代码比较复杂,我来把这块逻辑梳理一下

  • 首先判断结果集是否为空,如果是空就说明是insert update delete语句直接返回更新行数就行
  • 如果不为空,那么就需要获取方法返回类型,判断是否是泛型,
  • 如果是泛型,就取泛型里的第0个对象,简单起见吗,这里不考虑List>这种情况
  • 通过结果集的metaData获取select 的哪些字段,例如name age id这些字段存在list里面
  • 然后获取到具体对象之后,我们需要知道这个对象里面有哪些setXx方法,用map存放这些方法
  • 然后遍历结果集,这个时候需要通过反射来创建对象
  • 然后循环刚刚metaData获取的所有列的 list
  • 这个时候要考虑一种情况就是如果是User对象就肯定有set方法,如果是String或者Integer类型就没有set方法,所有需要加个判断
  • 如果是User对象,就遍历这些字段去执行set方法,通过Method.invoke来执行set方法并赋值
  • 如果是String或者Integer,就需要根据方法的返回类型来判断,应该结果集取值需要getInt或者getString,拿到方法的返回类型后,通过上面创建的处理器来实现从结果集里面取值
  • 这样就完成对象的创建,赋值,最后添加到list里面

这样就完成了吗?

还差最后一步,那么到底是返回List,还是User对象,还是Stirng ,还是Integer呢?

//这里不能直接返回list,需要根据方法返回的类型来判断 到底是返回list,还是对象,还是其他类型
if(method.getReturnType().equals(List.class)){//如果是list,就直接返回result = list;
}else if(method.getReturnType().equals(Object.class)){//如果是对象 就取第一个result = list.get(0);
}else{//如果是String 或者Integer或者其他类型if(list.size() > 0){result = list.get(0);}//其他类型,insert update delete不做处理
}

释放资源

//释放资源
if(conn != null){try {conn.close();} catch (SQLException e) {e.printStackTrace();}
}
if(stmt != null){try {stmt.close();} catch (SQLException e) {e.printStackTrace();}
}
if(resultSet != null){try {resultSet.close();} catch (SQLException e) {e.printStackTrace();}
}

完整代码

public static Object doInvoke(Object proxy, Method method, Object[] args) throws Throwable{//要返回的对象Object result = null;//返回的对象如果是list 就先存到list里面然后在放在Object里面返回List list = new ArrayList<>();//动态代理执行方法//获取连接----获取session----获取sql----获取参数----执行sql----返回数据// 获取数据库连接Connection conn = getConnection();//通过method参数拿到select注解上的 sqlSelect annotation = method.getAnnotation(Select.class);//sql = select * from user where name = #{name} and age = #{age} and id = #{name}String sql = annotation.value();//获取入参//这个map用来存入参和对应参数的值,例如这样的//name=1//age=2//name=3Map paramValueMapping = new HashMap<>();Parameter[] parameters = method.getParameters();for (int i = 0; i < parameters.length; i++) {Parameter parameter = parameters[i];//注意这里parameter.getName()拿到的 是arg0,arg1,并不是真正的参数名,这个时候就需要@param注解了paramValueMapping.put(parameter.getName(),args[i]);//通过@param注解的方式拿到参数名称String paramName = parameter.getAnnotation(Param.class).value();paramValueMapping.put(paramName,args[i]);}//生成解析器-就是把#{} 改成?ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();GenericTokenParser parse = new GenericTokenParser("#{","}",tokenHandler);//解析之后的sql=select * from user where name = ? and age = ? and id = ?String parseSql = parse.parse(sql);//这里面就是存放的替换?的 那些变量名name,age,nameList parameterMappings = tokenHandler.getParameterMappings();//获取执行SQL对象PreparedStatement stmt = conn.prepareStatement(parseSql);//赋值for (int i = 0; i < parameterMappings.size(); i++) {String property = parameterMappings.get(i).getProperty();//注意这里赋值的时候,会有个类型的问题,有可能是字符串,也有可能是数字类型,所以需要知道参数的类型,然后才能给参数赋值,不然会报错//stmt.setString(); ???//stmt.setInt(); ???//通过变量名获取变量的类型Object value = paramValueMapping.get(property);Class type = value.getClass();//利用类型处理器,根据字段类型来给变量赋值typeHandlerMap.get(type).setParameter(stmt,i+1,value);}//执行SQL语句stmt.execute();//获取结果ResultSet resultSet = stmt.getResultSet();//这里要判断返回结果是否为空,如果不是select,是insert或者update,delete的话就没有 返回结果集if(resultSet != null){//这里需要对返回的结果进行处理,要获取返回的类型到底是集合,还是的那个对象,还是别的其他类型//返回对象类型Class resultType = null;//获取方法的返回类型,来判断是否是泛型Type genericReturnType = method.getGenericReturnType();if(genericReturnType instanceof Class){//不是泛型resultType = (Class) genericReturnType;}else if(genericReturnType instanceof ParameterizedType){//是泛型,这里只考虑list 这种简单泛型,不考虑 List 这种Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();//取泛型的第一个ListresultType = (Class) actualTypeArguments[0];}//结果集的元数据ResultSetMetaData metaData = resultSet.getMetaData();//存放sql查询的 有哪些字段,例如 select id,name,age那这个list 里面就是对应的 字段名List columnList = new ArrayList<>();for (int i = 0; i < metaData.getColumnCount(); i++) {columnList.add(metaData.getColumnName(i+1));}//记录User对象里面有哪些set方法Map setterMethodMapping = new HashMap<>();//获取User对象中所有的方法for(Method declaredMethod : resultType.getDeclaredMethods()){//找到set方法if(declaredMethod.getName().startsWith("set")){//获取set方法后截取set之后的字符串就是 对应的字段名String propertyName = declaredMethod.getName().substring(3);//然后把首字母改成小写propertyName = propertyName.substring(0,1).toLowerCase(Locale.ROOT) + propertyName.substring(1);setterMethodMapping.put(propertyName,declaredMethod);}}while (resultSet.next()){//反射创建返回对象Object instance = resultType.newInstance();//创建出user对象之后,需要调用set方法赋值,但是又不能调用所有的set方法,是根据sql里面查询的结果来set ,//如果是select * 就要set所有,如果只查询了3个字段就只调用这3个字段的set方法for (int i = 0; i < columnList.size(); i++) {String column = columnList.get(i);if(setterMethodMapping.size() > 0){//如果有setter方法就说明返回类型 是对象类型//获取这个字段对应的setter方法Method setterMethod = setterMethodMapping.get(column);//然后通过setter方法找到入参的类型,因为setter方法只有1个参数,所以取0个,渠道的结果是String,Int或者其他类型Class clazz = setterMethod.getParameterTypes()[0];//然后根据入参的类型来调用具体的处理器,意思就是到底是调用setString,还是setInt,还是别的类型TypeHandler typeHandler = typeHandlerMap.get(clazz);//获取结果集里面字段对应的值Object resultValue = typeHandler.getResult(resultSet, column);//setter方法执行(2个参数,一个是对象,一个是执行setter方法的参数值),这样User对象里的属性就有值了setterMethod.invoke(instance,resultValue);}else{//如果没有setter方法,就说明返回类型是 数据类型//获取结果集里面字段对应的值Class clazz  = (Class)method.getGenericReturnType();//然后根据入参的类型来调用具体的处理器,意思就是到底是调用setString,还是setInt,还是别的类型TypeHandler typeHandler = typeHandlerMap.get(clazz);instance = typeHandler.getResult(resultSet, column);}}list.add(instance);}}else{//如果是insert或者update,delete,就获取更新条数result = stmt.getUpdateCount();}//这里不能直接返回list,需要根据方法返回的类型来判断 到底是返回list,还是对象,还是其他类型if(method.getReturnType().equals(List.class)){//如果是list,就直接返回result = list;}else if(method.getReturnType().equals(Object.class)){//如果是对象 就取第一个result = list.get(0);}else{//其他类型,insert update delete只需要返回 更新的条数if(list.size() > 0){result = list.get(0);}}//释放资源if(conn != null){try {conn.close();} catch (SQLException e) {e.printStackTrace();}}if(stmt != null){try {stmt.close();} catch (SQLException e) {e.printStackTrace();}}if(resultSet != null){try {resultSet.close();} catch (SQLException e) {e.printStackTrace();}}return result;}
 

执行一下

增删改查都没问题
在这里插入图片描述

有几个的问题?

  1. 这里实际上还是用的jdbc来创建连接并不是由会话来管理的,还是会频繁创建连接释放连接影响性能
  2. 可以加入连接池例如druid,hika等
  3. 这是通过注解的方式,那么要是通过xml的方式改怎么处理呢?
  4. mybatis的缓存机制怎么实现
  5. 如果让你来完成上面功能?你应该怎么处理呢?想一想

用到了哪些设计模式

  • 工厂模式:MapperProxyFactory
  • 代理模式:proxyInstance
  • 策略模式:TypeHandler、IntegerTypeHandler、StringTypeHandler

mybatis源码中还用到的设计模式

  • 建造者模式:SqlSessionFactoryBuilder
  • 单例模式:Configuration
  • 适配模式:Log4j、Slf4j适配Log接口
  • 装饰器模式:Wrapper
  • 模板模式:Executor–BaseExecutor–SimpleExecutor里面就有很多模板,代码复用

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...