模拟实现mybatis框架(跟原框架有出入)。

本次实现是非常基础的模拟实现,大佬们可以不用看了,如果对mybatis不怎么了解的同学可以看一看。
先看一下整个实现的结构目录,如下图:
在这里插入图片描述
第一步,加载配置的 mapper 文件

public class ChampionConfiguration {

    private static List<MapperBean> mappers = new ArrayList<>();
    private Logger log = Logger.getLogger(ChampionConfiguration.class);

    public ChampionConfiguration(String packagePath) {
        readMapperXML(packagePath);
    }

    public ChampionConfiguration() {
    }

    public List<MapperBean> getMappers() {
        return mappers;
    }

    /**
     * 加载数据库propertis文件,并返回数据库连接对象
     * @param resource
     * @return
     */
    public Connection loadProperties(String resource) {
        try {
            InputStream is = ClassLoader.getSystemResourceAsStream(resource);
            Properties prop = new Properties();
            prop.load(is);
            //获取配置文件中的数据库连接信息
            String driverClassName = prop.getProperty("datasource.driverClassName");
            String url = prop.getProperty("datasource.url");
            String username = prop.getProperty("datasource.username");
            String password = prop.getProperty("datasource.password");
            log.info("driver:" + driverClassName);
            log.info("url:" + url);
            log.info("username:" + username);
            log.info("password:" + password);
            //加载jdbc驱动类
            Class.forName(driverClassName);
            Connection connection = DriverManager.getConnection(url, username, password);
            return connection;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**某路径下的Mapper的xml文件
     * 读取
     * @param packagePath
     * @return
     */
    public List<MapperBean> readMapperXML(String packagePath) {
        StringBuilder path = new StringBuilder();
        //获取到项目路径
        path.append(System.getProperty("user.dir").replaceAll("\\\\" , "/"));
        //拼接资源路径
        path.append("/src/main/java/");
        packagePath = packagePath.replaceAll("\\.", "/");
        //拼接包路径
        path.append(packagePath);
        File directory = new File(path.toString());
        log.info(path.toString());
        //遍历包下所有文件,不能有子包
        File[] files = directory.listFiles();
        for (File file : files) {
            String name = file.getName();
            //过滤以.xml结尾的文件,进行操作
            if (name.endsWith(".xml")) {
                try {
                    FileInputStream fis = new FileInputStream(file);
                    SAXReader reader = new SAXReader();
                    Document document = reader.read(fis);
                    Element root = document.getRootElement();
                    //创建mapper文件对应的存储对象类
                    MapperBean mapper = new MapperBean();
                    //获取根节点命名空间的值
                    String interfaceName = root.attributeValue("namespace").trim();
                    //将命名空间的值设置到MapperBean中
                    mapper.setInterfaceName(interfaceName);
                    //创建mapper文件中每一个sql对应的sql对象类
                    List<SQLFunction> functions = new ArrayList<>();
                    //通过迭代器遍历根节点下元素
                    for (Iterator iterator = root.elementIterator() ; iterator.hasNext();) {
                        //获取节点
                        Element e = (Element) iterator.next();
                        //获取sql的类型,select、insert、update、delete
                        String sqlType = e.getName();
                        //获取方法名
                        String functionName = e.attributeValue("id").trim();
                        //获取参数类型
                        String parameterType = e.attributeValue("parameterType");
                        //不为空时将空格去掉
                        if (parameterType != null) parameterType = parameterType.trim();
                        //获取返回的类型
                        String resultType = e.attributeValue("resultType");
                        Object resultTypeClass = null;
                        //不为空时将空格去掉
                        if (resultType != null) {
                            resultType = resultType.trim();
                            //获取返回类型的一个类对象
                            resultTypeClass = Class.forName(resultType).newInstance();
                        }
                        //获取sql语句
                        String sql = e.getText();
                        //创建一个方法类对象并存入数据
                        SQLFunction function = new SQLFunction(sqlType, functionName, sql, resultTypeClass, parameterType);
                        //将方法类对象添加到方法列表中
                        functions.add(function);
                    }
                    mapper.setFunctions(functions);
                    mappers.add(mapper);
                } catch (DocumentException e) {
                    e.printStackTrace();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
            }
        }
        return mappers;
    }
}

该类中一共有两个方法,分别是用来加载数据库 properties 文件和 mapper 配置文件。
先看 loadProperties 方法,通过传入的 resource 路径,获取到数据库配置信息并获取到数据库连接。
然后是 readMapperXML 方法,通过传入的包名,解析路径后,依此将路径下所有后缀为 .xml 的文件进行解析。解析所有节点的信息后存入到 List mappers = new ArrayList<>() 中,先看一下 AccountInfoMapper.xml 配置文件内容:

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.champion.mybatis.mapper.AccountInfoMapper" >
  <select id="selectByPrimaryKey" resultType="com.champion.mybatis.pojo.AccountInfo" parameterType="java.lang.Integer" >
    select id,username,password,money
    from account_info
    where id = #{id}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from account_info
    where id = #{id}
  </delete>
  <insert id="insert" parameterType="com.champion.mybatis.pojo.AccountInfo" >
    insert into account_info (id, username, password, money)
    values (#{id}, '#{username}', '#{password}', #{money})
  </insert>
  <update id="updateByPrimaryKey" parameterType="com.champion.mybatis.pojo.AccountInfo" >
    update account_info
    set username = '#{username}',
      password = '#{password}',
      money = #{money}
    where id = #{id}
  </update>
</mapper>

通过 mapper 文件,可以看到一个 mapper 文件中存在多个 sql 查询语句,而一个 sql 语句,又包含了多条信息,比如id,sql类型,入参,sql语句等等,所以对于一个 mapper 文件,需要创建一个合适的数据结构来存储,就是 MapperBean 这个Java类。

public class MapperBean {

    /** 接口包名,用来做反射 */
    private String interfaceName;
    /** 所有 sql 集合,存储每一个 sql 的信息 */
    private List<SQLFunction> functions;

    public MapperBean() {
    }

    public MapperBean(String interfaceName, List<SQLFunction> functions) {
        this.interfaceName = interfaceName;
        this.functions = functions;
    }

    public String getInterfaceName() {
        return interfaceName;
    }

    public void setInterfaceName(String interfaceName) {
        this.interfaceName = interfaceName;
    }

    public List<SQLFunction> getFunctions() {
        return functions;
    }

    public void setFunctions(List<SQLFunction> functions) {
        this.functions = functions;
    }
}

MapperBean 类中还有一个 SQLFunction 类,如下:

public class SQLFunction {

    /** sql 类型(select,insert,update,delete) */
    private String sqlType;
    /** sql 查询对应接口的方法名 */
    private String functionName;
    /** sql 语句 */
    private String sql;
    /** sql 的返回类型 */
    private Object resultType;
    /** sql 方法传入的参数类型 */
    private String parameterType;

    public SQLFunction() {
    }

    public SQLFunction(String sqlType, String functionName, String sql, Object resultType, String parameterType) {
        this.sqlType = sqlType;
        this.functionName = functionName;
        this.sql = sql;
        this.resultType = resultType;
        this.parameterType = parameterType;
    }

    public String getSqlType() {
        return sqlType;
    }

    public void setSqlType(String sqlType) {
        this.sqlType = sqlType;
    }

    public String getFunctionName() {
        return functionName;
    }

    public void setFunctionName(String functionName) {
        this.functionName = functionName;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }

    public Object getResultType() {
        return resultType;
    }

    public void setResultType(Object resultType) {
        this.resultType = resultType;
    }

    public String getParameterType() {
        return parameterType;
    }

    public void setParameterType(String parameterType) {
        this.parameterType = parameterType;
    }

    @Override
    public String toString() {
        return "SQLFunction{" +
                "sqlType='" + sqlType + '\'' +
                ", functionName='" + functionName + '\'' +
                ", sql='" + sql + '\'' +
                ", resultType=" + resultType +
                ", parameterType='" + parameterType + '\'' +
                '}';
    }
}

Mapper 文件存储的结构就是这样,
在这里插入图片描述
包扫描后,存在 ChampionConfiguration 类中的 List 中。
第二步,初始化执行器 ChampionExecutor 类

public class ChampionExecutor implements Executor {

    private ChampionConfiguration configuration;
    /** 数据库连接对象 */
    private static Connection connection;
    private Logger log = Logger.getLogger(ChampionExecutor.class);

    /**
     * 获取数据库连接对象
     * @return
     */
    private Connection getConnection() {
        if (connection == null) { // 多线程环境下可能会有问题,可能会产生多个 connectioon
            connection = configuration.loadProperties("properties/db.properties");
            log.info("调用一次connection");
        }
        return connection;
    }


    public ChampionExecutor(ChampionConfiguration configuration) {
        this.configuration = configuration;
    }

    /**
     * 查找方法
     * @param sql 传入的sql语句
     * @param resultType 返回结果类对象
     * @param param 方法参数值
     * @param <T> 结果对象的类型
     * @return 返回查询的结果对象
     */
    @Override
    public <T> T query(String sql,Object resultType, Object param) throws Exception {
        Object newInstance = resultType.getClass().newInstance();
        //判断sql语句中时否包含了#字符,#字符表示一个占位符,#{XXX},匹配一个参数值
        if (sql.contains("#")) {
            //判断参数是否为空
            if (param == null) {
                //如果为空,则未设置参数值,将异常抛出
                throw new Exception("该查找sql中没有设置对应参数");
            }
            sql = sql.replaceFirst("#\\{.+}", String.valueOf(param));
        }
        log.info("\nSQL:" + sql);
        Connection connection = getConnection();
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            ps = connection.prepareStatement(sql);
            rs = ps.executeQuery();
            ReflectHelper helper = new ReflectHelper(newInstance);
            //获取全部成员变量
            Field[] fields = helper.getFields();
            while (rs.next()) {
                for (Field field : fields) {
                    String name = field.getName();
                    //给对象设置值
                    helper.setFieldValue(name, rs.getObject(name));
                }
            }
            return (T) newInstance;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (rs != null) {
                rs.close();
            }
            if (ps != null) {
                ps.close();
            }
        }
        return null;
    }

    /**
     * 更新方法
     * @param sql 传入的sql语句
     * @param param 方法参数值
     * @return
     * @throws Exception
     */
    @Override
    public Integer update(String sql, String parameterType, Object param) throws Exception {
        //创建传入的参数的反射工具类
        ReflectHelper helper = new ReflectHelper(param);
        List<Object> params = new ArrayList<>();
        //获取成员变量
        Field[] fields = helper.getFields();
        //遍历成员变量,获取每一个值,并存入变量值列表
        for (int i=0 ; i<fields.length ; i++) {
            //根据变量名获取变量值
            Object value = helper.getFieldValue(fields[i].getName());
            params.add(value);
        }
        //遍历成员遍历名,替换对应的占位符
        for (int i=0 ; i<fields.length ; i++) {
            sql = sql.replaceFirst("#\\{" + fields[i].getName() + "}", String.valueOf(params.get(i)));
        }
        log.info("\nSQL:" + sql);
        //获取数据库连接
        Connection connection = getConnection();
        PreparedStatement ps = null;
        try {
            ps = connection.prepareStatement(sql);
            //执行更新操作
            int i = ps.executeUpdate();
            return i;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        } finally {
            if (ps != null) {
                ps.close();
            }
        }
    }

    /**
     * 删除方法
     * @param sql 传入的sql语句
     * @param param 方法参数值
     * @return
     * @throws Exception
     */
    @Override
    public Integer delete(String sql, Object param) throws Exception {
        //判断sql语句中时否包含了#字符,#字符表示一个占位符,#{XXX},匹配一个参数值
        if (sql.contains("#")) {
            //判断参数是否为空
            if (param == null) {
                //如果为空,则未设置参数值,将异常抛出
                throw new Exception("该查找sql中没有设置对应参数");
            }
            sql = sql.replaceFirst("#\\{.+}", String.valueOf(param));
        }
        log.info("\nSQL:" + sql);
        Connection connection = getConnection();
        PreparedStatement ps = null;
        try {
            ps = connection.prepareStatement(sql);
            int i = ps.executeUpdate();
            return i;
        } catch (Exception e) {
            return 0;
        } finally {
            if (ps != null) {
                ps.close();
            }
        }
    }
}

因为是根据我的 mapper 配置文件写的,所以扩展性不是很好,你们可以进一步修改。
在查询和更新方法中有如下代码

			ReflectHelper helper = new ReflectHelper(newInstance);
            //获取全部成员变量
            Field[] fields = helper.getFields();
            //给对象设置值
            helper.setFieldValue(name, rs.getObject(name));
            //根据变量名获取变量值
            Object value = helper.getFieldValue(fields[i].getName());

这些操作都是在 ReflectHelper 类中封装好的,不过也是刚好满足我这次的需求而已,依然可以继续改进。下面看一下该类

public class ReflectHelper {

    /** 传入 Object 对象的 class 对象 */
    private Class clazz;
    /** 传入 Object 对象 */
    private Object object;
    /** 传入 Object 对象的所有成员变量 */
    private Field[] fields;
    /** 传入 Object 对象的所有成员变量的 get 方法 */
    private Hashtable<String, Method> getMethods = null;
    /** 传入 Object 对象的所有成员变量的 set 方法 */
    private Hashtable<String, Method> setMethods = null;

    public Field[] getFields() {
        return fields;
    }

    public Hashtable<String, Method> getGetMethods() {
        return getMethods;
    }

    public Hashtable<String, Method> getSetMethods() {
        return setMethods;
    }

    public ReflectHelper() {
    }

    public ReflectHelper(Object object) {
        this.object = object;
        initMethod();
    }

    /**
     * 初始化该对象的变量和setter,getter方法
     */
    private void initMethod() {
        //创建getter,setter集合
        getMethods = new Hashtable<>();
        setMethods = new Hashtable<>();
        //获取该对象的class
        clazz = object.getClass();
        //获取到成员变量列表并保存
        fields = clazz.getDeclaredFields();
        //获取到全部方法
        Method[] methods = clazz.getMethods();
        String gs = "get(\\w+)";
        String ss = "set(\\w+)";
        Pattern getM = Pattern.compile(gs);
        Pattern setM = Pattern.compile(ss);
        //遍历方法集合
        for (Method m : methods) {
            String name = m.getName();
            //判断方法名是否以get或者set开头
            if (Pattern.matches(gs, name)) {
                //如果get开头,则将get去掉并且将去掉后的字符串首字母小写
                String s = toLowerFirst(getM.matcher(name).replaceAll("$1"));
                //存入get方法集合中
                getMethods.put(s, m);
            } else if (Pattern.matches(ss, name)) {
                //如果set开头,则将set去掉并且将去掉后的字符串首字母小写
                String s = toLowerFirst(setM.matcher(name).replaceAll("$1"));
                //存入set方法集合中
                setMethods.put(s, m);
            } else {
                //如果开头get,set都不是,则不处理
            }
        }

    }

    /**
     * 给对象设置值
     * @param fieldName
     * @param value
     * @return
     */
    public boolean setFieldValue(String fieldName, Object value) {
        Method method = setMethods.get(fieldName);
        if (method != null) {
            boolean flag = false;
            try {
                method.invoke(object, value);
                flag = true;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                return flag;
            }
        } else {
            return false;
        }
    }

    public Object getFieldValue(String fieldName) {
        Method method = getMethods.get(fieldName);
        if (method != null) {
            try {
                Object invoke = method.invoke(object, null);
                return invoke;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * 转换首字母变为小写
     * @param s
     * @return
     */
    private String toLowerFirst(String s) {
        char[] chars = s.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }
}

该类就是将传入的 Object 对象解析出所有的成员变量,以及他们的 get,set 方法,并存储起来方便使用,并且提供了,对成员变量设第置值和获取值的方法。

第三步,就是ChampionSqlSession类,该类是将之前的 mapper 加载,以及执行器串起来形成完整的流程的一个类。

public class ChampionSqlSession {

    private ChampionConfiguration configuration;
    private ChampionExecutor executor;

    public ChampionSqlSession(String packagePath) {
        configuration = new ChampionConfiguration(packagePath);
        executor = new ChampionExecutor(configuration);
    }

    public Object selectOne(SQLFunction function, Object param) throws Exception {
        String sqlType = function.getSqlType();
        String parameterType = function.getParameterType();
        Object resultType = function.getResultType();
        String sql = function.getSql();
        switch (sqlType) {
            case "select" :
                try {
                    Object newInstance = executor.query(sql, resultType, param);
                    return newInstance;
                } catch (Exception e) {
                    e.printStackTrace();
                    return null;
                }
            case "insert" :
            case "update" :
                try {
                    Integer i = executor.update(sql, parameterType, param);
                    return i;
                } catch (Exception e) {
                    e.printStackTrace();
                    return 0;
                }
            case "delete" :
                try {
                    Integer i = executor.delete(sql, param);
                    return i;
                } catch (Exception e) {
                    e.printStackTrace();
                    return 0;
                }
            default:
                throw new Exception("mapper文件配置sql配置错误......");
        }
    }

    @SuppressWarnings("unchecked")
    public <T> T getMapper(Class clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
                new ChampionMapperProxy(configuration, this, clazz));
    }

该类中有两个方法,第一个是 selectOne 方法,它用来判断要执行方法中的 sql 是什么类型,然后根据类型的不同,调用执行器中不同的方法。第二个方法是 getMapper ,他通过动态代理获取到一个动态代理对象,这个动态代理对象每次调用方法都会执行 ChampionMapperProxy 这个动态代理类的 invoke 方法,动态代理类都必须实现 InvocationHandler 接口。如下所示:

public class ChampionMapperProxy implements InvocationHandler {

    private ChampionSqlSession sqlSession;
    private ChampionConfiguration configuration;
    private Class clazz;

    public ChampionMapperProxy(ChampionConfiguration configuration, ChampionSqlSession sqlSession, Class clazz) {
        this.configuration = configuration;
        this.sqlSession = sqlSession;
        this.clazz = clazz;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //获取configuration中的mappers集合
        List<MapperBean> mappers = configuration.getMappers();
        //如果集合中没有值则返回空
        if (mappers == null || mappers.size() == 0) {
            return null;
        } else {
            //获取该方法的接口名称
            String interfaceName = method.getDeclaringClass().getName();
            boolean exists = false;
            MapperBean mapper = null;
            //遍历集合
            for (MapperBean bean : mappers) {
                //如果集合中存在该接口,则将该mapperBean保存下来,并结束循环
                if (bean.getInterfaceName().equals(interfaceName)) {
                    exists = true;
                    mapper = bean;
                    break;
                }
            }
            //判断标识是否已获取到该接口的mapperBean
            if (exists) {
                //有则取其中的方法
                List<SQLFunction> functions = mapper.getFunctions();
                //判断方法集合是否为空
                if (functions != null && functions.size() > 0) {
                    //遍历方法集合
                    for (SQLFunction function : functions) {
                        //如果方法名跟调用方法名相等
                        if (function.getFunctionName().equals(method.getName())) {
                            //将调用sqlSession的selectOne方法,传入方法对象以及参数值
                            return sqlSession.selectOne(function, args[0]);
                        }
                    }
                }
            }
            return null;
        }
    }
}

在 invoke 方法中我们可以看到,他根本就没有执行外部调用的那个方法,而是去执行 sqlSession 的 selectOne 方法,传入获取的 mapper 信息,和外部调用时的实际参数,最后返回这个结果。也就是说在外部通过 ChampionSqlSession 类获取的接口,在调用过程中,实际上执行的是 ChampionExecutor 类中的方法,通过 mapper 文件的配置信息,操作数据库并返回。

最后,看一下测试类,以及结果。

public class ChampionBatisTest {

    @Test
    public void run() throws InterruptedException {
        Logger log = Logger.getLogger(ChampionBatisTest.class);
        ChampionSqlSession sqlSession = new ChampionSqlSession("com.champion.mybatis.mapper");
        AccountInfoMapper mapper = sqlSession.getMapper(AccountInfoMapper.class);
        AccountInfo accountInfo = mapper.selectByPrimaryKey(1);
        log.info(accountInfo.toString());

        AccountInfo acc = new AccountInfo(2, "李四", "123", 17.00);
        mapper.insert(acc);
        AccountInfo acc2 = mapper.selectByPrimaryKey(2);
        log.info(acc2.toString());

        acc.setMoney(27.00);
        mapper.updateByPrimaryKey(acc);
        AccountInfo acc3 = mapper.selectByPrimaryKey(2);
        log.info(acc3.toString());

        mapper.deleteByPrimaryKey(2);
        AccountInfo acc5 = mapper.selectByPrimaryKey(2);
        log.info(acc5.toString());
    }
}

结果:
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.readMapperXML(ChampionConfiguration.java:91)]  - C:/IDEA/workplace/champion-mybatis/src/main/java/com/champion/mybatis/mapper
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.query(ChampionExecutor.java:64)]  - 
SQL:
    select id,username,password,money
    from account_info
    where id = 1
  
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.loadProperties(ChampionConfiguration.java:58)]  - driver:com.mysql.jdbc.Driver
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.loadProperties(ChampionConfiguration.java:59)]  - url:jdbc:mysql://127.0.0.1:3306/spring_day03?useUnicode=true&characterEncoding=utf-8
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.loadProperties(ChampionConfiguration.java:60)]  - username:root
[INFO] [com.champion.mybatis.sqlSession.ChampionConfiguration.loadProperties(ChampionConfiguration.java:61)]  - password:123456
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.getConnection(ChampionExecutor.java:34)]  - 调用一次connection
[INFO] [com.championBatis.test.ChampionBatisTest.run(ChampionBatisTest.java:24)]  - AccountInfo{id=1, username='张三', password='123', money=12.0}
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.update(ChampionExecutor.java:119)]  - 
SQL:
    insert into account_info (id, username, password, money)
    values (2, '李四', '123', 17.0)
  
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.query(ChampionExecutor.java:64)]  - 
SQL:
    select id,username,password,money
    from account_info
    where id = 2
  
[INFO] [com.championBatis.test.ChampionBatisTest.run(ChampionBatisTest.java:29)]  - AccountInfo{id=2, username='李四', password='123', money=17.0}
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.update(ChampionExecutor.java:119)]  - 
SQL:
    update account_info
    set username = '李四',
      password = '123',
      money = 27.0
    where id = 2
  
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.query(ChampionExecutor.java:64)]  - 
SQL:
    select id,username,password,money
    from account_info
    where id = 2
  
[INFO] [com.championBatis.test.ChampionBatisTest.run(ChampionBatisTest.java:34)]  - AccountInfo{id=2, username='李四', password='123', money=27.0}
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.delete(ChampionExecutor.java:156)]  - 
SQL:
    delete from account_info
    where id = 2
  
[INFO] [com.champion.mybatis.sqlSession.ChampionExecutor.query(ChampionExecutor.java:64)]  - 
SQL:
    select id,username,password,money
    from account_info
    where id = 2
  
[INFO] [com.championBatis.test.ChampionBatisTest.run(ChampionBatisTest.java:38)]  - AccountInfo{id=0, username='null', password='null', money=0.0}

Process finished with exit code 0

给自己一些学习的记录,也希望能帮助看到这里的你。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值