Bootstrap

Spring Boot 高级应用

一、Spring Boot数据访问

  SpringData是Spring提供的一个用于简化数据库访问、支持云服务的开源框架。它是一个伞形项目,包含了大量关系型数据库及非关系型数据库的数据访问解决方案,其设计目的是使我们可以快速且简单地使用各种数据访问技术。Spring Boot默认采用整合SpringData的方式统一处理数据访问层,通过添加大量自动配置,引入各种数据访问模板xxxTemplate以及统一的Repository接口,从而达到简化数据访问层的操作。

  Spring Data提供了多种类型数据库支持,对支持的的数据库进行了整合管理,提供了各种依赖启动器,接下来,通过一张表罗列提供的常见数据库依赖启动器,如表所示。
在这里插入图片描述
  除此之外,还有一些框架技术,Spring Data项目并没有进行统一管理, Spring Boot官方也没有提供对应的依赖启动器,但是为了迎合市场开发需求、这些框架技术开发团队自己适配了对应的依赖启动器,例如,mybatis-spring-boot-starter支持MyBatis的使用。

1.1 Spring Boot整合MyBatis

  • MyBatis 是一款优秀的持久层框架,Spring Boot官方虽然没有对MyBatis进行整合,但是MyBatis团队自行适配了对应的启动器,进一步简化了使用MyBatis进行数据的操作。
  • 因为Spring Boot框架开发的便利性,所以实现Spring Boot与数据访问层框架(例如MyBatis)的整合非常简单,主要是引入对应的依赖启动器,并进行数据库相关参数设置即可。

1.1.1 基础环境搭建:

1、数据准备

  在MySQL中,先创建了一个数据库springbootdata,然后创建了两个表t_article和t_comment并向表中插入数据。其中评论表t_comment的a_id与文章表t_article的主键id相关联。

#创建数据库
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', '很全、很详细', 'luccy', '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');

2、创建项目,引入相应的启动器
在这里插入图片描述
3. 编写与数据库表t_comment和t_article对应的实体类Comment和Article

public class Comment {

	private Integer id;
	private String content;
	private String author;
	private Integer aId;
	
	// 省略属性getXX()和setXX()方法
	// 省略toString()方法
}
public class Article {

	private Integer id;
	private String title;
	private String content;
	
	// 省略属性getXX()和setXX()方法
	// 省略toString()方法
}

4、编写配置文件

(1)在application.properties配置文件中进行数据库连接配置

# MySQL数据库连接配置,数据库连接驱动是com.mysql.cj.jdbc.Driver,已经由框架默认集成,不需要手动编写
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

1.1.2 注解方式整合Mybatis

(1)创建一个用于对数据库表t_comment数据操作的接口CommentMapper

@Mapper
public interface CommentMapper {

    @Select("SELECT * FROM t_comment WHERE id =#{id}")
    Comment findById(Integer id);
}
  • @Mapper注解表示该类是一个MyBatis接口文件,并保证能够被Spring Boot自动扫描到Spring容器中
  • 如果编写的Mapper接口过多时,需要重复为每一个接口文件添加@Mapper注解,为了解决这种麻烦,可以直接在Spring Boot项目启动类上添加@MapperScan(“xxx”)注解,不需要再逐个添加@Mapper注解。
  • @MapperScan(“xxx”)注解的作用和@Mapper注解类似,但是它必须指定需要扫描的具体包名。

(2)编写测试方法

@RunWith(SpringRunner.class)
@SpringBootTest
class SpringbootDataApplicationTests {

    @Autowired
    private CommentMapper commentMapper;

    @Test
    void contextLoads() {
        Comment comment = commentMapper.findById(1);
        System.out.println(comment);
    }

}

打印结果:
在这里插入图片描述
  控制台中查询的Comment的aId属性值为null,没有映射成功。这是因为编写的实体类 Comment 中使用了驼峰命名方式将t_comment表中的a_id字段设计成了aId属性,所以无法正确映射查询结果。

  为了解决上述由于驼峰命名方式造成的表字段值无法正确映射到类属性的情况,可以在Spring Boot全局配置文件application.properties中添加开启驼峰命名匹配映射配置,示例代码如下:

#开启驼峰命名匹配映射
mybatis.configuration.map-underscore-to-camel-case=true

打印结果:
在这里插入图片描述

1.1.3 使用配置文件的方式整合MyBatis

(1)创建一个用于对数据库表t_article数据操作的接口ArticleMapper

@Mapper
public interface ArticleMapper {
    Article selectArticle(Integer id);
}

(2)创建XML映射文件

  resources目录下创建一个统一管理映射文件的包mapper,并在该包下编写与ArticleMapper接口方应的映射文件ArticleMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cyd.mapper.ArticleMapper">

    <select id="selectArticle" resultType="com.cyd.pojo.Article">
        SELECT * FROM t_article WHERE id = #{id}
    </select>

</mapper>

(3)配置XML映射文件路径。在项目中编写的XML映射文件,Spring Boot并无从知晓,所以无法扫描到该自定义编写的XML配置文件,还必须在全局配置文件application.properties中添加MyBatis映射文件路径的配置,同时需要添加实体类别名映射路径,示例代码如下

