这里续写上一章博客(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" >
< 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) ;
如果当前上下文中存在title变量并且程序已经启动,当前P标签中的默认文本内容将会被title变量的值所替换
从而达到模板引擎页面数据动态替换的效果
同时,Thymeleaf为变量所在域提供了一些内置对象,具体如下所示
结合上述内置对象的说明,假设要在Thymeleaf模板引擎页面中动态获取当前国家信息,可以使用#locale内置对象,示例代码如下
The locale country is: < span th: text= " ${#locale.country}" > US</ span>
至此我们可以测试一下:
Thymeleaf模板基本配置
首先 在Spring Boot项目中使用Thymeleaf模板,首先必须保证引入Thymeleaf依赖,示例代码如下:
< dependency>
< 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/> </ 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文件夹,上面都存在默认的:
在开发过程中通常会关闭缓存,保证项目调试过程中数据能够及时响应,默认为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 ) ) ; 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
</ 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; } @Override public 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中(如果操作的话)添加如下:
spring : 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. con fig; 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 { @Override public Locale resolveLocale ( HttpServletRequest httpServletRequest) { String l = httpServletRequest. getParameter ( "l" ) ; String 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; } @Override public void setLocale ( HttpServletRequest httpServletRequest, @Nullable HttpServletResponse httpServletResponse, @Nullable Locale locale) { } @Bean public 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 { @Autowired private 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,我们来说明一下操作流程(不考虑启动时的默认访问,或者启动后的多次访问)
但是发现,好像没有什么变化啊,不要着急,首先分析原因,其中:界面的显示是根据响应信息来的,但是语言的展示是根据请求信息来的(需要偏好用户),也就是说,上面只是改变响应信息,对展示没有任何功能的作用,也就是说,我们所设置的就是错的,那么如何处理,或者怎么处理请求,这里需要这样:
首先在前端操作如下:
< 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> < 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; } public 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; } @Override public 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 { @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) ) ; }
}
启动执行看看,若出现如下:
说明操作成功,但是我们来分析一下key的组成,其中前面两个:
既然上面只看参数,那么我们如下操作:
@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 ) ; System . out. println ( a. fa ( u) ) ;
发现还是操作了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;
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基础入门' , '从入门到精通讲解...' ) ;
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/> </ 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> < 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> < 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 ( name = "t_comment" )
public class Comment { @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; } @Override public 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 > { @Transactional @Modifying @Query ( "update t_comment c set c.author = ?1 where c.id=?2" ) public int updateComment ( String author, Integer id) ;
}
其中@Transactional在spring中是给一个实例的方法操作的,这里虽然是接口,但是在接口上也会,一般来说:
在类上:当在类级别上使用 @Transactional时,该类中所有的公共方法都会被事务管理
方法上:当在方法级别上使用 @Transactional时,只有该方法会被事务管理
接口上:Spring会在实现类中自动应用这些事务设置(也就是说,其实现类会判断接口中对应的方法是否存在这个注解来操作事务)
@Modifying用于标识查询方法执行的是更新、删除等修改操作,而不是查询操作,默认情况下,Spring Data JPA 中的查询方法都是只读的,这个注解告诉 Spring Data JPA 这是一个修改操作
那么@Modifying上面不加可以吗:一般不可以,由于默认是只读的,也就是默认操作select,如果不加就会出现sql的问题,会报错,如:
然后创建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 { @Autowired private CommentRepository commentRepository; public Comment findCommentById ( Integer id) { Optional < Comment > comment = commentRepository. findById ( id) ; if ( comment. isPresent ( ) ) { 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 { @Autowired private CommentService commentService; @RequestMapping ( value = "/findCommentById" ) public Comment findCommentById ( Integer id) { Comment comment = commentService. findCommentById ( id) ; return comment; }
}
配置文件:
application.yml:
spring : datasource : url : jdbc: mysql: //localhost: 3306/springbootdata? serverTimezone=UTCusername : rootpassword : 123456 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) { 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注解,看看是否会报错:
然后我们可以发现的确报错了,并且也的确是:
当然,不同版本之间可能错误是不同的,但是基本需要设置对应的注解
我们继续访问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" ) 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),该映射存储了缓存的键值对,我们直接看代码来认识吧(代码是最直观的说明):
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 ( ) { } @Bean ConcurrentMapCacheManager 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 ) ; @Nullable public 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 {
}
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 ;
}
缓存注解介绍:
刚刚通过使用@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将尝试选择并启用以下缓存组件(按照指定的顺序):
当然,他们要操作成功,首先是存在对应的路径,一般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/> </ 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> < groupId> junit</ groupId> < artifactId> junit</ artifactId> < scope> test</ scope> </ dependency> </ dependencies>
</ project>
创建com.pojo包,然后创建两个类(这里我们引入前面操作redis的过程):
package com. pojo ; import org. springframework. data. redis. core. index. Indexed ; public class Address { @Indexed private String city; @Indexed private 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; } @Override public 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" )
public class Person { @Id private String id; @Indexed private String firstname; @Indexed private 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; } @Override public 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 : host : 192.168.136.128 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 { @Autowired private PersonRepository repository; @Test public void savePerson ( ) { Person person = new Person ( ) ; person. setFirstname ( "张" ) ; person. setLastname ( "三" ) ; Address address = new Address ( ) ; address. setCity ( "北京" ) ; address. setCountry ( "中国" ) ; person. setAddress ( address) ; 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> < 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" )
public class Comment { @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; } @Override public 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 > { @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 { @Autowired private 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 { @Autowired private 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:
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 { @Autowired private CommentRepository commentRepository; @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:
@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中补充:
@CacheEvict ( cacheNames = "comment" ) public void deleteComment ( int comment_id) { commentRepository. deleteById ( comment_id) ; }
@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的数据,展示基本不好看,这个时候我们自然需要考虑序列化处理,一般正确存储的是这样的数据
具体显示是如此:
当然,上面序列化的处理前面已经说明了,这里了解即可
执行上面方法查询出的用户评论信息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 { @Autowired private CommentRepository commentRepository; @Autowired private RedisTemplate redisTemplate; public Comment findCommentById ( Integer id) { Object o = redisTemplate. opsForValue ( ) . get ( "comment_" + id) ; if ( o != null ) { return ( Comment ) o; } else { Optional < Comment > byId = commentRepository. findById ( id) ; if ( byId. isPresent ( ) ) { Comment comment = byId. get ( ) ; redisTemplate. opsForValue ( ) . set ( "comment_" + id, comment, 1 , TimeUnit . DAYS ) ; return comment; } else { return null ; } } } 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) ; 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 { @Autowired private 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 { @Nullable private RedisSerializer keySerializer = null ; @Nullable private RedisSerializer valueSerializer = null ; public void afterPropertiesSet ( ) { super . afterPropertiesSet ( ) ; boolean defaultUsed = false ; if ( this . defaultSerializer == null ) { this . defaultSerializer = new JdkSerializationRedisSerializer ( this . classLoader != null ? this . classLoader : this . getClass ( ) . getClassLoader ( ) ) ; } if ( this . enableDefaultSerializer) { if ( this . keySerializer == null ) { this . keySerializer = this . defaultSerializer; defaultUsed = true ; } 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; }
}
很明显根据前面的代码:
if ( this . keySerializer == null ) { this . keySerializer = this . defaultSerializer; defaultUsed = true ; } 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 ) 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. con fig; 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 { @Bean public RedisTemplate < Object , Object > redisTemplate ( RedisConnectionFactory redisConnectionFactory) { RedisTemplate < Object , Object > template = new RedisTemplate ( ) ; template. setConnectionFactory ( redisConnectionFactory) ; 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) ; template. setDefaultSerializer ( jacksonSeial) ; return template; }
}
这样,对应的序列化就不是默认的了,而是我们设置的,我们访问http://localhost:8080/findCommentById?id=3和http://localhost:8080/api/findCommentById?id=3看看redis保存的结果吧(在一些情况下,如果编码一致还出现乱码,一般是给出的数据不能识别造成的),一般第二个方法比较好看了,与前面的GenericJackson2JsonRedisSerializer有点类似,也可以看看其value的值了
为什么操作缓存的使用自定义的序列化是不行的,这就需要前面我们说过的处理了,前面我们说过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 ( ) { } @Bean RedisCacheManager cacheManager ( CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers, ObjectProvider < org. springframework. data. redis. cache. RedisCacheConfiguration> redisCacheConfiguration, ObjectProvider < RedisCacheManagerBuilderCustomizer > redisCacheManagerBuilderCustomizers, RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) { RedisCacheManagerBuilder builder = RedisCacheManager . builder ( redisConnectionFactory) . cacheDefaults ( this . determineConfiguration ( cacheProperties, redisCacheConfiguration, resourceLoader. getClassLoader ( ) ) ) ; List < String > cacheNames = cacheProperties. getCacheNames ( ) ; if ( ! cacheNames. isEmpty ( ) ) { builder. initialCacheNames ( new LinkedHashSet ( cacheNames) ) ; } if ( cacheProperties. getRedis ( ) . isEnableStatistics ( ) ) { builder. enableStatistics ( ) ; } redisCacheManagerBuilderCustomizers. orderedStream ( ) . forEach ( ( customizer) -> { customizer. customize ( builder) ; } ) ; return ( RedisCacheManager ) cacheManagerCustomizers. customize ( builder. build ( ) ) ; }
}
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) ; } ) ; } 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; }
从上述核心源码中可以看出,同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; }
一般对应的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组件,补充:
@Bean public RedisCacheManager cacheManager ( RedisConnectionFactory redisConnectionFactory) { RedisSerializer < String > strSerializer = new StringRedisSerializer ( ) ; 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 config = RedisCacheConfiguration . defaultCacheConfig ( ) . entryTtl ( Duration . ofDays ( 1 ) ) . serializeKeysWith ( RedisSerializationContext. SerializationPair . fromSerializer ( strSerializer) ) . serializeValuesWith ( RedisSerializationContext. SerializationPair . fromSerializer ( jacksonSeial) ) . disableCachingNullValues ( ) ; RedisCacheManager cacheManager = RedisCacheManager . builder ( redisConnectionFactory) . cacheDefaults ( config) . build ( ) ; return cacheManager; }
这样,我们重新启动,然后我们访问http://localhost:8080/findCommentById?id=3和http://localhost:8080/api/findCommentById?id=3看看redis保存的结果吧,看看结果:
可以发现,也操作了对应的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中设置,这里百度即可,因为:
由于前面说明了序列化和编码的问题,所以以后看到不正常的数据,记得先问一问自己,他是编码造成的吗,这里了解即可
最后,我们来验证一下,对应的前面的这个代码为什么要加上:
ObjectMapper om = new ObjectMapper ( ) ; om. setVisibility ( PropertyAccessor . ALL , JsonAutoDetect. Visibility . ANY ) ; om. enableDefaultTyping ( ObjectMapper. DefaultTyping . NON_FINAL ) ; jacksonSeial. setObjectMapper ( om) ;
测试过程如下:
首先创面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; } @Override public 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 ) ; Jackson2JsonRedisSerializer < User > serializer = new Jackson2JsonRedisSerializer < > ( User . class ) ; ObjectMapper defaultMapper = 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) ; Jackson2JsonRedisSerializer < User > configuredSerializer = new Jackson2JsonRedisSerializer < > ( User . class ) ; ObjectMapper om = new 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) ; }
}
可以发现,得到的结果是这样的:
说明的确有区别,我们可以看源码,可以发现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进行了操作,否则无论是否设置都没有作用的
其中主要的设置有两个:
om. setVisibility ( PropertyAccessor . ALL , JsonAutoDetect. Visibility . ANY ) ; om. activateDefaultTyping ( LaissezFaireSubTypeValidator . instance, ObjectMapper. DefaultTyping . NON_FINAL ) ;
我们去掉第一个:
结果:
没有变化,去掉第二个:
一样的了,所以:
om. activateDefaultTyping ( LaissezFaireSubTypeValidator . instance, ObjectMapper. DefaultTyping . NON_FINAL ) ;
现在我们来验证私有,给User中对应的get和set方法加上私有的:
会出现报错,恢复public,但是只给name的get和set恢复,会出现如下:
也就是说,不加上:
om. setVisibility ( PropertyAccessor . ALL , JsonAutoDetect. Visibility . ANY ) ;
那么他的字段不能显示,注意:一般只是针对于方法,所以上面的注释可能有点不对,但是不同版本可能有所不同,所以就不修改了,同样的om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);也是如此,至于其他的设置,可以百度了解其作用,这里只说明主要的
至于他们为什么,这取决于其底层实现,可以百度了解,这里就不多说了,因为没有必要(难道将所有的序列化处理都说明了,这没有意义,就跟将所有市面上的别人写的类都说明一样没有意义,因为他们是无穷尽的,并且随着版本而会改变,所以只需要知道即可,以后随着越来越厉害,自然的就会知道怎么来实现了(实现方式一般有多种,除非非常困难),到那个时候如果可以,可以选择参考)
至此,Spring boot的底层原理我们说明完毕,虽然之前的博客有加上底层原理的噱头,但是也的确说明了其原理,只不过没有将他们的一些操作,如注解的原理说明而已,这些说明只是点缀,都是一些补充,并不能是底层原理,所以这里说明底层原理也并非是错误的,当然,再后续博客中,会进行一次整体说明了,将所有的操作都会说明,现在先知道原理,他们的补充,如注解操作,后面再讲