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>

java mybatis null值不更新_mysql

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();
                }
            }
        }
    }

执行结果如下,可以获取查询结果

java mybatis null值不更新_java_02

可以看到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;
    }

执行一下

增删改查都没问题

java mybatis null值不更新_bc_03

有几个的问题?

  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里面就有很多模板,代码复用