#配置MyBatis的xml配置文件路径
mybatis.mapper-locations=classpath:mapper/*.xml
#配置XML映射文件中指定的实体类别名路径
mybatis.type-aliases-package=com.lagou.pojo

(4)编写单元测试进行接口方法测试

@RunWith(SpringRunner.class)
@SpringBootTest
class SpringbootDataApplicationTests {

    @Autowired
    private ArticleMapper articleMapper;

    @Test
    public void selectArticle() {
        Article article = articleMapper.selectArticle(1);
        System.out.println(article);
    }

}

打印结果:
在这里插入图片描述

1.2 Spring Boot整合JPA

(1)添加Spring Data JPA依赖启动器。在项目的pom.xml文件中添加Spring Data JPA依赖启动器,示例代码如下

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

(2)编写ORM实体类。

@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;
	
	// 省略属性getXX()和setXX()方法
	// 省略toString()方法
}

(3)编写Repository接口 :CommentRepository

public interface CommentRepository extends JpaRepository<Comment,Integer> {
}

(4)测试

@RunWith(SpringRunner.class)
@SpringBootTest
class SpringbootDataApplicationTests {

    @Autowired
    private CommentRepository repository;
    
    @Test
    public void selectComment() {
        Optional<Comment> optional = repository.findById(1);
        if(optional.isPresent()){
            System.out.println(optional.get());
        }
        System.out.println();
    }

}

在这里插入图片描述

1.3 Spring Boot整合Redis

  除了对关系型数据库的整合支持外,Spring Boot对非关系型数据库也提供了非常好的支持。Spring Boot与非关系型数据库Redis的整合使用。

(1)添加Spring Data Redis依赖启动器。先在项目的pom.xml文件中添加Spring Data Redis依赖启动器,示例代码如下

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

(2)编写实体类。此处为了演示Spring Boot与Redis数据库的整合使用,在项目的com.cyd.domain 包下编写几个对应的实体类

@RedisHash("persons") // 指定操作实体类对象在Redis数据库中的存储空间
public class Person {

	@Id // 标识实体类主键
	private String id;
	
	@Indexed // 标识对应属性在Redis数据库中生成二级索引
	private String firstname;
	
	@Indexed
	private String lastname;
	
	private Address address;
	
	// 省略属性getXX()和setXX()方法
	// 省略有参和无参构造方法
	// 省略toString()方法
}
public class Address {

	@Indexed
	private String city;
	
	@Indexed
	private String country;
	
	// 省略属性getXX()和setXX()方法
	// 省略有参和无参构造方法
	// 省略toString()方法
}

实体类示例中,针对面向Redis数据库的数据操作设置了几个主要注解,这几个注解的说明如下 :

  • @RedisHash(“persons”):用于指定操作实体类对象在Redis数据库中的存储空间,此处表示针对Person实体类的数据操作都存储在Redis数据库中名为persons的存储空间下。
  • @Id:用于标识实体类主键。在Redis数据库中会默认生成字符串形式的HashKey表示唯一的实体对象id,当然也可以在数据存储时手动指定id。
  • @Indexed:用于标识对应属性在Redis数据库中生成二级索引。使用该注解后会在Redis数据库中生成属性对应的二级索引,索引名称就是属性名,可以方便的进行数据条件查询。

(3)编写Repository接口。Spring Boot针对包括Redis在内的一些常用数据库提供了自动化配置,可以通过实现Repository接口简化对数据库中的数据进行增删改查操作。

public interface PersonRepository extends CrudRepository<Person,String> {
	List<Person> findByAddress_City(String name);
}

  需要说明的是,在操作Redis数据库时编写的Repository接口文件需要继承最底层的CrudRepository接口,而不是继承JpaRepository,这是因为JpaRepository是Spring Boot整合JPA特有的。当然,也可以在项目pom.xml文件中同时导入Spring Boot整合的JPA依赖和Redis依赖,这样就可以编写一个继承JpaRepository的接口操作Redis数据库。

(4)Redis数据库连接配置。在项目的全局配置文件application.properties中添加Redis数据库的连接配置,示例代码如下:

# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=

(5)编写单元测试进行接口方法测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTest {

    @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);
        // 向Redis数据库添加数据
        Person save = repository.save(person);
    }

    @Test
    public void selectPerson() {
        List<Person> list = repository.findByAddress_City("北京");
        for (Person person : list) {
            System.out.println(person);
        }
    }
}

添加数据结果:
在这里插入图片描述
查询结果:在这里插入图片描述
  执行savePerson()方法添加的数据在Redis数据库中存储成功。另外,在数据库列表左侧还生成了一些类似address.city、firstname、lastname等二级索引,这些二级索引是前面创建Person类时在对应属性上添加@Indexed注解而生成的。同时,由于在Redis数据库中生成了对应属性的二级索引,所以可以通过二级索引来查询具体的数据信息,例如repository.findByAddress_City(“北京”)通过address.city索引查询索引值为“北京”的数据信息。如果没有设置对应属性的二级索引,那么通过属性索引查询数据结果将会为空。

二、Spring Boot视图技术

2.1 支持的视图技术

  前端模板引擎技术的出现,使前端开发人员无需关注后端业务的具体实现,只关注自己页面的呈现效果即可,并且解决了前端代码错综复杂的问题、实现了前后端分离开发。Spring Boot框架对很多常用的模板引擎技术(如:FreeMarker、Thymeleaf、Mustache等)提供了整合支持。

Spring Boot不太支持常用的JSP模板,并且没有提供对应的整合配置,这是因为使用嵌入式Servlet容器的Spring Boot应用程序对于JSP模板存在一些限制 :

  • Spring Boot默认使用嵌入式Servlet容器以JAR包方式进行项目打包部署,这种JAR包方式不支持
    JSP模板。
  • 如果使用Undertow嵌入式容器部署Spring Boot项目,也不支持JSP模板。
  • Spring Boot默认提供了一个处理请求路径“/error”的统一错误处理器,返回具体的异常信息。使用JSP模板时,无法对默认的错误处理器进行覆盖,只能根据Spring Boot要求在指定位置定制错误页面。

  上面对Spring Boot支持的模板引擎进行了介绍,并指出了整合JSP模板的一些限制。接下来,对其中常用的Thymeleaf模板引擎进行介绍,并完成与Spring Boot框架的整合实现。

2.2 Thymeleaf

  Thymeleaf是一种现代的基于服务器端的Java模板引擎技术,也是一个优秀的面向Java的XML、XHTML、HTML5页面模板,它具有丰富的标签语言、函数和表达式,在使用Spring Boot框架进行页面设计时,一般会选择Thymeleaf模板。

2.2.1 Thymeleaf语法

2.2.1.1 常用标签

  在HTML页面上使用Thymeleaf标签,Thymeleaf 标签能够动态地替换掉静态内容,使页面动态展示。为了大家更直观的认识Thymeleaf,下面展示一个在HTML文件中嵌入了Thymeleaf的页面文件,示例代码如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <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的常用标签
在这里插入图片描述

2.2.1.2 标准表达式

  Thymeleaf模板引擎提供了多种标准表达式语法,在正式学习之前,先通过一张表来展示其主要语法及说明
在这里插入图片描述
1、变量表达式 ${…}

变量表达式${…}主要用于获取上下文中的变量值,示例代码如下:

<p th:text="${title}">这是标题</p>

  示例使用了Thymeleaf模板的变量表达式${…}用来动态获取P标签中的内容,如果当前程序没有启动或者当前上下文中不存在title变量,该片段会显示标签默认值“这是标题”;如果当前上下文中存在title变量并且程序已经启动,当前P标签中的默认文本内容将会被title变量的值所替换,从而达到模板引擎页面数据动态替换的效果。

同时,Thymeleaf为变量所在域提供了一些内置对象,具体如下所示

# ctx:上下文对象
# vars:上下文变量
# locale:上下文区域设置
# request:(仅限Web Context)HttpServletRequest对象
# response:(仅限Web Context)HttpServletResponse对象
# session:(仅限Web Context)HttpSession对象
# servletContext:(仅限Web Context)ServletContext对象

  结合上述内置对象的说明,假设要在Thymeleaf模板引擎页面中动态获取当前国家信息,可以使用 #locale内置对象,示例代码如下。

The locale country is: <span th:text="${#locale.country}">US</span>.

  上述代码中,使用th:text="${#locale.country}"动态获取当前用户所在国家信息,其中标签内默认内容为US(美国),程序启动后通过浏览器查看当前页面时,Thymeleaf会通过浏览器语言设置来识别当前用户所在国家信息,从而实现动态替换。

2、选择变量表达式 *{…}

   选择变量表达式和变量表达式用法类似,一般用于从被选定对象而不是上下文中获取属性值,如果没有选定对象,则和变量表达式一样,示例代码如下

<div th:object="${book}">
	<p>titile: <span th:text="*{title}">标题</span>.</p>
</div>

   *{title} 选择变量表达式获取当前指定对象book的title属性值。

3、消息表达式 #{…}

   消息表达式#{…}主要用于Thymeleaf模板页面国际化内容的动态替换和展示,使用消息表达式#{…}进行国际化设置时,还需要提供一些国际化配置文件。关于消息表达式的使用,后续会详细说明。

4、链接表达式 @{…}

   链接表达式@{…}一般用于页面跳转或者资源的引入,在Web开发中占据着非常重要的地位,并且使用也非常频繁,示例代码如下:

<a th:href="@{http://localhost:8080/order/details(orderId=${o.id})}">view</a>
<a th:href="@{/order/details(orderId=${o.id})}">view</a>

  上述代码中,链接表达式@{…}分别编写了绝对链接地址和相对链接地址。在有参表达式中,需要按照@{路径(参数名称=参数值,参数名称=参数值…)}的形式编写,同时该参数的值可以使用变量表达式来传递动态参数值

5、片段表达式 ~{…}

  片段表达式~{…}用来标记一个片段模板,并根据需要移动或传递给其他模板。其中,最常见的用法是使用th:insert或th:replace属性插入片段,示例代码如下:

<div th:insert="~{thymeleafDemo::title}"></div>

  上述代码中,使用th:insert属性将title片段模板引用到该标签中。thymeleafDemo为模板名称,Thymeleaf会自动查找“/resources/templates/”目录下的thymeleafDemo模板,title为片段名称。

2.2.2 基本使用

2.2.2.1 Thymeleaf模板基本配置

  首先在Spring Boot项目中使用Thymeleaf模板,首先必须保证引Thymeleaf依赖,示例代码如下:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

  其次,在全局配置文件中配置Thymeleaf模板的一些参数。一般Web项目都会使用下列配置,示例代码如:

#启用模板缓存
spring.thymeleaf.cache = true 
#模板编码
spring.thymeleaf.encoding = UTF-8 
#应用于模板的模板模式
spring.thymeleaf.mode = HTML5 
#指定模板页面存放路径
spring.thymeleaf.prefix = classpath:/templates/ 
#指定模板页面名称的后缀
spring.thymeleaf.suffix = .html 

  上述配置中,spring.thymeleaf.cache表示是否开启Thymeleaf模板缓存,默认为true,在开发过程中通常会关闭缓存,保证项目调试过程中数据能够及时响应;spring.thymeleaf.prefix指定了Thymeleaf模板页面的存放路径,默认为classpath:/templates/;spring.thymeleaf.suffix指定了Thymeleaf模板页面的名称后缀,默认为.html。

2.2.2.2 静态资源的访问

  开发Web应用时,难免需要使用静态资源。Spring boot默认设置了静态资源的访问路径。
  使用Spring Initializr方式创建的Spring Boot项目,默认生成了一个resources目录,在resources目录中新建public、resources、static三个子目录下,Spring boot默认会挨个从public、resources、static里面查找静态资源

2.2.3 完成数据的页面展示

2.2.3.1 创建Spring Boot项目,引入Thymeleaf依赖

在这里插入图片描述

2.2.3.2 编写配置文件

  打开application.properties全局配置文件,在该文件中对Thymeleaf模板页面的数据缓存进行设置

# thymeleaf页面缓存设置(默认为true),开发中方便调试应设置为false,上线稳定后应保持默认true
spring.thymeleaf.cache=false

  使用“spring.thymeleaf.cache=false”将Thymeleaf默认开启的缓存设置为了false,用来关闭模板页面缓存

2.2.3.3 创建web控制类

  在项目中创建名为com.lagou.controller的包,并在该包下创建一个用于前端模板页面动态数据替换效果测试的访问实体类LoginController

@Controller
public class LoginController {
	/**
	* 获取并封装当前年份跳转到登录页login.html
	*/
	@RequestMapping("/toLoginPage")
	public String toLoginPage(Model model){
		model.addAttribute("currentYear", Calendar.getInstance().get(Calendar.YEAR));
		return "login";
	}
}

  toLoginPage()方法用于向登录页面login.html跳转,同时携带了当前年份信息currentYear。

