IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 深入原理,仿写自己的Mybatis -> 正文阅读

[Java知识库]深入原理,仿写自己的Mybatis

MyBatis是一个持久层框架,使用简单,学习成本较低,所以是我们学习源码的首选框架。

为什么Mybatis可以根据我们的xml去执行相应的sql并将结果封装成实体呢,其实归功于Java中两个很重要的东西:反射、动态代理,这可以使你不去修改程序也不用重新编译,只需要修改xml文件就可以实现sql语句的修改,下面我们深入了解一下Mybatis的启动流程并简单的仿写一个自己的Mybatis

Mybatis层次结构

  • SqlSession: ,它是 MyBatis 核心 API,主要用来执行命令,获取映射,管理事务。接收开发人员提供 Statement Id 和参数。并返回操作结果。
  • Executor?:执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成以及查询缓存的维护。
  • StatementHandler?: 封装了JDBC Statement 操作,负责对 JDBC Statement 的操作,如设置参数、将Statement 结果集转换成 List 集合。
  • ParameterHandler?: 负责对用户传递的参数转换成 JDBC Statement 所需要的参数。
  • ResultSetHandler?: 负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合。
  • TypeHandler?: 用于 Java 类型和 JDBC 类型之间的转换。
  • MappedStatement?: 动态 SQL 的封装
  • SqlSource?: 表示从 XML 文件或注释读取的映射语句的内容,它创建将从用户接收的输入参数传递给数据库的 SQL。
  • Configuration: MyBatis 所有的配置信息都维持在 Configuration 对象之中。

下面开始仿写我们的Mybatis

目录结构

system--|

? ? ? ? BaseExecutor--基础的执行器

? ? ? ? Configuration--配置类,包含MappedStatement

? ? ? ? Executor--执行器接口

? ? ? ? MappedStatement--对应xml中的节点,其中包含方法的全限定名、sql、参数类型等

? ? ? ? XSqlSession--sqlsession工厂,实现接口的实例化及方法的暴露

utils--|

? ? ? ? UnderlineAndHumpUtil--驼峰与下划线互转

? ? ? ? XmlUtil--使用dom4j解析xml

?相关代码

首先是我们的sqlsession工厂中的逻辑,在我们创建一个sqlsession对象的时候需要传入配置文件的目录地址,以此来初始化Configuration对象,之后重写代理方法来执行我们的相关业务,在代理方法中我们可以看到其实数据库操作只需要实现查和改,因为删和增都可以用改来实现

XSqlSession.java代码如下

package com.system;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;

/**
 * @author xuwei
 */
public class XSqlSession implements InvocationHandler {
    private String className;
    private final Configuration configuration;

    public XSqlSession(String pathName) {
        this.configuration = new Configuration(pathName);
    }

    public Object getMapper(Class cls){
        className = cls.getName();
        return Proxy.newProxyInstance(
                cls.getClassLoader(),
                new Class[] { cls },
                this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //到configuration中去获取方法对应的MappedStatement
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(className+"."+method.getName());
        //建立数据库连接
        Class.forName(configuration.getDriver());
        Connection connection = DriverManager.getConnection(configuration.getUrl(), configuration.getUsername(), configuration.getPassword());
        //建立执行器
        BaseExecutor baseExecutor = new BaseExecutor();
        Object obj = null;
        if (mappedStatement.isQuery()){
            if (method.getReturnType().isInstance(new ArrayList<>())){
                obj = baseExecutor.queryList(connection,mappedStatement,args,configuration.isUnderlineAndHump());
            }else {
                obj = baseExecutor.queryOne(connection,mappedStatement,args,configuration.isUnderlineAndHump());
            }
        }else {
            obj =  baseExecutor.update(connection,mappedStatement,args);
        }
        connection.close();
        return obj;
    }
}

?Configuration.java

Configuration类中包含数据库连接的基本属性以及是否启用下划线和驼峰互转

还包含了我们所有xml中的业务sql,调用了我们的XMLUtil来解析并封装

package com.system;

import com.utils.XmlUtil;

import java.io.File;
import java.util.List;
import java.util.Map;

/**
 * @author xuwei
 */
public class Configuration {
    private String driver = null;
    private String url = null;
    private String username = null;
    private String password = null;
    private boolean underlineAndHump = false;
    private Map<String, MappedStatement> mappedStatementMap;

