搬运黑马视频配套笔记
回忆一下之前做SSMP整合的时候数据层解决方案涉及到了哪些技术?MySQL数据库与MyBatisPlus框架,后面又学了Druid数据源的配置,所以现在数据层解决方案可以说是Mysql+Druid+MyBatisPlus。而三个技术分别对应了数据层操作的三个层面:
数据源技术:Druid
持久化技术:MyBatisPlus
数据库技术:MySQL
下面的研究就分为三个层面进行研究,对应上面列出的三个方面,咱们就从第一个数据源技术开始说起。
目前我们使用的数据源技术是Druid,运行时可以在日志中看到对应的数据源初始化信息,具体如下:
INFO 28600 --- [ main] c.a.d.s.b.a.DruidDataSourceAutoConfigure : Init DruidDataSource
INFO 28600 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
如果不使用Druid数据源,程序运行后是什么样子呢?是独立的数据库连接对象还是有其他的连接池技术支持呢?将Druid技术对应的starter去掉再次运行程序可以在日志中找到如下初始化信息:
INFO 31820 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
INFO 31820 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
虽然没有DruidDataSource相关的信息了,但是我们发现日志中有HikariDataSource这个信息,就算不懂这是个什么技术,看名字也能看出来,以DataSource结尾的名称,这一定是一个数据源技术。我们又没有手工添加这个技术,这个技术哪里来的呢?这就是这一节要讲的知识,springboot内嵌数据源。
数据层技术是每一个企业级应用程序都会用到的,而其中必定会进行数据库连接的管理。springboot根据开发者的习惯出发,开发者提供了数据源技术,就用你提供的,开发者没有提供,那总不能手工管理一个一个的数据库连接对象啊,怎么办?我给你一个默认的就好了,这样省心又省事,大家都方便。
springboot提供了3款内嵌数据源技术,分别如下:
HikariCP
Tomcat提供DataSource
Commons DBCP
第一种,HikartCP,这是springboot官方推荐的数据源技术,作为默认内置数据源使用。
第二种,Tomcat提供的DataSource,如果不想用HikartCP,并且使用tomcat作为web服务器进行web程序的开发,使用这个。为什么是Tomcat,不是其他web服务器呢?因为web技术导入starter后,默认使用内嵌tomcat,既然都是默认使用的技术了,那就一用到底,数据源也用它的。有人就提出怎么才能不使用HikartCP用tomcat提供的默认数据源对象呢?把HikartCP技术的坐标排除掉就OK了。
第三种,DBCP,这个使用的条件就更苛刻了,既不使用HikartCP也不使用tomcat的DataSource时,默认给你用这个。
springboot这心操的,也是稀碎啊,就怕你自己管不好连接对象,给你一顿推荐,真是开发界的最强辅助。既然都给你奶上了,那就受用吧,怎么配置使用这些东西呢?之前我们配置druid时使用druid的starter对应的配置如下:
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
换成是默认的数据源HikariCP后,直接吧druid删掉就行了,如下:
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
当然,也可以写上是对hikari做的配置,但是url地址要单独配置,如下:
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
这就是配置hikari数据源的方式。如果想对hikari做进一步的配置,可以继续配置其独立的属性。例如:
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
maximum-pool-size: 50
如果不想使用hikari数据源,使用tomcat的数据源或者DBCP配置格式也是一样的。学习到这里,以后我们做数据层时,数据源对象的选择就不再是单一的使用druid数据源技术了,可以根据需要自行选择。
总结
说完数据源解决方案,再来说一下持久化解决方案。springboot充分发挥其最强辅助的特征,给开发者提供了一套现成的数据层技术,叫做JdbcTemplate。其实这个技术不能说是springboot提供的,因为不使用springboot技术,一样能使用它,谁提供的呢?spring技术提供的,所以在springboot技术范畴中,这个技术也是存在的,毕竟springboot技术是加速spring程序开发而创建的。
这个技术其实就是回归到jdbc最原始的编程形式来进行数据层的开发,下面直接上操作步骤:
步骤①:导入jdbc对应的坐标,记得是starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
步骤②:自动装配JdbcTemplate对象
@SpringBootTest
class Springboot15SqlApplicationTests {
@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){
}
}
步骤③:使用JdbcTemplate实现查询操作(非实体类封装数据的查询操作)
@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){
String sql = "select * from tbl_book";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
System.out.println(maps);
}
步骤④:使用JdbcTemplate实现查询操作(实体类封装数据的查询操作)
@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){
String sql = "select * from tbl_book";
RowMapper<Book> rm = new RowMapper<Book>() {
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
Book temp = new Book();
temp.setId(rs.getInt("id"));
temp.setName(rs.getString("name"));
temp.setType(rs.getString("type"));
temp.setDescription(rs.getString("description"));
return temp;
}
};
List<Book> list = jdbcTemplate.query(sql, rm);
System.out.println(list);
}
步骤⑤:使用JdbcTemplate实现增删改操作
@Test
void testJdbcTemplateSave(@Autowired JdbcTemplate jdbcTemplate){
String sql = "insert into tbl_book values(3,'springboot1','springboot2','springboot3')";
jdbcTemplate.update(sql);
}
如果想对JdbcTemplate对象进行相关配置,可以在yml文件中进行设定,具体如下:
spring:
jdbc:
template:
query-timeout: -1 # 查询超时时间
max-rows: 500 # 最大行数
fetch-size: -1 # 缓存行数
总结
截止到目前,springboot给开发者提供了内置的数据源解决方案和持久化解决方案,在数据层解决方案三件套中还剩下一个数据库,莫非springboot也提供有内置的解决方案?还真有,还不是一个,三个,这一节就来说说内置的数据库解决方案。
springboot提供了3款内置的数据库,分别是
H2
HSQL
Derby
以上三款数据库除了可以独立安装之外,还可以像是tomcat服务器一样,采用内嵌的形式运行在spirngboot容器中。内嵌在容器中运行,那必须是java对象啊,对,这三款数据库底层都是使用java语言开发的。
我们一直使用MySQL数据库就挺好的,为什么有需求用这个呢?原因就在于这三个数据库都可以采用内嵌容器的形式运行,在应用程序运行后,如果我们进行测试工作,此时测试的数据无需存储在磁盘上,但是又要测试使用,内嵌数据库就方便了,运行在内存中,该测试测试,该运行运行,等服务器关闭后,一切烟消云散,多好,省得你维护外部数据库了。这也是内嵌数据库的最大优点,方便进行功能测试。
下面以H2数据库为例讲解如何使用这些内嵌数据库,操作步骤也非常简单,简单才好用嘛
步骤①:导入H2数据库对应的坐标,一共2个
<dependency>
<groupId>com.h2databasegroupId>
<artifactId>h2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
步骤②:将工程设置为web工程,启动工程时启动H2数据库
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
步骤③:通过配置开启H2数据库控制台访问程序,也可以使用其他的数据库连接软件操作
spring:
h2:
console:
enabled: true
path: /h2
web端访问路径/h2,访问密码123456,如果访问失败,先配置下列数据源,启动程序运行后再次访问/h2路径就可以正常访问了
datasource:
url: jdbc:h2:~/test
hikari:
driver-class-name: org.h2.Driver
username: sa
password: 123456
步骤④:使用JdbcTemplate或MyBatisPlus技术操作数据库
(略)
其实我们只是换了一个数据库而已,其他的东西都不受影响。一个重要提醒,别忘了,上线时,把内存级数据库关闭,采用MySQL数据库作为数据持久化方案,关闭方式就是设置enabled属性为false即可。
总结
H2内嵌式数据库启动方式,添加坐标,添加配置
H2数据库线上运行时请务必关闭
到这里SQL相关的数据层解决方案就讲完了,现在的可选技术就丰富的多了。
数据源技术:Druid、Hikari、tomcat DataSource、DBCP
持久化技术:MyBatisPlus、MyBatis、JdbcTemplate
数据库技术:MySQL、H2、HSQL、Derby
现在开发程序时就可以在以上技术中任选一种组织成一套数据库解决方案了。
Redis是一款采用key-value数据存储格式的内存级NoSQL数据库,重点关注数据存储格式,是key-value格式,也就是键值对的存储形式。与MySQL数据库不同,MySQL数据库有表、有字段、有记录,Redis没有这些东西,就是一个名称对应一个值,并且数据以存储在内存中使用为主。什么叫以存储在内存中为主?其实Redis有它的数据持久化方案,分别是RDB和AOF,但是Redis自身并不是为了数据持久化而生的,主要是在内存中保存数据,加速数据访问的,所以说是一款内存级数据库。
Redis支持多种数据存储格式,比如可以直接存字符串,也可以存一个map集合,list集合,后面会涉及到一些不同格式的数据操作,这个需要先学习一下才能进行整合,所以在基本操作中会介绍一些相关操作。下面就先安装,再操作,最后说整合
启动服务器
redis-server.exe redis.windows.conf
初学者无需调整服务器对外服务端口,默认6379。
启动客户端
redis-cli.exe
如果启动redis服务器失败,可以先启动客户端,然后执行shutdown操作后退出,此时redis服务器就可以正常执行了。
服务器启动后,使用客户端就可以连接服务器,类似于启动完MySQL数据库,然后启动SQL命令行操作数据库。
放置一个字符串数据到redis中,先为数据定义一个名称,比如name,age等,然后使用命令set设置数据到redis服务器中即可
set name itheima
set age 12
从redis中取出已经放入的数据,根据名称取,就可以得到对应数据。如果没有对应数据就会得到(nil)
get name
get age
以上使用的数据存储是一个名称对应一个值,如果要维护的数据过多,可以使用别的数据存储结构。例如hash,它是一种一个名称下可以存储多个数据的存储模型,并且每个数据也可以有自己的二级存储名称。向hash结构中存储数据格式如下:
hset a a1 aa1 #对外key名称是a,在名称为a的存储模型中,a1这个key中保存了数据aa1
hset a a2 aa2
获取hash结构中的数据命令如下
hget a a1 #得到aa1
hget a a2 #得到aa2
有关redis的基础操作就普及到这里,需要全面掌握redis技术,请参看相关教程学习。
在进行整合之前先梳理一下整合的思想,springboot整合任何技术其实就是在springboot中使用对应技术的API。如果两个技术没有交集,就不存在整合的概念了。所谓整合其实就是使用springboot技术去管理其他技术,几个问题是躲不掉的。
第一,需要先导入对应技术的坐标,而整合之后,这些坐标都有了一些变化
第二,任何技术通常都会有一些相关的设置信息,整合之后,这些信息如何写,写在哪是一个问题
第三,没有整合之前操作如果是模式A的话,整合之后如果没有给开发者带来一些便捷操作,那整合将毫无意义,所以整合后操作肯定要简化一些,那对应的操作方式自然也有所不同
按照上面的三个问题去思考springboot整合所有技术是一种通用思想,在整合的过程中会逐步摸索出整合的套路,而且适用性非常强,经过若干种技术的整合后基本上可以总结出一套固定思维。
下面就开始springboot整合redis,操作步骤如下:
步骤①:导入springboot整合redis的starter坐标
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
上述坐标可以在创建模块的时候通过勾选的形式进行选择,归属NoSQL分类中
步骤②:进行基础配置
spring:
redis:
host: localhost
port: 6379
操作redis,最基本的信息就是操作哪一台redis服务器,所以服务器地址属于基础配置信息,不可缺少。但是即便你不配置,目前也是可以用的。因为以上两组信息都有默认配置,刚好就是上述配置值。
步骤③:使用springboot整合redis的专用客户端接口操作,此处使用的是RedisTemplate
@SpringBootTest
class Springboot16RedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void set() {
ValueOperations ops = redisTemplate.opsForValue();
ops.set("age",41);
}
@Test
void get() {
ValueOperations ops = redisTemplate.opsForValue();
Object age = ops.get("name");
System.out.println(age);
}
@Test
void hset() {
HashOperations ops = redisTemplate.opsForHash();
ops.put("info","b","bb");
}
@Test
void hget() {
HashOperations ops = redisTemplate.opsForHash();
Object val = ops.get("info", "b");
System.out.println(val);
}
}
在操作redis时,需要先确认操作何种数据,根据数据种类得到操作接口。例如使用opsForValue()获取string类型的数据操作接口,使用opsForHash()获取hash类型的数据操作接口,剩下的就是调用对应api操作了。
总结
StringRedisTemplate
由于redis内部不提供java对象的存储格式,因此当操作的数据以对象的形式存在时,会进行转码,转换成字符串格式后进行操作。为了方便开发者使用基于字符串为数据的操作,springboot整合redis时提供了专用的API接口StringRedisTemplate,你可以理解为这是RedisTemplate的一种指定数据泛型的操作API。
@SpringBootTest
public class StringRedisTemplateTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void get(){
ValueOperations ops = stringRedisTemplate.opsForValue();
String name = ops.get("name");
System.out.println(name);
}
}
redis客户端选择
springboot整合redis技术提供了多种客户端兼容模式,默认提供的是lettucs客户端技术,也可以根据需要切换成指定客户端技术,例如jedis客户端技术,切换成jedis客户端技术操作步骤如下:
步骤①:导入jedis坐标
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
jedis坐标受springboot管理,无需提供版本号
步骤②:配置客户端技术类型,设置为jedis
spring:
redis:
host: localhost
port: 6379
client-type: jedis
步骤③:根据需要设置对应的配置
spring:
redis:
host: localhost
port: 6379
client-type: jedis
lettuce:
pool:
max-active: 16
jedis:
pool:
max-active: 16
lettcus与jedis区别
总结
使用Redis技术可以有效的提高数据访问速度,但是由于Redis的数据格式单一性,无法操作结构化数据,当操作对象型的数据时,Redis就显得捉襟见肘。在保障访问速度的情况下,如果想操作结构化数据,看来Redis无法满足要求了,此时需要使用全新的数据存储结束来解决此问题,本节讲解springboot如何整合MongoDB技术。
MongoDB是一个开源、高性能、无模式的文档型数据库,它是NoSQL数据库产品中的一种,是最像关系型数据库的非关系型数据库。
上述描述中几个词,其中对于我们最陌生的词是无模式的。什么叫无模式呢?简单说就是作为一款数据库,没有固定的数据存储结构,第一条数据可能有A、B、C一共3个字段,第二条数据可能有D、E、F也是3个字段,第三条数据可能是A、C、E3个字段,也就是说数据的结构不固定,这就是无模式。有人会说这有什么用啊?灵活,随时变更,不受约束。基于上述特点,MongoDB的应用面也会产生一些变化。以下列出了一些可以使用MongoDB作为数据存储的场景,但是并不是必须使用MongoDB的场景:
存储位置:Mongodb
特征:临时存储,修改频度飞速
快速了解一下MongoDB,下面直接开始我们的学习,老规矩,先安装,再操作,最后说整合
启动服务器
mongod --dbpath=..\data\db
启动服务器时需要指定数据存储位置,通过参数--dbpath进行设置,可以根据需要自行设置数据存储路径。默认服务端口27017。
启动客户端
mongo --host=127.0.0.1 --port=27017
MongoDB虽然是一款数据库,但是它的操作并不是使用SQL语句进行的,因此操作方式各位小伙伴可能比较陌生,好在有一些类似于Navicat的数据库客户端软件,能够便捷的操作MongoDB,先安装一个客户端,再来操作MongoDB。
同类型的软件较多,本次安装的软件时Robo3t,Robot3t是一款绿色软件,无需安装,解压缩即可。解压缩完毕后进入安装目录双击robot3t.exe即可使用。
打开软件首先要连接MongoDB服务器,选择【File】菜单,选择【Connect...】
进入连接管理界面后,选择左上角的【Create】链接,创建新的连接设置
如果输入设置值即可连接(默认不修改即可连接本机27017端口)
连接成功后在命令输入区域输入命令即可操作MongoDB。
创建数据库:在左侧菜单中使用右键创建,输入数据库名称即可
创建集合:在Collections上使用右键创建,输入集合名称即可,集合等同于数据库中的表的作用
新增文档:(文档是一种类似json格式的数据,初学者可以先把数据理解为就是json数据)
db.集合名称.insert/save/insertOne(文档)
删除文档:
db.集合名称.remove(条件)
修改文档:
db.集合名称.update(条件,{操作种类:{文档}})
查询文档:
基础查询
查询全部: db.集合.find();
查第一条: db.集合.findOne()
查询指定数量文档: db.集合.find().limit(10) //查10条文档
跳过指定数量文档: db.集合.find().skip(20) //跳过20条文档
统计: db.集合.count()
排序: db.集合.sort({age:1}) //按age升序排序
投影: db.集合名称.find(条件,{name:1,age:1}) //仅保留name与age域
条件查询
基本格式: db.集合.find({条件})
模糊查询: db.集合.find({域名:/正则表达式/}) //等同SQL中的like,比like强大,可以执行正则所有规则
条件比较运算: db.集合.find({域名:{$gt:值}}) //等同SQL中的数值比较操作,例如:name>18
包含查询: db.集合.find({域名:{$in:[值1,值2]}}) //等同于SQL中的in
条件连接查询: db.集合.find({$and:[{条件1},{条件2}]}) //等同于SQL中的and、or
使用springboot整合MongDB该如何进行呢?其实springboot为什么使用的开发者这么多,就是因为他的套路几乎完全一样。导入坐标,做配置,使用API接口操作。整合Redis如此,整合MongoDB同样如此。
第一,先导入对应技术的整合starter坐标
第二,配置必要信息
第三,使用提供的API操作即可
下面就开始springboot整合MongoDB,操作步骤如下:
步骤①:导入springboot整合MongoDB的starter坐标
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-mongodbartifactId>
dependency>
上述坐标也可以在创建模块的时候通过勾选的形式进行选择,同样归属NoSQL分类中
步骤②:进行基础配置
spring:
data:
mongodb:
uri: mongodb://localhost/itheima
操作MongoDB需要的配置与操作redis一样,最基本的信息都是操作哪一台服务器,区别就是连接的服务器IP地址和端口不同,书写格式不同而已。
步骤③:使用springboot整合MongoDB的专用客户端接口MongoTemplate来进行操作
@SpringBootTest
class Springboot17MongodbApplicationTests {
@Autowired
private MongoTemplate mongoTemplate;
@Test
void contextLoads() {
Book book = new Book();
book.setId(2);
book.setName("springboot2");
book.setType("springboot2");
book.setDescription("springboot2");
mongoTemplate.save(book);
}
@Test
void find(){
List<Book> all = mongoTemplate.findAll(Book.class);
System.out.println(all);
}
}
总结
NoSQL解决方案已经讲完了两种技术的整合了,Redis可以使用内存加载数据并实现数据快速访问,MongoDB可以在内存中存储类似对象的数据并实现数据的快速访问,在企业级开发中对于速度的追求是永无止境的。下面要讲的内容也是一款NoSQL解决方案,只不过他的作用不是为了直接加速数据的读写,而是加速数据的查询的,叫做ES技术。
ES(Elasticsearch)是一个分布式全文搜索引擎,重点是全文搜索。
那什么是全文搜索呢?比如用户要买一本书,以Java为关键字进行搜索,不管是书名中还是书的介绍中,甚至是书的作者名字,只要包含java就作为查询结果返回给用户查看,上述过程就使用了全文搜索技术。搜索的条件不再是仅用于对某一个字段进行比对,而是在一条数据中使用搜索条件去比对更多的字段,只要能匹配上就列入查询结果,这就是全文搜索的目的。而ES技术就是一种可以实现上述效果的技术。
文件目录:
启动服务器
elasticsearch.bat
双击elasticsearch.bat文件即可启动ES服务器,默认服务端口9200。通过浏览器访问http://localhost:9200看到如下信息视为ES服务器正常启动
{
"name" : "CZBK-**********",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "j137DSswTPG8U4Yb-0T1Mg",
"version" : {
"number" : "7.16.2",
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "2b937c44140b6559905130a8650c64dbd0879cfb",
"build_date" : "2021-12-18T19:42:46.604893745Z",
"build_snapshot" : false,
"lucene_version" : "8.10.1",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
ES中保存有我们要查询的数据,只不过格式和数据库存储数据格式不同而已。在ES中我们要先创建倒排索引,这个索引的功能又点类似于数据库的表,然后将数据添加到倒排索引中,添加的数据称为文档。所以要进行ES的操作要先创建索引,再添加文档,这样才能进行后续的查询操作。
要操作ES可以通过Rest风格的请求来进行,也就是说发送一个请求就可以执行一个操作。比如新建索引,删除索引这些操作都可以使用发送请求的形式来进行。
创建索引,books是索引名称,下同
PUT请求 http://localhost:9200/books
发送请求后,看到如下信息即索引创建成功
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "books"
}
重复创建已经存在的索引会出现错误信息,reason属性中描述错误原因
{
"error": {
"root_cause": [
{
"type": "resource_already_exists_exception",
"reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists",
"index_uuid": "VgC_XMVAQmedaiBNSgO2-w",
"index": "books"
}
],
"type": "resource_already_exists_exception",
"reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists", # books索引已经存在
"index_uuid": "VgC_XMVAQmedaiBNSgO2-w",
"index": "book"
},
"status": 400
}
查询索引
GET请求 http://localhost:9200/books
查询索引得到索引相关信息,如下
{
"book": {
"aliases": {},
"mappings": {},
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"number_of_shards": "1",
"provided_name": "books",
"creation_date": "1645768584849",
"number_of_replicas": "1",
"uuid": "VgC_XMVAQmedaiBNSgO2-w",
"version": {
"created": "7160299"
}
}
}
}
}
如果查询了不存在的索引,会返回错误信息,例如查询名称为book的索引后信息如下
{
"error": {
"root_cause": [
{
"type": "index_not_found_exception",
"reason": "no such index [book]",
"resource.type": "index_or_alias",
"resource.id": "book",
"index_uuid": "_na_",
"index": "book"
}
],
"type": "index_not_found_exception",
"reason": "no such index [book]", # 没有book索引
"resource.type": "index_or_alias",
"resource.id": "book",
"index_uuid": "_na_",
"index": "book"
},
"status": 404
}
删除索引
DELETE请求 http://localhost:9200/books
删除所有后,给出删除结果
{
"acknowledged": true
}
如果重复删除,会给出错误信息,同样在reason属性中描述具体的错误原因
{
"error": {
"root_cause": [
{
"type": "index_not_found_exception",
"reason": "no such index [books]",
"resource.type": "index_or_alias",
"resource.id": "book",
"index_uuid": "_na_",
"index": "book"
}
],
"type": "index_not_found_exception",
"reason": "no such index [books]", # 没有books索引
"resource.type": "index_or_alias",
"resource.id": "book",
"index_uuid": "_na_",
"index": "book"
},
"status": 404
}
创建索引并指定分词器
前面创建的索引是未指定分词器的,可以在创建索引时添加请求参数,设置分词器。目前国内较为流行的分词器是IK分词器,使用前先在下对应的分词器,然后使用。
分词器下载后解压到ES安装目录的plugins目录中即可,安装分词器后需要重新启动ES服务器。使用IK分词器创建索引格式:
PUT请求 http://localhost:9200/books
请求参数如下(注意是json格式的参数)
{
"mappings":{ #定义mappings属性,替换创建索引时对应的mappings属性
"properties":{ #定义索引中包含的属性设置
"id":{ #设置索引中包含id属性
"type":"keyword" #当前属性可以被直接搜索
},
"name":{ #设置索引中包含name属性
"type":"text", #当前属性是文本信息,参与分词
"analyzer":"ik_max_word", #使用IK分词器进行分词
"copy_to":"all" #分词结果拷贝到all属性中
},
"type":{
"type":"keyword"
},
"description":{
"type":"text",
"analyzer":"ik_max_word",
"copy_to":"all"
},
"all":{ #定义属性,用来描述多个字段的分词结果集合,当前属性可以参与查询
"type":"text",
"analyzer":"ik_max_word"
}
}
}
}
创建完毕后返回结果和不使用分词器创建索引的结果是一样的,此时可以通过查看索引信息观察到添加的请求参数mappings已经进入到了索引属性中
{
"books": {
"aliases": {},
"mappings": { #mappings属性已经被替换
"properties": {
"all": {
"type": "text",
"analyzer": "ik_max_word"
},
"description": {
"type": "text",
"copy_to": [
"all"
],
"analyzer": "ik_max_word"
},
"id": {
"type": "keyword"
},
"name": {
"type": "text",
"copy_to": [
"all"
],
"analyzer": "ik_max_word"
},
"type": {
"type": "keyword"
}
}
},
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"number_of_shards": "1",
"provided_name": "books",
"creation_date": "1645769809521",
"number_of_replicas": "1",
"uuid": "DohYKvr_SZO4KRGmbZYmTQ",
"version": {
"created": "7160299"
}
}
}
}
}
目前我们已经有了索引了,但是索引中还没有数据,所以要先添加数据,ES中称数据为文档,下面进行文档操作。
添加文档,有三种方式
POST请求 http://localhost:9200/books/_doc #使用系统生成id
POST请求 http://localhost:9200/books/_create/1 #使用指定id
POST请求 http://localhost:9200/books/_doc/1 #使用指定id,不存在创建,存在更新(版本递增)
文档通过请求参数传递,数据格式json
{
"name":"springboot",
"type":"springboot",
"description":"springboot"
}
查询文档
GET请求 http://localhost:9200/books/_doc/1 #查询单个文档
GET请求 http://localhost:9200/books/_search #查询全部文档
条件查询
GET请求 http://localhost:9200/books/_search?q=name:springboot # q=查询属性名:查询属性值
删除文档
DELETE请求 http://localhost:9200/books/_doc/1
修改文档(全量更新)
PUT请求 http://localhost:9200/books/_doc/1
文档通过请求参数传递,数据格式json
{
"name":"springboot",
"type":"springboot",
"description":"springboot"
}
修改文档(部分更新)
POST请求 http://localhost:9200/books/_update/1
文档通过请求参数传递,数据格式json
{
"doc":{ #部分更新并不是对原始文档进行更新,而是对原始文档对象中的doc属性中的指定属性更新
"name":"springboot" #仅更新提供的属性值,未提供的属性值不参与更新操作
}
}
使用springboot整合ES该如何进行呢?老规矩,导入坐标,做配置,使用API接口操作。整合Redis如此,整合MongoDB如此,整合ES依然如此。太没有新意了,其实不是没有新意,这就是springboot的强大之处,所有东西都做成相同规则,对开发者来说非常友好。
下面就开始springboot整合ES,操作步骤如下:
步骤①:导入springboot整合ES的starter坐标
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
步骤②:进行基础配置
spring:
elasticsearch:
rest:
uris: http://localhost:9200
配置ES服务器地址,端口9200
步骤③:使用springboot整合ES的专用客户端接口ElasticsearchRestTemplate来进行操作
@SpringBootTest
class Springboot18EsApplicationTests {
@Autowired
private ElasticsearchRestTemplate template;
}
上述操作形式是ES早期的操作方式,使用的客户端被称为Low Level Client,这种客户端操作方式性能方面略显不足,于是ES开发了全新的客户端操作方式,称为High Level Client。高级别客户端与ES版本同步更新,但是springboot最初整合ES的时候使用的是低级别客户端,所以企业开发需要更换成高级别的客户端模式。
下面使用高级别客户端方式进行springboot整合ES,操作步骤如下:
步骤①:导入springboot整合ES高级别客户端的坐标,此种形式目前没有对应的starter
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
dependency>
步骤②:使用编程的形式设置连接的ES服务器,并获取客户端对象
@SpringBootTest
class Springboot18EsApplicationTests {
private RestHighLevelClient client;
@Test
void testCreateClient() throws IOException {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);
client.close();
}
}
配置ES服务器地址与端口9200,记得客户端使用完毕需要手工关闭。由于当前客户端是手工维护的,因此不能通过自动装配的形式加载对象。
步骤③:使用客户端对象操作ES,例如创建索引
@SpringBootTest
class Springboot18EsApplicationTests {
private RestHighLevelClient client;
@Test
void testCreateIndex() throws IOException {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);
CreateIndexRequest request = new CreateIndexRequest("books");
client.indices().create(request, RequestOptions.DEFAULT);
client.close();
}
}
高级别客户端操作是通过发送请求的方式完成所有操作的,ES针对各种不同的操作,设定了各式各样的请求对象,上例中创建索引的对象是CreateIndexRequest,其他操作也会有自己专用的Request对象。
当前操作我们发现,无论进行ES何种操作,第一步永远是获取RestHighLevelClient对象,最后一步永远是关闭该对象的连接。在测试中可以使用测试类的特性去帮助开发者一次性的完成上述操作,但是在业务书写时,还需要自行管理。将上述代码格式转换成使用测试类的初始化方法和销毁方法进行客户端对象的维护。
@SpringBootTest
class Springboot18EsApplicationTests {
@BeforeEach //在测试类中每个操作运行前运行的方法
void setUp() {
HttpHost host = HttpHost.create("http://localhost:9200");
RestClientBuilder builder = RestClient.builder(host);
client = new RestHighLevelClient(builder);
}
@AfterEach //在测试类中每个操作运行后运行的方法
void tearDown() throws IOException {
client.close();
}
private RestHighLevelClient client;
@Test
void testCreateIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("books");
client.indices().create(request, RequestOptions.DEFAULT);
}
}
现在的书写简化了很多,也更合理。下面使用上述模式将所有的ES操作执行一遍,测试结果
创建索引(IK分词器):
@Test
void testCreateIndexByIK() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("books");
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" +
" \"type\":{\n" +
" \"type\":\"keyword\"\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);
client.indices().create(request, RequestOptions.DEFAULT);
}
IK分词器是通过请求参数的形式进行设置的,设置请求参数使用request对象中的source方法进行设置,至于参数是什么,取决于你的操作种类。当请求中需要参数时,均可使用当前形式进行参数设置。
添加文档:
@Test
//添加文档
void testCreateDoc() throws IOException {
Book book = bookDao.selectById(1);
IndexRequest request = new IndexRequest("books").id(book.getId().toString());
String json = JSON.toJSONString(book);
request.source(json,XContentType.JSON);
client.index(request,RequestOptions.DEFAULT);
}
添加文档使用的请求对象是IndexRequest,与创建索引使用的请求对象不同。
批量添加文档:
@Test
//批量添加文档
void testCreateDocAll() throws IOException {
List<Book> bookList = bookDao.selectList(null);
BulkRequest bulk = new BulkRequest();
for (Book book : bookList) {
IndexRequest request = new IndexRequest("books").id(book.getId().toString());
String json = JSON.toJSONString(book);
request.source(json,XContentType.JSON);
bulk.add(request);
}
client.bulk(bulk,RequestOptions.DEFAULT);
}
批量做时,先创建一个BulkRequest的对象,可以将该对象理解为是一个保存request对象的容器,将所有的请求都初始化好后,添加到BulkRequest对象中,再使用BulkRequest对象的bulk方法,一次性执行完毕。
按id查询文档:
@Test
//按id查询
void testGet() throws IOException {
GetRequest request = new GetRequest("books","1");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
String json = response.getSourceAsString();
System.out.println(json);
}
根据id查询文档使用的请求对象是GetRequest。
按条件查询文档:
@Test
//按条件查询
void testSearch() throws IOException {
SearchRequest request = new SearchRequest("books");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termQuery("all","spring"));
request.source(builder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
String source = hit.getSourceAsString();
//System.out.println(source);
Book book = JSON.parseObject(source, Book.class);
System.out.println(book);
}
}
按条件查询文档使用的请求对象是SearchRequest,查询时调用SearchRequest对象的termQuery方法,需要给出查询属性名,此处支持使用合并字段,也就是前面定义索引属性时添加的all属性。
springboot整合ES的操作到这里就说完了,与前期进行springboot整合redis和mongodb的差别还是蛮大的,主要原始就是我们没有使用springboot整合ES的客户端对象。至于操作,由于ES操作种类过多,所以显得操作略微有点复杂。有关springboot整合ES就先学习到这里吧。
总结
springboot技术提供有内置的缓存解决方案,可以帮助开发者快速开启缓存技术,并使用缓存技术进行数据的快速操作,例如读取缓存数据和写入数据到缓存。
步骤①:导入springboot提供的缓存技术对应的starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
步骤②:启用缓存,在引导类上方标注注解@EnableCaching配置springboot程序中可以使用缓存
@SpringBootApplication
//开启缓存功能
@EnableCaching
public class Springboot19CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot19CacheApplication.class, args);
}
}
步骤③:设置操作的数据是否使用缓存
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Cacheable(value="cacheSpace",key="#id")
public Book getById(Integer id) {
return bookDao.selectById(id);
}
}
在业务方法上面使用注解@Cacheable声明当前方法的返回值放入缓存中,其中要指定缓存的存储位置,以及缓存中保存当前方法返回值对应的名称。上例中value属性描述缓存的存储位置,可以理解为是一个存储空间名,key属性描述了缓存中保存数据的名称,使用#id读取形参中的id值作为缓存名称。
使用@Cacheable注解后,执行当前操作,如果发现对应名称在缓存中没有数据,就正常读取数据,然后放入缓存;如果对应名称在缓存中有数据,就终止当前业务方法执行,直接返回缓存中的数据。
为了便于下面演示各种各样的缓存技术,我们创建一个手机验证码的案例环境,模拟使用缓存保存手机验证码的过程。
手机验证码案例需求如下:
输入手机号获取验证码,组织文档以短信形式发送给用户(页面模拟)
输入手机号和验证码验证结果
为了描述上述操作,我们制作两个表现层接口,一个用来模拟发送短信的过程,其实就是根据用户提供的手机号生成一个验证码,然后放入缓存,另一个用来模拟验证码校验的过程,其实就是使用传入的手机号和验证码进行匹配,并返回最终匹配结果。下面直接制作本案例的模拟代码,先以上例中springboot提供的内置缓存技术来完成当前案例的制作。
步骤①:导入springboot提供的缓存技术对应的starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
步骤②:启用缓存,在引导类上方标注注解@EnableCaching配置springboot程序中可以使用缓存
@SpringBootApplication
//开启缓存功能
@EnableCaching
public class Springboot19CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot19CacheApplication.class, args);
}
}
步骤③:定义验证码对应的实体类,封装手机号与验证码两个属性
@Data
public class SMSCode {
private String tele;
private String code;
}
步骤④:定义验证码功能的业务层接口与实现类
public interface SMSCodeService {
public String sendCodeToSMS(String tele);
public boolean checkCode(SMSCode smsCode);
}
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@CachePut(value = "smsCode", key = "#tele")
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
return code;
}
public boolean checkCode(SMSCode smsCode) {
//取出内存中的验证码与传递过来的验证码比对,如果相同,返回true
String code = smsCode.getCode();
String cacheCode = codeUtils.get(smsCode.getTele());
return code.equals(cacheCode);
}
}
获取验证码后,当验证码失效时必须重新获取验证码,因此在获取验证码的功能上不能使用@Cacheable注解,@Cacheable注解是缓存中没有值则放入值,缓存中有值则取值。此处的功能仅仅是生成验证码并放入缓存,并不具有从缓存中取值的功能,因此不能使用@Cacheable注解,应该使用仅具有向缓存中保存数据的功能,使用@CachePut注解即可。
对于校验验证码的功能建议放入工具类中进行。
步骤⑤:定义验证码的生成策略与根据手机号读取验证码的功能
@Component
public class CodeUtils {
private String [] patch = {"000000","00000","0000","000","00","0",""};
public String generator(String tele){
int hash = tele.hashCode();
int encryption = 20206666;
long result = hash ^ encryption;
long nowTime = System.currentTimeMillis();
result = result ^ nowTime;
long code = result % 1000000;
code = code < 0 ? -code : code;
String codeStr = code + "";
int len = codeStr.length();
return patch[len] + codeStr;
}
@Cacheable(value = "smsCode",key="#tele")
public String get(String tele){
return null;
}
}
步骤⑥:定义验证码功能的web层接口,一个方法用于提供手机号获取验证码,一个方法用于提供手机号和验证码进行校验
@RestController
@RequestMapping("/sms")
public class SMSCodeController {
@Autowired
private SMSCodeService smsCodeService;
@GetMapping
public String getCode(String tele){
String code = smsCodeService.sendCodeToSMS(tele);
return code;
}
@PostMapping
public boolean checkCode(SMSCode smsCode){
return smsCodeService.checkCode(smsCode);
}
}
手机验证码的案例已经完成了,下面就开始springboot整合各种各样的缓存技术,第一个整合Ehcache技术。Ehcache是一种缓存技术,使用springboot整合Ehcache其实就是变更一下缓存技术的实现方式,话不多说,直接开整
步骤①:导入Ehcache的坐标
<dependency>
<groupId>net.sf.ehcachegroupId>
<artifactId>ehcacheartifactId>
dependency>
此处为什么不是导入Ehcache的starter,而是导入技术坐标呢?其实springboot整合缓存技术做的是通用格式,不管你整合哪种缓存技术,只是实现变化了,操作方式一样。这也体现出springboot技术的优点,统一同类技术的整合方式。
步骤②:配置缓存技术实现使用Ehcache
spring:
cache:
type: ehcache
ehcache:
config: ehcache.xml
配置缓存的类型type为ehcache,此处需要说明一下,当前springboot可以整合的缓存技术中包含有ehcach,所以可以这样书写。其实这个type不可以随便写的,不是随便写一个名称就可以整合的。
由于ehcache的配置有独立的配置文件格式,因此还需要指定ehcache的配置文件,以便于读取相应配置
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="D:\ehcache" />
<defaultCache
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="60"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU" />
<cache
name="smsCode"
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="10"
timeToLiveSeconds="10"
memoryStoreEvictionPolicy="LRU" />
ehcache>
注意前面的案例中,设置了数据保存的位置是smsCode
@CachePut(value = "smsCode", key = "#tele")
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
return code;
}
这个设定需要保障ehcache中有一个缓存空间名称叫做smsCode的配置,前后要统一。在企业开发过程中,通过设置不同名称的cache来设定不同的缓存策略,应用于不同的缓存数据。
到这里springboot整合Ehcache就做完了,可以发现一点,原始代码没有任何修改,仅仅是加了一组配置就可以变更缓存供应商了,这也是springboot提供了统一的缓存操作接口的优势,变更实现并不影响原始代码的书写。
总结
上节使用Ehcache替换了springboot内置的缓存技术,其实springboot支持的缓存技术还很多,下面使用redis技术作为缓存解决方案来实现手机验证码案例。
比对使用Ehcache的过程,加坐标,改缓存实现类型为ehcache,做Ehcache的配置。如果还成redis做缓存呢?一模一样,加坐标,改缓存实现类型为redis,做redis的配置。差别之处只有一点,redis的配置可以在yml文件中直接进行配置,无需制作独立的配置文件。
步骤①:导入redis的坐标
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
步骤②:配置缓存技术实现使用redis
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
如果需要对redis作为缓存进行配置,注意不是对原始的redis进行配置,而是配置redis作为缓存使用相关的配置,隶属于spring.cache.redis节点下,注意不要写错位置了。
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
use-key-prefix: false
key-prefix: sms_
cache-null-values: false
time-to-live: 10s
总结
目前我们已经掌握了3种缓存解决方案的配置形式,分别是springboot内置缓存,ehcache和redis,本节研究一下国内比较流行的一款缓存memcached。
按照之前的套路,其实变更缓存并不繁琐,但是springboot并没有支持使用memcached作为其缓存解决方案,也就是说在type属性中没有memcached的配置选项,这里就需要更变一下处理方式了。在整合之前先安装memcached。
安装
安装服务的命令,如下:
memcached.exe -d install
服务安装完毕后可以使用命令启动和停止服务,如下:
memcached.exe -d start # 启动服务
memcached.exe -d stop # 停止服务
也可以在任务管理器中进行服务状态的切换
变更缓存为Memcached
由于memcached未被springboot收录为缓存解决方案,因此使用memcached需要通过手工硬编码的方式来使用,于是前面的套路都不适用了,需要自己写了。
memcached目前提供有三种客户端技术,分别是Memcached Client for Java、SpyMemcached和Xmemcached,其中性能指标各方面最好的客户端是Xmemcached,本次整合就使用这个作为客户端实现技术了。下面开始使用Xmemcached
步骤①:导入xmemcached的坐标
<dependency>
<groupId>com.googlecode.xmemcachedgroupId>
<artifactId>xmemcachedartifactId>
<version>2.4.7version>
dependency>
步骤②:配置memcached,制作memcached的配置类
@Configuration
public class XMemcachedConfig {
@Bean
public MemcachedClient getMemcachedClient() throws IOException {
MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder("localhost:11211");
MemcachedClient memcachedClient = memcachedClientBuilder.build();
return memcachedClient;
}
}
memcached默认对外服务端口11211。
步骤③:使用xmemcached客户端操作缓存,注入MemcachedClient对象
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@Autowired
private MemcachedClient memcachedClient;
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
try {
memcachedClient.set(tele,10,code);
} catch (Exception e) {
e.printStackTrace();
}
return code;
}
public boolean checkCode(SMSCode smsCode) {
String code = null;
try {
code = memcachedClient.get(smsCode.getTele()).toString();
} catch (Exception e) {
e.printStackTrace();
}
return smsCode.getCode().equals(code);
}
}
设置值到缓存中使用set操作,取值使用get操作,其实更符合我们开发者的习惯。
上述代码中对于服务器的配置使用硬编码写死到了代码中,将此数据提取出来,做成独立的配置属性。
定义配置属性
以下过程采用前期学习的属性配置方式进行,当前操作有助于理解原理篇中的很多知识。
定义配置类,加载必要的配置属性,读取配置文件中memcached节点信息
@Component
@ConfigurationProperties(prefix = "memcached")
@Data
public class XMemcachedProperties {
private String servers;
private int poolSize;
private long opTimeout;
}
定义memcached节点信息
memcached:
servers: localhost:11211
poolSize: 10
opTimeout: 3000
在memcached配置类中加载信息
@Configuration
public class XMemcachedConfig {
@Autowired
private XMemcachedProperties props;
@Bean
public MemcachedClient getMemcachedClient() throws IOException {
MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder(props.getServers());
memcachedClientBuilder.setConnectionPoolSize(props.getPoolSize());
memcachedClientBuilder.setOpTimeout(props.getOpTimeout());
MemcachedClient memcachedClient = memcachedClientBuilder.build();
return memcachedClient;
}
}
总结
思考
到这里已经完成了三种缓存的整合,其中redis和mongodb需要安装独立的服务器,连接时需要输入对应的服务器地址,这种是远程缓存,Ehcache是一个典型的内存级缓存,因为它什么也不用安装,启动后导入jar包就有缓存功能了。这个时候就要问了,能不能这两种缓存一起用呢?咱们下节再说。
目前我们使用的缓存都是要么A要么B,能不能AB一起用呢?这一节就解决这个问题。springboot针对缓存的整合仅仅停留在用缓存上面,如果缓存自身不支持同时支持AB一起用,springboot也没办法,所以要想解决AB缓存一起用的问题,就必须找一款缓存能够支持AB两种缓存一起用,有这种缓存吗?还真有,阿里出品,jetcache。
jetcache严格意义上来说,并不是一个缓存解决方案,只能说他算是一个缓存框架,然后把别的缓存放到jetcache中管理,这样就可以支持AB缓存一起用了。并且jetcache参考了springboot整合缓存的思想,整体技术使用方式和springboot的缓存解决方案思想非常类似。下面咱们就先把jetcache用起来,然后再说它里面的一些小的功能。
做之前要先明确一下,jetcache并不是随便拿两个缓存都能拼到一起去的。目前jetcache支持的缓存方案本地缓存支持两种,远程缓存支持两种,分别如下:
Redis
Tair
其实也有人问我,为什么jetcache只支持2+2这么4款缓存呢?阿里研发这个技术其实主要是为了满足自身的使用需要。最初肯定只有1+1种,逐步变化成2+2种。下面就以LinkedHashMap+Redis的方案实现本地与远程缓存方案同时使用。
步骤①:导入springboot整合jetcache对应的坐标starter,当前坐标默认使用的远程方案是redis
<dependency>
<groupId>com.alicp.jetcachegroupId>
<artifactId>jetcache-starter-redisartifactId>
<version>2.6.2version>
dependency>
步骤②:远程方案基本配置
jetcache:
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
其中poolConfig是必配项,否则会报错
步骤③:启用缓存,在引导类上方标注注解@EnableCreateCacheAnnotation配置springboot程序中可以使用注解的形式创建缓存
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20JetCacheApplication.class, args);
}
}
步骤④:创建缓存对象Cache,并使用注解@CreateCache标记当前缓存的信息,然后使用Cache对象的API操作缓存,put写缓存,get读缓存。
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@CreateCache(name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)
private Cache<String ,String> jetCache;
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele,code);
return code;
}
public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}
通过上述jetcache使用远程方案连接redis可以看出,jetcache操作缓存时的接口操作更符合开发者习惯,使用缓存就先获取缓存对象Cache,放数据进去就是put,取数据出来就是get,更加简单易懂。并且jetcache操作缓存时,可以为某个缓存对象设置过期时间,将同类型的数据放入缓存中,方便有效周期的管理。
上述方案中使用的是配置中定义的default缓存,其实这个default是个名字,可以随便写,也可以随便加。例如再添加一种缓存解决方案,参照如下配置进行:
jetcache:
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
sms:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
如果想使用名称是sms的缓存,需要再创建缓存时指定参数area,声明使用对应缓存即可
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@CreateCache(area="sms",name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)
private Cache jetCache;
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele,code);
return code;
}
public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}
远程方案中,配置中使用remote表示远程,换成local就是本地,只不过类型不一样而已。
步骤①:导入springboot整合jetcache对应的坐标starter
<dependency>
<groupId>com.alicp.jetcachegroupId>
<artifactId>jetcache-starter-redisartifactId>
<version>2.6.2version>
dependency>
步骤②:本地缓存基本配置
jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson
为了加速数据获取时key的匹配速度,jetcache要求指定key的类型转换器。简单说就是,如果你给了一个Object作为key的话,我先用key的类型转换器给转换成字符串,然后再保存。等到获取数据时,仍然是先使用给定的Object转换成字符串,然后根据字符串匹配。由于jetcache是阿里的技术,这里推荐key的类型转换器使用阿里的fastjson。
步骤③:启用缓存
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20JetCacheApplication.class, args);
}
}
步骤④:创建缓存对象Cache时,标注当前使用本地缓存
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.LOCAL)
private Cache<String ,String> jetCache;
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
jetCache.put(tele,code);
return code;
}
public boolean checkCode(SMSCode smsCode) {
String code = jetCache.get(smsCode.getTele());
return smsCode.getCode().equals(code);
}
}
cacheType控制当前缓存使用本地缓存还是远程缓存,配置cacheType=CacheType.LOCAL即使用本地缓存。
本地和远程方法都有了,两种方案一起使用如何配置呢?其实就是将两种配置合并到一起就可以了。
jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote:
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
sms:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
在创建缓存的时候,配置cacheType为BOTH即则本地缓存与远程缓存同时使用。
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.BOTH)
private Cache<String ,String> jetCache;
}
cacheType如果不进行配置,默认值是REMOTE,即仅使用远程缓存方案。关于jetcache的配置,参考以下信息
属性 | 默认值 | 说明 |
---|---|---|
jetcache.statIntervalMinutes | 0 | 统计间隔,0表示不统计 |
jetcache.hiddenPackages | 无 | 自动生成name时,隐藏指定的包名前缀 |
jetcache.[local|remote].${area}.type | 无 | 缓存类型,本地支持linkedhashmap、caffeine,远程支持redis、tair |
jetcache.[local|remote].${area}.keyConvertor | 无 | key转换器,当前仅支持fastjson |
jetcache.[local|remote].${area}.valueEncoder | java | 仅remote类型的缓存需要指定,可选java和kryo |
jetcache.[local|remote].${area}.valueDecoder | java | 仅remote类型的缓存需要指定,可选java和kryo |
jetcache.[local|remote].${area}.limit | 100 | 仅local类型的缓存需要指定,缓存实例最大元素数 |
jetcache.[local|remote].${area}.expireAfterWriteInMillis | 无穷大 | 默认过期时间,毫秒单位 |
jetcache.local.${area}.expireAfterAccessInMillis | 0 | 仅local类型的缓存有效,毫秒单位,最大不活动间隔 |
以上方案仅支持手工控制缓存,但是springcache方案中的方法缓存特别好用,给一个方法添加一个注解,方法就会自动使用缓存。jetcache也提供了对应的功能,即方法缓存。
方法缓存
jetcache提供了方法缓存方案,只不过名称变更了而已。在对应的操作接口上方使用注解@Cached即可
步骤①:导入springboot整合jetcache对应的坐标starter
<dependency>
<groupId>com.alicp.jetcachegroupId>
<artifactId>jetcache-starter-redisartifactId>
<version>2.6.2version>
dependency>
步骤②:配置缓存
jetcache:
local:
default:
type: linkedhashmap
keyConvertor: fastjson
remote:
default:
type: redis
host: localhost
port: 6379
keyConvertor: fastjson
valueEncode: java
valueDecode: java
poolConfig:
maxTotal: 50
sms:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 50
由于redis缓存中不支持保存对象,因此需要对redis设置当Object类型数据进入到redis中时如何进行类型转换。需要配置keyConvertor表示key的类型转换方式,同时标注value的转换类型方式,值进入redis时是java类型,标注valueEncode为java,值从redis中读取时转换成java,标注valueDecode为java。
注意,为了实现Object类型的值进出redis,需要保障进出redis的Object类型的数据必须实现序列化接口。
@Data
public class Book implements Serializable {
private Integer id;
private String type;
private String name;
private String description;
}
步骤③:启用缓存时开启方法缓存功能,并配置basePackages,说明在哪些包中开启方法缓存
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
//开启方法注解缓存
@EnableMethodCache(basePackages = "com.itheima")
public class Springboot20JetCacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20JetCacheApplication.class, args);
}
}
步骤④:使用注解@Cached标注当前方法使用缓存
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
public Book getById(Integer id) {
return bookDao.selectById(id);
}
}
由于远程方案中redis保存的数据可以被多个客户端共享,这就存在了数据同步问题。jetcache提供了3个注解解决此问题,分别在更新、删除操作时同步缓存数据,和读取缓存时定时刷新数据
更新缓存
@CacheUpdate(name="book_",key="#book.id",value="#book")
public boolean update(Book book) {
return bookDao.updateById(book) > 0;
}
删除缓存
@CacheInvalidate(name="book_",key = "#id")
public boolean delete(Integer id) {
return bookDao.deleteById(id) > 0;
}
定时刷新缓存
@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
@CacheRefresh(refresh = 5)
public Book getById(Integer id) {
return bookDao.selectById(id);
}
jetcache还提供有简单的数据报表功能,帮助开发者快速查看缓存命中信息,只需要添加一个配置即可
jetcache:
statIntervalMinutes: 1
设置后,每1分钟在控制台输出缓存数据命中信息
[DefaultExecutor] c.alicp.jetcache.support.StatInfoLogger : jetcache stat from 2022-02-28 09:32:15,892 to 2022-02-28 09:33:00,003
cache | qps| rate| get| hit| fail| expire| avgLoadTime| maxLoadTime
---------+-------+-------+------+-------+-------+---------+--------------+--------------
book_ | 0.66| 75.86%| 29| 22| 0| 0| 28.0| 188
---------+-------+-------+------+-------+-------+---------+--------------+--------------
总结
思考
jetcache解决了前期使用缓存方案单一的问题,但是仍然不能灵活的选择缓存进行搭配使用,是否存在一种技术可以灵活的搭配各种各样的缓存使用呢?有,咱们下一节再讲。
jetcache可以在限定范围内构建多级缓存,但是灵活性不足,不能随意搭配缓存,本节介绍一种可以随意搭配缓存解决方案的缓存整合框架,j2cache。下面就来讲解如何使用这种缓存框架,以Ehcache与redis整合为例:
步骤①:导入j2cache、redis、ehcache坐标
<dependency>
<groupId>net.oschina.j2cachegroupId>
<artifactId>j2cache-coreartifactId>
<version>2.8.4-releaseversion>
dependency>
<dependency>
<groupId>net.oschina.j2cachegroupId>
<artifactId>j2cache-spring-boot2-starterartifactId>
<version>2.8.0-releaseversion>
dependency>
<dependency>
<groupId>net.sf.ehcachegroupId>
<artifactId>ehcacheartifactId>
dependency>
j2cache的starter中默认包含了redis坐标,官方推荐使用redis作为二级缓存,因此此处无需导入redis坐标
步骤②:配置一级与二级缓存,并配置一二级缓存间数据传递方式,配置书写在名称为j2cache.properties的文件中。如果使用ehcache还需要单独添加ehcache的配置文件
# 1级缓存
j2cache.L1.provider_class = ehcache
ehcache.configXml = ehcache.xml
# 2级缓存
j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider
j2cache.L2.config_section = redis
redis.hosts = localhost:6379
# 1级缓存中的数据如何到达二级缓存
j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
此处配置不能乱配置,需要参照官方给出的配置说明进行。例如1级供应商选择ehcache,供应商名称仅仅是一个ehcache,但是2级供应商选择redis时要写专用的Spring整合Redis的供应商类名SpringRedisProvider,而且这个名称并不是所有的redis包中能提供的,也不是spring包中提供的。因此配置j2cache必须参照官方文档配置,而且还要去找专用的整合包,导入对应坐标才可以使用。
一级与二级缓存最重要的一个配置就是两者之间的数据沟通方式,此类配置也不是随意配置的,并且不同的缓存解决方案提供的数据沟通方式差异化很大,需要查询官方文档进行设置。
步骤③:使用缓存
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
@Autowired
private CodeUtils codeUtils;
@Autowired
private CacheChannel cacheChannel;
public String sendCodeToSMS(String tele) {
String code = codeUtils.generator(tele);
cacheChannel.set("sms",tele,code);
return code;
}
public boolean checkCode(SMSCode smsCode) {
String code = cacheChannel.get("sms",smsCode.getTele()).asString();
return smsCode.getCode().equals(code);
}
}
j2cache的使用和jetcache比较类似,但是无需开启使用的开关,直接定义缓存对象即可使用,缓存对象名CacheChannel。
j2cache的使用不复杂,配置是j2cache的核心,毕竟是一个整合型的缓存框架。缓存相关的配置过多,可以查阅j2cache-core核心包中的j2cache.properties文件中的说明。如下:
#J2Cache configuration
#########################################
# Cache Broadcast Method
# values:
# jgroups -> use jgroups's multicast
# redis -> use redis publish/subscribe mechanism (using jedis)
# lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend)
# rabbitmq -> use RabbitMQ publisher/consumer mechanism
# rocketmq -> use RocketMQ publisher/consumer mechanism
# none -> don't notify the other nodes in cluster
# xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy
#########################################
j2cache.broadcast = redis
# jgroups properties
jgroups.channel.name = j2cache
jgroups.configXml = /network.xml
# RabbitMQ properties
rabbitmq.exchange = j2cache
rabbitmq.host = localhost
rabbitmq.port = 5672
rabbitmq.username = guest
rabbitmq.password = guest
# RocketMQ properties
rocketmq.name = j2cache
rocketmq.topic = j2cache
# use ; to split multi hosts
rocketmq.hosts = 127.0.0.1:9876
#########################################
# Level 1&2 provider
# values:
# none -> disable this level cache
# ehcache -> use ehcache2 as level 1 cache
# ehcache3 -> use ehcache3 as level 1 cache
# caffeine -> use caffeine as level 1 cache(only in memory)
# redis -> use redis as level 2 cache (using jedis)
# lettuce -> use redis as level 2 cache (using lettuce)
# readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available.
# memcached -> use memcached as level 2 cache (xmemcached),
# [classname] -> use custom provider
#########################################
j2cache.L1.provider_class = caffeine
j2cache.L2.provider_class = redis
# When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations
# j2cache.L2.config_section = redis
# Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true)
# NOTICE: redis hash mode (redis.storage = hash) do not support this feature)
j2cache.sync_ttl_to_redis = true
# Whether to cache null objects by default (default false)
j2cache.default_cache_null_object = true
#########################################
# Cache Serialization Provider
# values:
# fst -> using fast-serialization (recommend)
# kryo -> using kryo serialization
# json -> using fst's json serialization (testing)
# fastjson -> using fastjson serialization (embed non-static class not support)
# java -> java standard
# fse -> using fse serialization
# [classname implements Serializer]
#########################################
j2cache.serialization = json
#json.map.person = net.oschina.j2cache.demo.Person
#########################################
# Ehcache configuration
#########################################
# ehcache.configXml = /ehcache.xml
# ehcache3.configXml = /ehcache3.xml
# ehcache3.defaultHeapSize = 1000
#########################################
# Caffeine configuration
# caffeine.region.[name] = size, xxxx[s|m|h|d]
#
#########################################
caffeine.properties = /caffeine.properties
#########################################
# Redis connection configuration
#########################################
#########################################
# Redis Cluster Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:[email protected]:6379/0)
#
#########################################
redis.mode = single
#redis storage mode (generic|hash)
redis.storage = generic
## redis pub/sub channel name
redis.channel = j2cache
## redis pub/sub server (using redis.hosts when empty)
redis.channel.host =
#cluster name just for sharded
redis.cluster_name = j2cache
## redis cache namespace optional, default[empty]
redis.namespace =
## redis command scan parameter count, default[1000]
#redis.scanCount = 1000
## connection
# Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379
redis.hosts = 127.0.0.1:6379
redis.timeout = 2000
redis.password =
redis.database = 0
redis.ssl = false
## redis pool properties
redis.maxTotal = 100
redis.maxIdle = 10
redis.maxWaitMillis = 5000
redis.minEvictableIdleTimeMillis = 60000
redis.minIdle = 1
redis.numTestsPerEvictionRun = 10
redis.lifo = false
redis.softMinEvictableIdleTimeMillis = 10
redis.testOnBorrow = true
redis.testOnReturn = false
redis.testWhileIdle = true
redis.timeBetweenEvictionRunsMillis = 300000
redis.blockWhenExhausted = false
redis.jmxEnabled = false
#########################################
# Lettuce scheme
#
# redis -> single redis server
# rediss -> single redis server with ssl
# redis-sentinel -> redis sentinel
# redis-cluster -> cluster servers
#
#########################################
#########################################
# Lettuce Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:[email protected]:6379/0)
#
#########################################
## redis command scan parameter count, default[1000]
#lettuce.scanCount = 1000
lettuce.mode = single
lettuce.namespace =
lettuce.storage = hash
lettuce.channel = j2cache
lettuce.scheme = redis
lettuce.hosts = 127.0.0.1:6379
lettuce.password =
lettuce.database = 0
lettuce.sentinelMasterId =
lettuce.maxTotal = 100
lettuce.maxIdle = 10
lettuce.minIdle = 10
# timeout in milliseconds
lettuce.timeout = 10000
# redis cluster topology refresh interval in milliseconds
lettuce.clusterTopologyRefresh = 3000
#########################################
# memcached server configurations
# refer to https://gitee.com/mirrors/XMemcached
#########################################
memcached.servers = 127.0.0.1:11211
memcached.username =
memcached.password =
memcached.connectionPoolSize = 10
memcached.connectTimeout = 1000
memcached.failureMode = false
memcached.healSessionInterval = 1000
memcached.maxQueuedNoReplyOperations = 100
memcached.opTimeout = 100
memcached.sanitizeKeys = false
总结
springboot整合第三方技术第二部分我们来说说任务系统,其实这里说的任务系统指的是定时任务。定时任务是企业级开发中必不可少的组成部分,诸如长周期业务数据的计算,例如年度报表,诸如系统脏数据的处理,再比如系统性能监控报告,还有抢购类活动的商品上架,这些都离不开定时任务。本节将介绍两种不同的定时任务技术。
Quartz技术是一个比较成熟的定时任务框架,怎么说呢?有点繁琐,用过的都知道,配置略微复杂。springboot对其进行整合后,简化了一系列的配置,将很多配置采用默认设置,这样开发阶段就简化了很多。再学习springboot整合Quartz前先普及几个Quartz的概念。
工作(Job):用于定义具体执行的工作
工作明细(JobDetail):用于描述定时工作相关的信息
触发器(Trigger):描述了工作明细与调度器的对应关系
调度器(Scheduler):用于描述触发工作的执行规则,通常使用cron表达式定义规则
简单说就是你定时干什么事情,这就是工作,工作不可能就是一个简单的方法,还要设置一些明细信息。工作啥时候执行,设置一个调度器,可以简单理解成设置一个工作执行的时间。工作和调度都是独立定义的,它们两个怎么配合到一起呢?用触发器。完了,就这么多。下面开始springboot整合Quartz。
步骤①:导入springboot整合Quartz的starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-quartzartifactId>
dependency>
步骤②:定义任务Bean,按照Quartz的开发规范制作,继承QuartzJobBean
public class MyQuartz extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("quartz task run...");
}
}
步骤③:创建Quartz配置类,定义工作明细(JobDetail)与触发器的(Trigger)bean
@Configuration
public class QuartzConfig {
@Bean
public JobDetail printJobDetail(){
//绑定具体的工作
return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
}
@Bean
public Trigger printJobTrigger(){
ScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
//绑定对应的工作明细
return TriggerBuilder.newTrigger().forJob(printJobDetail()).withSchedule(schedBuilder).build();
}
}
工作明细中要设置对应的具体工作,使用newJob()操作传入对应的工作任务类型即可。
触发器需要绑定任务,使用forJob()操作传入绑定的工作明细对象。此处可以为工作明细设置名称然后使用名称绑定,也可以直接调用对应方法绑定。触发器中最核心的规则是执行时间,此处使用调度器定义执行时间,执行时间描述方式使用的是cron表达式。有关cron表达式的规则,各位小伙伴可以去参看相关课程学习,略微复杂,而且格式不能乱设置,不是写个格式就能用的,写不好就会出现冲突问题。
总结
思考
上面的操作看上去不多,但是Quartz将其中的对象划分粒度过细,导致开发的时候有点繁琐,spring针对上述规则进行了简化,开发了自己的任务管理组件——Task,如何用呢?咱们下节再说。
spring根据定时任务的特征,将定时任务的开发简化到了极致。怎么说呢?要做定时任务总要告诉容器有这功能吧,然后定时执行什么任务直接告诉对应的bean什么时间执行就行了,就这么简单,一起来看怎么做
步骤①:开启定时任务功能,在引导类上开启定时任务功能的开关,使用注解@EnableScheduling
@SpringBootApplication
//开启定时任务功能
@EnableScheduling
public class Springboot22TaskApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot22TaskApplication.class, args);
}
}
步骤②:定义Bean,在对应要定时执行的操作上方,使用注解@Scheduled定义执行的时间,执行时间的描述方式还是cron表达式
@Component
public class MyBean {
@Scheduled(cron = "0/1 * * * * ?")
public void print(){
System.out.println(Thread.currentThread().getName()+" :spring task run...");
}
}
完事,这就完成了定时任务的配置。总体感觉其实什么东西都没少,只不过没有将所有的信息都抽取成bean,而是直接使用注解绑定定时执行任务的事情而已。
如何想对定时任务进行相关配置,可以通过配置文件进行
spring:
task:
scheduling:
pool:
size: 1 # 任务调度线程池大小 默认 1
thread-name-prefix: ssm_ # 调度线程名称前缀 默认 scheduling-
shutdown:
await-termination: false # 线程池关闭时等待所有任务完成
await-termination-period: 10s # 调度线程关闭前最大等待时间,确保最后一定关闭
总结
springboot整合第三方技术第三部分我们来说说邮件系统,发邮件是java程序的基本操作,springboot整合javamail其实就是简化开发。不熟悉邮件的小伙伴可以先学习完javamail的基础操作,再来看这一部分内容才能感触到springboot整合javamail究竟简化了哪些操作。简化的多码?其实不多,差别不大,只是还个格式而已。
学习邮件发送之前先了解3个概念,这些概念规范了邮件操作过程中的标准。
SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于发送电子邮件的传输协议
POP3(Post Office Protocol - Version 3):用于接收电子邮件的标准协议
IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议
简单说就是SMPT是发邮件的标准,POP3是收邮件的标准,IMAP是对POP3的升级。我们制作程序中操作邮件,通常是发邮件,所以SMTP是使用的重点,收邮件大部分都是通过邮件客户端完成,所以开发收邮件的代码极少。除非你要读取邮件内容,然后解析,做邮件功能的统一处理。例如HR的邮箱收到求职者的简历,可以读取后统一处理。但是为什么不制作独立的投递简历的系统呢?所以说,好奇怪的需求,因为要想收邮件就要规范发邮件的人的书写格式,这个未免有点强人所难,并且极易收到外部攻击,你不可能使用白名单来收邮件。如果能使用白名单来收邮件然后解析邮件,还不如开发个系统给白名单中的人专用呢,更安全,总之就是鸡肋了。下面就开始学习springboot如何整合javamail发送邮件。
步骤①:导入springboot整合javamail的starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
步骤②:配置邮箱的登录信息
spring:
mail:
host: smtp.126.com
username: [email protected]
password: test
java程序仅用于发送邮件,邮件的功能还是邮件供应商提供的,所以这里是用别人的邮件服务,要配置对应信息。
host配置的是提供邮件服务的主机协议,当前程序仅用于发送邮件,因此配置的是smtp的协议。
password并不是邮箱账号的登录密码,是邮件供应商提供的一个加密后的密码,也是为了保障系统安全性。不然外部人员通过地址访问下载了配置文件,直接获取到了邮件密码就会有极大的安全隐患。有关该密码的获取每个邮件供应商提供的方式都不一样,此处略过。可以到邮件供应商的设置页面找POP3或IMAP这些关键词找到对应的获取位置。下例仅供参考:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22xdxPwb-1660998943287)(img\image-20220228111251036.png)]
步骤③:使用JavaMailSender接口发送邮件
@Service
public class SendMailServiceImpl implements SendMailService {
@Autowired
private JavaMailSender javaMailSender;
//发送人
private String from = "[email protected]";
//接收人
private String to = "[email protected]";
//标题
private String subject = "测试邮件";
//正文
private String context = "测试邮件正文内容";
@Override
public void sendMail() {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from+"(小甜甜)");
message.setTo(to);
message.setSubject(subject);
message.setText(context);
javaMailSender.send(message);
}
}
将发送邮件的必要信息(发件人、收件人、标题、正文)封装到SimpleMailMessage对象中,可以根据规则设置发送人昵称等。
发送简单邮件仅需要提供对应的4个基本信息就可以了,如果想发送复杂的邮件,需要更换邮件对象。使用MimeMessage可以发送特殊的邮件。
发送网页正文邮件
@Service
public class SendMailServiceImpl2 implements SendMailService {
@Autowired
private JavaMailSender javaMailSender;
//发送人
private String from = "[email protected]";
//接收人
private String to = "[email protected]";
//标题
private String subject = "测试邮件";
//正文
private String context = "点开有惊喜";
public void sendMail() {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(to+"(小甜甜)");
helper.setTo(from);
helper.setSubject(subject);
helper.setText(context,true); //此处设置正文支持html解析
javaMailSender.send(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
发送带有附件的邮件
@Service
public class SendMailServiceImpl2 implements SendMailService {
@Autowired
private JavaMailSender javaMailSender;
//发送人
private String from = "[email protected]";
//接收人
private String to = "[email protected]";
//标题
private String subject = "测试邮件";
//正文
private String context = "测试邮件正文";
public void sendMail() {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message,true); //此处设置支持附件
helper.setFrom(to+"(小甜甜)");
helper.setTo(from);
helper.setSubject(subject);
helper.setText(context);
//添加附件
File f1 = new File("springboot_23_mail-0.0.1-SNAPSHOT.jar");
File f2 = new File("resources\\logo.png");
helper.addAttachment(f1.getName(),f1);
helper.addAttachment("最靠谱的培训结构.png",f2);
javaMailSender.send(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结
springboot整合第三方技术最后一部分我们来说说消息中间件,首先先介绍一下消息的应用。
从广义角度来说,消息其实就是信息,但是和信息又有所不同。信息通常被定义为一组数据,而消息除了具有数据的特征之外,还有消息的来源与接收的概念。通常发送消息的一方称为消息的生产者,接收消息的一方称为消息的消费者。这样比较后,发现其实消息和信息差别还是很大的。
为什么要设置生产者和消费者呢?这就是要说到消息的意义了。信息通常就是一组数据,但是消息由于有了生产者和消费者,就出现了消息中所包含的信息可以被二次解读,生产者发送消息,可以理解为生产者发送了一个信息,也可以理解为生产者发送了一个命令;消费者接收消息,可以理解为消费者得到了一个信息,也可以理解为消费者得到了一个命令。对比一下我们会发现信息是一个基本数据,而命令则可以关联下一个行为动作,这样就可以理解为基于接收的消息相当于得到了一个行为动作,使用这些行为动作就可以组织成一个业务逻辑,进行进一步的操作。总的来说,消息其实也是一组信息,只是为其赋予了全新的含义,因为有了消息的流动,并且是有方向性的流动,带来了基于流动的行为产生的全新解读。开发者就可以基于消息的这种特殊解,将其换成代码中的指令。
对于消息的理解,初学者总认为消息内部的数据非常复杂,这是一个误区。比如我发送了一个消息,要求接受者翻译发送过去的内容。初学者会认为消息中会包含被翻译的文字,已经本次操作要执行翻译操作而不是打印操作。其实这种现象有点过度解读了,发送的消息中仅仅包含被翻译的文字,但是可以通过控制不同的人接收此消息来确认要做的事情。例如发送被翻译的文字仅到A程序,而A程序只能进行翻译操作,这样就可以发送简单的信息完成复杂的业务了,是通过接收消息的主体不同,进而执行不同的操作,而不会在消息内部定义数据的操作行为,当然如果开发者希望消息中包含操作种类信息也是可以的,只是提出消息的内容可以更简单,更单一。
对于消息的生产者与消费者的工作模式,还可以将消息划分成两种模式,同步消费与异步消息。
所谓同步消息就是生产者发送完消息,等待消费者处理,消费者处理完将结果告知生产者,然后生产者继续向下执行业务。这种模式过于卡生产者的业务执行连续性,在现在的企业级开发中,上述这种业务场景通常不会采用消息的形式进行处理。
所谓异步消息就是生产者发送完消息,无需等待消费者处理完毕,生产者继续向下执行其他动作。比如生产者发送了一个日志信息给日志系统,发送过去以后生产者就向下做其他事情了,无需关注日志系统的执行结果。日志系统根据接收到的日志信息继续进行业务执行,是单纯的记录日志,还是记录日志并报警,这些和生产者无关,这样生产者的业务执行效率就会大幅度提升。并且可以通过添加多个消费者来处理同一个生产者发送的消息来提高系统的高并发性,改善系统工作效率,提高用户体验。一旦某一个消费者由于各种问题宕机了,也不会对业务产生影响,提高了系统的高可用性。
以上简单的介绍了一下消息这种工作模式存在的意义,希望对各位学习者有所帮助。
目前企业级开发中广泛使用的消息处理技术共三大类,具体如下:
JMS
AMQP
MQTT
为什么是三大类,而不是三个技术呢?因为这些都是规范,就想JDBC技术,是个规范,开发针对规范开发,运行还要靠实现类,例如MySQL提供了JDBC的实现,最终运行靠的还是实现。并且这三类规范都是针对异步消息进行处理的,也符合消息的设计本质,处理异步的业务。对以上三种消息规范做一下普及
JMS(Java Message Service),这是一个规范,作用等同于JDBC规范,提供了与消息服务相关的API接口。
JMS消息模型
JMS规范中规范了消息有两种模型。分别是点对点模型和发布订阅模型。
点对点模型:peer-2-peer,生产者会将消息发送到一个保存消息的容器中,通常使用队列模型,使用队列保存消息。一个队列的消息只能被一个消费者消费,或未被及时消费导致超时。这种模型下,生产者和消费者是一对一绑定的。
发布订阅模型:publish-subscribe,生产者将消息发送到一个保存消息的容器中,也是使用队列模型来保存。但是消息可以被多个消费者消费,生产者和消费者完全独立,相互不需要感知对方的存在。
以上这种分类是从消息的生产和消费过程来进行区分,针对消息所包含的信息不同,还可以进行不同类别的划分。
JMS消息种类
根据消息中包含的数据种类划分,可以将消息划分成6种消息。
TextMessage
MapMessage
BytesMessage
StreamMessage
ObjectMessage
Message (只有消息头和属性)
JMS主张不同种类的消息,消费方式不同,可以根据使用需要选择不同种类的消息。但是这一点也成为其诟病之处,后面再说。整体上来说,JMS就是典型的保守派,什么都按照J2EE的规范来,做一套规范,定义若干个标准,每个标准下又提供一大批API。目前对JMS规范实现的消息中间件技术还是挺多的,毕竟是皇家御用,肯定有人舔,例如ActiveMQ、Redis、HornetMQ。但是也有一些不太规范的实现,参考JMS的标准设计,但是又不完全满足其规范,例如:RabbitMQ、RocketMQ。
JMS的问世为消息中间件提供了很强大的规范性支撑,但是使用的过程中就开始被人诟病,比如JMS设置的极其复杂的多种类消息处理机制。本来分门别类处理挺好的,为什么会被诟病呢?原因就在于JMS的设计是J2EE规范,站在Java开发的角度思考问题。但是现实往往是复杂度很高的。比如我有一个.NET开发的系统A,有一个Java开发的系统B,现在要从A系统给B系统发业务消息,结果两边数据格式不统一,没法操作。JMS不是可以统一数据格式吗?提供了6种数据种类,总有一款适合你啊。NO,一个都不能用。因为A系统的底层语言不是Java语言开发的,根本不支持那些对象。这就意味着如果想使用现有的业务系统A继续开发已经不可能了,必须推翻重新做使用Java语言开发的A系统。
单从上面的说明中其实可以明确感知到,AMQP的出现解决的是消息传递时使用的消息种类的问题,化繁为简,但是其并没有完全推翻JMS的操作API,所以说AMQP仅仅是一种协议,规范了数据传输的格式而已。
AMQP(advanced message queuing protocol):一种协议(高级消息队列协议,也是消息代理规范),规范了网络交换的数据格式,兼容JMS操作。
优点
具有跨平台性,服务器供应商,生产者,消费者可以使用不同的语言来实现
JMS消息种类
AMQP消息种类:byte[]
AMQP在JMS的消息模型基础上又进行了进一步的扩展,除了点对点和发布订阅的模型,开发了几种全新的消息模型,适应各种各样的消息发送。
AMQP消息模型
direct exchange
fanout exchange
topic exchange
headers exchange
system exchange
目前实现了AMQP协议的消息中间件技术也很多,而且都是较为流行的技术,例如:RabbitMQ、StormMQ、RocketMQ
MQTT(Message Queueing Telemetry Transport)消息队列遥测传输,专为小设备设计,是物联网(IOT)生态系统中主要成分之一。由于与JavaEE企业级开发没有交集,此处不作过多的说明。
除了上述3种J2EE企业级应用中广泛使用的三种异步消息传递技术,还有一种技术也不能忽略,Kafka。
Kafka,一种高吞吐量的分布式发布订阅消息系统,提供实时消息功能。Kafka技术并不是作为消息中间件为主要功能的产品,但是其拥有发布订阅的工作模式,也可以充当消息中间件来使用,而且目前企业级开发中其身影也不少见。
本节内容讲围绕着上述内容中的几种实现方案讲解springboot整合各种各样的消息中间件。由于各种消息中间件必须先安装再使用,下面的内容采用Windows系统安装,降低各位学习者的学习难度,基本套路和之前学习NoSQL解决方案一样,先安装再整合。
为了便于下面演示各种各样的消息中间件技术,我们创建一个购物过程生成订单时为用户发送短信的案例环境,模拟使用消息中间件实现发送手机短信的过程。
手机验证码案例需求如下:
执行下单业务时(模拟此过程),调用消息服务,将要发送短信的订单id传递给消息中间件
消息处理服务接收到要发送的订单id后输出订单id(模拟发短信)
由于不涉及数据读写,仅开发业务层与表现层,其中短信处理的业务代码独立开发,代码如下:
订单业务
**业务层接口**
public interface OrderService {
void order(String id);
}
模拟传入订单id,执行下订单业务,参数为虚拟设定,实际应为订单对应的实体类
**业务层实现**
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private MessageService messageService;
@Override
public void order(String id) {
//一系列操作,包含各种服务调用,处理各种业务
System.out.println("订单处理开始");
//短信消息处理
messageService.sendMessage(id);
System.out.println("订单处理结束");
System.out.println();
}
}
业务层转调短信处理的服务MessageService
**表现层服务**
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("{id}")
public void order(@PathVariable String id){
orderService.order(id);
}
}
表现层对外开发接口,传入订单id即可(模拟)
短信处理业务
**业务层接口**
public interface MessageService {
void sendMessage(String id);
String doMessage();
}
短信处理业务层接口提供两个操作,发送要处理的订单id到消息中间件,另一个操作目前暂且设计成处理消息,实际消息的处理过程不应该是手动执行,应该是自动执行,到具体实现时再进行设计
**业务层实现**
@Service
public class MessageServiceImpl implements MessageService {
private ArrayList msgList = new ArrayList();
@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列,id:"+id);
msgList.add(id);
}
@Override
public String doMessage() {
String id = msgList.remove(0);
System.out.println("已完成短信发送业务,id:"+id);
return id;
}
}
短信处理业务层实现中使用集合先模拟消息队列,观察效果
**表现层服务**
@RestController
@RequestMapping("/msgs")
public class MessageController {
@Autowired
private MessageService messageService;
@GetMapping
public String doMessage(){
String id = messageService.doMessage();
return id;
}
}
短信处理表现层接口暂且开发出一个处理消息的入口,但是此业务是对应业务层中设计的模拟接口,实际业务不需要设计此接口。
下面开启springboot整合各种各样的消息中间件,从严格满足JMS规范的ActiveMQ开始
ActiveMQ是MQ产品中的元老级产品,早期标准MQ产品之一,在AMQP协议没有出现之前,占据了消息中间件市场的绝大部分份额,后期因为AMQP系列产品的出现,迅速走弱,目前仅在一些线上运行的产品中出现,新产品开发较少采用。
启动服务器
activemq.bat
运行bin目录下的win32或win64目录下的activemq.bat命令即可,根据自己的操作系统选择即可,默认对外服务端口61616。
访问web管理服务
ActiveMQ启动后会启动一个Web控制台服务,可以通过该服务管理ActiveMQ。
http://127.0.0.1:8161/
web管理服务默认端口8161,访问后可以打开ActiveMQ的管理界面
首先输入访问用户名和密码,初始化用户名和密码相同,均为:admin,成功登录后进入管理后台界面
看到上述界面视为启动ActiveMQ服务成功。
做了这么多springboot整合第三方技术,已经摸到门路了,加坐标,做配置,调接口,直接开工
步骤①:导入springboot整合ActiveMQ的starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-activemqartifactId>
dependency>
步骤②:配置ActiveMQ的服务器地址
spring:
activemq:
broker-url: tcp://localhost:61616
步骤③:使用JmsMessagingTemplate操作ActiveMQ
@Service
public class MessageServiceActivemqImpl implements MessageService {
@Autowired
private JmsMessagingTemplate messagingTemplate;
@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列,id:"+id);
messagingTemplate.convertAndSend("order.queue.id",id);
}
@Override
public String doMessage() {
String id = messagingTemplate.receiveAndConvert("order.queue.id",String.class);
System.out.println("已完成短信发送业务,id:"+id);
return id;
}
}
发送消息需要先将消息的类型转换成字符串,然后再发送,所以是convertAndSend,定义消息发送的位置,和具体的消息内容,此处使用id作为消息内容。
接收消息需要先将消息接收到,然后再转换成指定的数据类型,所以是receiveAndConvert,接收消息除了提供读取的位置,还要给出转换后的数据的具体类型。
步骤④:使用消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息
@Component
public class MessageListener {
@JmsListener(destination = "order.queue.id")
@SendTo("order.other.queue.id")
public String receive(String id){
System.out.println("已完成短信发送业务,id:"+id);
return "new:"+id;
}
}
使用注解@JmsListener定义当前方法监听ActiveMQ中指定名称的消息队列。
如果当前消息队列处理完还需要继续向下传递当前消息到另一个队列中使用注解@SendTo即可,这样即可构造连续执行的顺序消息队列。
步骤⑤:切换消息模型由点对点模型到发布订阅模型,修改jms配置即可
spring:
activemq:
broker-url: tcp://localhost:61616
jms:
pub-sub-domain: true
pub-sub-domain默认值为false,即点对点模型,修改为true后就是发布订阅模型。
总结
RabbitMQ是MQ产品中的目前较为流行的产品之一,它遵从AMQP协议。RabbitMQ的底层实现语言使用的是Erlang,所以安装RabbitMQ需要先安装Erlang。
启动服务器
rabbitmq-service.bat start # 启动服务
rabbitmq-service.bat stop # 停止服务
rabbitmqctl status # 查看服务状态
运行sbin目录下的rabbitmq-service.bat命令即可,start参数表示启动,stop参数表示退出,默认对外服务端口5672。
注意:启动rabbitmq的过程实际上是开启rabbitmq对应的系统服务,需要管理员权限方可执行。
说明:有没有感觉5672的服务端口很熟悉?activemq与rabbitmq有一个端口冲突问题,学习阶段无论操作哪一个?请确保另一个处于关闭状态。
说明:不喜欢命令行的小伙伴可以使用任务管理器中的服务页,找到RabbitMQ服务,使用鼠标右键菜单控制服务的启停。
访问web管理服务
RabbitMQ也提供有web控制台服务,但是此功能是一个插件,需要先启用才可以使用。
rabbitmq-plugins.bat list # 查看当前所有插件的运行状态
rabbitmq-plugins.bat enable rabbitmq_management # 启动rabbitmq_management插件
启动插件后可以在插件运行状态中查看是否运行,运行后通过浏览器即可打开服务后台管理界面
http://localhost:15672
web管理服务默认端口15672,访问后可以打开RabbitMQ的管理界面
首先输入访问用户名和密码,初始化用户名和密码相同,均为:guest,成功登录后进入管理后台界面
RabbitMQ满足AMQP协议,因此不同的消息模型对应的制作不同,先使用最简单的direct模型开发。
步骤①:导入springboot整合amqp的starter,amqp协议默认实现为rabbitmq方案
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
步骤②:配置RabbitMQ的服务器地址
spring:
rabbitmq:
host: localhost
port: 5672
步骤③:初始化直连模式系统设置
由于RabbitMQ不同模型要使用不同的交换机,因此需要先初始化RabbitMQ相关的对象,例如队列,交换机等
@Configuration
public class RabbitConfigDirect {
@Bean
public Queue directQueue(){
return new Queue("direct_queue");
}
@Bean
public Queue directQueue2(){
return new Queue("direct_queue2");
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange("directExchange");
}
@Bean
public Binding bindingDirect(){
return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
}
@Bean
public Binding bindingDirect2(){
return BindingBuilder.bind(directQueue2()).to(directExchange()).with("direct2");
}
}
队列Queue与直连交换机DirectExchange创建后,还需要绑定他们之间的关系Binding,这样就可以通过交换机操作对应队列。
步骤④:使用AmqpTemplate操作RabbitMQ
@Service
public class MessageServiceRabbitmqDirectImpl implements MessageService {
@Autowired
private AmqpTemplate amqpTemplate;
@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rabbitmq direct),id:"+id);
amqpTemplate.convertAndSend("directExchange","direct",id);
}
}
amqp协议中的操作API接口名称看上去和jms规范的操作API接口很相似,但是传递参数差异很大。
步骤⑤:使用消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息
@Component
public class MessageListener {
@RabbitListener(queues = "direct_queue")
public void receive(String id){
System.out.println("已完成短信发送业务(rabbitmq direct),id:"+id);
}
}
使用注解@RabbitListener定义当前方法监听RabbitMQ中指定名称的消息队列。
步骤①:同上
步骤②:同上
步骤③:初始化主题模式系统设置
@Configuration
public class RabbitConfigTopic {
@Bean
public Queue topicQueue(){
return new Queue("topic_queue");
}
@Bean
public Queue topicQueue2(){
return new Queue("topic_queue2");
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("topicExchange");
}
@Bean
public Binding bindingTopic(){
return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*.id");
}
@Bean
public Binding bindingTopic2(){
return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.orders.*");
}
}
主题模式支持routingKey匹配模式,*表示匹配一个单词,#表示匹配任意内容,这样就可以通过主题交换机将消息分发到不同的队列中,详细内容请参看RabbitMQ系列课程。
匹配键 | topic.*.* | topic.# |
---|---|---|
topic.order.id | true | true |
order.topic.id | false | false |
topic.sm.order.id | false | true |
topic.sm.id | false | true |
topic.id.order | true | true |
topic.id | false | true |
topic.order | false | true |
步骤④:使用AmqpTemplate操作RabbitMQ
@Service
public class MessageServiceRabbitmqTopicImpl implements MessageService {
@Autowired
private AmqpTemplate amqpTemplate;
@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rabbitmq topic),id:"+id);
amqpTemplate.convertAndSend("topicExchange","topic.orders.id",id);
}
od.java:498)
jvm 1 | at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
jvm 1 | at org.apache.activemq.console.Main.main(Main.java:115)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1 | at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1 | at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1 | at java.lang.Thread.run(Thread.java:748)
wrapper | <-- Wrapper Stopped
请按任意键继续. . .
做了这么多springboot整合第三方技术,已经摸到门路了,加坐标,做配置,调接口,直接开工
步骤①:导入springboot整合ActiveMQ的starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-activemqartifactId>
dependency>
步骤②:配置ActiveMQ的服务器地址
spring:
activemq:
broker-url: tcp://localhost:61616
步骤③:使用JmsMessagingTemplate操作ActiveMQ
@Service
public class MessageServiceActivemqImpl implements MessageService {
@Autowired
private JmsMessagingTemplate messagingTemplate;
@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列,id:"+id);
messagingTemplate.convertAndSend("order.queue.id",id);
}
@Override
public String doMessage() {
String id = messagingTemplate.receiveAndConvert("order.queue.id",String.class);
System.out.println("已完成短信发送业务,id:"+id);
return id;
}
}
发送消息需要先将消息的类型转换成字符串,然后再发送,所以是convertAndSend,定义消息发送的位置,和具体的消息内容,此处使用id作为消息内容。
接收消息需要先将消息接收到,然后再转换成指定的数据类型,所以是receiveAndConvert,接收消息除了提供读取的位置,还要给出转换后的数据的具体类型。
步骤④:使用消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息
@Component
public class MessageListener {
@JmsListener(destination = "order.queue.id")
@SendTo("order.other.queue.id")
public String receive(String id){
System.out.println("已完成短信发送业务,id:"+id);
return "new:"+id;
}
}
使用注解@JmsListener定义当前方法监听ActiveMQ中指定名称的消息队列。
如果当前消息队列处理完还需要继续向下传递当前消息到另一个队列中使用注解@SendTo即可,这样即可构造连续执行的顺序消息队列。
步骤⑤:切换消息模型由点对点模型到发布订阅模型,修改jms配置即可
spring:
activemq:
broker-url: tcp://localhost:61616
jms:
pub-sub-domain: true
pub-sub-domain默认值为false,即点对点模型,修改为true后就是发布订阅模型。
总结
RabbitMQ是MQ产品中的目前较为流行的产品之一,它遵从AMQP协议。RabbitMQ的底层实现语言使用的是Erlang,所以安装RabbitMQ需要先安装Erlang。
启动服务器
rabbitmq-service.bat start # 启动服务
rabbitmq-service.bat stop # 停止服务
rabbitmqctl status # 查看服务状态
运行sbin目录下的rabbitmq-service.bat命令即可,start参数表示启动,stop参数表示退出,默认对外服务端口5672。
注意:启动rabbitmq的过程实际上是开启rabbitmq对应的系统服务,需要管理员权限方可执行。
说明:有没有感觉5672的服务端口很熟悉?activemq与rabbitmq有一个端口冲突问题,学习阶段无论操作哪一个?请确保另一个处于关闭状态。
说明:不喜欢命令行的小伙伴可以使用任务管理器中的服务页,找到RabbitMQ服务,使用鼠标右键菜单控制服务的启停。
访问web管理服务
RabbitMQ也提供有web控制台服务,但是此功能是一个插件,需要先启用才可以使用。
rabbitmq-plugins.bat list # 查看当前所有插件的运行状态
rabbitmq-plugins.bat enable rabbitmq_management # 启动rabbitmq_management插件
启动插件后可以在插件运行状态中查看是否运行,运行后通过浏览器即可打开服务后台管理界面
http://localhost:15672
web管理服务默认端口15672,访问后可以打开RabbitMQ的管理界面
首先输入访问用户名和密码,初始化用户名和密码相同,均为:guest,成功登录后进入管理后台界面
RabbitMQ满足AMQP协议,因此不同的消息模型对应的制作不同,先使用最简单的direct模型开发。
步骤①:导入springboot整合amqp的starter,amqp协议默认实现为rabbitmq方案
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
步骤②:配置RabbitMQ的服务器地址
spring:
rabbitmq:
host: localhost
port: 5672
步骤③:初始化直连模式系统设置
由于RabbitMQ不同模型要使用不同的交换机,因此需要先初始化RabbitMQ相关的对象,例如队列,交换机等
@Configuration
public class RabbitConfigDirect {
@Bean
public Queue directQueue(){
return new Queue("direct_queue");
}
@Bean
public Queue directQueue2(){
return new Queue("direct_queue2");
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange("directExchange");
}
@Bean
public Binding bindingDirect(){
return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
}
@Bean
public Binding bindingDirect2(){
return BindingBuilder.bind(directQueue2()).to(directExchange()).with("direct2");
}
}
队列Queue与直连交换机DirectExchange创建后,还需要绑定他们之间的关系Binding,这样就可以通过交换机操作对应队列。
步骤④:使用AmqpTemplate操作RabbitMQ
@Service
public class MessageServiceRabbitmqDirectImpl implements MessageService {
@Autowired
private AmqpTemplate amqpTemplate;
@Override
public void sendMessage(String id) {
System.out.println("待发送短信的订单已纳入处理队列(rabbitmq direct),id:"+id);
amqpTemplate.convertAndSend("directExchange","direct",id);
}
}
amqp协议中的操作API接口名称看上去和jms规范的操作API接口很相似,但是传递参数差异很大。
步骤⑤:使用消息监听器在服务器启动后,监听指定位置,当消息出现后,立即消费消息
@Component
public class MessageListener {
@RabbitListener(queues = "direct_queue")
public void receive(String id){
System.out.println("已完成短信发送业务(rabbitmq direct),id:"+id);
}
}
使用注解@RabbitListener定义当前方法监听RabbitMQ中指定名称的消息队列。
步骤①:同上
步骤②:同上
步骤③:初始化主题模式系统设置
@Configuration
public class RabbitConfigTopic {
@Bean
public Queue topicQueue(){
return new Queue("topic_queue");
}
@Bean
public Queue topicQueue2(){
return new Queue("topic_queue2");
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("topicExchange");
}
@Bean
public Bindin