配置日志级别
如果需要spring进行配置日志的话,那么这时候我们通过创建日志对象,然后根据这个日志对象调用相应的方法来输出不同日志级别的信息了。而常见的日志级别主要有: trace <- debug <- info <- warn <- error <- fatal(级别是从左到右逐渐变高),fatal级别是灾难性的,所以通常不会使用这个级别。
@Controller
public class LoggerController {
//创建日志对象
private static final Logger log = LoggerFactory.getLogger(LoggerController.class);
@GetMapping("/testLog")
@ResponseBody
public void testLogging(){
System.out.println("testLogging is running........");
/*
日志的级别有:
(最低)trace <- debug <- info <- warn <- error <- fatal(最高)
其中fatal是灾难性的级别.默认是infor的日志级别
如果需要设置日志级别的时候,需要在配置文件中进行配置日志级别
同时可以设置某一个包的日志级别,但是如果有多个包的日志级别相同,那么
这时候设置包的日志级别显然麻烦,所以可以设置某一个分组的日志级别就可以
轻松的解决问题,但是在设置组的日志级别之前,需要创建分组即可
*/
log.debug("debug......");
log.info("info........");
log.warn("warn........");
log.error("error......");
}
}
spring中默认是info的日志级别,但是如果需要设置其他的日志级别,那么需要我们在配置文件中进行配置。同时我们可以设置某一个包或者分组的日志,但是要设置某一个分组的日志之前,需要我们先创建分组,对应的配置文件为:
# 配置日志级别
logging:
group: # 创建日志的分组
ebank: com.example.controller,com.example.service # 这些包在ebank分组
edruid: com.alibaba # alibaba包在edruid分组(这些分组时自定义的)
level:
root: info # 所有的包下面的日志级别都是info
#com.example.controller: debug # com.example.controller包下面的所有类都是debug级别
ebank: warn # 设置ebank分组的日志级别为warn
但是显然上面的代码中,我们每次都需要执行这个代码private static final Logger log = LoggerFactory.getLogger(LoggerController.class);
来创建日志对象,能不能不写这个代码呢?当然是可以的,我们导入lombook坐标,然后lombok中的注解@Slf4j
,这样就可以不用写这个代码来创建日志对象了,而是可以直接使用日志对象了。
@Controller
@Slf4j //导入lombok坐标之后,利用注解@Slf4j,这样就不需要再创建日志对象了
public class LoggerController {
@GetMapping("/testLog")
@ResponseBody
public void testLogging(){
System.out.println("testLogging is running........");
log.debug("debug......");
log.info("info........");
log.warn("warn........");
log.error("error......");
}
}
当我们点击target目录下面,找到对应的LoggerController类,就会发现在这个字节码对象中出现了日志对象,如下所示:
日志输出格式控制格式为:
根据上面的要求,如果需要自定义日志的输出格式的时候,那么在配置文件中配置logging.pattern.console
值即可:
# 配置日志级别
logging:
pattern:
console: "%d %p %t %s %m %n" # 其中#d表示的是日期,%p表示日志级别,%t表示所属的线程,%c表示的时对应的类或者接口,%m表示日志的信息,%n表示换行(不要忘记换行)
当我们在浏览器搜索框上面输入localhost:8080/testLog
(根据上面的LoggerController进行测试的)时,测试测试结果如下所示:
在⽣产环境上咱们需要将⽇志保存下来,以便出现问题之后追 溯问题,把⽇志保存下来的过程就叫做持久化。想要将⽇志进⾏持久化,只需要在配置⽂件中指定⽇志的存储⽬录或者是指定⽇志保存⽂件名之后, Spring Boot 就会将控制台的⽇志写到相应的⽬录或⽂件下.如果需要将日志信息打印到一个文件的时候,那么我们只需要在配置文件中配置下面信息即可:
logging:
pattern:
rolling-file-name:"server_%d{yyyy-MM-dd}_%i.log" # 当日志文件大小超过2KB,就会创建这样格式的日志文件,例如server_2022-08-11_0.log,其中%i表示的是文件的序号
file:
name: server.log # 配置单个日志文件(如果需要将这个日志文件保存在哪一个目录下面,就在名字前面加上路径即可)
max-size: 2KB # 当日志文件的大小超过了2KB,那么就会自动常见日志文件,对应日志文件的名字就是上面的rolling-file-name的值对应的格式
我们来到这个module(右击这个module,然后点击show in explorer
的目录下面,就可以看到这个日志文件了.
开启热部署
当我们的项目已经开启,但是中途中发现代码有问题的时候,那么必然需要进行修改代码,这时候修改完代码之后有需要重新启动项目,如何解决这个问题,使得我们在修改完项目代码之后,不需要我们重新启动,而是交给IDEA来重新启动项目呢?这就需要依靠热部署了,而对应的步骤为:
-
导入坐标spring-boot-starter-devtools
-
虽然这时候已经导入了坐标,但是还没有激活热部署,此时即使修改了代码,但是还是没有办法满足我们的需求。所以需要点击File-> settiong -> build -> compiler ->勾中build project automatically.然后关闭这个窗口,之后在点击
ctrl + alt + shift + /
,点击registry
,将勾中*compiler.automake.allow.when.app.running
.此时自动开启热部署已经完成了。
点击ctrl + alt + shift + /
,然后选中registry
,然后就勾选下面这项就可以了.
当我们修改完之后,我们离开IDEA,等待几秒之后,就会发现热部署重新启动,控制台中打印的信息是修改之后的。
但是如果需要设置热部署的范围,那么需要在配置文件中进行配置spring.devtools.restart.exclude
的值,这样就可以设置热部署的时候不会对这个值的文件进行重启。
第三方bean属性绑定
我们知道读取配置文件中的属性值有3种方式,其中一种方式则是利用注解@ConfigurationProperties
来进行属性绑定,这样就会将对这个注解修饰的类配置绑定配置文件属性。所以这个类需要保证含有get/set方法才可以,并且属性名需要和配置文件中的属性名相同。
此外注解@ConfigurationProperties
也可以给第三方的bean进行属性绑定,只要属性名相同就可以了。
如下所示:
@Component //将这个类添加到spring容器中,成为一个bean对象
@ConfigurationProperties(prefix = "servers") //和配置文件中的属性servers属性进行绑定
public class ServerCase {
private String ipAddress;
private int port;
private long timeout;
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
ipAddress = ipAddress;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
@Override
public String toString() {
return "ServerCase{" +
"IpAddress='" + ipAddress + '\'' +
", port=" + port +
", timeout=" + timeout +
'}';
}
}
servers:
port: 8888
ipAddress: 127.0.0.1
timeout: -1
dataSource:
driver-class-name: driver123
测试代码:
@SpringBootApplication
public class SSAPApplication { //spring boot的启动类
@Bean
@ConfigurationProperties(prefix = "datasource") //配置文件中的dataSource含有属性driverClassName这个属性,而DruidDataSource也有属性driverClassName
public DruidDataSource druidDataSource(){
DruidDataSource druid = new DruidDataSource();
return druid;
}
public static void main(String[] args) {
ConfigurableApplicationContext app = SpringApplication.run(SSAPApplication.class, args);
ServerCase serverCase = app.getBean(ServerCase.class);
System.out.println(serverCase);//输出ServerCase{IpAddress='127.0.0.1', port=8888, timeout=-1}
DruidDataSource druid = app.getBean(DruidDataSource.class);
System.out.println(druid.getDriverClassName());//输出driver123
}
}
但是我们会发现尽管配置文件中servers的属性port或者ipAddress等属性名不管以什么形式来写,例如全是大写,或者以常量的形式,或者以-
来分割不同的单词(dataSource中的属性driver-class-name就是烤肉串形式),都可以进行属性的绑定,这是因为**@ConfigurationProperties具有松散绑定**,也正是这样,dataSource中的属性driver-class-name可以被DruidDataSource绑定.而注解@Value
则没有松散绑定。
但是我们注意的是,注解@ConfigurationProperties
的属性prefix的值是有它的规范的,必须是字母或者数字以及下划线,并且字母都是小写形式的,而且第一个字母以字母开头。违反这些规定的时候,就会发生报错,这就是为什么上面给DruidDataSource进行绑定的时候,prefix的值是datasource,而不是配置文件中的dataSource。一旦我们写成了dataSource
,结果如下所示:
所以必须是小写形式的字母或者数字,并且用-
作为分隔符,并且必须要以字母来开头.
当然我们也可以利用注解@EnableConfigurationProperties
来进行属性绑定,但是修饰的是类,其中它的属性classes是修饰的类的字节码对象,这样他就会将对应的类添加到Spring容器中,所以对应的类上方不需要再使用注解来将这个类添加到spring容器中,否则就会提示这个bean不唯一,从而发生报错NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.domain.ServerCase' available: expected single matching bean but found 2: serverCase,servers-com.example.domain.ServerCase
,而在对应的类的上方,依旧是需要利用注解@ConfigurationProperties
来进行属性绑定,否则,如果在对应的类上方没有使用注解@ConfigurationProperties
进行属性绑定,那么就会发生报错, 提示No ConfigurationProperties annotation found on 'com.example.domain.ServerCase'
。
所以,**@EnableConfigurationProperties注解的作用是:令使用 @ConfigurationProperties 注解的类生效,将其添加到spring容器中。而要将哪些类添加到容器中,而是根据@EnableConfigurationProperties
的属性值来判断的,所以它的属性值对应的类必须要被注解@ConfigurationProperties
修饰.**那么既然也是将类添加到容器中,那为什么还需要用到这个注解呢?可以参考文章:springboot中EnableAutoConfiguration自动装配的使用
但是要进行配置文件进行属性绑定的时候,需要注意以下问题:
-
常用计量单位问题,在配置文件中没有办法知道某一个属性的单位,这时候我们需要利用注解
@Druation
来设置时间单位,也可以利用注解@DataSize
来设置字节大小 -
进制转换问题,在配置文件中,它支持二进制,八进制,十进制,十六进制,如果它的值是以0开始,并且所有的值都是1-7范围的数字,那么它是一个八进制的数字,同理,如果是以0x开始,并且所有的数字是0-9,a-f,那么就是一个十六进制数字。这时候进行属性绑定的时候,就会发生进制转换的问题。如下所示:
@Component @ConfigurationProperties(prefix = "servers") public class ServerCase { private String IpAddress; private int port; private long timeout; private int password; @DurationUnit(ChronoUnit.SECONDS) private Duration time; @DataSizeUnit(DataUnit.KILOBYTES) private DataSize size; public DataSize getSize() { return size; } public void setSize(DataSize size) { this.size = size; } public Duration getTime() { return time; } public void setTime(Duration time) { this.time = time; } public String getIpAddress() { return IpAddress; } public void setIpAddress(String ipAddress) { IpAddress = ipAddress; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public long getTimeout() { return timeout; } public void setTimeout(long timeout) { this.timeout = timeout; } public int getPassword() { return password; } public void setPassword(int password) { this.password = password; } @Override public String toString() { return "ServerCase{" + "IpAddress='" + IpAddress + '\'' + ", port=" + port + ", timeout=" + timeout + ", time=" + time + ", password=" + password + ", size=" + size + '}'; } }
配置文件:
servers: port: 8888 ipAddress: 127.0.0.1 timeout: -1 time: 3 password: 0127 # 这时候以0开始,并且其他数字是1-7范围,所以是一个八进制数字 size: 2
测试类输出的时候:
@SpringBootApplication public class SSAPApplication { public static void main(String[] args) { ConfigurableApplicationContext app = SpringApplication.run(SSAPApplication.class, args); ServerCase serverCase = app.getBean(ServerCase.class); System.out.println(serverCase); } }
输出结果:
所以解决办法就是将配置文件中的password的值设置为一个字符串,即利用单引号或者双引号进行括起来,或者注意这个进制问题即可.
-
异常问题,如果配置文件中的值是一个string类型,而在类中对应的属性是一个int类型,那么这时候就会发生类型转换错误。这时候就涉及到了属性校验的问题,此时我们需要导入坐标validation-api以及hibernate-validator来进行属性校验,然后再对应的类上面使用注解
@Validated
,来开启属性校验。对应的坐标为:
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> </dependency>
@Component @ConfigurationProperties(prefix = "servers") @Validated //开始属性校验 public class ServerCase { private String IpAddress; @Max(value=8080, message = "最大值为8080") @Min(value=20, message = "最小值为20") private int port; private long timeout; private int password; @DurationUnit(ChronoUnit.SECONDS) private Duration time; @DataSizeUnit(DataUnit.KILOBYTES) private DataSize size; public DataSize getSize() { return size; } public void setSize(DataSize size) { this.size = size; } public Duration getTime() { return time; } public void setTime(Duration time) { this.time = time; } public String getIpAddress() { return IpAddress; } public void setIpAddress(String ipAddress) { IpAddress = ipAddress; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public long getTimeout() { return timeout; } public void setTimeout(long timeout) { this.timeout = timeout; } public int getPassword() { return password; } public void setPassword(int password) { this.password = password; } @Override public String toString() { return "ServerCase{" + "IpAddress='" + IpAddress + '\'' + ", port=" + port + ", timeout=" + timeout + ", time=" + time + ", password=" + password + ", size=" + size + '}'; } }
测试结果为:
测试controller层
如果我们需要测试controller层,那么这时候我们在测试类中怎么做呢?基本步骤为:
- 在注解
@SpringbootTest
中,设置它的属性webEnvironment
的值为随机端口,即值为SpringBootTest.WebEnvironment.RANDOM_PORT
,如果这个属性的值为SpringBootTest.WebEnvironment.NONE
,那么就是没有办法进行测试controller层,如果是SpringBootTest.WebEnvironment.DEFINE_PORT
,那么对应的端口就是指定的端口。 - 在类上方使用注解
@AutoConfigureMockMvc
,从而开启虚拟mvc调用 - 同时需要在这个类的上面使用注解
@Transactional
,这样如果涉及到数据库的操作的时候,那么就不会影响到了数据库中表的数据了。 - 在进行测试的时候,需要从spring容器中取出MockMvc对象,这样通过这个对象调用对应的方法来执行对应的请求。
测试的基本模板:
- 首先需要获取请求,表明需要执行的是什么操作。通过MockMvcRequestBuilder对象调用对应的方法来设置执行的是什么操作,例如调用get方法的时候,表示执行的是GET请求。
- 然后通过MockMvc对象执行perform方法,来执行请求的内容,然后返回的是执行的结果ResultActions
- 设置执行的预期值。先获取对应的匹配器,通过MockMvcResultMatchers调用对应的方法,从而获取对应的匹配器,例如需要状态的,那么就调用status方法,从而返回的是StatusResultMatchers对象。
- 通过上面的操作中发牛的XXXResultMatcher调用对应的方法,来设置期望值,这时候就会返回一个ResultMatcher
- 通过上面的ResultActions对象,调用andExpect方法,从而将真实值和期望值进行比较,其中传递的参数就是ResultMatcher对象。
对应的代码为:
/*
测试业务层:
通过设置属性webEnvironment的值为WebEnvironment.NONE,这时候并没有指定
端口号,此时不可以进行测试业务层controller的业务.
如果webEnvironment的职位WebEnvironment.DEFINED_PORT,那么端口号就是8080
此时就容易出现端口占用的问题,所以需要设置他的值为RANDOM_PORT随机端口
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//设置虚拟的Mvc调用,用于发送虚拟请求
@AutoConfigureMockMvc
@Transactional //开启事务,这样测试的时候,数据不会影响到数据库表
public class BookControllerTest {
@Autowired
private MockMvc mvc;
@Test
public void test() throws Exception {
//创建虚拟请求,因为是get请求,所以MockMvcRequestBuilder调用的是get方法
//其中方法的参数是url,但是由于端口是随机产生的,所以不需要我们来写端口号了
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books");
//通过MockMvc调用方法perform,来执行这个请求,然后得到真实的响应
ResultActions resultActions = mvc.perform(requestBuilder);
//设置预期的希望,希望状态码是一个200
StatusResultMatchers status = MockMvcResultMatchers.status();//通过MockMvcResultMatchers调用status方法,来获取状态的匹配器
//通过状态的匹配器,来调用对应的发给发,来设置预期的状态码
ResultMatcher ok = status.isOk();
//通过执行之后的结构resultActions,调用方法andExpect,从而将真实值和期望值进行匹配
//如果匹配,就直接打印响应的结果,否则,就会发生报错
resultActions.andExpect(ok);
//状态码是200的时候,获取响应的内容,这时候就需要通过MockResultMatchers对象
//调用方法content来获取ContentResultMatcher,然后调用方法string,从而获取
//string类型的响应值,如果希望获得的是json格式的响应体,那么调用方法json即可
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher resultMatcher = content.string("spring boot");//设置预期的响应体内容为spring boot
//通过resultAction对象调用方法andExpect,将真实值和期望值进行比对
resultActions.andExpect(resultMatcher);
}
//测试获取响应体为json
@Test
public void testJson() throws Exception {
//发送虚拟请求,通过MockMvcRequestBuilders调用对应的方法,例如get方法,表示发送get请求
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/getJson");
//通过MockMvc对象调用perform方法,来执行请求,然后返回执行的结果
ResultActions resultActions = mvc.perform(builder);
/*
获取期望值,因为需要获取json的响应体,所以先通过MockMvcResultMatchers
调用方法content来获取content的匹配器,然后调用方法json,从而设置json格式的
期望值
*/
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher matcher = content.json("{\"id\":null,\"name\":\"springboot学习\",\"description\":\"springboot学习\"}");
//通过resultActions调用方法andExpect,从而真实值和期望值进行匹配,如果不能匹配,则发生报错
resultActions.andExpect(matcher);
}
//测试响应头
@Test
public void testHeader() throws Exception {
//发送虚拟请求,通过MockMvcRequestBuilder调用对应的方法,发送相应的请求
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/getJson");
//MockMvc调用perform方法来执行对应的请求
ResultActions resultActions = mvc.perform(builder);
//通过MockMvcResultMatchers调用header方法,从而获取响应头的匹配器
HeaderResultMatchers header = MockMvcResultMatchers.header();
//header对象调用方法,来设置对应的响应头的期望值
ResultMatcher matcher = header.string("Content-Type", "application/json");
//通过resultActions,调用方法andExpect,从而将期望值和真实值进行匹配
resultActions.andExpect(matcher);
}
/*
如果再controller中测试的请求中,涉及到数据库表的操作,那么这时候我们的测试数据
并不应该影响到数据库表,所以需要保证测试支持事务,因此需要利用注解@Transactional
从而使得测试支持事务,但是如果再利用注解@Rollback(false),那么就不支持事务回滚
所以测试已经会影响到数据库表,但是如果注解@Rollback的值为true,即@Rollback(true)
那么就会支持事务回滚,所以默认情况是true的,所以只需要使用注解@Transactional,
就可以支持事务回滚了
*/
@Test
public void testTransaction() throws Exception {
//通过MockMvcRequestBuilder,来调用对应的方法发送对应的请求
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/save");
//MockMvc调用perform方法执行请求,获取执行之后的结果
ResultActions actions = mvc.perform(builder);
//通过MockMvcResultMatchers,调用方法status,来获取状态的匹配器
StatusResultMatchers status = MockMvcResultMatchers.status();
//设置预期的状态码为200
ResultMatcher ok = status.isOk();
//通过actions调用andExpect方法,来进行真实值和期望值的匹配
actions.andExpect(ok);
}
@Autowired
private Book book;
@Test //测试随机生成测试数据
public void testRandom(){
System.out.println(book);
}
}
数据库层解决方案技术选型
在之前的ssmp项目开发中,数据库层所采用的是Druid + Mybatis-Plus + MySQL,也即数据源使用的是Druid,数据持久化所采用的技术是Mybatis-Plus,数据库使用的是MySQL,此时对应的配置信息为:
spring:
datasource:
druid: # 配置数据源为druid
url: jdbc:mysql://localhost:3306/ssmp?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: lf_MySQL
# 配置mybatis-plus
mybatis-plus:
global-config:
db-config:
id-type: auto # 配置id的类型是支持自增的
table-prefix: tb_ # 配置数据库表的前缀
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 配置日志是标准输出的
这时候就这3点,来选择其他的技术,如下所示:
-
数据源:在spring中其实有配置了内置的数据源,例如Hikari数据源,这时候如果我们在没有导入依赖druid,然后配置文件配置数据库驱动的时候,是这样配置的:
# 如果是这样写的话,那么url一定是要写在hikari外面的,否则如果写在hikari的里面,并且变成了jdbc-url,因为 # hikari里面的属性是jdbc-url,那么这时候运行的时候就会发生报错,提示url无法绑定。 spring: datasource: url: jdbc:mysql://localhost:3306/ssmp?serverTimezone=Asia/Shanghai hikari: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: lf_MySQL
当然也可以是这样配置:
spring: datasource: url: jdbc:mysql://localhost:3306/ssmp?serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: lf_MySQL
这时候因为依旧采用的是Mysql数据库,所以对应的url,driver-class-name等属性依旧是MySQL对应的值,如果下面使用的是H2数据库的时候,那么这时候就需要修改这些属性的值。在配置完毕之后,就可以进行测试了:
实体类:
public class Book { private Integer id; private String name; private String description; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "Book{" + "id=" + id + ", name='" + name + '\'' + ", description='" + description + '\'' + '}'; } }
dao层:
@Mapper public interface BookDao extends BaseMapper<Book> { }
测试类:
@SpringBootTest public class BookTest { @Autowired private BookDao bookDao; @Test public void testGetAll2(){ List<Book> books = bookDao.selectList(null); for(Book book : books){ System.out.println(book); } } }
运行结果如下所示:
通过观察下面数据,可以发现使用的数据源是Hikari。
-
数据持久化:
如果我们没有使用Mybatis-Plus来实现数据持久化,那么我们可以使用JdbcTemplate来实现数据的持久化,但是要使用这个类,我们需要导入依赖spring-boot-starter-jdbc,而mybatis-plus-boot-starter这个依赖里面已经包括了这个依赖。然后通过JdbcTemplate调用update方法来实现数据库的增删改,调用query方法来实现数据的查询。但是如果需要返回多个数据的时候,调用的是query,而调用queryForObject,则是返回的是单个数据。
如果我们需要返回的是某一个实体类,那么这时候我们在调用方法query的时候,还需要传递参数RowMapper(这是一个接口,要返回什么类型的实体类,我们就要自己定义一个类,然后让这个类来实现RowMapper<T>接口),当然我们也可以传递参数BeanPropertyRowMapper<T>类,这样就不需要我们自定义类了。
@Autowired private JdbcTemplate jdbcTemplate; @Test public void testGetAll(){ String sql = "select * from tb_book"; List<Book> books = jdbcTemplate.query(sql, new BeanPropertyRowMapper<Book>(Book.class)); for(Book book : books){ System.out.println(book); } } @Test public void testGetById(){ String sql = "select * from tb_book where id = ?"; int id = 2; RowMapper<Book> mapper = new RowMapper<Book>() { @Override public Book mapRow(ResultSet resultSet, int i) throws SQLException { //将查询到的数据封装到book里面 Book book = new Book(); book.setId(resultSet.getInt("id")); book.setName(resultSet.getString("name")); book.setDescription(resultSet.getString("description")); return book; } }; Book book = jdbcTemplate.queryForObject(sql,mapper, id); System.out.println("id为" + id + "的书为: " + book); } @Test public void testGetCount(){ String sql = "select count(*) from tb_book"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println("共有 " + count + " 行"); } @Test public void testInsert(){ String sql = "insert into tb_book (name,description) values (?,?)"; Object[] args = {"spring学习2","spring学习2"}; jdbcTemplate.update(sql,args); } @Test public void testDelete(){ String sql = "delete from tb_book where id = ?"; int id = 26; jdbcTemplate.update(sql,id); } @Test public void testUpdate(){ String sql = "update tb_book set name = ?, description = ? where id = ?"; Object[] args = {"spring学习33","spring学习33",24}; jdbcTemplate.update(sql,args); }
-
数据库:
我们可以采用H2数据库来进行替换MySQL数据库,H2数据库主要可以配置为作为内存数据库运行,这意味着数据将不会持久存储在磁盘上。由于具有嵌入式数据库,因此它不用于生产开发,而主要用于开发和测试。
所以要使用H2数据库,首先我们要导入对应的依赖h2以及spring-boot-starter-jpa:
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
然后在配置文件中加载数据库驱动:
spring: h2: console: enabled: true path: /h2 datasource: url: jdbc:h2:~/test hikari: # 使用的是Hikari数据源 driver-class-name: org.h2.Driver username: sa # H2数据库的用户名和密码默认是sa,123456 password: 123456
当我们设置了spring.h2.console.enabled为true之后,然后设置path为/h2,那么当我们在搜索框输入
localhost:8080/h2
,就会跳转到下面的页面:
这时候的驱动类以及url,用户名刚好对应的是上面配置文件中所说的,然后输入密码连接之后,就进入下面的页面:
然后再TB_BOOK表中进行操作进行以下操作的时候:
这时候如果我们利用mybatis-plus来实现数据持久化,那么同样可以获取对应的结果,同样的,jdbcTemplate也可以实现。
spring boot整合mongodb
先安装好Mongo,对应的步骤可以参考这个文章:MongoDB5.0安装总结(简单)
其中需要注意的是,要查看对应的数据库的时候,首先我们需要先开启mongo服务,然后来到mongo中的bin的路径下面,然后就可以执行mongo命令,之后就可以看到了mongo的版本信息了。
然后再次执行show databases
的时候,就可以看到对应的数据库了。
如果我们需要使用的mongodb数据库(非关系型数据库),那么这时候基本步骤为:
-
导入依赖spring-boot-starter-data-mongodb
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>
-
在配置文件中配置mongodb,表示要测试的是哪一个数据库文档,默认的值为:
http://mongodb/test
,其中test就是对应的collection名# 配置mongodb,使得明确要使用哪个数据库 spring: data: mongodb: uri: mongodb://localhost/itheima # 表示要使用的是哪一个数据库
-
然后利用MongoTemplate来进行各种操作
@SpringBootTest public class MongodbTest { @Autowired private MongoTemplate mongoTemplate; @Test //执行添加操作 public void testSave(){ Book book = new Book(); book.setId(2); book.setName("spring学习333"); book.setDescription("spring学习333"); mongoTemplate.save(book); } @Test //执行查询操作 public void testGetAll(){ List<Book> books = mongoTemplate.findAll(Book.class); System.out.println(books); } }
mongo中执行update操作时,发生报错,解决方案可以看这个:the update operation document must contain atomic operators : DBCollection.prototype.updateMany@src/mongo/shell/crud_api.js:655:19
spring book整合ES
所谓的ES,就是elasticsearch,即分布式全文搜素引擎,同样应用于在淘宝时搜素某一个商品的时候,我们需要输入关键词,就会出现多件和这个关键词相关的商品。
下载ES以及ik分词器的时候,需要注意的是ES的版本要和ik分词器的版本要一致,否则就会因为版本不一致,在cmd开启es的时候,就会出现闪退的现象,同时也需要注意ES的版本和jdk的版本,如果jdk不能满足,也是会出现闪退现象的。
当下载好之后,我们就可以整合es了:
- 导入依赖spring-boot-starter-elasticsearch
- 在配置文件中配置它的uris的值为
http://localhost:9200
,注意端口号为9200 - 然后利用spring中取出的ElasticsearchRestTemplate进行操作了
但是上面的是低级的ES客户端,而我们通常使用的是RestHighLeverClient来进行ES的操作,但是Spring默认的是低级的,所以我们需要使用高级的ES客户端的时候,对应的步骤为:
-
导入依赖
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
-
因为spring中并没有配置这个高级的ES客户端,所以不需要我们在配置文件中进行设置uris,而是需要我们在测试类中创建RestHighLeverClient的时候传递的。所以就可以进行测试了。
public class Book { private Integer id; private String name; private String description; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "Book{" + "id=" + id + ", name='" + name + '\'' + ", description='" + description + '\'' + '}'; } }
@Mapper public interface BookMapper extends BaseMapper<Book> { }
配置文件:
spring: datasource: druid: # 配置数据库库驱动 password: root username: root url: jdbc:mysql://localhost:3306/ssmp?serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver # 配置mybatis-plus mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: table-prefix: tb_ id-type: auto
测试类(值得注意的是,因为需要使用到了JSONObject的方法,所以需要导入依赖fastjson,这样就可以将实体类转成json字符串,也可以将json字符串转成实体类):
@SpringBootTest public class BookTest { @Autowired private BookMapper bookMapper; //这个时利用mybatis-plus来进行数据库的查询的 @Test public void testGetById(){ Book book = bookMapper.selectById(9); System.out.println(book); } /* 使用低级的客户端从而实现es的操作,spring默认使用ElasticsearchRestTemplate 所以在我们导入spring-boot-starter-elasticsearch依赖之后,然后 在配置文件中配置uris时,这时候就可以通过ElasticsearchRestTemplate 来进行各种操作了. 但是一般使用的是高级的es客户端,这时候由于spring中没有配置这个高级的客户端,所以 需要我们导入它的依赖,并且因为没有在spring中配置,所以在导入依赖之后,就不 可以在配置文件中配置uris的值了,而是我们手动进行配置。 */ private RestHighLevelClient client; @Test //创建client public void testClient() throws IOException { //相当于使用低级的客户端的时候,在配置文件中配置uris的值 RestClientBuilder builder = RestClient.builder(new HttpHost("localhost",9200,"http")); client = new RestHighLevelClient(builder); ///操作完毕之后,需要将client关闭 client.close(); } @Test //创建索引 public void testCreateIndex() throws IOException { /* 创建RestHighLeverClient,在创建builder对象的时候,不可以是下面的代码,否则 就会发生报错,提示java.lang.IllegalArgumentException: Illegal base64 character 3a String host = "http://localhost:9200" RestClientBuilder builder = RestClient.builder(host); */ RestClientBuilder builder = RestClient.builder(new HttpHost("localhost",9200,"http")); client = new RestHighLevelClient(builder); //client调用方法indices CreateIndexRequest request = new CreateIndexRequest("books");//创建books索引 String json = "{\n" + " \"mappings\":{\n" + " \"_doc\":{\n" + //注意记得添加这个,否则就会发生报错 " \"properties\":{\n" + " \"id\":{\n" + " \"type\":\"keyword\"\n" + " },\n" + " \"name\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\",\n" + " \"copy_to\":\"all\"\n" + " },\n" + " \"description\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\",\n" + " \"copy_to\":\"all\"\n" + " },\n" + " \"all\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\"\n" + " }\n" + " }\n" + " }\n" + "}}"; request.source(json, XContentType.JSON);//设置mappings的值为json格式的字符串 client.indices().create(request,RequestOptions.DEFAULT);//创建索引 //释放资源 client.close(); } @Test //添加文档数据 public void testSave() throws IOException { RestClientBuilder builder = RestClient.builder(new HttpHost("localhost",9200,"http")); client = new RestHighLevelClient(builder); Book book = bookMapper.selectById(1); //利用client来继续进行操作数据 //创建请求,其中的参数books表示要操作的是索引是books,然后1表示id为1 IndexRequest request = new IndexRequest("books").id(book.getId().toString()); String json = JSONObject.toJSONString(book); request.source(json,XContentType.JSON);//调用source方法,表示设置请求体的内容 client.index(request,RequestOptions.DEFAULT); client.close(); } /* 如果需要添加多条文档,那么这时候我们需要先获取List<Book> 然后不断遍历这个列表,每获取到一个元素,那么这时候我们就执行上面的操作: IndexRequest request = new IndexRequest("books").id(book.getId().toString()); String json = JsonObject.toJsonString(book);//其中这个JsonObject需要我们导入依赖fastjson request.source(json,XContent.JSON); client.index(request,RequestOptions.DEFAULT); 但是client中用于方法bulk,用于批处理,这时候只需要执行client.bulk(BulkRequest,RequestOptions.DEFAULT) 那么这时候就可以进行批处理了,但是上面的request需要设置请求体的数据,所以依旧是需要通过遍历 List<Book>,然后将通过request调用source来设置请求体的内容. 当设置好一个request之后,需要将这个request添加到BulkRequest中 */ @Test public void testBulkSave() throws IOException { RestClientBuilder builder = RestClient.builder(new HttpHost("localhost",9200,"http")); client = new RestHighLevelClient(builder); List<Book> books = bookMapper.selectList(null); BulkRequest bulkRequest = new BulkRequest(); String json; for(Book book: books){ IndexRequest req = new IndexRequest("books").id(book.getId().toString()); json = JSONObject.toJSONString(book); req.source(json,XContentType.JSON); bulkRequest.add(req); } client.bulk(bulkRequest,RequestOptions.DEFAULT); client.close(); } @Test //es测试获取id的文档 public void testGetById2() throws IOException { RestClientBuilder builder = RestClient.builder(new HttpHost("localhost",9200,"http")); client = new RestHighLevelClient(builder); //调用index方法,表示需要操作的是索引中的文档 GetRequest request = new GetRequest("books").id("17"); GetResponse response = client.get(request, RequestOptions.DEFAULT);//因为是要查询某一个id的文档,所以调用的是get方法 String json = response.getSourceAsString(); //将json格式的字符串变成实体类 Book book = JSONObject.parseObject(json, Book.class); System.out.println(book); client.close(); } //测试es中的search操作,从而获取所有的文档 @Test public void testSearch() throws IOException { RestClientBuilder builder = RestClient.builder(new HttpHost("localhost",9200,"http")); client = new RestHighLevelClient(builder); //调用search方法,从而获取所有的文档数据 SearchRequest request = new SearchRequest("books"); SearchSourceBuilder searchBuilder = new SearchSourceBuilder(); //查询name中含有value的所有book searchBuilder.query(QueryBuilders.termQuery("name","斗")); request.source(searchBuilder); SearchResponse response = client.search(request, RequestOptions.DEFAULT); SearchHits hits = response.getHits();//获取hits属性的值 for(SearchHit hit : hits){ Book book = JSONObject.parseObject(hit.getSourceAsString(),Book.class); System.out.println(book); } client.close(); } }
值得一提的是,在创建索引的时候,我们需要设置属性mappings(如上面的testCreateIndex)所示,这时候我们需要传递的是一个json格式的字符串,但是,如果json的值如果是下面的样子:
String json = "{\n" + " \"mappings\":{\n" + " \"properties\":{\n" + " \"id\":{\n" + " \"type\":\"keyword\"\n" + " },\n" + " \"name\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\",\n" + " \"copy_to\":\"all\"\n" + " },\n" + " \"description\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\",\n" + " \"copy_to\":\"all\"\n" + " },\n" + " \"all\":{\n" + " \"type\":\"text\",\n" + " \"analyzer\":\"ik_max_word\"\n" + " }\n" + " }\n" + " }\n" + "}";
那么测试的时候,就会发生报错,如下所示:
重点请看:
"reason":"Root mapping definition has unsupported parameters:
,以及Using include_type_name in create index requests is deprecated. The parameter will be removed in the next major version.]
.这是因为在elasticsearch 7.0以上不在支持索引为include_type_name,而是默认使用的是_doc
,所以解决方案就需要在mappings属性下面添加_doc
属性,正如上面testCreateIndex所写的.可以参考文章:ElasticSearch V7.10.2: Root mapping definition has unsupported parameters
缓存以及各种方案
缓存:是一个临时的存储器,这样当进行数据查询的时候,首先会从缓存中查询,如果找不到,再到数据库中查找,这样的话就可以减少数据库的查询次数,从而减少数据库的压力。
而在spring boot中的缓存方案主要有以下几种方式:
-
simple: 这是spring提供的默认的缓存方式,对应的步骤是
-
导入坐标spring-boot-starter-cache
-
因为这个是spring boot提供的默认的缓存方案,所以不需要再配置文件中设置缓存的类型了
-
在启动类中使用注解
@EnableCaching
,从而开启缓存 -
在对应的方法上方使用注解
@Cacheable
,如果在查询的时候,如果没有在缓存中,那么就会将执行对应的操作,然后将对应的返回值放到缓存中,否则,如果已经在缓存中了,那么就不会执行方法里面的操作,而是直接从缓存中取出数据返回。而如果使用注解@CachePut
的话,那么它是进行写操作的,并不会从缓存中取出数据,即如果没有在缓存中,他就会将数据存放到缓存中,如果已经在缓存中了,那么它会将原来缓存中的数据删除,然后再重新放到缓存中(也即类似于更新的操作,但是如果没有在缓存中的时候,却又会将数据添加到缓存中)对应的代码如下所示(验证码的生成以及验证:)
package com.example.domain; public class VerifyCode { private String tele; private String code; public VerifyCode() { } public VerifyCode(String tele, String code) { this.tele = tele; this.code = code; } public String getTele() { return tele; } public void setTele(String tele) { this.tele = tele; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } @Override public String toString() { return "VerifyCode{" + "tele='" + tele + '\'' + ", code='" + code + '\'' + '}'; } }
public interface VerifyCodeService { public String sendCode(String tele);//发送验证码 public boolean check(VerifyCode verifyCode);//校验验证码 }
@Service public class VerifyCodeServiceImpl implements VerifyCodeService { @Autowired private VerifyCodeUtils verifyCodeUtils; @Override //使用注解CachePut,这样就可以不断将数据放到缓存中了 //不是使用注解@Cacheable,否则,第一次生成的验证码放到缓存中,下一次直接 //从缓存中获取到验证码了,但是应该是每次发送的验证码都是不一样的 @CachePut(value="codeSpace",key="#tele") public String sendCode(String tele) { return verifyCodeUtils.getCode(tele); } @Override public boolean check(VerifyCode verifyCode) { String tele = verifyCode.getTele(); String code = verifyCode.getCode(); /* 获取真正的验证码(从缓存中取出),不可以在自己这个类中定义这个方法来从缓存取出 因为自己调用自己,那么这时候,不管在这个方法上方是否使用注解@Cacheable,那么 返回值都是null */ //Stirng cacheCode = getCacheCode(tele); String cacheCode = verifyCodeUtils.getCacheCode(tele); System.out.println(code + ", cacheCode = " + cacheCode); return code.equals(cacheCode); } /* 如果这个key在缓存中,那么直接从缓存中取出数据返回,而不会在执行方法里面的代码,否则,如果 key不在缓存中,就会执行方法里面的代码返回null @Cacheable(value="codeSpace", key="#tele") public String getCacheCode(String tele){ return null; } */ }
@Component public class VerifyCodeUtils { private String[] prefix = {"","0","00","000","0000","00000"}; //随机生成长度为6的验证码 public String getCode(String tele){ System.out.println("getCode is running........."); int hash = tele.hashCode(); long a = 202236951; long code = hash ^ a; code = code ^ System.currentTimeMillis(); //获取code的后面六位,所以需要取余1000000,但是可能最后的6位000123 //的形式,这时候,需要添加前缀0,也可能是负数 code = code % 1000000; code = code < 0 ? -code : code; String codeString = code + ""; int length = codeString.length(); return prefix[6 - length] + codeString; } @Cacheable(value="codeSpace",key="#tele") public String getCacheCode(String tele){ //因为是缓存中存在这个key,所以会从缓存中取出值来返回,如果没有,直接返回null return null; } }
@RestController @RequestMapping("/code") public class VerifyCondController { @Autowired private VerifyCodeService verifyCodeService; @GetMapping("sendCode/{tele}") public String sendCode(@PathVariable("tele")String tele){ return verifyCodeService.sendCode(tele); } @PostMapping public boolean check(VerifyCode verifyCode){ return verifyCodeService.check(verifyCode); } }
-
-
ehcache:如果需要使用ehcache缓存方案的时候,那么这时候对应的步骤为:
-
导入坐标spring-boot-starter-cache,以及ehcache
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!--使用ehcache缓存方案--> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> </dependency>
-
在配置文件中设置缓存的类型为ehcache,即
spring.cache.type=ehcache
-
在启动类中利用注解
@EnableCaching
开启缓存 -
在对应的方法上方使用注解
@Cacheable
,如果在查询的时候,如果没有在缓存中,那么就会将执行对应的操作,然后将对应的返回值放到缓存中,否则,如果已经在缓存中了,那么就不会执行方法里面的操作,而是直接从缓存中取出数据返回。而如果使用注解@CachePut
的话,那么它是进行写操作的,并不会从缓存中取出数据,即如果没有在缓存中,他就会将数据存放到缓存中,如果已经在缓存中了,那么它会将原来缓存中的数据删除,然后再重新放到缓存中(也即类似于更新的操作,但是如果没有在缓存中的时候,却又会将数据添加到缓存中)但是这时候经过上面的步骤之后,如果要启动的时候,就会发生如下的错误(如果已经设置了配置文件以及导入了坐标ehcache,但是没有导入坐标spring-boot-starter-cache,同样会发生下面的错误),因为我们需要在启动类中使用了注解
@EnableCaching
开启缓存,这个注解就是这个包下的.
此时我们需要在resouces目录下面创建ehcache的配置文件ehcache.xml,对应的内容如下所示:
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <!-- diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下: user.home – 用户主目录 user.dir – 用户当前工作目录 java.io.tmpdir – 默认临时文件路径 --> <diskStore path="java.io.tmpdir"/> <!-- defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。 --> <!-- name:缓存名称。 maxElementsInMemory:缓存最大数目 maxElementsOnDisk:硬盘最大缓存个数。 eternal:对象是否永久有效,一但设置了,timeout将不起作用。 overflowToDisk:是否保存到磁盘,当系统当机时 timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。 timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false. diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。 diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。 clearOnFlush:内存数量最大时是否清除。 memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。 FIFO,first in first out,这个是大家最熟的,先进先出。 LFU, Less Frequently Used,使用次数最少的被删除 LRU,Least Recently Used,最久未被使用的被删除 --> <defaultCache eternal="false" maxElementsInMemory="10000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="259200" memoryStoreEvictionPolicy="LRU"/> <cache name="codeSpace" eternal="false" maxElementsInMemory="5000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="1800" memoryStoreEvictionPolicy="LRU"/> </ehcache>
-
-
redis:如果我们需要使用的是redis缓存方案,那么对应的步骤和上面使用ehcache的步骤类似,只是不需要再创建对应的配置文件了,步骤如下:
-
导入坐标:spring-boot-starter-data-redis(这时候spring-boot-stater-cache可导可不导)
-
在配置文件中设置缓存的类型为redis,以及
spring.cache.type=redis
-
设置redis的相关信息,例如端口号,主机等
spring: cache: type: redis redis: cache-null-values: false # 是否允许null值 # 判断是否使用key的前缀,如果不使用,那么为false,那么key就可能会 # 出现覆盖的情况,在不同的场景下可能会有同一个key,此时如果没有前缀 # 这个key的值就会被新的值覆盖了,所以默认情况下为true.这时候 # 对应的key的名字就是 prefix + "cache的名字::" + key,其中的prefix就是 # 下面的key-prefix的值,所以此时的key的名字就变成了tele_ + cache的名字 + key的值 use-key-prefix: true key-prefix: tele_ time-to-live: 10s # 表示这个key在缓存中的存活时间,一旦过期,那么缓存中就不会有这个key了
-
在启动类中使用注解
@EnableCaching
来开启缓存 -
在对应的方法上方使用注解
@Cacheable
,如果在查询的时候,如果没有在缓存中,那么就会将执行对应的操作,然后将对应的返回值放到缓存中,否则,如果已经在缓存中了,那么就不会执行方法里面的操作,而是直接从缓存中取出数据返回。而如果使用注解@CachePut
的话,那么它是进行写操作的,并不会从缓存中取出数据,即如果没有在缓存中,他就会将数据存放到缓存中,如果已经在缓存中了,那么它会将原来缓存中的数据删除,然后再重新放到缓存中(也即类似于更新的操作,但是如果没有在缓存中的时候,却又会将数据添加到缓存中)
-
-
memcache:因为spring中没有支持memcached,所以如果需要使用这种缓存的时候,对应的步骤为:
-
导入坐标memcache
-
因为在spring中没有支持memcached,所以也就不可以使用注解
@Cacheable
了,同时也不可以使用注解@EnableCaching
来开启缓存了。所以不需要在配置文件中进行配置 -
但是因为我们需要使用memcached的客户端,所以需要将它的客户端添加到spring容器中,通过它的客户端调用set/get方法,从而将key存放到缓存中,并且将key从缓存中取出对应的数据.
@Configuration public class XMemCachedConfig { @Bean public MemcachedClient getMemcachedClient() throws IOException { //设置端口以及主机 MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder("localhost:11211"); MemcachedClient client = memcachedClientBuilder.build(); return client; } }
@Service public class VerifyCodeServiceImpl implements VerifyCodeService { //使用XMemcachedClient来实现缓存 @Autowired private MemcachedClient client; @Override public String sendCode(String tele) { String code = verifyCodeUtils.getCode(tele); //将验证码添加到缓存中 try { client.set(tele, 0, code);//设置存活时间,如果值为0,说明无限存活 } catch (Exception e) { e.printStackTrace(); } return code; } @Override public boolean check(VerifyCode verifyCode) { String code = verifyCode.getCode(); String cacheCode = null; try { //client.get(xxx)返回的是Object对象,所以需要转成string类型 cacheCode = client.get(verifyCode.getTele()).toString(); } catch (Exception e) { e.printStackTrace(); } return code.equals(cacheCode); } }
-
当然在我们将memcached的客户端存放到spring中的时候,这时候我们可以在配置文件中设置memcached的相关信息,例如端口号,主机,或者最大连接个数等。这时候只需要在配置文件中设置需要的属性,然后可以通过自定义一个类,使得绑定配置文件中的这些属性,然后将这个自定义类添加到sprng容器中。所以对应的代码为:
配置文件中的代码为:
# 创建属性XMemcachedProperties XMemcachedProperties: servers: localhost:11211 opTimeout: 100
自定义类XMemcachedProperties,来设置XMemcachedClientBuilder的属性:
@Component @ConfigurationProperties(prefix = "xmemcachedproperties") public class XMemcachedProperties { private String servers; private Long opTimeout; public XMemcachedProperties() { } public XMemcachedProperties(String servers, Long opTimeout) { this.servers = servers; this.opTimeout = opTimeout; } public String getServers() { return servers; } public void setServers(String servers) { this.servers = servers; } public Long getOpTimeout() { return opTimeout; } public void setOpTimeout(Long opTimeout) { this.opTimeout = opTimeout; } @Override public String toString() { return "XMemcacheProperties{" + "servers='" + servers + '\'' + ", opTimeout=" + opTimeout + '}'; } }
修改之后的XMemcacheConfig代码
@Configuration public class XMemCachedConfig { @Autowired private XMemcachedProperties properties; @Bean public MemcachedClient getMemcachedClient() throws IOException { //设置端口以及主机 MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder(properties.getServers()); memcachedClientBuilder.setOpTimeout(properties.getOpTimeout()); MemcachedClient client = memcachedClientBuilder.build(); return client; } }
-
-
jetcache:它支持本地缓存以及远程缓存,同时也可以设置key的存活时间,对应的步骤为:
-
导入依赖jetcache-boot-starter
-
在配置文件中进行配置相关信息,例如它的本地缓存以及远程缓存,对应的代码如下所示:
# 配置jetcache jetcache: local: # 本地缓存 default: type: linkedhashmap keyConvertor: fastjson # key的类型转换器为fastjson,即key将会转成json格式的字符串 remote: # 配置远程缓存 default: # 默认地区的缓存 type: redis # 缓存的类型为redis host: localhost port: 6379 poolConfig: # 这个必须要配置,否则就会发生报错 maxTotal: 50 # 最大连接数为50 codeSpace: # 自定义地区的缓存 type: redis host: localhost port: 6379 poolConfig: maxTotal: 50
-
利用注解
@EnableCreateCacheAnnotation
,从而开启创建缓存注解 -
在测试类中利用注解
@CreateCache
,从而告诉spring这个是用于缓存的,其中这个注解的各个属性为:area : 表示选择哪一个地区的缓存,默认是default name: 表示缓存的名字 expire: 表示缓存的过期时间 timeUnit: 表示过期时间的单位,例如TimeUnit.SECONDS表示为秒 cacheType:表示缓存的类型,默认是值是CacheType.REMOTE,当然也可以是CacheType.LOCAL,或者CacheType.BOTH
然后利用
Cache<K,T>
来进行操作。再上面的验证码示例中,在我们修改了配置文件,启动类之后,对应的service实现类代码为:@Service public class VerifyCodeServiceImpl implements VerifyCodeService { @Autowired private VerifyCodeUtils verifyCodeUtils; //利用注解@CreateCache,告诉spring,这个是用作缓存的,过期时间为3600s //area表示选择的是哪一个地区的缓存,默认选择的是default地区的缓存, //cacheType表示缓存的类型是本地还是远程的,还是2者都可,默认是远程的 @CreateCache(name = "jetcache_",expire = 3600,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.LOCAL) private Cache<String,String> cache; @Override public String sendCode(String tele) { String code = verifyCodeUtils.getCode(tele); System.out.println(code); cache.put(tele,code); return code; } @Override public boolean check(VerifyCode verifyCode) { return verifyCode.getCode().equals(cache.get(verifyCode.getTele())); } }
但是如果我们需要将从数据库查询到的实体类添加到缓存,也即是说能不能像其他的缓存那样,利用一个注解就可以将方法的返回值添加到缓存呢?答案是肯定的,这时候对应的步骤为:
-
在启动类上面还要添加注解
@EnableMethodCache
,表示能够允许方法缓存,其中要设置它的属性值basePackages的值是要添加到缓存中的类所在的包 -
利用注解
@Cached
,从而将方法的返回值添加到缓存中,其中的属性是和上面的@CreateCache
的属性是一样的注解
@CacheUpdate(name="缓存的名字",key="key的值",value="新值")
,从而可以进行更新缓存。注解
@CacheInvalidate(name="缓存的名字",key="key的值")
从而删除对应key的缓存。 -
但是需要注意的是,如果方法的返回值是一个实体类,并且使用的是远程缓存,那么这时候就会发生如下错误:
-
这是因为远程缓存中没有设置属性`keyConvertor`为fastjson,因为在redis中并没有办法存放Object类型的key,所以我们需要将其转成redis能够识别的类型,所以就将其转成json格式的字符串,因此对应的配置文件中远程缓存中还需要添加`keyConvertor: fastjson`.
然后再次运行的时候,发现依旧会发生报错:
表示实体类并不支持序列化,所以我们需要将对应的实体类支持序列化,并且在配置文件中添加属性valueEncode: java
以及valueDecode: java
.以图书管理为例,对应的代码为: 因为需要连接数据库,并且通过mybatis-plus来进行数据库所以还需要导入mybatis-plust-boot-starter,druid依赖,以及mysql-connector-java依赖,然后再配置文件中配置mybatis-plus以及数据库驱动,对应的配置文件代码:
```yml
# 配置mybatis-plus
mybatis-plus:
global-config:
db-config:
id-type: auto # id支持自增
table-prefix: tb_ # 表名的前缀
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志为标准输出
# 数据库驱动:
spring:
datasource:
druid: # 数据源名称
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: lf_MySQL
url: jdbc:mysql://localhost:3306/ssmp?serverTimezone=Asia/Shanghai # 数据库连接地址
# 配置jetcache
jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote: # 配置远程缓存
default:
type: redis
host: localhost
port: 6379
keyConvertor: fastjson
valueEncode: java
valueDecode: java
poolConfig:
maxTotal: 50
codeSpace:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
```
Book实体类:
```java
public class Book implements Serializable {
private Integer id;
private String name;
private String description;
public Book() {
}
public Book(Integer id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
```
```java
@Mapper
public interface BookDao {
@Select("select * from tb_book where id = #{id}")
Book getById(Integer id);
@Select("select * from tb_book")
List<Book> getAll();
@Update("update tb_book set name = #{name}, description = #{description} where id = #{id}")
void updateById(Book book);
@Insert("insert into tb_book (name, description) values (#{name},#{description})")
void save(Book book);
@Delete("delete from tb_book where id = #{id}")
void deleteById(Integer id);
}
```
```java
public interface BookService {
Book getById(Integer id);
List<Book> getAll();
void updateById(Book book);
void save(Book book);
void deleteById(Integer id);
}
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
/*private HashMap<Integer,Book> hashMap = new HashMap<>();
@Override
public Book getById(Integer id) {
Book book = hashMap.get(id);
if(book == null){
book = bookDao.getById(id);
hashMap.put(id,book);
}
return book;
}*/
@Override
@Cached(area="default",name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
public Book getById(Integer id) {
return bookDao.getById(id);
}
@Override
public List<Book> getAll() {
return bookDao.getAll();
}
@Override
@CacheUpdate(name="book_",key="#book.id",value="#book")
public void updateById(Book book) {
bookDao.updateById(book);
}
@Override
public void save(Book book) {
bookDao.save(book);
}
@Override
@CacheInvalidate(name="book_",key="#id")
public void deleteById(Integer id) {
bookDao.deleteById(id);
}
}
@RestController
@RequestMapping("/book")
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("getById/{id}")
public Book getById(@PathVariable("id")Integer id){
Book book = bookService.getById(id);
System.out.println(book);
return book;
}
@GetMapping("getAll")
public List<Book> getAll(){
return bookService.getAll();
}
@PostMapping("/updateBook")
public void updateBook(@RequestBody Book book){
System.out.println(book);
bookService.updateById(book);
}
@DeleteMapping("/deleteById/{id}")
public void deleteById(@PathVariable("id")int id){
bookService.deleteById(id);
}
}
-
j2cache:j2cache是一个整合缓存的框架,因为上面的jetcache只能整合特定的缓存方案,但是在j2cache中可以整合其他任意的缓存方案。所以这里将利用j2cache来整合redis和ehcache.对应的步骤为:
-
导入依赖j2cache-core以及j2cache-spring-boot2-starter,因为j2cache-spring-boot2-starter中含有了redis的依赖,所以不需要导入spring-boot-starter-data-redis依赖了,但是因为要整合redis和ehcache,所以还需要导入依赖ehcache
-
在配置文件中配置j2cache的配置文件,即
j2cache.config-location=j2cache.properties
-
因为我们要利用到了ehcache缓存方案,所以还需要创建ehcache的配置文件ehcache.xml,然后设置响应的信息
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <!-- diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下: user.home – 用户主目录 user.dir – 用户当前工作目录 java.io.tmpdir – 默认临时文件路径 --> <diskStore path="java.io.tmpdir"/> <!-- defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。 --> <!-- name:缓存名称。 maxElementsInMemory:缓存最大数目 maxElementsOnDisk:硬盘最大缓存个数。 eternal:对象是否永久有效,一但设置了,timeout将不起作用。 overflowToDisk:是否保存到磁盘,当系统当机时 timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。 timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false. diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。 diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。 clearOnFlush:内存数量最大时是否清除。 memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。 FIFO,first in first out,这个是大家最熟的,先进先出。 LFU, Less Frequently Used,使用次数最少的被删除 LRU,Least Recently Used,最久未被使用的被删除 --> <defaultCache eternal="false" maxElementsInMemory="10000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="259200" memoryStoreEvictionPolicy="LRU"/> <cache name="codeSpace" eternal="false" maxElementsInMemory="5000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="1800" memoryStoreEvictionPolicy="LRU"/> </ehcache>
-
在j2cache.properties中配置相应的信息,例如1级缓存,2级缓存等
# 配置j2cache的一级缓存的供应商为ehcache j2cache.L1.provider_class = ehcache # 配置ehcache的配置文件 ehcache.configXml = ehcache.xml # 配置是否开启2级缓存,如果为false,那么不会开启,否则会开启,默认是true j2cache.L2.open = true # 配置j2cach2的二级缓存的供应商为redis j2cache.L2.provider_class =net.oschina.j2cache.cache.support.redis.SpringRedisProvider j2cache.L2.config_section = redis # 配置redis的host以及port redis.hosts = localhost:6379 # 配置redis中的key的前缀 redis.namespace = j2cache # 1级缓存的数据发送给2级缓存 j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
-
利用注解
@Autowired
来从spring中取出CacheChannel对象,通过这个对象来调用set/get方法将数据添加到缓存中,并且从缓存中取出数据。对应的代码为:
@Service public class VerifyCodeServiceImpl implements VerifyCodeService { @Autowired private VerifyCodeUtils verifyCodeUtils; @Autowired private CacheChannel cacheChannel; @Override public String sendCode(String tele) { String code = verifyCodeUtils.getCode(tele); /* 存放验证码到缓存中,其中地区为region,然后key就是tele 如果没有在j2cache.properties配置文件中设置namespace的话,那么 缓存中key的名字就是 region的值:tele的值,也即code:tele的值。 但是如果设置了namespace的值,那么key的名字就是 namespace的值:region的值:tele的值 如上面设置了namespace的值为j2cache,所以这时候缓存中的key的值就是 j2cache:code:tele的值 */ cacheChannel.set("code",tele,code); return code; } @Override public boolean check(VerifyCode verifyCode) { //从code地区的缓存中取出key为tele的值 String cacheCode = cacheChannel.get("code", verifyCode.getTele()).asString(); return verifyCode.getCode().equals(cacheCode); } }
值得注意的是,一旦开始启动的时候,发生报错,提示:
-
这是因为j2cache.properties配置文件中出现了问题,导致没有办法注入CacheChannel,所以这时候的cacheChannel是null,从而发生了错误。所以**解决方案就是我们需要在j2cache.properties等配置文件中寻找错误,例如将`j2cache`写成了`j2cach2`**.
修改完毕之后,如果启动的时候,再次发生报错,提示: