Java:115-Spring Boot的底层原理(下篇)

这里续写上一章博客(115章博客)
SpringBoot视图技术:
支持的视图技术 :
前端模板引擎技术的出现(jsp也是),使前端开发人员无需关注后端业务的具体实现(jsp中,具体的前端通常需要让后端处理操作,所以jsp通常不会说成分离),只关注自己页面的呈现效果即可
并且解决了前端代码错综复杂的问题、实现了前后端分离开发
Spring Boot框架对很多常用的 模板引擎技术(如: FreeMarker、 Thymeleaf、 Mustache等)提供了整合支持
该技术一般对很多数据的加载时,直接生成静态的,使得访问速度非常块,如访问jd.com网站
搜索商品,点击一个商品,查看地址
我找到的就是https://item.jd.com/100003033647.html,发现他是静态的网站,我们也可以感受到,访问的速度非常块
因为是静态的(写死的页面),所以速度快,他就是使用了模板引擎的技术
Spring Boot不太支持常用的JSP模板,并且没有提供对应的整合配置
这是因为使用嵌入式Servlet容器的Spring Boot应用程序对于JSP模板存在一些限制 :
在Jetty和Tomcat容器中, Spring Boot应用被打包成war文件可以支持JSP
但Spring Boot默认使用嵌入式Servlet容器以JAR包方式进行项目打包部署,这种JAR包方式不支持JSP(或者不想支持)
如果使用Undertow嵌入式容器部署Spring Boot项目
也不支持JSP模板(Undertow 是红帽公 司开发的一款基于 NIO 的高性能 Web 嵌入式服务器)
Spring Boot默认提供了一个处理请求路径"/error"的统一错误处理器,返回具体的异常信息
使用JSP模板时,无法对默认的错误处理器进行覆盖,只能根据Spring Boot要求在指定位置定制错误页面
上面对Spring Boot支持的模板引擎进行了介绍,并指出了整合JSP模板的一些限制(或者说,这些限制并不好解决)
接下来,对其中常用的Thymeleaf模板引擎进行介绍,并完成与Spring Boot框架的整合实现
Thymeleaf:这里我们稍微复习一下,因为在88章博客有具体的讲解
先来一个示例代码:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <!--需要xmlns:th="http://www.thymeleaf.org"才可以操作th,在jsp中也有类似的,具体都是被java读取后的处理-->
<head>    
<meta charset="UTF-8">    
<link rel="stylesheet" type="text/css" media="all" href="../../css/gtvg.css" th:href="@{/css/gtvg.css}" />   
<!--
这个样式在这里并没有使用(实际上好像并没有该样式,因为没有创建),所以可以删除,但写上也是没有问题的
只是不会有对应的作用而已,基本不会对页面造成影响
--> 
<title>Title</title>
</head>
<body>   
<p th:text="${hello}">欢迎进入Thymeleaf的学习</p>
</body>
</html>
上述代码中,"xmlns:th="http://www.thymeleaf.org"用于引入Thymeleaf模板引擎标签,使用关键字"th"标注标签是Thymeleaf模板提供的标签,其中,"th:href"用于引入外联样式文件,"th:text"用于动态显示标签文本内容(没有则操作默认值,一般是标签里面的)
除此之外,Thymeleaf模板提供了很多标签,接下来,通过一张表罗列Thymeleaf的常用标签:

在这里插入图片描述

标准表达式:
Thymeleaf模板引擎提供了多种标准表达式语法,在正式学习之前,先通过一张表来展示其主要语法及说明

在这里插入图片描述

变量表达式 ${…}:
变量表达式${…}主要用于获取上下文中(作用域)的变量值,示例代码如下:
<p th:text="${title}">这是标题</p>
示例使用了Thymeleaf模板的变量表达式${…}用来动态获取P标签中的内容
如果当前程序没有启动,该片段会显示标签默认值"这是标题",若当前上下文中不存在title变量,一般返回空数据
因为对应的操作时,返回的就是null,如:
Object title = model.getAttribute("title");
System.out.println(title); //若没有则返回null,那么在前端显示的就是空值
如果当前上下文中存在title变量并且程序已经启动,当前P标签中的默认文本内容将会被title变量的值所替换
从而达到模板引擎页面数据动态替换的效果
同时,Thymeleaf为变量所在域提供了一些内置对象,具体如下所示

在这里插入图片描述

结合上述内置对象的说明,假设要在Thymeleaf模板引擎页面中动态获取当前国家信息,可以使用#locale内置对象,示例代码如下
The locale country is: <span th:text="${#locale.country}">US</span><!--
上述代码中,使用th:text="${#locale.country}"动态获取当前用户所在国家信息
其中标签内默认内容为US(美国),程序启动后通过浏览器查看当前页面时
Thymeleaf会通过浏览器语言设置来识别当前用户所在国家信息,从而实现动态替换 注意:模板操作对应的底层原理与jsp类似,也是java类,只是Thymeleaf快一些(没有那么复杂,但是也没有jsp的灵活了),且更加的符合html格式-->
至此我们可以测试一下:
Thymeleaf模板基本配置
首先 在Spring Boot项目中使用Thymeleaf模板,首先必须保证引入Thymeleaf依赖,示例代码如下:
<dependency>   <!--
使得可以操作对应的html变成模板,解析了数据后,然后返回前端显示,一般需要配置参数来确定格式
一起操作,这里提供类,配置则操作参数,类会使用参数,否则使用默认
-->
<groupId>org.springframework.boot</groupId>   
<artifactId>spring-boot-starter-thymeleaf</artifactId> 
</dependency>
现在我们将之前的项目内容都去掉,依赖也是,操作变成如下:

在这里插入图片描述