2.2.3.4 创建模板页面并引入静态资源文件

  在“classpath:/templates/”目录下引入一个用户登录的模板页面login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1,shrinkto-fit=no">
   <title>用户登录界面</title>
   <link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet">
   <link th:href="@{/login/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
   <!-- 用户登录form表单 -->
   <form class="form-signin">
       <img class="mb-4" th:src="@{/login/img/login.jpg}" width="72" height="72">
       <h1 class="h3 mb-3 font-weight-normal">请登录</h1>
       <input type="text" class="form-control"
              th:placeholder="用户名" required="" autofocus="">
       <input type="password" class="form-control"
              th:placeholder="密码" required="">
       <div class="checkbox mb-3">
           <label>
               <input type="checkbox" value="remember-me"> 记住我
           </label>
       </div>
       <button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button>
       <p class="mt-5 mb-3 text-muted">©
           <span th:text="${currentYear}">2019</span>-
           <span th:text="${currentYear}+1">2020</span>
       </p>
   </form>
</body>
</html>
  • 通过“xmlns:th=“http://www.thymeleaf.org””引入了Thymeleaf模板标签;
  • 使用“th:href”和“th:src”分别引入了两个外联的样式文件和一个图片;
  • 使用“th:text”引入了后台动态传递过来的当前年份currentYear
