SpringBoot笔记
SpringBoot整合SpringData JPA
-
SpringBoot整合SpringData JPA依赖导入
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
-
SpringBoot连接数据库配置,使用properties文件的方式(数据库版本MySQL 8.0)
spring.datasource.url=jdbc:mysql://localhost:3306/blog_system?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true spring.datasource.username=root spring.datasource.password=123456 #spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
上面注释了spring.datasource.driver-class-name配置,是因为在SpringBoot 2中默认提供了MySql的数据库连接驱动类 “com.mysql.cj.jdbc.Driver”
-
准备测试环境
-
创建数据库和表
CREATE DATABASE blog_system; DROP TABLE IF EXISTS t_article; CREATE TABLE t_article ( id INT(11) NOT NULL AUTO_INCREMENT, title VARCHAR(50) NOT NULL COMMENT '文章标题', content LONGTEXT COMMENT '文章具体内容', created DATE NOT NULL COMMENT '发表时间', modified DATE DEFAULT NULL COMMENT '修改时间', categories VARCHAR(200) DEFAULT '默认分类' COMMENT '文章分类', tags VARCHAR(200) DEFAULT NULL COMMENT '文章标签', allow_comment TINYINT(1) NOT NULL DEFAULT '1' COMMENT '是否允许评论', thumbnail VARCHAR(200) DEFAULT NULL COMMENT '文章缩略图', PRIMARY KEY (id) ) ENGINE=INNODB DEFAULT CHARSET=utf8;
-
准备数据
INSERT INTO t_article VALUES ('1', '2019新版Java学习路线图','Java学习路线图具体内容 具体内容具体内容具体内容具体内容具体内容具体内容','2019-10-10', NULL, '默认分类', '‘2019,Java,学习路线图', '1', NULL); INSERT INTO t_article VALUES ('2', '2019新版Python学习线路图','据悉,Python已经入驻 小学生教材,未来不学Python不仅知识会脱节,可能与小朋友都没有了共同话题~~所以,从今天起不要再 找借⼝,不要再说想学Python却没有资源,赶快行动起来,Python等你来探索' ,'2019-10-10', NULL, '默认分类', '‘2019,Java,学习路线图', '1', NULL); INSERT INTO t_article VALUES ('3', 'JDK 8——Lambda表达式介绍',' Lambda表达式是JDK 8中一个重要的新特性,它使一个清晰简洁的表达式来表达一个接口,同时Lambda表达式也简化了对集合 以及数组数据的遍历、过滤和提取等操作。下面,本篇文章就对Lambda表达式进行简要介绍,并进行演示 说明' ,'2019-10-10', NULL, '默认分类', '‘2019,Java,学习路线图', '1', NULL); INSERT INTO t_article VALUES ('4', '函数式接口','虽然Lambda表达式可以实现匿名内部类的 功能,但在使用时却有一个局限,即接口中有且只有一个抽象方法时才能使用Lamdba表达式代替匿名内部 类。这是因为Lamdba表达式是基于函数式接口实现的,所谓函数式接口是指有且仅有一个抽象方法的接 口,Lambda表达式就是Java中函数式编程的体现,只有确保接口中有且仅有一个抽象⽅法,Lambda表达 式才能顺利地推导出所实现的这个接口中的方法' ,'2019-10-10', NULL, '默认分类', '‘2019,Java,学习路线图', '1', NULL); INSERT INTO t_article VALUES ('5', '虚拟化容器技术——Docker运行机制介绍','Docker是一 个开源的应用容器引擎,它基于go语言开发,并遵从Apache2.0开源协议。使用Docker可以让开发者封装 他们的应用以及依赖包到一个可移植的容器中,然后发布到任意的Linux机器上,也可以实现虚拟化。 Docker容器完全使用沙箱机制,相互之间不会有任何接口,这保证了容器之间的安全性' ,'2019-10-10', NULL, '默认分类', '‘2019,Java,学习路线图', '1', NULL);
-
创建实体类,并配置和数据表的映射
package com.springboot.lean.pojo; import lombok.Data; import javax.persistence.*; import java.util.Date; @Entity @Table(name = "t_article") @Data public class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; @Lob private String content; private Date created; private Date modified; private String categories; private String tags; @Column(name = "allow_comment") private Integer allowComment; private String thumbnail; }
因为实体中allowComment字段和数据库表中的allow_comment不一致,所以需要手动映射。@Data注解是提供get,set,toString等方法的,使用该注解需要导入以下依赖
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
当然后也可以不使用该注解,自己提供get,set方法
-
创建dao层
package com.springboot.lean.repository; import com.springboot.lean.pojo.Article; import org.springframework.data.jpa.repository.JpaRepository; public interface ArticleRepository extends JpaRepository<Article,Integer> { }
-
创建Service接口层
package com.springboot.lean.service; import com.springboot.lean.pojo.Article; import org.springframework.data.domain.Page; import java.util.List; public interface ArticleService { public List<Article> selectList(); public Page<Article> page(int pageNum, int pageSize); }
-
创建Service实现层
package com.springboot.lean.service.impl; import com.springboot.lean.pojo.Article; import com.springboot.lean.repository.ArticleRepository; import com.springboot.lean.service.ArticleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.List; @Service public class ArticleServiceImpl implements ArticleService { @Autowired private ArticleRepository articleRepository; @Override public List<Article> selectList() { return articleRepository.findAll(); } @Override public Page<Article> page(int pageNum, int pageSize) { Pageable pageable = PageRequest.of(pageNum, pageSize); Page<Article> articles = articleRepository.findAll(pageable); return articles; } }
-
在properties文件中, 配置JPA打印SQL
##配置JPA打印SQL spring.jpa.show-sql=true
-
单元测试~
-
SpringBoot单元测试
-
使用SpringBoot的单元测试,需要导入以下依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
-
测试代码实例
package com.springboot.lean.springboothomework; import com.springboot.lean.pojo.Article; import com.springboot.lean.repository.ArticleRepository; 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; import javax.annotation.security.RunAs; import java.util.Optional; @RunWith(SpringRunner.class) @SpringBootTest class SpringbootHomeworkApplicationTests { @Autowired private ArticleRepository articleRepository; @Test void contextLoads() { Optional<Article> byId = articleRepository.findById(1); Article article = byId.get(); System.out.println(article); } }
-
测试结果
SpringBoot整合MyBatis
- 编写配置文件信息
- 在properties文件中配置数据库连接信息,同整合SpringData JPA
注解方式整合MyBatis
-
注解方式方式整合MyBatis
-
编写数据库表对应的实体类Article类
package com.learn.springoot.pojo; import lombok.Data; import java.util.Date; @Data//用于提供get,set等方法,可以不用自己提供get,set等方法 public class Article { private Integer id; private String title; private String content; private Date created; private Date modified; private String categories; private String tags; private Integer allowComment; private String thumbnail; }
-
创建对应的Mapper,ArticleMapper
package com.learn.springoot.mapper; import com.learn.springoot.pojo.Article; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface ArticleMapper { @Select("select * from t_article where id = #{id}") public Article findById(Integer id); }
@Mapper注解表示该类是一个Mabatis的Mapper文件,并保证能够被SpringBoot自动扫描到Spring容器中。
-
编写测试方法
package com.learn.springoot; import com.learn.springoot.mapper.ArticleMapper; import com.learn.springoot.pojo.Article; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class MybatisSpringbootApplicationTests { @Autowired private ArticleMapper articleMapper; @Test void contextLoads() { Article byId = articleMapper.findById(1); System.out.println(byId); } }
-
测试结果
从上面的图中可以看到,allowComment字段的值为null,这是因为,在Article实体类中,属性名和数据库表中的字段名不一致导致的,数据库表中,我们是以下划线的方式命名的,在实体类中是以驼峰的方式命名的,因此,在这里,在配置文件中开启mybatis的驼峰命名匹配配置
#### 开启驼峰命名匹配 mybatis.configuration.map-underscore-to-camel-case=true
然后再次测试
现在allowComment字段的值就查找出来了
-
@MapperScan
上面我们编写ArticleMapper时,使用了@Mapper注解,这种方式可以快速的标记,但是当有多个Mapper文件时,我们需要重复的添加@Mapper注解,为了解决这个麻烦,我们可以使用@MapperScan注解,直接在SpringBoot应用的启动类上添加@MapperScan(“xxx”)注解
-
@MapperScan测试
- 首先注释ArticleMapper上的@Mapper注解
package com.learn.springoot.mapper; import com.learn.springoot.pojo.Article; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; //@Mapper public interface ArticleMapper { @Select("select * from t_article where id = #{id}") public Article findById(Integer id); }
- 在启动类上添加@MapperScan注解
package com.learn.springoot; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = "com.learn.springoot.mapper") public class MybatisSpringbootApplication { public static void main(String[] args) { SpringApplication.run(MybatisSpringbootApplication.class, args); } }
- 执行测试方法结果
@Mapper注解和@MapperScan注解都能在让Spring扫描到Mapper文件,但是使用@MapperScan时,必须指定要扫描的包
-
XML方式整合Mybatis
-
使用XML文件的方式整合Mybatis
-
创建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.learn.springoot.mapper.ArticleMapper"> <select id="findById" resultType="com.learn.springoot.pojo.Article"> select * from t_article where id=#{id} </select> </mapper>
-
配置XML映射文件。使用XML的方式整合mybatis时,SpringBoot并不知道,所以无法扫描到该自定义编写的XML配置文件,因此还必须在全局配置文件application.properties中,配置MyBatis映射文件的路径,同时添加实体类别名映射路径(非必须)
### 配置映射文件路径 mybatis.mapper-locations=classpath:com/learn/springoot/mapper/*.xml ### 配置实体类别名路径 mybatis.type-aliases-package=com.learn.springoot.pojo
-
注释掉之前ArticleMapper中的注解查询
package com.learn.springoot.mapper; import com.learn.springoot.pojo.Article; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; //@Mapper public interface ArticleMapper { // @Select("select * from t_article where id = #{id}") public Article findById(Integer id); }
-
单元测试
-
SpringBoot整合Redis
-
SpringBoot整合Redis需要导入如下依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
编写实体类
package com.learn.springoot.domain; import org.springframework.data.redis.core.index.Indexed; public class Address { @Indexed private String city; @Indexed private String country; 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.learn.springoot.domain; import org.springframework.data.annotation.Id; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.index.Indexed; @RedisHash("person")//指定实体对象在Redis数据库中的存储空间 public class Person { @Id//标识实体类主键 private String id; @Indexed//标识属性在Redis数据库中生成二级索引 private String firstname; @Indexed private String lastname; private Address address; 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 + '}'; } }
实体类中,针对面向Redis数据库的数据操作,设置了几个主要的注解,注解说明如下:
-
@RedisHash(“person”):用于指定操作实体类对象在Redis数据库中的存储空间,此处表示,针对Person实体类的的数据操作都存储在Redis中名为person的存储空间下
-
@Id:用于标识实体类主键。在Redis中,会默认生成字符串形式的HashKey作为唯一的实体对象的Id,也可以在数据存储时手动设置Id值
-
@Indexed:用于标识对应属性在Redis中生存二级索引,属性名就是二级索引名称,方便进行数据条件查询
-
编写Repository接口。SpringBoot针对Redis在内的一些常用数据库,提供了自动化配置,可以通过实现Repository接口,简化对数据的增删改查操作
package com.learn.springoot.repository; import com.learn.springoot.domain.Person; import org.springframework.data.repository.CrudRepository; import java.util.List; public interface PersonRepository extends CrudRepository<Person, String> { List<Person> findByAddress_City(String city); }
-
Redis数据库连接配置
### Redis服务器地址 spring.redis.host=127.0.0.1 ### Redis服务端口 spring.redis.port=6379 ### Redis服务连接密码(默认为空) spring.redis.password=
-
编写测试方法
package com.learn.springoot; import com.learn.springoot.domain.Address; import com.learn.springoot.domain.Person; import com.learn.springoot.mapper.ArticleMapper; import com.learn.springoot.pojo.Article; import com.learn.springoot.repository.PersonRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest class MybatisSpringbootApplicationTests { @Autowired private PersonRepository personRepository; @Test public void savePerson() { Person person = new Person(); person.setFirstname("张"); person.setLastname("三"); Address address = new Address(); address.setCity("北京"); address.setCountry("中国"); person.setAddress(address); personRepository.save(person); } @Test public void findPerson() { List<Person> personList = personRepository.findByAddress_City("北京"); for (Person person : personList) { System.out.println(person); } } }
首先执行savePerson方法,然后执行findPerson方法,最后执行结果如下图:
-
通过Redis可视化工具Redis-Desktop-Manager查看数据信息
执行savePerson方法后,数据保存成功,在可视化工具中,可以看到有一些二级索引,这些都是通过在属性上标注了@Indexed注解后,自动生成的,便于条件查询,如上测试中的findByAdress_City方法,就是通过二级索引address.city来进行查找的
-
SpringBoot整合Thymeleaf
-
导入Thymeleaf依赖和Web依赖
<!--thymeleaf 依賴--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--WEB 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
在全局配置文件中配置Thymeleaf模板的一些参数
### 配置是否启用模板缓存 spring.thymeleaf.cache=false
在这里只配置了是否启用缓存,其他的使用的默认配置就可以了
-
Thymeleaf的静态资源访问
开发web应用是难免会使用静态资源,Springboot设置了默认的静态资源访问路径
- classpath:/META-INF/resources/
- classpath:/resources/
- classpath:/static/
- classpath:/public/
在上面四个目录下存放静态资源,可以直接访问
-
编写controller类
package com.learn.springboot.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import java.util.Calendar; @Controller public class LoginController { @GetMapping("/toLogin") public ModelAndView toLogin(){ ModelAndView andView = new ModelAndView(); andView.setViewName("login"); andView.addObject("currentYear", Calendar.getInstance().get(Calendar.YEAR)); return andView; } }
-
编写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,shrink-to-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"> <h1 class="h3 mb-3 font-weight-normal">请登录</h1> <input type="text" class="form-control" required="" autofocus=""> <input type="password" class="form-control" 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>
-
效果展示
上面html文件中使用了,从后台传到页面的值,currentYear。通过使用th:text标签实现了文本的展示
-
Thymeleaf国际化配置
配置国际化页面
-
编写国际化多语言配置文件
在项目的类路径下resouce下创建名称为i8n的文件夹,并在该文件夹中添加多语言国际化配置文件,login.properties,login_zh_CN.properties,login_en_US.properties文件
- login.properties
login.tip=请登录 login.username=用户名 login.password=密码 login.button = 登录 login.rememberme =记住我
- login_zh_CN.properties
login.tip=请登录 login.username=用户名 login.password=密码 login.button = 登录 login.rememberme =记住我
- login_en_US.properties
login.tip=Please sign in login.username=Username login.password=Password login.button = Login login.rememberme =Remember me
login.properties为默认的配置文件,login_zh_CN.properties是中文国际化配置文件,zh_en_US.properties是英文国际化配置文件
在SpringBoot中,默认的国际化配置文件路径为resource下的message.properties,其他语言国际化配置文件的名称必须严格按照“文件的前缀名 语言代码 国家代码.properties”的形式命名
我们现在是在resouce下的i18n文件夹下存放了国际化配置文件,因此需要在项目的全局的配置文件中进行国际化文件基础名配置,才能引用自定义的国际化配置文件
- 编写配置文件
### 配置国际化文件基础名 spring.messages.basename=i18n.login
i18n是相对于resouce文件夹,login表示多语言文件前缀名。如果是按照Springboot的默认识别机制,在resouce下编写message.properties国际化文件,就不需要配置该项。
-
测试配置结果
修改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,shrink-to-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"> <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">请登录</h1> <input type="text" class="form-control" required="" autofocus="" th:placeholder="#{login.username}"> <input type="password" class="form-control" required="" th:placeholder="#{login.password}"> <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> </form> </body> </html>
页面展示效果
在login.html文件中,我们使用了Thymeleaf的标签替换了文本值,重新加载后,页面内容正常显示,说明配置成功
-
定制区域信息解析器
在完成上面的配置后,就可以正式在前端页面结合thymeleaf模板相关属性进行国际化语言设置和展示了,不过这种方式默认使用的是请求头中的语言信息(浏览器的语言信息),自动进行语言切换,如果要使用手动切换语言,则需要定制区域解析器
定义区域解析器
package com.learn.springboot.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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 MyLocalResolver implements LocaleResolver { @Override public Locale resolveLocale(HttpServletRequest httpServletRequest) { //获取手动切换语言的请求参数 zh_CN String language = httpServletRequest.getParameter("language"); //获取请求头中自动传递的语言参数 String header = httpServletRequest.getHeader("Accept-Language"); Locale local = null; // if (!StringUtils.isEmpty(language)) { String[] s = language.split("_"); local = new Locale(s[0], s[1]); } else { //Accept-Language: zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6 String[] split = header.split(","); //zh-CN String l = split[0]; String[] s = l.split("_"); local = new Locale(s[0], s[1]); } return local; } @Override public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) { } /** * 将自定义的区域解析器注册到Spring容器中 * * @return */ @Bean public MyLocalResolver localResolver() { return new MyLocalResolver(); } }
-
login页面修改
<!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,shrink-to-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">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">请登录</h1>
<input type="text" class="form-control"
required="" autofocus="" th:placeholder="#{login.username}">
<input type="password" class="form-control"
required="" th:placeholder="#{login.password}">
<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="@{/toLogin(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/toLogin(l='en_US')}">English</a>
</form>
</body>
</html>
SpringBoot热部署
我们在开发过程中,修改了代码后要重新部署之后才能看到效果,但是这样重新部署造成了大量的时间的浪费,很影响我们的开发效率,因此热部署很有必要
- SpringBoot的热部署需要引入如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
-
导入了依赖后,需要设置开发工具自动编译,这里使用是IDEA
-
设置了自动编译之后,还需要进行以下一步操作
在项目任意界面使用组合快捷键“Ctrl+Shift+Alt+/”打开Registry,勾选选上其中的compiler.automake.allow.when.app.running
-
热部署测试
@GetMapping("/hello")
public String hello() {
return "hello world!";
}
- 修改测试代码,在不重新启动项目的情况下重新访问连接
@GetMapping("/hello")
public String hello() {
return "hello springboot!";
}
- 热部署配置成功~
SpringBoot缓存管理
环境准备
- 数据环境使用上面整合SpringdData JPA时的Artice
- Article实体
package com.learn.springboot.pojo;
import javax.persistence.*;
import java.util.Date;
/**
* @author leim
* @data 2020/8/27 22:35
* @descript
*/
@Entity
@Table(name = "t_article")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
@Lob
private String content;
private Date created;
private Date modified;
private String categories;
private String tags;
@Column(name = "allow_comment")
private Integer allowComment;
private String thumbnail;
//省略get,set方法
}
- ArticleRepository
package com.learn.springboot.repository;
import com.learn.springboot.pojo.Article;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
public interface ArticleRepository extends JpaRepository<Article, Integer> {
/**
* 修改文章的分类
*
* @param categories
* @param id
*/
@Transactional
@Modifying
@Query(value = "update t_article set categories =?1 where id=?2", nativeQuery = true)//nativeQuery表示使用原生SQL查询
public void updateArticle(String categories, Integer id);
}
- 编写Service
package com.learn.springboot.service;
import com.learn.springboot.pojo.Article;
import com.learn.springboot.repository.ArticleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
@Service
@Transactional
public class ArticleService {
@Autowired
private ArticleRepository articleRepository;
public Article findById(Integer id) {
Optional<Article> byId = articleRepository.findById(id);
if (byId != null) {
return byId.get();
}
return null;
}
}
- 编写controller
package com.learn.springboot.controller;
import com.learn.springboot.pojo.Article;
import com.learn.springboot.repository.ArticleRepository;
import com.learn.springboot.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class ArticleController {
@Autowired
private ArticleService articleService;
@GetMapping("findById")
public Article findById(Integer id) {
return articleService.findById(id);
}
}
- 连接数据库后进行测试,测试结果
现在是没有使用任何的缓存的,所以每次请求都会进行一次数据库查询
默认的缓存管理
- ringboot提供了默认的缓存管理,我i们只需要在项目启动类上添加,@EnableCaching注解,就可以开启SpringBoot基于注解的缓存支持
package com.learn.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching//开启基于注解的缓存支持
public class RedisSpringbbotApplication {
public static void main(String[] args) {
SpringApplication.run(RedisSpringbbotApplication.class, args);
}
}
- @Cacheable注解对数据操作方法进行缓存管理。将@Cacheable注解添加到Service中的查询方法上,对查询的结果进行缓存
@Cacheable(cacheNames = "article")
public Article findById(Integer id) {
Optional<Article> byId = articleRepository.findById(id);
if (byId != null) {
return byId.get();
}
return null;
}
在@Cacheable注解中使用了它的属性cacheNames,该属性的作用就是将查询方法的结果缓存到SpringBoot默认缓存中,名称为article的命名空间中,对应唯一的缓存标识,即缓存数据对应的主键key,默认为方法参数的id
- 测试访问
可以看到,在配置了Springboot的默认缓存后,我们重复进行查询请求,查询出来了结果,但是数据库只进行了一次SQL查询语句,说明项目开启的缓存支持已经生效。
- 底层结构
- SpringBoot默认缓存的装配使用的是SimpleCacheConfiguration,它使用的cacheManager是ConCurrentMapCacheManager,使用ConcurrentMap当底层数据结构,按照Cache的名称,查询出Cache(ConcurrentMapCache),每一个Cache中存在多个key-value
整合Redis缓存实现
SpringBoot支持的缓存组件
在SpringBoot中数据的缓存管理存储依赖于Spring框架Cache相关的org.springframework.cache.Cache和org.springframework.cache.CacheManager缓存管理器接口。如果程序中没有定义CacheManager的Bean或者是名为cacheResovler的CacheResovler缓存解析器,SpringBoot将尝试选择并开启以下缓存组件(按照指定的顺序):
- Generic
- JCache (JSR-107) (EhCache、Hazelcast、infinispan等)
- EhCache2.x
- Hazelcast
- infinispan
- Couchbase
- Redis
- Caffeine
- Simple
在项目中,如果添加了某个缓存的管理器组件(Redis)后,SpringBoot会选择并启用对应的缓存管理器。如果项目中同时添加了多个缓存组件,且没有指定缓存管理器和缓存解析器(CacheResovler活着CacheManager),那么SpringBoot会按照上述的顺序,在添加的多个缓存中,优秀启用指定的缓存组件进行管理。
在没有添加任何缓存管理组件时,使用@EnableCaching开启了缓存管理后,Spring会默认按照上诉的顺序,查找有效的缓存组件进行缓存管理,如果没有任何的缓存组件,会默认使用最后一个Simple缓存组件,进行缓存管理。Simple组件是SpringBoot默认的缓存管理组件,所以在没有任何第三方缓存组件的情况下,可以实现内存中的缓存管理。
基于注解的Redis缓存实现
- 添加Spring Data Redis依赖启动器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
当我们添加了Redis的依赖启动器后,Springboot会使用RedisCacheConfiguration当作生效的配置类,进行缓存相关的自动配置,容器使用的缓存管理器是RedisCacheManager,这个缓存管理器创建的Cache为RedisCache,进而操作Redis进行数据缓存
- Redis缓存配置
### Redis服务器地址
spring.redis.host=127.0.0.1
### Redis服务端口
spring.redis.port=6379
### Redis服务连接密码(默认为空)
spring.redis.password=
- 编写Service类
package com.learn.springboot.service;
import com.learn.springboot.pojo.Article;
import com.learn.springboot.repository.ArticleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
@Transactional
public class ArticleService {
@Autowired
private ArticleRepository articleRepository;
@Cacheable(cacheNames = "article")
public Article findById(Integer id) {
Optional<Article> byId = articleRepository.findById(id);
if (byId != null) {
return byId.get();
}
return null;
}
@CachePut(cacheNames = "article", key = "#result.id")
public Article updateArticleCategories(Article article) {
articleRepository.updateArticle(article.getCategories(), article.getId());
return article;
}
@CacheEvict(cacheNames = "article")
public void deleteArticle(Integer id) {
articleRepository.deleteById(id);
}
}
- 编写Controller
package com.learn.springboot.controller;
import com.learn.springboot.pojo.Article;
import com.learn.springboot.repository.ArticleRepository;
import com.learn.springboot.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class ArticleController {
@Autowired
private ArticleService articleService;
@GetMapping("findById")
public Article findById(Integer id) {
return articleService.findById(id);
}
@GetMapping("/updateArticle")
public Article updateArticle(String categories, Integer id) {
Article byId = articleService.findById(id);
byId.setCategories(categories);
return articleService.updateArticleCategories(byId);
}
@GetMapping("deleteById")
public void deleteById(Integer id) {
articleService.deleteArticle(id);
}
}
-
测试
-
访问findById?id=1,进行了一次数据查询
-
再次访问,数据库未进行SQL查询
-
访问updateArticle?categories=java&id=1
-
数据库执行了一次update SQL语句
-
再次访问findById?id=1
-
数据库未执行查询语句,返回结果中,categories字段的值变为了java
-
查看RedisDesktopManager,有一条缓存信息
-
访问deleteByid?id=1
-
数据库执行了 delete SQL语句
-
再次访问findById?id=1,返回结果报错,因此已经没有这个数据了
-
查看数据库中,无此条数据,查看RedisDesktopManager,里面已经没有缓存的数据信息了
至此,SpringBoot基于注解的Redis的缓存实现已经完成了。
总结:
SpringBoot整合Redis实现基于注解的的缓存管理,只需要添加Redis的相关依赖,然后使用@EnableCaching,开启基于注解的缓存支持,然后使用对应的注解@Cacheable,@CachePut,@CacheEvict就可以了。
另外,可以在全局配置文件中,添加Redis缓存的失效时间
### 配置Redis缓存的失效时间,5分钟,单位ms spring.cache.redis.time-to-live=300000
-
基于API的Redis缓存实现
-
使用API进行业务数据缓存管理
- Service编写
package com.learn.springboot.service; import com.learn.springboot.pojo.Article; import com.learn.springboot.repository.ArticleRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service @Transactional public class ApiArticleService { @Autowired private ArticleRepository articleRepository; @Autowired private RedisTemplate redisTemplate; public Article findById(Integer id) { Object o = redisTemplate.opsForValue().get("comment_" + id); if (o != null) { return (Article) o; } else { Optional<Article> byId = articleRepository.findById(id); if (byId != null) { return byId.get(); } } return null; } public Article updateArticleCategories(Article article) { articleRepository.updateArticle(article.getCategories(), article.getId()); //数据库更新完成后,更新缓存 redisTemplate.opsForValue().set("comment_" + article.getId(), article); return article; } public void deleteArticle(Integer id) { articleRepository.deleteById(id); redisTemplate.delete("comment_" + id); } }
- Controller编写
package com.learn.springboot.controller; import com.learn.springboot.pojo.Article; import com.learn.springboot.service.ApiArticleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/api") public class ApiArticleController { @Autowired private ApiArticleService articleService; @GetMapping("findById") public Article findById(Integer id) { return articleService.findById(id); } @GetMapping("/updateArticle") public Article updateArticle(String categories, Integer id) { Article byId = articleService.findById(id); byId.setCategories(categories); return articleService.updateArticleCategories(byId); } @GetMapping("deleteById") public void deleteById(Integer id) { articleService.deleteArticle(id); } }
- 测试~
注意:使用基于注解方式的Redis缓存实现,缓存的实体(Article)需要实现序列化接口Serializable
自定义Redis序列化机制
在上面完成的实现Redis进行缓存管理中,我们在RedisDesktopManager中查看缓存数据时,发现看到是一些看不明白的序列化信息,这个是因为Redis默认使用的是Jdk序列化方式,不便于使用可视化工具进行查看和管理。
针对基于注解的和基于API的redis缓存实现中的序列化进行介绍,并自定义JSON格式的数据序列化方式进行数据缓存管理
- 自定义 RedisTemplate
package com.learn.springboot.cofig;
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//在没有配置bean的名称时,方法名必须是redisTemplate
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
//使用Json序列化方式,序列化的对象
Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jsonRedisSerializer.setObjectMapper(om);
redisTemplate.setDefaultSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
- 测试
重新访问/api/findById,查看可视化工具,现在Redis中的数据已经使用了Json的数据格式进行存储了,说明自定义的RedisTemplate生效
-
自定义RedisCacheManager
上面实现了自定义RedisTemplate,实现了基于API的的json序列化方式管理缓存,但是这种方式对基于注解的Redis缓存来说是无效的。因为在SpringBoot2.X版本中,RedisCacheManager是单独进行构建的,因此对RedisTemplate进行了自定义序列化方式之后,无法覆盖RedisCacheManager内部的序列化机制,因此想要基于注解的Redis缓存管理,也使用自定义的序列化机制,需要自定义RedisCacheMananger
@Bean//完成基于注解的Redis缓存管理RedisCacheManager的定制后,实体类可以不用实现序列化接口 public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) { RedisSerializer<String> keySerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer valueSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); valueSerializer.setObjectMapper(om); //定制数据缓存序列化方式及时生效 //设置缓存有效期 1天 RedisCacheConfiguration redisConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer)).disableCachingNullValues(); RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory).cacheDefaults(redisConfiguration).build(); return redisCacheManager; }
测试:
在使用自定义的RedisCacheManager时,实体类可以不需要实现序列化接口