深挖 Mybatis 源码:缓存模块

简介: MyBatis 中的缓存分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是 Cache 接口的实现。在这篇文章里,我们就来分析 Cache 接口以及多个实现类的具体实现。

本文选自 Doocs 开源社区旗下“源码猎人”项目,作者 AmyliaY。


项目将会持续更新,欢迎 Star 关注。


项目地址:https://github.com/doocs/source-code-hunter


MyBatis 中的缓存分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是 Cache 接口的实现。在这篇文章里,我们就来分析 Cache 接口以及多个实现类的具体实现。


1 Cache 组件


MyBatis 中缓存模块相关的代码位于 org.apache.ibatis.cache 包 下,其中 Cache 接口 是缓存模块中最核心的接口,它定义了所有缓存的基本行为。


public interface Cache {  /**   * 获取当前缓存的 Id   */  String getId();  /**   * 存入缓存的 key 和 value,key 一般为 CacheKey对象   */  void putObject(Object key, Object value);  /**   * 根据 key 获取缓存值   */  Object getObject(Object key);  /**   * 删除指定的缓存项   */  Object removeObject(Object key);  /**   * 清空缓存   */  void clear();  /**   * 获取缓存的大小   */  int getSize();  /**   * !!!!!!!!!!!!!!!!!!!!!!!!!!   * 获取读写锁,可以看到,这个接口方法提供了默认的实现!!   * 这是 Java8 的新特性!!只是平时开发时很少用到!!!   * !!!!!!!!!!!!!!!!!!!!!!!!!!   */  default ReadWriteLock getReadWriteLock() {    return null;  }}


如下图所示,Cache 接口 的实现类有很多,但大部分都是装饰器,只有 PerpetualCache 提供了 Cache 接口 的基本实现。


11.png


1.1 PerpetualCache


PerpetualCache(Perpetual:永恒的,持续的)在缓存模块中扮演着被装饰的角色,其实现比较简单,底层使用 HashMap 记录缓存项,也是通过该 HashMap 对象 的方法实现的 Cache 接口 中定义的相应方法。


