十六.异步、定时、邮件发送
1.异步线程任务
异步处理还是非常常用的,比如我们在网站上发送邮件,如果不是开多线程用一个线程去处理发送邮件的操作,那么前端请求就会看到一直在转圈圈,直到发送邮件操作完成才响应。
这时就可以采用异步任务,给我们的发送邮件的方法标注@Async
注解,表时开启一个线程,这样controller方法执行完先响应,邮件发送线程处理时间我们不用等待。
1、创建一个类AsyncService
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况 。方法上标注@Async
注解,表示这是一个异步多线程任务
@Service
public class AsyncService {
@Async
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("你好,异步任务触发了");
}
}
2、在主启动类上标注 @EnableAsync
,开启异步任务注解功能
@EnableAsync //开启异步任务注解功能
@SpringBootApplication
public class Springboot02thymeleafApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot02thymeleafApplication.class, args);
}
}
3、编写controller方法,调用 asyncService.hello()方法,这时就有2个线程在跑,controller主线程处理请求先响应,同时hello()方法的线程处理业务
@Autowired
AsyncService asyncService;
@GetMapping(value = {"/", "/login"})
public String loginPage() {
asyncService.hello();
return "login";
}
访问http://localhost:8080/MrL/
,发现页面先给我们响应出来,3秒以后打印了hello方法处理的语句
也可以使用java基础阶段线程的实现方式,不过肯定哪个方便用哪个
2.定时任务
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口:1.TaskExecutor接口
2.TaskScheduler接口。两个注解:1.·@EnableScheduling
,2.@Scheduled
使用:
1、创建一个ScheduledService,其中有个hello方法需要定时执行,使用@Scheduled
注解表示该方法是定时方法,时间一到就会执行
@Service
public class ScheduledService {
//这里cron表达式表示。每隔3秒就会执行方法
@Scheduled(cron = "0/3 * * * * ?") //cron表达式,格式:秒 分 时 日 月 星期
public void hello(){
System.out.println("你触发了定时任务,当前时间:"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
2、在主启动类上增加 @EnableScheduling
注解,开启定时任务注解功能
@EnableScheduling //开启定时任务注解功能
@SpringBootApplication
public class Springboot02thymeleafApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot02thymeleafApplication.class, args);
}
}
3、启动项目,结果如下
cron常用的表达式
0/2 * * * * ?
:每2秒执行任务0 0/2 * * * ?
:每2分钟执行任务0 0 9 * * ?
:每天早上9点执行任务59 59 23 ? * MON-FRI
:周一到周五每天23:59:59执行任务0 0 9 ? * WED
:每个星期三早上9点执行任务0 0 9 15 * ?
:每月15日早上9点执行任务
3.邮件发送
邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持
邮件发送需要引入spring-boot-start-mail启动器,该启动器里有个MailSenderAutoConfiguration
自动配置类,自动注册了JavaMailSenderImpl bean组件(这个组件就是我们发送邮件需要使用的对象)。还有个MailProperties
类,绑定了配置文件,在application.yml中
1、pom.xml中导入spring-boot-starter-mail
启动器依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
可以看到其实就是这个启动器帮我们导入了实现邮件发送的jakarta.mail
依赖包
查看自动配置类:MailSenderAutoConfiguration
MailSenderJndiConfiguration配置类将JavaMailSenderImpl注册到了spring容器中
MailProperties类与配置文件绑定的前缀:spring.mail
2、在配置文件中配置邮件发送方的信息
spring.mail.username=1164623618@qq.com
spring.mail.password=你的qq授权码
spring.mail.host=smtp.qq.com
# qq邮件发送还需要配置ssl,其他邮箱不需要这一步
spring.mail.properties.mail.smtp.ssl.enable=true
如何获取授权码:QQ邮箱设置->账户->开启pop3和smtp服务
3、在Springboot的测试类中测试
@SpringBootTest
class Springboot02thymeleafApplicationTests {
@Autowired
BooksServiceImpl booksService;
//简单类型的邮件发送
@Test
void contextLoads() throws SQLException {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setSubject("你好,欢迎您注册英雄联盟手游成功!");
simpleMailMessage.setText("感谢您对英雄联盟手游的支持,我们赠送一个小礼包给你,请点击链接领取。");
simpleMailMessage.setFrom("1164623618@qq.com");//发送者
simpleMailMessage.setTo("1164623618@qq.com");//接收者
mailSender.send(simpleMailMessage);
}
//复杂类型的邮件发送
@Test
void contextLoads1() throws SQLException, MessagingException {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage,true,"utf-8"); //如果想要带附件加上true
mimeMessageHelper.setSubject("你好,欢迎您注册英雄联盟手游成功!");
mimeMessageHelper.setText("感谢您对英雄联盟手游的支持,我们赠送一个小礼包给你," +
"请点击 <a href='https://www.baidu.com/'>链接</a>领取。", true); //复杂类型的邮件支持html转义
mimeMessageHelper.addAttachment("研究内容.docx",
new File("C:\\Users\\秋天的思念\\Desktop\\研究内容.docx")); //复杂类型邮件支持附件
mimeMessageHelper.setFrom("1164623618@qq.com");//发送者
mimeMessageHelper.setTo("1164623618@qq.com");//接收者
mailSender.send(mimeMessage);
}
}
查看邮箱,邮件接收成功!
十七.Dubbo + Zookeeper
1.分布式系统概念
- 在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”
- 分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据
- 分布式系统(distributed system)是建立在网络之上的软件系统
- 首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题(比如网络不稳定造成多个计算机节点无法通信),为了解决这些问题又会引入更多的机制、协议,带来更多的问题
2 微服务架构问题及解决
- 这么多服务,客户护短如何去访问?答:通过API网关、服务路由等解决
- 这么多服务,服务与服务之间如何通信? 答:通过HTTP、RPC、异步调用等解决
- 这么多服务, 如何治理呢? 答:通过服务注册与发现、三高(高并发、高性能、高可用)等解决
- 这么多服务,如果某个服务挂了,怎么处理? 答:通过熔断机制,服务降级等解决
比如Dubbo+zookeeper这一套微服务架构解决系统:
- API没有,找第三方组件去实现
- Dubbo解决,本身就是高性能的RPC框架
- zookeeper解决,本身就是注册中心,能够管理服务
- 熔断机制没有,可以借助Hystrix框架等
对于今后继续学习,要基于这四个问题。话说回来,为什么要解决这些问题,其实本质就是由于网络的不可靠。但现在,5G的到来可能迎来微服务的巅峰
3.Dubbo
随着互联网的发展,网站应用的规模需求不断扩大,常规的垂直应用架构(如MVC架构)已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
Dubbo是一款高性能、轻量级的开源Java RPC框架
Dubbo提供了六大核心能力:
- 面向接口代理的高性能RPC调用
- 智能容错和负载均衡
- 服务自动注册和发现
- 高度可扩展能力
- 运行期流量调度
- 可视化的服务治理与运维
Dubbo官网文档:https://dubbo.apache.org/zh/docs/introduction/
- 服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务
- 服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
- 注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
- 监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明 - 服务容器负责启动,加载,运行服务提供者
- 服务提供者在启动时,向注册中心注册自己提供的服务功能
- 服务消费者在启动时,向注册中心查阅自己所需的服务功能
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
1、架构的演进
从上图可以获得4个架构的演进:
单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:性能扩展比较难、协同开发问题、不利于升级维护
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点:公用模块无法重复利用,开发性的浪费
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
3、RPC
RPC(Remote Procedure Call):是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节(即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同)
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数方法一样去调用远程函数方法
RPC基本原理
步骤解析:
RPC两个核心模块:通讯,Java类序列化
4.安装
1、Zookeeper 环境搭建
dubbo推荐使用Zookeeper
作为注册中心,关于dubbo注册中心部分的文档:
https://dubbo.apache.org/zh/docs/references/registry/zookeeper/
Window下安装zookeeper,地址 :https://zookeeper.apache.org/releases.html#download
下载后解压,添加zoo.cfg配置文件(将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可)
zoo.cfg文件中有2个重要信息:1、dataDir=./ 临时数据存储的目录(可写相对路径)
2、clientPort=2181(zookeeper的端口号)
运行zkServer.cmd ,zookeeper的服务就启动了
使用zkCli.cmd进行测试
输入指令:ls /
(列出zookeeper根下保存的所有节点)
输入指令create –e /liqingfeng 123
(创建一个liqingfeng 节点,值为123)
输入指令get /kuangshen
(获取liqingfeng节点的值),发现其实zookeeper存储就是按照key-value键值对 存储服务
我们再来查看一下节点
2、window下安装dubbo-admin
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。
但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin(可以用来查看在注册中心的服务提供者和服务消费者),不过这个监控即使不装也不影响使用。
从网上下载dubbo-admin-0.0.1-SNAPSHOT.jar
已经打包好的jar包,把它放在zookeeper项目文件里
打开jar里有个application.properties文件,主要配置内容如下(看出它也是springboot项目)
server.port=7001 #说明dubbo-admin可视化平台端口为7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root #用户名和密码是root
spring.guest.password=guest
dubbo.registry.address=zookeeper://127.0.0.1:2181 # 注册中心地址是指向的zookeeper端口地址
执行dubbo-admin-0.0.1-SNAPSHOT.jar (注意:zookeeper的服务一定要打开!)
执行完毕后访问:http://localhost:7001/
,这时需要输入登录账户和密码,默认都是root,登录成功后,查看界面(就可以在这里看我们服务注册情况,消费者使用服务情况等)
5.SpringBoot集成 Dubbo + zookeeper
1、启动zookeeper 双击zkServer.cmd
,并开启注册中心监控可视化页面(执行java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
,当然可以不开启,不会影响,开启只是为了可视化看到消费者和提供者信息)
2、服务提供者需要将服务注册到注册中心,服务消费者需要到注册中心找自己所需的服务,所以都需要springboot整合的dubbo启动器以及zookeeper相关依赖
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.8</version>
</dependency>
<!-- 引入zookeeper客户端依赖(除了注册中心,其他微服务应用都视为客户端) -->
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!-- 引入zookeeper服务依赖 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.3</version>
<!--排除这个slf4j-log4j12-->
<exclusions>
<!-- 防止zookeeper与springboot日志冲突,需要排除zookeeper日志依赖 -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
3、创建provider 服务提供者项目工程(模拟一台电脑的微服务应用),创建consumer 服务消费者项目工程(模拟另一台电脑的微服务应用),记得都添加web启动器(毕竟我们的微服务应用一般都是web应用)
4、在provider服务提供者项目工程中进行如下操作:
导入第二步的依赖
编写接口
public interface TicketService {
String getTicket();
}
编写实现类:标注 @DubboService
注解(表示将服务注册出去)
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
@Service
@DubboService
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket() {
return "《长津湖》";
}
}
在配置文件中配置dubbo相关属性
#项目端口
server.port=8081
#当前项目应用名
dubbo.application.name=provider
#注册中心地址,这里是本机的zookeeper册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#扫描指定包下服务注册到注册中心
dubbo.scan.base-packages=com.liqingfeng.service
应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它注册在指定的注册中心中!
5、在consumer服务消费者项目工程中进行如下操作:
导入第二步的依赖
将服务提供者的TicketService接口拿过来且和服务提供者全类名一致。(在以后的开发中骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,全类名必须保证和服务提供者一致)
public interface TicketService {
String getTicket();
}
编写一个service模拟业务操作去获取服务提供者的ticketService.getTicket()方法,标注 @DubboReference
注解,远程调用服务提供者在注册中心注册的服务(这里即获得getTicket()方法)
@Service
public class UserService {
@DubboReference
TicketService ticketService;
public void buyTicket(){
System.out.println("我买到了一张"+ticketService.getTicket()+"电影票");
}
}
配置参数
#项目端口
server.port=8082
#当前项目应用名
dubbo.application.name=consumer
#注册中心地址,这里是本机的zookeeper册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
在springboot的测试类中进行测试(看是服务消费者是否能拿到服务提供者在注册中心注册的getTicket()方法)
@SpringBootTest
class ConsumerApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
userService.buyTicket();
}
}
启动测试,(发现虽然我们服务消费者没有实现TicketService 接口,也能获得getTicket方法返回的结果,说明是从远程调用服务(getTicket方法)返回的结果:《长津湖》
这就是SpingBoot 集成dubbo + zookeeper实现分布式开发的应用,其实最重要的是服务拆分的思想
十八.富文本编辑器
1.概述
思考:我们平时在博客园,或者优快云等平台进行写作的时候,有同学思考过他们的编辑器是怎么实现的吗?
在博客园后台的选项设置中,可以看到一个文本编辑器的选项:
其实这个就是富文本编辑器,市面上有许多非常成熟的富文本编辑器,比如:
- Editor.md——功能非常丰富的编辑器,左端编辑,右端预览,非常方便,完全免费。官网:https://pandao.github.io/editor.md/
- wangEditor——基于javascript和css开发的 Web富文本编辑器,
轻量、简洁、界面美观、易用、开源免费。官网:http://www.wangeditor.com/ - TinyMCE——TinyMCE是一个轻量级的基于浏览器的所见即所得编辑器,由JavaScript写成。它对IE6+和Firefox1.5+都有着非常良好的支持。功能齐全,界面美观,就是文档是英文的,对开发人员英文水平有一定要求。官网:https://www.tiny.cloud/docs/demo/full-featured/
- 百度ueditor——UEditor是由百度web前端研发部开发所见即所得富文本web编辑器,具有轻量,功能齐全,可定制,注重用户体验等特点,开源基于MIT协议,允许自由使用和修改代码,缺点是已经没有更新了。官网:https://ueditor.baidu.com/website/onlinedemo.html
- kindeditor——界面经典。官网:http://kindeditor.net/demo.php
- Textbox——Textbox是一款极简但功能强大的在线文本编辑器,支持桌面设备和移动设备。主要功能包含内置的图像处理和存储、文件拖放、拼写检查和自动更正。此外,该工具还实现了屏幕阅读器等辅助技术,并符合WAI-ARIA可访问性标准。官网:https://textbox.io/
2.Editor.md富文本编辑器
推荐使用的就是Editor.md,作为一个资深码农,Mardown必然是我们程序猿最喜欢的文档格式
我们可以在官网下载它:https://pandao.github.io/editor.md/ ,得到它的压缩包!
解压以后,在examples目录下面,可以看到他的很多案例使用。
3.使用
项目结构
1、数据库建表
CREATE TABLE `article` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'int文章的唯一ID',
`title` varchar(100) NOT NULL COMMENT '标题',
`author` varchar(50) NOT NULL COMMENT '作者',
`content` longtext NOT NULL COMMENT '文章的内容',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2、创建springboot项目,所有依赖如下
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--在工作中,很多情况下我们打包是不想执行测试用例的(测试影响数据库数据),所以跳过测试-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!--跳过项目运行测试用例-->
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
3、实体类:
//文章类
public class Article implements Serializable {
private int id; //文章的唯一ID
private String title; //标题
private String author; //作者名
private String content; //文章的内容
//get、set、tosring
}
4、ArticleMapper 接口和mapper.xml sql映射文件
@Mapper
@Repository
public interface ArticleMapper {
int insertArticle(Article article); //增加一篇文章
Article getArticleById(int id); //通过id获得指定文章
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liqingfeng.mapper.ArticleMapper">
<insert id="insertArticle" parameterType="Article">
insert into article (title, author, content) values(#{title}, #{author}, #{content});
</insert>
<select id="getArticleById" resultType="Article" parameterType="int">
select * from article where id = #{id};
</select>
</mapper>
5、在配置文件中进行配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/ssmbuild?useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
druid:
aop-patterns: com.liqingfeng.pojo.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)
stat-view-servlet: # 配置监控页
enabled: true
login-username: admin #监控页面登录账号
login-password: 123456 #监控页面登录密码
web-stat-filter: # 监控web
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat: # 对上面filters里面的stat的详细配置
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
# 这里的配置主要是用来上传文件映射来使用的,这个location就是我们的存放图像的目录。当然这里还是要看你的数据库图片路径是如何设置的。
# 大家仔细的话,我在static目录下有个upload目录,这个目录就是我存放上传图片的目录。但是我们在location里面只到了 static这层,因此我的数据库中存放的便是 "/upload/xxxx.png"。拼接在一起就刚好。
# 图片的绝对路径:F:\JAVA\SpringBoot-MarkDown\src\main\resources\static\upload\0deeac80-6071-45e7-a1f4-d0107173a077.jpg
servlet:
multipart:
location: C:/Users/秋天的思念/Desktop/Java/springboot/springboot06-editor/src/main/resources/static
mybatis:
type-aliases-package: com.liqingfeng.pojo #类别名包扫描
configuration:
map-underscore-to-camel-case: true #驼峰名命开启
到这里先测试mapper能正常访问再进行下一步
6、ArticleService 和ArticleServiceImpl编写
public interface ArticleService {
boolean publishArticle(Article article);
Article getArticleById(int id);
}
@Service
public class ArticleServiceImpl implements ArticleService{
@Autowired
ArticleMapper articleMapper;
@Override
public boolean publishArticle(Article article) {
int res = articleMapper.insertArticle(article);
if(res > 0) {
return true;
}
return false;
}
@Override
public Article getArticleById(int id) {
return articleMapper.getArticleById(id);
}
}
7、因为Controller层会用到一个工具类FileUtil,所以编写FileUtils
/**
* 文件上传工具类
*/
public class FileUtils {
// static目录下的upload目录可自己建,也可不建。因为在上传的时候,会判断是否存在,若不存在便自动创建
private static final String prePath = System.getProperty("user.dir") + "/src/main/resources/static/upload/";
/**
* 上传文件
* @param file
* @return 返回文件路径(以相对路径放回)
*/
public static String uploadFile(MultipartFile file) {
if(file.isEmpty()) {
return "";
}
// 获取原文件名
String originFileName = file.getOriginalFilename();
// 我们通过UUID 来重新重组文件名
String uid = UUID.randomUUID().toString();
assert originFileName != null;
String suffix = originFileName.substring(originFileName.lastIndexOf('.') + 1);
String path = prePath + uid + "." + suffix;
String returnPath = "/upload/" + uid + "." + suffix;
File newFile = new File(path);
if(newFile.getParentFile() != null && !newFile.getParentFile().exists()) {
System.out.println("创建目录ing");
// 上面的 newFile.getParentFile() 已经保证了不为null.
if(newFile.getParentFile().mkdirs()) {
System.out.println("创建目录成功");
}else {
System.out.println("创建目录失败");
return "";
}
}
try {
file.transferTo(newFile);
} catch (IOException e) {
e.printStackTrace();
return "";
}
return returnPath;
}
}
8、ArticleController、MarkDownController编写
@Controller
public class ArticleController {
@Autowired
ArticleServiceImpl articleService;
@PostMapping("/article/publish")
@ResponseBody
public String publishArticle(Article article) {
System.out.println(article.getContent());
System.out.println(article.getAuthor());
System.out.println(article.getTitle());
boolean res = articleService.publishArticle(article);
if(res) {
return "success";
}
return "false";
}
@RequestMapping("/article/image/upload")
@ResponseBody
// 注意RequestParam中的name,不可改。
public JSONObject imageUpload(@RequestParam("editormd-image-file") MultipartFile image) {
JSONObject jsonObject = new JSONObject();
if(image != null) {
String path = FileUtils.uploadFile(image);
System.out.println(path);
jsonObject.put("url", path);
jsonObject.put("success", 1);
jsonObject.put("message", "upload success!");
return jsonObject;
}
jsonObject.put("success", 0);
jsonObject.put("message", "upload error!");
return jsonObject;
}
@GetMapping("/article/get/{id}")
public ModelAndView getArticleById(@PathVariable(name = "id")int id, ModelAndView modelAndView) {
Article article = articleService.getArticleById(id);
modelAndView.setViewName("article");
if(article == null) {
modelAndView.addObject("article", new Article());
}
modelAndView.addObject("article", article);
return modelAndView;
}
}
@Controller
public class MarkDownController {
// 这个接口,主要是进行跳转页面的。
@RequestMapping("/markdown/edit")
public String edit() {
return "edit";
}
}
9、前端页面
因为本人是后端研发,所以前端这边,花的时间挺久的,同样也挺丑的。哈哈哈,大家见谅。
首先就是我们的一个静态资源。
css : 我们需要将我们下载好的editormd解压后editor.md-master文件夹中的examples目录中的css文件夹中的style.css 拷贝到我们的 static/css/examples/style.css,以及editor.md-master文件夹下的 editormd.css 拷贝到我们的 static/css目录。以上的目录放在哪里,可以根据自己的个性以及想法,但是一定得保证访问的到,你如果觉得有问题,就跟我同样的目录。
js : 我们需要将我们下载好的editormd解压后editor.md-master文件夹中的examples目录中的js文件夹中的所有js文件 拷贝到我们的 static/css/examples/目录下,以及editor.md-master文件夹下的 editormd.min.js 拷贝到我们的 static/js目录。
fonts : 这个目录很重要,如果没有这个目录,Markdown的编辑工具栏的icon无法显示。需要将editor.md-master文件夹中的fonts文件夹直接拷贝到static文件夹中。
lib : 将editor.md-master文件夹中的lib文件拷贝到static文件夹中即可。
plugins : 将editor.md-master文件夹中的plugin文件拷贝到static文件夹中即可。
10、article.html、edit.html编写
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>文章</title>
<link rel="stylesheet" th:href="@{/css/style.css}" />
<link rel="stylesheet" th:href="@{/css/editormd.css}" />
<link rel="shortcut icon" href="https://pandao.github.io/editor.md/favicon.ico" type="image/x-icon" />
</head>
<body>
<div id="layout">
<header>
<h1 th:text="${article.title}"></h1>
<h2 th:text="${article.author}"></h2>
</header>
<div id="test-editormd">
<textarea style="display:none;" th:text="${article.content}"></textarea>
</div>
</div>
<!--一个JS文件都不能少,少一个便无法渲染。注意静态资源路径问题-->
<script th:src="@{/js/jquery.min.js}"></script>
<script th:src="@{/lib/marked.min.js}"></script>
<script th:src="@{/lib/prettify.min.js}"></script>
<script th:src="@{/lib/raphael.min.js}"></script>
<script th:src="@{/lib/underscore.min.js}"></script>
<script th:src="@{/lib/sequence-diagram.min.js}"></script>
<script th:src="@{/lib/flowchart.min.js}"></script>
<script th:src="@{/lib/jquery.flowchart.min.js}"></script>
<script th:src="@{/js/editormd.min.js}"></script>
<script type="text/javascript">
var testEditor;
$(function () {
testEditor = editormd.markdownToHTML("test-editormd", {
width: "90%",
height: 700,
path: "../lib/",
preview: true,
watch: true,
editor: false,
})
})
</script>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>Simple example - Editor.md examples</title>
<link rel="stylesheet" th:href="@{/css/style.css}" />
<link rel="stylesheet" th:href="@{/css/editormd.css}" />
<link rel="shortcut icon" href="https://pandao.github.io/editor.md/favicon.ico" type="image/x-icon" />
</head>
<body>
<div id="layout">
<header>
<h1>Simple example</h1>
</header>
<form name="mdEditorForm">
标题:<input type="text" name="title"><br>
作者:<input type="text" name="author">
<div id="test-editormd">
<textarea style="display:none;" name="content"></textarea>
</div>
</form>
</div>
<script th:src="@{/js/jquery.min.js}"></script>
<script th:src="@{/js/editormd.min.js}"></script>
<script type="text/javascript">
var testEditor;
$(function() {
testEditor = editormd("test-editormd", {
width : "90%",
height : 640,
syncScrolling : "single",
path : "../lib/",
// 表示支持上传图片
imageUpload : true,
imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
// 上传图片的请求接口
imageUploadURL : "/article/image/upload",
// 工具栏图标的设置,大家可以自定义。比如 publish就是我定义的。
toolbarIcons : function () {
return ["undo","redo","|","bold","del","italic","quote","ucwords","uppercase","lowercase","|","h1","h2","h3","h4","h5","h6","|","list-ul","list-ol","hr","|","link","reference-link","image","code","preformatted-text","code-block","table","datetime","emoji","html-entities","pagebreak","|","goto-line","watch","preview","fullscreen","clear","search","|","help","info", "||", "publish"];
},
// 自定义图标后,定义图标对应的文字
toolbarIconTexts: {
publish: "<span bgcolor='gray'>发布</span>"
},
// 自定义图标的触发
toolbarHandlers : {
publish: function (cm, icon, cursor, selection) {
mdEditorForm.method = "post";
mdEditorForm.action = "/article/publish";//提交至服务器的路径
mdEditorForm.submit();
}
}
});
/*
// or
testEditor = editormd({
id : "test-editormd",
width : "90%",
height : 640,
path : "../lib/"
});
*/
});
</script>
</body>
</html>
11、测试,访问url http://localhost:8080/markdown/edit
,编辑内容后,点击发布
可以看到数据库表中已保存该内容
访问url http://localhost:8080//article/get/12
,可以查看编辑的文章
十九、集成Spring Security
1.Spring Security概述
- 在 Web中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。 市面上存在比较有名的:Shiro,Spring Security
- Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,它实际上是保护基于spring的应用程序的标准。Spring Security安全性的强大之处在于它可以轻松地扩展以满足定制需求,即横向拓展(AOP思想)
- 没有安全框架之前,要实现用户登录验证,权限控制,只能有过滤器(或则拦截器)不过代码相对于使用框架的代码会显得非常的繁琐,冗余。怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种
- Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
2.使用
1、新建一个springboot项目
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2、导入静态资源
3、controller编写
@Controller
public class RouterController {
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "views/login";
}
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") int id){
return "views/level1/"+id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") int id){
return "views/level2/"+id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") int id){
return "views/level3/"+id;
}
认识SpringSecurity
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
WebSecurityConfigurerAdapter:自定义Security策略
AuthenticationManagerBuilder:自定义认证策略
@EnableWebSecurity:开启WebSecurity模式
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
认证和授权
目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能
1、引入 Spring Security 模块
org.springframework.boot spring-boot-starter-security 2、编写 Spring Security 配置类参考官网:https://spring.io/projects/spring-security
查看我们自己项目中的版本,找到对应的帮助文档:
https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5 #servlet-applications 8.16.4
图片
3、编写基础配置类
package com.kuang.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity // 开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
4、定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 定制请求的授权规则
// 首页所有人可以访问
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/").hasRole(“vip1”)
.antMatchers("/level2/").hasRole(“vip2”)
.antMatchers("/level3/**").hasRole(“vip3”);
}
5、测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!
6、在configure()方法中加入以下配置,开启自动配置的登录功能!
// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin();
7、测试一下:发现,没有权限的时候,会跳转到登录的页面!
图片
8、查看刚才登录页的注释信息;
我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿…
auth.inMemoryAuthentication()
.withUser(“kuangshen”).password(“123456”).roles(“vip2”,“vip3”)
.and()
.withUser(“root”).password(“123456”).roles(“vip1”,“vip2”,“vip3”)
.and()
.withUser(“guest”).password(“123456”).roles(“vip1”,“vip2”);
}
9、测试,我们可以使用这些账号登录进行测试!发现会报错!
There is no PasswordEncoder mapped for the id “null”
图片
10、原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿…
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser(“kuangshen”).password(new BCryptPasswordEncoder().encode(“123456”)).roles(“vip2”,“vip3”)
.and()
.withUser(“root”).password(new BCryptPasswordEncoder().encode(“123456”)).roles(“vip1”,“vip2”,“vip3”)
.and()
.withUser(“guest”).password(new BCryptPasswordEncoder().encode(“123456”)).roles(“vip1”,“vip2”);
}
11、测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!搞定
权限控制和注销
1、开启自动配置的注销的功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//…
//开启自动配置的注销的功能
// /logout 注销请求
http.logout();
}
2、我们在前端,增加一个注销的按钮,index.html 导航栏中
4、但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?
// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");
5、测试,注销完毕后,发现跳转到首页OK
6、我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如kuangshen这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?
我们需要结合thymeleaf中的一些功能
sec:authorize=“isAuthenticated()”:是否认证登录!来显示不同的页面
Maven依赖:
org.thymeleaf.extras thymeleaf-extras-springsecurity5 3.0.4.RELEASE 7、修改我们的 前端页面导入命名空间
xmlns:sec=“http://www.thymeleaf.org/thymeleaf-extras-springsecurity5”
修改导航栏,增加认证判断
9、如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
10、我们继续将下面的角色功能块认证完成!
Level 1
Level 2
Level 3
12、权限控制和注销搞定!
记住我
现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单
1、开启记住我功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//。。。。。。。。。。。
//记住我
http.rememberMe();
}
2、我们再次启动项目测试一下,发现登录页多了一个记住我功能,我们登录之后关闭 浏览器,然后重新打开浏览器访问,发现用户依旧存在!
思考:如何实现的呢?其实非常简单
我们可以查看浏览器的cookie
图片
3、我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie
图片4、结论:登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie,具体的原理我们在JavaWeb阶段都讲过了,这里就不在多说了!
定制登录页
现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?
1、在刚才的登录页配置后面指定 loginpage
http.formLogin().loginPage("/toLogin");
2、然后前端也需要指向我们自己定义的 login请求
在 loginPage()源码中的注释上有写明:
图片
http.formLogin()
.usernameParameter(“username”)
.passwordParameter(“password”)
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
5、在登录页增加记住我的多选框
记住我
6、后端验证处理!
//定制记住我的参数!
http.rememberMe().rememberMeParameter(“remember”);
7、测试,OK
完整配置代码
package com.kuang.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//开启自动配置的登录功能:如果没有权限,就会跳转到登录页面!
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
//开启自动配置的注销的功能
// /logout 注销请求
// .logoutSuccessUrl("/"); 注销成功来到首页
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
//记住我
http.rememberMe().rememberMeParameter("remember");
}
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿…
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}
}