2.2.3.5 效果测试

在这里插入图片描述

  可以看出,登录页面login.html显示正常,在文件4-3中使用“th:*”相关属性引入的静态文件生效,并且在页面底部动态显示了当前日期2019-2020,而不是文件中的静态数字2019-2020。这进一步说明了Spring Boot与Thymeleaf整合成功,完成了静态资源的引入和动态数据的显示。

2.2.4 配置国际化页面

2.2.4.1 编写多语言国际化配置文件

  在项目的类路径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”的形式命名
  • 本示例中,在项目类路径resources下自定义了一个i18n包用于统一配置管理多语言配置文件,并将项目默认语言配置文件名自定义为login.properties,因此,后续还必须在项目全局配置文件中进行国际化文件基础名配置,才能引用自定义国际化文件。
2.2.4.2 编写配置文件

  打开项目的application.properties全局配置文件,在该文件中添加国际化文件基础名设置,内容如文件。

# 配置国际化文件基础名
spring.messages.basename=i18n.login

  spring.messages.basename=i18n.login”设置了自定义国际化文件的基础名。其中,i18n表示国际化文件相对项目类路径resources的位置,login表示多语言文件的前缀名。如果开发者完全按照Spring Boot默认识别机制,在项目类路径resources下编写messages.properties等国际化文件,可以省略国际化文件基础名的配置。

2.2.4.3 定制区域信息解析器

  在完成上一步中多语言国际化文件的编写和配置后,就可以正式在前端页面中结合Thymeleaf模板相关属性进行国际化语言设置和展示了,不过这种实现方式默认是使用请求头中的语言信息(浏览器语言信息)自动进行语言切换的,有些项目还会提供手动语言切换的功能,这就需要定制区域解析器了。

  在项目中创建名为com.cyd.config的包,并在该包下创建一个用于定制国际化功能区域信息解析器的自定义配置类MyLocalResolver。

@Configuration
public class MyLocaleResolver implements LocaleResolver {

    // 自定义区域解析方式
    @Override
    public Locale resolveLocale(HttpServletRequest httpServletRequest) {
        // 获取页面手动切换传递的语言参数l
        String l = httpServletRequest.getParameter("locale");
        // 获取请求头自动传递的语言参数Accept-Language
        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 {
            // Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
            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) {
    }

    // 将自定义的MyLocaleResolver类重新注册为一个类型LocaleResolver的Bean组件
    @Bean
    public LocaleResolver localeResolver() {
        return new MyLocaleResolver();
    }
}

  MyLocalResolver自定义区域解析器配置类实现了LocaleResolver接口,并重写了其中的resolveLocale()方法进行自定义语言解析,最后使用@Bean注解将当前配置类注册成Spring容器中的一个类型为LocaleResolver的Bean组件,这样就可以覆盖默认的LocaleResolver组件。其中,在resolveLocale()方法中,根据不同需求(手动切换语言信息、浏览器请求头自动切换语言信息)分别获取了请求参数 locale 和请求头参数Accept-Language,然后在请求参数 locale 不为空的情况下就以 locale 参数携带的语言为标准进行语言切换,否则就定制通过请求头信息进行自动切换。

  需要注意的是,在请求参数l的语言手动切换组装时,使用的是下划线“_”进行的切割,这是由多语言配置文件的格式决定的(例如login_zh_CN.properties);而在请求头参数Accept-Language的语言自动切换组装时,使用的是短横线“-”进行的切割,这是由浏览器发送的请求头信息样式决定的(例如Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7)

2.2.4.4 页面国际化使用