    public Configuration(String pathName) {
        File file = new File(pathName);
        if (file.exists()) {
            XmlUtil xmlUtil = new XmlUtil();
            Map<String, Object> config = xmlUtil.analysisConfig(file);
            this.driver = (String) config.get("driver");
            this.url = (String) config.get("url");
            this.username = (String) config.get("username");
            this.password = (String) config.get("password");
            this.underlineAndHump = "true".equals(config.get("underlineAndHump"));
            this.mappedStatementMap = xmlUtil.getMappedStatements((List<String>) config.get("mappers"));
        }
    }

    public String getDriver() {
        return driver;
    }

    public String getUrl() {
        return url;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public boolean isUnderlineAndHump() {
        return underlineAndHump;
    }

    public Map<String, MappedStatement> getMappedStatementMap() {
        return mappedStatementMap;
    }

    @Override
    public String toString() {
        return "Configuration{" +
                "driver='" + driver + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", underlineAndHump=" + underlineAndHump +
                ", mappedStatementMap=" + mappedStatementMap +
                '}';
    }
}

?MappedStatement.java

id为对应接口的全限定名

resultType为返回类型

paramterType为sql的返回值类型

query为标记sql是否为查询语句

sql就是我们具体的业务了

package com.system;

/**
 * @author xuwei
 */
public class MappedStatement {
    /**
     * sql节点的id,对应接口方法名
     */
    private final String id;
    /**
     * sql的返回类型参数
     */
    private final String resultType;
    /**
     * sql的入参类型
     */
    private final String parameterType;
    /**
     * sql是否为查询
     */
    private final boolean query;
    /**
     * sql语句
     */
    private final String sql;

    public MappedStatement(String id, String resultType, String parameterType, boolean query, String sql) {
        this.id = id;
        this.resultType = resultType;
        this.parameterType = parameterType;
        this.query = query;
        this.sql = sql;
    }

    public String getId() {
        return id;
    }

    public String getResultType() {
        return resultType;
    }

    public String getParameterType() {
        return parameterType;
    }

    public boolean isQuery() {
        return query;
    }

    public String getSql() {
        return sql;
    }

    @Override
    public String toString() {
        return "MappedStatement{" +
                "id='" + id + '\'' +
                ", resultType='" + resultType + '\'' +
                ", parameterType='" + parameterType + '\'' +
                ", query=" + query +
                ", sql='" + sql + '\'' +
                '}';
    }
}

ExeCutor.java

执行器接口,我们只实现基础执行器,目前不考虑带缓存的高级执行器,里面暴露三个接口:查询多个,对应返回的参数为多个对象的list;查询单个,对应查询单个对象;更新接口,用于增删改

package com.system;

import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

/**
 * @author xuwei
 */
public interface Executor {
    /**
     * 查询列表接口
     *
     * @param connection
     * @param mappedStatement
     * @param args
     * @param underlineAndHump
     * @return
     * @throws Exception
     */
    List<Object> queryList(Connection connection, MappedStatement mappedStatement, Object[] args, boolean underlineAndHump) throws Exception;

    /**
     * 查询单个接口
     *
     * @param connection
     * @param mappedStatement
     * @param args
     * @param underlineAndHump
     * @return
     * @throws SQLException
     * @throws ClassNotFoundException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    Object queryOne(Connection connection, MappedStatement mappedStatement, Object[] args, boolean underlineAndHump) throws SQLException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException;

    /**
     * 修改接口
     *
     * @param connection
     * @param mappedStatement
     * @param args
     * @return
     * @throws SQLException
     * @throws ClassNotFoundException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     */
    int update(Connection connection, MappedStatement mappedStatement, Object[] args) throws SQLException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException;
}

?BaseExecutor.java

封装JDBC,对查询的全过程全权负责

package com.system;

import com.utils.UnderlineAndHumpUtil;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author xuwei
 */
public class BaseExecutor implements Executor {

