手写一个简单的mybatis

news2024/12/28 10:28:14

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

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>
    </dependencies>

在这里插入图片描述

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<User> 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<User> getUserNameAndAge(String name, Integer age);

    public List<User> 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> T getMapper(Class<T> mapper){
        //JDK动态代理,生成代理对象,也就是usermapper这个对象
		Object proxyInstance = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{mapper}, new InvocationHandler() {
			@Override
            public 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<User> 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> T getMapper(Class<T> mapper){
        //JDK动态代理,生成代理对象,也就是usermapper这个对象
        Object proxyInstance = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{mapper}, new InvocationHandler() {
            @Override
            public 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<User> getUserNameAndAge(@Param("name") String name, @Param("age") Integer age);

    @Select("SELECT * FROM `user` WHERE name = #{name}")
    public List<User> 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注解上的 sql
 Select 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<String,Object> 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<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    @Override
    public String handleToken(String content) {
    	//把变量名称存到list中,然后返回?,这样即拿到了参数名称,也替换了?
        parameterMappings.add(new ParameterMapping(content));
        return "?";
    }
    public List<ParameterMapping> 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 token
            int 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<ParameterMapping> 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<T> {

    /**
     * @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<Integer>{

    /**
     * @param statement st
     * @param i         第几个参数赋值
     * @param value     参数值
     * @return void
     * @author WangPan
     * @date 2022/12/7 15:37
     */
    @Override
    public 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
     */
    @Override
    public Integer getResult(ResultSet resultSet, String columnName) throws SQLException {
        return resultSet.getInt(columnName);
    }
}

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

public class StringTypeHandler implements TypeHandler<String>{

    /**
     * @param statement st
     * @param i         第几个参数赋值
     * @param value     参数值
     * @return void
     * @author WangPan
     * @date 2022/12/7 15:37
     */
    @Override
    public 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
     */
    @Override
    public String getResult(ResultSet resultSet, String columnName) throws SQLException {
        return resultSet.getString(columnName);
    }
}

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

    //类型处理器,sql给变量赋值的时候,到底是setString,还是setInt,还是别的类型
    private static Map<Class, TypeHandler> 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>
//查询单条数据返回对象
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<Object> 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<User> 这种简单泛型,不考虑 List<User> 这种
        Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
        //取泛型的第一个List<User>
        resultType = (Class) actualTypeArguments[0];
    }

    //结果集的元数据
    ResultSetMetaData metaData = resultSet.getMetaData();

    //存放sql查询的 有哪些字段,例如 select id,name,age那这个list 里面就是对应的 字段名
    List<String> columnList = new ArrayList<>();
    for (int i = 0; i < metaData.getColumnCount(); i++) {
        columnList.add(metaData.getColumnName(i+1));
    }

    //记录User对象里面有哪些set方法
    Map<String,Method> 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<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<Object> list = new ArrayList<>();

        //动态代理执行方法
        //获取连接----获取session----获取sql----获取参数----执行sql----返回数据


        // 获取数据库连接
        Connection conn = getConnection();

        //通过method参数拿到select注解上的 sql
        Select 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=3
        Map<String,Object> 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,name
        List<ParameterMapping> 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<User> 这种简单泛型,不考虑 List<User> 这种
                Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
                //取泛型的第一个List<User>
                resultType = (Class) actualTypeArguments[0];
            }

            //结果集的元数据
            ResultSetMetaData metaData = resultSet.getMetaData();

            //存放sql查询的 有哪些字段,例如 select id,name,age那这个list 里面就是对应的 字段名
            List<String> columnList = new ArrayList<>();
            for (int i = 0; i < metaData.getColumnCount(); i++) {
                columnList.add(metaData.getColumnName(i+1));
            }

            //记录User对象里面有哪些set方法
            Map<String,Method> 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里面就有很多模板,代码复用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/71562.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据库数据恢复】MS SQL数据库提示“附加数据库错误 823”怎么恢复数据?

MS SQL Server是微软公司研发的数据库管理系统&#xff0c;SQL Server是一个可扩展的、高性能的、与WindowsNT有机结合的&#xff0c;为分布式客户机/服务器所设计的数据库管理系统&#xff0c;提供基于事务的企业级信息管理系统方案。 SQL Server数据库故障情况&分析&…

RCNN学习笔记-MobileNet

Abstract 我们提出了一类叫做MobileNets的高效模型用于移动和嵌入式视觉应用。MobileNets基于一种简化的架构&#xff0c;该架构使用深度方向可分离卷积来构建轻量级深度神经网络。我们引入了两个简单的全局超参数&#xff0c;可以有效地在延迟和准确性之间进行权衡。这些超参…

套用bi模板,轻松搞定各类数据分析报表

bi模板是什么?是一个个提前预设的报表设计&#xff0c;套用后立即生效&#xff0c;轻轻松松搞定bi数据可视化分析报表。bi模板都有哪些类型&#xff1f;怎么套用&#xff1f;以奥威bi数据可视化软件为例&#xff0c;聊聊bi模板的种类和下载使用。 bi模板有哪些&#xff1f; …

Web Component入门

本文作者为奇舞团前端开发工程师引言前端开发者&#xff0c;现在在进行项目的开发时&#xff0c;一般很少使用原生的js代码&#xff0c;往往都会依靠Vue&#xff0c;React等框架进行开发&#xff0c;而不同的框架都有自己不同的开发规则&#xff0c;但是目前所使用的主流框架&a…

关于小程序swiper图片不能撑满解决方案

问题描述 最近在写小程序的时候使用了swiper组件&#xff0c;但是发现一个很奇怪的现象&#xff0c;如果给image组件设置mode“widthFix”的话&#xff0c;那么图片的高度是不够撑满swiper-item的这样就会导致swiper的指示器往下偏移&#xff08;其实没有偏移&#xff0c;只是…

代码随想录刷题Day58 | 739. 每日温度 | 496. 下一个更大元素 I

代码随想录刷题Day58 | 739. 每日温度 | 496. 下一个更大元素 I 739. 每日温度 题目&#xff1a; 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在…

剑指Offer51——数组中的逆序对

摘要 剑指 Offer 51. 数组中的逆序对 一、暴力的方法 1.1 暴力的解析 使用两层 for 循环枚举所有的数对&#xff0c;逐一判断是否构成逆序关系。 1.2 复杂度分析 时间复杂度&#xff1a;O(N^2)&#xff0c;这里N是数组的长度&#xff1b;空间复杂度&#xff1a;O(1)。 1…

【芯片应用】PA93

文章目录一、简介二、原理1、外部连接&#xff08;1&#xff09;相位补偿&#xff08;2&#xff09;限流电阻一、简介 性质&#xff1a;高压运算放大器 厂商&#xff1a;美国 APEX Microtechnology公司 供电电压&#xff1a;Vs to -Vs&#xff1a;最高400V&#xff0c;即200V …

【web课程设计】HTML+CSS仿QQ音乐网站

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

【Windows基础】NTFS文件系统

NTFS文件系统 windows上的文件系统 早期Windows上使用&#xff1a;FAT16或FAT32&#xff08;Windows98&#xff09;目前Windows操作系统基本使用的是NTFS文件系统ReFS文件系统 ReFS&#xff08;Resilient File System&#xff0c;复原文件系统&#xff09;是在 Windows Serve…

万众期待的Dyson Zone空气净化耳机确认将于中国首发,戴森重新定义“好声音”

同享纯净音质与洁净空气&#xff0c;Dyson Zone™ 空气净化耳机确认将在中国开启全球首发 中国&#xff0c; 2022年12月8日 – 今日&#xff0c;戴森首次公开了Dyson Zone™ 空气净化耳机的详细技术参数&#xff0c;该产品已确认将在中国开启全球首发&#xff0c;并在戴森指定…

玩好.NET高级调试,你也要会写点汇编

一&#xff1a;背景 1. 简介 .NET 高级调试要想玩的好&#xff0c;看懂汇编是基本功&#xff0c;但看懂汇编和能写点汇编又完全是两回事&#xff0c;所以有时候看的多&#xff0c;总手痒痒想写一点&#xff0c;在 Windows 平台上搭建汇编环境不是那么容易&#xff0c;大多还是…

[附源码]Python计算机毕业设计SSM佳音大学志愿填报系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Mybatis日志配置(slf4j、log4j、log4j2)

文章目录1. Mybatis日志1.1 日志实现原理1.2 日志实现方式2. SLF4J2.1 slf4j日志级别2.2 日志门面与日志实现2.3 日志门面与日志依赖配置3. LOG4J3.1 日志级别3.2 log4j重要组件3.3 mybatis日志配置log4j3. LOG4J23.1 mybatis配置log4j23.2 log4j2配置文件1. Mybatis日志 1.1 …

elasticsearch集群数据索引迁移自动化脚本

日常维护elasticsearch集群会出现新老集群数据迁移,这里使用的是snapshot api是Elasticsearch用于对数据进行备份和恢复的一组api接口,可以通过snapshot api进行跨集群的数据迁移,原理就是从源ES集群创建数据快照,然后在目标ES集群中进行恢复。 1、新老集群修改集群配置文…

潦草手写体也能轻松识别,快速提取文字不用愁

基于文本识别&#xff08;OCR&#xff09;技术的成熟与应用&#xff0c;日常生活中的大部分“印刷体识别”需求都能被满足&#xff0c;替代了人工信息录入与检测等操作&#xff0c;大大降低输入成本。 而对于复杂的手写体识别需求&#xff0c;业界识别质量却参差不齐。大部分手…

【Linux】进程优先级进程切换

索引➡️进程优先级1.什么叫做优先级2.为什么会存在优先级3.看看Linux怎么做的4.查看进程优先级的命令&#x1f60a;进程的一些特性➡️进程切换➡️进程优先级 1.什么叫做优先级 优先级和权限有些区别&#xff0c;权限决定能还是不能&#xff0c;优先级的前提是能&#xff0…

计算机存储器之逻辑地址和物理地址转换详解

文章目录1 概述2 转换2.1 逻辑地址 to 物理地址2.2 物理地址 to 逻辑地址3 扩展3.1 在线进制转换1 概述 #mermaid-svg-zTbJ3rKuirwBssRU {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-zTbJ3rKuirwBssRU .error-ico…

Zookeeper-全面详解(学习总结---从入门到深化)

目录 Zookeeper概念_集中式到分布式 单机架构 集群架构 什么是分布式 三者区别 Zookeeper概念_CAP定理 分区容错性 一致性 可用性 一致性和可用性的矛盾 Zookeeper概念_什么是Zookeeper 分布式架构 Zookeeper从何而来 Zookeeper介绍 Zookeeper概念_应用场景 数据发布/订阅 实…

vue框架常用的组件库:Element、vant4地址

这些组件库也只能解决UI问题&#xff0c;真正的业务还需要自己去写 pc端&#xff1a;Element&#xff1a;Element - The worlds most popular Vue UI frameworkElement&#xff0c;一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库https://element.eleme.io…