  打开项目templates模板文件夹中的用户登录页面login.html,结合Thymeleaf模板引擎实现国际化功能。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1,shrinkto-fit=no">
    <title>用户登录界面</title>
    <link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet">
    <link th:href="@{/login/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
    <!-- 用户登录form表单 -->
    <form class="form-signin">
        <img class="mb-4" th:src="@{/login/img/login.jpg}" width="72" height="72">
        <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">请登录</h1>
        <input type="text" class="form-control"
               th:placeholder="#{login.username}" required="" autofocus="">
        <input type="password" class="form-control"
               th:placeholder="#{login.password}" required="">
        <div class="checkbox mb-3">
            <label>
                <input type="checkbox" value="remember-me"> [[#{login.rememberme}]]
            </label>
        </div>
        <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.button}">登录</button>
        <p class="mt-5 mb-3 text-muted">©
            <span th:text="${currentYear}">2019</span>-
            <span th:text="${currentYear}+1">2020</span>
        </p>
        <a class="btn btn-sm" th:href="@{/toLoginPage(locale='zh_CN')}">中文</a>
        <a class="btn btn-sm" th:href="@{/toLoginPage(locale='en_US')}">English</a>
    </form>
</body>
</html>

  使用Thymeleaf模板的#{}消息表达式设置了国际化展示的部分信息。在对记住我rememberme国际化设置时,需要国际化设置的rememberme在标签外部,所以这里使用了行内表达式[[#{login.rememberme}]]动态获取国际化文件中的login.rememberme信息。另外,在表单尾部还提供了中文、English手动切换语言的功能链接,在单击链接时会分别携带国家语言参数向“/”路径请求跳转,通过后台定制的区域解析器进行手动语言切换。

2.2.4.5 整合效果测试

在这里插入图片描述
在这里插入图片描述
  单击“English”链接进行语言国际化切换时携带了指定的“locale=zh_CN”参数,后台定制的区域解析器配置类MyLocalResovel中的解析方法会根据定制规则进行语言切换,从而达到了手动切换国际化语言的效果。

三、Spring Boot 缓存管理

3.1 默认缓存管理

  Spring框架支持透明地向应用程序添加缓存对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法,从而减少操作数据的执行次数,同时不会对程序本身造成任何干扰。

  Spring Boot继承了Spring框架的缓存管理功能,通过使用@EnableCaching注解开启基于注解的缓存支持,Spring Boot就可以启动缓存管理的自动化配置。

  接下来针对Spring Boot支持的默认缓存管理进行讲解

3.1.1 基础环境搭建

1、准备数据

  使用创建的springbootdata的数据库,该数据库有两个表t_article和t_comment

2、创建项目,功能编写

(1)在Dependencies依赖选择项中添加SQL模块中的JPA依赖、MySQL依赖和Web模块中的Web依赖
(2)编写数据库表对应的实体类,并使用JPA相关注解配置映射关系

@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;
	
	// 省略属性getXX()和setXX()方法
	// 省略toString()方法
}

(3)编写数据库操作的Repository接口文件

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);
}

(4)编写service层

@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;
	}
}

(5)编写Controller层

@RestController
public class CommentController {

	@Autowired
	private CommentService commentService;
	
	@RequestMapping(value = "/findCommentById")
	public Comment findCommentById(Integer id){
		Comment comment = commentService.findCommentById(id);
		return comment;
	}
}

(6)编写配置文件

  在项目全局配置文件application.properties中编写对应的数据库连接配置

# MySQL数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?
serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
# 显示使用JPA进行数据库查询的SQL语句
spring.jpa.show-sql=true
# 开启驼峰命名匹配映射
mybatis.configuration.map-underscore-to-camel-case=true
# 解决乱码
spring.http.encoding.force-response=true

(7)测试
在这里插入图片描述
在这里插入图片描述
  上图情况,是因为没有在Spring Boot项目中开启缓存管理。在没有缓存管理的情况下,虽然数据表中的数据没有发生变化,但是每执行一次查询操作(本质是执行同样的SQL语句),都会访问一次数据库并执行一次SQL语句。

3.1.2 默认缓存体验

  在前面搭建的Web应用基础上,开启Spring Boot默认支持的缓存,体验Spring Boot默认缓存的使用效果

(1)使用@EnableCaching注解开启基于注解的缓存支持

@EnableCaching // 开启Spring Boot基于注解的缓存管理支持
@SpringBootApplication
public class SpringbootCacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootCacheApplication.class, args);
    }

}

(2)使用@Cacheable注解对数据操作方法进行缓存管理。

将@Cacheable注解标注在Service类的查询方法上,对查询结果进行缓存

	// 根据评论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(int comment_id)方法上添加了查询缓存注解@Cacheable,该注解的作用是将查询结果Comment存放在Spring Boot默认缓存中名称comment的名称空间(namespace)中,对应缓存唯一标识(即缓存数据对应的主键k)默认为方法参数comment_id的值。

(3)测试访问
在这里插入图片描述
在这里插入图片描述

  可以看出,再次 执行findCommentById()方法正确查询出用户评论信息Comment,在配置了Spring Boot默认注解后,重复进行同样的查询操作,数据库只执行了一次SQL查询语句,说明项目开启的默认缓存支持已经生效。

  • 底层结构:在诸多的缓存自动配置类中, SpringBoot默认装配的是 SimpleCacheConfiguration 他使用的, CacheManager 是 ConcurrentMapCacheManager, 使用 ConcurrentMap 当底层的数据结构按照Cache的名字查询出Cache,每一个Cache中存在多个k-v键值对缓存值。