    @Override
    public List<Object> queryList(Connection connection, MappedStatement mappedStatement, Object[] args,boolean underlineAndHump) throws ClassNotFoundException, SQLException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
        if (mappedStatement.getResultType()==null){
            System.out.println("未定义resultType!");
            return null;
        }
        return resultToBean(mappedStatement.getResultType(), initPreparedStatement(connection, mappedStatement, args).executeQuery(),underlineAndHump);
    }

    @Override
    public Object queryOne(Connection connection, MappedStatement mappedStatement, Object[] args,boolean underlineAndHump) throws SQLException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
        if (mappedStatement.getResultType()==null){
            System.out.println("未定义resultType!");
            return null;
        }
        return resultToBean(mappedStatement.getResultType(), initPreparedStatement(connection, mappedStatement, args).executeQuery(),underlineAndHump).size()==0?null:resultToBean(mappedStatement.getResultType(), initPreparedStatement(connection, mappedStatement, args).executeQuery(),underlineAndHump).get(0);
    }

    @Override
    public int update(Connection connection, MappedStatement mappedStatement, Object[] args) throws SQLException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        return initPreparedStatement(connection, mappedStatement, args).executeUpdate();
    }

    public PreparedStatement initPreparedStatement(Connection connection, MappedStatement mappedStatement, Object[] args) throws SQLException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        String regex = "\\{([^}]*)}";
        Pattern pattern = Pattern.compile(regex);
        String sql = mappedStatement.getSql();
        Matcher matcher = pattern.matcher(sql);
        List<String> params = new ArrayList<>();
        PreparedStatement preparedStatement = null;
        //处理sql为预编译形式
        if (mappedStatement.getSql().contains("#{")) {
            if (mappedStatement.getParameterType() == null) {
                //如果采用预编译形式却未定义parameterType则报错
                System.out.println("未定义parameterType!");
                return null;
            }
            while (matcher.find()) {
                String param = matcher.group(1);
                params.add(param);
                sql = sql.replace("#{" + param + "}", "?");
            }
            preparedStatement = connection.prepareStatement(sql);
            switch (mappedStatement.getParameterType()) {
                case "int":
                case "java.lang.Integer":
                    int num = Integer.parseInt(args[0].toString());
                    preparedStatement.setInt(1, num);
                    break;
                case "java.lang.String":
                    String str = args[0].toString();
                    preparedStatement.setString(1, str);
                    break;
                default:
                    Class clazz = Class.forName(mappedStatement.getParameterType());
                    Object obj = args[0];
                    String name = "";
                    for (int i = 0; i < params.size(); i++) {
                        name = params.get(i).substring(0, 1).toUpperCase() + params.get(i).substring(1);
                        String methodName = "get" + name;
                        Method methodObj = clazz.getMethod(methodName);
                        //调用getter方法完成赋值
                        String value = methodObj.invoke(obj).toString();
                        preparedStatement.setString(i + 1, value);
                    }
                    break;
            }
        }else {
            preparedStatement = connection.prepareStatement(sql);
        }
        return preparedStatement;
    }
    public List<Object> resultToBean(String resultType,ResultSet resultSet,boolean underlineAndHump) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, SQLException, InstantiationException, ClassNotFoundException {
        Object obj = null;
        List<Object> objectList = new ArrayList<>();
        while (resultSet.next()){
            //反射创建对象
            Class clazz = Class.forName(resultType);
            obj = clazz.newInstance();
            //获取ResultSet数据
            ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
            //遍历实体类属性集合,依次将结果集中的值赋给属性
            Field[] fields = clazz.getDeclaredFields();
            for(int i = 0; i < fields.length; i++){
                Object value = setFieldValueByResultSet(fields[i],resultSetMetaData,resultSet,underlineAndHump);
                //通过属性名找到对应的setter方法
                String name = fields[i].getName();
                name = name.substring(0, 1).toUpperCase() + name.substring(1);
                String methodName = "set"+name;
                Method methodObj = clazz.getMethod(methodName,fields[i].getType());
                //调用setter方法完成赋值
                methodObj.invoke(obj, value);
            }
            objectList.add(obj);
        }
        return objectList;
    }


    public Object setFieldValueByResultSet(Field field,ResultSetMetaData rsmd,ResultSet rs,boolean underlineAndHump){
        Object result = null;
        try {
            int count = rsmd.getColumnCount();
            for(int i=1;i<=count;i++){
                if(field.getName().equals(underlineAndHump? UnderlineAndHumpUtil.underlineToHump(rsmd.getColumnName(i)):rsmd.getColumnName(i))){
                    String type = field.getType().getName();
                    switch (type) {
                        case "int":
                        case "java.lang.Integer":
                            result = rs.getInt(underlineAndHump?UnderlineAndHumpUtil.humpToUnderline(field.getName()):field.getName());
                            break;
                        case "java.lang.String":
                            result = rs.getString(underlineAndHump?UnderlineAndHumpUtil.humpToUnderline(field.getName()):field.getName());
                            break;
                        default:
                            break;
                    }
                }
            }
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return result;
    }
}

