SpringBoot高级篇学习笔记(五、检索,任务和安全)
十一、SpringBoot与检索搭建环境使用linux中的docker拉取elasticsearch镜像(docker pull elasticsearch)。因为elasticsearch是java写的,开启时默认会占用2个G的内存空间,所以启动的时候需要用-e进行限制docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -
文章目录
十一、SpringBoot与检索
搭建环境
- 使用linux中的docker拉取elasticsearch镜像(
docker pull elasticsearch
)。 - 因为elasticsearch是java写的,开启时默认会占用2个G的内存空间,所以启动的时候需要用-e进行限制
docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 --name ES01 elasticsearch
。(elasticsearch进行web通信时默认使用9200端口,在分布式的情况下各个节点之间的通信使用的是9300端口) - 在浏览器输入
http://10.211.55.17:9200/
(10.211.55.17是我linux的ip,9200是暴露的端口)
elasticsearch官网:https://www.elastic.co/cn/
官方文档:https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
对于员工目录,我们将做如下操作:
- 每个员工索引一个文档,文档包含该员工的所有信息。
- 每个文档都将是 employee 类型 。
- 该类型位于 索引 megacorp 内。
- 该索引保存在我们的 Elasticsearch 集群中。
实践中这非常简单(尽管看起来有很多步骤),我们可以通过一条命令完成所有这些动作
PUT /megacorp/employee/1
{
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
注意,路径 /megacorp/employee/1 包含了三部分的信息:
megacorp
索引名称
employee
类型名称
1
特定雇员的ID
{…}里为请求体 —— JSON 文档 —— 包含了这位员工的所有详细信息,他的名字叫 John Smith ,今年 25 岁,喜欢攀岩。
这里需要用到postman软件(官方下载:https://www.postman.com/)
测试elasticsearch
- 用postman发送put请求保存员工信息:
响应回来的内容:
再保存2名员工:PUT /megacorp/employee/2 { "first_name" : "Jane", "last_name" : "Smith", "age" : 32, "about" : "I like to collect rock albums", "interests": [ "music" ] } PUT /megacorp/employee/3 { "first_name" : "Douglas", "last_name" : "Fir", "age" : 35, "about": "I like to build cabinets", "interests": [ "forestry" ] }
- 用postman发送get请求获取员工信息:
- 用postman发送head请求查询员工信息是否存在:
- 存在(状态码为200)
- 不存在(状态码为404)
- 存在(状态码为200)
- 用postman发送delete请求删除员工信息:
查询3号员工:
- 用postman发送put请求修改1号员工信息:
请求类型 | 效果 |
---|---|
PUT | 添加(或修改)数据 |
GET | 获取数据 |
POST | 获取数据(可使用查询表达式) |
HEAD | 查询是否存在(存在返回状态码为200,不存在为404) |
DELETE | 删除数据 |
- 用get请求查询所有员工信息(_search):
- url加上条件指定查询(q为查询字符串):
- 使用简单的查询表达式条件查询(因为get请求没有请求体,所有需要换成post请求):
{ "query" : { "match" : { "last_name" : "Smith" } } }
- 使用复杂的查询表达式条件查询:
{ "query" : { "bool": { "must": { "match" : { "last_name" : "smith" } }, "filter": { "range" : { "age" : { "gt" : 30 } } } } } }
- 使用全文搜索(类似模糊搜索):
{ "query" : { "match" : { "about" : "rock climbing" } } }
- 短语搜索:
{ "query" : { "match_phrase" : { "about" : "rock climbing" } } }
- 高亮搜索:
查出数据后的,把查到的对应字段放到highlight的about中,用< em >标签表示高亮。{ "query" : { "match_phrase" : { "about" : "rock climbing" } }, "highlight": { "fields" : { "about" : {} } } }
全文、短语、高亮搜索都属于查询表达式
整合ElasticSearch
新建项目
切换ElasticSearch版本
SpringBoot2.3.0对应的ElasticSearch是版本时7.6.2,而刚刚在docker直接拉取最新版的不知道为什么是5.6.12版本的,所以需要重新拉取7.6.2版本的ElasticSearch(docker pull elasticsearch:7.6.2
)。
运行新版本的ElasticSearch,开启其他端口(也可以停了之前的版本用默认端口)。
docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9201:9200 -p 9301:9300 -e "discovery.type=single-node" --name ES02 f29a1ee41030
(这里用了新下载的镜像id(也可以用elasticsearch:7.6.2开启)-e "discovery.type=single-node"
不加上的话一启动就直接停止)
浏览器输入http://10.211.55.17:9201/
我先put10.211.55.17:9201/megacorp/employee/1
插入一条数据,然后在put10.211.55.17:9201/megacorp/employee2/1
出现了错误,说明elasticsearch7以上一个索引只能有一个类型。
SpringBoot2.x中弃用了Jest,查看官方文档发现官方推荐我们使用高级REST客户端。(Java高级REST客户端是Elasticsearch的默认客户端,TransportClient它接受和返回完全相同的请求/响应对象,因此可以直接替换,因此取决于Elasticsearch核心项目。异步调用在客户端管理的线程池上进行,并要求在完成请求后通知回调。)
官方文档:https://docs.spring.io/spring-data/elasticsearch/docs/4.0.0.RELEASE/reference/html/#elasticsearch.clients.rest
ReactiveElasticsearchClient
DefaultReactiveElasticsearchClient
编写一个ElasticsearchRepository的子接口来操作ES
出现这个错误是因为使用了elasticsearch7之前的版本,而springboot版本为2.3,因为elasticsearch7只能有一个类型,所以springboot中关于设置类型的都弃用了,默认类型使用_doc,而elasticsearch7之前的版本类型不能使用带下划线。
org.springframework.data.elasticsearch.UncategorizedElasticsearchException: Elasticsearch exception [type=invalid_type_name_exception, reason=Document mapping type name can't start with '_', found: [_doc]]; nested exception is ElasticsearchStatusException[Elasticsearch exception [type=invalid_type_name_exception, reason=Document mapping type name can't start with '_', found: [_doc]]]
在bean包下新建一个Article实体类
@Data //lombok的注解,可以让我们不写set/get方法
@ToString //自动添加toString方法
@Document(indexName = "angenin") //设置保存的索引
public class Article {
// @Id 在网上看了说需要加@Id设置主键,不过经过测试,不加好像也可以,不过保存时,实体类的id一定要写,不然提交不成功
private Integer id;
// @Field(type = FieldType.Text) //@Field设置类型的,不加好像也可以
private String author;
// @Field(type = FieldType.Text)
private String title;
// @Field(type = FieldType.Text)
private String content;
public Article() {
}
public Article(Integer id, String author, String title, String content) {
this.id = id;
this.author = author;
this.title = title;
this.content = content;
}
}
lombok的依赖坐标:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
在application.properties中设置elasticsearch的uri
spring.elasticsearch.rest.uris=http://10.211.55.17:9201
使用ElasticsearchRestTemplate
从4.0版开始官方推荐ElasticsearchRestTemplate(不推荐使用ElasticsearchTemplate)。
在测试类中测试:
- 保存
@Autowired
ElasticsearchRestTemplate elasticsearchRestTemplate;
//保存
@Test
public void test01(){
Article article = new Article(1, "angenin", "bbb", "helloworld");
//修改也可以用save
elasticsearchRestTemplate.save(article);
}
- 获取
//获取
@Test
public void test02(){
//获取aaa索引中id为1的Article对象
Article article = elasticsearchRestTemplate.get("1", Article.class);
System.out.println(article);
}
- 删除
//删除
@Test
public void test03(){
//删除angenin索引中id为1的数据
elasticsearchRestTemplate.delete("1");
}
继承ElasticsearchRepository接口
在bean下新建Book实体类
@Data
@ToString
@Document(indexName = "book")
public class Book {
private Integer id;
private String bookName;
private String author;
public Book() {
}
public Book(Integer id, String bookName, String author) {
this.id = id;
this.bookName = bookName;
this.author = author;
}
}
在repository包下新建一个接口实现ElasticsearchRepository
//第一个泛型是实体类类型,第二个为实体类主键类型
public interface BookRepository extends ElasticsearchRepository<Book, Integer> {
//继承ElasticsearchRepository所有的方法
}
在测试类中测试
@Autowired
BookRepository bookRepository;
@Test
public void test05(){
Book book = new Book(1, "西游记", "吴承恩");
bookRepository.save(book);
}
在BookRepository接口中加入
//模糊查询书名
List<Book> findByBookNameLike(String bookName);
在测试类中加入
List<Book> bookNameLike = bookRepository.findByBookNameLike("游");
System.out.println(bookNameLike);
springboot会根据自定义的Repository中的方法名,来实现自定义的查询,具体可以看官方文档,也可以在自定义的Repository中的自定义方法上加上@Query
注解来自己定义查询的规则。
如:
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
Page<Book> findByName(String name,Pageable pageable);
}
十二、SpringBoot与任务
新建项目
异步任务
模拟处理数据时发生阻塞:
在service包里新建AsyncService
@Service
public class AsyncService {
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据处理中");
}
}
在controller包里新建AsyncController
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@GetMapping("/hello")
public String hello(){
asyncService.hello();
return "success";
}
}
浏览器输入http://localhost:8080/hello
,3秒后才显示
开启异步
@EnableAsync:在主配置类上加上开启异步注解功能
@Async:在需要异步的方法上加上使其成为异步方法
在Springboot04TaskApplication主配置类上加上@EnableAsync
注解开启异步注解功能,并在AsyncService的hello方法上加上@Async
注解告诉springboot这是一个异步方法,重新启动项目,访问/hello,立即响应返回success。
定时任务
在主配置类上加上@EnableScheduling
开启基于注解的定时任务。
在service包下新建ScheduledService
@Service
public class ScheduledService {
//@Scheduled开启定时执行
//cron属性为设置执行时间,可写以下参数
// * <li>second</li> 秒
// * <li>minute</li> 分
// * <li>hour</li> 时
// * <li>day of month</li> 日
// * <li>month</li> 月
// * <li>day of week</li> 周
// (书写格式:"0 * * * * MON-FRI" (周一到周五每分钟执行一次))具体写法看上图中的表格
// 秒 分 时 日 月 周
@Scheduled(cron = "0 * * * * MON-FRI")
public void hello(){
System.out.println("hello...");
}
}
// @Scheduled(cron = "0 * * * * MON-FRI") //周一到周五每分钟执行一次
// @Scheduled(cron = "0,1,2,3 * * * * MON-FRI") //周一到周五每0,1,2,3秒执行一次
// @Scheduled(cron = "0-4 * * * * MON-FRI") //周一到周五每0到4秒每秒执行一次
// @Scheduled(cron = "0/4 * * * * MON-FRI") //周一到周五每4秒执行一次
// @Scheduled(cron = "0/4 * * * * MON-FRI") //周一到周五每4秒执行一次
邮件任务
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
邮件发送流程:
获取qq邮箱的授权码:
查看SMTP服务器地址:
在application.properties配置文件中配置用户名和授权码:
spring.mail.username=xxx@qq.com
#password不是填写qq邮箱的密码,而是填写qq邮箱生成授权码
spring.mail.password=hgtltpsqaerxebhf
#主机地址(SMTP服务器地址)
spring.mail.host=smtp.qq.com
在测试类中测试:
- 发送简单邮件
//简单邮件发送(由qq邮箱发送到163邮箱)
@Test
void test01() {
//创建一个简单邮件
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
//设置邮件标题
simpleMailMessage.setSubject("通知-今晚峡谷集合");
//设置邮件内容
simpleMailMessage.setText("进行一项多人运动");
//设置收件人
simpleMailMessage.setTo("xxx@163.com");
//设置发件人
simpleMailMessage.setFrom("xxx@qq.com");
//发送邮件
mailSender.send(simpleMailMessage);
}
- 发送复杂邮件
//复杂邮件发送(复杂邮件可以发送html标签,文件等)
@Test
void test02() throws MessagingException {
//创建一个复杂邮件(MimeMessage没有set方法,只能通过MimeMessageHelper进行设置)
MimeMessage mimeMessage = mailSender.createMimeMessage();
//第二个参数为是否开启上传文件功能
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
//设置邮件标题
helper.setSubject("通知-今晚峡谷集合");
//设置邮件内容,第二个参数开启设置html标签,样式
helper.setText("进行一项加强时间管理的<b style='color:yellow'>多人运动</b>", true);
//设置收件人
helper.setTo("xxx@163.com");
//设置发件人
helper.setFrom("xxx@qq.com");
//上传文件()
helper.addAttachment("1.jpg", new File("/Users/pro/Documents/image/1.jpg"));
helper.addAttachment("2.jpg", new File("/Users/pro/Documents/image/2.jpg"));
//发送邮件
mailSender.send(mimeMessage);
}
十三、SpringBoot与安全
文件下载
链接:https://pan.baidu.com/s/1S60CSKZioVeM6AwOmBPcTA 密码:e59m
新建项目
把下载的文件java目录里的KungfuController.java放到项目中的controller包里,把templates目录里的文件放到resources/templates目录里。
启动项目,在浏览器中输入http://localhost:8080/
Spring Security
- 引入Spring Security模块
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
- 编写SpringSecurity的配置类(需要有@EnableWebSecurity并且继承WebSecurityConfigurerAdapter)
- 重写configure(HttpSecurity http)方法定义授权规则
启动项目,点击秘籍,访问别拒绝(需要登录并且有权限才能访问)@EnableWebSecurity //@EnableWebSecurity注解里已经标注了@Configuration注解 public class MySecurityConfig extends WebSecurityConfigurerAdapter { //授权规则 @Override protected void configure(HttpSecurity http) throws Exception { //定制请求的授权规则 //authorizeRequests授权请求 antMatchers路径匹配 http.authorizeRequests() // / 为首页路径,permitAll所有人都可以访问 .antMatchers("/").permitAll() //level1下的所有请求,hasAnyRole需要角色VIP1才可以访问 .antMatchers("/level1/**").hasAnyRole("VIP1") //level2下的所有请求,需要VIP2才可以访问 .antMatchers("/level2/**").hasAnyRole("VIP2") //level3下的所有请求,需要VIP3才可以访问 .antMatchers("/level3/**").hasAnyRole("VIP3"); } }
- 在MySecurityConfig的configure方法中加入
重启项目后,点击秘籍,如果没有权限,就会跳转到登录页面(此页面是自动生成的)//开启自动配置的登录功能 //1. /login来到登录页面 //2. 登录失败会重定向到/login?error表示登录失败 //3. 更多详情规定请自行点进去研究 http.formLogin();
登录失败会返回提示
- 在MySecurityConfig中重写configure(AuthenticationManagerBuilder auth),定义认证规则
重启项目,进行登录,成功访问(张三有全部权限,而李四只有VIP1权限,所以只能访问普通武功秘籍)//定义认证规则 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //进行演示,正确做法应该把数据放到数据库中 auth.inMemoryAuthentication() //设置BCryptPasswordEncoder密码编码器 .passwordEncoder(new BCryptPasswordEncoder()) //登录的用户名 .withUser("zhangsan") //登录的密码 .password(new BCryptPasswordEncoder().encode("123456")) //roles给予的权限 .roles("VIP1", "VIP2", "VIP3") //再加上一个用户 .and() .withUser("lisi") .password(new BCryptPasswordEncoder().encode("123456")).roles("VIP1"); }
- 在授权规则的configure方法里添加
重启项目,登录后进行注销//开启自动配置的注销功能 //1. 访问 /logout 表示用户注销,清空session //2. 注销成功后会返回 /login?logout 页面 //注销后默认到login页面,可以用logoutSuccessUrl设置注销后跳转的页面 http.logout().logoutSuccessUrl("/");
没加logoutSuccessUrl
加了logoutSuccessUrl(在首页点击注销后还是在首页)
实现动态显示页面
当没登录时,不显示注销按钮
当登录时,显示用户信息,注销按钮,权限相对应的秘籍
- 引入thymeleaf对Spring Security支持的依赖
<dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring4</artifactId> <version>3.0.11.RELEASE</version> </dependency>
- 修改页面上部分,显示用户信息
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/extras/spring-security"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <h1 align="center">欢迎光临武林秘籍管理系统</h1> <!--sec:authorize授权 isAuthenticated是否认证()--> <!--没认证的情况下--> <div sec:authorize="!isAuthenticated()"> <h2 align="center">游客您好,如果想查看武林秘籍 <a th:href="@{/login}">请登录</a></h2> </div> <!--认证了的情况下--> <div sec:authorize="isAuthenticated()"> <!-- sec:authentication="name"取出用户名 --> <!-- 显示用户信息 --> <h2><span sec:authentication="name"></span>,您好, <!-- sec:authentication="principal.authorities"获取所有的会员等级 --> 您的会员等级为:<span sec:authentication="principal.authorities"></span></h2> <!-- 显示注销按钮 --> <form th:action="@{/logout}" method="post"> <input type="submit" value="注销"> </form> </div> <hr>
- 修改下部分页面,显示相对应的秘籍
<!--判断是否有VIP1权限 hasRole为判断权限--> <div sec:authorize="hasRole('VIP1')"> <h3>普通武功秘籍</h3> <ul> <li><a th:href="@{/level1/1}">罗汉拳</a></li> <li><a th:href="@{/level1/2}">武当长拳</a></li> <li><a th:href="@{/level1/3}">全真剑法</a></li> </ul> </div> <div sec:authorize="hasRole('VIP1')"> <h3>高级武功秘籍</h3> <ul> <li><a th:href="@{/level2/1}">太极拳</a></li> <li><a th:href="@{/level2/2}">七伤拳</a></li> <li><a th:href="@{/level2/3}">梯云纵</a></li> </ul> </div> <div sec:authorize="hasRole('VIP1')"> <h3>绝世武功秘籍</h3> <ul> <li><a th:href="@{/level3/1}">葵花宝典</a></li> <li><a th:href="@{/level3/2}">龟派气功</a></li> <li><a th:href="@{/level3/3}">独孤九剑</a></li> </ul> </div> </body> </html>
开启记住我功能
MySecurityConfig定制请求的授权规则configure添加:
//开启自动配置的记住我功能
//登录成功后,将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录
//cookie保存时间为两周,但是点击注销会立即删除cookie
http.rememberMe();
登录页面就会有记住我功能
勾选后,当登录成功,springsecurity会给浏览器发送一个cookie,保存时间为两周,下次打开页面就会自动登录,点击注销后会删除cookie。
修改登录页面
把登录页面换成我们自己的登录页面
- 给
http.formLogin()
添加//usernameParameter("user")获取提交过来的表单中名为user的值作为username //passwordParameter("pwd")获取提交过来的表单中名为pwd的值作为password //loginPage("/userlogin")修改登录的页面 http.formLogin().usernameParameter("user").passwordParameter("pwd").loginPage("/userlogin");
- 把首页的
<a th:href="@{/login}">请登录</a>
中的/login
换成/userlogin
- 给pages目录下的登录页面的用户名和密码的name分别加上
user
和pwd
- 并把登录页面的from表单
action=""
改为th:action="@{/userlogin}"
(因为给/login发送post请求,是用来进行认证的,给/login发送get请求,是去登录的表单(即springsecurity默认的登录页面),但是如果是自定义的登录页面,就不是/login了,而是我们请求的地址,即/userlogin(可以使用.loginProcessingUrl("")
修改请求的地址),总结就是如果是没设置登录页面使用默认的,就给/login发送请求,如果自定义登录页面的,就给自定义的那个请求地址发送请求,如果先改变请求的地址,就用loginProcessingUrl来改)
- 在自定义的登录页面,密码下添加
<input type="checkbox" name="remeber">记住我<br>
- 然后给
http.rememberMe()
增加.rememberMeParameter("remeber")
获取提交过来表单中名为remeber的checkbox属性。
下一篇笔记:SpringBoot高级篇学习笔记(六、分布式,热部署和监控管理)
学习视频(p20-p30):https://www.bilibili.com/video/BV1KW411F7oX?p=20
更多推荐
所有评论(0)