(4)缓存注解介绍

  刚刚通过使用@EnableCaching、@Cacheable注解实现了Spring Boot默认的基于注解的缓存管理,除此之外,还有更多的缓存注解及注解属性可以配置优化缓存管理。

  1. @EnableCaching注解
    @EnableCaching是由spring框架提供的,springboot框架对该注解进行了继承,该注解需要配置在类上(在中,通常配置在项目启动类上),用于开启基于注解的缓存支持。
  2. @Cacheable注解
    (1)@Cacheable注解也是由spring框架提供的,可以作用于类或方法(通常用在数据查询方法上),用于对方法结果进行缓存存储。注解的执行顺序是,先进行缓存查询,如果为空则进行方法查询,并将结果进行缓存;如果缓存中有数据,不进行方法查询,而是直接使用缓存数据。
    (2)@Cacheable注解提供了多个属性,用于对缓存存储进行相关配置
    在这里插入图片描述执行流程&时机
    方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取,(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建;
    去Cache中查找缓存的内容,使用一个key,默认就是方法的参数,如果多个参数或者没有参数,是按照某种策略生成的,默认是使用KeyGenerator生成的,使用SimpleKeyGenerator生成key,SimpleKeyGenerator生成key的默认策略:
    在这里插入图片描述
    常用的SPEL表达式
    在这里插入图片描述
  3. @CachePut注解
    (1)目标方法执行完之后生效, @CachePut被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个注解保证这个方法依然会执行,执行之后的结果被保存在缓存中。
    (2)@CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同。
    (3)更新操作,前端会把id+实体传递到后端使用,我们就直接指定方法的返回值从新存进缓存时的key="#id" , 如果前端只是给了实体,我们就使用 key="#实体.id" 获取key. 同时,他的执行时机是目标方法结束后执行, 所以也可以使用 key="#result.id" , 拿出返回值的id。
  4. @CacheEvict注解
    @CacheEvict注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解的作用是删除缓存数据。@CacheEvict注解的默认执行顺序是,先进行方法调用,然后将缓存进行清除。

3.2 整合Redis缓存实现

3.2.1 Spring Boot 支持的缓存组件

  在Spring Boot中,数据的缓存管理存储依赖于Spring框架中cache相关的 org.springframework.cache.Cache和org.springframework.cache.CacheManager缓存管理器接口。如果程序中没有定义类型为CacheManager的Bean组件或者是名为cacheResolver的CacheResolver缓存解析器,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

  上面按照Spring Boot缓存组件的加载顺序,列举了支持的9种缓存组件,在项目中添加某个缓存管理组件(例如Redis)后,Spring Boot项目会选择并启用对应的缓存管理器。如果项目中同时添加了多个缓存组件,且没有指定缓存管理器或者缓存解析器(CacheManager或者cacheResolver),那么Spring Boot会按照上述顺序在添加的多个缓存中优先启用指定的缓存组件进行缓存管理。

  刚刚讲解的Spring Boot默认缓存管理中,没有添加任何缓存管理组件能实现缓存管理。这是因为开启缓存管理后,Spring Boot会按照上述列表顺序查找有效的缓存组件进行缓存管理,如果没有任何缓存组件,会默认使用最后一个Simple缓存组件进行管理。Simple缓存组件是Spring Boot默认的缓存管理组件,它默认使用内存中的ConcurrentMap进行缓存存储,所以在没有添加任何第三方缓存组件的情况下,可以实现内存中的缓存管理,但是我们不推荐使用这种缓存管理方式。

3.2.2 基于注解的Redis缓存实现

  在Spring Boot默认缓存管理的基础上引入Redis缓存组件,使用基于注解的方式讲解Spring Boot 整合Redis缓存的具体实现。

(1)添加Spring Data Redis依赖启动器。在pom.xml文件中添加Spring Data Redis依赖启动器

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

  当我们添加进redis相关的启动器之后, SpringBoot会使用 RedisCacheConfigratioin 当做生效的自动配置类进行缓存相关的自动装配,容器中使用的缓存管理器是RedisCacheManager , 这个缓存管理器创建的Cache为 RedisCache , 进而操控redis进行数据的缓存

(2)Redis服务连接配置

# Redis服务地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=

(3)对CommentService类中的方法进行修改使用@Cacheable、@CachePut、@CacheEvict三个注解定制缓存管理,分别进行缓存存储、缓存更新和缓存删除的演示

@Service
public class CommentService {

    @Autowired
    private CommentRepository commentRepository;

    /**
     * 该 @Cacheable 注解将查询结果存放到缓存中,未定义key,默认使用方法参数值,可以使用SpEL表达式
     * cacheNames:定义缓存空间名称
     * unless:定义哪一些不加入到缓存中
     */
    @Cacheable(cacheNames = "comment", unless = "#result==null")
    public Comment findCommentById(Integer id) {
        Optional<Comment> byId = commentRepository.findById(id);
        if (byId.isPresent()) {
            return byId.get();
        }
        return null;
    }

    /**
     * 该 @CachePut 注解将更新后的对象存放到缓存中
     * cacheNames:定义缓存空间名称
     * key:定义缓存的key,此次key为返回结果的id属性
     */
    @CachePut(cacheNames = "comment", key = "#result.id")
    public Comment updateComment(Comment comment) {
        commentRepository.updateComment(comment.getAuthor(), comment.getaId());
        return comment;
    }

    /**
     * 该 @CacheEvict 注解会删除缓存中的数据,未定义删除哪个key的值,默认是方法参数值
     */
    @CacheEvict(cacheNames = "comment")
    public void deleteComment(int comment_id) {
        commentRepository.deleteById(comment_id);
    }
}

  以上使用@Cacheable、@CachePut、@CacheEvict注解在数据查询、更新和删除方法上进行了缓存管理。其中,查询缓存@Cacheable注解中没有标记key值,将会使用默认参数值comment_id作为key进行数据保存,在进行缓存更新时必须使用同样的key;同时在查询缓存@Cacheable注解中,定义了“unless = “#result==null””表示查询结果为空不进行缓存。

(4) 基于注解的Redis查询缓存测试

在这里插入图片描述
  可以看出,查询用户评论信息Comment时执行了相应的SQL语句,但是在进行缓存存储时出现了 IllegalArgumentException非法参数异常,提示信息要求对应Comment实体类必须实现序列化(“DefaultSerializer requires a Serializable payload but received an object of type”)。

(5)将缓存对象实现序列化。

在这里插入图片描述
(6)再次启动测试

  访问“http://localhost:8080/findCommentById?id=1”查询id为1的用户评论信息,并重复刷新浏览器查询同一条数据信息,查询结果。
在这里插入图片描述
  查看控制台打印的SQL查询语句

在这里插入图片描述
  还可以打开Redis客户端可视化管理工具Redis Desktop Manager连接本地启用的Redis服务,查看具体的数据缓存效果
在这里插入图片描述
  执行findById()方法查询出的用户评论信息Comment正确存储到了Redis缓存库中名为comment的名称空间下。其中缓存数据的唯一标识key值是以“名称空间comment::+参数值(comment::1)”的字符串形式体现的,而value值则是经过JDK默认序列格式化后的HEX格式存储。这种JDK默认序列格式化后的数据显然不方便缓存数据的可视化查看和管理,所以在实际开发中,通常会自定义数据的序列化格式。

(7) 基于注解的Redis缓存更新测试。

  先通过浏览器访问“http://localhost:8080/updateComment?author=“jack”&id=1”更新id为1的评论作者名为 jack;

在这里插入图片描述
  接着,继续访问“http://localhost:8080/findCommentById?id=1”查询id为1的用户评论信息

在这里插入图片描述
在这里插入图片描述
  可以看出,执行updateComment()方法更新id为1的数据时执行了一条更新SQL语句,后续调用findById()方法查询id为1的用户评论信息时没有执行查询SQL语句,且浏览器正确返回了更新后的结果,表明@CachePut缓存更新配置成功

(8)基于注解的Redis缓存删除测试

   通过浏览器访问“http://localhost:8080/deleteComment?id=1”删除id为1的用户评论信息;

在这里插入图片描述
在这里插入图片描述
  执行deleteComment()方法删除id为1的数据后查询结果为空,之前存储在Redis数据库的comment相关数据也被删除,表明@CacheEvict缓存删除成功实现。

  通过上面的案例可以看出,使用基于注解的Redis缓存实现只需要添加Redis依赖并使用几个注解可以实现对数据的缓存管理。另外,还可以在Spring Boot全局配置文件中配置Redis有效期,示例代码如下:

# 对基于注解的Redis缓存数据统一设置有效期为1分钟,单位毫秒
spring.cache.redis.time-to-live=60000

  上述代码中,在Spring Boot全局配置文件中添加了“spring.cache.redis.time-to-live”属性统一配置Redis数据的有效期(单位为毫秒),但这种方式相对来说不够灵活。

3.2.3 基于API的Redis缓存实现

  在Spring Boot整合Redis缓存实现中,除了基于注解形式的Redis缓存实现外,还有一种开发中常用的方式——基于API的Redis缓存实现。这种基于API的Redis缓存实现,需要在某种业务需求下通过Redis提供的API调用相关方法实现数据缓存管理;同时,这种方法还可以手动管理缓存的有效期。下面,通过Redis API的方式讲解Spring Boot整合Redis缓存的具体实现。

(1)使用Redis API进行业务数据缓存管理。在com.cyd.service包下编写一个进行业务处理的类ApiCommentService。

@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();
                // 将查询结果存入到缓存中,并设置有效期为1天
                redisTemplate.opsForValue().set("comment_" + id, comment, 1, TimeUnit.DAYS);
                return comment;
            }
            return null;
        }
    }

    public Comment updateComment(Comment comment) {
        commentRepository.updateComment(comment.getAuthor(), comment.getId());
        // 更新数据后进行缓存更新
        redisTemplate.opsForValue().set("comment_" + comment.getId(), comment, 1, TimeUnit.DAYS);
        return comment;
    }

    public void deleteComment(int comment_id) {
        commentRepository.deleteById(comment_id);
        // 删除缓存数据
        redisTemplate.delete("comment_" + comment_id);
    }
}

