[{"content":"Java 并发编程 Java 是一种多线程编程语言，支持多线程的创建和管理。多线程编程可以提高程序的性能和响应能力，但也带来了线程安全和同步的问题。本文将介绍 Java 中的多线程编程，包括线程的创建、管理、同步和通信等方面。\n1. Java 多线程的创建 Java 中创建多线程有两种方式：继承 Thread 类和实现 Runnable 接口。\n1.1 继承 Thread 类 1 2 3 4 5 6 7 8 9 10 11 public class MyThread extends Thread { @Override public void run() { System.out.println(\u0026#34;Thread is running\u0026#34;); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 } } 1.2 实现 Runnable 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MyRunnable implements Runnable { @Override public void run() { System.out.println(\u0026#34;Thread is running\u0026#34;); } public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程 } } \u0026gt; 如何暂停线程呢，可以使用Thread.sleep(int millis)方法来暂停线程，参数是暂停的时间，单位是毫秒。 此模块比较基础，本文将不再赘述。\n2. Java 多线程安全 在多线程中，如果两个或多个线程同时访问一个资源（通常是变量），可能会导致数据不一致的情况，这时候就需要我们用一些机制来保证线程安全，来控制变量访问顺序。\n2.1 线程安全问题 接下来通过一个案例来演示线程安全问题： 在某些购物平台的双11活动期间，有时候一秒钟可能很多人同时订购一个商品，如果我们不去保证线程安全，可能会导致库存与平台显示的商品数量不一致的问题或者顺序混乱的问题：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package cn.programcx.multi.issue; public class Product { private static int count = 300; public void sell(){ if(count\u0026gt;0){ count--; System.out.println(Thread.currentThread().getName() +\u0026#34;:卖出一张，还剩 \u0026#34;+count+\u0026#34;张\u0026#34;); } } public int getCount(){ return count; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package cn.programcx.multi.issue; public class SellServices implements Runnable{ private Product product; public SellServices(Product product){ this.product = product; } @Override public void run(){ while(product.getCount()\u0026gt;0){ try { Thread.sleep(100); product.sell(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 package cn.programcx.multi.issue; public class Test { public static void main(String[] args) { for (int i = 0; i \u0026lt; 10; i++) { SellServices services = new SellServices(new Product()); Thread thread = new Thread(services); thread.start(); } } } 输出结果会出现数据一样的情况： 为什么会出现这个问题呢？ 请看这段代码：\n1 2 count--; System.out.println(Thread.currentThread().getName() +\u0026#34;:卖出一张，还剩 \u0026#34;+count+\u0026#34;张\u0026#34;); 如果两个同时执行到这段代码，可能会交叉执行count--和System.out.println，就可能整体出现这种代码顺序：\n1 2 3 4 count--; count--; System.out.println(Thread.currentThread().getName() +\u0026#34;:卖出一张，还剩 \u0026#34;+count+\u0026#34;张\u0026#34;); System.out.println(Thread.currentThread().getName() +\u0026#34;:卖出一张，还剩 \u0026#34;+count+\u0026#34;张\u0026#34;); 这就导致了数据不一致的问题。\n2.2 解决线程安全问题的几种方法 2.2.1 使用 synchronized 关键字 synchronized 关键字可以用来修饰方法或代码块，表示该方法或代码块是线程安全的。使用 synchronized 关键字可以保证同一时刻只有一个线程可以执行被修饰的方法或代码块。其它的线程会被阻塞等待，直到当前线程执行完毕。\n可以使用 synchronized 修饰实例方法： 比如一个实例方法：public void setCount(int count)，可以使用 synchronized 修饰这个方法，改为 public synchronized void setCount(int count)，这样就可以保证同一时刻只有一个线程可以执行这个方法。\n可以用 synchronized 包围代码块。\n上一个案例可以改为：\n1 2 3 4 5 6 7 public synchronized void sell(){ if(count\u0026gt;0){ count--; System.out.println(Thread.currentThread().getName() +\u0026#34;:卖出一张，还剩 \u0026#34;+count+\u0026#34;张\u0026#34;); } } 或者改为：\n1 2 3 4 5 6 7 8 9 public void sell(){ synchronized (this){ if(count\u0026gt;0){ count--; System.out.println(Thread.currentThread().getName() +\u0026#34;:卖出一张，还剩 \u0026#34;+count+\u0026#34;张\u0026#34;); } } } 这两种方式是等价的，都是对当前对象加锁。 但是如果我们使用了static修饰的方法，那么就会对类加锁，而不是对对象加锁。 这两种方式的区别在于：如果我们使用了static修饰的方法，那么就会对类加锁，而不是对对象加锁。\n运行修改后的代码，输出结果如下： synchronized 体现了两种特点：\n原子性：保证了同一时刻只有一个线程可以执行被修饰的方法或代码块。 可见性：保证了一个线程对共享变量的修改对其它线程是可见的。 但是 synchronized 也有一些缺点：\n性能开销：synchronized 的性能开销比较大，尤其是在高并发的情况下，可能会导致线程的阻塞和上下文切换。 死锁：如果多个线程同时持有锁，可能会导致死锁的情况。 可重入性：synchronized 是可重入的，即一个线程可以多次获得同一把锁，而不会导致死锁。 不能中断：synchronized 一旦获得锁，就不能被中断，可能会导致线程长时间等待。 不能指定锁的公平性：synchronized 是非公平锁，可能会导致线程饥饿的情况。 2.2.2 使用 ReentrantLock ReentrantLock 是 Java 并发包 java.util.concurrent.locks 下的一个可重入锁（Reentrant Lock），相比 synchronized，它提供了更灵活的锁定机制。我们可以通过自己控制加锁和释放锁还保证锁的公平性，也就是先到先得，它会按照线程请求锁的顺序来进行调度，避免线程“插队”。\n有以下常用方法：\n1 2 3 4 5 public void lock() // 获取锁 public void unlock() // 释放锁 public boolean tryLock() // 尝试获取锁，如果获取不到就返回 false public boolean tryLock(long timeout, TimeUnit unit) // 尝试获取锁，如果获取不到就等待指定的时间 public void lockInterruptibly() // 获取锁，如果被中断就抛出异常 上述方法中tryLock()加的锁是非阻塞的，也就是乐观锁，lockInterruptibly()、lock()、tryLock(long timeout, TimeUnit unit)是悲观锁，获取不到锁就会阻塞。\n前面的案例可以改为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package cn.programcx.multi.issue; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Product { private static int count = 300; private static final Lock lock = new ReentrantLock(true); // 创建锁对象，true表示公平锁 public void sell() { lock.lock(); // 获取锁 try { if (count \u0026gt; 0) { count--; System.out.println(Thread.currentThread().getName() + \u0026#34;: 卖出一张，还剩 \u0026#34; + count + \u0026#34;张\u0026#34;); } } finally { lock.unlock(); // 释放锁 } } public int getCount() { return count; } } 2.2.3 使用读写锁 ReentrantReadWriteLock 是比 ReetrantLock 更细粒度的锁，它允许多个线程同时读共享变量，但在写共享变量时，必须独占锁。它提供了两个锁：读锁和写锁。\n区分读锁和写锁的优势： 如果把读操作和写操作放在一个锁里面，那么读操作会被写操作阻塞，这样就会导致性能下降。可以通过区分读锁和写锁来提高性能：读锁：允许多个线程同时读共享变量，但是不允许其它线程进行写的操作；写锁：不允许其它线程进行读和写的操作。这种更细粒度的划分可以大大提高性能。\n比如有ReadData()和WriteData()两个方法，ReadData()方法是读操作，WriteData()方法是写操作。我们可以使用ReentrantReadWriteLock来实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package cn.programcx.multi.issue; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWrite { public static void main(String[] args) { ReadWriteThread rwThread = new ReadWriteThread(); // 创建多个写线程 for (int i = 0; i \u0026lt; 8; i++) { new Thread(rwThread::writeData, \u0026#34;写线程\u0026#34; + i).start(); } // 创建多个读线程 for (int i = 0; i \u0026lt; 3; i++) { new Thread(rwThread::readData, \u0026#34;读线程\u0026#34; + i).start(); } } } class ReadWriteThread{ private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.ReadLock lock = rwLock.readLock(); private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); static List\u0026lt;Integer\u0026gt; list = new CopyOnWriteArrayList\u0026lt;\u0026gt;(Arrays.asList(1,2,3,4,5,6,7,8,9,10)); public void writeData(){ try { writeLock.lock(); //获取写锁 int rd = new Random().nextInt(10); list.add(rd); Thread.sleep(10); //模拟写的时候消耗时间长 System.out.println(\u0026#34;已经写入:\u0026#34;+rd); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { writeLock.unlock(); //释放写锁 } } public void readData(){ try { lock.lock(); //获取读锁 list.forEach((i)-\u0026gt; System.out.print(i+\u0026#34; \u0026#34;)); System.out.println(); } finally { lock.unlock(); //释放读锁 } } } 输出结果如下： 从上面的输出结果可以看出，读操作在写操作后面，因为读锁会阻塞写锁。\n2.2.4 使用 AtomicInteger AtomicInteger 是 Java 并发包 java.util.concurrent.atomic 下的一个原子类，它提供了一些原子操作的方法，可以用来实现线程安全的计数器。它的性能比 synchronized 和 ReentrantLock 更高，因为它是基于 CAS（Compare And Swap）算法实现的。 CAS 是一种乐观锁的实现方式，它通过比较内存中的值和预期值，如果相等就交换，否则就不交换。这样就可以避免线程的阻塞和上下文切换，提高性能。\nAtomicInteger 的常用方法：\n1 2 3 4 5 6 7 8 public int get() // 获取当前值 public int incrementAndGet() // 自增 1，并返回自增后的值 public int getAndIncrement() // 自增 1，并返回自增前的值 public int decrementAndGet() // 自减 1，并返回自减后的值 public int getAndDecrement() // 自减 1，并返回自减前的值 public int add(int delta) // 自增 delta，并返回自增后的值 public int getAndAdd(int delta) // 自增 delta，并返回自增前的值 public AtomicInteger atomicInteger = new AtomicInteger(int initialValue) // 创建一个 AtomicInteger 对象 第一个案例我们可以改为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package cn.programcx.multi.issue; import java.util.concurrent.atomic.AtomicInteger; public class Product { private static final AtomicInteger count = new AtomicInteger(300); public boolean sell() { int remaining = count.decrementAndGet(); if (remaining \u0026gt;= 0) { System.out.println(Thread.currentThread().getName() + \u0026#34; 卖出一张票，剩余：\u0026#34; + (remaining)); return true; } else { return false; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package cn.programcx.multi.issue; public class SellServices implements Runnable{ private Product product; public SellServices(Product product){ this.product = product; } @Override public void run(){ while(product.sell()){ try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } 在上述代码中，我们使用了 AtomicInteger 来代替 int 类型的 count 变量。通过调用 decrementAndGet() 方法来实现自减操作，并返回自减后的值。decrementAndGet() 这个方法是原子操作，即递减和获取值操作不会被其他线程打断，因此可以保证线程安全。\n3. 并发容器 Java 提供了一些并发容器，用于实现代替Runnable接口和继承Thread类的方式代替多线程编程。并发容器是线程安全的，可以在多线程环境中使用。常用的并发容器为：CompletableFuture，接下来我们会介绍CompletableFuture的使用。\nCompletableFuture 是 Java 8 引入的一个类，它实现了 Future 接口，提供了异步编程的能力。它可以用来执行异步任务，并在任务完成后执行回调操作。CompletableFuture 支持链式调用，可以将多个异步任务组合在一起。\n在javascript中，我们使用Promise来实现异步编程，是这样的用法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 const promise =new Promise((resolve,reject)=\u0026gt;{ // 异步操作 if(success){ resolve(result); //传参到then方法 }else{ reject(error); //传参到catch方法 } }); promise.then(result=\u0026gt;{ // 处理结果 }).catch(error=\u0026gt;{ // 处理错误 }); 在 Java 中，CompletableFuture 提供了类似的功能。它可以用来表示一个异步计算的结果，并提供了一些方法来处理这个结果。CompletableFuture 的使用方式如下：\n1 2 3 4 5 6 7 8 9 CompletableFuture.supplyAsync(()=\u0026gt;{ // 异步任务 return result; }).thenApply(result -\u0026gt; { // 处理结果 return processedResult; }).thenAccept(result -\u0026gt; { // 消费结果 }); 在上述的代码中，CompletableFuture 执行了一个异步任务，处理完第一个任务后，将返回的结果作为thenApply方法的参数，继续执行下一个thenApply的任务，最后执行thenAccept方法来消费结果。 CompletableFuture 还提供了其他一些方法，比如 thenCombine()、thenCompose()、allOf()、anyOf() 等，可以用来实现更复杂的异步任务组合。 这个就有点像Promise的链式调用了，CompletableFuture 也可以用来实现类似于 Promise 的功能。它可以用来表示一个异步计算的结果，并提供了一些方法来处理这个结果。\n我们还可以通过ArrayList来存储多个CompletableFuture对象，然后使用allOf()方法来等待所有的异步任务完成。allOf()方法返回一个新的 CompletableFuture 对象，当所有的异步任务完成时，这个新的 CompletableFuture 对象也会完成。\n比如我们要完成多个学生同时注册的操作，先定义一个学生类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package cn.programcx.ch5.p2; public class Student { private String name; private int id; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public Student(String name, int id) { this.name = name; } public Student(String name) { this.name = name; } @Override public String toString() { return \u0026#34;Student{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, id=\u0026#34; + id + \u0026#39;}\u0026#39;; } } 然后定义一个注册类：\n1 2 3 4 5 6 7 8 9 10 11 package cn.programcx.ch5.p2; import java.util.concurrent.atomic.AtomicInteger; public class Register { private static AtomicInteger count = new AtomicInteger(0); public Student register(Student student) { student.setId(count.incrementAndGet()); return student; } } 在注册类中，我们使用了AtomicInteger来保证线程安全，count变量用来记录注册的学生数量。我们在register()方法中对学生进行注册，并返回注册后的学生对象。 在register()方法中，我们使用了incrementAndGet()方法来实现自增操作，并返回自增后的值。incrementAndGet() 这个方法是原子操作，即递增和获取值操作不会被其他线程打断，因此可以保证线程安全。\n之后我们就可以使用CompletableFuture来实现异步注册了：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 List\u0026lt;CompletableFuture\u0026gt; completableFutures = new ArrayList\u0026lt;\u0026gt;(); List\u0026lt;Student\u0026gt; students = new ArrayList\u0026lt;\u0026gt;(); for(int i=0;i\u0026lt;100;i++){ students.add(new Student(\u0026#34;student\u0026#34;+i)); } Register register =new Register(); students.forEach(student -\u0026gt; { CompletableFuture cf= CompletableFuture.supplyAsync(()-\u0026gt;{ register.register(student); return student; }).thenAccept(stu -\u0026gt; System.out.println(\u0026#34;注册成功，学生信息为\u0026#34;+stu) ); completableFutures.add(cf); //将CompletableFuture实例放到ArrayList容器中 }); try{ CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join(); }catch(Exception e){ e.printStackTrace(); } 在上述代码中，我们使用了 CompletableFuture.supplyAsync() 方法来执行异步任务，并将返回的结果作为参数传递给 thenAccept() 方法。最后，我们使用 CompletableFuture.allOf() 方法来等待所有的异步任务完成。 在 CompletableFuture.allOf() 方法中，我们将 ArrayList 转换为数组，并传递给 allOf() 方法。allOf() 方法返回一个新的 CompletableFuture 对象，我们在最后调用 join() 方法来等待所有的异步任务完成。、\n补充：CompletableFuture 还提供了其它一些方法，比如isDone()、cancel()、complete() 等，可以用来实现其他并发状态获取和控制功能。\n4.线程池 线程池是 Java 中用于管理线程的一个重要概念。它可以用来创建和管理多个线程，并提供了一些方法来控制线程的生命周期。线程池可以提高程序的性能和响应能力，避免频繁创建和销毁线程带来的性能开销。 Java 中我们可以使用ThreadPoolExecutor类来创建线程池。ThreadPoolExecutor 是一个可扩展的线程池实现，它提供了多种线程池的实现方式，比如固定大小的线程池、可缓存的线程池、单线程的线程池等。\n1 2 3 4 5 6 7 8 9 public void execute(Runnable command) // 执行一个任务 public Future\u0026lt;?\u0026gt; submit(Runnable task) // 提交一个任务，并返回一个 Future 对象 public \u0026lt;T\u0026gt; Future\u0026lt;T\u0026gt; submit(Callable\u0026lt;T\u0026gt; task) // 提交一个任务，并返回一个 Future 对象 public void shutdown() // 关闭线程池 public List\u0026lt;Runnable\u0026gt; shutdownNow() // 立即关闭线程池 public boolean isShutdown() // 判断线程池是否关闭 public boolean isTerminated() // 判断线程池是否终止 public \u0026lt;T\u0026gt; List\u0026lt;Future\u0026lt;T\u0026gt;\u0026gt; invokeAll(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks) // 执行一组任务，并返回结果 ... 下列实例我们会演示如何创建线程池：\n先重写Register类，使其继承Thread类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package cn.programcx.ch5.pool; import cn.programcx.ch5.p2.Student; import java.util.concurrent.atomic.AtomicInteger; public class Register extends Thread { private static final AtomicInteger count = new AtomicInteger(0); private final Student student; public Register(Student student) { this.student = student; } @Override public void run() { student.setId(count.incrementAndGet()); System.out.println(\u0026#34;有新的学生注册成功\u0026#34;+student); } } 然后创建一个线程池，并使用线程池来执行注册操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package cn.programcx.ch5.pool; import cn.programcx.ch5.p2.Student; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import java.util.concurrent.*; public class Pool { //创建线程工厂 private static final ThreadFactory threadFactory = new BasicThreadFactory.Builder() .namingPattern(\u0026#34;pool-%d\u0026#34;).build(); //创建阻塞队列 private static final BlockingQueue\u0026lt;Runnable\u0026gt; workQueue = new LinkedBlockingQueue\u0026lt;Runnable\u0026gt;(1024); //使用上面两个实例作为参数创建线程池 private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 20, 200, 30, TimeUnit.SECONDS, workQueue, threadFactory, new ThreadPoolExecutor.AbortPolicy() ); public static void main(String[] args) { for (int i = 0; i \u0026lt; 10; i++) { Student student = new Student(String.valueOf(i)); Register register = new Register(student); //使用线程池来执行注册操作，调用Runnable接口的run方法 threadPoolExecutor.execute(register); } } } **注意：**execute方法传入的参数的实例必须实现Runnable接口或者Callable接口，可以继承Thread类，或者实现Runnable接口。\n其实在 3 的例子中，我们可以通过使用ExecutorService来创建线程池控制并发数量，防止高并发导致服务器崩溃。\n3中的代码可以改为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package cn.programcx.ch5.p2; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestRegisterStudent { public static void main(String[] args) { List\u0026lt;CompletableFuture\u0026gt; completableFutures = new ArrayList\u0026lt;\u0026gt;(); ExecutorService executorService = Executors.newFixedThreadPool(20); List\u0026lt;Student\u0026gt; students = new ArrayList\u0026lt;\u0026gt;(); for(int i=0;i\u0026lt;100;i++){ students.add(new Student(\u0026#34;student\u0026#34;+i)); } Register register =new Register(); students.forEach(student -\u0026gt; { CompletableFuture cf= CompletableFuture.supplyAsync(()-\u0026gt;{ register.register(student); return student; },executorService).thenAccept(stu -\u0026gt; System.out.println(\u0026#34;注册成功，学生信息为\u0026#34;+stu) ); completableFutures.add(cf); //将CompletableFuture实例放到ArrayList容器中 }); try{ CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join(); }catch(Exception e){ e.printStackTrace(); } executorService.shutdown(); //关闭线程池 } } 这里，我们使用 ExecutorService executorService = Executors.newFixedThreadPool(20);来创建一个固定大小的线程池，大小为 20。然后在CompletableFuture.supplyAsync()方法中传入线程池对象，这样就可以使用线程池来执行异步任务了。 在最后，我们调用executorService.shutdown()方法来关闭线程池，释放资源。\n线程池的好处：\n线程池可以重用线程，避免频繁创建和销毁线程带来的性能开销。 线程池可以控制线程的数量，避免过多的线程导致系统资源耗尽。 可以通过线程池获取线程池的状态、任务的数量等。 线程池提供一些任务调度的功能，比如定时任务、周期任务等。 可以执行一些任务的取消和中断，比如取消正在执行的任务、中断等待的任务等。 5. 线程间通信 5.1 wait() 和 notify() wait() 和 notify() 是 Java 中用于线程间通信的方法。它们可以用来实现线程间的协作和同步。wait() 方法会使当前线程等待，直到其他线程调用 notify() 或 notifyAll() 方法来唤醒它。notify() 方法会随机唤醒一个等待的线程，而 notifyAll() 方法会唤醒所有等待的线程。 wait() 和 notify() 方法必须在同步代码块或同步方法中调用，否则会抛出 IllegalMonitorStateException 异常。因为不呢能在主线程上等待，否则会导致程序阻塞。\nwait() 方法会释放锁，并使当前线程进入等待状态，直到其他线程调用 notify() 或 notifyAll() 方法来唤醒它。 notify() 方法会随机唤醒一个等待的线程，并使其进入就绪状态，但不会释放锁。 notifyAll() 方法会唤醒所有等待的线程，并使它们进入就绪状态，但不会释放锁。 接下来是一个简单的 demo：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package cn.programcx.consumer; public class Data { private int data; public synchronized void increase() throws InterruptedException{ while(data!=0){ wait(); } data++; System.out.println(\u0026#34;data:\u0026#34;+data); notifyAll(); } public synchronized void decrease() throws InterruptedException{ while(data==0){ wait(); } data--; System.out.println(\u0026#34;data:\u0026#34;+data); notifyAll(); } public static void main(String[] args) { Data data = new Data(); new Thread(()-\u0026gt;{ for(int i=0;i\u0026lt;10;i++){ try{ data.increase(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); new Thread(()-\u0026gt;{ for(int i=0;i\u0026lt;10;i++){ try { data.decrease(); }catch (InterruptedException e){ throw new RuntimeException(e); } } }).start(); } } 在上述代码中，我们创建了一个基本的“生产——消费者”模型，该模型中有两个方法，一个负责在descrease()方法中减少数据，一个负责在increase()方法中增加数据。这两个方法应该是互斥的，所以我们使用了while循环来判断是否互斥方法已经执行了，如果没有执行，就使用wait()方法来等待，直到互斥方法调用notify()或notifyAll()方法来唤醒它。注意：这两个方法必须在不同线程中运行，否则程序会阻塞，即如果只有一个线程调用，它一旦wait()自己就再也没机会唤醒自己，程序就永远卡住了。\n我们还可以用这个”生产——消费者“模型来写一个厕所使用的例子，即一个公共厕所里面有5个厕所位置，当5个全部被占用时，其他人就要等着，直到有一个人上完厕所，释放一个位置。我们可以使用wait()和notify()方法来实现这个模型。\n以下是示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package cn.programcx.consumer; import java.util.HashMap; import java.util.Map; public class Toilet { private Map\u0026lt;Integer, Boolean\u0026gt; toiletMap = new HashMap\u0026lt;Integer, Boolean\u0026gt;(); private ThreadLocal\u0026lt;Integer\u0026gt; threadLocal = new InheritableThreadLocal\u0026lt;\u0026gt;(); public Toilet() { for (int i = 1; i \u0026lt;= 5; i++) { toiletMap.put(i, false); // 初始化5个厕所位置，声明为未被占用 } } private synchronized void acquirePosition() throws InterruptedException { int position = 0; while (true) { for (Integer item : toiletMap.keySet()) { if (!toiletMap.get(item)) { position = item; toiletMap.put(position, true); //占用厕所位置 threadLocal.set(position); //保存当前线程占用厕所的位置 System.out.println(Thread.currentThread().getName() + \u0026#34; 占用了厕所 \u0026#34; + item); return; } } wait(); } } private synchronized void releasePosition() { Integer position = threadLocal.get(); if (position != null) { toiletMap.put(position, false); //释放厕所位置 threadLocal.remove(); System.out.println(Thread.currentThread().getName() + \u0026#34; 上完了厕所 \u0026#34; + position); notifyAll(); } } public static void main(String[] args) throws InterruptedException { Toilet toilet = new Toilet(); for (int i = 1; i \u0026lt;= 7; i++) { new Thread(() -\u0026gt; { try { while (true) { toilet.acquirePosition(); Thread.sleep(2000); toilet.releasePosition(); Thread.sleep(1000); } } catch (InterruptedException e) { throw new RuntimeException(e); } }).start(); } } } 输出结果如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Thread-0 占用了厕所 1 Thread-6 占用了厕所 2 Thread-2 占用了厕所 3 Thread-4 占用了厕所 4 Thread-3 占用了厕所 5 Thread-3 上完了厕所 5 Thread-5 占用了厕所 5 Thread-4 上完了厕所 4 Thread-1 占用了厕所 4 Thread-2 上完了厕所 3 Thread-6 上完了厕所 2 Thread-0 上完了厕所 1 Thread-3 占用了厕所 1 Thread-6 占用了厕所 2 Thread-4 占用了厕所 3 Thread-1 上完了厕所 4 Thread-2 占用了厕所 4 Thread-5 上完了厕所 5 Thread-0 占用了厕所 5 Thread-3 上完了厕所 1 Thread-6 上完了厕所 2 Thread-1 占用了厕所 1 Thread-4 上完了厕所 3 Thread-5 占用了厕所 2 Thread-2 上完了厕所 4 Thread-6 占用了厕所 3 Thread-0 上完了厕所 5 Thread-4 占用了厕所 4 Thread-3 占用了厕所 5 Thread-1 上完了厕所 1 Thread-2 占用了厕所 1 Thread-5 上完了厕所 2 Thread-0 占用了厕所 2 Thread-6 上完了厕所 3 Thread-4 上完了厕所 4 Thread-1 占用了厕所 3 Thread-3 上完了厕所 5 Thread-5 占用了厕所 4 进程已结束，退出代码为 130 在上面的代码中，我们使用Map\u0026lt;Integer,Boolean\u0026gt; toiletMap来记录厕所占用情况，在acquirePosition()方法中，我们创建了一个while(true)循环，通过遍历toiletMap来判断是否有厕所位置被占用，如果没有，就使用wait()方法来等待，直到有一个人上完厕所，释放一个位置。我们还使用了ThreadLocal\u0026lt;Integer\u0026gt; threadLocal来保存当前线程占用厕所的位置，这样就可以在releasePosition()方法中释放自己占用的厕所位置了。\n5.2 BlockingQueue 我们还可以通过BlockingQueue来实现线程间通信，BlockingQueue 是一个线程安全的队列，它提供了一些方法来阻塞线程，直到队列中有可用的元素。BlockingQueue 提供了多种实现方式，比如 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等。\n在并发数不是太大的项目中，我们常用ArrayBlockingQueue，它是一个有界的阻塞队列，使用数组来实现。它的构造函数需要指定队列的大小，比如 ArrayBlockingQueue(int capacity)。而在并发数较大的项目中，我们常用LinkedBlockingQueue，它是一个无界的阻塞队列，使用链表来实现。它的构造函数可以指定队列的大小，比如 LinkedBlockingQueue(int capacity)，如果不指定大小，则默认为 Integer.MAX_VALUE。LinkedBlockingQueue 的性能比 ArrayBlockingQueue 更高，因为它是基于链表实现的，避免了数组扩容带来的性能开销，但是它的内存开销也更大，因为它需要维护一个链表结构。\n在BlockingQueue中，我们常用这些方法：\n1 2 3 4 5 6 7 8 9 public void add(E e) // 添加元素，如果队列满了就抛出异常,如果超出队列大小就抛出异常 public void put(E e) // 添加元素，如果队列满了就阻塞 public E take() // 获取元素，如果队列为空就阻塞 public boolean offer(E e, long timeout, TimeUnit unit) // 添加元素，如果队列满了就等待指定的时间，如果时间到了仍然无法添加元素，则返回 false。如果添加成功，则返回 true。 public E poll(long timeout, TimeUnit unit) // 获取元素，如果队列为空就等待指定的时间，如果时间到了仍然无法获取元素，则返回 null。如果成功获取到元素，则返回该元素。 public int remainingCapacity() // 返回队列的剩余容量 public int size() // 返回队列的大小 public boolean isEmpty() // 判断队列是否为空 public boolean isFull() // 判断队列是否满 我们可以用BlockingQueue来改写上一个厕所占用的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package cn.programcx.consumer; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class QueueToilet { public static void main(String[] args) { BlockingQueue\u0026lt;Integer\u0026gt; queue = new ArrayBlockingQueue\u0026lt;Integer\u0026gt;(5); for (int i = 1; i \u0026lt;=5; i++) { queue.add(i); } for(int i = 0; i \u0026lt; 7; i++){ new Thread(() -\u0026gt; { while(true){ try { Integer num = queue.take(); System.out.println(Thread.currentThread().getName()+\u0026#34;占用了厕所位置：\u0026#34;+num); Thread.sleep(2000); queue.put(num); System.out.println(Thread.currentThread().getName()+\u0026#34;离开了厕所位置：\u0026#34;+num); Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); } } } 输出结果如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Thread-1占用了厕所位置：2 Thread-2占用了厕所位置：3 Thread-4占用了厕所位置：5 Thread-6占用了厕所位置：4 Thread-0占用了厕所位置：1 Thread-5占用了厕所位置：2 Thread-1离开了厕所位置：2 Thread-6离开了厕所位置：4 Thread-3占用了厕所位置：4 Thread-0离开了厕所位置：1 Thread-4离开了厕所位置：5 Thread-2离开了厕所位置：3 Thread-1占用了厕所位置：1 Thread-0占用了厕所位置：5 Thread-6占用了厕所位置：3 Thread-5离开了厕所位置：2 Thread-2占用了厕所位置：2 Thread-3离开了厕所位置：4 Thread-4占用了厕所位置：4 Thread-5占用了厕所位置：5 Thread-3占用了厕所位置：3 Thread-0离开了厕所位置：5 Thread-6离开了厕所位置：3 Thread-1离开了厕所位置：1 Thread-2离开了厕所位置：2 Thread-4离开了厕所位置：4 Thread-1占用了厕所位置：1 Thread-6占用了厕所位置：2 Thread-0占用了厕所位置：4 进程已结束，退出代码为 130 5.3 Semaphore 信号量（Semaphore）是一个用来控制对共享资源的访问数量的同步工具。它广泛应用于并发编程中，通常用于限制能够访问某些资源的线程数量。Semaphore 提供了一个计数器来表示当前可用的资源数，并且可以用来实现访问共享资源的控制。\n信号量本身其实是一个计数器，表示当前可用资源的数量，并且可以和其它线程共享。\nSemaphore 用两个基本操作，一个是acquire()，一个是release()。acquire() 方法会获取一个信号量，如果信号量的计数器大于 0，就将计数器减 1，并返回；如果计数器为 0，就会阻塞当前线程，直到有其他线程调用 release() 方法来释放信号量。release() 方法会将信号量的计数器加 1，并唤醒一个等待的线程。 Smephore提供了以下构造方法：\n1 2 public Semaphore(int permits) // 创建一个信号量，初始计数器为 permits public Semaphore(int permits, boolean fair) // 创建一个信号量，初始计数器为 permits，fair 表示是否公平 Semaphore 的公平性是指在多个线程竞争信号量时，是否按照请求的顺序来分配信号量。如果设置为公平，则会按照请求的顺序来分配信号量；如果设置为非公平，则会随机分配信号量。公平性会影响性能，因为它会导致线程的上下文切换和调度开销。\nSemaphore 的常用方法：\n1 2 3 4 5 6 7 8 9 10 public void acquire() throws InterruptedException // 获取信号量 public void acquire(int permits) throws InterruptedException // 获取指定数量的信号量 public void release() // 释放信号量 public void release(int permits) // 释放指定数量的信号量 public int availablePermits() // 获取当前可用的信号量数量 public int drainPermits() // 获取当前可用的信号量数量，并将计数器清零 public boolean tryAcquire() // 尝试获取信号量，如果获取成功则返回 true，否则返回 false public boolean tryAcquire(int permits) // 尝试获取指定数量的信号量，如果获取成功则返回 true，否则返回 false public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException // 尝试获取信号量，如果获取成功则返回 true，否则返回 false public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException // 尝试获取指定数量的信号量，如果获取成功则返回 true，否则返回 false 我们可以用Semaphore简单地来改写上一个厕所占用的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package cn.programcx.consumer; import java.util.concurrent.Semaphore; public class SemaphoreToilet { public static void main(String[] args) { Semaphore semaphore = new Semaphore(5); for (int i = 0; i \u0026lt; 7; i++) { new Thread(() -\u0026gt; { while (true) { try{ semaphore.acquire(); System.out.println(Thread.currentThread().getName()+\u0026#34;线程占用了厕所\u0026#34;); Thread.sleep(2000); semaphore.release(); System.out.println(Thread.currentThread().getName()+\u0026#34;线程离开了厕所\u0026#34;); Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); } } } 这里我们使用了Semaphore来控制厕所的数量，Semaphore的构造函数中传入的参数为 5，表示有 5 个厕所位置。然后在每个线程中调用acquire()方法来获取信号量，占用一个厕所位置；调用release()方法来释放信号量，离开一个厕所位置。\n但是这样在获取或离开厕所时我们不能知道是哪一个厕所，这时候我们就需要修改一下Semaphore的构造函数，创建一个Semaphore数组，这个数组里面每个对象都代表一个厕所编号（通过数组下标），每个对象的permits值为 1，表示这个厕所可以被占用。然后遍历每一个Semaphore对象，通过tryAcquire()方法来尝试获取数组，直到获取到一个位置为止。\n以下是根据这个思路修改完成的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package cn.programcx.consumer; import java.util.concurrent.Semaphore; public class SemaphoreToilet { public static void main(String[] args) { Semaphore[] semaphores =new Semaphore[5]; // 创建一个Semaphore数组，表示5个厕所位置 for(int i=0;i\u0026lt;semaphores.length;i++){ semaphores[i]=new Semaphore(1); //初始化数组，每个只表示是否被占用 } for(int i=0;i\u0026lt;7;i++){ new Thread(()-\u0026gt;{ while(true) { for (int j = 1; j \u0026lt;=semaphores.length; j++) { if (semaphores[j-1].tryAcquire()) { //尝试获取一个厕所位置，非阻塞，获取失败返回false System.out.println(Thread.currentThread().getName() + \u0026#34;线程占用了一个厕所位置：\u0026#34; + j); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } semaphores[j-1].release(); System.out.println(Thread.currentThread().getName() + \u0026#34;线程离开了一个厕所位置：\u0026#34; + j); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } break; } } } }).start(); } } } ","date":"2025-04-28T00:00:00Z","image":"https://programcx.github.io/p/java-multithread/java_hu14469118171915315630.jpg","permalink":"https://programcx.github.io/p/java-multithread/","title":"Java 并发编程"},{"content":"Spring Boot 整合 Redis(基础篇) 1. 有关依赖的添加 1. 添加 Redis 依赖 1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 2. 连接器依赖 Spring Data Redis在org.springframework.data.redis.connection包中提供了接口 RedisConnection 和 RedisConnectionFactory，用于连接 Redis 服务器。Spring Data Redis 提供了多种连接器，如 Jedis、Lettuce、Redis 等。\nSpring 默认使用 Lettuce 作为连接器，如果想使用 Jedis 作为连接器，需要添加 Jedis 依赖。\n2.1. 添加 Jedis 依赖 1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;redis.clients\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jedis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 注意：Spring Boot 2.0 之后默认使用 Lettuce 作为连接器，如果想使用 Jedis 作为连接器，需要排除 Lettuce 依赖。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;io.lettuce.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lettuce-core\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;redis.clients\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jedis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 2.2. 添加 Lettuce 依赖 1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.lettuce.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lettuce-core\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 2. 配置 Redis 1. application.properties 配置 1 2 3 4 5 6 7 # application.properties spring.data.redis.host=127.0.0.1 spring.data.redis.port=6379 spring.data.redis.password=${REDIS_PASSWORD:} # 环境变量优先 spring.data.redis.database=0 spring.data.redis.lettuce.pool.max-active=8 # 连接池配置 spring.data.redis.timeout=5000 # 超时配置 3. RedisTemplate 与数据操作类的使用 3.1 数据操作类的使用 使用Spring 容器注入 RedisTemplate 对象，通过 RedisTemplate 对象操作 Redis 数据。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package cn.programcx.springbootinit.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; @Configuration public class Config { @Bean public LettuceConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(new RedisStandaloneConfiguration(\u0026#34;localhost\u0026#34;, 6379)); } @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; } } 然后在使用时，通过 RedisTemplate 对象操作 Redis 数据。\n3.2 序列化器的使用 StringRedisTemplate 继承 RedisTemplate，并指定了序列化器为StringRedisSerializer以及编码集为UTF-8。这样一来，使用StringRedisTemplate对象操作 Redis 数据时，数据会以字符串形式存储，而不是以字节数组形式存储，不需要翻译。\n上面创建RedisTemplate对象时，没有指定序列化器，那么Spring会使用默认的序列化器，即JdkSerializationRedisSerializer。这样一来，文本前面会出现“\\xac\\xed\\x00\\x05t\\x00\\”这样的字符，不方便阅读。\n解决这个问题有两种方法：\n通过编码为RedisTemplate对象指定序列化器。 示例： 1 2 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); 注入使用SpringRedisTemplate，首先在容器中注入StringRedisTemplate对象，然后使用StringRedisTemplate对象操作 Redis 数据。 1 2 3 4 @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){ return new StringRedisTemplate(redisConnectionFactory); } 在注入ValueOperations的位置指定stringRedisTemplate对象，然后使用stringRedisTemplate对象操作 Redis 数据：\n1 2 @Resource(name = \u0026#34;stringRedisTemplate\u0026#34;) private ValueOperations valueOperations; 补充： 如果我们要定义字符串、列表、集合、散列、有序集合等数据类型的操作。\n我们可以使用GenericJackson2JsonRedisSerializer作为序列化器，这样我们可以直接存储对象。 在此之前，我们要添加依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 配置类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Bean public RedisTemplate\u0026lt;String, Object\u0026gt; redisTemplate(RedisConnectionFactory factory) { RedisTemplate\u0026lt;String, Object\u0026gt; template = new RedisTemplate\u0026lt;\u0026gt;(); template.setConnectionFactory(factory); GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); // Hash key序列化 template.setHashValueSerializer(serializer); return template; } 3.3 RedisCallback、SessionCallback 接口和 Redis 事务的使用 RedisCallback 接口和 SessionCallback 接口是 Spring Data Redis 提供的两个回调接口，用于执行 Redis 命令。 RedisCallback 接口用于执行 Redis 命令，SessionCallback 接口用于执行 Redis 事务。\nRedisCallback 接口代码如下：\n1 2 3 public interface RedisCallback\u0026lt;T\u0026gt; { T doInRedis(RedisConnection connection) throws DataAccessException; } 比如，我们要获取Redis查询的结果，Redis查询完成后我们怎么获取结果呢，这时候就需要用到 RedisCallback 接口。\n示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Autowired StringRedisTemplate stringRedisTemplate; @Test void testRedisCallback() { String value = stringRedisTemplate.execute( (RedisCallback\u0026lt;String\u0026gt;)(connection) -\u0026gt;{ byte[] valueBytes = connection.get(\u0026#34;redis-template\u0026#34;.getBytes()); try { return new String(valueBytes, \u0026#34;UTF-8\u0026#34;); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } }); assert value.equals(\u0026#34;Hello, World!\u0026#34;); } SessionCallback 主要用于对 Redis 事务的支持。\nSessionCallback 接口代码如下：\n1 \u0026lt;K, V\u0026gt; T execute(RedisOperations\u0026lt;K, V\u0026gt; operations) throws DataAccessException; 在此之前，我们需要开启 Redis 事务支持，在 RedisTemplate 对象中设置开启事务支持：\n1 2 3 4 5 6 7 @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setEnableTransactionSupport(true); return redisTemplate; } 写： 示例\n1 2 3 4 5 6 7 8 9 10 11 12 @Test void testSessionCallback() { List\u0026lt;Object\u0026gt; results = stringRedisTemplate.execute(new SessionCallback\u0026lt;List\u0026lt;Object\u0026gt;\u0026gt;() { @Override public List\u0026lt;Object\u0026gt; execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.opsForValue().set(\u0026#34;key1\u0026#34;, \u0026#34;value1\u0026#34;); operations.opsForValue().set(\u0026#34;key2\u0026#34;, \u0026#34;value2\u0026#34;); return operations.exec(); } }); } 读： 示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test void readSessionCallback(){ List\u0026lt;Object\u0026gt; result = stringRedisTemplate.execute(new SessionCallback\u0026lt;List\u0026lt;Object\u0026gt;\u0026gt;() { @Override public List\u0026lt;Object\u0026gt; execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.opsForValue().get(\u0026#34;key1\u0026#34;); operations.opsForValue().get(\u0026#34;key2\u0026#34;); return operations.exec(); } }); for(Object o: result){ System.out.println(o); } } 3.4 RedisTemplate 的使用 RedisTemplate 是 Spring Data Redis 提供的核心类，用于操作 Redis 数据。RedisTemplate 提供了多种数据操作方法，如操作字符串、列表、集合、散列、有序集合等。\nRedisTemplate 提供了多种数据操作方法，如：\nopsForValue()：操作字符串类型的数据。\nopsForList()：操作列表类型的数据。\nopsForSet()：操作集合类型的数据。\nopsForZSet()：操作有序集合类型的数据。\nopsForHash()：操作散列类型的数据。\nexecute()：执行 Redis 命令。\ndelete()：删除 Redis 数据。\nexpire()：设置 Redis 数据的过期时间。\n如何使用呢？\n比如，我们要插入一个列表数据，可以使用opsForList()方法：\n1 2 3 stringRedisTemplate.opsForList().leftPush(\u0026#34;list\u0026#34;, \u0026#34;a\u0026#34;); stringRedisTemplate.opsForList().leftPush(\u0026#34;list\u0026#34;, \u0026#34;b\u0026#34;); stringRedisTemplate.opsForList().leftPush(\u0026#34;list\u0026#34;, \u0026#34;c\u0026#34;); 然后我们可以使用opsForList()方法获取列表数据：\n1 2 3 4 List\u0026lt;String\u0026gt; list = stringRedisTemplate.opsForList().range(\u0026#34;list\u0026#34;, 0, -1); for(String s: list){ System.out.println(s); } 如果我们要插入一个字符串数据，可以使用opsForValue()方法：\n1 2 stringRedisTemplate.opsForValue().set(\u0026#34;key\u0026#34;, \u0026#34;value\u0026#34;); 然后我们可以使用opsForValue()方法获取字符串数据：\n1 2 3 4 String value = stringRedisTemplate.opsForValue().get(\u0026#34;key\u0026#34;); System.out.println(value); 4. 使用 Spring 缓存注解操作 Redis 4.1 启用缓存和配置缓存管理器 Spring Boot 提供了 @EnableCaching 注解，用于启用缓存功能。在Spring Boot 程序入口处添加 @EnableCaching 注解，启用缓存功能。\n1 2 3 4 5 6 7 8 @SpringBootApplication @EnableAsync @EnableCaching public class SpringBootInitApplication { public static void main(String[] args) { SpringApplication.run(SpringBootInitApplication.class, args); } } 4.2 使用缓存注解开发 Spring Boot 提供了多个缓存注解，如 @Cacheable、@CachePut、@CacheEvict、@Caching 和 @CacheConfig。\n@Cacheable：缓存方法的返回结果。 @CachePut：将方法的返回结果放入缓存。 @CacheEvict：删除缓存。 @Caching：组合多个缓存操作。 4.2.1 配置缓存注解序列化器 Spring Boot 默认使用 JdkSerializationRedisSerializer 作为序列化器，会导致缓存的值前面出现“\\xac\\xed\\x00\\x05t\\x00\\”这样的字符，不方便阅读。所以我们要指定 StringRedisSerializer 作为序列化器。\n在配置类中添加以下代码：\n1 2 3 4 5 6 7 8 9 @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build(); } 4.2.2 缓存注解的使用 @Cacheable：缓存方法的返回结果。 新建一个 Service类型的类，添加 @Cacheable 注解，指定缓存的名称和缓存的 key。\n为什么要在 Service 类里面使用注解？\nService 类是由Spring Bean容器管理的，Redis 缓存注解的实现是依赖AOP的，所以需要在由Spring Bean容器管理的类中使用。否则缓存注解不生效。\n在 Service 类中使用缓存注解，可以将缓存的逻辑和业务逻辑分离，提高代码的可读性和可维护性。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package cn.programcx.springbootinit.services; ; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service public class CacheServices { @Cacheable(value = \u0026#34;bookname\u0026#34;,key = \u0026#34;#id\u0026#34;) public String findBookNameById(int id) { return \u0026#34;RedisCookbook\u0026#34;; } } 然后我们在测试类中调用这个方法，第一次调用会执行方法，第二次调用会从缓存中获取结果。\n1 2 3 4 5 6 7 @Autowired private CacheServices cacheServices; @Test void testFindBookNameById(){ System.out.println(cacheServices.findBookNameById(1)); //返回 RedisCookbook } 我们打开redis-cli查看键值存放：\n1 keys * 我们可以在结果中找到：\n1 13) \u0026#34;bookname::1\u0026#34; 输入以下命令查看键值对：\n1 2 127.0.0.1:6379\u0026gt; get bookname::1 \u0026#34;RedisCookbook\u0026#34; @CachePut：将方法的返回结果放入缓存。 1 2 3 4 @CachePut(value = \u0026#34;bookname\u0026#34;,key = \u0026#34;#id\u0026#34;) public String updateBookNameById(int id) { return \u0026#34;SpringBootCookbook\u0026#34;; } @CacheEvict：删除缓存。 1 2 3 4 @CacheEvict(value = \u0026#34;bookname\u0026#34;,key = \u0026#34;#id\u0026#34;) public void deleteBookNameById(int id) { System.out.println(\u0026#34;delete bookname by id\u0026#34;); } 5. Redis 全局异常处理 5.1. Redis 异常处理 在使用 Redis 过程中，可能会出现 Redis 连接异常、Redis 命令执行异常等问题。为了保证程序的正常运行，需要对 Redis 异常进行处理。\n5.2. Redis 异常处理示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @ControllerAdvice public class RedisExceptionHandler { @ExceptionHandler(RedisConnectionFailureException.class) public ResponseEntity\u0026lt;String\u0026gt; handleConnectionError(RedisConnectionFailureException ex) { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(\u0026#34;Redis服务不可用: \u0026#34; + ex.getMessage()); } @ExceptionHandler(DataAccessException.class) public ResponseEntity\u0026lt;String\u0026gt; handleDataAccessException(DataAccessException ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(\u0026#34;数据访问异常: \u0026#34; + ex.getMessage()); } } ","date":"2025-03-15T00:00:00Z","image":"https://programcx.github.io/p/springboot-redis/springboot_hu15901259467893397822.png","permalink":"https://programcx.github.io/p/springboot-redis/","title":"Spring Boot 整合 Redis(基础篇)"},{"content":"Redis 进阶 1. Redis 事务 Redis 中的事务(transaction)是一个单独的操作序列，可以一次执行多个命令，但是这些命令要么全部执行，要么全部不执行。Redis 事务的实现是通过 MULTI、EXEC、DISCARD 和 WATCH 这四个命令来完成的。\n1.1 MULTI 使用 MULTI 命令用于开启一个事务，它总是返回 OK。 MULTI 执行之后，客户端可以继续输入多条命令，这些命令不会立即执行，而是被放到一个队列中，当 EXEC 命令被调用时，所有队列中的命令才会被执行。\n1 2 3 4 MULTI SET key1 value1 SET key2 value2 EXEC 1.2 EXEC 使用 EXEC 命令用于执行所有事务块内的命令。如果事务块内的命令全部执行成功，返回事务块内所有命令的返回值，否则返回一个错误。 Redis 保证一个事务中的所有命令都会被执行，不会出现部分执 行的情况。如果在发送EXEC命令之前发生错误，那么事务将被取消，所有命令都不会执行。\n1.3 DISCARD 使用 DISCARD 命令用于取消事务，它清空事务队列并且恢复到正常模式。\n1.4 WATCH 使用 WATCH 命令用于在事务执行之前监视任意数量的键。如果在事务执行之前这些键被其他命令所改动，那么事务将被打断。\n注意： WATCH使用的是乐观锁，不是悲观锁。 那么，乐观锁和悲观锁有什么区别呢？\n乐观锁和悲观锁是并发控制的两种策略。悲观锁认为数据会被其他事务修改，因此在对数据进行操作时会锁住数据，直到操作完成。乐观锁认为数据不会被其他事务修改，因此在对数据进行操作时不会锁住数据，只是在更新数据时判断数据是否被其他事务修改过。如果数据没有被修改过，则更新成功；如果数据被修改过，则更新失败。\n示例：\n1 2 3 4 5 6 7 set key 1 watch key set key 2 multi set key 3 exec get key 输出结果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 127.0.0.1:6379\u0026gt; get key \u0026#34;2\u0026#34; 127.0.0.1:6379\u0026gt; set key 1 OK 127.0.0.1:6379\u0026gt; watch key OK 127.0.0.1:6379\u0026gt; set key 2 OK 127.0.0.1:6379\u0026gt; multi OK 127.0.0.1:6379\u0026gt; set key 3 QUEUED 127.0.0.1:6379\u0026gt; exec (nil) 127.0.0.1:6379\u0026gt; get key \u0026#34;2\u0026#34; 从上面示例我们可以看出，当某个键被 WATCH 监视后，如果在事务执行之前这个键被其他命令所改动，那么事务将被打断。无法执行事务中的命令，返回nil。\n2. Redis 过期时间 Redis 中可以为 key 设置过期时间，当 key 过期时，会自动删除。过期时间可以通过 EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT 四个命令来设置。\n2.1 EXPIRE 使用 EXPIRE 命令用于设置 key 的过期时间，单位为秒。当 key 过期时，会自动删除。\nEXPIRE 命令的使用方法为 EXPIRE key seconds，其中 key 为键名，seconds 为过期时间。\n例如要让session:342r 60秒后过期，可以使用如下命令：\n1 2 SET session:342r 11 EXPIRE session:342r 60 执行上述代码后，我们接着执行GET session:342r命令，会发现返回的是11，说明 key 还存在。但是等待 60 秒后再次执行GET session:342r命令，会发现返回的是(nil)，说明 key 已经过期被删除了。\n1 2 3 4 5 6 7 8 127.0.0.1:6379\u0026gt; SET session:342r 11 OK 127.0.0.1:6379\u0026gt; EXPIRE session:342r 60 (integer) 1 127.0.0.1:6379\u0026gt; GET session:342r \u0026#34;11\u0026#34; 127.0.0.1:6379\u0026gt; GET session:342r (nil) 想要知道 key 的剩余过期时间，可以使用 TTL 命令，TTL 命令的使用方法为 TTL key，其中 key 为键名。\n1 TTL session:342r 将会返回 key 的剩余过期时间，单位为秒。如果 key 没有设置过期时间，或者 key 已经过期，TTL 命令会返回 -2。如果 key 存在但没有设置过期时间，TTL 命令会返回 -1。\n在 Redis 2.6 版本中，无论键不存在还是没有过期时间，都会返回 -2。在 Redis 2.8 版本后，无论键不存在还是没有过期时间，都会返回 -1。\n2.2 PERSIST 使用 PERSIST 命令用于移除 key 的过期时间，使 key 永不过期。如果过期时间移除成功，返回 1；如果 key 不存在或者 key 没有设置过期时间，返回 0。\nPERSIST 命令的使用方法为 PERSIST key，其中 key 为键名。\n1 PERSIST session:342r 2.3 PEXPIRE 使用 PEXPIRE 命令用于设置 key 的过期时间，单位为毫秒。当 key 过期时，会自动删除。\nPEXPIRE 命令的使用方法为 PEXPIRE key milliseconds，其中 key 为键名，milliseconds 为过期时间。\n例如要让session:342r 60秒后过期，可以使用如下命令：\n1 2 SET session:342r 11 PEXPIRE session:342r 60000 2.4 EXPIREAT 使用 EXPIREAT 命令用于设置 key 的过期时间，以 UNIX 时间戳(unix timestamp)格式设置过期时间。当 key 过期时，会自动删除。\nEXPIREAT 命令的使用方法为 EXPIREAT key timestamp，其中 key 为键名，timestamp 为过期时间。\n例如要让session:342r 60秒后过期，可以使用如下命令：\n1 2 SET session:342r 11 EXPIREAT session:342r 1646880000 2.5 PEXPIREAT 使用 PEXPIREAT 命令用于设置 key 的过期时间，以 UNIX 时间戳(unix timestamp)格式设置过期时间，单位为毫秒。当 key 过期时，会自动删除。\nPEXPIREAT 命令的使用方法为 PEXPIREAT key timestamp，其中 key 为键名，timestamp 为过期时间。\n例如要让session:342r 60秒后过期，可以使用如下命令：\n1 2 SET session:342r 11 PEXPIREAT session:342r 1646880000000 2.6 设置过期时间的注意事项 如果 key 已经设置了过期时间，再次设置过期时间会覆盖之前的过期时间。\n如果 key 已经过期，再次设置过期时间不会生效，key 会立即被删除。\n如果 key 已经设置了过期时间，再次设置过期时间时，过期时间的单位不同，会覆盖之前的过期时间。\n2.7 过期时间的应用场景 缓存数据：可以为缓存数据设置过期时间，当数据过期时，自动删除。\n比如要设置一个缓存数据为 5 分钟，可以使用如下命令： 1 2 SET cache:342r 11 EXPIRE cache:342r 300 代码解释： SET cache:342r 11：设置 cache:342r 键的值为 11。 EXPIRE cache:342r 300：设置 cache:342r 键的过期时间为 300 秒。 会话管理：可以为会话设置过期时间，当会话过期时，自动删除。\n比如要设置一个会话为 30 分钟，可以使用如下命令： 1 2 SET session:342r 11 EXPIRE session:342r 1800 代码解释： SET session:342r 11：设置 session:342r 键的值为 11。 EXPIRE session:342r 1800：设置 session:342r 键的过期时间为 1800 秒。 验证码：可以为验证码设置过期时间，当验证码过期时，自动删除。\n比如要设置一个验证码为 5 分钟，可以使用如下命令： 1 2 SET code:342r 123456 EXPIRE code:342r 300 代码解释： SET code:342r 123456：设置 code:342r 键的值为 123456。 EXPIRE code:342r 300：设置 code:342r 键的过期时间为 300 秒。 限流：可以为限流的 key 设置过期时间，当限流的 key 过期时，自动删除。\n比如要限制用户每分钟访问100个网页，思路是对每个用户使用一个名为rate.limiting:ip的字符串类型键，每次访问网页时，对这个键进行自增操作，如果自增后的值大于100，就拒绝访问。为了防止这个键一直存在，可以设置过期时间，比如设置为1分钟。 代码示例： 1 2 INCR rate.limiting:ip EXPIRE rate.limiting:ip 60 代码解释： INCR rate.limiting:ip：对 rate.limiting:ip 键进行自增操作。 EXPIRE rate.limiting:ip 60：设置 rate.limiting:ip 键的过期时间为 60 秒。 2.8 实现缓存 为了提高网站的负载能力，需要将一些访问频率较高的数据缓存到 Redis 中，并且希望让这些缓存过一段时间自动过期。比如，教务网站要将所有学生各个科目的成绩汇总排名，并在首页上显示排名前十的学生，这个数据是实时变化的，并且非常消耗计算资源，所以不能每次都去数据库查询，而是将这个数据缓存到 Redis 中，并设置过期时间为 2 小时。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 SET ranking:student1 100 SET ranking:student2 99 SET ranking:student3 98 SET ranking:student4 97 SET ranking:student5 96 SET ranking:student6 95 SET ranking:student7 94 SET ranking:student8 93 SET ranking:student9 92 SET ranking:student10 91 EXPIRE ranking:student1 7200 EXPIRE ranking:student2 7200 EXPIRE ranking:student3 7200 EXPIRE ranking:student4 7200 EXPIRE ranking:student5 7200 EXPIRE ranking:student6 7200 EXPIRE ranking:student7 7200 EXPIRE ranking:student8 7200 EXPIRE ranking:student9 7200 EXPIRE ranking:student10 7200 如果服务器内存有限，如果大量地设置缓存且过期时间较长，可能会导致服务器内存不足。为了解决这个问题，可以使用 Redis 的淘汰策略。\n我们可以通过设置配置文件来解决这个问题，修改配置文件中的maxmemory参数，设置 Redis 的最大内存限制。当 Redis 的内存使用达到最大内存限制时，Redis 会根据配置文件中的maxmemory-policy参数来决定淘汰策略。\nmaxmemory-policy 参数有以下几种取值：\nvolatile-lru：在设置了过期时间的 key 中，优先淘汰最近最少使用的 key。 volatile-ttl：在设置了过期时间的 key 中，优先淘汰剩余时间最少的 key。 volatile-random：在设置了过期时间的 key 中，随机淘汰 key。 allkeys-lru：在所有 key 中，优先淘汰最近最少使用的 key。 allkeys-random：在所有 key 中，随机淘汰 key。 noeviction：不淘汰任何 key，只是返回错误。 其中，LRU是Least Recently Used的缩写，表示最近最少使用，TTL是Time To Live的缩写，表示剩余时间。\n3. Redis 排序 有序集合不支持交、并运算，Redis 还未加入相关命令。这是因为 Redis 认为开发者在做完交、并操作后不需要直接获得全部结果，而是希望将结果存入新的键中以便后续处理。所以没有序集合只有ZINTERSTORE和ZUNIONSTORE\n3.1 SORT 命令 SORT命令可以对列表类型、集合类型、有序集合类型的值进行排序。 SORT命令的使用方法为SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]。 ⚠️注意：SORT命令至对输出结果进行排序，并不对数值进行改变！！！\n3.1.1 排列列表 SORT命令可以对列表类型的值进行排序。比如，对一个列表类型的值进行排序，可以使用如下命令：\n1 2 3 lpush list 324 45 6 75 78 89 sort list # 默认升序 lrange list # 输出所有元素 输出结果为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 127.0.0.1:6379\u0026gt; lpush list 324 45 6 75 78 89 (integer) 6 127.0.0.1:6379\u0026gt; sort list 1) \u0026#34;6\u0026#34; 2) \u0026#34;45\u0026#34; 3) \u0026#34;75\u0026#34; 4) \u0026#34;78\u0026#34; 5) \u0026#34;89\u0026#34; 6) \u0026#34;324\u0026#34; 127.0.0.1:6379\u0026gt; lrange list 0 -1 1) \u0026#34;89\u0026#34; 2) \u0026#34;78\u0026#34; 3) \u0026#34;75\u0026#34; 4) \u0026#34;6\u0026#34; 5) \u0026#34;45\u0026#34; 6) \u0026#34;324\u0026#34; 3.1.2 排列集合 SORT命令可以对集合类型的值进行排序。比如，对一个集合类型的值进行排序，可以使用如下命令：\n1 2 3 sadd set 324 45 6 75 78 89 sort set # 默认升序 smembers set # 输出所有元素 在小数据集的情况下，smembers 输出的看似是有序的，但实际上是无序的。可以通过 sort 命令进行排序。 Redis 集合（Set） 的底层数据结构 主要是哈希表（hashtable）或者整数集合（intset）（当元素全是整数且个数较少时）。 在 SMEMBERS 输出时，哈希表的遍历方式可能会让元素按某种规则排列，但这不是保证的排序，只是某些情况下看起来有序。\n3.1.3 排列有序集合 SORT命令可以对有序集合类型的值进行排序。比如，对一个有序集合类型的值进行排序，可以使用如下命令：\n1 2 3 zadd zset 324 324 45 45 6 6 75 75 78 78 89 89 sort zset # 默认升序 zrange zset 0 -1 # 输出所有元素 注意；SORT命令对有序集合类型的值进行排序时，会将分数相同的元素按照成员的字典序进行排序。前面是分数，后面是成员。SORT 排列的是成员，而不是分数。如果成员是数字，那么按照数字的大小进行排序；如果成员是字符串，那么必须加上ALPHA才能按照字符串的字典序进行排序。\n3.1.4 LIMIT 参数 SORT命令的LIMIT参数用于限制输出结果的数量。LIMIT参数的使用方法为LIMIT offset count，其中offset为偏移量，count为数量。\n偏移量（offset）表示从第几个元素开始，数量（count）表示输出多少个元素。\n比如，对一个列表类型的值进行排序，并限制输出结果的数量，可以使用如下命令：\n1 2 3 lpush list 324 45 6 75 78 89 sort list limit 0 3 # 默认升序 lrange list 0 -1 # 输出所有元素 输出结果为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 127.0.0.1:6379\u0026gt; lpush list 324 45 6 75 78 89 (integer) 12 127.0.0.1:6379\u0026gt; sort list limit 0 3 1) \u0026#34;6\u0026#34; 2) \u0026#34;6\u0026#34; 3) \u0026#34;45\u0026#34; 127.0.0.1:6379\u0026gt; lrange list 0 -1 1) \u0026#34;89\u0026#34; 2) \u0026#34;78\u0026#34; 3) \u0026#34;75\u0026#34; 4) \u0026#34;6\u0026#34; 5) \u0026#34;45\u0026#34; 6) \u0026#34;324\u0026#34; 7) \u0026#34;89\u0026#34; 8) \u0026#34;78\u0026#34; 9) \u0026#34;75\u0026#34; 10) \u0026#34;6\u0026#34; 11) \u0026#34;45\u0026#34; 12) \u0026#34;324\u0026#34; 3.1.5 DESC 参数 SORT命令的DESC参数用于指定排序方式为降序。 例如：\n1 2 lpush list 324 45 6 75 78 89 sort list desc 3.1.6 ALPHA 参数 SORT命令的ALPHA参数用于指定排序方式为字典序。\n3.1.7 BY 参数 SORT命令的BY参数用于指定排序的依据。BY参数的使用方法为BY pattern，其中pattern为模式。 例如，我们先插入一些数据：\n1 2 3 4 5 lpush sortlist 2 1 3 set item:1 10 set item:2 5 set item:3 30 sort sortlist by item:* desc # 按照 item:* 的值降序排序 这种情况下，SORT命令会先根据item:*的值对sortlist进行排序，然后再输出排序后的结果。\n适用于需要根据特定属性对于集合进行排序的场景。\n还可以是哈希表的键值对，例如：\n1 2 3 4 5 lpush sortlist 2 1 3 hset item:1 score 10 hset item:2 score 5 hset item:3 score 30 sort sortlist by item:*-\u0026gt;score desc # 按照 item:*-\u0026gt;score 的值降序排序 3.1.8 GET 参数 GET 参数不影响排序，它的作用是使 POST 命令的返回结果不再是元素自身的值，而是GET 参数里面指定的键值。 例如：\n1 2 3 4 5 lpush sortlist 2 1 3 set item:1 10 set item:2 5 set item:3 30 sort sortlist by item:* desc get item:* # 按照 item:* 的值降序排序，并且返回 item:* 的值 3.1.9 STORE 参数 SORT命令的STORE参数用于将排序后的结果保存到一个新的键中。STORE参数的使用方法为STORE destination，其中destination存储的键。\n例如：\n1 2 3 4 5 lpush sortlist 2 1 3 set item:1 10 set item:2 5 set item:3 30 sort sortlist by item:* desc store newlist # 按照 item:* 的值降序排序，并且将结果保存到 newlist 键中 4 Redis 消息通知 Redis 提供了 发布/订阅(Pub/Sub)、Key 事件通知、Stream(消息队列)三种消息通知机制。\n4.1 发布/订阅 发布/订阅(Publish/Subscribe，简称 Pub/Sub) 是 Redis 提供的一种消息订阅机制，允许客户端订阅一个或多个频道(Channel)，然后发布者向这些频道发送这些消息，所有订阅者都会收到相应的消息。\n4.1.1 订阅频道(Subscribe) SUBSCRIBE命令用于订阅一个或多个频道。SUBSCRIBE命令的使用方法为SUBSCRIBE channel [channel ...]，其中channel为频道名。\n示例：\n1 subscribe channel 输出结果：\n1 2 3 4 5 127.0.0.1:6379\u0026gt; subscribe channel Reading messages... (press Ctrl-C to quit) 1) \u0026#34;subscribe\u0026#34; 2) \u0026#34;channel\u0026#34; 3) (integer) 1 4.1.2 发布消息(Publish) PUBLISH命令用于向指定频道发送消息。PUBLISH命令的使用方法为PUBLISH channel message，其中channel为频道名，message为消息内容。\n在 4.1.1 例子上的基础上，新建一个控制台，执行如下命令：\n1 publish channel \u0026#34;hello\u0026#34; 原来的控制台会输出：\n1 2 3 4 5 6 7 8 127.0.0.1:6379\u0026gt; subscribe channel Reading messages... (press Ctrl-C to quit) 1) \u0026#34;subscribe\u0026#34; 2) \u0026#34;channel\u0026#34; 3) (integer) 1 1) \u0026#34;message\u0026#34; 2) \u0026#34;channel\u0026#34; 3) \u0026#34;hello\u0026#34; 多了一行\u0026quot;hello\u0026quot;，说明发布成功，客户端已经收到消息。\n4.1.3 取消订阅(UNSUBSCRIBE) UNSUBSCRIBE命令用于取消订阅一个或多个频道。UNSUBSCRIBE命令的使用方法为UNSUBSCRIBE [channel [channel ...]]，其中channel为频道名。\n示例：\n1 unsubscribe channel 4.1.4 订阅多个频道 SUBSCRIBE命令可以订阅多个频道。比如，订阅channel1和channel2两个频道，可以使用如下命令：\n1 subscribe channel1 channel2 4.1.5 取消订阅所有频道 UNSUBSCRIBE命令可以取消订阅所有频道。比如，取消订阅所有频道，可以使用如下命令：\n1 unsubscribe 4.2 Key 事件通知 Redis Key 事件通知允许监听数据库键的变化事件，例如SET、DEL、EXPIRE。\n4.2.1 配置通知 默认情况下，Redis 不会 开启Key 事件通知，可以通过以下命令启用：\n1 config set notify-keyspace-events KEA notify-keyspace-events参数的取值有以下几种：\nK：键空间通知，事件以__keyspace@\u0026lt;db\u0026gt;__为前缀进行发布。(Key 事件通知 (keyspace events))\nE：键事件通知，事件以__keyevent@\u0026lt;db\u0026gt;__为前缀进行发布。(keyevent)\nA：参数通知，事件以__keyspace@\u0026lt;db\u0026gt;__为前缀进行发布。(ALL)\ng：DEL、EXPIRE、RENAME、EXPIREAT、SORT操作的通知。\n$：字符串命令通知。\n4.2.2 订阅通知 PSUBSCRIBE命令用于订阅一个或多个模式。PSUBSCRIBE命令的使用方法为PSUBSCRIBE pattern [pattern ...]，其中pattern为模式。\n示例：\n1 psubscribe __keyspace@0__:* 然后再另一个终端执行：\n1 set mykey 1 订阅者会收到：\n1 2 3 4 5 6 7 8 9 10 11 12 13 127.0.0.1:6379\u0026gt; psubscribe __keyspace@0__:* Reading messages... (press Ctrl-C to quit) 1) \u0026#34;psubscribe\u0026#34; 2) \u0026#34;__keyspace@0__:*\u0026#34; 3) (integer) 1 1) \u0026#34;pmessage\u0026#34; 2) \u0026#34;__keyspace@0__:*\u0026#34; 3) \u0026#34;__keyspace@0__:mykey\u0026#34; 4) \u0026#34;set\u0026#34; 1) \u0026#34;pmessage\u0026#34; 2) \u0026#34;__keyspace@0__:*\u0026#34; 3) \u0026#34;__keyspace@0__:mykey\u0026#34; 4) \u0026#34;del\u0026#34; 4.2.3 监听事件 事件：\nset: 触发操作为SET key value。 del: 触发操作为DEL key。 expire: 触发操作为EXPIRE key seconds。 rename: 触发操作为RENAME key newkey。 expired: 键过期时触发。 注意：\nKey 事件通知只能监键的变化，不能监值的变化。\n4.3 Redis Stream(消息队列) Redis Stream 是一个高效的消息队列，支持消息持久化、消费者组、消费确认等功能。适用于日志收集、事件流处理、消息队列等应用场景。\n4.3.1 添加消息 (XADD) 基本语法：\n1 XADD stream_name ID field1 value1 field2 value2 ... stream_name：流的名称。 ID：消息的唯一标识，可以是*或$。 field1 value1 field2 value2 ...：消息的键值对。 举例：\n1 XADD mystream * name Tom age 18 4.3.2 读取消息 4.3.2.1 读取所有消息 (XRANGE) 1 XRANGE mystream - + 会输出以下内容：\n1 2 3 4 5 6 127.0.0.1:6379\u0026gt; XRANGE mystream - + 1) 1) \u0026#34;1741594483162-0\u0026#34; 2) 1) \u0026#34;name\u0026#34; 2) \u0026#34;Tom\u0026#34; 3) \u0026#34;age\u0026#34; 4) \u0026#34;18\u0026#34; 4.3.2.2 阻塞式读取新消息 (XREAD) 基本语法：\n1 XREAD COUNT count BLOCK milliseconds STREAMS stream_name ID count：读取消息的数量。 milliseconds：阻塞时间，单位为毫秒。 stream_name：流的名称。 ID：消息的唯一标识。(当$时，表示从最新的消息开始读取) 举例：\n1 XREAD COUNT 1 STREAMS mystream 0 # 非阻塞式读取一条消息 还可以当有新的消息时，自动读取新消息：\n1 XREAD BLOCK 5000 STREAMS mystream $ # 阻塞式读取新消息 此时切换到另一个终端，执行：\n1 XADD mystream * name Tom age 18 第一个控制台会输出以下内容：\n1 2 3 4 5 6 7 127.0.0.1:6379\u0026gt; XREAD COUNT 1 STREAMS mystream 0 1) 1) \u0026#34;mystream\u0026#34; 2) 1) 1) \u0026#34;1741594483162-0\u0026#34; 2) 1) \u0026#34;name\u0026#34; 2) \u0026#34;Tom\u0026#34; 3) \u0026#34;age\u0026#34; 4) \u0026#34;18\u0026#34; 4.3.3 消费者组 4.3.4 创建消费者组 基本语法：\n1 XGROUP CREATE stream_name group_name ID stream_name：流的名称。 group_name：消费者组的名称。 ID：消息的唯一标识。 例如：\n1 XGROUP CREATE mystream mygroup 0 4.3.5 消费者读取消息 基本语法：\n1 XREADGROUP GROUP group_name consumer_name COUNT count BLOCK milliseconds STREAMS stream_name ID group_name：消费者组的名称。 consumer_name：消费者的名称。 count：读取消息的数量。 milliseconds：阻塞时间，单位为毫秒。 stream_name：流的名称。 ID：消息的唯一标识。(*表示从最新的消息开始读取，\u0026gt;表示从上次读取的位置开始读取) 例如：\n1 XREADGROUP GROUP mygroup myconsumer COUNT 1 STREAMS mystream \u0026gt; # 读取一条消息 4.3.6 消费者确认消息 基本语法：\n1 XACK stream_name group_name ID stream_name：流的名称。 group_name：消费者组的名称。 ID：消息的唯一标识。 例如：\n1 XACK mystream mygroup 1741594483162-0 4.3.4 三者之间的比较 发布/订阅(Pub/Sub)：\n✅ 低延迟，适用于实时消息推送 ✅ 适合广播通知，如聊天系统、游戏推送 ✅ 订阅者断开连接后不会收到历史消息 ❌ 无法保证消息可靠性（可能丢失） Key 事件通知：\n✅ 无法保证消息可靠性（可能丢失） ✅ 可以配合 Redis 作为消息通知机制 ❌ 可能会增加 Redis 的 CPU 负担 ❌ 只能监听键的变化，不能监听数据值的变化 Stream(消息队列)：\n✅ 支持消息持久化，不会丢失 ✅ 支持多个消费者，适用于高并发场景 ✅ 可以追踪消息处理状态 ❌ 比 Pub/Sub 稍微复杂，但更可靠 ","date":"2025-03-10T00:00:00Z","image":"https://programcx.github.io/p/redis-advancement/redis_hu17885680186398226011.jpg","permalink":"https://programcx.github.io/p/redis-advancement/","title":"Redis 进阶"},{"content":"Redis 基础 1. Redis 简介 Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库，并提供多种语言的 API。\n2. Redis 安装 2.1 下载 Linux 安装： 2.1.1 安装依赖 1 2 3 4 5 sudo apt update # 更新软件包 sudo apt install pkg-config # 安装 pkg-config sudo apt install libjemalloc-dev # 安装jemalloc sudo apt install gcc # 安装gcc sudo apt install g++ # 安装g++ 2.1.2 下载 Redis 1 2 3 4 wget http://download.redis.io/redis-stable.tar.gz tar xzf redis-stable.tar.gz cd redis-stable make PREFIX=/usr/local/redis install 等待安装完成\n2.2 启动 Redis 1 2 cd /usr/local/redis/bin ./redis-server 2.3 连接 Redis 1 ./redis-cli 会显示如下页面：\n此时我们输入 ping 命令，会返回 PONG，表示连接成功。\n3. Redis 基本操作 3.1 热身 3.1.1 获取符合条件的所有 key 1 keys * glob 风格匹配规则：\n* 匹配任意数量的字符 ? 匹配一个字符 [] 匹配括号内的任意一个字符 [-] 匹配括号内的任意一个字符范围 \\x 转义字符，匹配 x 字符 3.1.2 判断 key 是否存在 我们先创建一个·名叫 bar 的 key，然后判断它是否存在：\n1 set bar 1 判断是否存在：\n1 exists bar 如果存在，返回 1，否则返回 0。\n3.1.3 删除 key 1 del bar 3.1.4 获取 key 的类型 1 2 set foo 1 type foo # 返回 string 3.2 字符串 3.2.1 设置 key 的值 set 和 get 命令用于设置和获取 key 的值：\n1 2 set key hello get key # 返回 hello 3.2.2 递增数字 1 2 set counter 100 incr counter # 返回 101 当 key 不存在时，incr 命令会将 key 的值设置为 0，然后再执行递增操作。\n3.2.3 递减数字 1 decr counter # 返回 100 3.2.4 增加指定浮点数 1 2 set float 10.5 incrbyfloat float 0.1 # 返回 10.6 3.2.5 获取 key 的长度 1 2 set key hello strlen key # 返回 5 3.2.6 同时设置/获取多个 key 1 2 mset key1 hello key2 world # 设置 key1 和 key2 的值 mget key1 key2 # 获取 key1 和 key2 的值 3.3 位操作 优点： 位操作命令可以非常紧凑地存储布尔值，节省内存，而且读取速度非常快。\n语法:\n1 2 3 4 5 setbit key offset value getbit key offset bitcount key [start end] bitop operation destkey key1 [key2 ...] bitpos key bit [start end] 3.3.1 设置指定偏移量的位值 1 setbit bit 7 1 注意：\noffset 从 0 开始 3.3.2 获取指定偏移量的位值 1 getbit bit 7 # 返回 1 3.3.3 统计指定范围内的位值为 1 的个数 1 2 3 setbit bit 7 1 bitcount bit 0 0 # 返回 1 3.3.4 对一个或多个 key 进行位运算，并将结果保存到 destkey 1 2 3 4 setbit key1 7 1 setbit key2 7 1 bitop AND dest key1 key2 3.3.5 查找第一个设置为 1 或 0 的位 1 2 3 4 setbit key 7 1 setbit key 15 1 bitpos key 1 # 返回 7 3.3 哈希类型 哈希类型是一个键值对集合，适合存储对象。\n语法:\n1 2 3 4 5 hset key field value # 设置 key 的 field 字段的值为 value hget key field # 获取 key 的 field 字段的值 hmset key field1 value1 field2 value2 # 设置多个 field 的值 hgetall key # 获取 key 的所有 field 和 value 3.3.1 设置哈希值 1 2 hset user name tom hset user age 20 3.3.2 获取哈希值 1 hget user name # 返回 tom 3.3.3 设置多个哈希值 1 2 hmset user name tom age 20 3.3.4 获取所有哈希值 1 2 hgetall user # 返回 name tom age 20 3.3.5 判断字段是否存在 1 hexists user name # 返回 1 3.3.6 当字段不存在时设置值 1 hsetnx user name tom # 如果 name 字段不存在，则设置值 3.3.7 增加数字 1 2 hincrby user age 1 # 将 age 字段的值增加 1 3.3.8 删除字段 1 hdel user name # 删除 name 字段 3.4 列表类型 列表类型是一个有序的字符串链表。\n3.4.1 插入元素 1 lpush list 1 2 3 4 5 # 从左边插入元素 1 rpush list 1 2 3 4 5 # 从右边插入元素 3.4.2 弹出元素 redis 执行弹出元素操作时会先将需要弹出的值删除，然后返回这个值。\n1 lpop list # 从左边弹出元素 1 rpop list # 从右边弹出元素 3.4.3 获取列表中元素的个数 1 llen list # 返回列表中元素的个数 3.4.4 获取列表中指定范围的元素 语法:\n1 lrange key [start] [end] 1 lrange list 0 -1 # 返回列表中所有元素 3.4.5 删除列表中的元素 1 lrem list 1 2 # 删除列表中第一个值为 2 的元素 3.4.6 设置列表中指定索引的值 1 2 lset list 0 100 # 将列表中索引为 0 的值设置为 100 3.5 集合类型 集合类型是一个无序的字符串集合。\n3.5.1 添加/删除元素 1 sadd set 1 2 3 4 5 # 添加元素 1 srem set 1 # 删除元素 3.5.2 获取集合中元素的个数 1 2 scard set # 返回集合中元素的个数 3.5.3 获取集合中所有元素 1 2 smembers set # 返回集合中所有元素 3.5.4 判断元素是否存在 1 2 sismember set 1 # 判断 1 是否在集合中 3.5.5 随机获取元素 1 2 srandmember set # 随机获取一个元素 3.5.6 随机删除元素 1 2 spop set # 随机删除一个元素 3.5.7 求交集 1 2 3 4 5 6 sadd set1 1 2 3 4 5 sadd set2 3 4 5 6 7 sinter set1 set2 # 返回 3 4 5 3.5.8 求并集 1 2 sunion set1 set2 # 返回 1 2 3 4 5 6 7 3.5.9 求差集 1 2 3 4 5 6 sadd set1 1 2 3 4 5 sadd set2 3 4 5 6 7 sdiff set1 set2 # 返回 1 2 3.6 有序集合类型 有序集合类型是一个字符串成员与浮点数分值之间的映射。\n3.6.1 添加元素 1 2 zadd zset 1 tom 2 jerry 3 alice # 添加元素 3.6.2 获取元素的分值 1 2 zscore zset tom # 获取 tom 的分值 3.6.3 获取元素的排名 1 2 zrank zset tom # 获取 tom 的排名 3.6.4 获取指定范围内的元素 1 2 zrange zset 0 -1 # 获取所有元素 3.6.5 删除元素 1 2 zrem zset tom # 删除 tom 3.6.6 获取元素的个数 1 2 zcard zset # 获取元素的个数 3.6.7 获取指定分值 1 2 zcount zset 1 2 # 获取分值在 1 和 2 之间的元素个数 3.6.8 获取指定范围内的元素 1 2 zrangebyscore zset 1 2 # 获取分值在 1 和 2 之间的元素 ","date":"2025-03-06T00:00:00Z","image":"https://programcx.github.io/p/redis-entrance/redis_hu17885680186398226011.jpg","permalink":"https://programcx.github.io/p/redis-entrance/","title":"Redis 基础"},{"content":"使用 MyBatis 实现即时通讯消息的数据库操作 1.数据库设计 新建一个数据库db_im。设计数据表，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 # 用户表 CREATE TABLE tb_users ( user_id BIGINT PRIMARY KEY AUTO_INCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, user_name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL ); # 消息表 CREATE TABLE tb_messages ( message_id BIGINT PRIMARY KEY AUTO_INCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL, state ENUM(\u0026#39;sent\u0026#39;, \u0026#39;delivered\u0026#39;, \u0026#39;read\u0026#39;) NOT NULL DEFAULT \u0026#39;sent\u0026#39;, sender_user_id BIGINT NOT NULL, receiver_user_id BIGINT NOT NULL, FOREIGN KEY (sender_user_id) REFERENCES tb_users(user_id) ON DELETE CASCADE, FOREIGN KEY (receiver_user_id) REFERENCES tb_users(user_id) ON DELETE CASCADE ); # 好友表 CREATE TABLE tb_friends ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, friend_id BIGINT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, status ENUM(\u0026#39;pending\u0026#39;, \u0026#39;accepted\u0026#39;, \u0026#39;blocked\u0026#39;) NOT NULL DEFAULT \u0026#39;pending\u0026#39;, FOREIGN KEY (user_id) REFERENCES tb_users(user_id) ON DELETE CASCADE, FOREIGN KEY (friend_id) REFERENCES tb_users(user_id) ON DELETE CASCADE, UNIQUE KEY unique_friendship (user_id, friend_id) ); # 黑名单表 CREATE TABLE tb_blacklist ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, blocked_user_id BIGINT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES tb_users(user_id) ON DELETE CASCADE, FOREIGN KEY (blocked_user_id) REFERENCES tb_users(user_id) ON DELETE CASCADE, UNIQUE KEY unique_blacklist (user_id, blocked_user_id) ); # 群聊表 CREATE TABLE tb_groups ( group_id BIGINT PRIMARY KEY AUTO_INCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, group_name VARCHAR(255) NOT NULL, owner_user_id BIGINT NOT NULL, FOREIGN KEY (owner_user_id) REFERENCES tb_users(user_id) ON DELETE CASCADE ); # 群聊成员表 CREATE TABLE tb_group_members ( id BIGINT PRIMARY KEY AUTO_INCREMENT, group_id BIGINT NOT NULL, user_id BIGINT NOT NULL, joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, role ENUM(\u0026#39;member\u0026#39;, \u0026#39;admin\u0026#39;, \u0026#39;owner\u0026#39;) NOT NULL DEFAULT \u0026#39;member\u0026#39;, FOREIGN KEY (group_id) REFERENCES tb_groups(group_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES tb_users(user_id) ON DELETE CASCADE, UNIQUE KEY unique_group_membership (group_id, user_id) ); # 群聊消息表 CREATE TABLE tb_group_messages ( message_id BIGINT PRIMARY KEY AUTO_INCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL, sender_user_id BIGINT NOT NULL, group_id BIGINT NOT NULL, FOREIGN KEY (sender_user_id) REFERENCES tb_users(user_id) ON DELETE CASCADE, FOREIGN KEY (group_id) REFERENCES tb_groups(group_id) ON DELETE CASCADE ); # 群读取消息偏移量表 CREATE TABLE tb_group_device_read_offset ( id BIGINT PRIMARY KEY AUTO_INCREMENT, group_id BIGINT NOT NULL, user_id BIGINT NOT NULL, device_id VARCHAR(255) NOT NULL, last_read_msg_id BIGINT NOT NULL DEFAULT 0, last_read_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY unique_device_read_offset (group_id, user_id, device_id) ); # 单聊读取消息偏移量表 CREATE TABLE tb_device_read_offset ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, friend_user_id BIGINT NOT NULL, device_id VARCHAR(255) NOT NULL, last_read_msg_id BIGINT NOT NULL DEFAULT 0, last_read_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY unique_device_read_offset (friend_user_id, user_id, device_id) ); 表设计需要注意的点： 由于用户读取消息后不必重复读取消息，所以需要记录用户读取消息的偏移量，即最后一次读取的消息ID。 用户在一个设备上读取了消息，另一个设备上应该有此消息，所以需要记录用户在不同设备上的读取消息偏移量。 为了防止重复添加好友，需要在好友表中添加一个唯一索引。 为了防止重复添加黑名单，需要在黑名单表中添加一个唯一索引。 为了防止重复加入群聊，需要在群聊成员表中添加一个唯一索引。 为了防止重复发送消息，需要在消息表中添加一个唯一索引。 为了防止重复发送群聊消息，需要在群聊消息表中添加一个唯一索引。 为了防止重复读取消息，需要在单聊读取消息偏移量表中添加一个唯一索引。 为了防止重复读取消息，需要在群读取消息偏移量表中添加一个唯一索引。 为了防止重复创建群聊，需要在群聊表中添加一个唯一索引。 为了防止重复创建用户，需要在用户表中添加一个唯一索引。 为了防止偏移量表中重复记录用户读取消息的偏移量，需要在单聊读取消息偏移量表中添加一个唯一索引。并且当重复添加时，更新最后读取消息的时间以及消息ID。- 为了防止偏移量表中重复记录用户读取消息的偏移量，需要在群读取消息偏移量表中添加一个唯一索引。并且当重复添加时，更新最后读取消息的时间以及消息ID。 2.实体类设计 创建一个pojo包以及 User、Message、Friend、Blacklist、Group、GroupMember、GroupMessage、DeviceReadOffsetMapper、GroupDeviceReadOffsetMapper类。\n2.1 User 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package cn.programcx.im.pojo; import java.sql.Timestamp; public class User { private Long userId; private Timestamp createdAt; private String userName; private String email; private String passwordHash; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Timestamp getCreatedAt() { return createdAt; } public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPasswordHash() { return passwordHash; } public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } @Override public String toString() { return \u0026#34;Users{\u0026#34; + \u0026#34;userId=\u0026#34; + userId + \u0026#34;, createdAt=\u0026#34; + createdAt + \u0026#34;, userName=\u0026#39;\u0026#34; + userName + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, email=\u0026#39;\u0026#34; + email + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, passwordHash=\u0026#39;\u0026#34; + passwordHash + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 2.2 Message 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package cn.programcx.im.pojo; import java.sql.Timestamp; public class Message { public enum State { sent, delivered, read } private Long messageId; private Timestamp createdAt; private String content; private Long senderUserId; private Long receiverUserId; public Long getSenderUserId() { return senderUserId; } public void setSenderUserId(Long senderUserId) { this.senderUserId = senderUserId; } public Long getReceiverUserId() { return receiverUserId; } public void setReceiverUserId(Long receiverUserId) { this.receiverUserId = receiverUserId; } private State state; private User sender; private User receiver; public void setMessageId(Long messageId) { this.messageId = messageId; } public Long getMessageId() { return messageId; } public Timestamp getCreatedAt() { return createdAt; } public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public State getState() { return state; } public void setState(State state) { this.state = state; } public User getSender() { return sender; } public void setSender(User sender) { this.sender = sender; } public User getReceiver() { return receiver; } public void setReceiver(User receiver) { this.receiver = receiver; } } 数据表里面并没有State字段，这里是为了方便代码的编写，所以添加了一个State枚举类。也没有sender和receiver字段，这里添加了sender和receiver两个对象，用于存储发送者和接收者的信息。后续在查询数据表时，使用左连接来查询这些数据。\n2.3 Friend 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 package cn.programcx.im.pojo; import java.sql.Timestamp; public class Friend { public enum Status { pending,accepted,rejected } private Long id; private User friend; private User user; public Long getFriendId() { return friendId; } public void setFriendId(Long friendId) { this.friendId = friendId; } private Long userId; private Long friendId; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } private Timestamp createdAt; private Status status; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public User getFriend() { return friend; } public void setFriend(User friend) { this.friend = friend; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Timestamp getCreatedAt() { return createdAt; } public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } public Status getStatus() { return status; } public void setStatus(Status status) { this.status = status; } } 2.4 Blacklist 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package cn.programcx.im.pojo; import java.sql.Timestamp; public class Blacklist { private Long id; private Long userId; private Long blockedUserId; private User user; private User blockedUser; private Timestamp createdAt; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getBlockedUserId() { return blockedUserId; } public void setBlockedUserId(Long blockedUserId) { this.blockedUserId = blockedUserId; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public User getBlockedUser() { return blockedUser; } public void setBlockedUser(User blockedUser) { this.blockedUser = blockedUser; } public Timestamp getCreatedAt() { return createdAt; } public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } } 2.5 Group 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package cn.programcx.im.pojo; import java.sql.Timestamp; public class Group { private Long groupId; private Timestamp createdAt; private String groupName; public Long getOwnerUserId() { return ownerUserId; } public void setOwnerUserId(Long ownerUserId) { this.ownerUserId = ownerUserId; } private Long ownerUserId; private User ownerUser; public Long getGroupId() { return groupId; } public void setGroupId(Long groupId) { this.groupId = groupId; } public Timestamp getCreatedAt() { return createdAt; } public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } public String getGroupName() { return groupName; } public void setGroupName(String groupName) { this.groupName = groupName; } public User getOwnerUser() { return ownerUser; } public void setOwnerUser(User ownerUser) { this.ownerUser = ownerUser; } } 2.6 GroupMember 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 package cn.programcx.im.pojo; import java.sql.Timestamp; public class GroupMember { public enum Role{ member, admin, owner } private Long id; private Group group; private Long groupId; private User user; private Long userId; private String remark; //加个备注 private Timestamp joinedAt; private Role role; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getGroupId() { return groupId; } public void setGroupId(Long groupId) { this.groupId = groupId; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Group getGroup() { return group; } public void setGroup(Group group) { this.group = group; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Timestamp getJoinedAt() { return joinedAt; } public void setJoinedAt(Timestamp joinedAt) { this.joinedAt = joinedAt; } public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } } 2.7 GroupMessage 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package cn.programcx.im.pojo; import java.sql.Timestamp; public class GroupMessage { private Long messageId; private Timestamp createdAt; private String content; private Long senderUserId; private Long groupId; private User senderUser; private Group group; public Long getSenderUserId() { return senderUserId; } public void setSenderUserId(Long senderUserId) { this.senderUserId = senderUserId; } public Long getGroupId() { return groupId; } public void setGroupId(Long groupId) { this.groupId = groupId; } public Long getMessageId() { return messageId; } public void setMessageId(Long messageId) { this.messageId = messageId; } public Timestamp getCreatedAt() { return createdAt; } public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public User getSenderUser() { return senderUser; } public void setSenderUser(User senderUser) { this.senderUser = senderUser; } public Group getGroup() { return group; } public void setGroup(Group group) { this.group = group; } } 2.8 DeviceReadOffsetMapper 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package cn.programcx.im.pojo; import java.sql.Timestamp; public class DeviceReadOffset { private Long id; private Long userId; private Long friendUserId; private String deviceId; private Long lastReadMsgId; private Timestamp lastReadTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getFriendUserId() { return friendUserId; } public void setFriendUserId(Long friendUserId) { this.friendUserId = friendUserId; } public String getDeviceId() { return deviceId; } public void setDeviceId(String deviceId) { this.deviceId = deviceId; } public Long getLastReadMsgId() { return lastReadMsgId; } public void setLastReadMsgId(Long lastReadMsgId) { this.lastReadMsgId = lastReadMsgId; } public Timestamp getLastReadTime() { return lastReadTime; } public void setLastReadTime(Timestamp lastReadTime) { this.lastReadTime = lastReadTime; } } 2.9 GroupDeviceReadOffsetMapper 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package cn.programcx.im.pojo; import java.sql.Timestamp; public class GroupDeviceReadOffset { private Long id; private Long groupId; private String deviceId; private Long userId; private Long lastReadMsgId; private Timestamp lastReadTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getGroupId() { return groupId; } public void setGroupId(Long groupId) { this.groupId = groupId; } public String getDeviceId() { return deviceId; } public void setDeviceId(String deviceId) { this.deviceId = deviceId; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public Long getLastReadMsgId() { return lastReadMsgId; } public void setLastReadMsgId(Long lastReadMsgId) { this.lastReadMsgId = lastReadMsgId; } public Timestamp getLastReadTime() { return lastReadTime; } public void setLastReadTime(Timestamp lastReadTime) { this.lastReadTime = lastReadTime; } } 3.Mapper 接口设计 为了将封装好的MyBatis的接口和实体类进行绑定，提供操作数据库的方法，需要创建一个mapper包，然后在mapper包下创建UserMapper、MessageMapper、FriendMapper、BlacklistMapper、GroupMapper、GroupMemberMapper、GroupMessageMapper、DeviceReadOffsetMapper、GroupDeviceReadOffsetMapper接口。\n3.1 UserMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package cn.programcx.im.dao; import cn.programcx.im.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; import java.util.List; @Repository @Mapper public interface UserMapper { void addUser(User user); User getUserById(Long id); List\u0026lt;User\u0026gt; getUserByName(String name); void updateUser(User user); void deleteUserById(Long id); } 3.2 MessageMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package cn.programcx.im.dao; import cn.programcx.im.pojo.Message; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Repository; import java.util.List; @Repository @Mapper public interface MessageMapper { void insertMessage(Message message); List\u0026lt;Message\u0026gt; getMessageById(Long id); List\u0026lt;Message\u0026gt; getMessageBySenderId(Long id); List\u0026lt;Message\u0026gt; getMessageByReceiverId(Long id); List\u0026lt;Message\u0026gt; getMessageBySenderAndReceiverId(@Param(\u0026#34;senderId\u0026#34;) Long senderId, @Param(\u0026#34;receiverId\u0026#34;) Long receiverId, @Param(\u0026#34;lastMessageId\u0026#34;) Long lastMessageId); List\u0026lt;Message\u0026gt; getMessageBySenderOrReceiverId(Long id); void updateMessageState(Message message); void deleteMessageById(Long id); } 3.3 FriendMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package cn.programcx.im.dao; import cn.programcx.im.pojo.Friend; import cn.programcx.im.pojo.Message; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import java.util.List; @Repository @Mapper public interface FriendMapper { void addFriend(Friend friend); List\u0026lt;Friend\u0026gt; getAllFriends(Friend friend); List\u0026lt;Friend\u0026gt; getFriendByState(@Param(\u0026#34;userId\u0026#34;)Long userId , @Param(\u0026#34;state\u0026#34;) Friend.Status state); void updateFriendStatus(Friend friend); void deleteFriend(Friend friend); } 3.4 BlacklistMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package cn.programcx.im.dao; import cn.programcx.im.pojo.Blacklist; import cn.programcx.im.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; import java.util.List; @Repository @Mapper public interface BlacklistMapper { void insertBlacklist(Blacklist blacklist); List\u0026lt;Blacklist\u0026gt; getBlockedUsers(User user); void deleteBlockedUser(Blacklist blacklist); } 3.5 GroupMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package cn.programcx.im.dao; import cn.programcx.im.pojo.Group; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; import java.util.List; @Repository @Mapper public interface GroupMapper { void insertGroup(Group group); void deleteGroupById(Long groupId); void updateGroupName(Group group); Group getGroupById(Long groupId); List\u0026lt;Group\u0026gt; getCreatedUserGroups(Long userId); List\u0026lt;Group\u0026gt; getAdminUserGroups(Long userId); List\u0026lt;Group\u0026gt; getMemberUserGroups(Long userId); } 3.6 GroupMemberMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package cn.programcx.im.dao; import cn.programcx.im.pojo.GroupMember; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; import java.util.List; @Repository @Mapper public interface GroupMemberMapper { List\u0026lt;GroupMember\u0026gt; getGroupMembersByGroupId(Long groupId); void insertGroupMember(GroupMember groupMember); void modifyGroupMemberRole(GroupMember groupMember); void modifyGroupMemberRemark(GroupMember groupMember); void deleteGroupMember(GroupMember groupMember); } 3.7 GroupMessageMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package cn.programcx.im.dao; import cn.programcx.im.pojo.GroupMessage; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import java.util.List; @Repository @Mapper public interface GroupMessageMapper { void insertGroupMessage(GroupMessage groupMessage); void deleteGroupMessageByMessageId(Long messageId); List\u0026lt;GroupMessage\u0026gt; getGroupMessagesByGroupId(@Param(\u0026#34;groupId\u0026#34;) Long groupId,@Param(\u0026#34;lastMessageId\u0026#34;) Long lastMessageId); } 3.8 DeviceReadOffsetMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 package cn.programcx.im.dao; import cn.programcx.im.pojo.DeviceReadOffset; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Repository @Mapper public interface DeviceReadOffsetMapper { void insertDeviceReadOffset(DeviceReadOffset deviceReadOffset); DeviceReadOffset getDeviceReadOffset(@Param(\u0026#34;userId\u0026#34;)Long userId,@Param(\u0026#34;friendUserId\u0026#34;) Long friendUserId,@Param(\u0026#34;deviceId\u0026#34;) String deviceId); } 3.9 GroupDeviceReadOffsetMapper 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 package cn.programcx.im.dao; import cn.programcx.im.pojo.GroupDeviceReadOffset; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Repository @Mapper public interface GroupDeviceReadOffsetMapper { void insertGroupDeviceReadOffset(GroupDeviceReadOffset groupDeviceReadOffset); GroupDeviceReadOffset getGroupDeviceReadOffset(@Param(\u0026#34;groupId\u0026#34;) Long groupId, @Param(\u0026#34;deviceId\u0026#34;) String deviceId, @Param(\u0026#34;userId\u0026#34;) Long userId); } 一般，需要简单查询，我们传入id作为参数，如果较为复杂，可能需要传入多个参数，这里使用@Param注解来传入多个参数。也可以通过传入POJO对象来传入多个参数。\n4. MyBatis的XML文件设计 在resources目录下创建mapper目录，然后在mapper目录下创建UserMapper.xml、MessageMapper.xml、FriendMapper.xml、BlacklistMapper.xml、GroupMapper.xml、GroupMemberMapper.xml、GroupMessageMapper.xml、DeviceReadOffsetMapper.xml、GroupDeviceReadOffsetMapper.xml文件。\n4.1 UserMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.UserMapper\u0026#34;\u0026gt; \u0026lt;insert id=\u0026#34;addUser\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.User\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;userId\u0026#34;\u0026gt; insert ignore into tb_users (user_name, email, password_hash) values (#{userName},#{email},#{passwordHash}) \u0026lt;/insert\u0026gt; \u0026lt;select id=\u0026#34;getUserById\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; select * from tb_users where user_id = #{userId} \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getUserByName\u0026#34; parameterType=\u0026#34;String\u0026#34; resultType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; select * from tb_users where user_name = #{userName} \u0026lt;/select\u0026gt; \u0026lt;update id=\u0026#34;updateUser\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; update tb_users set user_name = #{userName}, email = #{email}, password_hash = #{passwordHash} where user_id = #{userId} \u0026lt;/update\u0026gt; \u0026lt;delete id=\u0026#34;deleteUserById\u0026#34; parameterType=\u0026#34;Long\u0026#34;\u0026gt; delete from tb_users where user_id = #{userId} \u0026lt;/delete\u0026gt; \u0026lt;/mapper\u0026gt; 4.2 MessageMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.MessageMapper\u0026#34;\u0026gt; \u0026lt;insert id=\u0026#34;insertMessage\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.Message\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;messageId\u0026#34;\u0026gt; insert into tb_messages(content, state, sender_user_id, receiver_user_id) values (#{content}, #{state}, #{sender.userId}, #{receiver.userId}) \u0026lt;/insert\u0026gt; \u0026lt;select id=\u0026#34;getMessageById\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultMap=\u0026#34;MessageResultMap\u0026#34;\u0026gt; select m.message_id, m.content, m.created_at, m.state, u1.user_id as sender_user_id, u1.user_name as sender_user_name, u1.email as sender_email, u1.password_hash as sender_password_hash, u2.user_id as receiver_user_id, u2.user_name as receiver_user_name, u2.email as receiver_email, u2.password_hash as receiver_password_hash from tb_messages m left join tb_users u1 on m.sender_user_id = u1.user_id left join tb_users u2 on m.receiver_user_id = u2.user_id where m.message_id = #{messageId} \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getMessageBySenderId\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultMap=\u0026#34;MessageResultMap\u0026#34;\u0026gt; select m.message_id,m.content,m.created_at,m.state, u1.user_id as sender_user_id, u1.user_name as sender_user_name, u1.email as sender_email, u1.password_hash as sender_password_hash, u2.user_id as receiver_user_id, u2.user_name as receiver_user_name, u2.email as receiver_email, u2.password_hash as receiver_password_hash from db_im.tb_messages m left join db_im.tb_users u1 on m.sender_user_id = u1.user_id left join db_im.tb_users u2 on m.receiver_user_id = u2.user_id where m.sender_user_id = #{senderId} \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getMessageByReceiverId\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultMap=\u0026#34;MessageResultMap\u0026#34;\u0026gt; select m.message_id,m.content,m.created_at,m.state, u1.user_id as sender_user_id, u1.user_name as sender_user_name, u1.email as sender_email, u1.password_hash as sender_password_hash, u2.user_id as receiver_user_id, u2.user_name as receiver_user_name, u2.email as receiver_email, u2.password_hash as receiver_password_hash from db_im.tb_messages m left join db_im.tb_users u1 on m.sender_user_id = u1.user_id left join db_im.tb_users u2 on m.receiver_user_id = u2.user_id where m.receiver_user_id = #{receiverId} \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getMessageBySenderOrReceiverId\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultMap=\u0026#34;MessageResultMap\u0026#34;\u0026gt; select m.message_id,m.content,m.created_at,m.state, u1.user_id as sender_user_id, u1.user_name as sender_user_name, u1.email as sender_email, u1.password_hash as sender_password_hash, u2.user_id as receiver_user_id, u2.user_name as receiver_user_name, u2.email as receiver_email, u2.password_hash as receiver_password_hash from db_im.tb_messages m left join db_im.tb_users u1 on m.sender_user_id = u1.user_id left join db_im.tb_users u2 on m.receiver_user_id = u2.user_id where m.sender_user_id = #{senderId} or m.receiver_user_id = #{receiverId} \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getMessageBySenderAndReceiverId\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultMap=\u0026#34;MessageResultMap\u0026#34;\u0026gt; select m.message_id,m.content,m.created_at,m.state, u1.user_id as sender_user_id, u1.user_name as sender_user_name, u1.email as sender_email, u1.password_hash as sender_password_hash, u2.user_id as receiver_user_id, u2.user_name as receiver_user_name, u2.email as receiver_email, u2.password_hash as receiver_password_hash from db_im.tb_messages m left join db_im.tb_users u1 on m.sender_user_id = u1.user_id left join db_im.tb_users u2 on m.receiver_user_id = u2.user_id where ((m.sender_user_id = #{senderId} and m.receiver_user_id = #{receiverId}) or (m.sender_user_id = #{receiverId} and m.receiver_user_id =#{senderId})) and m.message_id \u0026gt; #{lastMessageId} order by m.created_at desc \u0026lt;/select\u0026gt; \u0026lt;resultMap id=\u0026#34;MessageResultMap\u0026#34; type=\u0026#34;cn.programcx.im.pojo.Message\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;message_id\u0026#34; property=\u0026#34;messageId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;content\u0026#34; property=\u0026#34;content\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;created_at\u0026#34; property=\u0026#34;createdAt\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;state\u0026#34; property=\u0026#34;state\u0026#34;/\u0026gt; \u0026lt;association property=\u0026#34;sender\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;sender_user_id\u0026#34; property=\u0026#34;userId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;sender_user_name\u0026#34; property=\u0026#34;userName\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;sender_email\u0026#34; property=\u0026#34;email\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;sender_password_hash\u0026#34; property=\u0026#34;passwordHash\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;association property=\u0026#34;receiver\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;receiver_user_id\u0026#34; property=\u0026#34;userId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;receiver_user_name\u0026#34; property=\u0026#34;userName\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;receiver_email\u0026#34; property=\u0026#34;email\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;receiver_password_hash\u0026#34; property=\u0026#34;passwordHash\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;update id=\u0026#34;updateMessageState\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.Message\u0026#34;\u0026gt; update tb_messages set state = #{state} where message_id = #{messageId} \u0026lt;/update\u0026gt; \u0026lt;delete id=\u0026#34;deleteMessageById\u0026#34; parameterType=\u0026#34;Long\u0026#34;\u0026gt; delete from tb_messages where message_id = #{messageId} \u0026lt;/delete\u0026gt; \u0026lt;/mapper\u0026gt; 4.3 FriendMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.FriendMapper\u0026#34;\u0026gt; \u0026lt;insert id=\u0026#34;addFriend\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.Friend\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;id\u0026#34;\u0026gt; insert ignore into tb_friends(user_id,friend_id,status) values (#{user.userId},#{friend.userId},#{status}) \u0026lt;/insert\u0026gt; \u0026lt;select id=\u0026#34;getAllFriends\u0026#34; resultMap=\u0026#34;FriendsResultMap\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; select f.id, f.user_id,f.friend_id,f.status, tu.user_id as friend_user_id, tu.user_name as friend_user_name, tu.email as friend_email, tu1.user_id as user_user_id, tu1.user_name as user_user_name, tu1.email as user_email from tb_friends f left join tb_users tu on tu.user_id = f.friend_id left join tb_users tu1 on tu1.user_id = f.user_id where f.user_id = #{user.userId} order by tu.user_name \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getFriendByState\u0026#34; resultMap=\u0026#34;FriendsResultMap\u0026#34; \u0026gt; select f.id, f.user_id,f.friend_id,f.status, tu.user_id as friend_user_id, tu.user_name as friend_user_name, tu.email as friend_email, tu1.user_id as user_user_id, tu1.user_name as user_user_name, tu1.email as user_email from tb_friends f left join tb_users tu on tu.user_id = f.friend_id left join tb_users tu1 on tu1.user_id = f.user_id where f.user_id = #{userId} and f.status = #{state} order by tu.user_name \u0026lt;/select\u0026gt; \u0026lt;update id=\u0026#34;updateFriendStatus\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.Friend\u0026#34;\u0026gt; update tb_friends set status = #{status} where user_id = #{user.userId} and friend_id = #{friend.userId} \u0026lt;/update\u0026gt; \u0026lt;delete id=\u0026#34;deleteFriend\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.Friend\u0026#34;\u0026gt; delete from tb_friends where user_id = #{user.userId} and friend_id = #{friend.userId} \u0026lt;/delete\u0026gt; \u0026lt;resultMap id=\u0026#34;FriendsResultMap\u0026#34; type=\u0026#34;cn.programcx.im.pojo.Friend\u0026#34;\u0026gt; \u0026lt;id property=\u0026#34;id\u0026#34; column=\u0026#34;id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;userId\u0026#34; column=\u0026#34;user_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;friendId\u0026#34; column=\u0026#34;friend_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;status\u0026#34; column=\u0026#34;status\u0026#34;/\u0026gt; \u0026lt;association property=\u0026#34;user\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id property=\u0026#34;userId\u0026#34; column=\u0026#34;user_user_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;userName\u0026#34; column=\u0026#34;user_user_name\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;email\u0026#34; column=\u0026#34;user_email\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;association property=\u0026#34;friend\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id property=\u0026#34;userId\u0026#34; column=\u0026#34;friend_user_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;userName\u0026#34; column=\u0026#34;friend_user_name\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;email\u0026#34; column=\u0026#34;friend_email\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;/mapper\u0026gt; 4.4 BlacklistMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.BlacklistMapper\u0026#34;\u0026gt; \u0026lt;insert id=\u0026#34;insertBlacklist\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.Blacklist\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;id\u0026#34;\u0026gt; insert ignore into tb_blacklist(user_id,blocked_user_id) values (#{user.userId},#{blockedUser.userId}) \u0026lt;/insert\u0026gt; \u0026lt;resultMap id=\u0026#34;BlacklistResultMap\u0026#34; type=\u0026#34;cn.programcx.im.pojo.Blacklist\u0026#34;\u0026gt; \u0026lt;id property=\u0026#34;id\u0026#34; column=\u0026#34;id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;userId\u0026#34; column=\u0026#34;user_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;blockedUserId\u0026#34; column=\u0026#34;blocked_user_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;createdAt\u0026#34; column=\u0026#34;created_at\u0026#34;/\u0026gt; \u0026lt;association property=\u0026#34;user\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id property=\u0026#34;userId\u0026#34; column=\u0026#34;user_user_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;userName\u0026#34; column=\u0026#34;user_user_name\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;email\u0026#34; column=\u0026#34;user_email\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;association property=\u0026#34;blockedUser\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id property=\u0026#34;userId\u0026#34; column=\u0026#34;blocked_user_user_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;userName\u0026#34; column=\u0026#34;blocked_user_user_name\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;email\u0026#34; column=\u0026#34;blocked_user_email\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;select id=\u0026#34;getBlockedUsers\u0026#34; resultMap=\u0026#34;BlacklistResultMap\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; select b.id,b.user_id,b.blocked_user_id,b.created_at, tu.user_id as user_user_id,tu.user_name as user_user_name,tu.email as user_email, tu1.user_id as blocked_user_user_id,tu1.user_name as blocked_user_user_name,tu1.email as blocked_user_email from tb_blacklist b left join tb_users tu on tu.user_id = b.user_id left join tb_users tu1 on tu1.user_id = b.blocked_user_id where b.user_id = #{userId} \u0026lt;/select\u0026gt; \u0026lt;delete id=\u0026#34;deleteBlockedUser\u0026#34; parameterType=\u0026#34;cn.programcx.im.dao.BlacklistMapper\u0026#34;\u0026gt; delete from tb_blacklist where user_id = #{user.userId} and blocked_user_id = #{blockedUser.userId} \u0026lt;/delete\u0026gt; \u0026lt;/mapper\u0026gt; 4.5 GroupMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.GroupMapper\u0026#34;\u0026gt; \u0026lt;resultMap id=\u0026#34;groupMapper\u0026#34; type=\u0026#34;cn.programcx.im.pojo.Group\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;group_id\u0026#34; property=\u0026#34;groupId\u0026#34; /\u0026gt; \u0026lt;result property=\u0026#34;groupName\u0026#34; column=\u0026#34;group_name\u0026#34; /\u0026gt; \u0026lt;result property=\u0026#34;createdAt\u0026#34; column=\u0026#34;created_at\u0026#34; /\u0026gt; \u0026lt;result property=\u0026#34;ownerUserId\u0026#34; column=\u0026#34;owner_u\u0026#34; /\u0026gt; \u0026lt;association property=\u0026#34;ownerUser\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;user_id\u0026#34; property=\u0026#34;userId\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;user_name\u0026#34; property=\u0026#34;userName\u0026#34; /\u0026gt; \u0026lt;result column=\u0026#34;email\u0026#34; property=\u0026#34;email\u0026#34; /\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;select id=\u0026#34;getGroupById\u0026#34; resultMap=\u0026#34;groupMapper\u0026#34; parameterType=\u0026#34;Long\u0026#34;\u0026gt; select g.group_id,g.group_name,g.created_at,g.owner_user_id, u.user_id,u.user_name,u.email from tb_groups g left join tb_users u on g.owner_user_id = u.user_id where g.group_id = #{groupId} \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getCreatedUserGroups\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultMap=\u0026#34;groupMapper\u0026#34;\u0026gt; select g.group_id,g.group_name,g.created_at,g.owner_user_id, u.user_id,u.user_name,u.email from tb_groups g left join tb_users u on g.owner_user_id = u.user_id where g.owner_user_id = #{userId} \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getMemberUserGroups\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultMap=\u0026#34;groupMapper\u0026#34;\u0026gt; select g.*, u.user_id,u.user_name,u.email from tb_groups g inner join tb_group_members gm on g.group_id = gm.group_id left join tb_users u on g.owner_user_id = u.user_id where gm.user_id = #{userId} and gm.role = \u0026#39;member\u0026#39; \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;getAdminUserGroups\u0026#34; parameterType=\u0026#34;Long\u0026#34; resultMap=\u0026#34;groupMapper\u0026#34;\u0026gt; select g.*, u.user_id,u.user_name,u.email from tb_groups g inner join tb_group_members gm on g.group_id = gm.group_id left join tb_users u on g.owner_user_id = u.user_id where gm.user_id = #{userId} and gm.role = \u0026#39;admin\u0026#39; \u0026lt;/select\u0026gt; \u0026lt;update id=\u0026#34;updateGroupName\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.Group\u0026#34;\u0026gt; update tb_groups set group_name = #{groupName} where group_id = #{groupId} \u0026lt;/update\u0026gt; \u0026lt;insert id=\u0026#34;insertGroup\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.Group\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;groupId\u0026#34;\u0026gt; insert ignore into tb_groups(group_name,owner_user_id) values (#{groupName},#{ownerUser.userId}) \u0026lt;/insert\u0026gt; \u0026lt;delete id=\u0026#34;deleteGroupById\u0026#34; parameterType=\u0026#34;Long\u0026#34;\u0026gt; delete from tb_groups where group_id=#{groupId} \u0026lt;/delete\u0026gt; \u0026lt;/mapper\u0026gt; 4.6 GroupMemberMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.GroupMemberMapper\u0026#34;\u0026gt; \u0026lt;resultMap id=\u0026#34;groupMemberResultMapper\u0026#34; type=\u0026#34;cn.programcx.im.pojo.GroupMember\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;id\u0026#34; property=\u0026#34;id\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;group_id\u0026#34; property=\u0026#34;groupId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;user_id\u0026#34; property=\u0026#34;userId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;role\u0026#34; property=\u0026#34;role\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;joined_at\u0026#34; property=\u0026#34;joinedAt\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;remark\u0026#34; property=\u0026#34;remark\u0026#34;/\u0026gt; \u0026lt;association property=\u0026#34;user\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;user_id\u0026#34; property=\u0026#34;userId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;user_name\u0026#34; property=\u0026#34;userName\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;email\u0026#34; property=\u0026#34;email\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;association property=\u0026#34;group\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.Group\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;group_id\u0026#34; property=\u0026#34;groupId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;group_name\u0026#34; property=\u0026#34;groupName\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;created_at\u0026#34; property=\u0026#34;createdAt\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;owner_user_id\u0026#34; property=\u0026#34;ownerUserId\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;select id=\u0026#34;getGroupMembersByGroupId\u0026#34; resultMap=\u0026#34;groupMemberResultMapper\u0026#34; parameterType=\u0026#34;Long\u0026#34;\u0026gt; select gm.*, u.user_id,u.user_name,u.email, g.group_id,g.group_name,g.created_at,g.owner_user_id from tb_group_members gm left join tb_users u on gm.user_id = u.user_id left join tb_groups g on gm.group_id = g.group_id where g.group_id = #{groupId} \u0026lt;/select\u0026gt; \u0026lt;insert id=\u0026#34;insertGroupMember\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.GroupMember\u0026#34; keyProperty=\u0026#34;id\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;if test=\u0026#34;remark != null\u0026#34;\u0026gt; insert ignore into tb_group_members(user_id,role,group_id,remark) values (#{user.userId},#{role},#{group.groupId},#{remark}) \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;remark == null\u0026#34;\u0026gt; insert into tb_group_members(user_id,role,group_id) values (#{user.userId},#{role},#{group.groupId}) \u0026lt;/if\u0026gt; \u0026lt;/insert\u0026gt; \u0026lt;update id=\u0026#34;modifyGroupMemberRole\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.GroupMember\u0026#34;\u0026gt; update tb_group_members set role = #{role} where group_id = #{group.groupId} and user_id = #{user.userId} \u0026lt;/update\u0026gt; \u0026lt;update id=\u0026#34;modifyGroupMemberRemark\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.GroupMember\u0026#34;\u0026gt; update tb_group_members set remark = #{remark} where group_id = #{group.groupId} and user_id = #{user.userId} \u0026lt;/update\u0026gt; \u0026lt;delete id=\u0026#34;deleteGroupMember\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.GroupMember\u0026#34;\u0026gt; delete from tb_group_members where id = #{id} or (user_id = #{user.userId} and group_id = #{group.groupId}) \u0026lt;/delete\u0026gt; \u0026lt;/mapper\u0026gt; 4.7 GroupMessageMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.GroupMessageMapper\u0026#34;\u0026gt; \u0026lt;resultMap id=\u0026#34;groupMessageMap\u0026#34; type=\u0026#34;cn.programcx.im.pojo.GroupMessage\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;message_id\u0026#34; property=\u0026#34;messageId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;content\u0026#34; property=\u0026#34;content\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;created_at\u0026#34; property=\u0026#34;createdAt\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;sender_user_id\u0026#34; property=\u0026#34;senderUserId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;group_id\u0026#34; property=\u0026#34;groupId\u0026#34;/\u0026gt; \u0026lt;association property=\u0026#34;senderUser\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.User\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;user_id\u0026#34; property=\u0026#34;userId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;user_name\u0026#34; property=\u0026#34;userName\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;email\u0026#34; property=\u0026#34;email\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;association property=\u0026#34;group\u0026#34; javaType=\u0026#34;cn.programcx.im.pojo.Group\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;group_id\u0026#34; property=\u0026#34;groupId\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;group_name\u0026#34; property=\u0026#34;groupName\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;created_at\u0026#34; property=\u0026#34;createdAt\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;owner_user_id\u0026#34; property=\u0026#34;ownerUserId\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;insert id=\u0026#34;insertGroupMessage\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.GroupMessage\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;messageId\u0026#34;\u0026gt; insert into tb_group_messages(content,sender_user_id,group_id) values (#{content},#{senderUser.userId},#{group.groupId}) \u0026lt;/insert\u0026gt; \u0026lt;select id=\u0026#34;getGroupMessagesByGroupId\u0026#34; resultMap=\u0026#34;groupMessageMap\u0026#34; parameterType=\u0026#34;Long\u0026#34;\u0026gt; select gm.*, u.user_id,u.user_name,u.email, g.group_id,g.group_name,g.created_at,g.owner_user_id from tb_group_messages gm left join tb_users u on gm.sender_user_id = u.user_id left join tb_groups g on gm.group_id = g.group_id where gm.group_id = #{groupId} and gm.message_id \u0026gt; #{lastMessageId} order by gm.created_at desc \u0026lt;/select\u0026gt; \u0026lt;delete id=\u0026#34;deleteGroupMessageByMessageId\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.GroupMessage\u0026#34;\u0026gt; delete from tb_group_messages where message_id = #{messageId} \u0026lt;/delete\u0026gt; \u0026lt;/mapper\u0026gt; 4.8 DeviceReadOffsetMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.DeviceReadOffsetMapper\u0026#34;\u0026gt; \u0026lt;insert id=\u0026#34;insertDeviceReadOffset\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.DeviceReadOffset\u0026#34;\u0026gt; INSERT INTO tb_device_read_offset (device_id, user_id,friend_user_id, last_read_msg_id) VALUES (#{deviceId}, #{userId}, #{friendUserId}, #{lastReadMsgId}) ON DUPLICATE KEY UPDATE last_read_msg_id = #{lastReadMsgId} \u0026lt;/insert\u0026gt; \u0026lt;select id=\u0026#34;getDeviceReadOffset\u0026#34; resultType=\u0026#34;cn.programcx.im.pojo.DeviceReadOffset\u0026#34; \u0026gt; select * from tb_device_read_offset where device_id = #{deviceId} and user_id = #{userId} and friend_user_id = #{friendUserId} \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 4.9 GroupDeviceReadOffsetMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.programcx.im.dao.GroupDeviceReadOffsetMapper\u0026#34;\u0026gt; \u0026lt;resultMap id=\u0026#34;groupDeviceReadOffsetResultMap\u0026#34; type=\u0026#34;cn.programcx.im.pojo.GroupDeviceReadOffset\u0026#34;\u0026gt; \u0026lt;id property=\u0026#34;id\u0026#34; column=\u0026#34;id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;groupId\u0026#34; column=\u0026#34;group_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;deviceId\u0026#34; column=\u0026#34;device_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;userId\u0026#34; column=\u0026#34;user_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;lastReadMsgId\u0026#34; column=\u0026#34;last_read_msg_id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;lastReadTime\u0026#34; column=\u0026#34;last_read_time\u0026#34;/\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;insert id=\u0026#34;insertGroupDeviceReadOffset\u0026#34; parameterType=\u0026#34;cn.programcx.im.pojo.GroupDeviceReadOffset\u0026#34;\u0026gt; INSERT INTO tb_group_device_read_offset (group_id, device_id, user_id, last_read_msg_id) VALUES (#{groupId}, #{deviceId}, #{userId}, #{lastReadMsgId}) ON DUPLICATE KEY UPDATE last_read_msg_id = #{lastReadMsgId} \u0026lt;/insert\u0026gt; \u0026lt;select id=\u0026#34;getGroupDeviceReadOffset\u0026#34; resultMap=\u0026#34;groupDeviceReadOffsetResultMap\u0026#34;\u0026gt; select * from tb_group_device_read_offset where group_id = #{groupId} and device_id = #{deviceId} and user_id = #{userId} \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 部分代码解析 \u0026lt;insert\u0026gt;标签用于插入数据，parameterType属性指定传入的参数类型，useGeneratedKeys属性指定是否使用自动生成的主键，keyProperty属性指定主键的属性名。比如我给用户创建了一个账号，也就是执行了addUser方法，这时候就会执行insert标签中的SQL语句，将用户的信息插入到数据库中。但是如果要获取用户id给用户呢？难道还要再进行一次查询吗？这时候就可以使用useGeneratedKeys属性，将keyProperty属性指定为userId，MyBatis 会自动将这个数值填到传入的类user的userId里面。这样就可以获取到插入的用户id了。 如果要查询的数据将要填到的POJO类里面有关联的类，可以使用\u0026lt;association\u0026gt;标签，将关联的类的属性填充到POJO类中。写insert语句时需要用左连接left join查询关联的表，将这些结果填到关联类的属性里面。 我们如果已经使用UNIQUE约束来保证数据的唯一性，那么在插入数据时，如果数据已经存在，就不会再插入数据，这时候可以使用ON DUPLICATE KEY UPDATE语句(比如上述代码的insertGroupDeviceReadOffset，这样，用户读取消息后，后端只需要执行这个insertGroupDeviceReadOffset，而不需要先查询这个记录是否存在，然后再执行类似于updateGroupDeviceReadOffset的方法)，如果数据已经存在，就会更新数据，如果数据不存在，就会插入数据。这些据情况而定，如果不需要更新数据，可以使用INSERT IGNORE，这样即使有重复的键，也不会报错 对于怎样获取用户未读取的消息，可以使用where语句，例如 GroupDeviceReadOffsetMapper.xml 文件的where m.message_id \u0026gt; #{lastMessageId}，这样就可以获取到用户未读取的消息了。 该文章时作者学习 MyBatis 写的，为了迁移运用所学知识。里面代码可能不是最佳实践，仅供学习参考。欢迎广大读者指出错误。\n项目地址：https://github.com/ProgramCX/flow-im-backend\n该项目会随着作者学习的技术栈的丰富不断完成其它层的设计，优化已经写的代码。\n","date":"2025-03-05T00:00:00Z","image":"https://programcx.github.io/p/springboot-im-mybatis/MyBatis_hu1891604869159411546.png","permalink":"https://programcx.github.io/p/springboot-im-mybatis/","title":"使用 MyBatis 实现即时通讯消息的数据库操作"},{"content":"JPA 在Spring Boot中的基本使用 1. 添加依赖 1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-jpa\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 2. 配置数据源 1 2 3 4 5 spring: datasource: url: jdbc:mysql://localhost:3306/init?useUnicode=true\u0026amp;characterEncoding=utf-8\u0026amp;useSSL=false\u0026amp;serverTimezone=GMT%2B8 username: root password: mysql 3.常用注解 @Entity：声明一个实体类 @Table：指定实体类对应的表 name：指定表名 catalog：指定数据库名 schema：指定数据库模式 uniqueConstraints：指定表的唯一约束 indexes：指定表的索引 @Id：声明一个实体类的主键，一个实体只能有一个属性被映射为主键 @GeneratedValue：指定主键的生成策略 AUTO：自动选择合适的策略, 默认值 IDENTITY：数据库自增 SEQUENCE：数据库序列 TABLE：数据库表 默认为AUTO\n@Column：指定实体类属性与数据库表字段的映射关系 name：指定字段名 nullable：是否允许为空 unique：是否唯一 length：字段长度 precision：精度 scale：小数位数 insertable：是否插入 updatable：是否更新 columnDefinition：字段定义 @Transient：声明一个实体类属性不与数据库表字段映射 @OneToMany：一对多关联 targetEntity：目标实体类 mappedBy：指定关联的属性 cascade：级联操作 fetch：加载策略 @ManyToOne：多对一关联 targetEntity：目标实体类 cascade：级联操作 fetch：加载策略 @ManyToMany：多对多关联 targetEntity：目标实体类 mappedBy：指定关联的属性 cascade：级联操作 fetch：加载策略 @OneToOne：一对一关联 targetEntity：目标实体类 mappedBy：指定关联的属性 cascade：级联操作 fetch：加载策略 optional：是否可选 @JoinColumns：指定多个外键列 @JoinColumn：指定外键列 name：指定外键列名 referencedColumnName：指定参考列名 nullable：是否允许为空 unique：是否唯一 insertable：是否插入 updatable：是否更新 @JoinColumn：指定外键列 name：指定外键列名 referencedColumnName：指定参考列名 nullable：是否允许为空 unique：是否唯一 insertable：是否插入 updatable：是否更新 @JoinTable：指定中间表 name：指定中间表名 joinColumns：指定当前表的外键列 inverseJoinColumns：指定目标表的外键列 @OrderBy：指定排序 value：排序字段 asc：是否升序 @Query：自定义查询 value：查询语句 nativeQuery：是否使用原生SQL countQuery：查询总数 name：查询名称 hints：查询提示 @PrePersist：保存前执行 解释：\n多对一查询（@ManyToOne） 在多对一查询中，我们一般使用 @ManyToOne 注解，让“多”的一方存储外键，指向“单个”的一方。比如，消息表 (tb_messages) 里的 senderUserId 作为外键，指向用户表 (tb_users) 的 id。\n@ManyToOne 注解中，我们可以：\n使用 targetEntity 指定目标实体类。\n使用 cascade 指定级联操作。\n使用 fetch 指定加载策略。\n配合 @JoinColumn 注解，指定外键字段及相关属性：\nname：指定外键列名（不是外键对应的键的列名）。\nreferencedColumnName：指定参考列名（即目标表的主键）。\nnullable：是否允许为空。\nunique：是否唯一。\ninsertable：是否允许插入。\nupdatable：是否允许更新。\n一对多查询（@OneToMany） 在一对多查询中，我们一般使用 @OneToMany 注解，表示一个实体类可以对应多个子实体。**数据库不会创建外键列，而是通过 \u0026ldquo;多\u0026rdquo; 的一方的 @ManyToOne 维护关系。**例如：\n1 2 @OneToMany(mappedBy = \u0026#34;user\u0026#34;, cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List\u0026lt;Messages\u0026gt; messages; 上述代码写在要被映射到的POJO类中，表示这个类有一个 List 类型的 messages 属性，这个属性是一个一对多关系。\nmappedBy = \u0026ldquo;user\u0026rdquo;：必须指定，表示这个关系是由 Messages 实体的 user 维护的。\ncascade：可以指定级联操作，如 CascadeType.ALL 代表级联新增、删除、更新。\nfetch：\nFetchType.LAZY（默认）：懒加载，只有访问 messages 时才查询数据库。\nFetchType.EAGER：立即加载，查询 Users 时会一并查询 messages。\n多对多查询（@ManyToMany） 在多对多查询中，我们一般使用 @ManyToMany 注解，表示两个实体类之间有多对多关系。**数据库会创建中间表，中间表存储两个实体类的主键。**例如：\n1 2 3 @ManyToMany @JoinTable(name = \u0026#34;tb_user_to_user\u0026#34;, joinColumns = @JoinColumn(name = \u0026#34;userId_1\u0026#34;), inverseJoinColumns = @JoinColumn(name = \u0026#34;userId_2\u0026#34;)) private List\u0026lt;Users\u0026gt; users; 上述代码写在要被映射到的POJO类中，表示这个类有一个 List 类型的 users 属性，这个属性是一个多对多关系。\n@JoinTable：指定中间表。\nname：指定中间表名。\njoinColumns：指定当前表的外键列。\ninverseJoinColumns：指定目标表的外键列。\n一对一查询（@OneToOne） 在一对一查询中，我们一般使用 @OneToOne 注解，表示两个实体类之间有一对一关系。**数据库会创建外键列，外键列存储两个实体类的主键。**例如：\n1 2 3 4 @OneToOne @JoinColumn(name = \u0026#34;userToUserId\u0026#34;, nullable = false) private UserToUser userToUser; 上述代码写在要被映射到的POJO类中，表示这个类有一个 UserToUser 类型的 userToUser 属性，这个属性是一个一对一关系。\n@JoinColumn：指定外键列。\nname：指定外键列名。\nreferencedColumnName：指定参考列名。\nnullable：是否允许为空。\nunique：是否唯一。\ninsertable：是否允许插入。此属性在我们不希望JPA操作这个字段时，可以设置为false。\nupdatable：是否允许更新。此属性在我们不希望JPA操作这个字段时，可以设置为false。\noptional：是否可选。\n4.简单增删改查 随便创建一个数据库，命名为init，创建一个表new，表结构如下：\n4.1 创建实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package cn.programcx.springbootinit.model; import javax.persistence.*; @Entity @Table(name = \u0026#34;new\u0026#34;) public class Newer { @Id @Column(name = \u0026#34;id\u0026#34;) @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column(name = \u0026#34;name\u0026#34;) private String name; public Newer(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Newer() { } @Override public String toString() { return \u0026#34;Newers{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 注解解释：\n@Entity：声明一个实体类，比如Newer就是一个实体类 @Table：指定实体类对应的表，比如Newer对应的表是new @Id：声明一个实体类的主键，一个实体只能有一个属性被映射为主键 @GeneratedValue：指定主键的生成策略，这里使用IDENTITY，即数据库自增。我们用的是MySQL数据库，所以是IDENTITY，如果是Oracle数据库，就是SEQUENCE。 @Column：指定实体类属性与数据库表字段的映射关系，比如id对应的字段是id，name对应的字段是name 4.2 创建Repository 我们需要使用一个类来操作数据库，这个类就是NewerDao，我们需要创建一个NewerDao接口，继承JpaRepository接口，JpaRepository接口有很多方法，比如save、delete、findAll等等，我们可以直接使用这些方法，也可以自定义方法。\n1 2 3 4 5 6 7 8 package cn.programcx.springbootinit.dao; import cn.programcx.springbootinit.model.Newer; import org.springframework.data.jpa.repository.JpaRepository; public interface NewerDao extends JpaRepository\u0026lt;Newer, Integer\u0026gt; { } 4.3 增 我们在进行每一次操作之前，都需要先创建一个Newer对象，然后再进行操作。操作结束后，我们需要调用save方法，将对象保存到数据库中。\n创建一个测试类，测试增加操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package cn.programcx.springbootinit; import cn.programcx.springbootinit.dao.NewerDao; import cn.programcx.springbootinit.model.Newer; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class SpringBootInitApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Autowired NewerDao newerDao; @Test void test() { Newer newer = new Newer(); newer.setName(\u0026#34;Program\u0026#34;); newerDao.save(newer); //保存到数据库 } } 4.4 删 删除操作和增加操作类似，我们需要先创建一个Newer对象，然后再进行操作。操作结束后，我们需要调用delete方法，将对象从数据库中删除。\n1 2 3 4 5 void test() { Newer newer = new Newer(); newer.setId(1); newerDao.delete(newer); //删除 } 4.5 改 修改操作和增加操作类似，我们需要先创建一个Newer对象，然后再进行操作。操作结束后，我们需要调用save方法，将对象保存到数据库中。\n1 2 3 4 5 6 7 void test() { Newer newer = new Newer(); newer.setId(1); newer.setName(\u0026#34;ProgramCX\u0026#34;); newerDao.save(newer); //修改 } 4.6 查 查询操作和增加操作类似，我们需要先创建一个Newer对象，然后再进行操作。操作结束后，我们需要调用findAll方法，将对象保存到数据库中。\n1 2 3 4 5 6 7 void test() { List\u0026lt;Newer\u0026gt; list = newerDao.findAll(); //查询 for (Newer newer : list) { System.out.println(newer); } } 5. 复杂增删改查 5.1 多表 有点难度哈，让我尝试尝试😭 本例中，我们设计一个简单的即时通信软件的数据库。即时通信软件的数据库设计是一个比较复杂的问题，我们只设计一个简单的数据库，只包含用户表、消息表和用户关系表。用于学习JPA的多对多和多对一关联。 设计三个表: tb_messages，tb_user_to_user，tb_users。tb_messages表存储消息，tb_user_to_user表存储用户之间的消息关系（中间表），tb_users表存储用户信息。\n创建表的代码\u0026hellip;不提供了，我懒，qwq。就展示一下表结构吧。\n简述一下表结构：\ntb_messages表：消息表，存储消息。其中，userToUserId是外键，指向tb_user_to_user表的user_to_user_id。 tb_user_to_user表：用户关系表，存储用户与用户之间的关系。其中,userId_1和userId_2是外键，指向tb_users表的user_id。两者顺序可以是随机的。因为在及时通信软件中，发消息的人和收消息的人是没有先后顺序的。 tb_users表：用户表，存储用户信息。其中，createdAt是用户创建时间戳。 注意：所有表的外键属性都要设置为CASCADE！！！不然进行修改会造成外键冲突！！！\n5.1.1 创建实体类 创建三个实体类：Messages、UserToUser、Users。放在model包下。\nMessages实体类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package cn.programcx.springbootinit.model; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.persistence.*; import java.sql.Timestamp; @Entity @Table(name = \u0026#34;tb_messages\u0026#34;) @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Messages { @Id @Column(name = \u0026#34;message_id\u0026#34;) @GeneratedValue(strategy = GenerationType.IDENTITY) //自增 private Long id; @Column(name = \u0026#34;created_at\u0026#34;,nullable = false,updatable = false) private Timestamp time; @Column(name = \u0026#34;content\u0026#34;,columnDefinition = \u0026#34;TEXT\u0026#34;,nullable = false) //声明为TEXT类型 private String content; @Enumerated(EnumType.STRING) //声明为枚举类型 @Column(name = \u0026#34;state\u0026#34;,nullable = false) private State state = State.sent; @ManyToOne @JoinColumn(name = \u0026#34;user_to_user_id\u0026#34;, nullable = false) private UserToUser userToUser; @PrePersist //保存前执行 protected void onCreate() { time = new Timestamp(System.currentTimeMillis()); //设置时间戳 } public enum State { sent,delivered,read } @Override public String toString() { return \u0026#34;Messages{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, time=\u0026#34; + time + \u0026#34;, content=\u0026#39;\u0026#34; + content + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, state=\u0026#34; + state + \u0026#34;, sender=\u0026#34; + sender + \u0026#34;, userToUser=\u0026#34; + userToUser + \u0026#39;}\u0026#39;; } } UserToUser实体类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package cn.programcx.springbootinit.model; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.persistence.*; @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Table(name = \u0026#34;tb_user_to_user\u0026#34;) public class UserToUser { @Id @Column(name = \u0026#34;user_to_user_id\u0026#34;) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = \u0026#34;user_id_1\u0026#34;, nullable = false) private Users user1; @ManyToOne @JoinColumn(name = \u0026#34;user_id_2\u0026#34;, nullable = false) private Users user2; } Users实体类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package cn.programcx.springbootinit.model; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.persistence.*; import java.sql.Timestamp; import java.util.Set; @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Table(name = \u0026#34;tb_users\u0026#34;) public class Users { @Id @Column(name = \u0026#34;user_id\u0026#34;,nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = \u0026#34;created_at\u0026#34;,updatable = false,nullable = false) private Timestamp time; @Column(name = \u0026#34;user_name\u0026#34;,nullable = false) private String userName; @Column(name = \u0026#34;email\u0026#34;,nullable = false) private String email; @Column(name = \u0026#34;password_hash\u0026#34;,nullable = false) private String passwordHash; @PrePersist void setTime(){ this.time = new Timestamp(System.currentTimeMillis()); } @Override public String toString() { return \u0026#34;Users{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, time=\u0026#34; + time + \u0026#34;, userName=\u0026#39;\u0026#34; + userName + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, email=\u0026#39;\u0026#34; + email + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, passwordHash=\u0026#39;\u0026#34; + passwordHash + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 5.1.2 创建Repository 创建三个Repository接口：MessagesDao、UserToUserDao、UsersDao。放在dao包下。\nMessagesDao接口\n1 2 3 4 5 6 7 package cn.programcx.springbootinit.dao; import cn.programcx.springbootinit.model.Messages; import org.springframework.data.jpa.repository.JpaRepository; public interface MessagesDao extends JpaRepository\u0026lt;Messages, Long\u0026gt; { } UserToUserDao接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package cn.programcx.springbootinit.dao; import cn.programcx.springbootinit.model.UserToUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; public interface UserToUserDao extends JpaRepository\u0026lt;UserToUser, Long\u0026gt; { //自定义SQL语句，用于插入数据，如果已经存在则忽略 @Modifying @Transactional @Query(value = \u0026#34;INSERT IGNORE INTO tb_user_to_user (user_id_1, user_id_2) VALUES (?1, ?2)\u0026#34;, nativeQuery = true) void insertIgnore(Long userId1, Long userId2); } UsersDao接口\n1 2 3 4 5 6 7 package cn.programcx.springbootinit.dao; import cn.programcx.springbootinit.model.Users; import org.springframework.data.jpa.repository.JpaRepository; public interface UsersDao extends JpaRepository\u0026lt;Users, Long\u0026gt; { } 5.1.3 测试 5.1.3.1 设置自增基数 执行以下SQL语句，设置自增基数。\n1 2 3 ALTER TABLE tb_messages AUTO_INCREMENT = 1; ALTER TABLE tb_user_to_user AUTO_INCREMENT = 1; ALTER TABLE tb_users AUTO_INCREMENT = 1; 5.1.3.2 准备数据 在MySQL中执行以下SQL语句，准备数据。\n1 2 INSERT INTO tb_users (user_id, user_name, email, password_hash) VALUES (1, \u0026#39;ProgramCX\u0026#39;, \u0026#39;2860245799@qq.com\u0026#39;,\u0026#39;dfa423sfasf342arf\u0026#39;); INSERT INTO tb_users (user_id, user_name, email, password_hash) VALUES (2, \u0026#39;ChengXu\u0026#39;, \u0026#39;admin@programcx.cn\u0026#39;,\u0026#39;da3ssafq12323farf\u0026#39;); 5.1.3.3 编写测试类 创建一个测试类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 package cn.programcx.springbootinit; import cn.programcx.springbootinit.dao.MessagesDao; import cn.programcx.springbootinit.dao.UserToUserDao; import cn.programcx.springbootinit.dao.UsersDao; import cn.programcx.springbootinit.model.Messages; import cn.programcx.springbootinit.model.UserToUser; import cn.programcx.springbootinit.model.Users; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.annotation.Order; import org.springframework.data.domain.Example; import java.util.List; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class SpringBootInitApplicationTests { @Autowired private UsersDao usersDao; @Autowired private UserToUserDao userToUserDao; @Autowired private MessagesDao messagesDao; @Test @Order(1) void createAccount1(){ Users users = new Users(); users.setUserName(\u0026#34;Program\u0026#34;); users.setEmail(\u0026#34;noreply@programcx.cn\u0026#34;); users.setPasswordHash(\u0026#34;124aefr8rgtg\u0026#34;); users.setId(1L); usersDao.save(users); } @Test @Order(2) void createAccount2(){ Users users = new Users(); users.setUserName(\u0026#34;ChengXu\u0026#34;); users.setEmail(\u0026#34;admin@programcx.cn\u0026#34;); users.setPasswordHash(\u0026#34;12aw3ae3sf4tg\u0026#34;); users.setId(2L); usersDao.save(users); } @Test @Order(3) void findAccountById() { try { Users users = usersDao.findById(1L).get(); System.out.println(users); } catch (Exception e) { System.out.println(\u0026#34;User not found.\u0026#34;); } } @Test @Order(4) void findAccountByName(){ Users example = new Users(); example.setUserName(\u0026#34;Program\u0026#34;); List\u0026lt;Users\u0026gt; usersList = usersDao.findAll(Example.of(example)); for (Users users : usersList) { System.out.println(users); } } @Test @Order(5) void addMessages(){ Users users = usersDao.findById(1L).get(); Users users2 = usersDao.findById(2L).get(); userToUserDao.insertIgnore(users.getId(),users2.getId()); UserToUser example = new UserToUser(); example.setUser1(users); example.setUser2(users2); UserToUser userToUser = userToUserDao.findOne(Example.of(example)).get(); Messages messages = new Messages(); messages.setUserToUser(userToUser); messages.setContent(\u0026#34;Hello, World!\u0026#34;); messages.setState(Messages.State.sent); messages.setSender(users); messagesDao.save(messages); } @Test @Order(6) void findMessages(){ Users users = usersDao.findById(1L).get(); Users users2 = usersDao.findById(2L).get(); UserToUser example = new UserToUser(); example.setUser1(users); example.setUser2(users2); UserToUser userToUser = userToUserDao.findOne(Example.of(example)).get(); Messages example2 = new Messages(); example2.setUserToUser(userToUser); List\u0026lt;Messages\u0026gt; messagesList = messagesDao.findAll(Example.of(example2)); for (Messages messages : messagesList) { System.out.println(messages); } } @Test @Order(7) void deleteMessages(){ Users users = usersDao.findById(1L).get(); Users users2 = usersDao.findById(2L).get(); UserToUser example = new UserToUser(); example.setUser1(users); example.setUser2(users2); UserToUser userToUser = userToUserDao.findOne(Example.of(example)).get(); Messages example2 = new Messages(); example2.setUserToUser(userToUser); List\u0026lt;Messages\u0026gt; messagesList = messagesDao.findAll(Example.of(example2)); messagesDao.deleteAll(messagesList); } @Test @Order(8) void updateAccount() { Users users = usersDao.findById(1L).get(); users.setEmail(\u0026#34;2860245799@qq.com\u0026#34;); users.setId(1L); usersDao.save(users); } @Test @Order(9) void deleteAccount() { usersDao.deleteById(1L); } } 5.1.4 测试结果 依次执行测试方法，查看结果。\n6.使用@Query自定义查询 我们可以使用@Query注解自定义查询，这样可以更灵活地查询数据。@Query可以在分页查询时提高查询效率。\n@Query注解的属性：\nvalue：查询语句 nativeQuery：是否使用原生SQL countQuery：在分页查询时，指定查询总数 name: 使用分页查询时，指定查询名称 hints：查询提示 6.1 创建Repository 创建一个MessagesDao接口，继承JpaRepository接口，添加一个自定义查询方法。\n1 2 3 4 5 6 7 8 9 10 11 12 package cn.programcx.springbootinit.dao; import cn.programcx.springbootinit.model.Messages; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; public interface MessagesDao extends JpaRepository\u0026lt;Messages, Long\u0026gt; { @Query(value = \u0026#34;SELECT * FROM tb_messages WHERE content = ?1 and user_to_user_id = ?2 ORDER BY created_at DESC LIMIT 1\u0026#34;, nativeQuery = true) Messages findLastMessageByContent(String content, Long userToUserId); } 6.2 测试 创建一个测试类，测试自定义查询方法。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package cn.programcx.springbootinit; import cn.programcx.springbootinit.dao.MessagesDao; import cn.programcx.springbootinit.model.Messages; import cn.programcx.springbootinit.model.UserToUser; import cn.programcx.springbootinit.model.Users; import cn.programcx.springbootinit.dao.UserToUserDao; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class SpringBootInitApplicationTests { @Autowired private MessagesDao messagesDao; @Test void findLastMessageByQueryAnnotation(){ Users users = usersDao.findById(1L).get(); Users users2 = usersDao.findById(2L).get(); UserToUser example = new UserToUser(); example.setUser1(users); example.setUser2(users2); UserToUser userToUser = userToUserDao.findOne(Example.of(example)).orElse(null); Messages messagesList = null; if (userToUser != null) { messagesList = messagesDao.findLastMessageByContent(\u0026#34;Hello, World!\u0026#34;, userToUser.getId()); } UserToUser example2 = new UserToUser(); example2.setUser1(users2); example2.setUser2(users); UserToUser userToUser2 = userToUserDao.findOne(Example.of(example2)).orElse(null); if (userToUser2 != null) { messagesList = messagesDao.findLastMessageByContent(\u0026#34;Hello, World!\u0026#34;, userToUser2.getId()); } if (messagesList != null) { System.out.println(messagesList); } } } 6.3 测试结果 执行测试方法，查看结果。\n该文章为本人学习笔记，如有错误，欢迍指正。谢谢！\n本人个人博客上该文章的链接：https://www.programcx.cn/p/spingboot-jpa/\n","date":"2025-03-02T00:00:00Z","image":"https://programcx.github.io/p/springboot-jpa/springboot_hu17231296552232599347.png","permalink":"https://programcx.github.io/p/springboot-jpa/","title":"Spring Data JPA ( Hibernate ) 操作数据"},{"content":"获取 Kimi 网页端对话 api，并实现后端推流 声明：本文章所授内容仅供学习。如果需要使用 Kimi Api 请在 https://platform.moonshot.cn 中申请官方正规渠道的 API。造成的一切后果，本人不承担任何责任！！！\n1. 前言 本文主要分成两个部分，第一部分是获取 Kimi 网页端对话 api，第二部分是实现后端推流。第二部分是基于 SpringBoot 实现的，如果你对 SpringBoot 不熟悉，可以先学习一下 SpringBoot 的基础知识。在第二部分中，我们会用到CompletableFuture异步编程、SseEmitter后端推流、实现回调等技术。\n2. 获取 Kimi 网页端对话 api 打开 https://kimi.moonshot.cn/ 网站，登录你自己的账号。\n按下F12打开开发者工具，点击Network选项卡。\n在网页上输入你想要的对话内容，比如”你好“，点击发送按钮。\n在名称一栏里面找到stream，如图： 右键，点击复制，复制为fetch，如图： 打开记事本，存储一下这个fetch请求，如图：\n3. 实现后端推流 使用 IDEA 提供的 Spring Boot 生成器创建一个 SpringBoot 项目，添加依赖：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!--fastjson依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.fastjson2\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;fastjson2\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.56\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--okhttp依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.squareup.okhttp3\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;okhttp\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 3.1 使用 OKhttp 向 Kimi 后端发送 api 请求 3.1.1 创建一个KimiService类 在cn.programcx.springbootinit.services包下创建一个KimiService类，并创建一个返回类型为CompletableFuture\u0026lt;String\u0026gt;的chat方法，大致框架如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package cn.programcx.springbootinit.services; import okhttp3.*; import org.apache.shiro.SecurityUtils; import org.apache.shiro.mgt.SecurityManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.concurrent.CompletableFuture; @Service public class KimiService { private static final OkHttpClient client = new OkHttpClient(); @Async public CompletableFuture\u0026lt;String\u0026gt; chat(String requestContent,Callback callback) throws IOException { { } } } 3.1.2 配置请求头 现在我们先把发送请求的请求头写好。先打开原来我们临时保存的fetch请求的代码，找到headers和referer，如图： 拼接响应体: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 final String url = \u0026#34;https://kimi.moonshot.cn/api/chat/cv031bl96bkb13hgn0p0/completion/stream\u0026#34;; final String json = \u0026#34;{\u0026#34; + \u0026#34;\\\u0026#34;kimiplus_id\\\u0026#34;: \\\u0026#34;kimi\\\u0026#34;,\u0026#34; + \u0026#34;\\\u0026#34;extend\\\u0026#34;: {\\\u0026#34;sidebar\\\u0026#34;: true},\u0026#34; + \u0026#34;\\\u0026#34;model\\\u0026#34;: \\\u0026#34;k1\\\u0026#34;,\u0026#34; + \u0026#34;\\\u0026#34;use_research\\\u0026#34;: false,\u0026#34; + \u0026#34;\\\u0026#34;use_search\\\u0026#34;: true,\u0026#34; + \u0026#34;\\\u0026#34;messages\\\u0026#34;: [{\\\u0026#34;role\\\u0026#34;: \\\u0026#34;user\\\u0026#34;, \\\u0026#34;content\\\u0026#34;: \\\u0026#34; \u0026#34;+requestContent+\u0026#34;\\\u0026#34;}],\u0026#34; + \u0026#34;\\\u0026#34;refs\\\u0026#34;: [],\u0026#34; + \u0026#34;\\\u0026#34;history\\\u0026#34;: [],\u0026#34; + \u0026#34;\\\u0026#34;scene_labels\\\u0026#34;: []\u0026#34; + \u0026#34;}\u0026#34;; RequestBody body = RequestBody.create( MediaType.parse(\u0026#34;application/json\u0026#34;), json); 根据临时保存的fetch请求的代码的headers和referer，我们可以写出请求头的代码，如下： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Request request = new Request.Builder() .url(url) .addHeader(\u0026#34;accept\u0026#34;, \u0026#34;application/json, text/plain, */*\u0026#34;) .addHeader(\u0026#34;accept-language\u0026#34;, \u0026#34;zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\u0026#34;) .addHeader(\u0026#34;authorization\u0026#34;, \u0026#34;Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyLWNlbnRlciIsImV4cCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxW5fYWNjZXNzIl0sInNzaWQiOiIxNzMxMTIxNjg0MzEwMjQzNTcxIiwiZGV2aWNlX2lkIjoiNzQ1ODUwMDU0MjQ4NzIwMzMyOSJ9.if8b0gLsEWREisI6B8Bo-bHfbgYnUt5Rn_PZodayPXMoUUXq2V9rOMlhhoPHXYGnxbYKxOKHiv1VyzOrp1kB6w\u0026#34;) //这里的authorization内容在请求头的authorization字段中，这里我用xxxx代替 .addHeader(\u0026#34;content-type\u0026#34;, \u0026#34;application/json\u0026#34;) .addHeader(\u0026#34;origin\u0026#34;, \u0026#34;https://kimi.moonshot.cn\u0026#34;) .addHeader(\u0026#34;referer\u0026#34;, \u0026#34;https://kimi.moonshot.cn/chat/cv031bl96bkb13hgn0p0\u0026#34;) .addHeader(\u0026#34;sec-ch-ua\u0026#34;, \u0026#34;\\\u0026#34;Not(A:Brand)\\\u0026#34;;v=\\\u0026#34;99\\\u0026#34;, \\\u0026#34;Microsoft Edge\\\u0026#34;;v=\\\u0026#34;133\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;133\\\u0026#34;\u0026#34;) .addHeader(\u0026#34;sec-ch-ua-mobile\u0026#34;, \u0026#34;?0\u0026#34;) .addHeader(\u0026#34;sec-ch-ua-platform\u0026#34;, \u0026#34;\\\u0026#34;Windows\\\u0026#34;\u0026#34;) .addHeader(\u0026#34;sec-fetch-dest\u0026#34;, \u0026#34;empty\u0026#34;) .addHeader(\u0026#34;sec-fetch-mode\u0026#34;, \u0026#34;cors\u0026#34;) .addHeader(\u0026#34;sec-fetch-site\u0026#34;, \u0026#34;same-origin\u0026#34;) .addHeader(\u0026#34;user-agent\u0026#34;, \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0\u0026#34;) .addHeader(\u0026#34;x-language\u0026#34;, \u0026#34;zh-CN\u0026#34;) .addHeader(\u0026#34;x-msh-device-id\u0026#34;, \u0026#34;7458500542487203329\u0026#34;) .addHeader(\u0026#34;x-msh-platform\u0026#34;, \u0026#34;web\u0026#34;) .addHeader(\u0026#34;x-msh-session-id\u0026#34;, \u0026#34;1730464176363583706\u0026#34;) .addHeader(\u0026#34;x-traffic-id\u0026#34;, \u0026#34;csr1c57d0p80ihhe0h1g\u0026#34;) .post(body) .build(); 3.1.3 发送请求 1 2 3 4 5 6 7 try(Response response = client.newCall(request).execute()){ if (!response.isSuccessful()) { throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); } //接下来我们要对返回的数据进行处理 } 3.1.4 处理返回的数据大致思路 使用这个 API 发送请求之后，得到的响应体是“流式”的，而并非一次性返回所有数据。我们可以在浏览器开发者工具里面观察： 其实，这个叫做“EventStream”。\nEvent Stream 技术是一种允许服务器向客户端推送实时数据的技术。与传统的客户端定期轮询服务器以获取数据的方式不同，Event Stream 技术允许服务器在有新数据可用时立即向客户端推送数据。这种技术通常基于 HTTP/2 协议，通过持久连接实现数据的实时推送。\n我们现在要做的就是在后端接收这个“EventStream”，然后再推送给前端。这个技术在 SpringBoot 中有一个很好的实现，叫做SseEmitter。\n我们现在接着要做的是处理 Kimi 后端返回的EventStream流：在 OKHttp 发送请求之后，我们获取响应体流，然后解析每一行数据，通过回调函数返回给前端。\n3.1.5 创建回调Callback接口 在util包下新建一个文件Callback.java，定义Callback接口：\n1 2 3 4 5 6 7 package cn.programcx.springbootinit.utils; public interface Callback { void onMessage(String message); //处理返回的数据 void onCompleted(); //处理完成 void onError(Exception e); //处理异常 } 3.1.6 处理返回的数据 先将 Callback 接口引入到KimiService类中，然后在chat方法中处理返回的数据：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // 发起请求并处理响应 try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); } // 获取响应体流 BufferedSource source = response.body().source(); // 读取响应流并处理 while (true) { String line = source.readUtf8Line(); if (line != null \u0026amp;\u0026amp; !line.trim().isEmpty()) { // 这里可以根据需要解析流数据 if (line.startsWith(\u0026#34;data:\u0026#34;)) { String data = line.substring(5).trim(); JSONObject jsonObject = JSONObject.parseObject(data); //将返回的数据转换为json对象 //处理返回的数据 if(jsonObject.getString(\u0026#34;event\u0026#34;).equals(\u0026#34;k1\u0026#34;)||jsonObject.getString(\u0026#34;event\u0026#34;).equals(\u0026#34;cmpl\u0026#34;)){ //判断是否是对话内容 String content = jsonObject.getString(\u0026#34;text\u0026#34;); if (content != null) { callback.onMessage(content); System.out.print(content); } }else if (jsonObject.getString(\u0026#34;event\u0026#34;).equals(\u0026#34;all_done\u0026#34;)) { //判断是否对话结束 callback.onCompleted(); return CompletableFuture.completedFuture(null); } } } } }catch (Exception e){ callback.onError(e); return CompletableFuture.completedFuture(null); } 3.2 使用SseEmitter推送数据 在controller包下创建一个KimiController类，创建一个kimi方法，以下是KimiController类的代码大致框架：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package cn.programcx.springbootinit.controller; import cn.programcx.springbootinit.services.KimiService; import cn.programcx.springbootinit.utils.Callback; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; @Controller @RequestMapping(\u0026#34;chat\u0026#34;) public class ChatController { @Autowired private KimiService kimiService; @PostMapping(\u0026#34;/kimi\u0026#34;) @ResponseBody private SseEmitter kimi(String message) throws IOException { SseEmitter sseEmitter = new SseEmitter(); //此处调用KimiService的chat方法，并处理回调函数，将数据通过sseEmitter推流的方式（也就是上文的EventSteam）推送给前端 return sseEmitter; } } 3.2.1 处理回调函数 SseEmitter 使用方法 接下来讲解一下SseEmitter的使用方法。SseEmitter是 SpringBoot 提供的一个用于推送数据的类，它可以将数据推送给前端，实现实时更新\n我们需要用到SseEmitter类的SseEmitter(Long timeout)、send(Object object)、completeWithError(Throwable)方法。可以通过查询官方文档了解更多关于SseEmitter的使用方法。\nSseEmitter(Long timeout) 创建具有自定义超时值的 SseEmitter。\n参数: timeout - 超时值（以毫秒为单位）。 send(Object object) 发送格式为单个 SSE \u0026ldquo;data\u0026rdquo; 行的对象。\n参数: object - 要发送的对象。可以是任何对象，但它将被转换为字符串。 SseEmitter(Long timeout) 创建具有自定义超时值的 SseEmitter。\n参数: timeout - 超时值（以毫秒为单位）。 completeWithError(Throwable ex) 使用给定的异常完成此 SseEmitter。\n参数: ex - 异常。 处理回调函数 在KimiController类中，我们需要处理回调函数，将数据通过SseEmitter推送给前端。我们可以在KimiController类中创建一个Callback接口的实现类，然后在KimiService类中调用这个实现类的方法，将数据推送给前端。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @PostMapping(\u0026#34;/kimi\u0026#34;) @ResponseBody private SseEmitter kimi(String message) throws IOException { System.out.println(message); SseEmitter sseEmitter = new SseEmitter(0L); //取消超时时间，防止中断传输 kimiService.chat(message, new Callback() { @Override public void onMessage(String message) { try { sseEmitter.send(message); //推送数据 } catch (IOException e) { e.printStackTrace(); } } @Override public void onCompleted() { sseEmitter.complete(); } @Override public void onError(Exception e) { sseEmitter.completeWithError(e); } }); return sseEmitter; } 3.3 测试 现在我们可以测试一下我们的代码是否能够正常运行。我们可以使用 Hoppscotch 或者 curl 来测试我们的代码。\n运行Gif：\nHoppscotch 截图： 4. 完整代码 cn.programcx.springbootinit.controller.ChatController.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package cn.programcx.springbootinit.controller; import cn.programcx.springbootinit.services.KimiService; import cn.programcx.springbootinit.utils.Callback; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; @Controller @RequestMapping(\u0026#34;chat\u0026#34;) public class ChatController { @Autowired private KimiService kimiService; @PostMapping(\u0026#34;/kimi\u0026#34;) @ResponseBody private SseEmitter kimi(String message) throws IOException { System.out.println(message); SseEmitter sseEmitter = new SseEmitter(0L); kimiService.chat(message, new Callback() { @Override public void onMessage(String message) { try { sseEmitter.send(message); } catch (IOException e) { e.printStackTrace(); } } @Override public void onCompleted() { sseEmitter.complete(); } @Override public void onError(Exception e) { sseEmitter.completeWithError(e); } }); return sseEmitter; } } cn.programcx.springbootinit.services.KimiService.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 package cn.programcx.springbootinit.services; import com.alibaba.fastjson2.JSONObject; import okhttp3.*; import okio.BufferedSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.concurrent.CompletableFuture; import cn.programcx.springbootinit.utils.Callback; @Service public class KimiService { private static final OkHttpClient client = new OkHttpClient(); @Async public CompletableFuture\u0026lt;String\u0026gt; chat(String requestContent,Callback callback) throws IOException { { final String url = \u0026#34;https://kimi.moonshot.cn/api/chat/cv031bl96bkb13hgn0p0/completion/stream\u0026#34;; final String json = \u0026#34;{\u0026#34; + \u0026#34;\\\u0026#34;kimiplus_id\\\u0026#34;: \\\u0026#34;kimi\\\u0026#34;,\u0026#34; + \u0026#34;\\\u0026#34;extend\\\u0026#34;: {\\\u0026#34;sidebar\\\u0026#34;: true},\u0026#34; + \u0026#34;\\\u0026#34;model\\\u0026#34;: \\\u0026#34;k1\\\u0026#34;,\u0026#34; + \u0026#34;\\\u0026#34;use_research\\\u0026#34;: false,\u0026#34; + \u0026#34;\\\u0026#34;use_search\\\u0026#34;: true,\u0026#34; + \u0026#34;\\\u0026#34;messages\\\u0026#34;: [{\\\u0026#34;role\\\u0026#34;: \\\u0026#34;user\\\u0026#34;, \\\u0026#34;content\\\u0026#34;: \\\u0026#34; \u0026#34;+requestContent+\u0026#34;\\\u0026#34;}],\u0026#34; + \u0026#34;\\\u0026#34;refs\\\u0026#34;: [],\u0026#34; + \u0026#34;\\\u0026#34;history\\\u0026#34;: [],\u0026#34; + \u0026#34;\\\u0026#34;scene_labels\\\u0026#34;: []\u0026#34; + \u0026#34;}\u0026#34;; RequestBody body = RequestBody.create( MediaType.parse(\u0026#34;application/json\u0026#34;), json); Request request = new Request.Builder() .url(url) .addHeader(\u0026#34;accept\u0026#34;, \u0026#34;application/json, text/plain, */*\u0026#34;) .addHeader(\u0026#34;accept-language\u0026#34;, \u0026#34;zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\u0026#34;) .addHeader(\u0026#34;authorization\u0026#34;, \u0026#34;Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyLWNlbnRlciIsImV4cCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxW5fYWNjZXNzIl0sInNzaWQiOiIxNzMxMTIxNjg0MzEwMjQzNTcxIiwiZGV2aWNlX2lkIjoiNzQ1ODUwMDU0MjQ4NzIwMzMyOSJ9.if8b0gLsEWREisI6B8Bo-bHfbgYnUt5Rn_PZodayPXMoUUXq2V9rOMlhhoPHXYGnxbYKxOKHiv1VyzOrp1kB6w\u0026#34;) //这里是你的token，保密起见，这里部分字符串用x代替 .addHeader(\u0026#34;content-type\u0026#34;, \u0026#34;application/json\u0026#34;) .addHeader(\u0026#34;origin\u0026#34;, \u0026#34;https://kimi.moonshot.cn\u0026#34;) .addHeader(\u0026#34;referer\u0026#34;, \u0026#34;https://kimi.moonshot.cn/chat/cv031bl96bkb13hgn0p0\u0026#34;) .addHeader(\u0026#34;sec-ch-ua\u0026#34;, \u0026#34;\\\u0026#34;Not(A:Brand)\\\u0026#34;;v=\\\u0026#34;99\\\u0026#34;, \\\u0026#34;Microsoft Edge\\\u0026#34;;v=\\\u0026#34;133\\\u0026#34;, \\\u0026#34;Chromium\\\u0026#34;;v=\\\u0026#34;133\\\u0026#34;\u0026#34;) .addHeader(\u0026#34;sec-ch-ua-mobile\u0026#34;, \u0026#34;?0\u0026#34;) .addHeader(\u0026#34;sec-ch-ua-platform\u0026#34;, \u0026#34;\\\u0026#34;Windows\\\u0026#34;\u0026#34;) .addHeader(\u0026#34;sec-fetch-dest\u0026#34;, \u0026#34;empty\u0026#34;) .addHeader(\u0026#34;sec-fetch-mode\u0026#34;, \u0026#34;cors\u0026#34;) .addHeader(\u0026#34;sec-fetch-site\u0026#34;, \u0026#34;same-origin\u0026#34;) .addHeader(\u0026#34;user-agent\u0026#34;, \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0\u0026#34;) .addHeader(\u0026#34;x-language\u0026#34;, \u0026#34;zh-CN\u0026#34;) .addHeader(\u0026#34;x-msh-device-id\u0026#34;, \u0026#34;7458500542487203329\u0026#34;) .addHeader(\u0026#34;x-msh-platform\u0026#34;, \u0026#34;web\u0026#34;) .addHeader(\u0026#34;x-msh-session-id\u0026#34;, \u0026#34;1730464176363583706\u0026#34;) .addHeader(\u0026#34;x-traffic-id\u0026#34;, \u0026#34;csr1c57d0p80ihhe0h1g\u0026#34;) .post(body) .build(); // 发起请求并处理响应 try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); } // 获取响应体流 BufferedSource source = response.body().source(); // 读取响应流并处理 while (true) { String line = source.readUtf8Line(); if (line != null \u0026amp;\u0026amp; !line.trim().isEmpty()) { // 这里可以根据需要解析流数据 if (line.startsWith(\u0026#34;data:\u0026#34;)) { String data = line.substring(5).trim(); JSONObject jsonObject = JSONObject.parseObject(data); if(jsonObject.getString(\u0026#34;event\u0026#34;).equals(\u0026#34;k1\u0026#34;)||jsonObject.getString(\u0026#34;event\u0026#34;).equals(\u0026#34;cmpl\u0026#34;)){ String content = jsonObject.getString(\u0026#34;text\u0026#34;); if (content != null) { callback.onMessage(content); System.out.print(content); } }else if (jsonObject.getString(\u0026#34;event\u0026#34;).equals(\u0026#34;all_done\u0026#34;)) { callback.onCompleted(); return CompletableFuture.completedFuture(null); } } } } }catch (Exception e){ callback.onError(e); return CompletableFuture.completedFuture(null); } } } } cn.programcx.springbootinit.utils.Callback.java\n1 2 3 4 5 6 7 package cn.programcx.springbootinit.utils; public interface Callback { void onMessage(String message); void onCompleted(); void onError(Exception e); } 5. 总结 本文主要介绍了如何获取 Kimi 网页端对话 api，并实现后端推流。在实现后端推流的过程中，我们使用了CompletableFuture异步编程、SseEmitter后端推流、实现回调等技术。希望本文对你有所帮助。\n","date":"2025-02-28T00:00:00Z","image":"https://programcx.github.io/p/springboot-kimi/springboot_hu15901259467893397822.png","permalink":"https://programcx.github.io/p/springboot-kimi/","title":"获取Kimi网页端对话api，并实现后端推流"},{"content":"在Spring Boot中实现邮件发送 本文主要通过部分简单的发送验证码的例子来介绍在Spring Boot中实现邮件发送。\n1. 添加依赖 在pom.xml文件中添加JavaMailSender的依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-mail\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 2. 在application.yaml中配置邮件发送信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 spring: mail: host: smtp.qq.com port: 465 username: xxx@qq.com password: xxxxxxxxx # 授权码 # 下方的属性必须带着，不然会报错 properties: mail: smtp: auth: true socketFactory: class: javax.net.ssl.SSLSocketFactory starttls: enable: true 3.创建Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package cn.programcx.springbootinit.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import cn.programcx.springbootinit.model.Tester; import cn.programcx.springbootinit.services.*; @Controller public class TestController { @Autowired private VerificationService service; //当用户点击发送验证码时，调用此方法 @PostMapping(\u0026#34;verifycallback\u0026#34;) @ResponseBody public void sendVerifyCode(@RequestParam(\u0026#34;email\u0026#34;) String email){ service.sendVerificationCode(email); } } ## 4.创建Service ```java package cn.programcx.springbootinit.services; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; import java.util.Random; @Service public class VerificationService { @Value(\u0026#34;${spring.mail.username}\u0026#34;) private String userName; @Value(\u0026#34;${spring.mail.host}\u0026#34;) private String host; @Value(\u0026#34;${spring.mail.password}\u0026#34;) private String password; @Value(\u0026#34;${spring.mail.port}\u0026#34;) private String port; @Autowired private JavaMailSender mailSender; public void sendVerificationCode(String toAddress){ Random random =new Random(System.currentTimeMillis()); int code = random.nextInt((999999 - 100000) + 1); SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(userName); message.setTo(toAddress); message.setSubject(\u0026#34;验证码登录\u0026#34;); message.setText(\u0026#34;欢迎注册程旭工作室官网，验证码: \u0026#34;+Integer.toString(code)+\u0026#34; 10分钟内有效.请勿泄露。注册后您将成为程旭工作室终身SSSSSVIP。\u0026#34;); Logger logger= LoggerFactory.getLogger(getClass()); try { mailSender.send(message); logger.info(\u0026#34;已发送验证码\u0026#34;+Integer.toString(code)+\u0026#34;邮件地址\u0026#34;+toAddress); } catch (Exception e){ logger.error(e.toString()); logger.error(\u0026#34;未成功验证码\u0026#34;+Integer.toString(code)+\u0026#34;邮件地址\u0026#34;+toAddress,e.getMessage()); } } } ","date":"2025-02-24T00:00:00Z","image":"https://programcx.github.io/p/springboot-javamailsender/springboot_hu15901259467893397822.png","permalink":"https://programcx.github.io/p/springboot-javamailsender/","title":"在Spring Boot中使用 JavaMailSender"},{"content":"在Spring Boot中实现简单定时任务 本文主要介绍在Spring Boot中使用Quartz实现定时任务的方法。\n1. 添加依赖 在pom.xml文件中添加Quartz的依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-quartz\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 2. 编写定时任务代码 创建一个继承QuartzJobBean的类，实现executeInternal方法，编写定时任务的逻辑。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package cn.programcx.springbootinit.jobs; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.quartz.*; import java.util.Date; public class MyJob extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { Logger logger = LoggerFactory.getLogger(getClass()); logger.info(new Date().toString()); // 打印当前时间 } } 2.在容器中注册JobDetail和Trigger，并将其与MyJob关联。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package cn.programcx.springbootinit.config; import cn.programcx.springbootinit.jobs.MyJob; import org.quartz.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CustomConfiguration { @Bean public JobDetail printTimeJobDetail() { // 指定具体的Job类 return JobBuilder.newJob(MyJob.class).withIdentity(\u0026#34;MyJobDetail\u0026#34;).storeDurably().build(); } @Bean public Trigger printTimeJobtrigger(){ // 指定具体的触发器 CronScheduleBuilder builder = CronScheduleBuilder.cronSchedule(\u0026#34;0/1 * * * * ?\u0026#34;); // 每秒执行一次 // 指定触发器关联的JobDetail return (Trigger) TriggerBuilder.newTrigger().forJob(\u0026#34;MyJobDetail\u0026#34;).withIdentity(\u0026#34;quartzTaskService\u0026#34;).withSchedule(builder).build(); } } 3. 启动定时任务 在Spring Boot的启动类中添加@EnableScheduling注解，启用定时任务。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package cn.programcx.springbootinit; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling public class SpringBootInitApplication { public static void main(String[] args) { // 启动Spring Boot应用 SpringApplication.run(SpringBootInitApplication.class, args); } } 4. 测试 启动Spring Boot应用，可以看到控制台每秒输出一次当前时间。\n","date":"2025-02-22T00:00:00Z","image":"https://programcx.github.io/p/springboot-quartz-simple-job/springboot_hu15901259467893397822.png","permalink":"https://programcx.github.io/p/springboot-quartz-simple-job/","title":"在Spring Boot中使用定时任务"},{"content":"题目： 给定一个数组 nums，编写一个函数将所有 0 移动到数组的末尾，同时保持非零元素的相对顺序。\n请注意 ，必须在不复制数组的情况下原地对数组进行操作。\n示例 1:\n输入: nums = [0,1,0,3,12]\n输出: [1,3,12,0,0]\n示例 2:\n输入: nums = [0]\n输出: [0]\n提示:\n$1 \u0026lt;= nums.length \u0026lt;= 104$ $-231 \u0026lt;= nums[i] \u0026lt;= 231 - 1$ 解法一：二次遍历 思路： 第一个指针用来遍历数组，另外一个指针位于上次与第一个指针指向的数交换的位置。\n大致原理： 一个指针A指向索引为0的位置，另一个指针B从头开始遍历。每当指针B遇到一个非零数，都要把这个数依次从这个数组从头部往后放。从而保证非零数连续的在这个数组前面。然后其它部分填上0。\n主要过程： 指针B 用来遍历数组。 每当遇到非零元素时，指针A 就把它放到当前的位置，并移动到下一个空位置。 最终，指针A会停在数组中的最后一个非零数的位置，而指针B会遍历整个数组。 数组中剩下的部分自动是零，因为非零元素已经被移到了前面。 动画演示（来源于网络）： 解题代码： 1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution { public void moveZeroes(int[] nums) { int insertPos = 0; for (int i = 0; i \u0026lt; nums.length; i++) { if (nums[i] != 0) { nums[insertPos++] = nums[i]; //把非零元素放到当前的前面 } } for (int i = insertPos; i \u0026lt; nums.length; i++) { nums[i] = 0; //其余部分补上0 } } } 复杂度： 时间复杂度：$O(n)$ 空间复杂度：$O(1)$ 解法二：交换法（官方给出的解法） 大致原理： 左指针指向上一次交换的位置，右指针遍历数组，当遇到不为零的数，就与左指针指向的数交换位置，总体上会把为零的数不断向右赶。 左指针左边均为非零数； 右指针左边直到左指针处均为零。 因此每次交换，都是将左指针的零与右指针的非零数交换，且非零数的相对顺序并未改变。\n解题代码： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public void moveZeroes(int[] nums) { int left = 0; for (int right = 0; right \u0026lt; nums.length; right++) { if (nums[right] != 0) { swap(nums, right, left); left++; } } } public void swap(int[] nums, int left, int right) { int temp = nums[left]; nums[left] = nums[right]; nums[right] = temp; } } 复杂度： 时间复杂度：$O(n)$ 空间复杂度：$O(1)$ 两者的优缺点分析： 第一种“二次遍历“方法直接赋值操作，节省了开销，但是需要二次遍历，可能会对性能产生影响。 第二种”交换法“无需二次遍历，但是交换操作在某些情况下可能会达到性能瓶颈。 ","date":"2025-02-18T00:00:00Z","image":"https://programcx.github.io/p/movezeros/moveZeros_hu3133581709542609208.gif","permalink":"https://programcx.github.io/p/movezeros/","title":"LeetCode 第283题 移动零"},{"content":"一、启用Windows Subsystem for Linux 在Windows 10 21H2或更高版本上，启用Windows Subsystem for Linux。\n打开控制面板。 选择“程序和功能”。 选择“启用或关闭Windows功能”。 勾选“适用于Linux的Windows子系统”。 重新启动计算机。 二、安装Ubuntu 22.04 重新启动后，会弹出一个提示更新的窗口，按Enter键等待更新完成。\n1.打开PowerShell，输入以下命令安装Ubuntu 22.04：\n1 2 wsl --set-default-version 2 wsl --install -d Ubuntu 2.等待安装完成，按照提示设置用户名和密码。\n3.此时，默认安装位置为C盘，可以按照以下步骤安装到其它盘：\n1 2 3 wsl --export Ubuntu F:\\wsl\\ubuntu-backup.tar # 导出Ubuntu备份 wsl --unregister Ubuntu # 注销Ubuntu wsl --import Ubuntu F:\\wsl\\ubuntu F:\\wsl\\ubuntu-backup.tar --version 2 # 导入Ubuntu 4.安装完成后，输入以下命令启动Ubuntu：\n1 wsl -d Ubuntu 5.输入以下命令更新软件包：\n1 2 sudo apt update sudo apt upgrade 6.至此，Ubuntu 22.04安装完成。\n记录一下，以后有需要可以参考。\n","date":"2025-02-02T00:00:00Z","image":"https://programcx.github.io/p/windows-%E5%AE%89%E8%A3%85-ubuntu-22.04-%E8%AF%A6%E7%BB%86%E6%95%99%E7%A8%8B/Ubuntu_hu5340899523754387089.png","permalink":"https://programcx.github.io/p/windows-%E5%AE%89%E8%A3%85-ubuntu-22.04-%E8%AF%A6%E7%BB%86%E6%95%99%E7%A8%8B/","title":"Windows 安装 Ubuntu 22.04 详细教程"},{"content":"1. \u0026lt;if\u0026gt;元素 当\u0026lt;if\u0026gt;中的test 的值为真时就执行加括号里面的语句，否则就不执行。\n示例如下：\n数据库准备 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 切换数据库 use mall; # 创建数据表 create table customer ( id int(32) primary key auto_increment comment \u0026#39;ID\u0026#39;, name varchar(50) not null comment \u0026#39;Customer Name\u0026#39;, address varchar(50) comment \u0026#39;Customer Address\u0026#39;, phone varchar(20) comment \u0026#39;Customer Phone\u0026#39; ); # 插入数据 insert into customer values(1,\u0026#39;ChengXu\u0026#39;,\u0026#39;AUST\u0026#39;,\u0026#39;666666666666\u0026#39;); insert into customer values(2,\u0026#39;Program\u0026#39;,\u0026#39;AUST\u0026#39;,\u0026#39;888888888888\u0026#39;); insert into customer values(3,\u0026#39;ProgramCX\u0026#39;,\u0026#39;AUST\u0026#39;,\u0026#39;99999999999\u0026#39;); POJO 类准备 在 org.exmaple.pojo 下新建一个 Customer 类，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package org.example.pojo; public class Customer { private int id; private String name; private String address; private String phone; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public String toString() { return \u0026#34;Customer{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, address=\u0026#39;\u0026#34; + address + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, phone=\u0026#39;\u0026#34; + phone + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 创建映射文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;org.example.pojo.Customer\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;queryCustomerByNameAndAddress\u0026#34; parameterType=\u0026#34;org.example.pojo.Customer\u0026#34; resultType=\u0026#34;org.example.pojo.Customer\u0026#34;\u0026gt; SELECT * from customer where 1=1 \u0026lt;if test=\u0026#34;name !=null and name !=\u0026#39;\u0026#39;\u0026#34;\u0026gt; and name=#{name} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;phone !=null and phone!=\u0026#39;\u0026#39;\u0026#34;\u0026gt; and phone=#{phone} \u0026lt;/if\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 修改核心配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;!-- 配置文件的根元素 --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 属性：定义配置外在化 --\u0026gt; \u0026lt;properties resource=\u0026#34;db.properties\u0026#34;/\u0026gt; \u0026lt;!-- 环境：配置mybatis的环境 --\u0026gt; \u0026lt;environments default=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;!-- 环境变量：可以配置多个环境变量，比如使用多数据源时，就需要配置多个环境变量 --\u0026gt; \u0026lt;environment id=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;!-- 事务管理器 --\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;!-- 数据源 --\u0026gt; \u0026lt;dataSource type=\u0026#34;POOLED\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;${mysql.driver}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${mysql.url}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${mysql.username}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${mysql.password}\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;mapper/UserMapper.xml\u0026#34;/\u0026gt; \u0026lt;mapper resource=\u0026#34;mapper/CustomMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; \u0026lt;/configuration\u0026gt; 创建测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package Test; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; import java.io.Reader; import org.example.pojo.Customer; public class CustomerTest { @Test public void test() throws IOException { //初始化配置 Reader read = Resources.getResourceAsReader(\u0026#34;mybatis-config.xml\u0026#34;); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(read); SqlSession sqlSession = sqlSessionFactory.openSession(); //创建POJO类的实例 Customer customer = new Customer(); customer.setId(1); customer.setAddress(\u0026#34;AUST\u0026#34;); customer.setPhone(\u0026#34;666666666666\u0026#34;); customer = sqlSession.selectOne(\u0026#34;queryCustomerByNameAndAddress\u0026#34;, customer); System.out.println(customer); } } 运行结果 1 2 3 4 \u0026#34;D:\\Program Files\\Java\\jdk-23\\bin\\java.exe\u0026#34; -ea -Didea.test.cyclic.buffer.size=1048576 \u0026#34;-javaagent:D:\\Program Files\\JetBrains\\IntelliJ IDEA 2024.3.1.1\\lib\\idea_rt.jar=50887:D:\\Program Files\\JetBrains\\IntelliJ IDEA 2024.3.1.1\\bin\u0026#34; -javaagent:C:\\Users\\Progr\\AppData\\Local\\JetBrains\\IntelliJIdea2024.3\\captureAgent\\debugger-agent.jar -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath \u0026#34;D:\\Program Files\\JetBrains\\IntelliJ IDEA 2024.3.1.1\\lib\\idea_rt.jar;D:\\Program Files\\JetBrains\\IntelliJ IDEA 2024.3.1.1\\plugins\\junit\\lib\\junit5-rt.jar;D:\\Program Files\\JetBrains\\IntelliJ IDEA 2024.3.1.1\\plugins\\junit\\lib\\junit-rt.jar;C:\\Users\\Progr\\IdeaProjects\\untitled\\target\\test-classes;C:\\Users\\Progr\\IdeaProjects\\untitled\\target\\classes;D:\\Program Files\\apache-maven-3.9.9\\repository\\org\\mybatis\\mybatis\\3.5.19\\mybatis-3.5.19.jar;D:\\Program Files\\apache-maven-3.9.9\\repository\\com\\mysql\\mysql-connector-j\\8.0.33\\mysql-connector-j-8.0.33.jar;D:\\Program Files\\apache-maven-3.9.9\\repository\\com\\google\\protobuf\\protobuf-java\\3.21.9\\protobuf-java-3.21.9.jar;D:\\Program Files\\apache-maven-3.9.9\\repository\\junit\\junit\\4.13.2\\junit-4.13.2.jar;D:\\Program Files\\apache-maven-3.9.9\\repository\\org\\hamcrest\\hamcrest-core\\1.3\\hamcrest-core-1.3.jar\u0026#34; com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit4 Test.CustomerTest,test Customer{id=1, name=\u0026#39;ChengXu\u0026#39;, address=\u0026#39;AUST\u0026#39;, phone=\u0026#39;666666666666\u0026#39;} 进程已结束，退出代码为 0 代码分析 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;select id=\u0026#34;queryCustomerByNameAndAddress\u0026#34; parameterType=\u0026#34;org.example.pojo.Customer\u0026#34; resultType=\u0026#34;org.example.pojo.Customer\u0026#34;\u0026gt; SELECT * from customer where 1=1 \u0026lt;if test=\u0026#34;name !=null and name !=\u0026#39;\u0026#39;\u0026#34;\u0026gt; and name=#{name} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;phone !=null and phone!=\u0026#39;\u0026#39;\u0026#34;\u0026gt; and phone=#{phone} \u0026lt;/if\u0026gt; \u0026lt;/select\u0026gt; 这段代码接受一个Customer类作为参数主要对where部分进行了条件判断，当 Customer.name存在时进行name筛选，当Customer.phone存在时进行phone筛选。\n2. \u0026lt;choose\u0026gt;、\u0026lt;when\u0026gt;、\u0026lt;otherwise\u0026gt;元素 使用\u0026lt;if\u0026gt;元素时，可以选择多个元素进行执行；如果需要选择单个元素，用\u0026lt;if\u0026gt;是不合适的，此时应该使用\u0026lt;when\u0026gt;，\u0026lt;when\u0026gt;只能选择单个元素进行执行。有点类似于switch...case...default...语句。\nchoose: 类似于switch。 when: 类似于case。 otherwise: 类似于default。 基本格式:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;choose\u0026gt; \u0026lt;when test=\u0026#34;...\u0026#34;\u0026gt; \u0026lt;!--SQL 语句--\u0026gt; \u0026lt;/when\u0026gt; \u0026lt;when test=\u0026#34;...\u0026#34;\u0026gt; \u0026lt;!--SQL 语句--\u0026gt; \u0026lt;/when\u0026gt; \u0026lt;when test=\u0026#34;...\u0026#34;\u0026gt; \u0026lt;!--SQL 语句--\u0026gt; \u0026lt;/when\u0026gt; \u0026lt;when test=\u0026#34;...\u0026#34;\u0026gt; \u0026lt;!--SQL 语句--\u0026gt; \u0026lt;/when\u0026gt; \u0026lt;otherwise\u0026gt; \u0026lt;!--SQL 语句--\u0026gt; \u0026lt;/otherwise\u0026gt; \u0026lt;/choose\u0026gt; 3. \u0026lt;where\u0026gt;和\u0026lt;trim\u0026gt; 3.1 \u0026lt;where\u0026gt; 之前我们在使用\u0026lt;if\u0026gt;元素时，会在where后面加上1=1来确保即使后面条件判断不成立这个 SQL 语句没有语法错误，能够执行\u0026lt;where\u0026gt;可以替代1=1,只要在\u0026lt;if\u0026gt;元素外边包裹一层\u0026lt;where\u0026gt;就可以了，这样既符合逻辑，又清晰明了。\n3.2 \u0026lt;trim\u0026gt; 除了可以使用\u0026lt;where\u0026gt;来实现之外，还可以使用\u0026lt;trim\u0026gt;来实现这个功能，\u0026lt;trim\u0026gt;用来删除多余的关键字。\n下面是\u0026lt;trim\u0026gt;元素的属性：\nprefix：指定给SQL语句增加的前缀； prefixOverrides：指定要给SQL语句中去掉的前缀字符串； suffix：指定要给SQL语句增加的后缀； suffixOverrides：指定要给SQL语句中去掉都后缀字符串。 4. 更新操作 在实际开发中，一般我们只想更新某个对象的某个或多个字段，如果像传统的Hibernate框架，就必须发送所有的字段给持久化对象，这个会导致执行效率非常低。而MyBatis给出了\u0026lt;set\u0026gt;元素，和\u0026lt;if\u0026gt;元素结合可以实现更新需要更新字段的目的。\n格式为：\n1 2 3 4 5 6 7 8 9 update ... \u0026lt;set\u0026gt; \u0026lt;if test=\u0026#34;...\u0026#34;\u0026gt; ... \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;...\u0026#34;\u0026gt; ... \u0026lt;/if\u0026gt; \u0026lt;/set\u0026gt; 5. 复杂查询操作 5.1 \u0026lt;foreach\u0026gt;元素的属性 5.1.2 迭代数组 假如有一个数据表，结构如下：\n1 2 3 4 CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(50) ); 我们要根据一个id数组来查询多个用户的名字，映射文件可以这样配置：\n1 2 3 4 5 6 7 \u0026lt;select id=\u0026#34;findUsersByIds\u0026#34; parameterType=\u0026#34;int[]\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; SELECT * FROM users WHERE id IN \u0026lt;foreach collection=\u0026#34;array\u0026#34; item=\u0026#34;id\u0026#34; open=\u0026#34;(\u0026#34; separator=\u0026#34;,\u0026#34; close=\u0026#34;)\u0026#34;\u0026gt; #{id} \u0026lt;/foreach\u0026gt; \u0026lt;/select\u0026gt; 如果我们这样调用：\n1 2 int[] ids = [1,2,3]; List\u0026lt;User\u0026gt; users = userMapper.findUsersByIds(ids); 会生成下列语句：\n1 SELECT * FROM users WHERE id IN (1, 2, 3); 这个id IN (1, 2, 3)语句其实就是id=1 OR id=2 OR id=3的简写。\n根据所学知识，上述映射文件代码就等价于：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;select id=\u0026#34;findUsersByIds\u0026#34; parameterType=\u0026#34;int[]\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; SELECT * FROM users WHERE \u0026lt;where\u0026gt; \u0026lt;foreach collection=\u0026#34;array\u0026#34; item=\u0026#34;id\u0026#34; separator=\u0026#34;OR\u0026#34;\u0026gt; \u0026lt;if test=\u0026#34;id != null and id != \u0026#39;\u0026#39;\u0026#34;\u0026gt; id = #{id} \u0026lt;/if\u0026gt; \u0026lt;/foreach\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; 5.1.3 迭代List 使用方法类似于Array，只是把传入参数改成List。\n映射文件可以这样配置：\n1 2 3 4 5 6 7 \u0026lt;select id=\u0026#34;findUsersByIds\u0026#34; parameterType=\u0026#34;java.util.Arrays\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; SELECT * FROM users WHERE id IN \u0026lt;foreach collection=\u0026#34;array\u0026#34; item=\u0026#34;id\u0026#34; open=\u0026#34;(\u0026#34; separator=\u0026#34;,\u0026#34; close=\u0026#34;)\u0026#34;\u0026gt; #{id} \u0026lt;/foreach\u0026gt; \u0026lt;/select\u0026gt; 调用方法：\n1 2 3 4 List\u0026lt;Integer\u0026gt; ids = new ArrayList\u0026lt;Integer\u0026gt;(); ids.add(1); ids.add(2); List\u0026lt;User\u0026gt; users = userMapper.findUsersByIds(ids); 5.1.3 迭代 Map 通过指定多个限定条件来查询符合条件的用户(AND)，可以使用Map。\n通过id和姓名来查询用户：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;select id=\u0026#34;findUsersByParams\u0026#34; parameterType=\u0026#34;java.util.Map\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; SELECT * FROM users WHERE \u0026lt;where\u0026gt; \u0026lt;if test=\u0026#34;ids != null and ids.size() \u0026gt; 0\u0026#34;\u0026gt; id IN \u0026lt;foreach collection=\u0026#34;ids\u0026#34; item=\u0026#34;id\u0026#34; open=\u0026#34;(\u0026#34; separator=\u0026#34;,\u0026#34; close=\u0026#34;)\u0026#34;\u0026gt; #{id} \u0026lt;/foreach\u0026gt; \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;name != null and name != \u0026#39;\u0026#39;\u0026#34;\u0026gt; AND name = #{name} \u0026lt;/if\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; 其中，里面的ids就是传入的Map对象的ids键所对应的值（是一个数组），然后通过\u0026lt;foreach\u0026gt;遍历获取每一个id，ids就是传入的Map对象的ids键所对应的值。\n1 2 3 4 5 Map\u0026lt;String, Object\u0026gt; params = new HashMap\u0026lt;\u0026gt;(); params.put(\u0026#34;ids\u0026#34;, Arrays.asList(1, 2, 3)); // 传入 ids 数组 params.put(\u0026#34;name\u0026#34;, \u0026#34;Alice\u0026#34;); // 传入 name List\u0026lt;User\u0026gt; users = userMapper.findUsersByParams(params); 就会生成这个sql语句：\n1 select * from user where id in (1,2,3) AND name=\u0026#34;Alice\u0026#34;; 实践：学生信息查询系统 利用本章属于知识完成一个学生信息查询系统。该系统要求实现以下三个功能：\n当用户输入的学生姓名不为空时，则只根据学生姓名进行学生信息的查询。 当用户输入的学生姓名为空，而学生专业为空时，则指根据学生专业进行学生信息的查询。 当用户输入的学生姓名和专业都为空，则要求查询出所有学号不为空的学生信息。 第一步：数据库数据准备 登录 MySQL 终端，输入以下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 use mall; create table student( name varchar(50), # 学生姓名 major varchar(256), # 专业 id int primary key auto_increment # 学生编号 ); create index idx_student_id on student(id); # 为学生编号创建索引 # 插入数据 insert into student(name, major) values(\u0026#39;zhangsan\u0026#39;, \u0026#39;computer\u0026#39;); insert into student(name, major) values(\u0026#39;lisi\u0026#39;, \u0026#39;mathematics\u0026#39;); insert into student(name, major) values(\u0026#39;wangwu\u0026#39;, \u0026#39;physics\u0026#39;); insert into student(name, major) values(\u0026#39;zhaoliu\u0026#39;, \u0026#39;chemistry\u0026#39;); insert into student(name, major) values(\u0026#39;sunqi\u0026#39;, \u0026#39;biology\u0026#39;); insert into student(name, major) values(\u0026#39;zhouba\u0026#39;, \u0026#39;engineering\u0026#39;); insert into student(name, major) values(\u0026#39;wujiu\u0026#39;, \u0026#39;literature\u0026#39;); insert into student(name, major) values(\u0026#39;zhengshi\u0026#39;, \u0026#39;history\u0026#39;); insert into student(name, major) values(\u0026#39;liuyi\u0026#39;, \u0026#39;philosophy\u0026#39;); insert into student(name, major) values(\u0026#39;qibai\u0026#39;, \u0026#39;art\u0026#39;); insert into student(name, major) values(\u0026#39;bajiu\u0026#39;, \u0026#39;music\u0026#39;); insert into student(name, major) values(\u0026#39;shiyi\u0026#39;, \u0026#39;economics\u0026#39;); insert into student(name, major) values(\u0026#39;shier\u0026#39;, \u0026#39;political science\u0026#39;); insert into student(name, major) values(\u0026#39;shisan\u0026#39;, \u0026#39;sociology\u0026#39;); insert into student(name, major) values(\u0026#39;shisi\u0026#39;, \u0026#39;psychology\u0026#39;); insert into student(name, major) values(\u0026#39;shiwu\u0026#39;, \u0026#39;law\u0026#39;); insert into student(name, major) values(\u0026#39;shiliu\u0026#39;, \u0026#39;medicine\u0026#39;); insert into student(name, major) values(\u0026#39;shiqi\u0026#39;, \u0026#39;nursing\u0026#39;); insert into student(name, major) values(\u0026#39;shiba\u0026#39;, \u0026#39;education\u0026#39;); insert into student(name, major) values(\u0026#39;shijiu\u0026#39;, \u0026#39;business\u0026#39;); insert into student(name, major) values(\u0026#39;ershi\u0026#39;, \u0026#39;architecture\u0026#39;); 第二步：创建 POJO 类 在org.example.pojo包下新建一个Student.java文件，创建 POJO 类 Student\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package Test; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.example.pojo.Student; import org.junit.Test; import java.io.IOException; import java.io.Reader; import java.util.List; public class StudentTest { private static SqlSession sqlSession; @Test public void test1() throws IOException { Student student = new Student(); student.setName(\u0026#34;zhangsan\u0026#34;); SqlSession session=getSession(); List\u0026lt;Student\u0026gt; list=session.selectList(\u0026#34;findStudent\u0026#34;,student); for(Student s:list){ System.out.println(s); } } @Test public void test2() throws IOException { Student student = new Student(); student.setMajor(\u0026#34;economics\u0026#34;); SqlSession session=getSession(); List\u0026lt;Student\u0026gt; list=session.selectList(\u0026#34;findStudent\u0026#34;,student); for(Student s:list){ System.out.println(s); } } @Test public void test3() throws IOException { Student student = new Student(); sqlSession=getSession(); List\u0026lt;Student\u0026gt; list=sqlSession.selectList(\u0026#34;findStudent\u0026#34;,student); for(Student s:list){ System.out.println(s); } } public static SqlSession getSession() throws IOException { if(sqlSession == null){ Reader reader = Resources.getResourceAsReader(\u0026#34;mybatis-config.xml\u0026#34;); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); sqlSession = sqlSessionFactory.openSession(); } return sqlSession; } } 第三步：Mapper 映射文件配置 创建 StudentMapper，创建映射关系和查询条件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;org.example.pojo.Student\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;findStudent\u0026#34; parameterType=\u0026#34;org.example.pojo.Student\u0026#34; resultType=\u0026#34;org.example.pojo.Student\u0026#34;\u0026gt; select * from student \u0026lt;where\u0026gt; \u0026lt;!--当用户输入的学生姓名不为空时，则只根据学生姓名进行学生信息的查询。--\u0026gt; \u0026lt;if test=\u0026#34;name!=null and name!=\u0026#39;\u0026#39;\u0026#34;\u0026gt; name=#{name} \u0026lt;/if\u0026gt; \u0026lt;!--当用户输入的学生姓名为空，而学生专业为空时，则指根据学生专业进行学生信息的查询。--\u0026gt; \u0026lt;if test=\u0026#34;(name==null or name==\u0026#39;\u0026#39;) and (major!=\u0026#39;\u0026#39; and major!=null)\u0026#34;\u0026gt; major=#{major} \u0026lt;/if\u0026gt; \u0026lt;!--当用户输入的学生姓名和专业都为空，则要求查询出所有学号不为空的学生信息。--\u0026gt; \u0026lt;if test=\u0026#34;(name==null or name==\u0026#39;\u0026#39;) and (major==\u0026#39;\u0026#39; or major==null)\u0026#34;\u0026gt; id!=0 \u0026lt;/if\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 第四步：配置 MyBatis 配置文件 在mybatis-config.xml 里，把 StudentMapper.xml 文件包含进去：\n1 \u0026lt;mapper resource=\u0026#34;mapper/StudentMapper.xml\u0026#34;/\u0026gt; 第五步：创建测试类 在 test/java/Test 目录下创建 StudentTest.java 文件，添加以下测试方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package Test; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.example.pojo.Student; import org.junit.Test; import java.io.IOException; import java.io.Reader; import java.util.List; public class StudentTest { private static SqlSession sqlSession; @Test //当用户输入的学生姓名不为空时，则只根据学生姓名进行学生信息的查询。 public void test1() throws IOException { Student student = new Student(); student.setName(\u0026#34;zhangsan\u0026#34;); SqlSession session=getSession(); List\u0026lt;Student\u0026gt; list=session.selectList(\u0026#34;findStudent\u0026#34;,student); for(Student s:list){ System.out.println(s); } } @Test //当用户输入的学生姓名为空，而学生专业为空时，则指根据学生专业进行学生信息的查询。 public void test2() throws IOException { Student student = new Student(); student.setMajor(\u0026#34;economics\u0026#34;); SqlSession session=getSession(); List\u0026lt;Student\u0026gt; list=session.selectList(\u0026#34;findStudent\u0026#34;,student); for(Student s:list){ System.out.println(s); } } @Test //当用户输入的学生姓名和专业都为空，则要求查询出所有学号不为空的学生信息。 public void test3() throws IOException { Student student = new Student(); sqlSession=getSession(); List\u0026lt;Student\u0026gt; list=sqlSession.selectList(\u0026#34;findStudent\u0026#34;,student); for(Student s:list){ System.out.println(s); } } public static SqlSession getSession() throws IOException { if(sqlSession == null){ Reader reader = Resources.getResourceAsReader(\u0026#34;mybatis-config.xml\u0026#34;); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); sqlSession = sqlSessionFactory.openSession(); } return sqlSession; } } 第五步：查看运行结果😆 成功了，这一章学习圆满结束！🎆🎆\n输出结果如下：\ntest1：\n1 Student [name=zhangsan, major=computer, id=1] test2：\n1 Student [name=shiyi, major=economics, id=12] test3：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Student [name=zhangsan, major=computer, id=1] Student [name=lisi, major=mathematics, id=2] Student [name=wangwu, major=physics, id=3] Student [name=zhaoliu, major=chemistry, id=4] Student [name=sunqi, major=biology, id=5] Student [name=zhouba, major=engineering, id=6] Student [name=wujiu, major=literature, id=7] Student [name=zhengshi, major=history, id=8] Student [name=liuyi, major=philosophy, id=9] Student [name=qibai, major=art, id=10] Student [name=bajiu, major=music, id=11] Student [name=shiyi, major=economics, id=12] Student [name=shier, major=political science, id=13] Student [name=shisan, major=sociology, id=14] Student [name=shisi, major=psychology, id=15] Student [name=shiwu, major=law, id=16] Student [name=shiliu, major=medicine, id=17] Student [name=shiqi, major=nursing, id=18] Student [name=shiba, major=education, id=19] Student [name=shijiu, major=business, id=20] Student [name=ershi, major=architecture, id=21] ","date":"2025-01-22T00:00:00Z","image":"https://programcx.github.io/p/mybatis-%E5%8A%A8%E6%80%81-sql-%E8%AF%AD%E5%8F%A5/MyBatis_hu1891604869159411546.png","permalink":"https://programcx.github.io/p/mybatis-%E5%8A%A8%E6%80%81-sql-%E8%AF%AD%E5%8F%A5/","title":"MyBatis 动态 SQL 语句"},{"content":"MyBatis 中核心对象 1. SqlSession SqlSession 中提供了不同的查询方法。其中可以查询单个，也可以查询符合查询条件的集合。不但可以进行查询操作，也可以进行增删改的功能。下面介绍几个常用的方法：\n\u0026lt;T\u0026gt; T selectOne(String statement, Object parameter) : parameter 是语句所需要的参数，用来将参数传递给 SQL 语句。该方法会返回一个xml映射配置中指定的类型。 \u0026lt;E\u0026gt; List\u0026lt;E\u0026gt; selectList(String statement, Object parameter) :查询泛型对象的集合。 int insert(String statement, Object parameter) ：插入方法，参数statement 是\u0026lt;insert\u0026gt; 的id，parameter 是插入语句所需要的参数，也就是后面要学到的parameterType int update(String statement, Object parameter),int delete(String statement, Object parameter) 用法类似。 void commit()：提交事务。 void rollback()：回滚事务。 void close()：关闭SqlSession对象。 2. MyBatis 核心配置文件元素 2.2 \u0026lt;properties\u0026gt;元素 用来存放数据库连接驱动类型、sql地址、用户名和密码。然后在mybatis-config.xml里面引入、使用。\n2.3 \u0026lt;typeAlias\u0026gt;元素 给全限定名起一个别名(Alias)。\n在mybatis-config.xml文件里面加上：\n1 2 3 \u0026lt;typeAliases\u0026gt; \u0026lt;typeAlias alias=\u0026#34;User\u0026#34; type=\u0026#34;org.example.pojo.User\u0026#34; /\u0026gt; \u0026lt;/typeAliases\u0026gt; 2.4 \u0026lt;environments\u0026gt;元素 可以给开发环境、生产环境进行不同的配置。\nUNPOOLED：无连接池类型。每次请求会打开和关闭数据库。适用于对性能要求不高的简单应用程序。 POOLED：连接池类型。 2.6 \u0026lt;mappers\u0026gt;元素 用来引入映射文件。基本结构：\n1 2 3 \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;mapper/UserMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; 2.7 \u0026lt;select\u0026gt;元素 该标签可以实现查询操作，可以将查询结果映射为POJO对象，并且返回。\n简单的查询例子：\n1 2 3 4 5 6 \u0026lt;select id=\u0026#34;findUserById\u0026#34; parameterType=\u0026#34;int\u0026#34; resultType=\u0026#34;cn.programcx.POJO.User\u0026#34; \u0026gt; SELECT * FROM user WHERE id = #{id} \u0026lt;/select\u0026gt; 这个语句接受一个int类型的参数，返回一个cn.programcx.POJO包中的对象。\n参数：\n属性 描述 id 在命名空间中唯一的标识符，可以被用来引用这条语句。 parameterType 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的，因为 MyBatis 可以通过类型处理器（TypeHandler）推断出具体传入语句的参数，默认值为未设置（unset）。 resultType 期望从这条语句中返回结果的类全限定名或别名。注意，如果返回的是集合，那应该设置为集合包含的类型，而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。 resultMap 期望从这条语句中返回结果的类全限定名或别名。注意，如果返回的是集合，那应该设置为集合包含的类型，而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。 flushCache 将其设置为 true 后，只要语句被调用，都会导致本地缓存和二级缓存被清空，默认值：false。 useCache 将其设置为 true 后，将会导致本条语句的结果被二级缓存缓存起来，默认值：对 select 元素为 true。 timeout 这个设置是在抛出异常之前，驱动程序等待数据库返回请求结果的秒数。默认值为未设置（unset）（依赖数据库驱动）。 fetchSize 这是一个给驱动的建议值，尝试让驱动程序每次批量返回的结果行数等于这个设置值。默认值为未设置（unset）（依赖驱动）。 statementType 可选 STATEMENT，PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement，PreparedStatement 或 CallableStatement，默认值：PREPARED。 resultSetType FORWARD_ONLY，SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT（等价于 unset）中的一个，默认值为 unset（依赖数据库驱动）。 databaseId 如果配置了数据库厂商标识（databaseIdProvider），MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句；如果带和不带的语句都有，则不带的会被忽略。 resultOrdered 这个设置仅针对嵌套结果 select 语句：如果为 true，将会假设包含了嵌套结果集或是分组，当返回一个主结果行时，就不会产生对前面结果集的引用。默认值：false。 resultSets 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称，多个名称之间以逗号分隔。 注意：如果映射的POJO对象和指定查询的数据表中名称不一致，不能使用resultType，否则无法进行映射，会抛出异常；如果名称不一致，应使用resultMap（后面介绍）。\n2.8 \u0026lt;insert\u0026gt;元素 insert标签用来指定插入记录的操作和属性。\n1 2 3 4 5 6 \u0026lt;insert id=\u0026#34;insertUser\u0026#34; parameterType=\u0026#34;cn.programcx.POJO\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;id\u0026#34;\u0026gt; INSERT INTO user(name,sex,email,phone,address,password) values (#{name},#{sex},#{email},#{phone},#{address},#{password}) \u0026lt;/insert\u0026gt; 其中，#{name}表示映射的POJO对象的成员变量·name。\n属性 描述 id 在命名空间中唯一的标识符，可以被用来引用这条语句。 parameterType 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的，因为 MyBatis 可以通过类型处理器（TypeHandler）推断出具体传入语句的参数，默认值为未设置（unset）。 parameterMap 用于引用外部 parameterMap 的属性，目前已被废弃。请使用行内参数映射和 parameterType 属性。 flushCache 将其设置为 true 后，只要语句被调用，都会导致本地缓存和二级缓存被清空，默认值：（对 insert、update 和 delete 语句）true。 timeout 这个设置是在抛出异常之前，驱动程序等待数据库返回请求结果的秒数。默认值为未设置（unset）（依赖数据库驱动）。 statementType 可选 STATEMENT，PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement，PreparedStatement 或 CallableStatement，默认值：PREPARED。 useGeneratedKeys （仅适用于 insert 和 update）这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键（比如：像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段），默认值：false。 keyProperty （仅适用于 insert 和 update）指定能够唯一识别对象的属性，MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值，默认值：未设置（unset）。如果生成列不止一个，可以用逗号分隔多个属性名称。 keyColumn （仅适用于 insert 和 update）设置生成键值在表中的列名，在某些数据库（像 PostgreSQL）中，当主键列不是表中的第一列的时候，是必须设置的。如果生成列不止一个，可以用逗号分隔多个属性名称。 databaseId 如果配置了数据库厂商标识（databaseIdProvider），MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句；如果带和不带的语句都有，则不带的会被忽略。 2.9 \u0026lt;update\u0026gt;元素 用来更新记录。\n1 2 3 4 5 6 \u0026lt;update id=\u0026#34;updateUser\u0026#34; parameterType=\u0026#34;cn.programcx.POJO.user\u0026#34; \u0026gt; UPDATE user SET name = #{name},email = #{email},password = #{password} WHERE userId = #{userId} \u0026lt;/update\u0026gt; 属性和上面的insert一样。\n2.10 \u0026lt;delete\u0026gt;元素 用来删除记录\n1 2 3 4 5 6 \u0026lt;delete id=\u0026#34;deleteUser\u0026#34; \u0026gt;\u0026lt;/delete\u0026gt; DELETE FROM user WHERE id=#{id} \u0026lt;/delete\u0026gt; 2.11 \u0026lt;selectKey\u0026gt;元素 用来自定义生成一个属性的值。\n例如，插入用户为用户随机生成id并插入数据表：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;insert id=\u0026#34;insertUser\u0026#34; parameterType=\u0026#34;cn.programcx.POJO.user\u0026#34;\u0026gt; \u0026lt;selectKey keyProperty=\u0026#34;userId\u0026#34; resultType=\u0026#34;int\u0026#34; order=\u0026#34;BEFORE\u0026#34;\u0026gt; select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1 \u0026lt;/selectKey\u0026gt; INSERT INTO user(userId,name,sex,email,phone,address,password) values (#{userId},#{name},#{sex},#{email},#{phone},#{address},#{password}) \u0026lt;/insert\u0026gt; 2.11 \u0026lt;resultMap\u0026gt;元素 \u0026lt;resultMap\u0026gt;的作用是自定义映射规则。如果数据表字段名称和POJO类中所需要映射到属性的名称不一致，就不能仅仅指定parameterType或resultType来实现映射，必须要通过自定义映射来实现。 其基本格式是：\n1 2 3 4 \u0026lt;resultMap type=\u0026#34;[完整类名]\u0026#34; id=\u0026#34;[标识这个自定义映射的字符串]\u0026#34;\u0026gt; \u0026lt;id property=\u0026#34;[POJO类中的属性名称]\u0026#34; column=\u0026#34;[字段名称]\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;[POJO类中的属性名称]\u0026#34; column=\u0026#34;[字段名称]\u0026#34;/\u0026gt; \u0026lt;/resultMap\u0026gt; ","date":"2025-01-16T00:00:00Z","image":"https://programcx.github.io/p/mybatis-%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE/MyBatis_hu1891604869159411546.png","permalink":"https://programcx.github.io/p/mybatis-%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE/","title":"MyBatis 常用配置"},{"content":"一、环境搭建 依赖引入 在pom.xml文件中添加以下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;dependencies\u0026gt; \u0026lt;!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.19\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;9.1.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- https://mvnrepository.com/artifact/junit/junit --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.13.2\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt;\u0026lt;/dependencies\u0026gt; 创建数据库连接信息配置文件 在src/main/resources 目录下创建数据库连接的配置文件，命名为db.properties，用来配置数据库连接时的参数。\n1 2 3 4 mysql.driver = com.mysql.cj.jdbc.Driver mysql.url = jdbc:mysql://localhost:3306/mall?serverTimezone=UTC\u0026amp;\\characterEncoding=utf8\u0026amp;useUnicode=true\u0026amp;useSSL=false mysql.username = root mysql.password = mysql 创建 Mybatis 的核心配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;!-- 配置文件的根元素 --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 属性：定义配置外在化 --\u0026gt; \u0026lt;properties resource=\u0026#34;db.properties\u0026#34;/\u0026gt; \u0026lt;!-- 环境：配置mybatis的环境 --\u0026gt; \u0026lt;environments default=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;!-- 环境变量：可以配置多个环境变量，比如使用多数据源时，就需要配置多个环境变量 --\u0026gt; \u0026lt;environment id=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;!-- 事务管理器 --\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;!-- 数据源 --\u0026gt; \u0026lt;dataSource type=\u0026#34;POOLED\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;${mysql.driver}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${mysql.url}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${mysql.username}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${mysql.password}\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;/configuration\u0026gt; 二、MyBatis 入门程序 第一步：创建 POJO 实体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package org.example.pojo; public class User { private int userID; private String userName; private String sex; public int getUserID() { return userID; } public void setUserID(int userID) { this.userID = userID; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } } POJO 实体用来从数据库映射的值。\n第二步：创建映射文件 UserMapper.xml 文件命名 文件命名通常使用POJO 实体类名+Mapper 命名。例如User 实体类映射文件名为 UserMapper.xml 。\n1 2 3 4 5 6 7 8 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;org.example.pojo.User\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;findUserById\u0026#34; parameterType=\u0026#34;int\u0026#34; resultType=\u0026#34;org.example.pojo.User\u0026#34;\u0026gt; select * from user where userID = # {id} \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 第三步：在 mybatis-config.xml 配置文件里面添加映射 1 2 3 \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;mapper/UserMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; 此代码在\u0026lt;configuration\u0026gt;\u0026lt;configuration/\u0026gt; 里面直接添加。\n第四步：编写测试类 在 test/java/Test 下创建 UserTest.java 文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package Test; import org.example.pojo.User; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; import java.io.Reader; public class UserTest { @Test public void test() throws IOException { //第一步：将 mybatis-config.xml 文件里面的内容加载到 reader 对象中 Reader reader = Resources.getResourceAsReader(\u0026#34;mybatis-config.xml\u0026#34;); //第二步：初始化MyBatis数据库，创建 SqlSessionFactory 类的实例 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); //第三步：创建SqlSession会话实例 SqlSession sqlSession = sqlSessionFactory.openSession(); //获取查询到的结果映射到的对象 User user = sqlSession.selectOne(\u0026#34;findUserById\u0026#34;, 1); System.out.println(user.getUserName()); sqlSession.close(); } } 在上述代码中，先通过Resource的getResourceReader()方法来将 MyBatis 配置文件加载到 reader 对象中，接着将read传入 SqlSessionFactoryBuilder().build() 工厂方法来构建SqlSessionFactory 类的对象。之后这个对象调用openSession方法来获取一个SqlSession会话。然后调用selectOne()来获取查询到的结果映射到的对象。最后就可以用Get方法来获取想得到的数据了。\n运行测试，得到结果：\n1 ChengXu ","date":"2025-01-15T00:00:00Z","image":"https://programcx.github.io/p/mybatis-%E5%85%A5%E9%97%A8/MyBatis_hu1891604869159411546.png","permalink":"https://programcx.github.io/p/mybatis-%E5%85%A5%E9%97%A8/","title":"MyBatis 入门"},{"content":"Java 实现文件下载 使用HttpClient下载 阻塞方式下载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpClient; import java.nio.file.Paths; import java.nio.file.Path; public class Main { public static void main(String[] args) { downloadAFile(\u0026#34;https://pc-package.wpscdn.cn/wps/download/W.P.S.20.2735.exe\u0026#34;, \u0026#34;W.P.S.20.2735.exe\u0026#34;); } public static void downloadAFile(String url, String fileName) { HttpClient client = HttpClient.newHttpClient(); //创建一个HttpRequest请求 HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build(); Path filePath = Paths.get(fileName); try { //发送Http请求 HttpResponse\u0026lt;Path\u0026gt; response = client.send(request, HttpResponse.BodyHandlers.ofFile(filePath)); //判断是否成功下载 if (response.statusCode() == 200) { System.out.printf(\u0026#34;文件%s已经成功保存!\u0026#34;, fileName); } } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } } 在上述代码中，实现了一个名为downloadAFile()函数。使用 HttpClient.send() 方法发送Http请求并将响应体写入文件，并获得HttpResponse\u0026lt;Path\u0026gt;类型的response对象，使用该对象获得请求的状态码statusCode。\n异步方式下载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpClient; import java.nio.file.Paths; import java.nio.file.Path; import java.util.Scanner; public class Main { public static void main(String[] args) { downloadAFile(\u0026#34;https://pc-package.wpscdn.cn/wps/download/W.P.S.20.2735.exe\u0026#34;, \u0026#34;W.P.S.20.2735.exe\u0026#34;); System.out.println(\u0026#34;下载已经开始，按回车键结束...\u0026#34;); Scanner scanner = new Scanner(System.in); scanner.nextLine(); } public static void downloadAFile(String url, String fileName) { HttpClient client = HttpClient.newHttpClient(); //创建一个HttpRequest请求 HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build(); Path filePath = Paths.get(fileName); //异步下载 client.sendAsync(request, HttpResponse.BodyHandlers.ofFile(filePath)).thenAccept(Main::downloadCallBack).exceptionally(e -\u0026gt; { System.out.println(\u0026#34;下载失败！\u0026#34; + e.getMessage()); return null; }); } private static void downloadCallBack(HttpResponse\u0026lt;Path\u0026gt; pathHttpResponse) { if (pathHttpResponse.statusCode() == 200) { System.out.println(\u0026#34;下载成功！文件已经保存至：\u0026#34; + pathHttpResponse.body()); } else { System.out.printf(\u0026#34;下载失败！状态码为%d。\\n\u0026#34;, pathHttpResponse.statusCode()); } } } 这里使用client.sendAsync()异步下载文件，thenAccept()来指定下载完成时的回调函数，exceptionally()来处理异常。注意，由于这里是异步下载的，不能使用try{}catch{}来处理异常，异常不能被捕获。\n使用OKHttp下载 导入OkHttp依赖 在pom.xml的\u0026lt;dependencies\u0026gt;\u0026lt;/dependencies\u0026gt;中添加以下代码：\n1 2 3 4 5 6 \u0026lt;!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.squareup.okhttp3\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;okhttp\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.14.9\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; OKHttp获取文本文件 Download.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.example; import okhttp3.*; import java.io.IOException; public class Download{ public final OkHttpClient client = new OkHttpClient(); public void run() throws IOException { Request request = new Request.Builder().url(\u0026#34;http://www.baidu.com\u0026#34;).build(); try(Response response = client.newCall(request).execute()){ if (response.body() != null) { System.out.println(response.body().string()); } }catch (Exception e){ System.out.println(e.getMessage()); } } } Main.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example; import java.io.IOException; public class Main { public static void main(String[] args) { Download download = new Download(); try{ download.run(); } catch (IOException e) { throw new RuntimeException(e); } } } OKHttp异步请求 Download.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package org.example; import okhttp3.*; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Objects; public class Download { private final String url; private final String fileName; private final String filePath; private final OkHttpClient client = new OkHttpClient(); public Download(String url, String fileName, String filePath) { this.url = url; this.fileName = fileName; this.filePath = filePath; } public void run() { Request request = new Request.Builder().url(url).build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override //响应时的回调函数 public void onResponse(Call call, Response response) throws IOException { if (!response.isSuccessful()) { throw new IOException(\u0026#34;Unexpected code \u0026#34; + response); } try (InputStream inputStream = Objects.requireNonNull(response.body()).byteStream(); //从响应体获取字节流 FileOutputStream outputStream = new FileOutputStream(new File(filePath, fileName))) { //创建文件输出流 byte[] buffer = new byte[2048]; //创建缓存区，大小为2048KB int len; //用于还剩下多少字节没有被读取 while ((len = inputStream.read(buffer)) != -1) { //还没有被读取完 outputStream.write(buffer, 0, len); //将缓冲区数据写入文件 } System.out.printf(\u0026#34;文件%s已下载至%s%n\u0026#34;, fileName, filePath); } catch (IOException e) { e.printStackTrace(); } } }); } } Main.java\n1 2 3 4 5 6 7 8 9 10 11 package org.example; public class Main { public static void main(String[] args) { String url = \u0026#34;https://down-tencent.huorong.cn/sysdiag-all-x64-6.0.4.0-2024.11.16.1.exe\u0026#34;; String filename = \u0026#34;sysdiag-all-x64-6.0.4.0-2024.11.16.1.exe\u0026#34;; Download download = new Download(url, filename, \u0026#34;D:/\u0026#34;); download.run(); } } 上述代码中，使用Request.Builder().url(url).build()创建一个Request对象，使用Call类的call对象发起Http Get请求，使用enqueue()方法处理响应和错误。 在处理响应时，首先将response.body()响应体通过byteStream()方法转化为字节流，然后于FileInputStream一起使用写入文件。\nOKHttp同步请求 无需使用call.enqueue()方法，最后使用断言assertThat(response.code(), equalTo(200));判断状态码是否等于200即可。\n自定义请求头 1 Request request = new Request.Builder().url(url).addHeader(\u0026#34;Content-Length\u0026#34;,\u0026#34;application/x-msdownload\u0026#34;).build(); 允许重定向 1 private final OkHttpClient client = new OkHttpClient().newBuilder().followRedirects(true).build(); 超时设置 1 private final OkHttpClient client = new OkHttpClient().newBuilder().readTimeout(3, TimeUnit.SECONDS).build(); 取消请求 可以使用Call.cancel()终止请求。\n","date":"2024-11-16T00:00:00Z","image":"https://programcx.github.io/p/java-%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6%E4%B8%8B%E8%BD%BD/java_hu14469118171915315630.jpg","permalink":"https://programcx.github.io/p/java-%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6%E4%B8%8B%E8%BD%BD/","title":"Java 实现文件下载"},{"content":"Vue 3 报错Uncaught TypeError:xxx.component is not a function 记录学习Vue 3过程中踩的坑\n一、错误代码 有以下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const vueWatcher = { methods: { click(index) { if (index == 0) { alert(\u0026#34;外层\u0026#34;); } else if (index == 1) { alert(\u0026#34;中层\u0026#34;); } else if (index == 2) { alert(\u0026#34;内层\u0026#34;); } }, KeyDownCtrl() { alert(\u0026#34;Ctrl Pressed\u0026#34;); } } } const app = Vue.createApp(vueWatcher).mount(\u0026#34;#vue-watcher\u0026#34;); const alertComponent = { data() { return { msg: \u0026#34;警告\u0026#34;, } }, methods: { click() { alert(this.msg); } }, template: \u0026#39;\u0026lt;div\u0026gt;\u0026lt;button @click=\u0026#34;click\u0026#34;\u0026gt;按钮\u0026lt;/button\u0026gt;\u0026lt;/div\u0026gt;\u0026#39; } app.component(\u0026#34;alert-component\u0026#34;, alertComponent); 我们是想要给 app对象添加一个标准的Vue组件alert-component，但是浏览器报错Uncaught TypeError:app.component is not a function。\n二、分析原因 其实这是一个添加子组件顺序的问题。上述错误代码中，先创建一个名为app的Vue对象，接着挂载到vue-watcher上，最后添加子组件alert-componet。 这种顺序是错误的，正确的顺序应该是：\n创建一个名为app的Vue对象； 1 const app = Vue.createApp(vueWatcher); 添加子组件alert-componet； 1 app.component(\u0026#34;alert-component\u0026#34;, alertComponent); 挂载到vue-watcher上。 1 app.mount(\u0026#34;#vue-watcher\u0026#34;); 三、正确代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const vueWatcher = { methods: { click(index) { if (index == 0) { alert(\u0026#34;外层\u0026#34;); } else if (index == 1) { alert(\u0026#34;中层\u0026#34;); } else if (index == 2) { alert(\u0026#34;内层\u0026#34;); } }, KeyDownCtrl() { alert(\u0026#34;Ctrl Pressed\u0026#34;); } } } const app = Vue.createApp(vueWatcher); const alertComponent = { data() { return { msg: \u0026#34;警告\u0026#34;, } }, methods: { click() { alert(this.msg); } }, template: \u0026#39;\u0026lt;div\u0026gt;\u0026lt;button @click=\u0026#34;click\u0026#34;\u0026gt;按钮\u0026lt;/button\u0026gt;\u0026lt;/div\u0026gt;\u0026#39; } app.component(\u0026#34;my\u0026#34;, alertComponent); app.mount(\u0026#34;#vue-watcher\u0026#34;); 欢迎指正错误！\n","date":"2024-10-15T00:00:00Z","image":"https://programcx.github.io/p/uncaught-typeerrorxxx.component-is-not-a-function/vue.svg","permalink":"https://programcx.github.io/p/uncaught-typeerrorxxx.component-is-not-a-function/","title":"Uncaught TypeError:xxx.component is not a function"},{"content":"Qt 5.15 connect() 函数的一个大坑 有以下代码：\n1 2 3 4 5 connect(ui-\u0026gt;styleComboBox, \u0026amp;QComboBox::currentIndexChanged, this, [this](int index) { //TODO: change theme spdlog::info(\u0026#34;User change default theme to {}\u0026#34;, index); AppConfig::instance().setBasic(\u0026#34;style\u0026#34;, index); }); 以上代码将ui-\u0026gt;styleComboBox的QComboBox::currentTextChanged信号连接至lambda表达式里的匿名函数。看似没有什么问题，但是在Qt 5.15上报错：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /Users/runner/work/FlowD/FlowD/src/SettingsBasicWidget.cpp:21:5: error: no matching member function for call to \u0026#39;connect\u0026#39; connect(ui-\u0026gt;styleComboBox, \u0026amp;QComboBox::currentIndexChanged, this, [this](int index) { ^~~~~~~ /Users/runner/work/FlowD/Qt/5.15.2/clang_64/lib/QtCore.framework/Headers/qobject.h:222:36: note: candidate function not viable: no overload of \u0026#39;currentIndexChanged\u0026#39; matching \u0026#39;const char *\u0026#39; for 2nd argument static QMetaObject::Connection connect(const QObject *sender, const char *signal, ^ /Users/runner/work/FlowD/Qt/5.15.2/clang_64/lib/QtCore.framework/Headers/qobject.h:225:36: note: candidate function not viable: no overload of \u0026#39;currentIndexChanged\u0026#39; matching \u0026#39;const QMetaMethod\u0026#39; for 2nd argument static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod \u0026amp;signal, ^ /Users/runner/work/FlowD/Qt/5.15.2/clang_64/lib/QtCore.framework/Headers/qobject.h:481:41: note: candidate function not viable: no overload of \u0026#39;currentIndexChanged\u0026#39; matching \u0026#39;const char *\u0026#39; for 2nd argument inline QMetaObject::Connection QObject::connect(const QObject *asender, const char *asignal, ^ /Users/runner/work/FlowD/Qt/5.15.2/clang_64/lib/QtCore.framework/Headers/qobject.h:242:43: note: candidate template ignored: couldn\u0026#39;t infer template argument \u0026#39;Func1\u0026#39; static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer\u0026lt;Func1\u0026gt;::Object *sender, Func1 signal, ^ /Users/runner/work/FlowD/Qt/5.15.2/clang_64/lib/QtCore.framework/Headers/qobject.h:283:13: note: candidate template ignored: couldn\u0026#39;t infer template argument \u0026#39;Func1\u0026#39; connect(const typename QtPrivate::FunctionPointer\u0026lt;Func1\u0026gt;::Object *sender, Func1 signal, const QObject *context, Func2 slot, ^ /Users/runner/work/FlowD/Qt/5.15.2/clang_64/lib/QtCore.framework/Headers/qobject.h:322:13: note: candidate template ignored: couldn\u0026#39;t infer template argument \u0026#39;Func1\u0026#39; connect(const typename QtPrivate::FunctionPointer\u0026lt;Func1\u0026gt;::Object *sender, Func1 signal, const QObject *context, Func2 slot, ^ /Users/runner/work/FlowD/Qt/5.15.2/clang_64/lib/QtCore.framework/Headers/qobject.h:274:13: note: candidate function template not viable: requires 3 arguments, but 4 were provided connect(const typename QtPrivate::FunctionPointer\u0026lt;Func1\u0026gt;::Object *sender, Func1 signal, Func2 slot) ^ /Users/runner/work/FlowD/Qt/5.15.2/clang_64/lib/QtCore.framework/Headers/qobject.h:314:13: note: candidate function template not viable: requires 3 arguments, but 4 were provided connect(const typename QtPrivate::FunctionPointer\u0026lt;Func1\u0026gt;::Object *sender, Func1 signal, Func2 slot) ^ 1 error generated. 为什么会出现这种情况呢？ 因为QComboBox 有两个重载的信号，原型分别是void\tcurrentTextChanged(const QString \u0026amp;text)和void\tcurrentIndexChanged(int index)，上面的代码我们明显是想要连接void\tcurrentIndexChanged(int index)这个信号，但是编译器当成了void\tcurrentTextChanged(const QString \u0026amp;text)这个信号，所以会报出参数不匹配的错误:matching member function for call to 'connect' connect(ui-\u0026gt;styleComboBox, \u0026amp;QComboBox::currentIndexChanged, this, [this](int index) 。 上面的代码在Qt 6上不会报错。\n我们该如何解决呢？只需使用预定义宏，将比Qt 6.0.0版本低的部分的代码改成QOverload\u0026lt;int\u0026gt;::of(\u0026amp;QComboBox::currentIndexChanged)即可：\n1 2 3 4 5 6 7 8 9 10 11 12 13 #if QT_VERSION \u0026lt;= QT_VERSION_CHECK(6, 0, 0) connect(ui-\u0026gt;styleComboBox, QOverload\u0026lt;int\u0026gt;::of(\u0026amp;QComboBox::currentIndexChanged), this, [this](int index) { //TODO: change theme spdlog::info(\u0026#34;User change default theme to {}\u0026#34;, index); AppConfig::instance().setBasic(\u0026#34;style\u0026#34;, index); }); #else connect(ui-\u0026gt;styleComboBox, \u0026amp;QComboBox::currentIndexChanged, this, [this](int index) { //TODO: change theme spdlog::info(\u0026#34;User change default theme to {}\u0026#34;, index); AppConfig::instance().setBasic(\u0026#34;style\u0026#34;, index); }); #endif ","date":"2024-09-16T00:00:00Z","image":"https://programcx.github.io/p/qt-5.15-connect-%E5%87%BD%E6%95%B0%E7%9A%84%E4%B8%80%E4%B8%AA%E5%A4%A7%E5%9D%91/qt_hu16901985884131608922.webp","permalink":"https://programcx.github.io/p/qt-5.15-connect-%E5%87%BD%E6%95%B0%E7%9A%84%E4%B8%80%E4%B8%AA%E5%A4%A7%E5%9D%91/","title":"Qt 5.15 connect() 函数的一个大坑"},{"content":"Java中super关键字的理解 访问超类的成员函数和变量 举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Main { public static void main(String[] args) { Manager manager = new Manager(); manager.setBonus(8000); System.out.println(manager.getSalary()); } } class Employee{ private int salary = 6000; public void setSalary(int salary){ this.salary=salary; } public int getSalary(){ return salary; } } class Manager extends Employee{ private int bonus = 0; public void setBonus(int bonus){ this.bonus=bonus; } public int getBonus(){ return bonus; } //覆盖超类的函数 @Override public int getSalary(){ return bonus + this.salary; } } 这段代码声明了超类Employee，以及继承了这个超类的Manager类。定义了Manager类的manager对象，manager的奖金设置为8000元。最后打印出manager的总薪水。 但是，这段代码有一个很严重的错误。由于在超类Employee中，salary为private类型的，所以manager无法访问salary的值。为了遵循数据的隐蔽性原则，我们不能将它定义为public类型。不过，我们可以借助super关键字来达到目的。\n以下是修改过的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Main { public static void main(String[] args) { Manager manager = new Manager(); manager.setBonus(8000); System.out.println(manager.getSalary()); } } class Employee{ private int salary = 6000; public void setSalary(int salary){ this.salary = salary; } //获得salary的值 public int getSalary(){ return salary; } } class Manager extends Employee{ private int bonus = 0; public void setBonus(int bonus){ this.bonus = bonus; } public int getBonus(){ return bonus; } @Override public int getSalary(){ return bonus + super.getSalary();\t//改为 super.getSalary } } 在这段代码中，增加了getSalary()函数，这个函数返回salary的值。由于可以使用关键字super访问超类的成员函数和变量，super.getSalary()就获得了Employee类的salary变量。\n调用父类的构造方法 举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Main { public static void main(String[] args) { Manager manager = new Manager(8000); manager.setBonus(800); System.out.println(manager.getSalary()); } } class Employee{ public Employee(int salary){ this.salary = salary; } private int salary = 6000; public void setSalary(int salary){ this.salary = salary; } public int getSalary(){ return salary; } } class Manager extends Employee{ private int bonus = 0; public Manager(int salary) { super(salary);\t//调用超类的构造器 } public void setBonus(int bonus){ this.bonus = bonus; } public int getBonus(){ return bonus; } @Override public int getSalary(){ return bonus + super.getSalary(); } } manger无法访问超类的私有成员，但是可以通过超类的构造器来访问它们。\n注意：超类的构造器必须是子类的构造器里面的第一个语句！\n解决成员变量名或方法名冲突 举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 class Parent { int x = 10; } class Child extends Parent { int x = 20; void display() { System.out.println(\u0026#34;Child x = \u0026#34; + x); // 访问 Child 类的 x System.out.println(\u0026#34;Parent x = \u0026#34; + super.x); // 使用 super 访问 Parent 类的 x } } 当子类中的成员变量名或方法名与超类中的成员变量名或方法名相同时，子类中的方法或变量会隐藏超类中的方法或变量。此时，如果你想在子类中访问被隐藏的超类成员，就需要使用super 关键字。\n","date":"2024-09-09T00:00:00Z","image":"https://programcx.github.io/p/java%E4%B8%ADsuper%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E7%90%86%E8%A7%A3/java_hu14469118171915315630.jpg","permalink":"https://programcx.github.io/p/java%E4%B8%ADsuper%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E7%90%86%E8%A7%A3/","title":"Java中super关键字的理解"},{"content":"C++代码规范总结（部分） 下面内容总结（有的是直接复制概述，因为原文讲的比较严谨）于https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/\n头文件 #include的路径和顺序（每个类型之间要有空行） 配套的头文件 C语言系统文件 C++标准库头文件 其他库的头文件 本项目的头文件 要有#define防护符 避免向前声明 10行以下用内联函数 作用域 禁止使用using namespace ...; 大型项目要使用命名空间，以注释结尾 尽量将非成员函数放入命名空间，不要使用全局函数 最好初始化时声明 要在循环外声明类 尽量不要使用全局的静态变量、类，除非它们可以平凡地析构 构造函数中不得使用虚函数， 尽量不要使用隐式转换类型，单参数函数使用explicit关键字 int、double等类型建议使用传值、std::string等类型建议使用传引用，尽量不要传指针 基类最好是抽象类，且构造函数和析构函数必须声明为protected，最好不要继承具体类 只能用 struct 定义那些用于储存数据的被动对象. 其他情况应该使用 class 组合优于继承，使用public继承 谨慎使用重载运算符，除非与直觉相同；禁止使用自定义字面量 必须将所有数据成员声明为private 声明次序 public protected private 以下是每一个不同作用域的声明次序： 类型和类型别名 （仅适用于结构体），非静态数据成员 静态常量 工厂函数 构造函数和赋值运算符 析构函数 所有其他函数，包括静态和非静态函数，以及友元函数 其它数据成员，包括静态和非静态的 函数 行数超过40行在不影响程序逻辑的情况下要进行分割 只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法 尽量不使用缺省函数参数，少数极端情况除外。尽可能改用函数重载 允许合理的使用友元类及友元函数 其它C++特性 尽量不使用 C++ 异常. 不要使用强制转换 除了写日志，尽量不要使用流 对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符 任何可能的情况下都要使用 const 使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之 指针使用nullptr，字符使用'\\0'，而不是0字面量 尽可能用 sizeof(varname) 代替 sizeof(type) 使用auto绕过繁琐的类型名称，但要可读性好 可以用列表初始化 使用预处理宏时要非常谨慎，下面是要使用宏时需要遵守的规则： 不要在头文件中使用宏 在马上要使用时才进行 #define, 使用后要立即#undef 不要只是对已经存在的宏使用#undef，选择一个不会冲突的名称 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为 不要用 ## 处理函数，类和变量的名字 适当使用lambda表达式，当lambda将转移当前作用域时，首选显式捕捉 不要使用复杂的模板编程 命名约定 函数命名, 变量命名, 文件命名要有描述性; 少用缩写 文件名全部要小写，可以包含“-”或“_” 类型名称首字母要大写，不能有下划线 变量命名要全部小写，单词之间使用下划线 常规函数使用大小写混合 命名空间要小写，不可以出现缩写 枚举的命名应当和常量或宏一致 通常不应该使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线 注释 在每一个文件开头加入版权公告，包含法律、版权、作者信息。应当对文件的内容做一个大致的说明, 同时说明各概念之间的联系。 每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显 函数声明处的注释描述函数功能; 定义处的注释描述函数实现 每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途,如果变量可以接受 NULL 或 -1 等警戒值, 须加以说明 所有全局变量也要注释说明含义及用途, 以及作为全局变量的原因 对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释 巧妙或复杂的代码段前要加注释 比较隐晦的地方要在行尾加入注释 永远不要 用自然语言翻译代码作为注释 对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释，并加上自己身份标识 格式 每一行代码字符数不超过 80 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码 只使用空格, 每次缩进 2 个空格 返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用 一致 如果能增强可读性, 简短的条件语句允许写在同一行. 只有当语句简单并且没有使用 else 子句时使用 单行语句不需要使用大括号, 如果你喜欢用也没问题; 复杂的条件或循环语句用大括号可读性会更好. 也有一些项目要求 if 必须总是使用大括号 如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配，如果 default 应该永远执行不到, 简单的加条 assert 在单语句循环里, 括号可用可不用 空循环体应使用 {} 或 continue, 而不是一个简单的分号 句点或箭头前后不要有空格. 指针/地址操作符 (*, \u0026amp;) 之后不能有空格 如果一个布尔表达式超过 标准行宽, 断行方式要统一一下 预处理指令不要缩进, 从行首开始 构造函数初始化列表放在同一行或按四格缩进并排多行 命名空间内容不缩进 两个函数定义之间的空行不要超过 2 行, 函数体首尾不要留空行, 函数体中也不要随意添加空行 规则特例 不要使用 #pragma once ","date":"2024-09-05T00:00:00Z","image":"https://programcx.github.io/p/c-/cpp_hu17877837520322365017.jpg","permalink":"https://programcx.github.io/p/c-/","title":"C++ 代码规范总结"},{"content":"该文章用来记录java学习过程中看似应该不会发生的异常\n我们有一个 Main.java 和 cn/pinsoftstd/Study.java，源代码如下：\nMain.java:\n1 2 3 4 5 6 7 8 9 10 11 12 import cn.pinsoftstd.study.Study; public class Main { public static void main(String[] args) { Study study=new Study(); study.printHelloWorld(); study.addProgramming(\u0026#34;XiaoMing\u0026#34;); study.addProgramming(\u0026#34;XiaoHong\u0026#34;); study.addProgramming(\u0026#34;LiJun\u0026#34;); study.removeProgramming(\u0026#34;LiJun\u0026#34;);\t//发生异常 study.printProgramingNames(); } } cn/pinsoftstd/Study.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package cn.pinsoftstd.study; import java.util.ArrayList; import java.util.Iterator; /** * The class study provides an interface for developers to add and remove * the ones who are studying programming or not. * @author Program * @version 1.0 */ public class Study { public Study(){ } /** This prints \u0026#34;Hello World\u0026#34;,which is the person who must print once * study a programming language. * @author Program */ public void printHelloWorld(){ System.out.println(\u0026#34;Hello World\u0026#34;); } /** * This method adds a person to the Study Programming category. */ public Programming addProgramming(String name){ Programming pr=new Programming(name); programmings.add(pr); return pr; } /** * This method removes a person from the Study Programming category. */ public boolean removeProgramming(String name) { boolean removed = false; for (Programming pr : programmings) { if (pr.name.equals(name)) { programmings.remove(pr);\t//发生异常 removed = true; } } return removed; } /** * This method prints the names of people who are studying programming. */ public void printProgramingNames(){ for(Programming pr:programmings){ System.out.println(pr.name); } } private ArrayList\u0026lt;Programming\u0026gt; programmings=new ArrayList\u0026lt;\u0026gt;(); public class Programming{ private String name; public Programming(String name){ this.name=name; } public String name(){ return this.name; } public void printStudyingProgramming(){ System.out.println(\u0026#34;Study Programming:\u0026#34;+name); } } } 自己写的时候感觉没有问题，结果运行的时候抛出了 java.ConcurrentModificationException 异常。异常代码为Main.java中的 study.printProgramingNames();，Study.java中的programmings.remove(pr);。 其实，是因为在 Java 中增强型for循环使用了迭代器(iterator)遍历，如果在遍历的时候使用了 study.printProgramingNames();，迭代器会检测到数组列表的变化，从而抛出java.util.ConcurrentModificationException 异常。\n解决方法 方法一：使用迭代器遍历programmings数组列表，不使用增强型for循环遍历。下面是修改后的代码： 1 2 3 4 5 6 7 8 9 10 11 12 public boolean removeProgramming(String name) { boolean removed=false; Iterator\u0026lt;Programming\u0026gt;it=programmings.iterator(); while (it.hasNext()){\t//使用迭代器遍历 Programming pr=it.next(); if(pr.name.equals(name)){ it.remove(); removed=true; } } return removed; } 方法二：通过遍历索引(index)来遍历数组列表programmings。下面是修改后的代码:\n1 2 3 4 5 6 7 8 9 10 public boolean removeProgramming(String name) { boolean removed = false; for(int i=0;i\u0026lt;programmings.size();i++){ if(programmings.get(i).name.equals(name)){ programmings.remove(i); removed = true; } } return removed; } ","date":"2024-09-05T00:00:00Z","image":"https://programcx.github.io/p/java/java_hu14469118171915315630.jpg","permalink":"https://programcx.github.io/p/java/","title":"Java 一种 java.util.ConcurrentModificationException 异常原因"},{"content":"这个博客用来回忆这道题目，本人算法新手\n题目： 在给定的 m x n 网格 grid 中，每个单元格可以有以下三个值之一：\n值 0 代表空单元格； 值 1 代表新鲜橘子； 值 2 代表腐烂的橘子。 每分钟，腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。\n返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能，返回 -1 。\n示例 1： 输入：grid = [[2,1,1],[1,1,0],[0,1,1]] 输出：4\n示例 2： 输入：grid = [[2,1,1],[0,1,1],[1,0,1]] 输出：-1 解释：左下角的橘子（第 2 行， 第 0 列）永远不会腐烂，因为腐烂只会发生在 4 个方向上。\n示例 3： 输入：grid = [[0,2]] 输出：0 解释：因为 0 分钟时已经没有新鲜橘子了，所以答案就是 0 。\n提示：\nm == grid.length n == grid[i].length 1 \u0026lt;= m, n \u0026lt;= 10 grid[i][j] 仅为 0、1 或 2 解题思路 先遍历所有的网格，统计新鲜橘子的个数，并把腐烂的橘子添加到队列。 使用广度优先搜索（BFS），不断感染四周的橘子，将已经感染过其他橘子的烂橘子从队列中弹出（便于统计还有哪些橘子没有感染其他好橘子）。每感染一个橘子，将新鲜橘子个数减去一。每感染一轮，时间加一。 最后检测新鲜的橘子个数是否为零，如果为零，则返回总共花去的分钟数；如果新鲜的橘子个数不为零，说明还有橘子永远不会被感染，根据题目要求，返回-1。 解题代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Solution { public: int orangesRotting(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; grid) { int m = grid.size();\t//记录每一行的橘子个数 if (m == 0) return -1; //如果没有橘子，直接返回-1 int n = grid[0].size(); //记录每一列橘子个数 if (n == 0) return -1; queue\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; q; int fresh = 0; //遍历所有网格 for (int i = 0; i \u0026lt; m; ++i) { for (int j = 0; j \u0026lt; n; ++j) { if (grid[i][j] == 1) {\t//如果为新鲜橘子 fresh++;\t//新鲜橘子个数就加一 } if (grid[i][j] == 2) { //如果橘子不是新鲜的 q.emplace(i, j);\t//就把烂橘子放入队列 } } } //开始广度优先搜索（BFS） int minutes = 0; vector\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; dirs = { {1,0},{-1,0},{0,1},{0,-1} }; //定义四个方向 while (!q.empty() \u0026amp;\u0026amp; fresh \u0026gt; 0) {\t//如果队列q不为空且有新鲜橘子 int size = q.size();\t//记录才开始或几轮下来还有多少未感染其他橘子的烂橘子 for (int k = 0; k \u0026lt; size; ++k) {\t//对这些橘子进行遍历 auto [i, j] = q.front(); //获取队列中第一个未感染其它橘子的烂橘子 q.pop();\t//将该橘子弹出 for (const auto\u0026amp; [x, y] : dirs) {\t//遍历四个方向 int dx = i + x, dy = j + y;\t//计算烂橘子四周的坐标值 if (dx \u0026gt;= 0 \u0026amp;\u0026amp; dx \u0026lt; m \u0026amp;\u0026amp; dy \u0026gt;= 0 \u0026amp;\u0026amp; dy \u0026lt; n \u0026amp;\u0026amp; grid[dx][dy] == 1) {\t//如果在矩阵范围内且该橘子是好橘子（避免重复感染和越界问题） grid[dx][dy] = 2;\t//就感染这个橘子 q.emplace(dx, dy); //将这个橘子添加到未感染其它橘子的烂橘子的队列 fresh--;\t//好橘子数量就减一 } } } ++minutes; //计算时间 } return fresh == 0 ? minutes : -1; } }; 提交结果 ","date":"2024-09-05T00:00:00Z","image":"https://programcx.github.io/p/leetcode-%E7%AC%AC994%E9%A2%98-%E8%85%90%E7%83%82%E7%9A%84%E6%A9%98%E5%AD%90/question_hu920221992150309940.png","permalink":"https://programcx.github.io/p/leetcode-%E7%AC%AC994%E9%A2%98-%E8%85%90%E7%83%82%E7%9A%84%E6%A9%98%E5%AD%90/","title":"LeetCode 第994题 腐烂的橘子"},{"content":"记录学习\n填涂颜色 题目描述 由数字 $0$ 组成的方阵中，有一任意形状的由数字 $1$ 构成的闭合圈。现要求把闭合圈内的所有空间都填写成 $2$。例如：$6\\times 6$ 的方阵（$n=6$），涂色前和涂色后的方阵如下：\n如果从某个 $0$ 出发，只向上下左右 $4$ 个方向移动且仅经过其他 $0$ 的情况下，无法到达方阵的边界，就认为这个 $0$ 在闭合圈内。闭合圈不一定是环形的，可以是任意形状，但保证闭合圈内的 $0$ 是连通的（两两之间可以相互到达）。\n1 2 3 4 5 6 0 0 0 0 0 0 0 0 0 1 1 1 0 1 1 0 0 1 1 1 0 0 0 1 1 0 0 1 0 1 1 1 1 1 1 1 1 2 3 4 5 6 0 0 0 0 0 0 0 0 0 1 1 1 0 1 1 2 2 1 1 1 2 2 2 1 1 2 2 1 2 1 1 1 1 1 1 1 输入格式 每组测试数据第一行一个整数 $n(1 \\le n \\le 30)$。\n接下来 $n$ 行，由 $0$ 和 $1$ 组成的 $n \\times n$ 的方阵。\n方阵内只有一个闭合圈，圈内至少有一个 $0$。\n输出格式 已经填好数字 $2$ 的完整方阵。\n样例 #1 样例输入 #1 1 2 3 4 5 6 7 6 0 0 0 0 0 0 0 0 1 1 1 1 0 1 1 0 0 1 1 1 0 0 0 1 1 0 0 0 0 1 1 1 1 1 1 1 样例输出 #1 1 2 3 4 5 6 0 0 0 0 0 0 0 0 1 1 1 1 0 1 1 2 2 1 1 1 2 2 2 1 1 2 2 2 2 1 1 1 1 1 1 1 提示 对于 $100%$ 的数据，$1 \\le n \\le 30$。\n思路 1.输入矩阵 2.进行BFS搜索，标记搜索过的 3.输出，如果值为零且未被标记，则一定是被包围的，就输出2\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include \u0026lt;iostream\u0026gt; #include \u0026lt;queue\u0026gt; using namespace std; struct point { int x = 0; int y = 0; }; queue\u0026lt;point\u0026gt; q; int dx[4] = { -1,1,0,0 }, dy[4] = { 0,0,-1,1 }; int land[35][35] = {0}, mark[35][35] = {0}, n, tmp; int main() { cin \u0026gt;\u0026gt; n; //输入 for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; n; j++) { cin \u0026gt;\u0026gt; tmp; if (tmp == 1) { land[i][j] = tmp; } } } //把起始点标记为经过 mark[0][0] = 1; q.push(point{0,0}); //开始bfs搜索 while (!q.empty())\t//一直到搜索完成 { point p = q.front();\t//取出搜索队列的第一个元素 q.pop();\t//弹出第一个元素 //前后左右 for (int i = 0; i \u0026lt; 4; i++) { int x = dx[i] + p.x; int y = dy[i] + p.y; //如果越界，就跳出此次循环 if (x \u0026lt; 0 || x \u0026gt; n || y \u0026lt; 0 || y \u0026gt; n ) continue; //如果未被搜索且数值为零 if (land[x][y] == 0 \u0026amp;\u0026amp; mark[x][y] == 0) { mark[x][y] = 1;\t//标记为已经搜索 q.push(point{ x,y });\t//加入搜索队列 } } } cout \u0026lt;\u0026lt; endl; //遍历矩阵 for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; n; j++) { //如果没被搜索过 if (land[i][j] == 0 \u0026amp;\u0026amp; mark[i][j] == 0) { cout \u0026lt;\u0026lt; 2 \u0026lt;\u0026lt; \u0026#34; \u0026#34;;\t//说明被包围，则输出2 } else if (land[i][j] == 1) { cout \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; \u0026#34; \u0026#34;;\t//1就是边界 } else { cout \u0026lt;\u0026lt; 0 \u0026lt;\u0026lt; \u0026#34; \u0026#34;;\t//否则就是搜过的0 } } cout \u0026lt;\u0026lt; endl; } return 0; } ","date":"2024-09-05T00:00:00Z","permalink":"https://programcx.github.io/p/%E6%B4%9B%E8%B0%B7-p1162-%E5%A1%AB%E6%B6%82%E9%A2%9C%E8%89%B2-%E5%B9%BF%E5%BA%A6%E4%BC%98%E5%85%88%E6%90%9C%E7%B4%A2/","title":"洛谷 P1162 填涂颜色 广度优先搜索"}]