yml里面是空的,也没有测试类和他的包
对应的依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><!--固定的--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.2</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>bootmyba</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies><build><plugins><plugin><!--固定的--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
其次,在全局配置文件中配置Thymeleaf模板的一些参数,一般Web项目都会使用下列配置,示例代码如下:
spring:thymeleaf:cache: true #启用模板缓存encoding: UTF-8 #模板编码mode: HTML5 #应用于模板的模板模式#下面两个表示开头和结尾prefix: classpath:/templates/ #指定模板页面存放路径suffix: .html #指定模板页面名称的后缀
所以我们需要在资源文件夹下创建templates文件夹,上面都存在默认的:
/*private String prefix = "classpath:/templates/";private String suffix = ".html";
*/
在开发过程中通常会关闭缓存,保证项目调试过程中数据能够及时响应,默认为true
静态资源的访问:
开发Web应用时,难免需要使用静态资源,Spring boot默认设置了静态资源的访问路径
若在resources目录中有public、resources、static三个子目录,Spring boot默认会挨个从public、resources、static里面查找静态资源
当然,因为是静态资源,除了对应的js,css,图片,html等,其他的没有特殊含义的,都会当成是普通的文件
虽然前面的也是普通的文件,但是浏览器一般会特殊的处理
且现在的版本,对应的顺序可能是resources,static,public(不同的版本可能不同,但无关紧要)
因为不管怎么说,只要使用一个文件夹即可,通常使用static文件,因为见名知意
一般创建的项目只有static文件夹,其他两个文件夹没有
我们可以测试一下,在static(没有自行在resources目录中创建)里面创建index.js文件:
function sum(a,b){return a+b;
}
直接访问http://localhost:8080/index.js即可,即可以得到该信息
因为默认加上对应的三个子目录进行测试,如这里默认加上static,所以不要加上其他路径,否则一般会找不到文件,使得访问不了
即浏览器一般显示没有对应的网页信息
完成数据的页面展示:
引入依赖:
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
创建web控制类:
然后创建com包,再创建controller包,然后创建如下的类:
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;import java.util.Calendar;@Controller
public class LoginController {@RequestMapping("/toLoginPage")public String toLoginPage(Model model) {model.addAttribute("currentYear", Calendar.getInstance().get(Calendar.YEAR));//Calendar.getInstance().get(Calendar.YEAR),获取当前年份return "login";}
}
toLoginPage()方法用于向登录页面login.html跳转,同时携带了当前年份信息currentYear,是否发现,与jsp有点类似
因为模板操作他也是jsp的那个模式,底层都是java类来操作
创建模板页面并引入静态资源文件:
在对应的资源文件夹下可以看到templates文件夹,在里面创建login.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><link rel="stylesheet" type="text/css" media="all"href="../../css/gtvg.css" th:href="@{/css/gtvg.css}" /><title>Title</title>
</head>
<body>
The locale country is: <span th:text="${#locale.country}">US</span><br>
The locale country is: <span th:text="${#locale.language}">中文</span><br>
<span th:text="${currentYear}">2019</span><br>
<span th:text="${currentYearr}">2019</span><br>
9<!--如果${}里面是对象,则结果是他的toString方法,若是对象.变量,则是调用该变量首字母大写的get方法
当然,若该变量的首字母本来大写,那么也是需要首字母大写的get方法,即不会变即假设是a.b,那么调用a对象的getB方法,无论是否有该变量,我们只获得getB方法的结果作为该结果没有对应的get方法,则报错,访问不了页面,当然,没有toString,自然使用父类的,到那时
也就是对应"类名@hashCode值"形式的字符串了
-->
</body>
</html>
访问http://localhost:8080/toLoginPage看看结果吧

在这里插入图片描述

直接的运行,是不会操作模板的(相当于访问这个页面而已,而没有启动程序),所以我们发现,他的确进行了替换
至此测试完毕,后面的操作,可以自行加上进行测试
选择变量表达式 *{…}:
选择变量表达式和变量表达式用法类似,一般用于从被选定对象而不是上下文中获取属性值
如果没有选定对象,则和变量表达式一样,示例代码如下:
<div th:object="${book}">  
<p>titile: <span th:text="*{title}">标题</span></p>
</div>
*{title} 选择变量表达式获取当前指定对象book的title属性值
我们也进行测试一下:
在前端(页面文件)加上上面的代码,然后操作如下:
在pojo包(没有就创建)下创建book类(对象):
package com.pojo;public class book {private String title;public book() {}public book(String title) {this.title = title;}@Overridepublic String toString() {return "book{" +"title='" + title + '\'' +'}';}public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}
}
并在后端的toLoginPage方法下,加上如下代码:
model.addAttribute("book", new book("111"));
重新部署,访问,会发现替换了值,则代表操作成功,当然还有很多操作,这些操作到88章博客可以明白的
这里进行补充一下:
配置国际化页面,在88章博客我们也具体说明了,但是具体为什么使用,或者在什么时候使用并没有进行说明,现在我们开始处理:
首先创建项目:
在项目的类路径resources下创建名称为i18n的文件夹,并在该文件夹中根据需要编写对应的多语言国际化文件login.properties、login_zh_CN.properties和login_en_US.properties文件
login.properties:
login.tip=请登录
login.username=用户名
login.password=密码
login.rememberme=记住我
login.button=登录
login_zh_CN.properties:
login.tip=请登录
login.username=用户名
login.password=密码
login.rememberme=记住我
login.button=登录
login_en_US.properties:
login.tip=Please sign in
login.username=Username
login.password=Password
login.rememberme=Remember me
login.button=Login
login.properties为自定义默认语言配置文件,login_zh_CN.properties为自定义中文国际化文 件,login_en_US.properties为自定义英文国际化文件
需要说明的是,Spring Boot默认识别的语言配置文件为类路径resources下的messages.properties(无论你是否写),其他语言国际化文件的名称必须严格按照"文件前缀名语言代码国家代 码.properties"的形式命名,具体细节在88章博客
然后在yml中(如果操作的话)添加如下:
#private String basename = "messages";默认的值,默认识别messages.propertiesspring:messages:basename: i18n.login
在完成上一步中多语言国际化文件的编写和配置后,就可以正式在前端页面中结合Thymeleaf模板相关属性进行国际化语言设置和展示了,不过这种实现方式默认是使用请求头中的语言信息(浏览器语言信息)自动进行语言切换的,以及默认的单纯展示,有些项目还会提供手动语言切换的功能,这些通常需要定制区域解析器了
依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
在项目中创建名为com.config的包,并在该包下创建一个用于定制国际化功能区域信息解析器的自定义配置类MyLocalResovel
package com.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;@Configuration
public class MyLocalResovel implements LocaleResolver {// 自定义区域解析方式@Overridepublic Locale resolveLocale(HttpServletRequest httpServletRequest) {// 获取页面手动切换传递的语言参数lString l = httpServletRequest.getParameter("l");// 获取请求头自动传递的语言参数Accept-LanguageString header = httpServletRequest.getHeader("Accept-Language");Locale locale = null;// 如果手动切换参数不为空,就根据手动参数进行语言切换,否则默认根据请求头信息切换if (!StringUtils.isEmpty(l)) {String[] split = l.split("_");locale = new Locale(split[0], split[1]);} else {String[] splits = header.split(",");String[] split = splits[0].split("-");locale = new Locale(split[0], split[1]);}return locale;}@Overridepublic void setLocale(HttpServletRequest httpServletRequest, @NullableHttpServletResponse httpServletResponse, @Nullable Locale locale) {}// 将自定义的MyLocalResovel类重新注册为一个类型LocaleResolver的Bean组件@Beanpublic LocaleResolver localeResolver() {return new MyLocalResovel();}
}
MyLocalResolver自定义区域解析器配置类实现了LocaleResolver接口,并重写了其中的 resolveLocale()方法进行自定义语言解析,最后使用@Bean注解将当前配置类注册成Spring容器中的一 个类型为LocaleResolver的Bean组件,这样就可以覆盖默认的LocaleResolver组件,其中,在 resolveLocale()方法中,根据不同需求(手动切换语言信息、浏览器请求头自动切换语言信息)分别获取了请求参数"l"和请求头参数Accept-Language,然后在请求参数"l"不为空的情况下就以"l"参数携带的语言 为标准进行语言切换,否则就定制通过请求头信息进行自动切换
然后前端可以选择请求,然后后端通过上面的操作进行获取信息,操作响应信息的语言信息(进行操作语言的区域),可以继续跳转到原来的页面,也就完成了切换(如点击中文切换中文,英文切换英文),这里了解即可,可以测试一下,有问题可以改一改,在88章博客有一个小例子
这里我们也来测试一下,然后我们创建LoginController类(在com.controller包下):
package com.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.LocaleResolver;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;@Controller
public class LoginController {@Autowiredprivate LocaleResolver localeResolver;@RequestMapping("/toLoginPage")public String toLoginPage(HttpServletRequest request, HttpServletResponse response) {Locale locale = localeResolver.resolveLocale(request);//设置响应信息头response.setLocale(locale);return "login";}
}
前端页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><link rel="stylesheet" type="text/css" media="all"href="../../css/gtvg.css" th:href="@{/css/gtvg.css}"/><title>Title</title>
</head>
<body>
<a th:href="@{/toLoginPage(l='zh_CN')}">中文</a>
<a th:href="@{/toLoginPage(l='en_US')}">English</a></body>
</html>
有些文件没有的,自然不会操作的
然后我们访问启动,访问:http://localhost:8080/toLoginPage,我们来说明一下操作流程(不考虑启动时的默认访问,或者启动后的多次访问)
/*
一般访问通常会操作三次(应该设置的是内部的原因)
第二次开始从response.setLocale(locale);中出现(包括第二次)
第一次访问:
请求信息:Accept-Language:zh-CN,zh;q=0.9
响应信息:Content-Language:zh-CN
界面的显示是根据响应信息来的,因为其信息就是响应体,当然,有些请求信息会根据响应信息来处理(如重定向)
操作默认的:那么:
String[] splits = header.split(","); //zh-CN  zh;q=0.9
String[] split = splits[0].split("-"); //zh CN
locale = new Locale(split[0], split[1]);//zh CN
//最终变成zh_CN,那么不用说,上面的是操作浏览器默认的请求信息的,前面的还有默认的展示第二次访问:我们点击中文:
请求信息:Accept-Language:zh-CN,zh;q=0.9
响应信息:Content-Language:zh-CN
String[] split = l.split("_"); //zh CN
locale = new Locale(split[0], split[1]);
//自然也是zh_CN,且也是默认的请求信息(因为我们前端没有改变)第三次访问:我们点击英文:
请求信息:Accept-Language:zh-CN,zh;q=0.9
响应信息:Content-Language:en-USString[] split = l.split("_"); //en US
locale = new Locale(split[0], split[1]); en_US*/
但是发现,好像没有什么变化啊,不要着急,首先分析原因,其中:界面的显示是根据响应信息来的,但是语言的展示是根据请求信息来的(需要偏好用户),也就是说,上面只是改变响应信息,对展示没有任何功能的作用,也就是说,我们所设置的就是错的,那么如何处理,或者怎么处理请求,这里需要这样:
首先在前端操作如下:
<button th:text="#{login.button}">
这个时候会发现,点击英文和中文有不同的区别了,为什么,首先在操作了国际化时,其中默认操作的是login,而不是login_en_US,login_zh_CN,我们先试着将后面两个名称中间修改一下,发现不能,说明他们是一套写法,通常必须写上正确的,否则展示会出现问题(88章博客也有具体的展示问题,在于删除默认的)
具体原理是什么:
首先我们知道,前面只是修改响应信息,去掉这个修改,还是一样的可以点击中文和英文进行改变,这里就要思考一个地方了,我们在测试时会执行三次,后面的两次就是默认的处理,换句话说,他们的处理都在判断选择哪一个文件(因为有两个login_en_US,login_zh_CN),判断好后,选择对应的一个文件来进行操作,最终给对应的login来操作具体的展示,如果没有这些文件,那么操作默认的或者跳过(对应于88章博客的删除对应的展示解释)(如果存在,但是是login开头的,且后面写法不对的,展示出现错误,对应于上面的解释),所以既然不能通过请求头来修改展示,但是我们可以通过修改国际化的处理来手动的进行修改的展示
也就是说,上面的判断在覆盖对应的组件后,就是用来判断操作哪一个配置文件的操作,至此,国际化操作说明完毕
SpringBoot缓存管理:
默认缓存管理:
Spring框架支持透明地向应用程序添加缓存对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法,从而减少操作数据的执行次数,同时不会对程序本身造成任何干扰,Spring Boot继承了Spring框架的缓存管理功能,通过使用@EnableCaching注解开启基于注解的缓存支持(spring的注解),Spring Boot就可以启动缓存管理的自动化配置,接下来针对Spring Boot支持的默认缓存管理进行讲解
首先spring boot的缓存是基于spring的缓存来操作的,那么我们先操作一下spring的缓存操作,我们举个例子:
首先创建一个项目,引入依赖:
 <dependency><!--Spring容器,即IOC容器的使用需要这个,比如ApplicationContext--><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.1.5.RELEASE</version></dependency>
创建com.pojo包,然后创建User类:
package com.pojo;public class User {private String id;private String name;public User(String id, String name) {this.id = id;this.name = name;}// Getters and Setterspublic String getId() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "User{" +"id='" + id + '\'' +", name='" + name + '\'' +'}';}}
然后在com包下创建一个service包,然后创建UserService类:
package com.service;import com.pojo.User;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;@Service
public class UserService {//用user.id作为key(其中包含调用者和方法名称来指定唯一),然后保存好返回值//当下次继续操作同样对象来调用这个方法,并且对应的user.id与保存的一样时,那么直接返回保存的返回值,而不是操作这个(可以通过反射得到信息的)//当然,这个key的组成还是需要看后面的测试才行,上面只是理论@Cacheable(value = "users", key = "#user.id")public User getUserById(User user) {// 模拟从数据库获取用户信息System.out.println("Fetching user from database...");return user;}@Cacheable(value = "users", key = "#user.id")public User updateUser(User user) {// 更新用户信息System.out.println("Updating user in database...");return user;}
}
然后我们创建spring.xml文件(在资源文件夹下),内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:cache="http://www.springframework.org/schema/cache"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/cachehttp://www.springframework.org/schema/cache/spring-cache.xsd"><context:component-scan base-package="com"/><!-- 启用注解驱动的缓存管理 --><cache:annotation-driven cache-manager="cacheManager"/><!-- 配置缓存管理器 --><bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager"><constructor-arg value="users"/> <!--直接操作值给构造函数,前提是有存在匹配他类型的单个参数的构造函数,否则自然会操作报错--></bean>
</beans>
然后在com包下,创建MainTest类:
package com;import com.pojo.User;
import com.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class MainTest {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");UserService userService = context.getBean(UserService.class);User updatedUser1 = new User("1", "Jane Doe");// 第一次调用获取用户System.out.println(userService.getUserById(updatedUser1));// 第二次调用获取用户System.out.println(userService.getUserById(updatedUser1));// 更新用户User updatedUser2 = new User("1", "Jane Doee");User user = userService.updateUser(updatedUser2);System.out.println(2);System.out.println(user);// 再次调用,获取更新后的用户System.out.println(userService.getUserById(updatedUser2));}
}
启动执行看看,若出现如下:
/*
Fetching user from database...
User{id='1', name='Jane Doe'}
User{id='1', name='Jane Doe'}
2
User{id='1', name='Jane Doe'}
User{id='1', name='Jane Doe'}
*/
说明操作成功,但是我们来分析一下key的组成,其中前面两个:
/*
User{id='1', name='Jane Doe'}
User{id='1', name='Jane Doe'}
是正常的,但是后面明明重新创建一个对象,那么应该不会操作啊,但是还是操作了,说明其key的组成,只会查看参数,也就是说,方法名称和对象都不看,但是还需要注意:必须操作对应的注解,如果没有,不会得到key的value值的*/
既然上面只看参数,那么我们如下操作:
@Service
public class a {@Cacheable(value = "users", key = "#user.id")public User fa(User user) {System.out.println(7);return null;}
}a a = context.getBean(a.class);User u = new User("1", "2");System.out.println(9);//当然,需要返回值,否则自然不会得到value的System.out.println(a.fa(u)); //User{id='1', name='Jane Doe'}
发现还是操作了User{id=‘1’, name=‘Jane Doe’},并且没有打印7,说明的确只看参数,判断成功后,直接给返回值,并不会执行里面的操作
然后修改User u = new User(“2”, “2”);,继续查看,发现是null的结果了,并且操作了打印7,说明的确只是操作参数的
至此我们的spring的测试完毕,那么现在我们操作spring boot是怎么处理的
但是在处理之前,首先需要分析,为什么他是根据参数的,在mybatis中也存在缓存,只不过他的缓存由SqlSession或者xml或者其他的决定,这都是缓存的处理,根据某个变量或者数据来决定,所以根据参数也并非不可能,或者说,只需要代表一个重要的数据或者某些唯一数据也行的,他们都可以作为缓存的标识,这里了解即可
那么现在我们操作spring boot的缓存:
基础环境搭建:
首先我们先创建两个数据库表,sql语句如下:
-- 创建数据库    
CREATE DATABASE springbootdata;    
-- 选择使用数据库    
USE springbootdata;    
-- 创建表t_article并插入相关数据    
DROP TABLE IF EXISTS t_article;    
CREATE TABLE t_article (      id int(20) NOT NULL AUTO_INCREMENT COMMENT '文章id',      title varchar(200) DEFAULT NULL COMMENT '文章标题',      content longtext COMMENT '文章内容',      PRIMARY KEY (id)    
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO t_article VALUES ('1', 'Spring Boot基础入门', '从入门到精通讲解...');    
INSERT INTO t_article VALUES ('2', 'Spring Cloud基础入门', '从入门到精通讲解...');        -- 创建表t_comment并插入相关数据   
DROP TABLE IF EXISTS t_comment;    
CREATE TABLE t_comment (      id int(20) NOT NULL AUTO_INCREMENT COMMENT '评论id',      content longtext COMMENT '评论内容',      author varchar(200) DEFAULT NULL COMMENT '评论作者',      a_id int(20) DEFAULT NULL COMMENT '关联的文章id',      PRIMARY KEY (id)    
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;  INSERT INTO t_comment VALUES ('1', '很全、很详细', 'lucy', '1');    
INSERT INTO t_comment VALUES ('2', '赞一个', 'tom', '1');    
INSERT INTO t_comment VALUES ('3', '很详细', 'eric', '1');    
INSERT INTO t_comment VALUES ('4', '很好,非常详细', '张三', '1');    
INSERT INTO t_comment VALUES ('5', '很不错', '李四', '2');
创建项目(项目的创建就不说明了,这些是非常简单的),添加如下的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><!--固定的--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.2</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>bootSpring</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--mysql驱动包--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><!--固定的--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><!--一般需要他来操作测试,如@RunWith(JUnit4.class)--><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency></dependencies><build><plugins><plugin><!--固定的--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
创建com.pojo包,然后创建Comment类:
package com.pojo;import javax.persistence.*;
//Entity不加,其他的注解可能不会起作用,他可以结合Table注解,比如这里
@Entity(name = "t_comment") // 设置ORM实体类,并指定映射的表名
//这里还有一个好处就是,在写原生sql时,不用设置为true了,否则需要
public class Comment {@Id  // 表明映射对应的主键id@GeneratedValue(strategy = GenerationType.IDENTITY) // 设置主键自增策略private Integer id;private String content;private String author;@Column(name = "a_id") //指定映射的表字段名private Integer aId;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public String getAuthor() {return author;}public void setAuthor(String author) {this.author = author;}public Integer getaId() {return aId;}public void setaId(Integer aId) {this.aId = aId;}@Overridepublic String toString() {return "Comment{" +"id=" + id +", content='" + content + '\'' +", author='" + author + '\'' +", aId=" + aId +'}';}
}
然后在com包下创建mapper包,然后创建CommentRepository接口:
package com.mapper;import com.pojo.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;import javax.transaction.Transactional;public interface CommentRepository extends JpaRepository<Comment, Integer> {//根据评论id修改评论作者author@Transactional@Modifying@Query("update t_comment c set c.author = ?1 where c.id=?2") //不是查询通常不考虑全部的是否原生nativeQuery(前面的设置的),还要?后面需要紧紧贴着,否则报错,比如"? 1"就会报错,而"?1"则不会,这是底层的判断操作//一般他通常都需要设置为原生的,所以增删改需要,但是删除可以省略一点,但是我们还是建议操作@Entity(name = "t_comment"),避免原生的设置public int updateComment(String author, Integer id);
}
其中@Transactional在spring中是给一个实例的方法操作的,这里虽然是接口,但是在接口上也会,一般来说:
在类上:当在类级别上使用 @Transactional时,该类中所有的公共方法都会被事务管理
方法上:当在方法级别上使用 @Transactional时,只有该方法会被事务管理
接口上:Spring会在实现类中自动应用这些事务设置(也就是说,其实现类会判断接口中对应的方法是否存在这个注解来操作事务)
@Modifying用于标识查询方法执行的是更新、删除等修改操作,而不是查询操作,默认情况下,Spring Data JPA 中的查询方法都是只读的,这个注解告诉 Spring Data JPA 这是一个修改操作
那么@Modifying上面不加可以吗:一般不可以,由于默认是只读的,也就是默认操作select,如果不加就会出现sql的问题,会报错,如:
/*
java.lang.IllegalStateException: Executing an update/delete query
*/
然后创建com.service包,继续创建CommentService类:
package com.service;import com.mapper.CommentRepository;
import com.pojo.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.Optional;@Service
public class CommentService {@Autowiredprivate CommentRepository commentRepository;public Comment findCommentById(Integer id) {Optional<Comment> comment = commentRepository.findById(id);if (comment.isPresent()) { //判断是否为null的值(自己看源码就知道了)Comment comment1 = comment.get();return comment1;}return null;}
}
然后再com包下创建controller包,然后创建CommentController类:
package com.controller;import com.pojo.Comment;
import com.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class CommentController {@Autowiredprivate CommentService commentService;@RequestMapping(value = "/findCommentById")public Comment findCommentById(Integer id) {Comment comment = commentService.findCommentById(id);return comment;}
}
配置文件:
application.yml:
# MySQL数据库连接配置
spring:datasource:url: jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTCusername: rootpassword: 123456#显示使用JPA进行数据库查询的SQL语句jpa:show-sql: true
#开启驼峰命名匹配映射
mybatis:configuration:map-underscore-to-camel-case: true
server:servlet:encoding:charset: UTF-8 #操作请求或者响应的编码
创建启动类,再com包下创建,如BootApplication类:
package com;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class BootApplication {public static void main(String[] args) {SpringApplication.run(BootApplication.class, args);}
}
启动,访问http://localhost:8080/findCommentById?id=1,看看结果吧,然后修改或者补充CommentService:
  public int updateComment(Integer id) {//返回是否修改成功(通常1代表成功,0代表失败)int ss = commentRepository.updateComment("ss", id);return ss;}
CommentController也进行补充:
 @RequestMapping(value = "/updateComment")public int updateComment(Integer id) {int i = commentService.updateComment(id);return i;}
启动,访问http://localhost:8080/updateComment?id=1,然后看数据库是否修改即可,去掉@Modifying注解,看看是否会报错:
然后我们可以发现的确报错了,并且也的确是:
/*
java.lang.IllegalStateException: Executing an update/delete query
*/
当然,不同版本之间可能错误是不同的,但是基本需要设置对应的注解
我们继续访问http://localhost:8080/findCommentById?id=1,看看后端的打印的sql语句
多输出几次,后面都会操作sql语句,也就是说,他并没有直接得到数据,其底层还是操作了对应的方法来操作sql,所以其是因为没有在Spring Boot项目中开启缓存管理,在没有缓存管理的情况下,虽然数据表中的数据没有发生变化,但每执行一次查询操作(本质是执行同样的SQL语句),都会访问一次数据库并执行一次SQL语句,这个时候我们可以选择修改数据库,会发现访问后的结果是不同的了
前面我们知道缓存只不过是一个标识,具体这个标识是变量还是什么数据都可以的,那么我们先来体验一下spring boot操作缓存的过程:
在启动类上加上:
package com;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;@SpringBootApplication
@EnableCaching
public class BootApplication {public static void main(String[] args) {SpringApplication.run(BootApplication.class, args);}
}
虽然是spring的注解,但是他只是用来支持可以扫描的注解的,类似于前面的:
 <!-- 启用注解驱动的缓存管理 --><cache:annotation-driven cache-manager="cacheManager"/><!-- 配置缓存管理器 --><bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager"><constructor-arg value="users"/> <!--直接操作值给构造函数,前提是有存在匹配他类型的单个参数的构造函数,否则自然会操作报错--></bean>
而由于spring boot的存在,其中cacheManager是自动配置的,那么我们只需要操作启用注解驱动的缓存管理即可,相当于看到相关注解就操作,类似于扫描,所以@EnableCaching就是操作cache:annotation-driven
使用@Cacheable注解对数据操作方法进行缓存管理,将@Cacheable注解标注在Service类的查询方法上,对查询结果进行缓存
 /*@Cacheable(cacheNames = "comment"):
cacheNames 指定缓存的名称为 comment,此名称用于标识缓存存储区
如果没有指定 key 属性,Spring 会使用默认的键生成策略,默认情况下,缓存的键将是方法的所有参数的组合,或者SimpleKey.EMPTY或者就是参数值
@Cacheable(value = "users", key = "#user.id"):
value 是 cacheNames 的别名(可以说是等价的,只是命名名称不同而已),同样指定缓存的名称为 users,这个名称用于标识缓存存储区
key 属性指定缓存的键,#user.id 表示使用传入的 user 对象的 id 属性作为缓存的键*/@Cacheable(cacheNames = "comment")public Comment findCommentById(Integer id) {Optional<Comment> comment = commentRepository.findById(id);if (comment.isPresent()) {Comment comment1 = comment.get();return comment1;}return null;}
上述代码中,在CommentService类中的findCommentById方法上添加了查询缓存注解@Cacheable,该注解的作用是将查询结果Comment存放在Spring Boot默认缓存中名称为comment 的名称空间(namespace)中,对应缓存唯一标识
启动,然后访问:http://localhost:8080/findCommentById?id=1,多次访问后发现只会操作一次sql,但是他们好像与普通的spring的缓存有一些区别,在前面操作spring时,需要配置一下对应的缓存名称,但是这里没有,为什么:如果使用 Spring Boot,Spring Boot 提供了自动配置功能,在大多数情况下,只要你在代码中使用了缓存注解(如 @Cacheable),Spring Boot 会自动配置一个默认的缓存管理器(通常是 ConcurrentMapCacheManager),而无需手动设置缓存名(通常可以设置名称为单纯指定的名称或者方法名称,只是底层继续判断操作罢了),再者,spring缓存本质是操作读取配置文件形成的,当然,也会给出配置类的形式来完成(一般是得到ConcurrentMapCacheManager),自然spring boot在自动配置时会处理到
换句话说,配置类和配置文件都是操作对应的名称的,可以并存,如果并存可能需要看先后顺序,一般来说,都是配置类优先的
底层结构:
在诸多的缓存自动配置类中,SpringBoot默认装配的是 SimpleCacheConfiguration
他使用的 CacheManager 是 ConcurrentMapCacheManager(实现了 CacheManager 接口),使用 ConcurrentMap 当底层的数据结构(缓存是:ConcurrentMapCache)
按照Cache的名字查询出Cache缓存列表,每一个Cache缓存列表中存在多个k-v键值对,即键和缓存值,也就是一个key对应一个值,换句话说,一个缓存里面存在多个key-value,上面的操作就是从缓存中找到comment的对应缓存,而对应缓存是map,名称是key,值是value
也就是说,每个缓存名称对应一个缓存映射(map),该映射存储了缓存的键值对,我们直接看代码来认识吧(代码是最直观的说明):
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.boot.autoconfigure.cache;import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;@Configuration(proxyBeanMethods = false
)
@ConditionalOnMissingBean({CacheManager.class})
@Conditional({CacheCondition.class})
class SimpleCacheConfiguration {SimpleCacheConfiguration() {}//看这里(这个代码很少,百度一下意思即可):@BeanConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers) {ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();List<String> cacheNames = cacheProperties.getCacheNames();if (!cacheNames.isEmpty()) {cacheManager.setCacheNames(cacheNames);}return (ConcurrentMapCacheManager)cacheManagerCustomizers.customize(cacheManager);}
}public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);//String是名称,Cache是缓存列表//..   @Nullablepublic Cache getCache(String name) {Cache cache = (Cache)this.cacheMap.get(name);if (cache == null && this.dynamic) {synchronized(this.cacheMap) {cache = (Cache)this.cacheMap.get(name);if (cache == null) {//这里cache = this.createConcurrentMapCache(name);this.cacheMap.put(name, cache);}}}return cache;}//..//这里protected Cache createConcurrentMapCache(String name) {SerializationDelegate actualSerialization = this.isStoreByValue() ? this.serialization : null;return new ConcurrentMapCache(name, new ConcurrentHashMap(256), this.isAllowNullValues(), actualSerialization);}
}public class ConcurrentMapCache extends AbstractValueAdaptingCache {//..   
}
public abstract class AbstractValueAdaptingCache implements Cache {//..
}
//得到的是Cache:cache = this.createConcurrentMapCache(name);public interface ConcurrentMap<K,V> extends Map<K,V> {//..   
}
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>implements ConcurrentMap<K,V>, Serializable {private static final long serialVersionUID = 7249069246763182397L;
//..
}//操作的缓存列表是:ConcurrentMapCache,保存的对应列表是:new ConcurrentHashMap(256),其中private final ConcurrentMap<Object, Object> store;,也就是说这里是key和value的地方//总结:一个名称对应于一个缓存标识,而这个缓存标识里面保存了一个缓存列表,这个缓存列表也是map,保存了key和value//通常来说,一个key默认是方法的参数值,而如果是多个参数或者没有参数,那么就是他们的组合了或者操作SimpleKey.EMPTY,当然,一般情况下,如果是单个参数,通常key就是参数,如果是无参或者多参,通常使用SimpleKeyGenerator来生成key,具体情况需要看他的实现,后面会说明的
缓存注解介绍:
刚刚通过使用@EnableCaching、@Cacheable注解实现了Spring Boot默认的基于注解的缓存管理,除此之外,还有更多的缓存注解及注解属性可以配置优化缓存管理
@EnableCaching注解:
@EnableCaching是由spring框架提供的,springboot框架对该注解进行了继承,该注解需要配置在类上(通常配置在项目启动类上),用于开启基于注解的缓存支持
@Cacheable注解:
@Cacheable注解也是由spring框架提供的,可以作用于类或方法(通常用在数据查询方法上),用于对方法结果进行缓存存储,注解的执行顺序是,先进行缓存查询,如果为空则进行方法查询,并将结果进行缓存,如果缓存中有数据,不进行方法查询,而是直接使用缓存数据
在方法上:如果没有指定或者配置默认名称,一般是方法名称
如果是类上的:如果没有指定或者配置默认名称,一般是方法名称,但是在类上操作,默认是里面所有的方法操作,这个时候由于名称如果一致,那么只有一个缓存了
那么他们的key是怎么处理的,key不会相同吗,一般来说,其key如果不操作指定,通常是不会相同的,可能会根据方法名来进行生成
还要注意:如果不加上名称,那么可能由于找不到缓存而保存,所以我们通常建议设置名称,而不是依靠默认的名称来处理,还有,spring中通常需要指定名称,因为他没有spring boot中的默认处理,如单纯的指定,或者方法名称操作,当然,在高版本下,spring boot通常也需要指定名称,而不会操作方法名称,所以综上所述:我们建议操作指定名称,这样不会出现任何问题
根据前面的所有说明进行总结:对应的名称必须写上,这个名称代表map(缓存)的标识或者key,map是自动装配操作得到的,其中map保存了缓存列表,也是map,保存了key和value,其中key若是单个参数,那么就是该参数,如果是多个或者无参,那么操作生成(一般是:SimpleKeyGenerator),value就是缓存值
简单来说,名称和缓存列表作为key和value放入map中(这个基本唯一),而这个缓存列表里面也存在一个map,里面保存了key和value(缓存值)
还要注意一点:他们由于是spring下面的,通常在初始化时就已经处理了,所以如果key不存在或者名称没有等等,通常启动时就会报错的(在spring中,基本没有在运行时再来判断,也就是注解的处理通常发生在初始化时,除非是我们手动写的,或者某些spring的处理,但是以后不确定了,这里了解即可),当然大多数框架基本都是如此(这里就不多说)
@Cacheable注解提供了多个属性,用于对缓存存储进行相关配置:

在这里插入图片描述

执行流程和时机:
方法运行之前,先去查询Cache(缓存组件,也就是缓存列表),一般是按照cacheNames指定的名字获取(CacheManager 先获取相应的缓存(列表),然后名称指定,这里由一个map来保存,并且这个map基本唯一),第一次获取缓存如果没有Cache组件会自动创建(创建后,名称指定,放入一个map集合),然后将key和value放入名称对应的缓存组件中,否则通过名称拿取组件,然后通过key得到value,其中key一般是如下:
比如默认就是方法的参数(当然,默认是建立在没有手动设置的情况下),如果多个参数或者没有参数,是按照某种策略生成的,默认是使用KeyGenerator生成的,使用SimpleKeyGenerator(实现了KeyGenerator接口)生成key, SimpleKeyGenerator生成key的默认策略:
public class SimpleKeyGenerator implements KeyGenerator {public SimpleKeyGenerator() {}//..
}

在这里插入图片描述

public class SimpleKey implements Serializable {public static final SimpleKey EMPTY = new SimpleKey(new Object[0]);private final Object[] params;private transient int hashCode;public SimpleKey(Object... elements) {Assert.notNull(elements, "Elements must not be null");this.params = (Object[])elements.clone();this.hashCode = Arrays.deepHashCode(this.params);}//..   
}
所以SimpleKey.EMPTY相当于空参数,否则就是多个参数,clone()方法只复制数组本身,而不会复制数组中对象的内容(所以是浅拷贝,深拷贝包括内容的),当然,这些浅拷贝或者深拷贝的专有名词是建议在引用类型上的说明,这里需要注意
常用的SPEL表达式(操作key的表达式,这里先了解,后面会说明):

在这里插入图片描述

继续总结:一个名称对应一个map,而保存他们的关系的也是map
@CachePut注解:
目标方法执行完之后生效,@CachePut被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个注解保证这个方法依然会执行(保证修改),执行之后的结果被保存在缓存中(这个时候建议返回修改后的数据使得覆盖原来的数据)
@CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同
其中更新操作,前端会把id+实体传递到后端使用,我们就直接指定方法的返回值(实体)为其缓存,其中重新存进缓存时的key=“#id”,如果前端只是给了实体,我们就使用 key=“#实体.id” 获取key,当然,由于他的执行时机考虑是目标方法结束后的返回,所以也可以使用 key=“#result.id”,拿出返回值的id,作为key,然后放入缓存,当然,具体怎么处理,看你自己
@CacheEvict注解:
@CacheEvict注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解的作用是删除缓存数据
@CacheEvict注解的默认执行顺序是,先进行方法调用,然后将缓存进行清除(具体还是找key的,看你是否操作指定或者是否操作默认了)
Spring Boot支持的缓存组件:
在Spring Boot中,数据的缓存管理存储依赖于Spring框架中cache相关的org.springframework.cache.Cache和org.springframework.cache.CacheManager缓存管理器接口(在前面就是CacheManager的实现类里面存在Cache),如果程序中没有定义类型为CacheManager的Bean组件或者是名为cacheResolver的CacheResolver缓存解析器(虽然通常都会存在Bean组件的,也就是前面操作的),Spring Boot将尝试选择并启用以下缓存组件(按照指定的顺序):
/*
1:Generic
2:JCache (JSR-107) (EhCache 3、Hazelcast、Infinispan等)
3:EhCache 2.x
4:Hazelcast
5:Infinispan
6:Couchbase
7:Redis
8:Caffeine
9:Simple,这个就是我们默认的,也就是SimpleCacheConfiguration
顺序的判断自然在代码上是可行的
*/
当然,他们要操作成功,首先是存在对应的路径,一般spring boot会判断是否存在对应的starter的,或者包或者说路径来进行处理的,上面按照Spring Boot缓存组件的加载顺序,列举了支持的9种缓存组件(当然,在以后可能会有所改变),在项目中添加某个缓存管理组件(例如Redis)后,Spring Boot项目会选择并启用对应的缓存管理器,如果项目中同时添加了多个缓存组件,且没有指定缓存管理器或者缓存解析器(CacheManager或者cacheResolver),那么 Spring Boot会按照上述顺序在添加的多个缓存中优先启用指定的缓存组件进行缓存管理,默认情况下,由于只存在Simple(第9),所以spring boot就会操作前面所操作的
在Spring Boot默认缓存管理中,没有添加任何缓存管理组件能实现缓存管理,这是因为开启缓存管理后,Spring Boot会按照上述列表顺序查找有效的缓存组件进行缓存管理,如果没有任何缓存组件,会默认使用最后一个Simple缓存组件进行管理,Simple缓存组件是Spring Boot默认的缓存管理组件,它默认使用内存中的ConcurrentMap进行缓存存储,所以在没有添加任何第三方缓存组件的 情况下,可以实现内存中的缓存管理,但是我们不推荐使用这种缓存管理方式 ,为什么:
内存使用限制:
ConcurrentMapCacheManager基于 Java 的 ConcurrentMap实现,它将所有缓存数据存储在 JVM 的堆内存中,如果缓存数据量大,可能会导致内存溢出(OutOfMemoryError),影响应用程序的稳定性和性能
缺乏持久性:
这种缓存实现是纯内存的,应用程序重启后缓存数据会丢失,如果需要缓存数据在重启后仍然可用,需要一个持久化的缓存解决方案
分布式支持不足:
ConcurrentMapCacheManager仅适用于单个 JVM 实例,不支持分布式缓存,这在分布式系统或微服务架构中是一个重大缺陷,因为每个实例都会有自己的缓存,无法共享数据
缺乏高级特性:
其他缓存解决方案(如 Redis、Ehcache、Hazelcast 等)提供了更多高级特性,如 TTL(生存时间)、LRU(最近最少使用)驱逐策略、统计监控、集群支持等,而 ConcurrentMapCacheManager 不提供这些高级特性
基于注解的Redis缓存实现:
在Spring Boot默认缓存管理的基础上引入Redis缓存组件,使用基于注解的方式讲解Spring Boot 整合Redis缓存的具体实现
首先我们创建项目,引入依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><!--固定的--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.2</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>bootredis</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>jakarta.persistence</groupId><artifactId>jakarta.persistence-api</artifactId></dependency><dependency><!--固定的--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><!--一般需要他来操作测试,如@RunWith(JUnit4.class)--><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency></dependencies>
</project>
<!--这里说明一下打包:前面我们虽然操作过,但是也只是操作,具体为什么还没有说明:
在spring boot虽然是内嵌tomcat,使得jar包,一般情况下,无论你怎么设置打包方式,由于他是代码层面的,所以一般都是jar包并且直接的启动,具体打包处理,需要maven的打包的,maven默认情况下,打包方式都是<packaging>jar</packaging>,前面的博客中存在这样的打包处理,就不多说了还有一些打包插件:
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
</build>
他只是补充了一些信息,具体本质上单纯的使用maven即可,一般他可以不需要,或者spring boot默认存在了(一般可以在父依赖中找到的)一般为什么spring boot可以打包成war呢,如果说一些程序中web项目可以打包被tomcat识别,那么spring boot怎么识别的呢,这是因为在spring boot项目中的打包处理中,spring boot提供了插件(其引入的父依赖中,如上面的打包插件),这个插件,使得进行打包时,操作一些代码,完成不启动启动类,但是使用启动类的其他处理,如spring扫描,自然可以创建war包,换句话说,覆盖了maven的传统war打包方式
所以maven三种打包:
jar默认
war打包
pom依赖的后续jar或者war打包,pom只是依赖的搞法,与打包没有具体关系,所以只需要考虑jar和war即可如果加上spring boot,那么就会覆盖原来的war打包,这样spring boot就可以操作具体的他的war打包方式了(具体方式了解即可,可以百度,这里就不多说了)
-->
创建com.pojo包,然后创建两个类(这里我们引入前面操作redis的过程):
package com.pojo;import org.springframework.data.redis.core.index.Indexed;public class Address {@Indexedprivate String city;@Indexedprivate String country;public Address(String city, String country) {this.city = city;this.country = country;}public Address() {}public String getCity() {return city;}public void setCity(String city) {this.city = city;}public String getCountry() {return country;}public void setCountry(String country) {this.country = country;}@Overridepublic String toString() {return "Address{" +"city='" + city + '\'' +", country='" + country + '\'' +'}';}
}
package com.pojo;import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;import javax.persistence.Id;@RedisHash("persons") // 指定操作实体类对象在Redis数据库中的存储空间
public class Person {@Id    // 标识实体类主键private String id;@Indexed // 标识对应属性在Redis数据库中生成二级索引private String firstname;@Indexedprivate String lastname;private Address address;public Person(String id, String firstname, String lastname, Address address) {this.id = id;this.firstname = firstname;this.lastname = lastname;this.address = address;}public Person() {}public String getId() {return id;}public void setId(String id) {this.id = id;}public String getFirstname() {return firstname;}public void setFirstname(String firstname) {this.firstname = firstname;}public String getLastname() {return lastname;}public void setLastname(String lastname) {this.lastname = lastname;}public Address getAddress() {return address;}public void setAddress(Address address) {this.address = address;}@Overridepublic String toString() {return "Person{" +"id='" + id + '\'' +", firstname='" + firstname + '\'' +", lastname='" + lastname + '\'' +", address=" + address +'}';}
}
创建com.mapper包,然后创建如下的接口:
package com.mapper;import com.pojo.Person;
import org.springframework.data.repository.CrudRepository;import java.util.List;public interface PersonRepository extends CrudRepository<Person, String> {List<Person> findByAddress_City(String name);
}
启动类:
package com;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class BootApplication {public static void main(String[] args) {SpringApplication.run(BootApplication.class, args);}
}
配置文件application.yml:
spring:#这里就是redis的配置了,通常是自动连接的,也就是说,启动redis,会连接上的(循环的)redis:#自己redis的地址(默认的服务器密码为空)host: 192.168.136.128 #redis注解配置port: 6379 #端口号
在测试java资源文件夹中创建com.te包,然后创建BootTest类:
package com.te;import com.mapper.PersonRepository;
import com.pojo.Address;
import com.pojo.Person;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)
@SpringBootTest
class BootTest {@Autowiredprivate PersonRepository repository;@Testpublic void savePerson() {Person person = new Person();person.setFirstname("张");person.setLastname("三");Address address = new Address();address.setCity("北京");address.setCountry("中国");person.setAddress(address);// 向Redis数据库添加数据Person save = repository.save(person);System.out.println(save);System.out.println(person==save);}
}
启动redis,执行上面的代码,看看结果吧
基础环境已经操作好了,接下来我们来说明:
当我们添加进redis相关的启动器之后(也就是Starter),SpringBoot会使用 RedisCacheConfigratioin 当做生效的自动配置类进行缓存相关的自动装配,容器中使用的缓存管理器是 RedisCacheManager,这个缓存管理器创建的Cache为 RedisCache,进而操控redis进行数据的缓存,他里面的操作自然是进行保存到redis中,而不是保存到jvm中
我们为了更加好的测试,引入jpa,也就是前面的操作,来操作一下数据库吧:
继续添加依赖:
 <dependency><!--这个jpa是操作数据库的,而数据库需要驱动,否则报错,而有驱动通常需要配置,有些必须配置,否则也会报错,如url地址--><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency>
在pojo包下创建:
package com.pojo;import javax.persistence.*;@Entity(name = "t_comment") // 设置ORM实体类,并指定映射的表名
public class Comment {@Id  // 表明映射对应的主键id@GeneratedValue(strategy = GenerationType.IDENTITY) // 设置主键自增策略private Integer id;private String content;private String author;@Column(name = "a_id") //指定映射的表字段名private Integer aId;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public String getAuthor() {return author;}public void setAuthor(String author) {this.author = author;}public Integer getaId() {return aId;}public void setaId(Integer aId) {this.aId = aId;}@Overridepublic String toString() {return "Comment{" +"id=" + id +", content='" + content + '\'' +", author='" + author + '\'' +", aId=" + aId +'}';}
}
创建com.mapper包(如果有,不需要创建),然后创建如下接口:
package com.mapper;import com.pojo.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;import javax.transaction.Transactional;public interface CommentRepository extends JpaRepository<Comment, Integer> {//根据评论id修改评论作者author@Transactional@Modifying@Query("update t_comment c set c.author = ?1 where c.id=?2")public int updateComment(String author, Integer id);
}
创建com.service和com.controller包,然后创建如下:
package com.service;import com.mapper.CommentRepository;
import com.pojo.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;import java.util.Optional;@Service
public class CommentService {@Autowiredprivate CommentRepository commentRepository;public Comment findCommentById(Integer id) {Optional<Comment> comment = commentRepository.findById(id);if (comment.isPresent()) {Comment comment1 = comment.get();return comment1;}return null;}}
package com.controller;import com.pojo.Comment;
import com.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class CommentController {@Autowiredprivate CommentService commentService;@RequestMapping(value = "/findCommentById")public Comment findCommentById(Integer id) {Comment comment = commentService.findCommentById(id);return comment;}}
上面需要依赖:
 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
先启动,然后访问:http://localhost:8080/findCommentById?id=1,若出现数据,说明我们操作成功
然后我们操作如下:
对CommentService类中的方法进行修改,使用@Cacheable、@CachePut、@CacheEvict三个注解定制缓存管理,分别进行缓存存储、缓存更新和缓存删除的演示
操作后的CommentService:
/*
大多数中间件在编写好后,基本上都会编写语言层面的连接(简称客户端库,而他中间件则是服务端,受请求的自然是服务端),比如java中引入对应的依赖来连接redis*/package com.service;import com.mapper.CommentRepository;
import com.pojo.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;import java.util.Optional;@Service
public class CommentService {@Autowiredprivate CommentRepository commentRepository;//#result代表结果,这里可以加上判断,这里就代表了,如果查询结果为null,那么不会操作操作缓存//换句话说,默认情况下,他的返回是false的(可以认为是判断来处理,或者通过字符串的执行来处理(这个虽然通常需要引入一些依赖))//如果不是null,自然将key是id(默认,没有指定的话),value是返回值放入缓存,注意这里是unless,而不是key,他是SPEL表达式(前面有说明),默认情况下他是false,即保存缓存@Cacheable(cacheNames = "comment", unless = "#result==null")public Comment findCommentById(Integer id) {Optional<Comment> comment = commentRepository.findById(id);if (comment.isPresent()) {Comment comment1 = comment.get();return comment1;}return null;}}
还要注意一点:由于缓存管理组件变成了redis,那么具体的保存则是redis的操作了,但是流程基本上还是一样的,一个名称对应一个缓存列表,该列表保存key和value
具体怎么保存,可以看源码,这里就不说明了
继续补充CommentService:
//返回值的id作为key,执行后,无论是否有缓存,都需要先执行才操作@CachePut(cacheNames = "comment", key = "#result.id")public Comment updateComment(Comment comment) {commentRepository.updateComment(comment.getAuthor(), comment.getaId());return comment;}
对应的CommentController里面补充如下:
 @RequestMapping(value = "/updateComment")public Comment updateComment(String str, Integer id) {Comment comment = new Comment();comment.setAuthor(str);comment.setId(id);return commentService.updateComment(comment);}
由于上面操作了类的缓存保存,一般来说,中间件操作类时基本会考虑序列化,而大多数序列化,通常需要类实现接口Serializable(自定义的序列化,通常是不需要的),所以我们给要操作的类,也就是Comment实现这个接口
在启动类中加上注解:@EnableCaching(需要让缓存起作用就需要他哦),然后访问(后面的测试访问,建议先清空redis数据库)如下:
http://localhost:8080/findCommentById?id=1,http://localhost:8080/findCommentById?id=123,当然,前提是启动redis,还有,有些中间件只有在执行对应代码时才会操作连接,否则不会,而redis也是如此,所以访问时操作缓存会连接redis,如果没有,那么访问上面的url就会报错,当然可能也不会报错,因为并非都会进行报错的处理,还有,报错也并非一定结束后续执行,比如报错信息只是打印的(catch的处理)
当然了,防止一些中间件的会操作一些心跳(判断是否存在),这种操作通常在启动时就会进行连接判断或者之间连接,如果没有启动,那么在spring boot启动时就会报错,导致启动失败,所以我们建议在java程序执行之前,先将其需要连接的中间件都先启动,而不是去博取他不会报错的情况,因为随着版本来说,的确需要这样的操作,这里了解即可,没有多大的意义
还有启动虚拟机时,如果出现不能启动(不正常关闭),可以删除其里面的所有.lck文件,基本万能,特别的是,一些连接虚拟机的操作通常会尝试重试,当然这个重试可能有时间限制(如Xshell)
多次执行,查看后端sql打印,其中id=1的只会打印一次,而后面的会打印多次(因为不会放入缓存)
还要注意的一点,redis是操作时连接,所以你启动后,再启动redis,也是可行的,你可以试一下,先不启动redis,访问一下,当出现错误后,再启动redis,再访问,自行测试吧
我们继续访问:http://localhost:8080/updateComment?id=1&str=ff,sql打印了,然后访问http://localhost:8080/findCommentById?id=1,是修改后的值,并且没有sql打印,因为放入了缓存,这个时候还是继续访问http://localhost:8080/updateComment?id=1&str=fg,sql打印了(因为修改是需要先执行的),其缓存值又是修改后的值了,继续访问http://localhost:8080/findCommentById?id=1,sql没有打印,得到的是修改后的值
至此,测试完毕,我们继续在CommentController中和CommentService中补充:
//CommentService中
@CacheEvict(cacheNames = "comment")public void deleteComment(int comment_id) {commentRepository.deleteById(comment_id); //会删除数据的}
//CommentController中  
@RequestMapping(value = "/deleteComment")public void deleteComment(Integer id) {commentService.deleteComment(id);}
启动后,访问http://localhost:8080/findCommentById?id=1,打印了sql,再访问http://localhost:8080/deleteComment?id=1,打印了sql,继续访问http://localhost:8080/findCommentById?id=1,发现还需要打印了,因为删除了缓存
但是还要注意一点:由于缓存是放在redis里面的,也就是说,重启spring boot,对应的缓存还是存在(而不是像默认的缓存管理重启就不在了,因为默认的是放在jvm的,重启的话,jvm自然也重启了),所以上面的操作可以建议先访问http://localhost:8080/deleteComment?id=1
以上使用@Cacheable、@CachePut、@CacheEvict注解在数据查询、更新和删除方法上进行了缓存管理
首先我们清空redis数据,访问http://localhost:8080/findCommentById?id=2(前面已经删除数据了,所以id=2),当然了CacheEvict对应的方法也可以不操作删除,他只是对缓存删除处理而已,具体测试数据可以不进行删除的
访问之后,我们看redis的数据,展示基本不好看,这个时候我们自然需要考虑序列化处理,一般正确存储的是这样的数据
/*
127.0.0.1:6379> keys *
1) "comment::2"
127.0.0.1:6379> 
*/
具体显示是如此:

在这里插入图片描述

当然,上面序列化的处理前面已经说明了,这里了解即可
执行上面方法查询出的用户评论信息Comment正确存储到了Redis缓存库中,其中"comment(cacheNames的值)"是缓存的名称空间下,缓存数据的唯一标识key值是以"缓存名称空间::参数值(comment::2)"的字符串形式体现的,而value值则是经过JDK默认序列格式化后的HEX格式存储(或者说是默认操作到redis的序列化,而默认操作的序列化则是jdk造成的序列化,这种操作编码或者序列化经过一系列处理后,变成上面的情况),这种JDK默认序列格式化后的数据显然不方便缓存数据的可视化查看和管理,所以在实际开发中,通常会自定义数据的序列化格式,前面我们就已经自定义了
另外,还可以在Spring Boot全局配置文件中配置Redis有效期,示例代码如下:
spring:cache: redis.time-to-live: 10000  
这个配置好后,等待10秒,缓存会自动的清除(或者说,添加到redis的数据会加上过期时间),重启,试一试吧
还有,在前面可能说过,一些依赖可能只会在测试资源文件夹下操作,除了自身的处理外,大多数基本都是因为配置文件中设置了scope为test的缘故,因为检查是不会骗人的,基本上依赖都可以在测试和开发等环境中处理,至于存在只能在测试中处理,通常指运行过程中路径的判断,而出现的一系列结果,这里了解即可
基于API的Redis缓存实现:
在Spring Boot整合Redis缓存实现中,除了基于注解形式的Redis缓存实现外,还有一种开发中常用的方式,也就是基于API的Redis缓存实现,这种基于API的Redis缓存实现,需要在某种业务需求下通过 Redis提供的API调用相关方法实现数据缓存管理,同时,这种方法还可以手动管理缓存的有效期(而不是全部按照配置文件的过期时间来操作,比较灵活)
简单来说,就是手动的设置缓存而已,这里我们通过api来模拟前面的注解操作
那么我们再service包下创建一个ApiCommentService类:
package com.service;import com.mapper.CommentRepository;
import com.pojo.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;import java.util.Optional;
import java.util.concurrent.TimeUnit;@Service
public class ApiCommentService {@Autowiredprivate CommentRepository commentRepository;@Autowiredprivate RedisTemplate redisTemplate;//用代码的方式来完成注解缓存的处理//大多数注解的缓存处理通常是代理增强而已(放入redis的增强),redis中则具体与下面的代码是类似的public Comment findCommentById(Integer id) {//得到对应的comment_的值Object o = redisTemplate.opsForValue().get("comment_" + id);if (o != null) {//存在,那么直接返回return (Comment) o;} else {//缓存中没有,从数据库查询Optional<Comment> byId = commentRepository.findById(id); //前后可以看成增强,相当于返回后,然后增强,只不过api这里是在返回时中间开始的if (byId.isPresent()) {Comment comment = byId.get();//将查询结果存入到缓存中,并设置有效期为1天redisTemplate.opsForValue().set("comment_" + id, comment, 1, TimeUnit.DAYS);return comment;} else {return null;}}}//有些情况下,jpa和mp有所不同,特别的,jpa一般会再更新后得到更新后的对象,而mp则不会public Comment updateComment(Comment comment) {commentRepository.updateComment(comment.getAuthor(), comment.getaId());//更新数据后进行缓存更新redisTemplate.opsForValue().set("comment_" + comment.getId(), comment);return comment;}public void deleteComment(int comment_id) {commentRepository.deleteById(comment_id); //这里可以不删除,随便写什么也可以(前面也提醒过,如"当然了CacheEvict对应的方法也可以不操作删除,他只是对缓存删除处理而已,具体测试数据可以不进行删除的")redisTemplate.delete("comment_" + comment_id);}
}
在controller包下创建ApiCommentController类:
package com.controller;import com.pojo.Comment;
import com.service.ApiCommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("api")
public class ApiCommentController {@Autowiredprivate ApiCommentService commentService;@RequestMapping(value = "/findCommentById")public Comment findCommentById(Integer id) {Comment comment = commentService.findCommentById(id);return comment;}@RequestMapping(value = "/updateComment")public Comment updateComment(String str, Integer id) {Comment comment = new Comment();comment.setAuthor(str);comment.setId(id);return commentService.updateComment(comment);}@RequestMapping(value = "/deleteComment")public void deleteComment(int id) {commentService.deleteComment(id);}
}
基于API的Redis缓存实现不需要@EnableCaching注解,所以这里可以选择将添加在项目启动类上的@EnableCaching进行删除或者注释
那么我们来操作吧,同样的,流程也是一样,具体测试根据前面的即可,以后我们建议使用api的方式来操作缓存,因为更加的灵活,如果需要好看点,可以操作注解,只是不够灵活而已,大多数情况下,我们通常都会使用api的方式的
而且redis作为非关系型数据库,他并不是像关系型数据库一样的,这里需要说明关系型数据库和非关系型数据库的区别了:
非关系型数据库和关系型数据库的区别:
1:数据结构:
关系型数据库:重于表的结构,数据以表格形式组织,表与表之间通过外键建立关系,每个表都有预定义的模式(Schema),要求数据严格遵循这种模式,预定义的模式(Schema)是指数据库在创建表时,需要定义表的结构,包括表的名称、字段的名称、数据类型、约束条件(如主键、外键、唯一性约束、非空约束等)等,这个模式在关系型数据库中是必须的,数据必须严格遵循这种预定义的模式进行存储和操作
非关系型数据库:没有固定的表结构,可以存储键值对、文档、列族或图形等多种数据结构,它们更加灵活,适合处理非结构化或半结构化数据,比如redis数据结构就是键值对,mongodb数据结构则是文档(JSON-like 格式)
2:存储方式:
关系型数据库:数据存储在行和列的表格中,使用SQL语言进行操作
非关系型数据库:根据不同类型存储方式不同,比如键值数据库(如Redis)、文档数据库(如MongoDB)、列族数据库(如HBase)、图数据库(如Neo4j)等
3:查询语言:
关系型数据库:使用结构化查询语言(SQL)进行查询和操作,提供丰富的查询功能和复杂的事务处理能力。
非关系型数据库:通常使用各自的查询API或查询语言,操作方式多样,但一般不支持复杂的事务处理
4:应用场景:
关系型数据库:适用于需要复杂查询、事务处理的场景,比如金融系统、ERP系统、传统企业应用等
非关系型数据库:适用于大数据、实时分析、高并发读写等场景,比如社交网络、物联网、大数据分析等
5:事务支持:
关系型数据库:遵循ACID特性(原子性、一致性、隔离性、持久性),能够提供强一致性的事务处理
非关系型数据库:通常遵循BASE理论(基本可用、软状态、最终一致性),在高可用性和一致性之间进行权衡,更适合分布式系统
ACID(ACID是关系型数据库中事务的四个关键属性,确保数据的一致性和可靠性):
原子性(Atomicity):
描述:事务中的所有操作要么全部成功,要么全部失败,不会出现部分完成的状态
示例:银行转账过程中,钱从一个账户扣除并存入另一个账户,这两个操作要么都成功,要么都失败
一致性(Consistency):
描述:事务开始之前和结束之后,数据库的状态必须是合法的,必须满足所有的预设规则和约束
示例:在转账操作后,总金额应保持不变,数据库状态一致
隔离性(Isolation):
描述:一个事务的执行不应受到其他事务的干扰,事务之间的操作应该相互隔离
示例:两个转账操作同时进行,一个不会看到另一个中间状态的数据
持久性(Durability):
描述:一旦事务提交,其结果应该永久保存在数据库中,即使发生系统故障也不会丢失
示例:转账操作完成后,即使系统崩溃,交易结果仍然保留
上面主要说明强一致,但是一般是非高可用
BASE(BASE是NoSQL数据库中一种事务模型,更加侧重于系统的可用性和性能,特别适合分布式系统):
基本可用(Basically Available):
描述:系统在故障时允许部分功能的降级,但核心功能仍然可用
示例:在高峰期,电商网站可能会限制某些非关键功能,但购物车和结账功能仍然正常运行
软状态(Soft State):
描述:系统中的数据状态可以是暂时不一致的,允许一定时间内的数据同步延迟
示例:数据在不同节点之间同步可能会有延迟,允许短时间内的不一致
最终一致性(Eventual Consistency):
描述:系统保证在没有新的更新操作后,所有副本最终会达到一致状态
示例:社交媒体上发布一条状态,可能在不同的用户设备上显示有延迟,但最终所有用户都能看到一致的状态
上面主要说明弱一致和高可用
主要区别:
ACID:强调强一致性,事务必须满足所有一致性约束,事务在每个时刻都保持一致状态
BASE:允许短暂的不一致性,最终在一段时间后达到一致性,更关注系统的可用性和性能
非关系型数据库也存在隔离性和持久性,还有原子性,所以他们的主要区别抛开其他的任何说明,就只有一致性的问题了,默认关系型数据库就是强一致性的,但是可用通常不是高可用,非关系型数据库一般都是弱一致性,但是高可用
6:性能:
关系型数据库:在处理关系型数据时性能较好,但在高并发、大数据量场景下可能会出现性能瓶颈
非关系型数据库:在高并发、大数据量场景下性能较好,尤其在数据读写频繁的情况下表现优异,因为其数据结构的原因
7:可扩展性的难度:
垂直扩展:一般指当前机器的性能直接提升,如用好的机器
水平扩展:多用几个机器做集群
关系型数据库:在垂直扩展时,一般较为容易,但是水平扩展较难
非关系型数据库:在垂直扩展时,虽然也容易,但是他在水平扩展上更加的好,所以无论是垂直还是水平,非关系型数据库都是好的
继续给出测试流程吧:
http://localhost:8080/api/findCommentById?id=2,http://localhost:8080/api/findCommentById?id=123,访问第一个,打印sql,继续访问则没有,第二个一直访问都会有sql打印(因为没有缓存)
清空redis数据库,我们继续访问:http://localhost:8080/api/updateComment?id=2&str=ff,sql打印了,然后访问http://localhost:8080/api/findCommentById?id=2,是修改后的值,并且没有sql打印,因为放入了缓存,这个时候还是继续访问http://localhost:8080/api/updateComment?id=2&str=fg,sql打印了(因为修改是需要先执行的),其缓存值又是修改后的值了,继续访问http://localhost:8080/api/findCommentById?id=2,sql没有打印,得到的是修改后的值
清空redis数据库,访问http://localhost:8080/api/findCommentById?id=2,打印了sql,再访问http://localhost:8080/api/deleteComment?id=2,打印了sql,继续访问http://localhost:8080/api/findCommentById?id=2,发现还需要打印了,因为删除了缓存
这里我们也可以看一下,我们api的序列化保存的结果,自然与前面注解是一样的,因为都是操作默认的序列化,但是通过注解的方式与api手动的处理中,数据有一点不同:
以访问http://localhost:8080/findCommentById?id=3和http://localhost:8080/api/findCommentById?id=3为例子:

在这里插入图片描述

为什么会这样:前面说明的是缓存管理器,并没有说明序列化器,一般他们默认操作的是同一个序列化器,只不过注解的形式会操作缓存管理器,而redis中的缓存管理器会额外的处理(处理序列化或者结果),所以上面的注解操作得到的结果(可以认为操作了好的编码和序列化)与api操作的是不同的结果,一般默认的是JdkSerializationRedisSerializer(JDK序列化,很明显他是redis提供的操作,因为存在redis的字眼,但是虽然是redis提供的,但是具体操作还是JDK的序列化处理),而额外的处理可能是让数据合理,或者操作了手动的序列化,这里了解即可,通常jdk序列化是指:使用 Java 的 ObjectOutputStream来序列化对象,这意味着它将对象转换为 Java 的字节流格式(OutputStream)(byte[]),这是 Java 的原生序列化机制所生成的格式
自定义Redis缓存序列化机制:
自定义RedisTemplate:
前面我们虽然自定义了,这里我们具体更加的细说,或者换一个方式来操作:
Redis API默认序列化机制:
基于API的Redis缓存实现是使用RedisTemplate模板(前面的:private RedisTemplate redisTemplate;)进行数据缓存操作的,这里打开 RedisTemplate类,查看该类的源码信息
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {//..@Nullableprivate RedisSerializer keySerializer = null;@Nullableprivate RedisSerializer valueSerializer = null;//..   // 进行默认序列化方式设置,设置为JDK序列化方式public void afterPropertiesSet() {super.afterPropertiesSet();boolean defaultUsed = false;if (this.defaultSerializer == null) {//的确是JdkSerializationRedisSerializer(JDK序列化)this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());}if (this.enableDefaultSerializer) {//如果key对应的序列化为null,那么使用默认的if (this.keySerializer == null) {this.keySerializer = this.defaultSerializer;defaultUsed = true;}//如果value对应的序列化为null,那么使用默认的if (this.valueSerializer == null) {this.valueSerializer = this.defaultSerializer;defaultUsed = true;}if (this.hashKeySerializer == null) {this.hashKeySerializer = this.defaultSerializer;defaultUsed = true;}if (this.hashValueSerializer == null) {this.hashValueSerializer = this.defaultSerializer;defaultUsed = true;}}if (this.enableDefaultSerializer && defaultUsed) {Assert.notNull(this.defaultSerializer, "default serializer null and not all serializers initialized");}if (this.scriptExecutor == null) {this.scriptExecutor = new DefaultScriptExecutor(this);}this.initialized = true;}//..
}
从上述RedisTemplate核心源码可以看出,在RedisTemplate内部声明了缓存数据key、value的各种序列化方式,且初始值都为空
在afterPropertiesSet()方法中,判断如果默认序列化参数 defaultSerializer为空
将数据的默认序列化方式设置为JdkSerializationRedisSerializer
根据上述源码信息的分析,可以得到以下两个重要的结论:
1:使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是 JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接口(例如Serializable),虽然大多数序列化都需要实现这个接口
2:使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式 defaultSerializer,那么将使用自定义的序列化方式,如redisTemplate.setKeySerializer(new StringRedisSerializer());,当然这个自定义是设置的意思,我们可以看他的这个setKeySerializer方法:
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {//..public void setKeySerializer(RedisSerializer<?> serializer) {this.keySerializer = serializer;}//..
}
很明显根据前面的代码:
  //如果key对应的序列化为null,那么使用默认的if (this.keySerializer == null) {this.keySerializer = this.defaultSerializer;defaultUsed = true;}//如果value对应的序列化为null,那么使用默认的if (this.valueSerializer == null) {this.valueSerializer = this.defaultSerializer;defaultUsed = true;}
这里setKeySerializer自然设置的是key的序列化
那么为什么afterPropertiesSet会执行呢,通过调试,最终是调用initializeBean来使得执行的,而initializeBean是spring中操作afterPropertiesSet方法的处理,这时实现接口会自动调用的,这里了解即可,可以到107章博客找到
所以他的确会执行
另外,在RedisTemplate类源码中,看到的缓存数据key、value的各种序列化类型都是 RedisSerializer,进入RedisSerializer源码查看RedisSerializer支持的序列化方式(进入该类后,使用 Ctrl+Alt+左键单击类名查看)

在这里插入图片描述

里面的JdkSerializationRedisSerializer则是默认的,总共有7个实现类,当然不同的版本可能数量不同,我这里是7个
自定义RedisTemplate序列化机制:
在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效(前面说明的RedisCacheConfigratioin 是缓存管理方面的),打开 RedisAutoConfiguration类(注意"fi"这两个字母,可能复制时会出现问题),查看内部源码中关于RedisTemplate的定义方式:
@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {//..@Bean@ConditionalOnMissingBean(name = {"redisTemplate"})@ConditionalOnSingleCandidate(RedisConnectionFactory.class) //查看是否存在RedisConnectionFactory类型的bean,如果存在,那么这里可以操作@Bean得到bean,否则不会(依赖关系,所以存在先后)//上面是@Bean所以可以得到public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate();template.setConnectionFactory(redisConnectionFactory);return template;}//..
}
从上述RedisAutoConfiguration核心源码中可以看出,在Redis自动配置类中,通过Redis连接工厂 RedisConnectionFactory初始化了一个RedisTemplate(至于连接工厂在哪里创建,这里就不多说,肯定通常是配置类处理的),上方添加了 @ConditionalOnSingleCandidate注解(当某个Bean存在时生效,有且只有一个,通常还有这个注解:@ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效))
一般@ConditionalOnMissingBean是考虑自定义,而@ConditionalOnSingleCandidate考虑参数中的存在(也可以认为是参数的自定义,只不过他可能有其他不受你控制的处理,因为是参数,自然大多数都会被处理,而不是不处理(很少的情况))
他们两个通常是Spring boot中的注解,很显然,在spring进行创建bean时,可能由一些接口拦截来进行判断(一般是通过spring类的接口的实现方法来对bean的创建结果进行处理,比如返回null(实例对应存在也可能为null的),或者删除等等),这里了解即可,这非常复杂,特别的,随着时间的推移,整合的框架基本都是很多人写的,单独一人要说明清除非常难,以后可以考虑一下
注意:RedisTemplate封装的就是连接redis的类,具体了解即可
如果想要使用自定义序列化方式的RedisTemplate进行数据缓存操作,而不是set进行设置,可以参考上述核心代码创建 一个名为redisTemplate的Bean组件,并在该组件中设置对应的序列化方式即可
接下来,在项目中创建名为com.config的包,在该包下创建一个Redis自定义配置类 RedisConfig,并按照上述思路自定义名为redisTemplate的Bean组件:
package com.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;@Configuration
public class RedisConfig {//既然对应的源码中存在RedisConnectionFactory实例,那么这里自然也可以得到,只是idea检查不出来而已,因为是运行时//或者说idea一般检查的是当前项目的代码(其他jar包可能不会检查到,但一般父子可以,具体可能会随着时间而改变)@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 创建一个 RedisTemplate 实例,RedisTemplate 是 Spring Data Redis 提供的模板类,用于与 Redis 进行交互RedisTemplate<Object, Object> template = new RedisTemplate();// 设置 RedisConnectionFactory,这个工厂类用于创建 Redis 连接template.setConnectionFactory(redisConnectionFactory);//上面是默认的处理,那么我们需要自定义的处理,自然不能只是这样//所以看如下:// 使用JSON格式序列化对象,对缓存数据key和value进行转换,自带的7种里面的其中一个Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);// 解决查询缓存转换异常的问题,创建一个 ObjectMapper 实例,用于配置 JSON 序列化和反序列化的行为ObjectMapper om = new ObjectMapper();// 设置 ObjectMapper 的可见性策略,允许所有字段和方法都被访问(即使它们是私有的)om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 启用默认类型的处理,以支持序列化和反序列化时对对象类型的自动处理// 这样可以在反序列化时正确地恢复对象的类型om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);// 将配置好的 ObjectMapper 设置到 Jackson2JsonRedisSerializer 中jacksonSeial.setObjectMapper(om);//简单来说ObjectMapper是防止json的潜在问题,建议补上// 设置RedisTemplate模板API的序列化方式为JSON,使得key和value使用这个,而不是默认的template.setDefaultSerializer(jacksonSeial);//一般情况下,具体的操作设置序列化在这个代码之后,可以调试即可,或者说是配置类扫描之后,一般这样的操作依赖,通常都是最开始的,如创建bean等//而只有创建bean后,才会考虑其他的处理,如方法的操作,那么前面的afterPropertiesSet自然就是后执行了return template;}
}//注意:使用了自定义的,那么原来的实例呢,答:如果名称是redisTemplate,比如这里,那么就会进行替换,这是因为这里的注解虽然也是创建实例,但是他是比默认的要后操作的,这是因为自动配置先于扫描被注册操作,spring中是按照顺序遍历注册的信息beanDefinition来操作创建bean(这里可以参照107章博客),所以我们后处理
这样,对应的序列化就不是默认的了,而是我们设置的,我们访问http://localhost:8080/findCommentById?id=3和http://localhost:8080/api/findCommentById?id=3看看redis保存的结果吧(在一些情况下,如果编码一致还出现乱码,一般是给出的数据不能识别造成的),一般第二个方法比较好看了,与前面的GenericJackson2JsonRedisSerializer有点类似,也可以看看其value的值了
/*
127.0.0.1:6379> get "comment::3"
"\xac\xed\x00\x05sr\x00\x10com.pojo.Comment\x91\x06N\x14\x02\xfbM\x82\x02\x00\x04L\x00\x03aIdt\x00\x13Ljava/lang/Integer;L\x00\x06authort\x00\x12Ljava/lang/String;L\x00\acontentq\x00~\x00\x02L\x00\x02idq\x00~\x00\x01xpsr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01t\x00\x04erict\x00\t\xe5\xbe\x88\xe8\xaf\xa6\xe7\xbb\x86sq\x00~\x00\x04\x00\x00\x00\x03"
127.0.0.1:6379> keys *
1) "comment::3"
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\tcomment_3"
127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\tcomment_3"
"\xac\xed\x00\x05sr\x00\x10com.pojo.Comment\x91\x06N\x14\x02\xfbM\x82\x02\x00\x04L\x00\x03aIdt\x00\x13Ljava/lang/Integer;L\x00\x06authort\x00\x12Ljava/lang/String;L\x00\acontentq\x00~\x00\x02L\x00\x02idq\x00~\x00\x01xpsr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01t\x00\x04erict\x00\t\xe5\xbe\x88\xe8\xaf\xa6\xe7\xbb\x86sq\x00~\x00\x04\x00\x00\x00\x03"//上面是使用默认的序列化//下面是使用我们自定义的序列化127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> keys *
1) "\"comment_3\""
127.0.0.1:6379> get "\"comment_3\""
"[\"com.pojo.Comment\",{\"id\":3,\"content\":\"\xe5\xbe\x88\xe8\xaf\xa6\xe7\xbb\x86\",\"author\":\"eric\",\"aId\":1}]"
127.0.0.1:6379> keys *
1) "comment::3"
2) "\"comment_3\""
127.0.0.1:6379> get "comment::3"
"\xac\xed\x00\x05sr\x00\x10com.pojo.Comment\x91\x06N\x14\x02\xfbM\x82\x02\x00\x04L\x00\x03aIdt\x00\x13Ljava/lang/Integer;L\x00\x06authort\x00\x12Ljava/lang/String;L\x00\acontentq\x00~\x00\x02L\x00\x02idq\x00~\x00\x01xpsr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01t\x00\x04erict\x00\t\xe5\xbe\x88\xe8\xaf\xa6\xe7\xbb\x86sq\x00~\x00\x04\x00\x00\x00\x03"
127.0.0.1:6379> keys *
1) "\"comment_3\""
127.0.0.1:6379> */
为什么操作缓存的使用自定义的序列化是不行的,这就需要前面我们说过的处理了,前面我们说过redis中的缓存管理器会额外的处理,一般可以认为他对key是一个他操作的序列化(可以认为是前面的StringRedisSerializer),但是默认value的序列化可能就是jdk序列化,所以上面的结果中get "comment::3"不变
通过@Configuration注解定义了一个RedisConfig配置类,并使用@Bean注解注入了一个默认名称为方法名的redisTemplate组件(注意,该Bean组件名称必须是redisTemplate),在定义的Bean组件中,自定义了一个RedisTemplate,使用自定义的Jackson2JsonRedisSerializer数据序列化方式,在定制序列化方式中,定义了一个ObjectMapper用于进行数据转换设置
为什么名称必须是redisTemplate,因为Spring Boot整合redis后,会操作配置类的,一般会提供两个对象,即RedisConnectionFactory和RedisTemplate,其中RedisTemplate在操作变量名时,一般只能使用redisTemplate,否则是会报错的,这是因为一般有多个他,那么只能操作名称(完全相同),而Autowired注解操作时,如果存在多个实例,但是变量名称与对应操作的名称一样,那么可以注入而不会报错,由于Spring Boot 的自动配置机制会设置名称,即使用默认名称redisTemplate,所以我们建议变量名称为redisTemplate,当然了,上面说明的必须是夸张的说明,只是正常情况下,我们建议一样
自定义RedisCacheManager:
前面我们知道,缓存管理有很多判断,上面是使用redis的管理,进而引出序列化的问题,这里我们回到缓存管理的问题:
刚刚针对基于 API方式的RedisTemplate进行了自定义序列化方式的改进,从而实现了JSON序列化方式缓存数据,但是这种自定义的RedisTemplate对于基于注解的Redis缓存来说,是没有作用的,这是因为其缓存管理中进行了额外的处理,上面的测试可以看到:
所以我们需要看如下:
Redis注解默认序列化机制:
要解决其额外的序列化处理,自然需要直接看他的缓存管理器,其中RedisCacheConfiguration得到缓存管理RedisCacheManager,这个缓存管理器创建的Cache为 RedisCache,具体操作可能也是前面默认缓存管理的通过map来的,具体看源码
我们先进入RedisCacheConfiguration:
@Configuration(proxyBeanMethods = false
)
@ConditionalOnClass({RedisConnectionFactory.class})
@AutoConfigureAfter({RedisAutoConfiguration.class})
@ConditionalOnBean({RedisConnectionFactory.class})
@ConditionalOnMissingBean({CacheManager.class})
@Conditional({CacheCondition.class})
class RedisCacheConfiguration {RedisCacheConfiguration() {}@BeanRedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers, RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {// 创建 RedisCacheManagerBuilder,使用 RedisConnectionFactory 来配置缓存管理器// 设置缓存的默认配置(cacheDefaults)RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(this.determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));// 如果 cacheProperties 中指定了缓存名称,则初始化这些缓存名称List<String> cacheNames = cacheProperties.getCacheNames();if (!cacheNames.isEmpty()) {builder.initialCacheNames(new LinkedHashSet(cacheNames));}// 如果启用了统计功能,则启用统计if (cacheProperties.getRedis().isEnableStatistics()) {builder.enableStatistics();}// 使用自定义的 RedisCacheManagerBuilderCustomizer 来定制 RedisCacheManagerBuilderredisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> {customizer.customize(builder);});// 使用 CacheManagerCustomizers 来定制最终的 RedisCacheManager 实例return (RedisCacheManager)cacheManagerCustomizers.customize(builder.build());}//..
}//我们可以进入determineConfiguration方法里面:
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(CacheProperties cacheProperties, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ClassLoader classLoader) {return (org.springframework.data.redis.cache.RedisCacheConfiguration)redisCacheConfiguration.getIfAvailable(() -> {return this.createConfiguration(cacheProperties, classLoader);});}//进入return this.createConfiguration(cacheProperties, classLoader);里面private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(CacheProperties cacheProperties, ClassLoader classLoader) {Redis redisProperties = cacheProperties.getRedis();org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));if (redisProperties.getTimeToLive() != null) {config = config.entryTtl(redisProperties.getTimeToLive());}if (redisProperties.getKeyPrefix() != null) {config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());}if (!redisProperties.isCacheNullValues()) {config = config.disableCachingNullValues();}if (!redisProperties.isUseKeyPrefix()) {config = config.disableKeyPrefix();}return config;}//上面总结起来可以看到存在JdkSerializationRedisSerializer//上面一直操作的都是config,可以适当的总结一下,他们最终会被赋值,被赋值给RedisCacheManager.RedisCacheManagerBuilder里面的defaultCacheConfiguration/*
RedisCacheConfiguration
.defaultCacheConfig.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));.entryTtl(redisProperties.getTimeToLive());.prefixCacheNameWith(redisProperties.getKeyPrefix());.disableCachingNullValues
.disableKeyPrefix存在这样的得到RedisCacheConfiguration的
*/
从上述核心源码中可以看出,同RedisTemplate核心源码类似,RedisCacheConfiguration内部同样通过Redis连接工厂RedisConnectionFactory定义了一个缓存管理器RedisCacheManager(builder),同时定制 RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式(具体可以百度),所以导致,虽然我们自定义了对应的序列化方式,但是使用缓存时,他进行额外的处理,导致使用的还是jdk序列化方式,具体的key可能是一种,而value也是一种,这里了解即可,通常可以再RedisCacheManager中看到如下:
private final Map<String, RedisCacheConfiguration> initialCacheConfiguration;
其中RedisCacheConfiguration:
public class RedisCacheConfiguration {private final Duration ttl;private final boolean cacheNullValues;private final CacheKeyPrefix keyPrefix;private final boolean usePrefix;private final SerializationPair<String> keySerializationPair;private final SerializationPair<Object> valueSerializationPair;private final ConversionService conversionService;//..}//对前面的config.serializeValuesWith进入会到上面的,这里了解即可
一般对应的map可能只有一个,String可能会将key和缓存名称结合,所以前面出现comment::3,前面没有操作redis的基本是两个map
所以如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以参考上述核心代码创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可,因为@ConditionalOnMissingBean({CacheManager.class}),前面说明了"通常还有这个注解:@ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效))",这样RedisCacheConfiguration就不会进行处理了,而他不处理,自然的,就不会得到对应的RedisCacheManager,那么由于这个是自动的,而之所以自动,在前面9种中,是因为存在RedisCacheManager,而是否判断使用其9种的顺序就是其缓存管理是否存在而已,所以我们手动创建这个缓存管理作为bean,并且他实现CacheManager接口,就能完成不使用自带的,而是使用我们自定义的,由于RedisCacheManager本身其父类就是CacheManager接口,所以我们单纯的生成RedisCacheManager的bean即可
注意:类似于这样的注解:@ConditionalOnMissingBean,是自定义的一种注解方式,很多框架中,为了实现能自定义,都会操作一些注解来代表条件,如这里的@ConditionalOnMissingBean,来使得实现自定义
自定义RedisCacheManager:
在项目的Redis配置类RedisConfig中,按照上一步分析的定制方法自定义名为cacheManager的Bean组件,补充:
 @Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {// 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换RedisSerializer<String> strSerializer = new StringRedisSerializer(); //前面操作过//这个与GenericJackson2JsonRedisSerializer有点类似的Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);// 解决查询缓存转换异常的问题ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jacksonSeial.setObjectMapper(om);// 定制缓存数据序列化方式及时生效/*RedisCacheConfiguration
.defaultCacheConfig.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));.entryTtl(redisProperties.getTimeToLive()); //判null.prefixCacheNameWith(redisProperties.getKeyPrefix()); //判null.disableCachingNullValues //判null
.disableKeyPrefix //判null这些了解即可,意义不大*/RedisCacheConfiguration config =RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial)).disableCachingNullValues();//return (RedisCacheManager)cacheManagerCustomizers.customize(builder.build());//上面的builder.build()RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();return cacheManager;}
这样,我们重新启动,然后我们访问http://localhost:8080/findCommentById?id=3和http://localhost:8080/api/findCommentById?id=3看看redis保存的结果吧,看看结果:
/*
127.0.0.1:6379> get "comment::3"
"[\"com.pojo.Comment\",{\"id\":3,\"content\":\"\xe5\xbe\x88\xe8\xaf\xa6\xe7\xbb\x86\",\"author\":\"eric\",\"aId\":1}]"
127.0.0.1:6379> keys *
1) "comment::3"
2) "\"comment_3\""
127.0.0.1:6379> get "\"comment_3\""
"[\"com.pojo.Comment\",{\"id\":3,\"content\":\"\xe5\xbe\x88\xe8\xaf\xa6\xe7\xbb\x86\",\"author\":\"eric\",\"aId\":1}]"
127.0.0.1:6379> 
*/
可以发现,也操作了对应的key和value的序列化了,说明我们定制成功,这个时候对应的配置应该存在两个,一个是操作序列化的,另外一个是操作缓存的,如果按照顺序的话,缓存的由于覆盖,所以缓存肯定使用我们自定义的
注意,在有些Spring Boot版本中,RedisCacheManager是单独进行构建的,因此,在有些Spring Boot版本中,对RedisTemplate进行自定义序列化机制构建后,仍然无法对 RedisCacheManager内部默认序列化机制进行覆盖(这也就解释了基于注解的Redis缓存实现仍然会使用JDK默认序列化机制的原因)
至此想要基于注解的Redis缓存实现通常使用自定义序列化机制,需要自定义RedisCacheManager
上述代码中,在RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的 cacheManager组件,在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和value 分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String格式),而 value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用 entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天,完成基于注解的Redis缓存管理器RedisCacheManager定制后,可以对该缓存管理器的效果进行测试(使用自定义序列化机制的RedisCacheManager测试时,实体类可以不用实现序列化接口,前面说了虽然大多数序列化需要,但是也只是大多数,他不需要,但是,我们建议,在操作序列化时,无论是否是本机处理还是远程处理,我们都最好实现序列化接口,以防万一)
还有mvc存在启动时加载和访问后加载,spring boot通常需要进行设置才可以,一般需要yml中设置,这里百度即可,因为:
/*
由于Servlet的生命周期,只有当请求来时,才会进行实例创建
才可进行配置文件扫描其中mvc配置文件中:<load-on-startup>2</load-on-startup>
具体在67章博客可以看到
*/
由于前面说明了序列化和编码的问题,所以以后看到不正常的数据,记得先问一问自己,他是编码造成的吗,这里了解即可
最后,我们来验证一下,对应的前面的这个代码为什么要加上:
 // 解决查询缓存转换异常的问题,创建一个 ObjectMapper 实例,用于配置 JSON 序列化和反序列化的行为ObjectMapper om = new ObjectMapper();// 设置 ObjectMapper 的可见性策略,允许所有字段和方法都被访问(即使它们是私有的)om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 启用默认类型的处理,以支持序列化和反序列化时对对象类型的自动处理// 这样可以在反序列化时正确地恢复对象的类型om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);// 将配置好的 ObjectMapper 设置到 Jackson2JsonRedisSerializer 中jacksonSeial.setObjectMapper(om);//简单来说ObjectMapper是防止json的潜在问题,建议补上
测试过程如下:
首先创面com.json包,然后创建User 类:
package com.json;public class User {private String name;private int age;public User() {}public User(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
然后创建ObjectMapperTest类:
package com.json;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import java.io.IOException;public class ObjectMapperTest {public static void main(String[] args) throws IOException {User user = new User("Alice", 30);// 不使用配置的 ObjectMapperJackson2JsonRedisSerializer<User> serializer = new Jackson2JsonRedisSerializer<>(User.class);ObjectMapper defaultMapper = new ObjectMapper();//什么都不做,自然就是不使用,因为private ObjectMapper objectMapper = new ObjectMapper();serializer.setObjectMapper(defaultMapper);//模拟//发送数据byte[] serializedDefault = serializer.serialize(user);//得到数据User deserializedDefault = serializer.deserialize(serializedDefault);System.out.println("Default ObjectMapper:");System.out.println(new String(serializedDefault));System.out.println(deserializedDefault);// 使用配置的 ObjectMapperJackson2JsonRedisSerializer<User> configuredSerializer = new Jackson2JsonRedisSerializer<>(User.class);ObjectMapper om = new ObjectMapper();// 设置 ObjectMapper 的可见性策略,允许所有字段和方法都被访问(即使它们是私有的)om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 启用默认类型的处理,以支持序列化和反序列化时对对象类型的自动处理// 这样可以在反序列化时正确地恢复对象的类型om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);configuredSerializer.setObjectMapper(om);byte[] serializedConfigured = configuredSerializer.serialize(user);User deserializedConfigured = configuredSerializer.deserialize(serializedConfigured);System.out.println("Configured ObjectMapper:");System.out.println(new String(serializedConfigured));System.out.println(deserializedConfigured);}
}
可以发现,得到的结果是这样的:
/*
Default ObjectMapper:
{"name":"Alice","age":30}
User{name='Alice', age=30}
Configured ObjectMapper:
["com.json.User",{"name":"Alice","age":30}]
User{name='Alice', age=30}
*/
说明的确有区别,我们可以看源码,可以发现Jackson2JsonRedisSerializer操作发送和接收是操作他的,因为:
public T deserialize(@Nullable byte[] bytes) throws SerializationException {if (SerializationUtils.isEmpty(bytes)) {return null;} else {try {return this.objectMapper.readValue(bytes, 0, bytes.length, this.javaType);} catch (Exception var3) {throw new SerializationException("Could not read JSON: " + var3.getMessage(), var3);}}}public byte[] serialize(@Nullable Object t) throws SerializationException {if (t == null) {return SerializationUtils.EMPTY_ARRAY;} else {try {return this.objectMapper.writeValueAsBytes(t);} catch (Exception var3) {throw new SerializationException("Could not write JSON: " + var3.getMessage(), var3);}}}
所以他的设置才会有作用,也只是因为Jackson2JsonRedisSerializer进行了操作,否则无论是否设置都没有作用的
其中主要的设置有两个:
// 设置 ObjectMapper 的可见性策略,允许所有字段和方法都被访问(即使它们是私有的)om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 启用默认类型的处理,以支持序列化和反序列化时对对象类型的自动处理// 这样可以在反序列化时正确地恢复对象的类型om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
我们去掉第一个:
结果:
/*
Default ObjectMapper:
{"name":"Alice","age":30}
User{name='Alice', age=30}
Configured ObjectMapper:
["com.json.User",{"name":"Alice","age":30}]
User{name='Alice', age=30}
*/
没有变化,去掉第二个:
/*
Default ObjectMapper:
{"name":"Alice","age":30}
User{name='Alice', age=30}
Configured ObjectMapper:
{"name":"Alice","age":30}
User{name='Alice', age=30}
*/
一样的了,所以:
 // 启用默认类型的处理,以支持序列化和反序列化时对对象类型的自动处理// 这样可以在反序列化时正确地恢复对象的类型om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);//他相当于的确操作了对对象类型的自动处理
现在我们来验证私有,给User中对应的get和set方法加上私有的:
会出现报错,恢复public,但是只给name的get和set恢复,会出现如下:
/*
Default ObjectMapper:
{"name":"Alice"}
User{name='Alice', age=0}
Configured ObjectMapper:
{"name":"Alice","age":30} 只操作第一个
User{name='Alice', age=30}*/
也就是说,不加上:
// 设置 ObjectMapper 的可见性策略,允许所有字段和方法都被访问(即使它们是私有的)om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
那么他的字段不能显示,注意:一般只是针对于方法,所以上面的注释可能有点不对,但是不同版本可能有所不同,所以就不修改了,同样的om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);也是如此,至于其他的设置,可以百度了解其作用,这里只说明主要的
至于他们为什么,这取决于其底层实现,可以百度了解,这里就不多说了,因为没有必要(难道将所有的序列化处理都说明了,这没有意义,就跟将所有市面上的别人写的类都说明一样没有意义,因为他们是无穷尽的,并且随着版本而会改变,所以只需要知道即可,以后随着越来越厉害,自然的就会知道怎么来实现了(实现方式一般有多种,除非非常困难),到那个时候如果可以,可以选择参考)
至此,Spring boot的底层原理我们说明完毕,虽然之前的博客有加上底层原理的噱头,但是也的确说明了其原理,只不过没有将他们的一些操作,如注解的原理说明而已,这些说明只是点缀,都是一些补充,并不能是底层原理,所以这里说明底层原理也并非是错误的,当然,再后续博客中,会进行一次整体说明了,将所有的操作都会说明,现在先知道原理,他们的补充,如注解操作,后面再讲

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/381864.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Doris]阿里云搭建Doris,测试环境1FE 1BE

首先&#xff1a;阿里云的国内服务器千万不要用容器搭建&#xff0c;或者自己Dockfile构建镜像。两种方式都不得行&#xff0c;压根拉不到github的镜像&#xff0c;开了镜像加速器也拉不到&#xff0c;不要折腾了&#xff0c;极其愚蠢。 背景&#xff1a;现在测试环境&#xff…

openmv学习笔记(24电赛备赛笔记)

#openmv简介 openmv一种小型&#xff0c;可编程机器视觉摄像头&#xff0c;设计应用嵌入式应用和计算边缘&#xff0c;是图传模块&#xff0c;或者认为是一种&#xff0c;具有图像处理功能的单片机&#xff0c;提供多种接口&#xff08;I2C SPI UART CAN ADC DAC &#xff0…

Linux云计算 |【第一阶段】ENGINEER-DAY4

主要内容&#xff1a; 配置Linux网络参数、配置静态主机名、查看/修改/激活/禁用网络连接、指定DNS、虚拟网络连接、虚拟机克隆、SSH客户端、SCP远程复制、SSH无密码验证&#xff08;SERVICE-DAY5&#xff09;、虚拟网络类型 一、网络参数配置 修改网卡配置文件主要是需要配置…

人工智能与社交变革:探索Facebook如何领导智能化社交平台

在过去十年中&#xff0c;人工智能&#xff08;AI&#xff09;技术迅猛发展&#xff0c;彻底改变了我们与数字世界互动的方式。Facebook作为全球最大的社交媒体平台之一&#xff0c;充分利用AI技术&#xff0c;不断推动社交平台的智能化&#xff0c;提升用户体验。本文将深入探…

资源调度的艺术:大规模爬虫管理的优化策略

摘要 本文深入探讨了在处理大规模数据抓取项目时&#xff0c;如何通过优化资源调度策略来提升爬虫管理的效率与稳定性。从技术选型到策略实施&#xff0c;揭示了优化的核心技巧&#xff0c;助力企业与开发者高效驾驭大数据采集的挑战。 正文 在互联网信息爆炸的时代&#xf…

TypeScript 开发或面试中常见问题合集

目录 typescript 与 babel 区别编译编译器 模块模块解析规则 命名空间interface 合并逻辑声明合并 普通项目怎么从 js 迁移到 ts解决冲突 第三方工具生成.d.ts文件三斜线指令模块解析逻辑types 发布书写 ts 的声明文件Property includes does not exist on type number[] globa…

RSA非对称加密

前言 RSA是一种非对称加密算法&#xff0c;也是目前最常用的加密算法之一。它由三位发明家&#xff08;Rivest、Shamir、Adleman&#xff09;于1977年提出&#xff0c;并以他们的姓氏命名。RSA算法使用了两个密钥&#xff1a;公钥和私钥。公钥可用于对数据进行加密&#xff0c…

《Exploring Aligned Complementary Image Pair for Blind Motion Deblurring》

这篇论文的标题《Exploring Aligned Complementary Image Pair for Blind Motion Deblurring》可以翻译为《探索对齐的互补图像对用于盲运动去模糊》。从标题可以推断,论文的焦点在于开发一种算法或技术,利用成对的图像来解决运动模糊问题,特别是在不知道模糊核(即造成模糊…

第一弹:基于ABAP OLE技术实现对服务器文件进行读写操作

前言 最近遇到这样一个需求&#xff0c;需要对BW服务器上的文件进行下载的同时写入每个用户相对应的数据。之前的服务器模版是一个死模版&#xff0c;对于这样的要求&#xff0c;我就想到了OLE技术&#xff0c;那么什么是OLE技术呢&#xff1f; 一、什么是OLE技术&#xff1f…

Modbus转BACnet/IP网关快速对接Modbus协议设备与BA系统

摘要 在智能建筑和工业自动化领域&#xff0c;Modbus和BACnet/IP协议的集成应用越来越普遍。BA&#xff08;Building Automation&#xff0c;楼宇自动化&#xff09;系统作为现代建筑的核心&#xff0c;需要高效地处理来自不同协议的设备数据&#xff0c;负责监控和管理建筑内…

深入浅出mediasoup—通信框架

libuv 是一个跨平台的异步事件驱动库&#xff0c;用于构建高性能和可扩展的网络应用程序。mediasoup 基于 libuv 构建了包括管道、信号和 socket 在内的一整套通信框架&#xff0c;具有单线程、事件驱动和异步的典型特征&#xff0c;是构建高性能 WebRTC 流媒体服务器的重要基础…

使用 spring MVC 简单的案例 (1)计算器

一、计算器 1.1前端代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> …

Git报错fatal: detected dubious ownership in repository

报错信息 fatal: detected dubious ownership in repository at 解决办法 一行代码解决 git config --global --add safe.directory "*";如何使用git工具初始胡项目并且和远程仓库建立联系 git init–建立一个本地仓库 git add README.md–将README.md文件加入…

MySQL添加索引时会锁表吗?

目录 简介Online DDL概念Online DDL用法总结 简介 在MySQL5.5以及之前的版本&#xff0c;通常更改数据表结构操作&#xff08;DDL&#xff09;会阻塞对表数据的增删改操作&#xff08;DML&#xff09;。 MySQL5.6提供Online DDL之后可支持DDL与DML操作同时执行&#xff0c;降低…

算法通关:005对数器

就是你有优解&#xff0c;但是不知道对不对&#xff0c;或者你遇到了题&#xff0c;但是没有在线网站能跑&#xff0c;无法检查你的思路是否正确。 写一个随机生成符合输入要求的方法。 此时用暴力解法写一个&#xff0c;因为答案肯定是对的&#xff0c;再写一个优解方法。将两…

斐波那契数列的多种解法 C++实现,绘图部分用Python实现

斐波那契数列的多种解法 C实现&#xff0c;绘图部分用Python实现 flyfish 斐波那契数列&#xff08;Fibonacci sequence&#xff09;是一个经典的数列&#xff0c;定义如下&#xff1a; { 0 if n 0 1 if n 1 F ( n − 1 ) F ( n − 2 ) if n > 1 \begin{cases} 0 &…

HackTheBox--Knife

Knife 测试过程 1 信息收集 端口扫描 80端口测试 echo "10.129.63.56 knife.htb" | sudo tee -a /etc/hosts网站是纯静态的&#xff0c;无任何交互功能&#xff0c;检查网页源代码也未发现任何可利用的文件。 检查页面请求时&#xff0c;请求与响应内容&#xff0…

高频面试题-CSS

BFC 介绍下BFC (块级格式化上下文) 1>什么是BFC BFC即块级格式化上下文&#xff0c;是CSS可视化渲染的一部分, 它是一块独立的渲染区域&#xff0c;只有属于同一个BFC的元素才会互相影响&#xff0c;且不会影响其它外部元素。 2>如何创建BFC 根元素&#xff0c;即HTM…

RabbitMQ的学习和模拟实现|sqlite轻量级数据库的介绍和简单使用

SQLite3 项目仓库&#xff1a;https://github.com/ffengc/HareMQ SQLite3 什么是SQLite为什么需要用SQLite官方文档封装Helper进行一些实验 什么是SQLite SQLite是一个进程内的轻量级数据库&#xff0c;它实现了自给自足的、无服务器的、零配置的、事务性的 SQL数据库引擎…

lua 游戏架构 之 LoaderWallet 异步加载

定义了一个名为LoaderWallet class&#xff0c;用于管理资源加载器&#xff08;Loader&#xff09;。这个类封装了资源加载的功能&#xff0c;包括异步加载&#xff0c;以及资源的释放和状态查询。下面是对代码的详细解释&#xff1a; ### 类定义和初始化 这里定义了一个名为…