(2)编写Web访问层Controller文件

@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 void updateComment(Comment comment) {
        commentService.updateComment(comment);
    }

    @RequestMapping(value = "/deleteComment")
    public void deleteComment(int id) {
        commentService.deleteComment(id);
    }
}
  • 基于API的Redis缓存实现的相关配置。基于API的Redis缓存实现不需要@EnableCaching注解开启基于注解的缓存支持,所以这里可以选择将添加在项目启动类上的@EnableCaching进行删除或者注释。

3.2.4 自定义Redis缓存序列化机制

  刚刚完成了Spring Boot整合Redis进行了数据的缓存管理,但缓存管理的实体类数据使用的是JDK序列化方式,不便于使用可视化管理工具进行查看和管理。
在这里插入图片描述
  接下来分别针对基于注解的Redis缓存实现和基于API的Redis缓存实现中的数据序列化机制进行介绍,并自定义JSON格式的数据序列化方式进行数据缓存管理。

3.2.4.1 自定义RedisTemplate
3.2.4.1.1 Redis API默认序列化机制

  基于API的Redis缓存实现是使用RedisTemplate模板进行数据缓存操作的,这里打RedisTemplate 类,查看该类的源码信息。

public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {

	// 声明了key、value的各种序列化方式,初始值为空
	@Nullable
	private RedisSerializer keySerializer = null;
	@Nullable
	private RedisSerializer valueSerializer = null;
	@Nullable
	private RedisSerializer hashKeySerializer = null;
	@Nullable
	private RedisSerializer hashValueSerializer = null;
	...
	
	// 进行默认序列化方式设置,设置为JDK序列化方式
	public void afterPropertiesSet() {
		super.afterPropertiesSet();
		boolean defaultUsed = false;
		if(this.defaultSerializer == null) {
			this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null?this.classLoader:this.getClass().getClassLoader());
		}
		...
	}
	
	...
}

  从上述RedisTemplate核心源码可以看出,在RedisTemplate内部声明了缓存数据key、value的各种序列化方式,且初始值都为空;在afterPropertiesSet()方法中,判断如果默认序列化参数defaultSerializer为空,将数据的默认序列化方式设置为JdkSerializationRedisSerializer。

