在使用框架的时候,运行起来比较容易,但是如果不了解底层运行机制,就很难灵活使用框架。我花了三天的时间去理解原理,这篇文章只是在基础查询的基础上进行分析,不涉及所有情况。能够基本了解MyBatis运行的机制,如何读取配置文件等。

(转载请注明出处:知乎 @唐Sir )

在继续往下读之前,读者需要了解以下两个基本知识

  1. java反射机制:反射机制是实现动态代理模式的核心。
  2. java代理模式(主要是动态代理):通过读取配置文件生成动态代理,在代理方法中利用反射机制封装结果集。

本文分析思路按照调用mybatis框架的顺序进行分析,首先了解如下mybatis持久化过程(以查询为例):

public static void main(String[] args)throws Exception {//1.读取配置文件InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");//2.创建SqlSessionFactory工厂SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();SqlSessionFactory factory = builder.build(in);//3.使用工厂生产SqlSession对象SqlSession session = factory.openSession();//4.使用SqlSession创建Dao接口的代理对象IUserDao userDao = session.getMapper(IUserDao.class);//5.使用代理对象执行方法List<User> users = userDao.findAll();for(User user : users){System.out.println(user);}//6.释放资源session.close();in.close();}

按照以上步骤分析如下

第一步:读取配置文件

Resources读取xml配置文件是利用类加载器读取的,返回输入流。代码如下:

/*** @author 唐Sir* 使用类加载器读取配置文件的类*/
public class Resources {/*** 根据传入的参数,获取一个字节输入流* @param filePath* @return*/public static InputStream getResourceAsStream(String filePath){return Resources.class.getClassLoader().getResourceAsStream(filePath);}
}

第二步:创建SqlSessionFactory工厂

这一步利用了构建者模式,本文最后将会讲解。首先创建了一个SqlSessionFactoryBuilder对象,调用build方法,方法参数为InputStream 类型。利用字节输入流,SqlSessionFactory Builder对象封装了Configuration对象,Configuration类如下:

/*** @author 唐Sir* 自定义mybatis的配置类*/
public class Configuration {private String driver;private String url;private String username;private String password;private Map<String,Mapper> mappers = new HashMap<String,Mapper>();public Map<String, Mapper> getMappers() {return mappers;}public void setMappers(Map<String, Mapper> mappers) {this.mappers.putAll(mappers);//此处需要使用追加的方式}public String getDriver() {return driver;}public void setDriver(String driver) {this.driver = driver;}public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}
}

这一步就利用了主配置文件封装Configuration对象,配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<!--导入约束-->
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--mysql主配置文件-->
<configuration><!-- 配置mysql环境--><environments default="mysql"><!--default和id的值相同即可--><environment id="mysql"><!--配置事务的类型(JDBC)--><transactionManager type="JDBC"></transactionManager><!--配置数据源(连接池)--><dataSource type="POOLED"><property name="driver" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/eexy_mybatis?serverTimezone=UTC&amp;useSSL=false"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></environment></environments><!--指定映射配置文件的位置 映射配置文件是指每个dao独立的配置文件--><mappers><mapper resource="dao/IUserdao.xml"/></mappers>
</configuration>

可以看到dataSource 的id值与Configuration对象的属性值是一一对应的,此外Configuration对象还多了一个Map集合,Map集合是用来存储mappers的映射文件的,映射文件xml文档如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.IUserDao"><select id="findAll" resultType="domain.User">select * from user</select><insert id="saveUser" parameterType="domain.User">
-- 配置插入操作后,获取插入操作的id
insert into user(username,address,sex,birthday) values (#{username}, #{address}, #{sex}, #{birthday})<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">select last_insert_id()</selectKey></insert><update id="updateUser" parameterType="domain.User">update user set username = #{username},address=#{address},sex=#{sex},birthday=#{birthday} where id = #{id}</update><update id="deleteUser" parameterType="Integer">delete from user where id=#{uId}</update><select id="getUserById" parameterType="Integer" resultType="domain.User">select * from user where id=#{uid}</select><select id="findByName" parameterType="String" resultType="domain.User">
--         select * from user where username like #{name}
--             like ? -> ?=%name%select * from user where username='%${value}%'
--         like %value%</select><select id="findTotal" resultType="int">select count(id) from user</select><select id="findByVo" parameterType="domain.QueryVo" resultType="domain.User">select * from user where username like #{user.username}</select>
</mapper>

Map的key值是String类型,代表namespace+id的值,Map的value是Mapper类型,它封装了sql语句和返回值类型(resultType),值得注意的是,以上mapper配置文件中的映射很多,每一个映射对应唯一的键值对,最后封装成Map集合。那么,为什么是Map集合不是List集合或者其他集合呢?答案是Map集合的查询速度更快,而我们主要用map集合来查询对应的value,进行封装动态代理对象。综上,简单的Mapper类的封装如下:

/*** @author 唐Sir* 用于封装执行的SQL语句和结果类型的全限定类名*/
public class Mapper {private String queryString;//SQL语句private String resultType;//结果类型的全限定类名public String getQueryString() {return queryString;}public void setQueryString(String queryString) {this.queryString = queryString;}public String getResultType() {return resultType;}public void setResultType(String resultType) {this.resultType = resultType;}
}

那么,Configuration对象的属性值和配置文件是怎样实现一一对应的呢?这时以XMLConfigBuilder 的XML解析器为例,将xml配置文件中的标签名和属性名读取,将值赋值给Configuration对象的属性,最终会返回一个封装好的Configuration对象,如果对解析过程感兴趣,参考以下代码和注释:

/*** @author 唐Sir*  用于解析配置文件*/
public class XMLConfigBuilder {/*** 解析主配置文件,把里面的内容填充到DefaultSqlSession所需要的地方* 使用的技术:*      dom4j+xpath*/public static Configuration loadConfiguration(InputStream config){try{//定义封装连接信息的配置对象(mybatis的配置对象)Configuration cfg = new Configuration();//1.获取SAXReader对象SAXReader reader = new SAXReader();//2.根据字节输入流获取Document对象Document document = reader.read(config);//3.获取根节点Element root = document.getRootElement();//4.使用xpath中选择指定节点的方式,获取所有property节点List<Element> propertyElements = root.selectNodes("//property");//5.遍历节点for(Element propertyElement : propertyElements){//判断节点是连接数据库的哪部分信息//取出name属性的值String name = propertyElement.attributeValue("name");if("driver".equals(name)){//表示驱动//获取property标签value属性的值String driver = propertyElement.attributeValue("value");cfg.setDriver(driver);}if("url".equals(name)){//表示连接字符串//获取property标签value属性的值String url = propertyElement.attributeValue("value");cfg.setUrl(url);}if("username".equals(name)){//表示用户名//获取property标签value属性的值String username = propertyElement.attributeValue("value");cfg.setUsername(username);}if("password".equals(name)){//表示密码//获取property标签value属性的值String password = propertyElement.attributeValue("value");cfg.setPassword(password);}}//取出mappers中的所有mapper标签,判断他们使用了resource还是class属性List<Element> mapperElements = root.selectNodes("//mappers/mapper");//遍历集合for(Element mapperElement : mapperElements){//判断mapperElement使用的是哪个属性Attribute attribute = mapperElement.attribute("resource");if(attribute != null){System.out.println("使用的是XML");//表示有resource属性,用的是XML//取出属性的值String mapperPath = attribute.getValue();//获取属性的值"com/itheima/dao/IUserDao.xml"//把映射配置文件的内容获取出来,封装成一个mapMap<String,Mapper> mappers = loadMapperConfiguration (mapperPath);//给configuration中的mappers赋值cfg.setMappers(mappers);}else{System.out.println("使用的是注解");//表示没有resource属性,用的是注解//获取class属性的值String daoClassPath = mapperElement.attributeValue("class");//根据daoClassPath获取封装的必要信息Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath);//给configuration中的mappers赋值cfg.setMappers(mappers);}}//返回Configurationreturn cfg;}catch(Exception e){throw new RuntimeException(e);}finally{try {config.close();}catch(Exception e){e.printStackTrace();}}}/*** 根据传入的参数,解析XML,并且封装到Map中* @param mapperPath    映射配置文件的位置* @return  map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)*          以及执行所需的必要信息(value是一个Mapper对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)*/private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException {InputStream in = null;try{//定义返回值对象Map<String,Mapper> mappers = new HashMap<String,Mapper>();//1.根据路径获取字节输入流in = Resources.getResourceAsStream(mapperPath);//2.根据字节输入流获取Document对象SAXReader reader = new SAXReader();Document document = reader.read(in);//3.获取根节点Element root = document.getRootElement();//4.获取根节点的namespace属性取值String namespace = root.attributeValue("namespace");//是组成map中key的部分//5.获取所有的select节点List<Element> selectElements = root.selectNodes("//select");//6.遍历select节点集合for(Element selectElement : selectElements){//取出id属性的值      组成map中key的部分String id = selectElement.attributeValue("id");//取出resultType属性的值  组成map中value的部分String resultType = selectElement.attributeValue("resultType");//取出文本内容            组成map中value的部分String queryString = selectElement.getText();//创建KeyString key = namespace+"."+id;//创建ValueMapper mapper = new Mapper();mapper.setQueryString(queryString);mapper.setResultType(resultType);//把key和value存入mappers中mappers.put(key,mapper);}return mappers;}catch(Exception e){throw new RuntimeException(e);}finally{in.close();}}/*** 根据传入的参数,得到dao中所有被select注解标注的方法。* 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息* @param daoClassPath* @return*/private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{//定义返回值对象Map<String,Mapper> mappers = new HashMap<String, Mapper>();//1.得到dao接口的字节码对象Class daoClass = Class.forName(daoClassPath);//2.得到dao接口中的方法数组Method[] methods = daoClass.getMethods();//3.遍历Method数组for(Method method : methods){//取出每一个方法,判断是否有select注解boolean isAnnotated = method.isAnnotationPresent(Select.class);if(isAnnotated){//创建Mapper对象Mapper mapper = new Mapper();//取出注解的value属性值Select selectAnno = method.getAnnotation(Select.class);String queryString = selectAnno.value();mapper.setQueryString(queryString);//获取当前方法的返回值,还要求必须带有泛型信息Type type = method.getGenericReturnType();//List<User>//判断type是不是参数化的类型if(type instanceof ParameterizedType){//强转ParameterizedType ptype = (ParameterizedType)type;//得到参数化类型中的实际类型参数Type[] types = ptype.getActualTypeArguments();//取出第一个Class domainClass = (Class)types[0];//获取domainClass的类名String resultType = domainClass.getName();//给Mapper赋值mapper.setResultType(resultType);}//组装key的信息//获取方法的名称String methodName = method.getName();String className = method.getDeclaringClass().getName();String key = className+"."+methodName;//给map赋值mappers.put(key,mapper);}}return mappers;}
}

因此,Configuration对象封装完成,这里详细关注一下Configuration对象的组成部分:除Map集合外,其余属性被用于创建mysql的JDBC链接对象Connection,这里用到了另一个工具类:

/*** @author 唐Sir* 用于创建数据源的工具类*/
public class DataSourceUtil {/*** 用于获取一个连接* @param cfg* @return*/public static Connection getConnection(Configuration cfg){try {Class.forName(cfg.getDriver());return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());}catch(Exception e){throw new RuntimeException(e);}}
}

而Configuration对象的Map集合被用于创建动态代理,这就是接下来的第三步了。在此之前,我们看一下Configuration对象封装完成后程序是如何运行的。SqlessionFactoryBuilder对象调用build方法,将会返回SqlSessionFactory实例对象DefaultSqlSessionFactory,SqlSessionFactory是一个接口,DefaultSqlSessionFactory对象实现了SqlSessionFactory接口,DefaultSqlSessionFactory对象需要传入封装好的Configuration对象,SqlessionFactoryBuilder的类代码如下:

public class SqlSessionFactoryBuilder {/*** 根据参数的字节输入流来构建一个SqlSessionFactory工厂*/public SqlSessionFactory build(InputStream config){Configuration cfg = XMLConfigBuilder.loadConfiguration(config);return  new DefaultSqlSessionFactory(cfg);}
}

传入封装好的Configuration对象并返回DefaultSqlSessionFactory对象后,接下来就是第三步。

第三步:使用工厂生产SqlSession对象

SqlSession是一个接口,工厂返回的是SqlSession的实例化对象DefaultSqlSession,DefaultSqlSession的构造器同样需要传入Configuration的对象。这里是简化版的,使用了工厂模式,工厂模式可以降低耦合度,后文会有讲解。代码如下:

public class DefaultSqlSessionFactory implements SqlSessionFactory{private Configuration cfg;public DefaultSqlSessionFactory(Configuration cfg){this.cfg = cfg;}/*** 用于创建一个新的操作数据库对象* @return*/@Overridepublic SqlSession openSession() {return new DefaultSqlSession(cfg);}
}
}

接下来进入核心部分!mybatis的灵魂!灵魂!

第四步:使用SqlSession创建Dao接口的代理对象

对大部分兢兢业业工作码代码的码农来说,mybatis最神奇的地方在于只要写好配置文件并传入接口,即可创建代理对象,我们不用写接口实现类,当然mybatis允许我们这么做,但是大多数情况不用多此一举,mybatis直接跳过了接口实现类创建出了其动态代理对象!废话不多说,继续深入分析。在此之前,你需要具备java反射机制和动态代理相关的基础知识,本文不会对反射机制和动态代理进行分析。

下面是一个SqlSession接口的类代码,当然,为了更好地简化代码,本文省略了大部分方法,只保留创建代理对象的getMapper()方法进行分析:

/*** 自定义Mybatis中和数据库交互的核心类*  它里面可以创建dao接口的代理对象*/
public interface SqlSession {/*** 根据参数创建一个代理对象* @param daoInterfaceClass dao的接口字节码*/<T> T getMapper(Class<T> daoInterfaceClass);/*** 释放资源*/void close();
}

SqlSession实现类DefaultSqlSession类的代码如下,首先大概了解一下,你可能在读到后文的时候需要反复查看此代码。

public class DefaultSqlSession implements SqlSession {private Configuration cfg;private Connection connection;public DefaultSqlSession(Configuration cfg){this.cfg = cfg;connection = DataSourceUtil.getConnection(cfg);}/*** 用于创建代理对象* @param daoInterfaceClass dao的接口字节码* @param <T>* @return*/@Overridepublic <T> T getMapper(Class<T> daoInterfaceClass) {return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(),new Class[]{daoInterfaceClass},new MapperProxy(cfg.getMappers(),connection));}/*** 用于释放资源*/@Overridepublic void close() {if(connection != null) {try {connection.close();} catch (Exception e) {e.printStackTrace();}}}
}

getMapper方法使用了动态代理模式,动态代理设计模式可以扩展实现类的功能等,后文将会对动态代理模式进行讲解。DefaultSqlSession 实现类才开始使用封装好的Configuration对象,正如前文所述,这里分别被用来创建jdbc链接和获取封装好的map集合,首先了解如何创建jdbc链接,这里比较简单,用一个工具类DataSourceUtil 对Configuration对象的属性进行读取和创建链接。DataSourceUtil 工具类的静态方法getConnection会返回一个Connection对象。Connection对象被传入DefaultSqlSession的构造器。

public class DataSourceUtil {/*** 用于获取一个连接* @param cfg* @return*/public static Connection getConnection(Configuration cfg){try {Class.forName(cfg.getDriver());return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());}catch(Exception e){throw new RuntimeException(e);}}
}

其次,Configuration对象的map集合也会被传入构造器,map集合在getMapper方法用来生成动态代理对象,在使用Proxy工具类生成动态代理对象时,类加载器为传入接口的加载器,接口为传入的接口,需要注意的是代理方法,这里我们需要利用一个InvocationHandler 的实现类MapperProxy 来重写invoke方法,需要传入的参数有两个,一个是前面创建好的Connection对象,另一个是Configuration对象中的map集合,map集合的值是配置文件mapper列表的namespace和id的值,为String,这个值一定是唯一的。因为其中namespace的值代表接口的全限定类名,id的值代表方法,全限定类名.方法一定是唯一的,所以一个全限定类型的方法组合的String作为key值唯一,如下图所示。

为了防止不好往上翻,这里再一次给出Mapper对象的属性:

public class Mapper {private String queryString;//SQL查询语句private String resultType;//结果对象的全限定类名public String getQueryString() {return queryString;}public void setQueryString(String queryString) {this.queryString = queryString;}public String getResultType() {return resultType;}public void setResultType(String resultType) {this.resultType = resultType;}
}

所以有了map集合,我们就可以根据map的key值也就是用户调用的方法找到唯一的value的值,也就是Mapper对象,前面提到过Mapper对象,它包含两个属性,分别是sql查询语句和结果对象的全限定类名。总而言之,我们的目的就是为了找到Mapper的值进行封装,那么问题就在于怎么找到这个value,毫无疑问是根据key值找到的,所以问题就变成了怎么找到key值,还记得我们传入的Configuration对象吗?该对象已经封装好了所有的map集合,用户在调用某个接口的某个方法的时候,实际上已经给我们提供了完整的key值,例如用户利用IUserDao的字节码文件返回一个sqlsession对象userDao,userDao的全限定类名为dao.userDao,调用userDao.findAll()方法,他的key值即为dao.userDao.findAll,我们去map集合里面找,就可以找到对应的Mapper对象了。这一过程的代码如下所示:

public class MapperProxy implements InvocationHandler {//map的key是全限定类名+方法名private Map<String,Mapper> mappers;private Connection conn;public MapperProxy(Map<String,Mapper> mappers,Connection conn){this.mappers = mappers;this.conn = conn;}/*** 用于对方法进行增强的,我们的增强其实就是调用selectList方法*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//1.获取方法名String methodName = method.getName();//2.获取方法所在类的名称String className = method.getDeclaringClass().getName();//3.组合keyString key = className+"."+methodName;//4.获取mappers中的Mapper对象Mapper mapper = mappers.get(key);//5.判断是否有mapperif(mapper == null){throw new IllegalArgumentException("传入的参数有误");}//6.调用工具类执行查询所有return new Executor().selectList(mapper,conn);}
}

这段代码中,前五步很好理解,第六步其实是调用了一个Executor类的selectList方法进行封装,Executor可以理解为接口的实现类,代理对象实际上调用的还是selectList方法,这里简单理解成对selectList方法的增强,Executor类selectList方法是根据mapper的属性和Connection对象来创建jdbc链接,再利用预处理对象执行mapper的sql语句(queryString属性),查询结果集最终被封装成mapper对象的resultType属性,resultType属性是String类型,这里用到了反射的原理,根据泛型动态生成结果集类型。Executor类代码如下:

/*** @author 唐Sir* 负责执行SQL语句,并且封装结果集*/
public class Executor {public <E> List<E> selectList(Mapper mapper, Connection conn) {PreparedStatement pstm = null;ResultSet rs = null;try {//1.取出mapper中的数据String queryString = mapper.getQueryString();//select * from userString resultType = mapper.getResultType();//com.itheima.domain.UserClass domainClass = Class.forName(resultType);//2.获取PreparedStatement对象pstm = conn.prepareStatement(queryString);//3.执行SQL语句,获取结果集rs = pstm.executeQuery();//4.封装结果集List<E> list = new ArrayList<E>();//定义返回值while(rs.next()) {//实例化要封装的实体类对象E obj = (E)domainClass.newInstance();//取出结果集的元信息:ResultSetMetaDataResultSetMetaData rsmd = rs.getMetaData();//取出总列数int columnCount = rsmd.getColumnCount();//遍历总列数for (int i = 1; i <= columnCount; i++) {//获取每列的名称,列名的序号是从1开始的String columnName = rsmd.getColumnName(i);//根据得到列名,获取每列的值Object columnValue = rs.getObject(columnName);//给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)PropertyDescriptor pd = new PropertyDescriptor (columnName, domainClass) ;//要求:实体类的属性和数据库表的列名保持一种//获取它的写入方法Method writeMethod = pd.getWriteMethod();//把获取的列的值,给对象赋值writeMethod.invoke(obj,columnValue);}//把赋好值的对象加入到集合中list.add(obj);}return list;} catch (Exception e) {throw new RuntimeException(e);} finally {release(pstm,rs);}}private void release(PreparedStatement pstm,ResultSet rs){if(rs != null){try {rs.close();}catch(Exception e){e.printStackTrace();}}if(pstm != null){try {pstm.close();}catch(Exception e){e.printStackTrace();}}}
}

因此,根据代理对象动态生成的方法实际上是调用了这个类,代理对象的作用是为了找到map集合中的Mapper,并传入Connection对象,最终才能根据这个对象去执行查询操作,封装结果集返回。至此全部的查询操作讲解完成。

对于这一块用到的三个设计模式,第一个是构建者模式,SqlSessionFactoryBuilder这一部分是为了隐藏内部实现细节,使开发者关注逻辑;第二个用到了工厂模式生成SqlSession对象,这是为了降低耦合度,在开发的过程中减少代码维护;第三个利用了代理模式,动态生成代理对象,对原有的方法(selectList)进行了扩展(查找map集合,传入Mapper对象)。详细过程参考如图所示:

整个调用过程的流程图如下所示:

本文声明:创作目的是为了学习,复习以及分享,如果有技术问题交流请给我留言,不接受不友好的评价,欢迎补充不足和探讨!

—————————作者:唐Sir 2020-04-02 22:58—————————

mybatis插入时间_深入分析MyBatis源码相关推荐

  1. http请求gmt时间_从Chrome源码看HTTP

    本篇解读基于Chromium 66.HTTP协议起很大作用的是http头,它主要是由一个个键值对组成的,例如Content-Type: text/html表示发送的数据是html格式,而Content ...

  2. 深入分析Ribbon源码分析

    本文来分析下Ribbon源码 文章目录 Ribbon源码分析 负载均衡器 AbstractLoadBalancer BaseLoadBalancer DynamicServerListLoadBala ...

  3. X11 Xlib截屏问题及深入分析二 —— 源码实现1

    接上一篇文章<X11 Xlib截屏问题及深入分析一 -- 源码位置>,链接为: X11 Xlib截屏问题及深入分析一 -- 源码位置_蓝天居士的博客-CSDN博客 上一篇文章讲到了源码包的 ...

  4. 【钟表识别】基于计算机视觉实现钟表时间识别含Matlab源码

    1 简介 基于计算机视觉实现钟表时间识别含Matlab源码​ 2 部分代码 function [time_clock]= read(filepath) I = imread(filepath); [e ...

  5. PHP大灌篮投篮游戏源码 微信+手机wap源码 带控制_大灌篮游戏源码

    内含详细安装教程,请严格按照文档来安装,顺序错了也会安装不起来. PHP大灌篮游戏源码,投篮游戏源码,手动提现 后台密码自己替换MD5 [完整源码链接] PHP大灌篮投篮游戏源码微信+手机wap源码带 ...

  6. HTML唯美雷姆时间动态特效网站源码+UI超好看

    正文: HTML唯美雷姆时间动态特效网站源码,一个UI非常好看的时钟网页源码,是动态的,但是由于我没办法放GIF图片,所以大家只能自己去体验了,演示图就是上方那个就是. 程序: wwwu.lanzou ...

  7. 框架源码专题:Mybatis启动和执行流程、源码级解析

    文章目录 1. Mybatis 启动流程 步骤一: 把xml配置文件解析成Configuration类 步骤二: 创建SqlSession会话 mybatis的三种执行器 步骤三: 在sqlSessi ...

  8. mybatis redis_SpringBoot + Mybatis + Shiro + mysql + redis智能平台源码分享

    后端技术栈 基于 SpringBoot + Mybatis + Shiro + mysql + redis构建的智慧云智能教育平台 基于数据驱动视图的理念封装 element-ui,即使没有 vue ...

  9. Eclipse中实现SpringBoot与Mybatis整合(图文教程带源码)

    场景 数据库中数据 实现效果 项目结构 前面参照 Eclipse中新建SpringBoot项目并输出HelloWorld https://blog.csdn.net/BADAO_LIUMANG_QIZ ...

  10. java版商城 springcloud+springboot+mybatis+redis+uniapp 多商户电子商务源码 直播带货源码 短视频带货源码 社交电商源码 分布式 微服务电子商务源码

    涉及平台:平台管理(包含自营店面).商家端(PC端.手机端).买家平台(PC端.H5/公众号.小程序.APP端(IOS/Android).微服务平台(业务服务) 核心架构:Spring Cloud.S ...

最新文章

  1. 客快物流大数据项目(十二):Docker的迁移与备份
  2. [lcm] Qualcomm平台的显示屏lcd驱动移植步骤
  3. [Kaggle] Sentiment Analysis on Movie Reviews(BERT)
  4. 题解 P2610 【[ZJOI2012]旅游】
  5. 雷电模拟器Android obb,exagear模拟器数据obb包
  6. 水域大小 Java_我的世界:Java版开发者们畅聊水域更新
  7. Tracking 1.3 Online Trackers
  8. i310100f和i310105f有什么区别 i3 10100f和i3 10105f 选哪个好
  9. XSS网站漏洞如何修复 大牛支招让您网站更安全
  10. es - elasticsearch search - DSL - decay functions
  11. 坚定文化新自信 提升文化软实力
  12. 定期存款遇调息怎么处理?
  13. 生信学习——基于R的可视化习题30个(附详细答案解读)
  14. 已解决NameError: name ‘unichr‘ is not defined
  15. RHEL8安装podman
  16. 入门学习duilib的要点
  17. 一些象素画、图标的网站收集
  18. 针对kindeditor编辑器的修改记录整理
  19. 计算机投标书开题报告,投标书开题报告.doc
  20. 偶然遇见斗鱼的小姐姐

热门文章

  1. 9.卷1(套接字联网API)---基本SCTP套接字编程
  2. 38. Linux 备份
  3. 7. 代码中特殊的注释技术——TODO、FIXME和XXX的用处
  4. 16. JavaScript Boolean(逻辑)对象
  5. [2019杭电多校第二场][hdu6599]I Love Palindrome String(回文自动机hash)
  6. rest syntax(parameters)
  7. python del 函数
  8. [OS] 进程相关知识点
  9. W3C小组宣布:HTML5标准制定完成
  10. JavaScript遍历DOM