UnderlineAndHumpUtil.java

package com.utils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author xuwei
 */
public class UnderlineAndHumpUtil {
    private static final Pattern LINE_PATTERN = Pattern.compile("_(\\w)");
    private static final Pattern HUMP_PATTERN = Pattern.compile("[A-Z]");

    /**
     * 下划线转驼峰
     */
    public static String underlineToHump(String str) {
        str = str.toLowerCase();
        Matcher matcher = LINE_PATTERN.matcher(str);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    /**
     * 驼峰转下划线
     */
    public static String humpToUnderline(String str) {
        Matcher matcher = HUMP_PATTERN.matcher(str);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
        }
        matcher.appendTail(sb);
        return sb.toString();
    }
}

?XmlUtil.java

用来解析xml配置文件和xml业务代码

package com.utils;

import com.system.MappedStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * @author xuwei
 */
public class XmlUtil {
    /**
     * 解析Configuration
     *
     * @param file
     * @return
     */
    public Map<String, Object> analysisConfig(File file) {
        Map<String, Object> config = new HashMap<>();
        SAXReader reader = new SAXReader();
        List<String> mappers = new ArrayList<>();
        Document document = null;
        try {
            document = reader.read(file);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        assert document != null;
        Element root = document.getRootElement();
        List<Element> childElements = root.elements();
        for (Element child : childElements) {
            if (!child.elements().isEmpty()) {
                for (Element c : child.elements()) {
                    if ("driver".equals(c.attributeValue("name"))) {
                        config.put("driver", c.attributeValue("value"));
                    }
                    if ("url".equals(c.attributeValue("name"))) {
                        config.put("url", c.attributeValue("value"));
                    }
                    if ("username".equals(c.attributeValue("name"))) {
                        config.put("username", c.attributeValue("value"));
                    }
                    if ("password".equals(c.attributeValue("name"))) {
                        config.put("password", c.attributeValue("value"));
                    }
                    if ("underlineAndHump".equals(c.attributeValue("name"))) {
                        config.put("underlineAndHump", c.attributeValue("value"));
                    }
                    if (!"".equals(c.attributeValue("resource")) && c.attributeValue("resource") != null) {
                        mappers.add(c.attributeValue("resource"));
                    }
                }
            }
        }
        config.put("mappers", mappers);
        return config;
    }

    /**
     * 获取mapper
     * @param className
     * @param mappers
     * @return
     */
    public String getMapperFile(String className, List<String> mappers) {
        for (String mapper : mappers) {
            SAXReader reader = new SAXReader();
            Document document = null;
            try {
                document = reader.read(new File(mapper));
            } catch (DocumentException e) {
                e.printStackTrace();
            }
            assert document != null;
            if (className.equals(document.getRootElement().attributeValue("namespace"))) {
                return mapper;
            }
        }
        return null;
    }

    /**
     * 获取mapper
     * @param mappers
     * @return
     */
    public Map<String, MappedStatement> getMappedStatements(List<String> mappers) {
        Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
        for (String mapper : mappers) {
            SAXReader reader = new SAXReader();
            Document document = null;
            try {
                document = reader.read(new File(mapper));
            } catch (DocumentException e) {
                e.printStackTrace();
            }
            assert document != null;
            Element root = document.getRootElement();
            List<Element> childElements = root.elements();
            for (Element child : childElements) {
                MappedStatement mappedStatement = new MappedStatement(child.attributeValue("id"),
                        child.attributeValue("resultType"),
                        child.attributeValue("parameterType"),
                        "select".equals(child.getName()),
                        child.getTextTrim());
                mappedStatementMap.put(root.attributeValue("namespace") + "." + child.attributeValue("id"), mappedStatement);
            }
        }
        return mappedStatementMap;
    }
}

xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <dataSource>
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/user?serverTimezone=GMT&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;allowMultiQueries=true&amp;useAffectedRows=true&amp;useSSL=false&amp;zeroDateTimeBehavior=convertToNull"/>
        <property name="username" value="root"/>
        <property name="password" value="Gyj1113.."/>
    </dataSource>
    <setting>
        <property name="underlineAndHump" value="true"/>
    </setting>
    <mappers>
        <mapper resource="src/main/resources/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

?到此我们的所有部分就已经结束了,接下来我们测试一下

首先定义实体

package com.entity;

/**
 * @author xuwei
 */
public class User {
    private Integer id;
    private String realName;
    private String sex;
    private String address;