根据上述源码信息的分析,可以得到以下两个重要的结论:

  1. 使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是 JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接口(例如Serializable);
  2. 使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式defaultSerializer,那么将使用自定义的序列化方式。

  另外,在RedisTemplate类源码中,看到的缓存数据key、value的各种序列化类型都是RedisSerializer。进入RedisSerializer源码查看RedisSerializer支持的序列化方式(进入该类后,使用Ctrl+Alt+左键单击类名查看)

在这里插入图片描述
  可以看出,RedisSerializer是一个Redis序列化接口,默认有7个实现类,这7个实现类代表了7种不同的数据序列化方式。其中,JdkSerializationRedisSerializer是JDK自带的,也是RedisTemplate内部默认使用的数据序列化方式,开发者可以根据需要选择其他支持的序列化方式(例如JSON方式)

3.2.4.1.2 自定义RedisTemplate序列化机制

在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效。打开RedisAutoConfiguration类,查看内部源码中关于RedisTemplate的定义方式。

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

  从上述RedisAutoConfiguration核心源码中可以看出,在Redis自动配置类中,通过Redis连接工厂RedisConnectionFactory初始化了一个RedisTemplate;该类上方添加了@ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效),用来表明如果开发者自定义了一个名为redisTemplate的Bean,则该默认初始化的RedisTemplate不会生效。

  如果想要使用自定义序列化方式的RedisTemplate进行数据缓存操作,可以参考上述核心代码创建一个名为redisTemplate的Bean组件,并在该组件中设置对应的序列化方式即可。

  接下来,在项目中创建名为com.cyd.config的包,在该包下创建一个Redis自定义配置类RedisConfig,并按照上述思路自定义名为redisTemplate的Bean组件。

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        // 使用JSON格式序列化对象,对缓存数据key和value进行转换
        Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class);

        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSerializer.setObjectMapper(om);

        // 设置RedisTemplate模板API的序列化方式为JSON
        template.setDefaultSerializer(jacksonSerializer);
        return template;

    }

}

  通过@Configuration注解定义了一个RedisConfig配置类,并使用@Bean注解注入了一个默认名称为方法名的redisTemplate组件(注意,该Bean组件名称必须是redisTemplate)。在定义的Bean组件中,自定义了一个RedisTemplate,使用自定义的Jackson2JsonRedisSerializer数据序列化方式;在定制序列化方式中,定义了一个ObjectMapper用于进行数据转换设置。

3.2.4.1.3 效果测试

在这里插入图片描述
查看控制台打印的SQL查询语句:

在这里插入图片描述
  可以看出,执行findById()方法正确查询出用户评论信息Comment,重复进行同样的查询操作,数据库只执行了一次SQL语句,这说明定制的Redis缓存生效。

  使用Redis客户端可视化管理工具Redis Desktop Manager查看缓存数据 :
在这里插入图片描述
  执行findCommentById()方法查询出用户评论信息Comment正确存储到了Redis缓存库中,且缓存到Redis服务的数据已经使用了JSON格式存储展示,查看和管理也非常方便,说明自定义的Redis API模板工具RedisTemplate生效。

3.2.4.2 自定义RedisCacheManager

  刚刚针对基于 API方式的RedisTemplate进行了自定义序列化方式的改进,从而实现了JSON序列化方式缓存数据,但是这种自定义的RedisTemplate对于基于注解的Redis缓存来说,是没有作用的。接下来,针对基于注解的Redis缓存机制和自定义序列化方式进行讲解。

3.2.4.2.1 Redis注解默认序列化机制

  打开Spring Boot整合Redis组件提供的缓存自动配置类RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache包下的),查看该类的源码信息,其核心代码如下:

@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));
        }

        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;同时定制RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式。

  如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以参考上述核心代码创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可。

  注意,在Spring Boot 2.X版本中,RedisCacheManager是单独进行构建的。因此,在Spring Boot 2.X版本中,对RedisTemplate进行自定义序列化机制构建后,仍然无法对RedisCacheManager内部默认序列化机制进行覆盖(这也就解释了基于注解的Redis缓存实现仍然会使用JDK默认序列化机制的原因),想要基于注解的Redis缓存实现也使用自定义序列化机制,需要自定义RedisCacheManager。

3.2.4.2.2 自定义RedisCacheManager

  在项目的Redis配置类RedisConfig中,按照上一步分析的定制方法自定义名为cacheManager的Bean组件

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        // 使用JSON格式序列化对象,对缓存数据key和value进行转换
        Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class);

        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSerializer.setObjectMapper(om);

        // 设置RedisTemplate模板API的序列化方式为JSON
        template.setDefaultSerializer(jacksonSerializer);
        return template;
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
        RedisSerializer<String> strSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class);

        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSerializer.setObjectMapper(om);

        // 定制缓存数据序列化方式及时效
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                        .entryTtl(Duration.ofDays(1))
                        .serializeKeysWith(RedisSerializationContext.SerializationPair
                                .fromSerializer(strSerializer))
                        .serializeValuesWith(RedisSerializationContext.SerializationPair
                                .fromSerializer(jacksonSerializer))
                        .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
        return cacheManager;
    }

}

  上述代码中,在RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的cacheManager组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和value分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String格式),而value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天

  完成基于注解的Redis缓存管理器RedisCacheManager定制后,可以对该缓存管理器的效果进行测试(使用自定义序列化机制的RedisCacheManager测试时,实体类可以不用实现序列化接口)。

;