public class PerpetualCache implements Cache {  // Cache对象 的唯一标识  private final String id;  // 其所有的缓存功能实现,都是基于 JDK 的 HashMap 提供的方法  private Map<Object, Object> cache = new HashMap<>();  public PerpetualCache(String id) {    this.id = id;  }  @Override  public String getId() {    return id;  }  @Override  public int getSize() {    return cache.size();  }  @Override  public void putObject(Object key, Object value) {    cache.put(key, value);  }  @Override  public Object getObject(Object key) {    return cache.get(key);  }  @Override  public Object removeObject(Object key) {    return cache.remove(key);  }  @Override  public void clear() {    cache.clear();  }  /**   * 其重写了 Object 中的 equals() 和 hashCode()方法,两者都只关心 id字段   */  @Override  public boolean equals(Object o) {    if (getId() == null) {      throw new CacheException("Cache instances require an ID.");    }    if (this == o) {      return true;    }    if (!(o instanceof Cache)) {      return false;    }    Cache otherCache = (Cache) o;    return getId().equals(otherCache.getId());  }  @Override  public int hashCode() {    if (getId() == null) {      throw new CacheException("Cache instances require an ID.");    }    return getId().hashCode();  }}


下面来看一下 cache.decorators 包 下提供的装饰器,它们都直接实现了 Cache 接口,扮演着装饰器的角色。这些装饰器会在 PerpetualCache 的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求。


1.2 BlockingCache


BlockingCache 是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据。


public class BlockingCache implements Cache {  // 阻塞超时时长  private long timeout;  // 持有的被装饰者  private final Cache delegate;  // 每个 key 都有其对应的 ReentrantLock锁对象  private final ConcurrentHashMap<Object, ReentrantLock> locks;  // 初始化 持有的持有的被装饰者 和 锁集合  public BlockingCache(Cache delegate) {    this.delegate = delegate;    this.locks = new ConcurrentHashMap<>();  }}


假设 线程 A 在 BlockingCache 中未查找到 keyA 对应的缓存项时,线程 A 会获取 keyA 对应的锁,这样,线程 A 在后续查找 keyA 时,其它线程会被阻塞。


// 根据 key 获取锁对象,然后上锁  private void acquireLock(Object key) {    // 获取 key 对应的锁对象    Lock lock = getLockForKey(key);    // 获取锁,带超时时长    if (timeout > 0) {      try {        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);        if (!acquired) { // 超时,则抛出异常          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());        }      } catch (InterruptedException e) {        // 如果获取锁失败,则阻塞一段时间        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);      }    } else {      // 上锁      lock.lock();    }  }  private ReentrantLock getLockForKey(Object key) {    // Java8 新特性,Map系列类 中新增的方法    // V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)    // 表示,若 key 对应的 value 为空,则将第二个参数的返回值存入该 Map集合 并返回    return locks.computeIfAbsent(key, k -> new ReentrantLock());  }


假设 线程 A 从数据库中查找到 keyA 对应的结果对象后,将结果对象放入到 BlockingCache 中,此时 线程 A 会释放 keyA 对应的锁,唤醒阻塞在该锁上的线程。其它线程即可从 BlockingCache 中获取 keyA 对应的数据,而不是再次访问数据库。


@Override  public void putObject(Object key, Object value) {    try {      // 存入 key 和其对应的缓存项      delegate.putObject(key, value);    } finally {      // 最后释放锁      releaseLock(key);    }  }  private void releaseLock(Object key) {    ReentrantLock lock = locks.get(key);    // 锁是否被当前线程持有    if (lock.isHeldByCurrentThread()) {      // 是,则释放锁      lock.unlock();    }  }


1.3 FifoCache 和 LruCache


在很多场景中,为了控制缓存的大小,系统需要按照一定的规则清理缓存。FifoCache 是先入先出版本的装饰器,当向缓存添加数据时,如果缓存项的个数已经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。


public class FifoCache implements Cache {  // 被装饰对象  private final Cache delegate;  // 用一个 FIFO 的队列记录 key 的顺序,其具体实现为 LinkedList  private final Deque<Object> keyList;  // 决定了缓存的容量上限  private int size;  // 国际惯例,通过构造方法初始化自己的属性,缓存容量上限默认为 1024个  public FifoCache(Cache delegate) {    this.delegate = delegate;    this.keyList = new LinkedList<>();    this.size = 1024;  }  @Override  public String getId() {    return delegate.getId();  }  @Override  public int getSize() {    return delegate.getSize();  }  public void setSize(int size) {    this.size = size;  }  @Override  public void putObject(Object key, Object value) {    // 存储缓存项之前,先在 keyList 中注册    cycleKeyList(key);    // 存储缓存项    delegate.putObject(key, value);  }  private void cycleKeyList(Object key) {    // 在 keyList队列 中注册要添加的 key    keyList.addLast(key);    // 如果注册这个 key 会超出容积上限,则把最老的一个缓存项清除掉    if (keyList.size() > size) {      Object oldestKey = keyList.removeFirst();      delegate.removeObject(oldestKey);    }  }  @Override  public Object getObject(Object key) {    return delegate.getObject(key);  }  @Override  public Object removeObject(Object key) {    return delegate.removeObject(key);  }  // 除了清理缓存项,还要清理 key 的注册列表  @Override  public void clear() {    delegate.clear();    keyList.clear();  }}


LruCache 是按照"近期最少使用算法"(Least Recently Used, LRU)进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。


public class LruCache implements Cache {  // 被装饰者  private final Cache delegate;  // 这里使用的是 LinkedHashMap,它继承了 HashMap,但它的元素是有序的  private Map<Object, Object> keyMap;  // 最近最少被使用的缓存项的 key  private Object eldestKey;  // 国际惯例,构造方法中进行属性初始化  public LruCache(Cache delegate) {    this.delegate = delegate;    // 这里初始化了 keyMap,并定义了 eldestKey 的取值规则    setSize(1024);  }  public void setSize(final int size) {    // 初始化 keyMap,同时指定该 Map 的初始容积及加载因子,第三个参数true 表示 该LinkedHashMap    // 记录的顺序是 accessOrder,即,LinkedHashMap.get()方法 会改变其中元素的顺序    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {      private static final long serialVersionUID = 4267176411845948333L;      // 当调用 LinkedHashMap.put()方法 时,该方法会被调用      @Override      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {        boolean tooBig = size() > size;        if (tooBig) {          // 当已达到缓存上限,更新 eldestKey字段,后面将其删除          eldestKey = eldest.getKey();        }        return tooBig;      }    };  }  // 存储缓存项  @Override  public void putObject(Object key, Object value) {    delegate.putObject(key, value);    // 记录缓存项的 key,超出容量则清除最久未使用的缓存项    cycleKeyList(key);  }  private void cycleKeyList(Object key) {    keyMap.put(key, key);    // eldestKey 不为空,则表示已经达到缓存上限    if (eldestKey != null) {      // 清除最久未使用的缓存      delegate.removeObject(eldestKey);      // 制空      eldestKey = null;    }  }  @Override  public Object getObject(Object key) {    // 访问 key元素 会改变该元素在 LinkedHashMap 中的顺序    keyMap.get(key); //touch    return delegate.getObject(key);  }  @Override  public String getId() {    return delegate.getId();  }  @Override  public int getSize() {    return delegate.getSize();  }  @Override  public Object removeObject(Object key) {    return delegate.removeObject(key);  }  @Override  public void clear() {    delegate.clear();    keyMap.clear();  }}


1.4 SoftCache 和 WeakCache


在分析 SoftCache 和 WeakCache 实现之前,我们再温习一下 Java 提供的 4 种引用类型,强引用 StrongReference、软引用 SoftReference、弱引用 WeakReference 和虚引用 PhantomReference。


强引用 平时用的最多的,如 Object obj = new Object(),新建的 Object 对象 就是被强引用的。如果一个对象被强引用,即使是 JVM 内存空间不足,要抛出 OutOfMemoryError 异常,GC 也绝不会回收该对象。

软引用 仅次于强引用的一种引用,它使用类 SoftReference 来表示。当 JVM 内存不足时,GC 会回收那些只被软引用指向的对象,从而避免内存溢出。软引用适合引用那些可以通过其他方式恢复的对象,例如, 数据库缓存中的对象就可以从数据库中恢复,所以软引用可以用来实现缓存,下面要介绍的 SoftCache 就是通过软引用实现的。


另外,由于在程序使用软引用之前的某个时刻,其所指向的对象可能己经被 GC 回收掉了,所以通过 Reference.get()方法 来获取软引用所指向的对象时,总是要通过检查该方法返回值是否为 null,来判断被软引用的对象是否还存活。


弱引用 弱引用使用 WeakReference 表示,它不会阻止所引用的对象被 GC 回收。在 JVM 进行垃圾回收时,如果指向一个对象的所有引用都是弱引用,那么该对象会被回收。所以,只被弱引用所指向的对象,其生存周期是 两次 GC 之间 的这段时间,而只被软引用所指向的对象可以经历多次 GC,直到出现内存紧张的情况才被回收。


虚引用 最弱的一种引用类型,由类 PhantomReference 表示。虚引用可以用来实现比较精细的内存使用控制,但很少使用。


引用队列(ReferenceQueue ) 很多场景下,我们的程序需要在一个对象被 GC 时得到通知,引用队列就是用于收集这些信息的队列。在创建 SoftReference 对象 时,可以为其关联一个引用队列,当 SoftReference 所引用的对象被 GC 时, JVM 就会将该 SoftReference 对象 添加到与之关联的引用队列中。当需要检测这些通知信息时,就可以从引用队列中获取这些 SoftReference 对象。不仅是 SoftReference,弱引用和虚引用都可以关联相应的队列。


现在来看一下 SoftCache 的具体实现。


public class SoftCache implements Cache {  // 这里使用了 LinkedList 作为容器,在 SoftCache 中,最近使用的一部分缓存项不会被 GC  // 这是通过将其 value 添加到 hardLinksToAvoidGarbageCollection集合 实现的(即,有强引用指向其value)  private final Deque<Object> hardLinksToAvoidGarbageCollection;  // 引用队列,用于记录已经被 GC 的缓存项所对应的 SoftEntry对象  private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;  // 持有的被装饰者  private final Cache delegate;  // 强连接的个数,默认为 256  private int numberOfHardLinks;  // 构造方法进行属性的初始化  public SoftCache(Cache delegate) {    this.delegate = delegate;    this.numberOfHardLinks = 256;    this.hardLinksToAvoidGarbageCollection = new LinkedList<>();    this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();  }  private static class SoftEntry extends SoftReference<Object> {    private final Object key;    SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {      // 指向 value 的引用是软引用,并且关联了 引用队列      super(value, garbageCollectionQueue);      // 强引用      this.key = key;    }  }  @Override  public void putObject(Object key, Object value) {    // 清除已经被 GC 的缓存项    removeGarbageCollectedItems();    // 添加缓存    delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));  }  private void removeGarbageCollectedItems() {    SoftEntry sv;    // 遍历 queueOfGarbageCollectedEntries集合,清除已经被 GC 的缓存项 value    while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {      delegate.removeObject(sv.key);    }  }  @Override  public Object getObject(Object key) {    Object result = null;    @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache      // 用一个软引用指向 key 对应的缓存项      SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);    // 检测缓存中是否有对应的缓存项    if (softReference != null) {      // 获取 softReference 引用的 value      result = softReference.get();      // 如果 softReference 引用的对象已经被 GC,则从缓存中清除对应的缓存项      if (result == null) {        delegate.removeObject(key);      } else {        synchronized (hardLinksToAvoidGarbageCollection) {          // 将缓存项的 value 添加到 hardLinksToAvoidGarbageCollection集合 中保存          hardLinksToAvoidGarbageCollection.addFirst(result);          // 如果 hardLinksToAvoidGarbageCollection 的容积已经超过 numberOfHardLinks          // 则将最老的缓存项从 hardLinksToAvoidGarbageCollection 中清除,FIFO          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {            hardLinksToAvoidGarbageCollection.removeLast();          }        }      }    }    return result;  }  @Override  public Object removeObject(Object key) {    // 清除指定的缓存项之前,也会先清理被 GC 的缓存项    removeGarbageCollectedItems();    return delegate.removeObject(key);  }  @Override  public void clear() {    synchronized (hardLinksToAvoidGarbageCollection) {      // 清理强引用集合      hardLinksToAvoidGarbageCollection.clear();    }    // 清理被 GC 的缓存项    removeGarbageCollectedItems();    // 清理最底层的缓存项    delegate.clear();  }  @Override  public String getId() {    return delegate.getId();  }  @Override  public int getSize() {    removeGarbageCollectedItems();    return delegate.getSize();  }  public void setSize(int size) {    this.numberOfHardLinks = size;  }}


WeakCache 的实现与 SoftCache 基本类似,唯一的区别在于其中使用 WeakEntry(继承了 WeakReference)封装真正的 value 对象,其他实现完全一样。


另外,还有 ScheduledCache、LoggingCache、SynchronizedCache、SerializedCache 等。ScheduledCache 是周期性清理缓存的装饰器,它的 clearInterval 字段 记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear 字段 记录了最近一次清理的时间戳。ScheduledCache 的 getObject()、putObject()、removeObject() 等核心方法,在执行时都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。


LoggingCache 在 Cache 的基础上提供了日志功能,它通过 hit 字段 和 request 字段 记录了 Cache 的命中次数和访问次数。在 LoggingCache.getObject()方法 中,会统计命中次数和访问次数 这两个指标,井按照指定的日志输出方式输出命中率。


SynchronizedCache 通过在每个方法上添加 synchronized 关键字,为 Cache 添加了同步功能,有点类似于 JDK 中 Collections 的 SynchronizedCollection 内部类。


SerializedCache 提供了将 value 对象 序列化的功能。SerializedCache 在添加缓存项时,会将 value 对应的 Java 对象 进行序列化,井将序列化后的 byte[]数组 作为 value 存入缓存 。SerializedCache 在获取缓存项时,会将缓存项中的 byte[]数组 反序列化成 Java 对象。不使用 SerializedCache 装饰器 进行装饰的话,每次从缓存中获取同一 key 对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程,以及缓存中的对象。而使用 SerializedCache 每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。SerializedCache 使用的序列化方式是 Java 原生序列化。


2 CacheKey


在 Cache 中唯一确定一个缓存项,需要使用缓存项的 key 进行比较,MyBatis 中因为涉及 动态 SQL 等多方面因素, 其缓存项的 key 不能仅仅通过一个 String 表示,所以 MyBatis 提供了 CacheKey 类 来表示缓存项的 key,在一个 CacheKey 对象 中可以封装多个影响缓存项的因素。CacheKey 中可以添加多个对象,由这些对象共同确定两个 CacheKey 对象 是否相同。


public class CacheKey implements Cloneable, Serializable {  private static final long serialVersionUID = 1146682552656046210L;  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();  private static final int DEFAULT_MULTIPLYER = 37;  private static final int DEFAULT_HASHCODE = 17;  // 参与计算hashcode,默认值DEFAULT_MULTIPLYER = 37  private final int multiplier;  // 当前CacheKey对象的hashcode,默认值DEFAULT_HASHCODE = 17  private int hashcode;  // 校验和  private long checksum;  private int count;  // 由该集合中的所有元素 共同决定两个CacheKey对象是否相同,一般会使用一下四个元素  // MappedStatement的id、查询结果集的范围参数(RowBounds的offset和limit)  // SQL语句(其中可能包含占位符"?")、SQL语句中占位符的实际参数  private List<Object> updateList;  // 构造方法初始化属性  public CacheKey() {    this.hashcode = DEFAULT_HASHCODE;    this.multiplier = DEFAULT_MULTIPLYER;    this.count = 0;    this.updateList = new ArrayList<>();  }  public CacheKey(Object[] objects) {    this();    updateAll(objects);  }  public void update(Object object) {    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);    // 重新计算count、checksum和hashcode的值    count++;    checksum += baseHashCode;    baseHashCode *= count;    hashcode = multiplier * hashcode + baseHashCode;    // 将object添加到updateList集合    updateList.add(object);  }  public int getUpdateCount() {    return updateList.size();  }  public void updateAll(Object[] objects) {    for (Object o : objects) {      update(o);    }  }  /**   * CacheKey重写了 equals() 和 hashCode()方法,这两个方法使用上面介绍   * 的 count、checksum、hashcode、updateList 比较两个 CacheKey对象 是否相同   */  @Override  public boolean equals(Object object) {    // 如果为同一对象,直接返回 true    if (this == object) {      return true;    }    // 如果 object 都不是 CacheKey类型,直接返回 false    if (!(object instanceof CacheKey)) {      return false;    }    // 类型转换一下    final CacheKey cacheKey = (CacheKey) object;    // 依次比较 hashcode、checksum、count,如果不等,直接返回 false    if (hashcode != cacheKey.hashcode) {      return false;    }    if (checksum != cacheKey.checksum) {      return false;    }    if (count != cacheKey.count) {      return false;    }    // 比较 updateList 中的元素是否相同,不同直接返回 false    for (int i = 0; i < updateList.size(); i++) {      Object thisObject = updateList.get(i);      Object thatObject = cacheKey.updateList.get(i);      if (!ArrayUtil.equals(thisObject, thatObject)) {        return false;      }    }    return true;  }  @Override  public int hashCode() {    return hashcode;  }  @Override  public String toString() {    StringJoiner returnValue = new StringJoiner(":");    returnValue.add(String.valueOf(hashcode));    returnValue.add(String.valueOf(checksum));    updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);    return returnValue.toString();  }  @Override  public CacheKey clone() throws CloneNotSupportedException {    CacheKey clonedCacheKey = (CacheKey) super.clone();    clonedCacheKey.updateList = new ArrayList<>(updateList);    return clonedCacheKey;  }}


全文完!


希望本文对大家有所帮助。如果感觉本文有帮助,有劳转发或点一下“在看”!让更多人收获知识!

目录
相关文章
|
2月前
|
缓存 Java 数据库连接
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
文章介绍了MyBatis的缓存机制,包括一级缓存和二级缓存的配置和使用,以及如何整合第三方缓存EHCache。详细解释了一级缓存的生命周期、二级缓存的开启条件和配置属性,以及如何通过ehcache.xml配置文件和logback.xml日志配置文件来实现EHCache的整合。
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
|
2月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
19天前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
34 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
25天前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
178 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
|
2月前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
155 24
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
|
11天前
|
缓存 Java 数据库连接
使用MyBatis缓存的简单案例
MyBatis 是一种流行的持久层框架,支持自定义 SQL 执行、映射及复杂查询。本文介绍了如何在 Spring Boot 项目中集成 MyBatis 并实现一级和二级缓存,以提高查询性能,减少数据库访问。通过具体的电商系统案例,详细讲解了项目搭建、缓存配置、实体类创建、Mapper 编写、Service 层实现及缓存测试等步骤。
|
22天前
|
缓存 NoSQL Ubuntu
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
42 3
|
26天前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
31 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
27天前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
3月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。