分布式锁CREATE TABLE `stock` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_code` varchar(20) NOT NULL, `warehouse` varchar(20) NOT NULL, `count` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_pc` (`product_code`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;1.JVM本地锁2.一个sql3.悲观锁：select ...from updatemysql悲观锁中使用行级锁：1.锁的查询或者更新条件必须是索引字段 2.查询或者更新条件必须是具体值 不能使用 like这样的模糊查询Mapper代码实现：@select("select * from stock where product_code = #{productCode} for update;") List<Stock> queryStock(String productCode);service实现：解决了锁定的范围 同时一个商品的多条库存记录 记录了库存变化前后的状态缺点问题： 1.性能问题 性能较低2.会产生死锁： 对多条数据加锁时，加锁的顺序要一致 3.库存操作要统一：用select ...for update 而用普通select锁不住4.mysql的乐观锁：时间戳version版本号CAS机制CREATE TABLE `stock` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_code` varchar(20) NOT NULL, `warehouse` varchar(20) NOT NULL, `count` int(11) NOT NULL, `version` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `idx_pc` (`product_code`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;如果version与mysql中的不相等就更新失败 再次进行尝试！servic实现：会出现栈溢出 还有超时问题！解决栈溢出问题MDL 更新删除操作 会进行加锁 就会出现超时问题去掉手动事务@Transactional（事务也会加锁） 后面当进行到 if（this....update...）时候 本来有悲观锁 当执行失败的就会放掉锁 后面就不会阻塞 最终：乐观锁存在的问题：1.高并发的情况下，性能极低。 （乐观锁适合读多写少的情况）2.CAS会产生ABA问题。3.读写分离的情况下导致乐观锁不可靠myqsl锁总结：性能：一个sql>悲观锁>JVM锁>乐观锁如果追求极致的性能，业务场景简单并不需要记录前后变化的情况下 优先选择：一个sql；如果写并发量低（读多），争抢不是很激烈的情况下 优先选择：乐观锁；如果写并发量较高，一般会经常冲突，此时选择乐观锁的话，会导致业务代码间的不断重试。 应该优先选择：悲观锁；不推荐使用JVM本地锁；
在Spring Security 中有两种资源放行的策略若是你但愿用户不用登陆就能访问，那么通常来讲，有两种配置策略：java第一种就是在 configure(WebSecurity web) 方法中配置放行，像下面这样：web@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode"); }第二种方式是在 configure(HttpSecurity http) 方法中进行配置：springhttp.authorizeRequests() .antMatchers("/hello").permitAll() .anyRequest().authenticated()两种方式最大的区别在于，第一种方式是不走 Spring Security 过滤器链，而第二种方式走 Spring Security 过滤器链，在过滤器链中，给请求放行。有的资源可使用第一种方式额外放行，不须要验证，例如前端页面的静态资源，就能够按照第一种方式配置放行。有的资源放行，则必须使用第二种方式，例如登陆接口。你们知道，登陆接口也是必需要暴露出来的，不须要登陆就能访问到的，可是咱们却不能将登陆接口用第一种方式暴露出来，登陆请求必需要走 Spring Security 过滤器链，由于在这个过程当中，还有其余事情要作。
一、string常用方法valueOf(基础数据类型 b) 将基础类型数据的文本转换为字符串 substring(int beginIndex) 切割原字符串，返回一个新的字符串， beginIndex：表示从下标为几的地方开始切割 replace(char oldChar, char newChar) 返回一个新的字符串，它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 trim() 返回字符串的副本，忽略前导空白和尾部空白。 isEmpty() 当且仅当 length() 为 0 时返回 true。 toLowerCase() 将此 String 中的所有字符都转换为小写。 toUpperCase() 将此 String 中的所有字符都转换为大写。 二、在字符串中查询指定子字符串的位置charAt(int index) 获取指定索引处的 char 值。没有则为null indexOf(string) 返回指定字符在此字符串中第一次出现处的索引。没有则为0 lastIndexOf(int ch) 返回指定字符在此字符串中最后一次出现处的索引。 lastIndexOf(int ch, int fromIndex) 返回指定字符在此字符串中最后一次出现处的索引，从指定的索引处开始进行反向搜索。 contains(CharSequence s) 判断字符串是否包含指定的 char 值，有则返回 true。三、比较两个字符串的内容equals(Object anObject) 将此字符串与指定的对象比较。 equalsIgnoreCase(String anotherString) 将此 String 与另一个 String 比较，不考虑大小写。四、判断是否以指定的字符开头或者结尾endsWith(String suffix) 判断此字符串是否以指定的后缀结束。 startsWith(String prefix) 判断此字符串是否以指定的前缀开始。五、将字符串转换为字符数组，和字节数组format(Locale l, String format, Object... args) 格式字符串和参数返回一个格式化字符串。 getBytes(String charset) 将string转换为byte 数组。 getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 将string转换为字符数组 toCharArray() 将此字符串转换为一个新的字符数组。六、正则表达式matches(String regex) 判断此字符串是否匹配给定的正则表达式。 replaceAll(String regex, String replacement) 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 replaceFirst(String regex, String replacement) 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 split(String regex) 根据给定正则表达式的匹配拆分此字符串。七、StringBuffer/StringBuilder类常用方法StringBuffer 上的主要操作是 append 和 insert 方法， append 方法始终将这些字符添加到缓冲区的末端；而 insert 方法则在指定的索引下添加字符。 StringBuffer(String str) 构造一个字符串缓冲区，并将其内容初始化为指定的字符串内容。 append(基础类型数据 b) 将 基础类型的字符串表示形式追加到序列后面。 charAt(int index) 返回此序列中指定索引处的 char 值。 delete(int start, int end) 移除此序列的子字符串中的字符。 deleteCharAt(int index) 移除此序列指定位置的 char。 insert(int offset, 基础类型数据 b) 将 基础类型参数的字符串表示形式插入指定位置中。 toString() 返回此序列中数据的字符串表示形式。
synchronized的变化①. java5以前,只有Synchronized,这个是操作系统级别的重量级操作,重量级锁,假如锁的竞争比较激烈的话,性能下降②. 在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁③. 为什么每一个对象都可以成为一个锁?Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高④. Mutex LockMonitor是在jvm底层实现的,底层代码是c++。本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间成本非常高。所以synchronized是Java语言中的一个重量级操作。⑤. Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,需要有个逐步升级的过程,别一开始就捅到重量级锁⑥. synchronized锁:由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略无锁偏向锁单个线程多次访问定义：①. 主要作用:当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁(偏向锁)同一个老顾客来访,直接老规矩行方便偏向锁为了解决只有在一个线程执行同步时提高性能②. 64位标记图再看(通过CAS方式修改markword中的线程ID)③. 偏向锁的理论在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的④. 技术实现一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID,无需再进入Monitor去竞争对象了。⑤. 对于如上的③、④进行细化锁对比偏向锁的操作不用直接捅到操作系统，不涉及用户到内核转换，不必要直接升级为最高级，我们以一个account对象的“对象头”为例，假如有一个线程执行到synchronized代码块的时候，JVMM使用CAS操作把线程指针ID记录到Mark Word当中，并修改标偏向标示，标示当前线程就获得该锁。锁对象变成偏向锁（通过CAS修改对象头里的锁标志位〉，字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后，线程并不会主动释放偏向锁。这时线程获得了锁，可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己（持有锁的线程ID也在对象头里)，JVM通过account对象的Mark Word判断:当前线程ID还在，说明还持有着这个对象的锁，就可以继续进入临界区工作。由于之前没有释放锁，这里也就不需要重新加锁。如果自始至终使用锁的线程风有一个，很明显偏向锁几乎没有额外开销，性能极高。结论:JVM不用和操作系统协商设置Mutex(争取内核)，它只需要记录下线程ID就标示自己获得了当前锁，不用操作系统接入。上述就是偏向锁:在没有其他线程竞争的时候，一直偏向偏心当前线程，当前线程可以一直执行。实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟, 4秒所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0关闭偏向锁:关闭之后程序默认会直接进入轻量级锁状态。-XX:-UseBiasedLockingpublic class MyObject{ public static void main(String[] args){ Object o = new Object(); new Thread(() -> { synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } },"t1").start(); } } //查看状态此时情况下偏向锁4秒后开启偏向锁的撤销偏向锁的撤销(偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行)第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁 ,重新偏向(我的理解是,其实如果线程A执行完毕,如果不再去竞争,那么就会重新线程B为偏向锁;如果线程A继续竞争,那么就会CAS自旋 也就升级到了轻量级锁)轻量级锁多线程竞争，但是任意时刻最多只有一个线程竞争，即不存在锁竞争太过激烈的情况，也就没有线程阻塞。有线程来参与锁的竞争,但是获取锁的冲突时间极短（本质就是自旋锁）轻量级锁的获取如果关闭偏向锁,就可以直接进入轻量级锁 -XX:-UseBiasedLocking自旋达到一定次数和程度没有获取则会升级锁java6之前(了解):默认启用,默认情况下自旋的次数是10次,-XX:PreBlockSpin=10来修改或者自旋线程数超过cpu核数一半Java6之后:自适应(自适应意味着自旋的次数不是固定不变的),而是根据:同一个锁上一次自旋的时间和拥有锁线程的状态来决定。重锁①. 有大量的线程参与锁的竞争,冲突性很高②. 锁标志位锁升级后，hashcode值无锁地址在31位偏向锁没有，需要获取的时候需要锁升级。当一个对象已经计算过identity hash code 它就无法进入到偏向锁状态时跳过偏向锁，直接生成轻量级锁。偏向锁过程中遇到一致性哈希计算请求，立马撤销偏向模式，膨胀为重量级锁锁的优缺点synchronized锁升级过程总结:一句话,就是先自旋,不行再阻塞。 实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。. 偏向锁、轻量级锁、重量级锁总结偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁锁消除/ 锁粗化锁消除:从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用** * 锁消除 * 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用, * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用 */ public class LockClearUPDemo{ static Object objectLock = new Object();//正常的 public void m1(){ //锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的 Object o = new Object(); synchronized (o){ System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode()); } } public static void main(String[] args){ LockClearUPDemo demo = new LockClearUPDemo(); for (int i = 1; i <=10; i++) { new Thread(() -> { demo.m1(); },String.valueOf(i)).start(); } } } /** * 锁粗化 * 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块, * 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能 */ public class LockBigDemo { static Object objectLock = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (objectLock) { System.out.println("11111"); } synchronized (objectLock) { System.out.println("22222"); } synchronized (objectLock) { System.out.println("33333"); } },"a").start(); new Thread(() -> { synchronized (objectLock) { System.out.println("44444"); } synchronized (objectLock) { System.out.println("55555"); } synchronized (objectLock) { System.out.println("66666"); } },"b").start(); } }
volatile的特性可见性:可见性：保证不同线程对某个变量完成操作后结果及时可见，即该共享变量一旦改变所有线程立即可见诉求:1.线程中修改了自己工作内存中的副本之后，立即将其刷新到主内存;2.工作内存中每次读取共享变量时，都去主内存中重新读取，然后拷贝到工作内存。使用volatile修饰共享变量，就可以达到上面的效果，被volatile修改的变量有以下特点:1.线程中读取的时候，每次读取都会去主内存中读取共享变量最新的值，然后将其复制到工作内存2.线程中修改了工作内存中变量的副本，修改之后会立即刷新到主内存没有原子性：volatile变量不适合参与到依赖当前值的运算，如i =i+ 1; i++;之类的那么依靠可见性的特点volatile可以用在哪些地方呢?通常volatile用做保存某个状态的boolean值or int值。由于volatile变量只能保证可见性，在不符合以下两条规则的运算场景中，我们仍然要通过加锁(使用synchronized、java.util.concurrent中的锁或原子类）来保证原子性·运算结果并不依赖变量的当前值，或者能够确保只有单一的线程修改变量的值.·变量不需要与其他的状态变量共同参与不变约束。禁止指令重排重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段，有时候会改变程序语句的先后顺序不存在数据依赖关系，可以重排序;存在数据依赖关系，禁止重排序四种内存屏障策略内存屏障︰是一种屏障指令，它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也叫内存栅栏或栅栏指令作用：阻止屏障两边的指令重排序写数据时加入屏障，强制将线程私有工作内存的数据刷回主物理内存读数据时加入屏障，线程私有工作内存的数据失效，重新到主物理内存中获取最新的数据happens-before之volatile变量规则①.当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前②.当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后③.当第一个操作为volatile写时,第二个操作为volatile读时,不能重排①. 写在每个volatile写操作的前⾯插⼊⼀个StoreStore屏障在每个volatile写操作的后⾯插⼊⼀个StoreLoad屏障写指令：②. 读在每个volatile读操作的后⾯插⼊⼀个LoadLoad屏障在每个volatile读操作的后⾯插⼊⼀个LoadStore屏障读指令：数据依赖性:若两个操作访问同一变量，且这两个操作中有一个为写操作，此时两操作间就存在数据依赖性。volatile的用法状态标志，判断业务是否结束使用:作为一个布尔状态标志，用于指示发生了一个重要的一次性事件，例如完成初始化或任务结 理由:状态标志并不依赖于程序内任何其他状态，且通常只有一种状态转换例子:判断业务是否结京public class UseVolatileDemo{ private volatile static boolean flag = true; public static void main(String[] args){ new Thread(() -> { while(flag) { //do something...... } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { flag = false; },"t2").start(); } } 开销较低的读,写锁策略public class UseVolatileDemo{ /** * 使用:当读远多于写，结合使用内部锁和 volatile 变量来减少同步的开销 * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性 */ public class Counter{ private volatile int value; public int getValue(){ return value; //利用volatile保证读取操作的可见性 } public synchronized int increment(){ return value++; //利用synchronized保证复合操作的原子性 } } } 单列模式 DCL双端锁的发布public class SafeDoubleCheckSingleton{ //通过volatile声明，实现线程安全的延迟初始化。 private volatile static SafeDoubleCheckSingleton singleton; //私有化构造方法 private SafeDoubleCheckSingleton(){ } //双重锁设计 public static SafeDoubleCheckSingleton getInstance(){ if (singleton == null){ //1.多线程并发创建对象时，会通过加锁保证只有一个线程能创建对象 synchronized (SafeDoubleCheckSingleton.class){ if (singleton == null){ //隐患：多线程环境下，由于重排序，该对象可能还未完成初始化就被其他线程读取 //原理:利用volatile，禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序 singleton = new SafeDoubleCheckSingleton(); } } } //2.对象创建完毕，执行getInstance()将不需要获取锁，直接返回创建对象 return singleton; } } 原因：(1). DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象 可能没有完成初始化instance=new SingletonDem(); 可以分为以下步骤(伪代码)memory=allocate();//1.分配对象内存空间instance(memory);//2.初始化对象instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null(2). 步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.memory=allocate();//1.分配对象内存空间instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.instance(memory);//2.初始化对象(3). 但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题(4). 我们使用volatile禁止instance变量被执行指令重排优化即可private volatile static SafeDoubleCheckSingleton singleton;采用静态内部类的方式实现public class SingletonDemo { private SingletonDemo() { } private static class SingletonDemoHandler { private static SingletonDemo instance = new SingletonDemo(); } public static SingletonDemo getInstance() { return SingletonDemoHandler.instance; } public static void main(String[] args) { for (int i = 0; i <10 ; i++) { new Thread(()->{ SingletonDemo instance = getInstance(); // 可以知道这里获取到的地址都是同一个 System.out.println(instance); },String.valueOf(i)).start(); } } }
乐观锁和悲观锁1、 悲观锁(synchronized关键字和Lock的实现类都是悲观锁)什么是悲观锁？认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改适合写操作多的场景,先加锁可以保证写操作时数据正确(写操作包括增删改)、显式的锁定之后再操作同步资源synchronized关键字和Lock的实现类都是悲观锁2、 乐观锁概念:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作乐观锁在Java中通过使用无锁编程来实现，最常采用的时CAS算法,Java原子类中的递增操作就通过CAS自旋实现的适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅度提升乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现) //悲观锁的调用方式 public synchronized void m1(){ //加锁后的业务逻辑 } //保证多个线程使用的是同一个lock对象的前提下 ReetrantLock lock=new ReentrantLock(); public void m2(){ lock.lock(); try{ //操作同步资源 }finally{ lock.unlock(); } } //乐观锁的调用方式 //保证多个线程使用的是同一个AtomicInteger private AtomicInteger atomicIntege=new AtomicInteger(); atomicIntege.incrementAndGet();对象锁/类锁【强制】高并发时，同步调用应该去考量锁的性能损耗。能用无锁数据结构，就不要用锁;能锁区块，就不要锁整个方法体﹔能用对象锁，就不要用类锁。说明︰尽可能使加锁的代码块工作量尽可能的小，避免在锁代码块中调用RPC方法。作用于实例方法，当前实例加锁，进入同步代码前要获得当前实例的锁;作用于代码块，对括号里配置的对象加锁。作用于静态方法，当前类加锁，进去同步代码前要获得当前类对象的锁;//入口 public static void main(string[] args){ Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone. sendEmail(); }, name: "a" ).start(); //暂停毫秒,保证a线程先启动 try { TimeUnit.ILLISECONDs.sleep( timeout 200); } catch (InterruptedException e) { e.printstackTrace(); } new Thread(() -> { phone.sendSMS( ); // phone.hello(); // phone2.sendSMS(); }, name: "b" ).start(); } class Phone{ public synchronized void sendEmail(){ try { TimeUnit.SECONDs.sleep( timeout: 3); } catch (InterruptedException e) { e.printStackTrace(); } system.out.println( "-----sendEmail" ); } public synchronized void sendsMs(){ system.out.println( "-----sendSMS" ); } public void hello(){ system.out.println( "-----hello" ); } }*一个对象里面如果有多个synchronized方法，某一个时刻内，只要一个线程去调归其中的一个synchronized方法了，*其它的线程都只能等待，换句话说，某一个时刻内，只能有唯一的一个线程去访问这些synchronized方法*锁的是当前对象this，被锁定后，其它的线程都不能进入到当前对象的其它的synchronized方法//入口 public static void main(string[] args){ Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone. sendEmail(); }, name: "a" ).start(); //暂停毫秒,保证a线程先启动 try { TimeUnit.ILLISECONDs.sleep( timeout 200); } catch (InterruptedException e) { e.printstackTrace(); } new Thread(() -> { // phone.sendSMS( ); phone.hello(); // phone2.sendSMS(); }, name: "b" ).start(); } class Phone{ public synchronized void sendEmail(){ try { TimeUnit.SECONDs.sleep( timeout: 3); } catch (InterruptedException e) { e.printStackTrace(); } system.out.println( "-----sendEmail" ); } public synchronized void sendsMs(){ system.out.println( "-----sendSMS" ); } public void hello(){ system.out.println( "-----hello" ); } } -----hello -----sendEmail//入口 public static void main(string[] args){ Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone. sendEmail(); }, name: "a" ).start(); //暂停毫秒,保证a线程先启动 try { TimeUnit.ILLISECONDs.sleep( timeout 200); } catch (InterruptedException e) { e.printstackTrace(); } new Thread(() -> { // phone.sendSMS( ); // phone.hello(); phone2.sendSMS(); }, name: "b" ).start(); } class Phone{ public synchronized void sendEmail(){ try { TimeUnit.SECONDs.sleep( timeout: 3); } catch (InterruptedException e) { e.printStackTrace(); } system.out.println( "-----sendEmail" ); } public synchronized void sendsMs(){ system.out.println( "-----sendSMS" ); } public void hello(){ system.out.println( "-----hello" ); } } -----sendSMS -----sendEmail*加个普通方法后发现和同步锁无关*换成两个对象后，不是同一把锁了，情况立刻变化。//入口 public static void main(string[] args){ Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone. sendEmail(); }, name: "a" ).start(); //暂停毫秒,保证a线程先启动 try { TimeUnit.ILLISECONDs.sleep( timeout 200); } catch (InterruptedException e) { e.printstackTrace(); } new Thread(() -> { phone.sendSMS( ); // phone.hello(); // phone2.sendSMS(); }, name: "b" ).start(); } class Phone{ public static synchronized void sendEmail(){ try { TimeUnit.SECONDs.sleep( timeout: 3); } catch (InterruptedException e) { e.printStackTrace(); } system.out.println( "-----sendEmail" ); } public static synchronized void sendsMs(){ system.out.println( "-----sendSMS" ); } public void hello(){ system.out.println( "-----hello" ); } } -----sendEmail -----sendSMS//入口 public static void main(string[] args){ Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone. sendEmail(); }, name: "a" ).start(); //暂停毫秒,保证a线程先启动 try { TimeUnit.ILLISECONDs.sleep( timeout 200); } catch (InterruptedException e) { e.printstackTrace(); } new Thread(() -> { // phone.sendSMS( ); // phone.hello(); phone2.sendSMS(); }, name: "b" ).start(); } class Phone{ public static synchronized void sendEmail(){ try { TimeUnit.SECONDs.sleep( timeout: 3); } catch (InterruptedException e) { e.printStackTrace(); } system.out.println( "-----sendEmail" ); } public static synchronized void sendsMs(){ system.out.println( "-----sendSMS" ); } public void hello(){ system.out.println( "-----hello" ); } } -----sendEmail -----sendSMS**都换成静态同步方法后，情况又变化三种synchronized 锁的内容有一些差别:*对于普通同步方法，锁的是当前实例对象，通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁—>实例对象本身，*对于静态同步方法，锁的是当前类的Class对象，如Phone.class唯一的一个模板。 Phone phone = new Phone(); Phone phone2 = new Phone(); //对于静态同步方法 看的是当前的类 比竟：static关键字修饰的方法，又叫类方法.属于类的，不属于对象， 在实例化对象之前就可以通过类名.方法名调用静态方法。 public static synchronized void sendEmail(){}; //static synchronized 是属于类锁 public synchronized void sendsMs(){}; //synchronize 是属于对象锁 //静态方法static（类方法）和 非静态方法（实例方法）的区别： //方法我们主要分为三种： //1.构造方法 //2.非静态方法（普通方法/实例方法） //3.静态方法（类方法） //一、静态方法和非静态方法的区别（调用对象、引用变量不同） //静态方法:是使用static关键字修饰的方法，又叫类方法.属于类的，不属于对象， 在实例化对象之前就可以通过类名.方法名调用静态方法。 （静态属性，静态方法都是属于类的，可以直接通过类名调用）。 //A.在静态方法中，可以调用静态方法。 //B.在静态方法中，不能调用非静态方法。 //C.在静态方法中，可以引用类变量（即，static修饰的变量）。 //D.在静态方法中，不能引用成员变量（即，没有static修饰的变量）。 //E.在静态方法中，不能使用super和this关键字 //非静态方法:是不含有static关键字修饰的普通方法，又称为实例方法，成员方法。属于对象的，不属于类的。（成员属性，成员方法是属于对象的，必须通过new关键字创建对象后，再通过对象调用）。 //A.在普通方法中，可以调用普通方法。 //B.在普通方法中，可以调用静态方法 //C.在普通方法中，可以引用类变量和成员变量 //D.在普通方法中，可以使用super和this关键字 //二、静态方法和非静态方法的区别（调用方法不同） //静态方法可以直接调用，类名调用和对象调用。（类名.方法名 / 对象名.方法名） //但是非静态方法只能通过对象调用。（对象名.方法名） //三、静态方法和非静态方法的区别（生命周期不同） //静态方法的生命周期跟相应的类一样长，静态方法和静态变量会随着类的定义而被分配和装载入内存中。一直到线程结束，静态属性和方法才会被销毁。（也就是静态方法属于类） //非静态方法的生命周期和类的实例化对象一样长，只有当类实例化了一个对象，非静态方法才会被创建，而当这个对象被销毁时，非静态方法也马上被销毁。（也就是非静态方法属于对象） //总结：类方法可以直接通过类名调用，实例方法必需先实例化类，再初始化对象，然后通过类的实例对象才能调用*对于同步方法块，锁的是synchronized 括号内的对象//入口 public static void main(string[] args){ Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone. sendEmail(); }, name: "a" ).start(); //暂停毫秒,保证a线程先启动 try { TimeUnit.ILLISECONDs.sleep( timeout 200); } catch (InterruptedException e) { e.printstackTrace(); } new Thread(() -> { phone.sendSMS( ); // phone.hello(); // phone2.sendSMS(); }, name: "b" ).start(); } class Phone{ public static synchronized void sendEmail(){ try { TimeUnit.SECONDs.sleep( timeout: 3); } catch (InterruptedException e) { e.printStackTrace(); } system.out.println( "-----sendEmail" ); } public synchronized void sendsMs(){ system.out.println( "-----sendSMS" ); } public void hello(){ system.out.println( "-----hello" ); } } -----sendSMS -----sendEmail //入口 public static void main(string[] args){ Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone. sendEmail(); }, name: "a" ).start(); //暂停毫秒,保证a线程先启动 try { TimeUnit.ILLISECONDs.sleep( timeout 200); } catch (InterruptedException e) { e.printstackTrace(); } new Thread(() -> { // phone.sendSMS( ); // phone.hello(); phone2.sendSMS(); }, name: "b" ).start(); } class Phone{ public static synchronized void sendEmail(){ try { TimeUnit.SECONDs.sleep( timeout: 3); } catch (InterruptedException e) { e.printStackTrace(); } system.out.println( "-----sendEmail" ); } public synchronized void sendsMs(){ system.out.println( "-----sendSMS" ); } public void hello(){ system.out.println( "-----hello" ); } } -----sendSMS -----sendEmail**当一个线程试图访问同步代码时它首先必须得到锁，正常退出或抛出异常时必须释放锁。*所有的普通同步方法用的都是同一把锁—实例对象本身，就是new出来的具体实例对象本身,本类this*也就是说如果一个实例对象的普通同步方法获取锁后，该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。*所有的静态同步方法用的也是同一把锁—类对象本身，就是我们说过的唯一模板class*具体实例对象this和唯一模板class，这两把锁是两个不同的对象，所以静态同步方法与普通同步方法之间是不会有竞态条件的*但是一旦一个静态同步方获取锁后，其他的静态同步方法都必须等待该方法释放锁后才能获取锁。公平锁和非公平锁公平锁：是指多个线程按照申请锁的顺序来获取锁类似排队打饭先来后到非公平锁：是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象注意：synchronized 和 ReentrantLock 默认是非公平锁为什么会有公平锁和非公平锁1恢复挂起的线程到真正锁的获取还是有时间差的，从开发人员来看这个时间微乎其微，但是从CPU的角度来看，这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片，尽量减少CPU空闲状态时间。2使用多线程很重要的考量点是线程切换的开销，当采用非公平锁时，当1个线程请求锁获取同步状态，然后释放同步状态，所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大，所以就减少了线程的开销。可重入锁又名递归锁是指在同一个线程在外层方法获取锁的时候，再进入该线程的内层方法会自动获取锁(前提，锁对象得是同一个对象)，不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁，可重入锁的一个优点是可一定程度避免死锁。显示锁/隐式锁隐式锁（即synchronized关键字使用的锁）默认是可重入锁显式锁（即Lock）也有ReentrantLock这样的可重入锁。new Thread(-> { lock.lock(); try{ system.out.println(Thread.currentThread().getName()+"\t ----come in外层调用"); lock.lock(); try{ system.out.println(Thread.currentThread().getName()+"\t ---come in内层调用"); }finally { lock.unLock( ); } }finally { lock. unlock(); } },name: "t1").start(); //加锁和释放锁次数一致，程序运行正常！ new Thread(-> { lock.lock(); try{ system.out.println(Thread.currentThread().getName()+"\t ----come in外层调用"); lock.lock(); try{ system.out.println(Thread.currentThread().getName()+"\t ---come in内层调用"); }finally { lock.unLock( ); } }finally { //由于加锁次数和释放次数不一样，第二个线程始终无法获取到锁，导致一直在等待。 //lock. unlock(); } },name: "t1").start(); new Thread(() -> { lock. lock(); try{ system.out.println(Thread.currentThread().getName()+"\t ----come in外层调用"); }finally { lock.unlock(); } }, name: "t2" ).start(); //加锁和释放锁次数不一致，线程t2不会执行，程序一直等待！死锁死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去，如果系统资源充足，进程的资源请求都能够得到满足，死锁出现的可能性就很低，否则就会因争夺有限的资源而陷入死锁。线程死锁描述的是这样一种情况：多个线程同时被阻塞，它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞，因此程序不可能正常终止。如下图所示，线程 A 持有资源 2，线程 B 持有资源 1，他们同时都想申请对方的资源，所以这两个线程就会互相等待而进入死锁状态。public class DeadLockDemo { private static Object resource1 = new Object();//资源 1 private static Object resource2 = new Object();//资源 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "线程 2").start(); } }小结指针指向monitor对象（也称为管程或监视器锁）的起始地址。每个对象都存在着一个monitor与之关联，当一个 monior被某个线程持有后，它便处于锁定状态。在Java虚拟机(HotSpot)中，monitor是由ObjectMonitor实现的，其主要数据结构如下〈位于HotSpot虚拟机源码 ObjectMonitor.hpp文件，C++实现的)
MybatisPlus快速入门使用第三方组件：1、导入对应的依赖2、研究依赖是如何配置3、代码的编写4、提高扩展技术能力！步骤创建数据库mybatis_plus创建User表DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) ); INSERT INTO user (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com'); 真是开发中，version（乐观锁）、deleted（逻辑删除）、gmt_create、gmt_modified编写项目。初始化项目！使用SpringBoot初始化导入依赖 <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency>使用MybatisPlus可以节省大量代码，尽量不要同时导入mybatis和mybatisplus 版本差异！、连接数据库 这一步和Mybatis相同！#mysql 5驱动不同 com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=juzi spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?userSSL=false&userUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mysql 8 驱动不同com.mysql.cj.jdbc.Driver 需要增加时区配置 serverTimezone=GMT%2B8pojo -dao(之前需要 连接mybatis 配置mapper.xml)-service-controller使用了Mybatis-Plus 之后pojo/** * @author: ZzRG * @version: 1.0 * Date: 2022/6/27 */ @Data @AllArgsConstructor @NoArgsConstructor public class User { private Long id; private String name; private Integer age; private String email; }mapper接口/** * @author: ZzRG * @version: 1.0 * Date: 2022/6/27 */ //在对应的Mapper上面继承基本的接口 BaseMapper public interface UserMapper extends BaseMapper<User> { // 所有的curd 操作编写完成了 // 不需要像以前那样写一大堆文件 }注意点：我们需要在主启动器上去扫描 我们的Mapper包下的所有接口 @MapperScan("com.zzrg.xuexi.mapper")测试类中测试@SpringBootTest class MybatisPlusApplicationTests { //继承了BaserMapper的所有方法都来自父类， 我们也可以写自己的方法 @Autowired private UserMapper userMapper; @Test void contextLoads() { //Wrapper<T>是一个参数 条件构造器 我们可以先不用设置为null //查询所有用户 List<User> users = userMapper.selectList(null); users.forEach(System.out::println); } }使用这里的关键所在1、SQL谁帮我们写的? MyBatis-Plus都写好了2、方法哪里来的? MyBatis-Plus 都写好了配置日志我们所有的sq|现在是不可见的,我们希望知道它是怎么执行的,所以我们必须要看日志!#配置日志 mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl配置完日志之后，在学习时候就需要注意这个自动生成的SQLCRUD扩展自动插入insert插入 @Test public void testInsert(){ User user = new User(); user.setName("ZzRG"); user.setAge(18); user.setEmail("123@qq.com"); int insert = userMapper.insert(user);//帮我们自动生成id System.out.println(insert);//受影响的行数 System.out.println(user);//发现id自动回填 }数据库插入的id的默认值：全局的唯一id主键生成策略默认 ID_WORKER全局唯一id分布式系统的唯一id生成：雪花算发：snowflake是Twitter开源的分布式ID生成算法，结果是一个long型的ID。其核心思想是︰使用41bit作为毫秒数，10bit作为机器的ID (5个bit是数据中心，5个bit的机器ID)，12bit作为毫秒内的流水号（意味着每个节点在每毫秒可以产生4096个ID），最后还有一个符号位，永远是0。主键自增：@TableId(type = IdType.AUTO)我们需要配置主键自增数据库字段一定要自增！与其他源码解释public enum IdType{ AUTO(0),//数据库id自增 NONE(1),//未设置主键 INPUT(2),//手动输入 ID_WORKER(3),//默认的全局唯一id UUID(4),//全局唯一id uuid ID_WORKER_STR(5);// ID_WORKER 字符串表示法 }更新操作 @Test public void testUpdate(){ User user = new User(); user.setId(5L); //自动拼接动态sql user.setAge(15) user.setEmail("123456@qq.com"); int update = userMapper.updateById(user); System.out.println(update); System.out.println(user); } }注意：所有的sql都是自动动态配置！自动填充创建时间、修改时间!这些个操作一遍都是自动化完成的，我们不希望手动更新!阿里巴巴开发手册：所有的数据库表：gmt_create、gmt_modified 几乎所有的表都套配置上！且需要自动化！方法一:数据库级别（工作中不允许使用）1、在表中新增字段create_time,update_time2、再次测试插入方法,我们需要先把实体类同步 private Date createTime; private Date updateTime;方式二:代码级别 //字段填充内容 @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; 编写处理器package com.zzrg.xuexi.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import javax.xml.crypto.Data; import java.util.Date; /** * @author: ZzRG * @version: 1.0 * Date: 2022/6/27 */ @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { //插入时的填充策略 @Override public void insertFill(MetaObject metaObject) { log.info("开始插入..."); //setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject) this.setFieldValByName("createTime", new Date(),metaObject); this.setFieldValByName("updateTime", new Date(),metaObject); } //更新时的填充策略 @Override public void updateFill(MetaObject metaObject) { log.info("开始更新..."); this.setFieldValByName("updateTime", new Date(),metaObject); } }插入测试乐观锁乐观锁：故名思意十分乐观，它总是认为不会出现问题，无论干什么不去上锁!如果出现了问题，再次更新值测试悲观锁：故名思意十分悲观,它总是认为总是出现问题，无论干什么都会上锁!再去操作!我们这里主要讲解乐观锁机制!乐观锁实现方式:取出记录时，获取当前version·更新时，带上这个version执行更新时，set version = newVersion where version = oldVersion如果version不对，就更新失败乐观锁：1、查询，获取版本 version = 1 ---线程A update user set name = "kuangshen", version = version + 1 where id = 2 and version = 1 ---线程B 抢先完成 这时候version = 2 线程A执行到的时候version不是1 然后会导致修改失败 update user set name = "kuangshen", version = version + 1 where id = 2 and version = 1测试一下Mp的乐观锁插件增加字段2、实体类上添加相应字段 @Version//乐观锁的注解 private Integer version;3、注册插件旧方法：/** * @author: ZzRG * @version: 1.0 * Date: 2022/6/27 */ //扫描mapper文件夹 @MapperScan("com.zzrg.xuexi.mapper") @EnableTransactionManagement @Configuration//配置类 public class MybatisPlusConfig { //注册乐观锁插件 (老版) public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } } 新版/** * @author: ZzRG * @version: 1.0 * Date: 2022/6/27 */ //扫描mapper文件夹 @MapperScan("com.zzrg.xuexi.mapper") @Configuration//配置类 public class MybatisPlusConfig { //注册乐观锁插件 /** * 新版 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return mybatisPlusInterceptor; } } @Test public void testOptimisticLocker(){ //查询用户信息 User user = userMapper.selectById(1L); //修改信息 user.setName("橘子"); user.setEmail("110110@123.com"); //执行更新 userMapper.updateById(user); } @Test public void testOptimisticLocker2(){ //线程1 //查询用户信息 User user = userMapper.selectById(1L); user.setName("橘子"); user.setEmail("110110@123.com"); //线程2进行插队操作 User user2 = userMapper.selectById(1L); user2.setName("橘子21"); user2.setEmail("110110@123.com"); userMapper.updateById(user2); //自旋锁多次尝试提交！ userMapper.updateById(user);//没有乐观锁就会覆盖插队线程2的值 }查询操作 //批量查询id @Test public void testSelectByBatchId(){ List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3)); users.forEach(System.out::println); } //条件查询Map @Test public void testSelectByBatchIds(){ HashMap<String, Object> map = new HashMap<>(); //自定义查询可以当多个条件 map.put("name","ZzRG"); map.put("id",1); map.put("age",3); List<User> users = userMapper.selectByMap(map); users.forEach(System.out::println); } 分页查询分页在网站上的使用1、原始limit进行分页2、pageHler分页插件3、MybatisPlus内置分页1、配置拦截器组件即可@Configuration @MapperScan("scan.your.mapper.package") public class MybatisPlusConfig { /** * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除) */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2)); return interceptor; } //旧的 @Bean public ConfigurationCustomizer configurationCustomizer() { return configuration -> configuration.setUseDeprecatedExecutor(false); } }2、直接使用Page对象即可@Test public void selectPage(){ //分页参数 Page<User> pageParams = new Page<>(1,5); //执行分页 userMapper.selectPage(pageParams,null); //查看分页参数成员 System.out.println(pageParams); } 删除操作 //测试删除 @Test public void testDeleteById(){ userMapper.deleteById(1L); } //批量删除 @Test public void testDeleteBachId(){ userMapper.deleteBatchIds(Arrays.asList(1L,2L,3L)); } //通过map删除 @Test public void testDeleteByMap(){ HashMap<String, Object> map = new HashMap<>(); map.put("id",1L); map.put("name","橘子"); userMapper.deleteByMap (map); }在工作会遇到逻辑删除的问题！逻辑删除物理删除：从数据库中直接移除逻辑删除：再数据库中没有被移除，而是通过一个变量来让他失效! deleted =0 => deleted = 1管理员可以查看被删除的记录!防止数据的丢失，类似于回收站!1、在数据表中增加字段 ：deleted2、实体类增加 属性 @TableLogic//逻辑删除 private Integer deleted;3、配置 //逻辑删除的配置 @Bean public ISqlInjector sqlInjector(){ return new LogicSqlInjector(); }#配置逻辑删除 mybatis-plus.global-config.db-config.logic-delete-value=1 mybatis-plus.global-config.db-config.logic-not-delete-value=0记录依旧在数据，但是值确实已经变化！执行查询语句！后面会自动拼接。性能分析插件我们在平时的开发中，会遇到一些慢sql。测试! druid...作用∶性能分析拦截器，用于输出每条SQL语句及其执行时间MP也提供性能分析插件，如果超过这个时间就停止运行!1、导入插件 /** * SQL执行效率 * @return */ @Bean @Profile({"dev","test"})//设置dev test 环境开启，保证效率 public PerformanceInterceptor performanceInterceptor(){ PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor(); performanceInterceptor.setMaxTime(100);//ms 设置sql执行最大时间 如果超过则不执行 performanceInterceptor.setFormat(true);//是否格式化 return performanceInterceptor; }配置环境#设置开发环境 spring.profiles.active=dev2、测试使用 只要超出设定最大时间就会抛出异常j！条件构造器核心部分！！！写一些复杂SQL的就可以用它代替！
登录使用JWT技术。jwt 可以生成 一个加密的token，做为用户登录的令牌，当用户登录成功之后，发放给客户端。请求需要登录的资源或者接口的时候，将token携带，后端验证token是否合法。jwt 有三部分组成：A.B.CA：Header，{"type":"JWT","alg":"HS256"} 固定B：playload，存放信息，比如，用户id，过期时间等等，可以被解密，不能存放敏感信息C: 签证，A和B加上秘钥 加密而成，只要秘钥不丢失，可以认为是安全的。jwt 验证，主要就是验证C部分 是否合法。依赖包: <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>导入JWT工具类package com.zzrg.blog.utils; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @author: ZzRG * @version: 1.0 * Date: 2022/6/23 */ public class JWTUtils { private static final String jwtToken = "123456ZzRG!@###$$"; public static String createToken(Long userId){ Map<String,Object> claims = new HashMap<>(); claims.put("userId",userId); JwtBuilder jwtBuilder = Jwts.builder() .signWith(SignatureAlgorithm.HS256,jwtToken) // 签发算法，秘钥为jwtToken .setClaims(claims) // body数据，要唯一，自行设置 .setIssuedAt(new Date()) // 设置签发时间 .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间 String token = jwtBuilder.compact(); return token; } public static Map<String, Object> checkToken(String token){ try { Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token); return (Map<String, Object>) parse.getBody(); }catch (Exception e){ e.printStackTrace(); } return null; } }登录和注册Servicepackage com.zzrg.blog.service; import com.zzrg.blog.dao.pojo.SysUser; import com.zzrg.blog.vo.Result; import com.zzrg.blog.vo.params.LoginParam; /** * @author: ZzRG * @version: 1.0 * Date: 2022/6/23 */ public interface LoginService { /** * 登录功能 * @param loginParam * @return */ Result login(LoginParam loginParam); SysUser checkToken(String token); /** * 注册 * @param loginParam * @return */ Result register(LoginParam loginParam); }需要md5加密的依赖包： <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency>去实现登录和注册Service方法package com.zzrg.blog.service.impl; import com.alibaba.fastjson.JSON; import com.zzrg.blog.dao.pojo.SysUser; import com.zzrg.blog.service.LoginService; import com.zzrg.blog.service.SysUserService; import com.zzrg.blog.utils.JWTUtils; import com.zzrg.blog.vo.ErrorCode; import com.zzrg.blog.vo.Result; import com.zzrg.blog.vo.params.LoginParam; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Map; import java.util.concurrent.TimeUnit; /** * @author: ZzRG * @version: 1.0 * Date: 2022/6/23 */ @Service @Transactional public class LoginServiceImpl implements LoginService { //需要查询用户的表 @Autowired private SysUserService sysUserService; //redies用的 @Autowired private RedisTemplate<String,String> redisTemplate; //加密盐用于加密 private static final String slat="ZzRG!@###"; @Override public Result login(LoginParam loginParam) { /** * 检查参数是否合法 * 根据用户名和密码去User表中查询 是否安全 * 如果不存在 登陆失败 * 如果存在 使用JWT 生成token 返回前端 * 最好 把token放入到redis当中 token中的User信息设置过期时间 * （登陆认证的时候 先认证token字符串是否合理，去redis认证是否存在） */ String account = loginParam.getAccount(); String password = loginParam.getPassword(); if (StringUtils.isBlank(account)||StringUtils.isBlank(password)){ return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg()); } //处理加密信息 String pwd = DigestUtils.md5Hex(password + slat); //在User表中查询用户 SysUser sysUser = sysUserService.findUser(account,pwd); if (sysUser == null){ return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg()); } //存在 //登录成功，使用JWT生成token，返回token和redis中 String token = JWTUtils.createToken(sysUser.getId()); //放入redis中 //过期时间是一天 redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser),1, TimeUnit.DAYS); return Result.success(token); } @Override public SysUser checkToken(String token) { //token为空返回null if (StringUtils.isBlank(token)){ return null; } //解析失败 Map<String, Object> stringObjectMap = JWTUtils.checkToken(token); if (stringObjectMap ==null){ return null; } //如果成功 String userJson = redisTemplate.opsForValue().get("TOKEN_" + token); if (StringUtils.isBlank(userJson)){ return null; } //解析回sysUser对象 SysUser sysUser = JSON.parseObject(userJson, SysUser.class); return sysUser; } /** * 注册服务逻辑 * @param loginParam * @return */ @Override public Result register(LoginParam loginParam) { /** * 判断参数是否合法 * 判断账户是否存在，存在 返回存在 * 不存在 注册用户 * 生成token * 存入redis 并返回 * 注意 加上事务 一旦中间的任何过程出现问题 注册用户 需要回滚 * */ String account = loginParam.getAccount(); String password = loginParam.getPassword(); String nickname = loginParam.getNickname(); if (StringUtils.isBlank(account) || StringUtils.isBlank(password) || StringUtils.isBlank(nickname)) { return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg()); } SysUser sysUser = this.sysUserService.findUserByAccount(account); if (sysUser !=null){ return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),"账户已经被注册了"); } sysUser = new SysUser(); sysUser.setNickname(nickname); sysUser.setAccount(account); sysUser.setPassword(DigestUtils.md5Hex(password+slat)); sysUser.setCreateDate(System.currentTimeMillis()); sysUser.setLastLogin(System.currentTimeMillis()); sysUser.setAvatar("/static/img/logo.b3a48c0.png"); sysUser.setAdmin(1); //1 为true sysUser.setDeleted(0); // 0 为false sysUser.setSalt(""); sysUser.setStatus(""); sysUser.setEmail(""); this.sysUserService.save(sysUser); //token String token = JWTUtils.createToken(sysUser.getId()); redisTemplate.opsForValue().set("TOKEN_" + token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS); return Result.success(token); } //测试生成我们想要的密码，放于数据库用于登陆 public static void main(String[] args) { System.out.println(DigestUtils.md5Hex("admin"+slat)); } }写Controller层 调用Service的方法
使用Mybaits的时候，注入mapper的是一直会有一个红色的警告，虽然不影响代码的运行,但对有强迫症的小伙伴来说，总是有些闹心！接下来告诉大家去掉这些警告的方法。1. 为 @Autowired 注解设置required = false： 使用 @Autowired 注解时，若希望允许null值，可设置required = false。 @Autowired(required = false) private UserMapper userMapper；这样就不会有警告了。原因很好理解：IDEA认为userMapper是个null，给了警告；加上required = false后，使用 @Autowired 注解不再去校验userMapper是否存在了。也就不会有警告了。但是对于一个庞大的既有项目，可能到处都在引用Mapper，总不能到处都补上 required = false 吧。而且对于新手来说，很难一眼看懂加required = false属性只是为了解决IDEA的警告。2. 用 @Resource 替换 @Autowired。 @Resource private UserMapper userMapper；这样也不会再有讨厌的警告。但是如果一个项目已经大量使用@Autowired，然后为了个警告到处改成@Resource，难免会有些麻烦。3. 在Mapper接口上加上@Repository注解。 @Mapper @Repository public interface UserMapper { } @Autowired private UserMapper userMapper；这样也不会有警告，如果你用@Component替换@Repository也是可以的。原理大致：IDEA不是认为 userMapper 是个null，加个@Repository注解骗一下IDEA就好了。4.用Lombok：也可以解决警告的问题，但是如果手写成Lombok生成的代码，Idea依然会报警告。对于不会使用Lombok的小伙伴可参见 Lombok官方文档！5.把IDEA的警告关掉，不推荐这么做。6.安装mybatis plugin插件,但是这个插件时收费的。
{timeline}{timeline-item color="#19be6b"}2022-05-29 17:05:15 星期日 {collapse}{collapse-item label="响应处理" open}【源码分析】-ReturnValueHandler原理假设给前端自动返回json数据，需要引入相关的依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> <version>2.3.4.RELEASE</version> <scope>compile</scope> </dependency> 控制层代码如下：@Controller public class ResponseTestController { @ResponseBody //利用返回值处理器里面的消息转换器进行处理 @GetMapping(value = "/test/person") public Person getPerson(){ Person person = new Person(); person.setAge(28); person.setBirth(new Date()); person.setUserName("zhangsan"); return person; } }请求处理-【源码分析】-各种类型参数解析原理 - 返回值处理器有讨论 ReturnValueHandler 。现在直接看看重点：public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { ... ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) {//<----关注点 invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ... invocableMethod.invokeAndHandle(webRequest, mavContainer);//看下块代码 if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); ... try { //看下块代码 this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { ... } } public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { ... @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { //selectHandler()实现在下面 HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); } //开始处理 handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } @Nullable private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { boolean isAsyncValue = isAsyncReturnValue(value, returnType); for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { continue; } if (handler.supportsReturnType(returnType)) { return handler; } } return null; } @ResponseBody 注解，即 RequestResponseBodyMethodProcessor ，它实现 HandlerMethodReturnValueHandler 接口public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { ... @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { mavContainer.setRequestHandled(true); ServletServerHttpRequest inputMessage = createInputMessage(webRequest); ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); // 使用消息转换器进行写出操作，本方法下一章节介绍： // Try even with null return value. ResponseBodyAdvice could get involved. writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } } 【源码分析】-HTTPMessageConverter原理返回值处理器ReturnValueHandler原理：返回值处理器判断是否支持这种类型返回值 supportsReturnType返回值处理器调用 handleReturnValue 进行处理RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。利用 MessageConverters 进行处理 将数据写为json内容协商（浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型）服务器最终根据自己自身的能力，决定服务器能生产出什么样内容类型的数据，SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ，看谁能处理？得到MappingJackson2HttpMessageConverter可以将对象写为json利用MappingJackson2HttpMessageConverter将对象转为json再写出去。//RequestResponseBodyMethodProcessor继承这类 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... //承接上一节内容 protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class<?> valueType; Type targetType; if (value instanceof CharSequence) { body = value.toString(); valueType = String.class; targetType = String.class; } else { body = value; valueType = getReturnValueType(body, returnType); targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); } ... //内容协商（浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型） MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null && contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response"); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); //服务器最终根据自己自身的能力，决定服务器能生产出什么样内容类型的数据 List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); if (body != null && producibleTypes.isEmpty()) { throw new HttpMessageNotWritableException( "No converter found for return value of type: " + valueType); } List<MediaType> mediaTypesToUse = new ArrayList<>(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (mediaTypesToUse.isEmpty()) { if (body != null) { throw new HttpMediaTypeNotAcceptableException(producibleTypes); } if (logger.isDebugEnabled()) { logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes); } return; } MediaType.sortBySpecificityAndQuality(mediaTypesToUse); //选择一个MediaType for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } if (logger.isDebugEnabled()) { logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes); } } if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); //本节主角：HttpMessageConverter for (HttpMessageConverter<?> converter : this.messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); //判断是否可写 if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); addContentDispositionHeader(inputMessage, outputMessage); //开始写入 if (genericConverter != null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } return; } } } ... } HTTPMessageConverter 接口：/** * Strategy interface for converting from and to HTTP requests and responses. */ public interface HttpMessageConverter<T> { /** * Indicates whether the given class can be read by this converter. */ boolean canRead(Class<?> clazz, @Nullable MediaType mediaType); /** * Indicates whether the given class can be written by this converter. */ boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType); /** * Return the list of {@link MediaType} objects supported by this converter. */ List<MediaType> getSupportedMediaTypes(); /** * Read an object of the given type from the given input message, and returns it. */ T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; /** * Write an given object to the given output message. */ void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; } HttpMessageConverter: 看是否支持将 此 Class类型的对象 ，转为 MediaType类型的数据 。例子： Person对象 转为 JSON ，或者 JSON 转为 Person ，这将用到 MappingJackson2HttpMessageConverter public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { ... }关于MappingJackson2HttpMessageConverter的实例化请看下节。 关于 HttpMessageConverters 的初始化 DispatcherServlet 的初始化时会调用 initHandlerAdapters(ApplicationContext context)public class DispatcherServlet extends FrameworkServlet { ... private void initHandlerAdapters(ApplicationContext context) { this.handlerAdapters = null; if (this.detectAllHandlerAdapters) { // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts. Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerAdapters = new ArrayList<>(matchingBeans.values()); // We keep HandlerAdapters in sorted order. AnnotationAwareOrderComparator.sort(this.handlerAdapters); } } ...上述代码会加载 ApplicationContext 的所有 HandlerAdapter ，用来处理 @RequestMapping 的 RequestMappingHandlerAdapter 实现 HandlerAdapter 接口， RequestMappingHandlerAdapter 也被实例化。public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... private List<HttpMessageConverter<?>> messageConverters; ... public RequestMappingHandlerAdapter() { this.messageConverters = new ArrayList<>(4); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); if (!shouldIgnoreXml) { try { this.messageConverters.add(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } } this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); } 在构造器中看到 一堆 HttpMessageConverter 。接着，重点查看 AllEncompassingFormHttpMessageConverter 类：public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter { /** * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to * ignore XML, i.e. to not initialize the XML-related infrastructure. * The default is "false". */ private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); private static final boolean jaxb2Present; private static final boolean jackson2Present; private static final boolean jackson2XmlPresent; private static final boolean jackson2SmilePresent; private static final boolean gsonPresent; private static final boolean jsonbPresent; private static final boolean kotlinSerializationJsonPresent; static { ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader(); jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader); jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader); kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); } public AllEncompassingFormHttpMessageConverter() { if (!shouldIgnoreXml) { try { addPartConverter(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } if (jaxb2Present && !jackson2XmlPresent) { addPartConverter(new Jaxb2RootElementHttpMessageConverter()); } } if (jackson2Present) { addPartConverter(new MappingJackson2HttpMessageConverter());//<----重点看这里 } else if (gsonPresent) { addPartConverter(new GsonHttpMessageConverter()); } else if (jsonbPresent) { addPartConverter(new JsonbHttpMessageConverter()); } else if (kotlinSerializationJsonPresent) { addPartConverter(new KotlinSerializationJsonHttpMessageConverter()); } if (jackson2XmlPresent && !shouldIgnoreXml) { addPartConverter(new MappingJackson2XmlHttpMessageConverter()); } if (jackson2SmilePresent) { addPartConverter(new MappingJackson2SmileHttpMessageConverter()); } } } public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> { ... private List<HttpMessageConverter<?>> partConverters = new ArrayList<>(); ... public void addPartConverter(HttpMessageConverter<?> partConverter) { Assert.notNull(partConverter, "'partConverter' must not be null"); this.partConverters.add(partConverter); } ... } 在 AllEncompassingFormHttpMessageConverter 类构造器看到 MappingJackson2HttpMessageConverter 类的实例化， AllEncompassingFormHttpMessageConverter 包含 MappingJackson2HttpMessageConverter 。ReturnValueHandler 是怎么与 MappingJackson2HttpMessageConverter 关联起来？请看下节。ReturnValueHandler与MappingJackson2HttpMessageConverter关联再次回顾RequestMappingHandlerAdapterpublic class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... @Nullable private HandlerMethodReturnValueHandlerComposite returnValueHandlers;//我们关注的returnValueHandlers @Override @Nullable//本方法在AbstractHandlerMethodAdapter public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); } @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; ... mav = invokeHandlerMethod(request, response, handlerMethod); ... return mav; } @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) {//<---我们关注的returnValueHandlers invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ... invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } @Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans ... if (this.returnValueHandlers == null) {//赋值returnValueHandlers List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() { List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20); ... // Annotation-based return value types //这里就是 ReturnValueHandler与 MappingJackson2HttpMessageConverter关联 的关键点 handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),//<---MessageConverters也就传参传进来的 this.contentNegotiationManager, this.requestResponseBodyAdvice));// ... return handlers; } //------ public List<HttpMessageConverter<?>> getMessageConverters() { return this.messageConverters; } //RequestMappingHandlerAdapter构造器已初始化部分messageConverters public RequestMappingHandlerAdapter() { this.messageConverters = new ArrayList<>(4); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); if (!shouldIgnoreXml) { try { this.messageConverters.add(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } } this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); } ... }应用中WebMvcAutoConfiguration（底层是WebMvcConfigurationSupport实现）传入更多messageConverters，其中就包含MappingJackson2HttpMessageConverter。【源码分析】-内容协商原理根据客户端接收能力不同，返回不同媒体类型的数据。引入XML依赖： <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>可用Postman软件分别测试返回json和xml：只需要改变请求头中Accept字段（application/json、application/xml）。Http协议中规定的，Accept字段告诉服务器本客户端可以接收的数据类型。 内容协商原理：判断当前响应头中是否已经有确定的媒体类型MediaType。获取客户端（PostMan、浏览器）支持接收的内容类型。（获取客户端Accept请求头字段application/xml）（这一步在下一节有详细介绍）contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型遍历循环所有当前系统的 MessageConverter，看谁支持操作这个对象（Person）找到支持操作Person的converter，把converter支持的媒体类型统计出来。客户端需要application/xml，服务端有10种MediaType。进行内容协商的最佳匹配媒体类型用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。//RequestResponseBodyMethodProcessor继承这类 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... //跟上一节的代码一致 protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class<?> valueType; Type targetType; if (value instanceof CharSequence) { body = value.toString(); valueType = String.class; targetType = String.class; } else { body = value; valueType = getReturnValueType(body, returnType); targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); } ... //本节重点 //内容协商（浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型） MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null && contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response"); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); //服务器最终根据自己自身的能力，决定服务器能生产出什么样内容类型的数据 List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); if (body != null && producibleTypes.isEmpty()) { throw new HttpMessageNotWritableException( "No converter found for return value of type: " + valueType); } List<MediaType> mediaTypesToUse = new ArrayList<>(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (mediaTypesToUse.isEmpty()) { if (body != null) { throw new HttpMediaTypeNotAcceptableException(producibleTypes); } if (logger.isDebugEnabled()) { logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes); } return; } MediaType.sortBySpecificityAndQuality(mediaTypesToUse); //选择一个MediaType for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } if (logger.isDebugEnabled()) { logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes); } } if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); //本节主角：HttpMessageConverter for (HttpMessageConverter<?> converter : this.messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); //判断是否可写 if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); addContentDispositionHeader(inputMessage, outputMessage); //开始写入 if (genericConverter != null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug("Nothing to write: null body"); } } return; } } } ... }【源码分析】-基于请求参数的内容协商原理上一节内容协商原理的第二步：获取客户端（PostMan、浏览器）支持接收的内容类型。（获取客户端Accept请求头字段application/xml）contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型//RequestResponseBodyMethodProcessor继承这类 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... //跟上一节的代码一致 protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class<?> valueType; Type targetType; ... //本节重点 //内容协商（浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型） MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null && contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response"); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); //服务器最终根据自己自身的能力，决定服务器能生产出什么样内容类型的数据 List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); ... } //在AbstractMessageConverterMethodArgumentResolver类内 private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { //内容协商管理器 默认使用基于请求头的策略 return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); } } public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver { ... public ContentNegotiationManager() { this(new HeaderContentNegotiationStrategy());//内容协商管理器 默认使用基于请求头的策略 } @Override public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { for (ContentNegotiationStrategy strategy : this.strategies) { List<MediaType> mediaTypes = strategy.resolveMediaTypes(request); if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) { continue; } return mediaTypes; } return MEDIA_TYPE_ALL_LIST; } ... } //基于请求头的策略 public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy { /** * {@inheritDoc} * @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed */ @Override public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT); if (headerValueArray == null) { return MEDIA_TYPE_ALL_LIST; } List<String> headerValues = Arrays.asList(headerValueArray); try { List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues); MediaType.sortBySpecificityAndQuality(mediaTypes); return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST; } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotAcceptableException( "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage()); } } } 开启浏览器参数方式内容协商功能 为了方便内容协商，开启基于请求参数的内容协商功能。spring: mvc: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式内容协商管理器，就会多了一个 ParameterContentNegotiationStrategy （由Spring容器注入）public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { private String parameterName = "format";// /** * Create an instance with the given map of file extensions and media types. */ public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) { super(mediaTypes); } /** * Set the name of the parameter to use to determine requested media types. * By default this is set to {@code "format"}. */ public void setParameterName(String parameterName) { Assert.notNull(parameterName, "'parameterName' is required"); this.parameterName = parameterName; } public String getParameterName() { return this.parameterName; } @Override @Nullable protected String getMediaTypeKey(NativeWebRequest request) { return request.getParameter(getParameterName()); } //---以下方法在AbstractMappingContentNegotiationStrategy类 @Override public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest)); } /** * An alternative to {@link #resolveMediaTypes(NativeWebRequest)} that accepts * an already extracted key. * @since 3.2.16 */ public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException { if (StringUtils.hasText(key)) { MediaType mediaType = lookupMediaType(key); if (mediaType != null) { handleMatch(key, mediaType); return Collections.singletonList(mediaType); } mediaType = handleNoMatch(webRequest, key); if (mediaType != null) { addMapping(key, mediaType); return Collections.singletonList(mediaType); } } return MEDIA_TYPE_ALL_LIST; } } 然后，浏览器地址输入带format参数的URL：http://localhost:8080/test/person?format=json 或 http://localhost:8080/test/person?format=xml这样，后端会根据参数format的值，返回对应json或xml格式的数据。【源码分析】-自定义MessageConverter实现多协议数据兼容。json、xml、x-guigu （这个是自创的）@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理Processor 处理方法返回值。通过 MessageConverter 处理所有 MessageConverter 合起来可以支持各种媒体类型数据的操作（读、写）内容协商找到最终的 messageConverterSpringMVC 的什么功能，一个入口给容器中添加一个 WebMvcConfigurer@Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new GuiguMessageConverter()); } } } } /** * 自定义的Converter */ public class GuiguMessageConverter implements HttpMessageConverter<Person> { @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return false; } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { return clazz.isAssignableFrom(Person.class); } /** * 服务器要统计所有MessageConverter都能写出哪些内容类型 * * application/x-guigu * @return */ @Override public List<MediaType> getSupportedMediaTypes() { return MediaType.parseMediaTypes("application/x-guigu"); } @Override public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } @Override public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { //自定义协议数据的写出 String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth(); //写出去 OutputStream body = outputMessage.getBody(); body.write(data.getBytes()); } } import java.util.Date; @Controller public class ResponseTestController { /** * 1、浏览器发请求直接返回 xml [application/xml] jacksonXmlConverter * 2、如果是ajax请求 返回 json [application/json] jacksonJsonConverter * 3、如果硅谷app发请求，返回自定义协议数据 [appliaction/x-guigu] xxxxConverter * 属性值1;属性值2; * * 步骤： * 1、添加自定义的MessageConverter进系统底层 * 2、系统底层就会统计出所有MessageConverter能操作哪些类型 * 3、客户端内容协商 [guigu--->guigu] * * 作业：如何以参数的方式进行内容协商 * @return */ @ResponseBody //利用返回值处理器里面的消息转换器进行处理 @GetMapping(value = "/test/person") public Person getPerson(){ Person person = new Person(); person.setAge(28); person.setBirth(new Date()); person.setUserName("zhangsan"); return person; } } 用 Postman 发送 /test/person （请求头 Accept:application/x-guigu )，将返回自定义协议数据的写出。响应处理-【源码分析】-浏览器与PostMan内容协商完全适配假设你想基于自定义请求参数的自定义内容协商功能。换句话，在地址栏输入 http://localhost:8080/test/person?format=gg 返回数据，跟 http://localhost:8080/test/person 且请求头参数 Accept:application/x-guigu 的返回自定义协议数据的一致。@Configuration(proxyBeanMethods = false) public class WebConfig /*implements WebMvcConfigurer*/ { //1、WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { /** * 自定义内容协商策略 * @param configurer */ @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { //Map<String, MediaType> mediaTypes Map<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); //自定义媒体类型 mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu")); //指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); // parameterStrategy.setParameterName("ff"); //还需添加请求头处理策略，否则accept:application/json、application/xml则会失效 HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(parameterStrategy, headeStrategy)); } } } ... } 日后开发要注意， 有可能我们添加的自定义的功能会覆盖默认很多功能，导致一些默认的功能失效。 {/collapse-item}{collapse-item label="视图解析-Thymeleaf初体验" }{callout color="#f0ad4e"}hymeleaf is a modern server-side Java template engine for both web and standalone environments.Thymeleaf’s main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.—— Link {/callout}Thymeleaf官方文档 thymeleaf使用 引入Starter<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>自动配置好了thymeleaf@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class }) @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration { ... }自动配好的策略所有thymeleaf的配置值都在 ThymeleafProperties配置好了 SpringTemplateEngine配好了 ThymeleafViewResolver我们只需要直接开发页面public static final String DEFAULT_PREFIX = "classpath:/templates/";//模板放置处 public static final String DEFAULT_SUFFIX = ".html";//文件的后缀名@Controller public class ViewTestController { @GetMapping("/hello") public String hello(Model model){ //model中的数据会被放在请求域中 request.setAttribute("a",aa) model.addAttribute("msg","一定要大力发展工业文化"); model.addAttribute("link","http://www.baidu.com"); return "success"; } } /templates/success.html ： Title nice 去百度 去百度 server: servlet: context-path: /app #设置应用名这个设置后，URL要插入 /app , 如 http://localhost:8080/app/hello.html 。 基本语法表达式表达式名字语法用途变量取值${…}获取请求域、session域、对象等值选择变量*{…}获取上下文对象值消息#{…}获取国际化等值链接@{…}生成链接片段表达式~{…}jsp:include 作用，引入公共页面片段字面量文本值: ‘one text’ , ‘Another one!’ ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false空值: null变量： one，two，… 变量不能有空格文本操作字符串拼接: +变量替换: |The name is ${name}|数学运算运算符: + , - , * , / , %布尔运算运算符: and , or一元运算: ! , not比较运算比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )条件运算If-then: (if) ? (then)If-then-else: (if) ? (then) : (else)Default: (value) ?: (defaultvalue)特殊操作无操作： _设置属性值-th:attr设置单个值 设置多个值 官方文档 - 5 Setting Attribute Values迭代 Onions 2.41 yes Onions 2.41 yes 条件运算view User is an administrator User is a manager User is some other thing 属性优先级 官方文档 - 10 Attribute Precedence{/collapse-item}{collapse-item label="web实验-后台管理系统基本功能"}项目创建 使用IDEA的Spring Initializr。thymeleaf、web-starter、devtools、lombok登陆页面 /static 放置 css，js等静态资源 /templates/login.html 登录页 ... ... /templates/main.html 主页 thymeleaf内联写法： Hello, [[${session.user.name}]]! 登录控制层@Controller public class IndexController { /** * 来登录页 * @return */ @GetMapping(value = {"/","/login"}) public String loginPage(){ return "login"; } @PostMapping("/login") public String main(User user, HttpSession session, Model model){ //RedirectAttributes if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){ //把登陆成功的用户保存起来 session.setAttribute("loginUser",user); //登录成功重定向到main.html; 重定向防止表单重复提交 return "redirect:/main.html"; }else { model.addAttribute("msg","账号密码错误"); //回到登录页面 return "login"; } } /** * 去main页面 * @return */ @GetMapping("/main.html") public String mainPage(HttpSession session, Model model){ //最好用拦截器,过滤器 Object loginUser = session.getAttribute("loginUser"); if(loginUser != null){ return "main"; }else { //session过期，没有登陆过 //回到登录页面 model.addAttribute("msg","请重新登录"); return "login"; } } } 模型@AllArgsConstructor @NoArgsConstructor @Data public class User { private String userName; private String password; }{/collapse-item}{/collapse}{/timeline-item}{timeline-item color="#ed4014"}学习结束！！！{/timeline-item}{/timeline}{mtitle title="结束"/}