    public String getRealName() {
        return realName;
    }

    public void setRealName(String name) {
        this.realName = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", realName='" + realName + '\'' +
                ", sex='" + sex + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

方法接口

package com.mapper;

import com.entity.User;

import java.util.List;

/**
 * @author xuwei
 */
public interface UserMapper {
    /**
     * 查询所有
     *
     * @return List
     */
    List<User> findAll();

    /**
     * 查询单个
     *
     * @param id
     * @return User
     */
    User findById(Integer id);

    /**
     * 更新操作
     *
     * @param user
     * @return int
     */
    int updateUser(User user);

    /**
     * 添加操作
     *
     * @param user
     * @return int
     */
    int addUser(User user);

    /**
     * 删除操作
     *
     * @param id
     * @return int
     */
    int deleteUser(Integer id);
}

?mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.mapper.UserMapper">
    <select id="findAll" resultType="com.entity.User">
        select * from user;
    </select>
    <select id="findById" parameterType="int" resultType="com.entity.User">
        select * from user where id = #{id};
    </select>
    <update id="updateUser" parameterType="com.entity.User">
        update user set real_name = #{realName} , sex = #{sex} , address = #{address} where id = #{id};
    </update>
    <insert id="addUser" parameterType="com.entity.User">
        insert into user (real_name,sex,address) values (#{realName},#{sex},#{address});
    </insert>
    <delete id="deleteUser" parameterType="int">
        delete from user where id = #{id};
    </delete>
    <select id="find">

    </select>
</mapper>

测试

import com.entity.User;
import com.mapper.UserMapper;
import com.system.XSqlSession;
import org.junit.Test;

public class XWTest {
    @Test
    public void test() {
        XSqlSession xSqlSession = new XSqlSession("src/main/resources/xuwei-mybatis.xml");
        UserMapper userMapper = (UserMapper) xSqlSession.getMapper(UserMapper.class);
        //先查询一下数据库
        System.out.println(userMapper.findAll());
        //创建几个对象,保存到数据库中
        User user1 = new User();
        User user2 = new User();
        User user3 = new User();
        user1.setRealName("一号");
        user1.setSex("男");
        user1.setAddress("河北");
        user2.setRealName("二号");
        user2.setSex("女");
        user2.setAddress("广东");
        user3.setRealName("三号");
        user3.setSex("保密");
        user3.setAddress("北京");
        userMapper.addUser(user1);
        userMapper.addUser(user2);
        userMapper.addUser(user3);
        //再查询一下
        System.out.println(userMapper.findAll());
        //查询一个具体的,然后修改
        User queryOne = userMapper.findById(1);
        System.out.println(queryOne);
        queryOne.setSex("保密");
        userMapper.updateUser(queryOne);
        //看看是否修改成功了
        System.out.println(userMapper.findById(1));
        //删除一个
        userMapper.deleteUser(1);
        //查询
        System.out.println(userMapper.findAll());
    }
}

?测试结果

源码:

qihua99/xuwei-mybatis (github.com)https://github.com/qihua99/xuwei-mybatis

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-02-28 15:14:49  更:2022-02-28 15:16:48 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 11:37:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码