SpringBoot-2.0学习笔记
1-SpringBoot2.0入门
1.1-SpringBoot2.x依赖环境和版本新特性
简介:讲解新版本依赖环境和springboot2新特性概述
1、依赖版本jdk8以上, Springboot2.x用JDK8, 因为底层是 Spring framework5
2、安装maven最新版本,maven3.2以上版本,下载地址 :https://maven.apache.org/download.cgi
3、Eclipse或者IDE
4、新特性
5、翻译工具:https://translate.google.cn/
6、springbootGitHub地址:https://github.com/spring-projects/spring-boot
7、springboot官方文档:https://spring.io/guides/gs/spring-boot/
1.2-快速创建SpringBoot2.x应用之手工创建web应用
简介:使用Maven手工创建SpringBoot2.x应用
官方推荐包命名接口,不要使用默认 defaultPackage
- 例子:com +- example +- myapplication +- Application.java | +- customer | +- Customer.java | +- CustomerController.java | +- CustomerService.java | +- CustomerRepository.java | +- order +- Order.java +- OrderController.java +- OrderService.java +- OrderRepository.java
1.3-快速创建SpringBoot2.x应用之工具类自动创建web应用
简介:使用构建工具自动生成项目基本架构 工具自动创建:http://start.spring.io/
1.4-SpringBoot2.x的依赖默认Maven版本
简介:讲解SpringBoot2.x的默认Maven依赖版本
2-SpringBoot接口Http协议开发实战
2.1-SpringBoot2.xHTTP请求配置讲解
简介:SpringBoot2.xHTTP请求注解讲解和简化注解配置技巧
- @RestController and @RequestMapping是springMVC的注解,不是springboot特有的
- @RestController = @Controller+@ResponseBody
- @SpringBootApplication = @Configuration+@EnableAutoConfiguration+@ComponentScanlocalhost:8080
2.2-开发必备工具PostMan接口工具介绍和使用
用户在开发或者调试网络程序或者是网页B/S模式的程序的时候是需要一些方法来跟踪网页请求的,用户可以使用一些网络的监视工具比如著名的Firebug等网页调试工具。今天给大家介绍的这款网页调试工具不仅可以调试简单的css、html、脚本等简单的网页基本信息,它还可以发送几乎所有类型的HTTP请求!Postman在发送网络HTTP请求方面可以说是Chrome插件类产品中的代表产品之一。
简介:模拟Http接口测试工具PostMan安装和讲解
- 接口调试工具安装和基本使用
- 下载地址:https://www.getpostman.com/
2.3-HTTP接口GET请求实战
简介:讲解springboot接口,http的get请求,各个注解使用
GET请求
- 1、单一参数@RequestMapping(path = “/{id}”, method = RequestMethod.GET)
public String getUser( String id ){} |
|
|
Restful协议的GET请求
@GetMapping注解
public Object pageUser(int from, int size) {
params.clear();
params.put("from", from);
params.put("size", size);
return params;
}参数设置默认值
public Object pageUser2(int from, int size) {
params.clear();
params.put("from", from);
params.put("size", size);
return params;
}获取http的头部
public Object getHeader( String accessToken, String id){
params.clear();
params.put("access_token", accessToken);
params.put("id", id);
return params;
}使用HttpServletRequest
public Object testRequest(HttpServletRequest request) {
params.clear();
String id = request.getParameter("id");
params.put("id", id);
return params;
}
2.4-HTTP其他提交方法请求实战
简介:讲解http请求post,put, delete提交方式
2.4.1-Post请求
|
2.4.2-Put请求
|
2.4.3-Delete请求
|
2.5-Jackson使用
简介:介绍常用json框架和注解的使用,自定义返回json结构和格式
- 常用框架 阿里 fastjson,谷歌gson等
- JavaBean序列化为Json,
- 性能:Jackson > FastJson > Gson > Json-lib 同个结构
- Jackson、FastJson、Gson类库各有优点,各有自己的专长
- 空间换时间,时间换空间
- jackson处理相关自动
- 指定字段不返回:@JsonIgnore
- 指定日期格式:@JsonFormat(pattern=”yyyy-MM-dd hh:mm:ss”,locale=”zh”,timezone=”GMT+8”)
- 空字段不返回:@JsonInclude(Include.NON_NUll)
- 指定别名:@JsonProperty
2.6-SpringBoot目录文件结构
简介:讲解SpringBoot目录文件结构和官方推荐的目录规范
目录讲解
- src/main/java:存放代码
- src/main/resources
- static: 存放静态文件,比如 css、js、image, (访问方式 http://localhost:8080/js/main.js)
- templates:存放静态页面jsp,html,tpl
- config:存放配置文件,application.properties
- resources:
引入依赖 Thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
//注意:如果不引人这个依赖包,html文件应该放在默认加载文件夹里面,
//比如resources、static、public这个几个文件夹,才可以访问同个文件的加载顺序,静态资源文件 Spring Boot 默认会挨个从
- META/resources >
- resources >
- static >
- public
里面找是否存在相应的资源,如果有则直接返回。
默认配置
- 官网地址:https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-spring-mvc-static-content
- spring.resources.static-locations = classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
静态资源文件存储在CDN
2.7-SpringBoot文件上传实战
简介:讲解HTML页面文件上传和后端处理实战
springboot文件上传 MultipartFile file,源自SpringMVC
- 静态页面直接访问:localhost:8080/index.html
- 注意点:如果想要直接访问html页面,则需要把html放在springboot默认加载的文件夹下面
- MultipartFile 对象的transferTo方法,用于文件保存(效率和操作比原先用FileOutStream方便和高效)
访问路径 http://localhost:8080/images/39020dbb-9253-41b9-8ff9-403309ff3f19.jpeg
- 静态页面直接访问:localhost:8080/index.html
2.8-jar包方式运行web项目文件上传和访问
简介:讲解SpingBoot2.x使用 java -jar运行方式的图片上传和访问处理
文件大小配置,启动类里面配置
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//单个文件最大
factory.setMaxFileSize("10240KB"); //KB,MB
// 设置总上传数据总大小
factory.setMaxRequestSize("1024000KB");
return factory.createMultipartConfig();
}打包成jar包,需要增加maven依赖
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
如果没加相关依赖,执行maven打包,运行后会报错:no main manifest attribute, in XXX.jar
GUI:反编译工具,作用就是用于把class文件转换成java文件文件上传和访问需要指定磁盘路径
application.properties中增加下面配置
web.images-path=/Users/jack/Desktop
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/test/,file:${web.upload-path}文件服务器:fastdfs,阿里云oss,nginx搭建一个简单的文件服务器等
2.9-构建RESTful API与单元测试
首先,回顾并详细说明一下在快速入门中使用的@Controller
、@RestController
、@RequestMapping
注解。如果您对Spring MVC不熟悉并且还没有尝试过快速入门案例,建议先看一下快速入门的内容。
@Controller
:修饰class,用来创建处理http请求的对象@RestController
:Spring4之后加入的注解,原来在@Controller
中返回json需要@ResponseBody
来配合,如果直接用@RestController
替代@Controller
就不需要再配置@ResponseBody
,默认返回json格式@RequestMapping
:配置url映射。现在更多的也会直接用以Http Method直接关联的映射注解来定义,比如:GetMapping
、PostMapping
、DeleteMapping
、PutMapping
等
下面我们通过使用Spring MVC来实现一组对User对象操作的RESTful API,配合注释详细说明在Spring MVC中如何映射HTTP请求、如何传参、如何编写单元测试。
RESTful API具体设计如下:

|
单元测试:
|
2.10-使用Swagger2构建强大的API文档
随着前后端分离架构和微服务架构的流行,我们使用Spring Boot来构建RESTful API项目的场景越来越多。通常我们的一个RESTful API就有可能要服务于多个不同的开发人员或开发团队:IOS开发、Android开发、Web开发甚至其他的后端服务等。为了减少与其他团队平时开发期间的频繁沟通成本,传统做法就是创建一份RESTful API文档来记录所有接口细节,然而这样的做法有以下几个问题:
- 由于接口众多,并且细节复杂(需要考虑不同的HTTP请求类型、HTTP头部信息、HTTP请求内容等),高质量地创建这份文档本身就是件非常吃力的事,下游的抱怨声不绝于耳。
- 随着时间推移,不断修改接口实现的时候都必须同步修改接口文档,而文档与代码又处于两个不同的媒介,除非有严格的管理机制,不然很容易导致不一致现象。
为了解决上面这样的问题,本文将介绍RESTful API的重磅好伙伴Swagger2,它可以轻松的整合到Spring Boot中,并与Spring MVC程序配合组织出强大RESTful API文档。它既可以减少我们创建文档的工作量,同时说明内容又整合入实现代码中,让维护文档和修改代码整合为一体,可以让我们在修改代码逻辑的同时方便的修改文档说明。另外Swagger2也提供了强大的页面测试功能来调试每个RESTful API。具体效果如下图所示:
添加swagger-spring-boot-starter依赖
在
pom.xml
中加入依赖,具体如下:<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.9.0.RELEASE</version>
</dependency>应用主类中添加
@EnableSwagger2Doc
注解,具体如下
public class Chapter22Application {
public static void main(String[] args) {
SpringApplication.run(Chapter22Application.class, args);
}
}application.properties
中配置文档相关内容,比如swagger.title=spring-boot-starter-swagger
swagger.description=Starter for swagger 2.x
swagger.version=1.4.0.RELEASE
swagger.license=Apache License, Version 2.0
swagger.licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.html
swagger.termsOfServiceUrl=https://github.com/dyc87112/spring-boot-starter-swagger
swagger.contact.name=didi
swagger.contact.url=http://blog.didispace.com
swagger.contact.email=dyc87112@qq.com
swagger.base-package=com.didispace
swagger.base-path=/**各参数配置含义如下:
swagger.title
:标题swagger.description
:描述swagger.version
:版本swagger.license
:许可证swagger.licenseUrl
:许可证URLswagger.termsOfServiceUrl
:服务条款URLswagger.contact.name
:维护人swagger.contact.url
:维护人URLswagger.contact.email
:维护人emailswagger.base-package
:swagger扫描的基础包,默认:全扫描swagger.base-path
:需要处理的基础URL规则,默认:/**
更多配置说明可见官方说明:https://github.com/SpringForAll/spring-boot-starter-swagger
启动应用,访问:
http://localhost:8080/swagger-ui.html
,就可以看到如下的接口文档页面:添加文档内容
在整合完Swagger之后,在
http://localhost:8080/swagger-ui.html
页面中可以看到,关于各个接口的描述还都是英文或遵循代码定义的名称产生的。这些内容对用户并不友好,所以我们需要自己增加一些说明来丰富文档内容。如下所示,我们通过@Api
,@ApiOperation
注解来给API增加说明、通过@ApiImplicitParam
、@ApiModel
、@ApiModelProperty
注解来给参数增加说明。
// 通过这里配置使下面的映射都在/users下
public class UserController {
// 创建线程安全的Map,模拟users信息的存储
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());
public List<User> getUserList() {
List<User> r = new ArrayList<>(users.values());
return r;
}
public String postUser( User user){
users.put(user.getId(), user);
return "success";
}
public User getUser( Long id){
return users.get(id);
}
public String putUser( Long id, User user){
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}
public String deleteUser( Long id){
users.remove(id);
return "success";
}
}
public class User {
private Long id;
private String name;
private Integer age;
}完成上述代码添加后,启动Spring Boot程序,访问:
http://localhost:8080/swagger-ui.html
,就能看到下面这样带中文说明的文档了(其中标出了各个注解与文档元素的对应关系以供参考):API文档访问与调试
在上图请求的页面中,我们看到user的Value是个输入框?是的,Swagger除了查看接口功能外,还提供了调试测试功能,我们可以点击上图中右侧的Model Schema(黄色区域:它指明了User的数据结构),此时Value中就有了user对象的模板,我们只需要稍适修改,点击下方“Try it out!”按钮,即可完成了一次请求调用!此时,你也可以通过几个GET请求来验证之前的POST请求是否正确。
相比为这些接口编写文档的工作,我们增加的配置内容是非常少而且精简的,对于原有代码的侵入也在忍受范围之内。因此,在构建RESTful API的同时,加入Swagger来对API文档进行管理,是个不错的选择。
2.11-JSR-303实现请求参数校验
请求参数的校验是很多新手开发非常容易犯错,或存在较多改进点的常见场景。比较常见的问题主要表现在以下几个方面:
- 仅依靠前端框架解决参数校验,缺失服务端的校验。这种情况常见于需要同时开发前后端的时候,虽然程序的正常使用不会有问题,但是开发者忽略了非正常操作。比如绕过前端程序,直接模拟客户端请求,这时候就会突然在前端预设的各种限制,直击各种数据访问接口,使得我们的系统存在安全隐患。
- 大量地使用
if/else
语句嵌套实现,校验逻辑晦涩难通,不利于长期维护。
所以,针对上面的问题,建议服务端开发在实现接口的时候,对于请求参数必须要有服务端校验以保障数据安全与稳定的系统运行。同时,对于参数的校验实现需要足够优雅,要满足逻辑易读、易维护的基本特点。
接下来,我们就在本篇教程中详细说说,如何优雅地实现Spring Boot服务端的请求参数校验。
JSR-303
在开始动手实践之前,我们先了解一下接下来我们将使用的一项标准规范:JSR-303
什么是JSR?
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
JSR-303定义的是什么标准?
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
Bean Validation中内置的constraint
Hibernate Validator附加的constraint
快速入门
我们先来做一个简单的例子,比如:定义字段不能为Null
。只需要两步
第一步:在要校验的字段上添加上@NotNull
注解,具体如下:
|
第二步:在需要校验的参数实体前添加@Valid
注解,具体如下:
|
完成上面配置之后,启动应用,并用POST请求访问localhost:8080/users/
接口,body使用一个空对象,{}
。你可以用Postman等测试工具发起,也可以使用curl发起,比如这样:
curl -X POST \ |
不出意外,你可以得到如下结果:
{ |
其中返回内容的各参数含义如下:
timestamp
:请求时间status
:HTTP返回的状态码,这里返回400,即:请求无效、错误的请求,通常参数校验不通过均为400error
:HTTP返回的错误描述,这里对应的就是400状态的错误描述:Bad Requesterrors
:具体错误原因,是一个数组类型;因为错误校验可能存在多个字段的错误,比如这里因为定义了两个参数不能为Null
,所以存在两条错误记录信息message
:概要错误消息,返回内容中很容易可以知道,这里的错误原因是对user对象的校验失败,其中错误数量为2
,而具体的错误信息就定义在上面的errors
数组中path
:请求路径
请求的调用端在拿到这个规范化的错误信息之后,就可以方便的解析并作出对应的措施以完成自己的业务逻辑了。
尝试一些其他校验
在完成了上面的例子之后,我们还可以增加一些校验规则,比如:校验字符串的长度、校验数字的大小、校验字符串格式是否为邮箱等。下面我们就来定义一些复杂的校验定义,比如:
|
发起一个可以出发name
、age
、email
都校验不通过的请求,比如下面这样:
curl -X POST \ |
我们将得到如下的错误返回:
{ |
从errors
数组中的各个错误明细中,知道各个字段的defaultMessage
,可以看到很清晰的错误描述。
3-SpringBoot热部署devtool和配置文件自动注入实战
3.1-使用Dev-tool热部署
简介:介绍什么是热部署,使用springboot结合dev-tool工具,快速加载启动应用
什么是热部署?
在应用运行的时升级软件,无需重新启动的方式有两种,
热部署
和热加载
。对于Java应用程序来说,
热部署
就是在服务器运行时重新部署项目,热加载
即在在运行时重新加载class,从而升级应用。热加载
的实现原理主要依赖java的类加载机制,在实现方式可以概括为在容器启动的时候起一条后台线程,定时的检测类文件的时间戳变化,如果类的时间戳变掉了,则将类重新载入。对比反射机制,反射是在运行时获取类信息,通过动态的调用来改变程序行为; 热加载则是在运行时通过重新加载改变类信息,直接改变程序行为。
热部署
原理类似,但它是直接重新加载整个应用,这种方式会释放内存,比热加载更加干净彻底,但同时也更费时间。
添加依赖
- ```xml
org.springframework.boot spring-boot-devtools true
- 添加配置
- ```properties
#热部署
spring.devtools.restart.enabled=true
spring.devtools.restart.additional-paths=src/main/java
#关闭缓存,及时刷新
#spring.thymeleaf.cache=false
#排除无需热部署目录
#spring.devtools.restart.exclude=static/**,public/**
#srping.devtools.restart.exclude=WEB-INF/**
- ```xml
IDEA配置
不被热部署的文件 :
1、/META-INF/maven, /META-INF/resources, /resources, /static, /public, or /templates
2、指定文件不进行热部署 spring.devtools.restart.exclude=static/** ,public/**
3、手工触发重启
spring.devtools.restart.trigger-file=trigger.txt
改代码不重启,通过一个文本去控制注意点
:生产环境不要开启这个功能,如果用java -jar启动,springBoot是不会进行热部署的
3.2-SpringBoot配置文件
简介:SpringBoot2.x常见的配置文件 xml、yml、properties的区别和使用
- xml、properties、json、yaml
- 常见的配置文件 xx.yml, xx.properties,
- 1)YAML(Yet Another Markup Language) 写 YAML 要比写 XML 快得多(无需关注标签或引号) 使用空格 Space 缩进表示分层,不同层次之间的缩进可以使用不同的空格数目 注意:key后面的冒号,后面一定要跟一个空格,树状结构 application.properties示例 server.port=8090
server.session-timeout=30
server.tomcat.max-threads=0
server.tomcat.uri-encoding=UTF-8
- 1)YAML(Yet Another Markup Language) 写 YAML 要比写 XML 快得多(无需关注标签或引号) 使用空格 Space 缩进表示分层,不同层次之间的缩进可以使用不同的空格数目 注意:key后面的冒号,后面一定要跟一个空格,树状结构 application.properties示例 server.port=8090
- application.yml示例 server:
port: 8090
session-timeout: 30
tomcat.max-threads: 0
tomcat.uri-encoding: UTF-8 - 默认示例文件仅作为指导。 不要将整个内容复制并粘贴到您的应用程序中,只挑选您需要的属性。
- 参考:https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/#common-application-properties
- 如果需要修改,直接复制对应的配置文件,加到application.properties里面
3.3-SpringBoot注解配置文件自动映射到属性和实体类
简介:讲解使用@value注解配置文件自动映射到属性和实体类
1、配置文件加载
方式一
1、Controller上面配置
@PropertySource({"classpath:resource.properties"})
2、增加属性
private String name;
方式二:实体类配置文件
1、添加 @Component 注解;
2、使用 @PropertySource 注解指定配置文件位置;
3、使用 @ConfigurationProperties 注解,设置相关属性;
4、必须 通过注入IOC对象Resource 进来 , 才能在类中使用获取的配置文件值。
@Autowired private ServerSettings serverSettings
;
例子
1.配置类
public class ServerSettings {
//应用名称
private String name;
//域名地址
private String domain;
}2.配置文件application.properties
#配置文件注入
springboot =
www.fangpeng.com =3.注入测试
public class TestController {
private ServerSettings serverSettings;
public Object testProperties() {
return serverSettings;
}
}
常见问题:
1、配置文件注入失败,Could not resolve placeholder
解决:根据springboot启动流程,会有自动扫描包没有扫描到相关注解,
默认Spring框架实现会从声明package进行扫描,来自动注入, 所在的类的
因此启动类最好放在根路径下面,或者指定扫描包范围
spring-boot扫描启动类对应的目录和子目录
2、注入bean的方式,属性名称和配置文件里面的key一一对应,就用加 这个注解
如果不一样,就要加
第二种方式
//配置文件的属性前缀
public class ServerSettings {
private String name;
private String domain;
}
4-Springboot单元测试进阶实战和自定义异常处理
4.1-SpringBootTest单元测试实战
简介:讲解SpringBoot的单元测试
1、引入相关依赖
|
2、编写单元测试用例
//启动整个springboot工程 |
3、Junit5断言
- assertAll:断言所有提供的可执行文件都不会抛出异常。若提供的标题(heading),其将包含在MultipleFailuresError的消息字符串中。
- assertArrayEquals:断言期望的和实际的XX类型数组是相等的。若失败,将显示提供的失败消息。
- assertDoesNotThrow:虽然从测试方法抛出的任何异常都会导致测试失败,但在某些用例中,显式断言测试方法中的给定代码块不会抛出异常会很有用。若提供的标题(heading),其将包含在MultipleFailuresError的消息字符串中。
- assertEquals:断言预期和实际是相等的。如有必要,将从提供的messageSupplier中懒惰地检索失败消息。
- assertFalse:断言提供的条件不是真。失败并显示提供的失败消息。
- assertIterableEquals:断言预期和实际的迭代是完全相同的。类似于检查assertArrayEquals(Object [],Object [],String)中的完全相等,如果遇到两个迭代(包括期望和实际),则它们的迭代器必须以相同的顺序返回相等的元素。注意:这意味着迭代器不需要是同一类型。
- assertNotNull:断言提供的条件不为null。
- assertNotSame:断言预期和实际不会引用同一个对象。
- assertNull:断言提供的实际为null。
- assertSame:断言预期和实际引用同一个对象。
- assertThrows:断言所提供的可执行代码块的执行会引发expectedType的异常并返回异常。如果没有抛出异常,或者抛出了不同类型的异常,则此方法将失败。如果不想对异常实例执行其他检查,只需忽略返回值。
- assertTimeout:断言在超出给定超时之前,所提供的可执行代码块的执行完成。注意:可执行代码块将在与调用代码相同的线程中执行。因此,如果超过超时,则不会抢先中止执行可执行代码块。
- assertTimeoutPreemptively:断言在超出给定超时之前,所提供的可执行代码块的执行完成。注意:可执行代码块将在与调用代码不同的线程中执行。此外,如果超过超时,则可抢占地执行可执行代码块。
- assertTrue:断言提供的条件为true。
- fail:使用给定的失败消息以及根本原因进行测试失败。泛型返回类型V允许此方法直接用作单语句lambda表达式,从而避免需要实现具有显式返回值的代码块。 由于此方法在其return语句之前抛出AssertionFailedError,因此该方法实际上永远不会向其调用者返回值。
4.2-SpringBoot测试之MockMvc讲解
简介: 讲解MockMvc类的使用和模拟Http请求实战
- 增加类注解
@AutoConfigureMockMvc
和@SpringBootTest
- 相关API
- perform:执行一个RequestBuilder请求
- andExpect:添加ResultMatcher->MockMvcResultMatchers验证规则 andReturn:最后返回相应的MvcResult->Response
- andReturn:最后返回相应的MvcResult->Response
4.2.1-什么是Mock?
在面向对象的程序设计中,模拟对象(英语:mock object
)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。
4.2.2-为什么使用Mock对象?
使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。
在以下情况可以采用模拟对象来替代真实对象:
- 真实对象的行为是不确定的(例如,当前的时间或温度);
- 真实对象很难搭建起来;
- 真实对象的行为很难触发(例如,网络错误);
- 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
- 真实的对象是用户界面,或包括用户界面在内;
- 真实的对象使用了回调机制;
- 真实对象可能还不存在;
- 真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。
注:使用Mockito一般分三个步骤:
- 模拟测试类所需的外部依赖;
- 执行测试代码;
- 判断执行结果是否达到预期;
4.2.3-MockMvc
MockMvc
是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
接口MockMvcBuilder
,提供一个唯一的build
方法,用来构造MockMvc。
主要有两个实现:StandaloneMockMvcBuilder
和DefaultMockMvcBuilder
,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。
4.2.4-在SpringBoot中使用
- 引入依赖(jar包),创建SpringBoot项目中默认引入的spring-boot-starter-test间接引入了spring-test,因此无需再额外引入jar包。
<dependency> |
- 创建Controller类并编写相关方法
public class TestController { |
编写测试类。实例化MockMvc有两种形式,一种是使用StandaloneMockMvcBuilder,另外一种是使用DefaultMockMvcBuilder。
测试类及初始化MockMvc初始化:
public class HelloWorldTest {
private MockMvc mockMvc;
private WebApplicationContext webApplicationContext;
public void setup() {
// 实例化方式一
mockMvc = MockMvcBuilders.standaloneSetup(new HelloWorldController()).build();
// 实例化方式二
// mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}单元测试方法
public class MockMvcTestDemo {
private MockMvc mockMvc;
public void apiTest() throws Exception {
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/test/mockmvc"))
.andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
int status = mvcResult.getResponse().getStatus();
System.out.println(status);
}
public void testHello() throws Exception {
/*
* 1、mockMvc.perform执行一个请求。
* 2、MockMvcRequestBuilders.get("XXX")构造一个请求。
* 3、ResultActions.param添加请求传值
* 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
* 5、ResultActions.andExpect添加执行完成后的断言。
* 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
* 比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
* 7、ResultActions.andReturn表示执行完成后返回相应的结果。
*/
mockMvc.perform(MockMvcRequestBuilders
.get("/hello")
// 设置返回值类型为utf-8,否则默认为ISO-8859-1
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
.param("name", "Tom"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("Hello Tom!"))
.andDo(MockMvcResultHandlers.print());
}
}
4.3-SpringBoot个性化启动banner设置和debug日志
简介:自定义应用启动的趣味性日志图标和查看调试日志
java -jar xxx.jar --debug |
输出denbug日志
修改application.properties配置文件
debug =
指定路径
debug =修改logback-spring.xml配置文件
增加
<logger name="com.XXX.XXX.mapper" level="DEBUG"></logger>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="/test/log" />
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- show parameters for hibernate sql 专为 Hibernate 定制 -->
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" />
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="DEBUG" />
<logger name="org.hibernate.SQL" level="DEBUG" />
<logger name="org.hibernate.engine.QueryParameters" level="DEBUG" />
<logger name="org.hibernate.engine.query.HQLQueryPlan" level="DEBUG" />
<!--myibatis log configure-->
<logger name="com.apache.ibatis" level="TRACE"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
4.4-SpringBoot配置全局异常
4.4.1-模拟全局异常
编写异常代码段
public Object index() {
int i = 1/0;
return new User("11", "123456", "1", 18, new Date());
}返回结果如下
4.4.2-异常注解介绍
@ControllerAdvice
顾名思义,这是一个增强的 Controller。需要配合@ExceptionHandler使用,
当将异常抛到controller时,可以对异常进行统一处理,规定返回的json格式或是跳转到一个错误页面。
使用这个 Controller ,可以实现三个方面的功能:
- 全局异常处理
- 全局数据绑定
- 全局数据预处理
如果是返回json数据 则用 RestControllerAdvice
,就可以不加 @ResponseBody
//捕获全局异常,处理所有不可知的异常@ExceptionHandler(value=Exception.class)
注解用来指明异常的处理类型
4.4.3-处理全局异常
|
返回结果如下:
4.4.4-处理自定义异常
自定义异常类
//自定义异常类
public class MyException extends RuntimeException {
private String code;
private String msg;
public MyException(String code, String msg){
super(msg);
this.code = code;
this.msg = msg;
}
public MyException(String msg){
super(msg);
this.msg = msg;
}
}处理自定义异常
//处理自定义异常
public Object handleMyException(MyException e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("code", 100);
map.put("msg", e.getMsg());
map.put("url", request.getRequestURL());
return map;
}返回自定义页面
//处理自定义异常
public Object handleMyExceptio(Exception e, HttpServletRequest request) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error.html");
modelAndView.addObject("msg", e.getMessage());
return modelAndView;
}
5-SpringBoot部署war项目到tomcat9和启动原理
5.1-SpringBoot启动方式和部署war项目到tomcat9
IDE启动
jar包方式启动
//maven插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>如果没有加,则执行jar包 ,报错如下
java -jar spring-boot-demo-0.0.1-SNAPSHOT.jar
no main manifest attribute, in spring-boot-demo-0.0.1-SNAPSHOT.jarjar包项目结构
example.jar
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-BOOT-INF
+-classes
| +-mycompany
| +-project
| +-YourClasses.class
+-lib
+-dependency1.jar
+-dependency2.jarwar包方式启动
在pom.xml中将打包形式
jar
修改为war
war 构建项目名称
springboot_demo tocmat下载和安装 https://tomcat.apache.org/download-90.cgi
修改启动类
public class DemoApplication extends SpringBootServletInitializer {
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DemoApplication.class);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(DemoApplication.class, args);
}
}打包项目,启动tomcat
5.2-SpringBoot启动原理
6-SpringBoot拦截器和 Servlet3.0自定义Filter、Listener
6.1-SpringBoot过滤器和Servlet3.0配置过滤器
- SpringBoot启动默认加载的Filter
characterEncodingFilter |
- Filter优先级
Ordered.HIGHEST_PRECEDENCE
Ordered.LOWEST_PRECEDENCE
低位值意味着更高的优先级 Higher values are interpreted as lower priority
自定义Filter,避免和默认的Filter优先级一样,不然会冲突
注册Filter的bean FilterRegistrationBean
同模块里面有相关默认Filter
web->servlet->filter
- 自定义Filter
- 使用Servlet3.0的注解进行配置
- 启动类里面增加 @ServletComponentScan,进行扫描
- 新建一个Filter类,implements Filter,并实现对应的接口
@WebFilter
标记一个类为filter,被spring进行扫描urlPatterns
:拦截规则,支持正则- 控制chain.doFilter的方法的调用,来实现是否通过放行
不放行,web应用resp.sendRedirect(“/index.html”);
场景:权限控制、用户登录(非前端后端分离场景)等
|
6.2-Servlet3.0注解自定义原生Servlet
|
6.3-自定义监听器Listener
常用的监听器
servletContextListener
、httpSessionListener
、servletRequestListener
Servlet注解自定义监听器
public class RequestListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
// TODO Auto-generated method stub
System.out.println("======requestDestroyed========");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("======requestInitialized========");
}
public class CustomContextListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
System.out.println("======contextInitialized======");
}
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("contextDestroyed");
}
}
6.4-自定义拦截器Interceptor
@Configuration
- 继承WebMvcConfigurationAdapter(SpringBoot2.X之前旧版本)
- SpringBoot2.X 新版本配置拦截器 implements
WebMvcConfigurer
自定义拦截器 HandlerInterceptor
- preHandle:调用Controller某个方法之前
- postHandle:Controller之后调用,视图渲染之前,如果控制器Controller出现了异常,则不会执行此方法
- afterCompletion:不管有没有异常,这个afterCompletion都会被调用,用于资源清理
按照注册顺序进行拦截,先注册,先被拦截
拦截器不生效常见问题:
- 是否有加@Configuration
- 拦截路径是否有问题
**
和*
- 拦截器最后路径一定要 “/**”, 如果是目录的话则是 ”/ */“
Filter
是基于函数回调 doFilter(),而Interceptor则是基于AOP思想
Filter在只在Servlet前后起作用,而Interceptor够深入到方法前后、异常抛出前后等依赖于Servlet容器即web应用中,而Interceptor不依赖于Servlet容器所以可以运行在多种环境。
在接口调用的生命周期里,Interceptor可以被多次调用,而Filter只能在容器初始化时调用一次。
Filter和Interceptor的执行顺序:
过滤前->拦截前->action执行->拦截后->过滤后
public class LoginInterceptor implements HandlerInterceptor { |
|
7-数据库操作之整合Mybatis和事务
7.1-SpringBoot持久化数据方式
原始java访问数据库JDBC(开发流程会很麻烦)
注册驱动/加载驱动——
Class.forName("com.mysql.jdbc.Driver")
建立连接
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/dbname","root","root");
创建Statement
执行SQL语句
处理结果集
关闭连接,释放资源
apache dbutils框架
比JDBC简单点
官网:https://commons.apache.org/proper/commons-dbutils/jpa框架
spring-data-jpa
jpa在复杂查询的时候性能不是很好Hiberante
解释:ORM:对象关系映射Object Relational Mapping
企业大都喜欢使用hibernateMybatis框架
互联网行业通常使用mybatis
不提供对象和关系模型的直接映射,半ORM
7.2-SpringBoot2.x整合Mybatis3.x注解实战
使用starter, maven仓库地址:http://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter
加入依赖(可以用 http://start.spring.io/ 下载)
<!-- 引入starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
<scope>runtime</scope>
</dependency>
<!-- MySQL的JDBC驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 引入第三方数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>加入配置文件
#mybatis.type-aliases-package=com.fangpeng.base_project.domain
#可以自动识别
#spring.datasource.driver-class-name =com.mysql.jdbc.Driver
jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8 =
root =
password =
#如果不使用默认的数据源 (com.zaxxer.hikari.HikariDataSource)
com.alibaba.druid.pool.DruidDataSource =加载配置,注入到
sqlSessionFactory
等都是springBoot帮我们完成启动类增加mapper扫描
技巧:保存对象,获取数据库自增id开发mapper
参考语法 http://www.mybatis.org/mybatis-3/zh/java-api.htmlpublic interface UserMapper {
//推荐使用#{}取值,不要用${}取值,因为存在SQL注入风险
int insert(User user);
}开发service
public class UserServiceImpl implements UserService {
private UserMapper userMapper;
public int add(User user) {
userMapper.insert(user);
int id = user.getId();
return id;
}
}开发controller
public class UserController {
private UserService userService;
private UserMapper userMapper;
public Object add() {
User user = new User();
user.setAge(24);
user.setCreateTime(new Date());
user.setName("kobe");
user.setPhone("10086");
int id = userService.add(user);
return JsonData.buildSuccess(id);
}
}sql脚本
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL COMMENT '名称',
`phone` varchar(16) DEFAULT NULL COMMENT '用户手机号',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`age` int(4) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;相关资料:
http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/#Configuration
https://github.com/mybatis/spring-boot-starter/tree/master/mybatis-spring-boot-samples
整合问题集合:
https://my.oschina.net/hxflar1314520/blog/1800035
https://blog.csdn.net/tingxuetage/article/details/80179772
7.3-SpringBoot整合Mybatis实操和打印SQL语句
控制台打印sql语句
#增加打印sql语句,一般用于本地开发测试 `mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl`
增加mapper代码
List<User> getAll();
User findById(Long id);
void update(User user);
void delete(Long userId);增加API
public Object findAll(){
return JsonData.buildSuccess(userMapper.getAll());
}
public Object findById(long id){
return JsonData.buildSuccess(userMapper.findById(id));
}
public Object delById(long id){
userMapper.delete(id);
return JsonData.buildSuccess();
}
public Object update(String name,int id){
User user = new User();
user.setName(name);
user.setId(id);
userMapper.update(user);
return JsonData.buildSuccess();
}
7.4-事务介绍和常见的隔离级别,传播行为
什么是事务?
指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
简单的说,事务就是并发控制的单位,是用户定义的一个操作序列。
而一个逻辑工作单元要成为事务,就必须满足ACID属性。
A:原子性(Atomicity)事务中的操作要么都不做,要么就全做。
C:一致性(Consistency)
事务执行的结果必须是从数据库从一个一致性状态转换到另一个一致性状态。
I:隔离性(Isolation)
一个事务的执行不能被其他事务干扰
D:持久性(Durability)
一个事务一旦提交,它对数据库中数据的改变就应该是永久性的
事务的隔离级别
读未提交(
Read Uncommitted
):保证了读取过程中不会读取到非法数据 引发脏读(读取了未提交的数据)
读已提交(
Read Committed
)这是大多数数据库系统默认的隔离级别,但不是MySQL默认的 只能看见已经提交事务所做的改变 引发不可重复读,不可重读读意味着我们同一事务执行完全相同的select语句时可能看到不一样的结果。 ——>导致这种情况的原因可能有:(1)有一个交叉的事务有新的commit,导致了数据的改变;(2)一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit 多个commit提交时,只读一次出现结果不一致
可重复读(
Repeatable Read
):保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据 这是MySQL的默认事务隔离级别
它确保同一事务的多个实例在并发读取数据时,看到同样的数据行
此级别可能出现的问题–幻读(Phantom Read),当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题可串行化(
Serializable
):最严格,串行处理,消耗资源大 这是最高的隔离级别
它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它在每个读的数据行上加上共享锁。
可能导致大量的超时现象和锁竞争
常见的传播行为
PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务,最常见的选择。
PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。
两个事务之间没有关系,一个异常,一个提交,不会同时回滚
PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常
7.5-SpringBoot整合mybatis之事务处理实战
- service逻辑引入事务
|
8-SpringBoot整合Redis
8.1-分布式缓存Redis介绍
REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。
Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
redis官网 https://redis.io/download
新手入门redis在线测试工具:http://try.redis.io/
8.2-源码编译安装Redis4.x
快速安装 https://redis.io/download#installation
wget http://download.redis.io/releases/redis-4.0.9.tar.gz tar xzf redis-4.0.9.tar.gz cd redis-4.0.9 make
启动服务端:src/redis-server
启动客户端:src/redis-cli默认是本地访问的,需要开放外网访问
1)打开redis.conf文件在NETWORK部分修改 注释掉bind 127.0.0.1可以使所有的ip访问redis 修改 protected-mode,值改为no
8.3-SpringBoot整合redis实战
官网:https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/#boot-features-redis
集群文档:https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#clusterspringboot整合redis相关依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>相关配置文件配置
#=========redis基础配置=========
0 =
127.0.0.1 =
6379 =
# 连接超时时间 单位 ms(毫秒)
3000 =
#=========redis线程池设置=========
# 连接池中的最大空闲连接,默认值也是8。
200 =
#连接池中的最小空闲连接,默认值也是0。
200 =
# 如果赋值为-1,则表示不限制;pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
2000 =
# 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时
1000 =常见redistemplate种类讲解和缓存实操(使用自动注入)
注入模板
private StirngRedisTemplate strTplRedis类型String,List,Hash,Set,ZSet
对应的方法分别是opsForValue()、opsForList()、opsForHash()、opsForSet()、opsForZSet()
8.5-Redis配置类和工具类
RedisTemplate的自动配置(源代码如下)
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}通过源码可以看出,SpringBoot自动帮我们在容器中生成了一个
RedisTemplate
和一个StringRedisTemplate
。但是,这个RedisTemplate的泛型是<Object,Object>,写代码不方便,需要写好多类型转换的代码;我们需要一个泛型为<String,Object>形式的RedisTemplate。并且,这个RedisTemplate没有设置数据存在Redis时,key及value的序列化方式。看到这个@ConditionalOnMissingBean注解后,就知道如果Spring容器中有了RedisTemplate对象了,这个自动配置的RedisTemplate不会实例化。因此我们可以直接自己写个配置类,配置RedisTemplate。
重新写一个Redis配置类
//Redis配置类
public class RedisConfig {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}封装Redis工具类
public final class RedisUtil {
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* å
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
9-SpringBoot整合定时任务和异步任务
9.1-SpringBoot定时任务schedule
常见定时任务
Java自带的java.util.Timer类
timer:配置比较麻烦,时间延后问题
timertask:不推荐Quartz框架
配置更简单
xml或者注解SpringBoot使用注解方式开启定时任务
- 启动类里面 @EnableScheduling开启定时任务,自动扫描
- 定时任务业务类 加注解 @Component被容器扫描
- 定时执行的方法加上注解 @Scheduled(fixedRate=2000) 定期执行一次
定时任务schedule
SpringBoot内置了定时任务Scheduled,能够很好的实现定时任务。
- 在SpringBoot应用添加
@EnableScheduling
注解启动定时任务
public class SpringbootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTestApplication.class, args);
}
}- 添加测试定时任务的代码
public class ScheduledTask {
public void scheduledTask1(){
System.out.println("定时任务1");
}
public void scheduledTask2(){
System.out.println("任务2执行时间:"+System.currentTimeMillis());
System.out.println("定时任务2");
try {
Thread.sleep(2*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务2结束时间:"+System.currentTimeMillis());
}
public void scheduledTask3(){
System.out.println("任务3执行时间:"+System.currentTimeMillis());
System.out.println("定时任务3");
try {
Thread.sleep(2*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务3结束时间:"+System.currentTimeMillis());
}
}注:
- corn表达式在linux使用广泛,具体可以参考cron表达式详解以及在线Cron表达式生成器
- initialDelay:启动后多久开始执行,单位时毫秒
- fixedRate:下次执行时间,任务开始运行的时候就计时
- fixedDelay:下次执行时间,fixedDelay等任务进行完了才开始计时,上一次执行结束时间点后xx秒再次执行
- fixedDelayString: 字符串形式,可以通过配置文件指定
9.2-SpringBoot异步任务
启动类里面使用@EnableAsync注解开启功能,自动扫描
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}定义异步任务类并使用@Component标记组件被容器扫描,异步方法加上@Async
注意点: 1)要把异步任务封装到类里面,不能直接写到Controller 2)增加Future<String> 返回结果 AsyncResult<String>("task执行完成"); 3)如果需要拿到结果 需要判断全部的 task.isDone()
@EnableAsync
表示支持异步任务,springboot对于异步,定时,缓存,切面等的配置都是通过在启动类上加 @EnableXXX来配置的。@Async
表示该方法会异步执行,也就是说主线程会直接跳过该方法,而是使用线程池中的线程来执行该方法。
public class AsyncTask {
public Future<String> execTaskA() throws InterruptedException {
System.out.println("TaskA开始");
long star = new Date().getTime();
Thread.sleep(5000);
long end = new Date().getTime();
System.out.println("TaskA结束,耗时毫秒数:" + (end - star));
return new AsyncResult<>("TaskA结束");
}
public Future<String> execTaskB() throws InterruptedException {
System.out.println("TaskB开始");
long star = new Date().getTime();
Thread.sleep(3000);
long end = new Date().getTime();
System.out.println("TaskB结束,耗时毫秒数:" + (end - star));
return new AsyncResult<>("TaskB结束");
}
public Future<String> execTaskC() throws InterruptedException {
System.out.println("TaskC开始");
long star = new Date().getTime();
Thread.sleep(4000);
long end = new Date().getTime();
System.out.println("TaskC结束,耗时毫秒数:" + (end - star));
return new AsyncResult<>("TaskC结束");
}
}通过注入方式,注入到controller里面,如果测试前后区别则改为同步则把Async注释掉
public class TaskController {
private AsyncTask asyncTask;
public void testTask() throws InterruptedException {
long start = new Date().getTime();
System.out.println("任务开始,当前时间" +star );
Future<String> taskA = asyncTask.execTaskA();
Future<String> taskB = asyncTask.execTaskB();
Future<String> taskC = asyncTask.execTaskC();
//间隔一秒轮询 直到 A B C 全部完成
while (true) {
if (taskA.isDone() && taskB.isDone() && taskC.isDone()) {
break;
}
Thread.sleep(1000);
}
long end = new Date().getTime();
System.out.println("任务结束,当前时间" + end);
System.out.println("总耗时:"+(end-start));
}
}
- 在SpringBoot应用添加
9.3-SpringBootz整合Quartz
Quartz是一款功能强大的任务调度器,可以实现较为复杂的调度功能,如每月一号执行、每天凌晨执行、每周五执行等等,还支持分布式调度。本文使用Springboot+Mybatis+Quartz实现对定时任务的增、删、改、查、启用、停用等功能。并把定时任务持久化到数据库以及支持集群。
Quartz的3个基本要素
- Scheduler:调度器。所有的调度都是由它控制。
- Trigger: 触发器。决定什么时候来执行任务。
- JobDetail & Job: JobDetail定义的是任务数据,而真正的执行逻辑是在Job中。使用JobDetail + Job而不是Job,这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail & Job 方式,sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。
引入依赖jar包
<!--QuartZ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>添加配置application-quartz.yml
spring:
#配置数据源
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/testquartz?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: password
quartz:
#持久化到数据库方式
job-store-type: jdbc
initialize-schema: embedded
properties:
org:
quartz:
scheduler:
instanceName: MyScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 10000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true实现
Job
接口并且在execute
方法中实现自己的业务逻辑public class HelloworldJob extends QuartzJobBean {
HelloworldService helloworldService;
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
helloworldService.printHelloWorld();
System.out.println("Hello world! :" + jobExecutionContext.getJobDetail().getKey());
}
}添加配置类
public class QuartzConfig {
public JobDetail myJobDetail(){
JobDetail jobDetail = JobBuilder.newJob(HiJob.class)
.withIdentity("myJob1","myJobGroup1")
//JobDataMap可以给任务execute传递参数
.usingJobData("job_param","job_param1")
.storeDurably()
.build();
return jobDetail;
}
public Trigger myTrigger(){
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(myJobDetail())
.withIdentity("myTrigger1","myTriggerGroup1")
.usingJobData("job_trigger_param","job_trigger_param1")
.startNow()
//.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
.withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ? 2018"))
.build();
return trigger;
}
}Quartz使用同一组数据库表作集群只需要配置相同的
instanceName
实例名称,以及设置org.quartz.jobStore.isClustered = true
启动两个节点后关闭其中正在跑任务的节点,另一个节点会自动检测继续运行定时任务
注:多任务的问题,多个JobDetail
使用同一个Trigger
报错:Trigger does not reference given job!
一个Job可以对应多个Trigger,但多个Job绑定一个Trigger报错。
10-Logback日志框架介绍和SpringBoot整合
10.1-新日志框架LogBack介绍
常用处理java的日志组件 slf4j,log4j,logback,common-logging 等
logback介绍:
基于Log4j基础上大量改良,不能单独使用,推荐配合日志框架SLF4J来使用
logback当前分成三个模块:logback-core,logback-classic和logback-access;
logback-core是其它两个模块的基础模块Logback的核心对象:
Logger:日志记录器 Appender:指定日志输出的目的地,目的地可以是控制台,文件 Layout:日志布局 格式化日志信息的输出
日志级别:DEBUG < INFO < WARN < ERROR
===================== =
### 设置###
debug,stdout,D,E =
### 输出信息到控制抬 ###
org.apache.log4j.ConsoleAppender =
System.out =
org.apache.log4j.PatternLayout =
[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n =
### 输出DEBUG 级别以上的日志到=D://logs/error.log ###
org.apache.log4j.DailyRollingFileAppender =
D://logs/log.log =
true =
DEBUG =
org.apache.log4j.PatternLayout =
%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n =
### 输出ERROR 级别以上的日志到=D://logs/error.log ###
org.apache.log4j.DailyRollingFileAppender =
E://logs/error.log =
true =
ERROR =
org.apache.log4j.PatternLayout =
%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n =Log4j日志转换为logback在线工具(支持log4j.properties转换为logback.xml,不支持 log4j.xml转换为logback.xml) https://logback.qos.ch/translator/
10.2-SpringBoot2.x日志讲解和Logback
分析SpringBoot启动日志
- 默认情况下,Spring Boot将日志输出到控制台
整合Logback实战
- 创建 日志文件logback-spring.xml,官方推荐 -spring.xml结尾
默认加载加载配置顺序 logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
- 创建 日志文件logback-spring.xml,官方推荐 -spring.xml结尾
注释:
<configuration> 子节点 <appender></appender> <logger></logger> <root></root>(要加在最后)
Logback.xml
|
11-搜索框架ElasticSearch介绍和整合SpringBoot
11.1-搜索知识
mysql:like 模糊,性能问题
solr:针对企业,Lucene
elasticsearch:
针对数据量特别大,PB,TB
纯java开发,springboot使用,5.6版本
es升级4->5版本,改动大,但是5版本后,改动不大
11.2-ElasticSearch介绍
Elasticsearch(ES)是一个基于Apache的开源索引库Lucene而构建的开源、分布式、具有RESTful接口的全文搜索引擎, 还是一个分布式文档数据库.
ES可以轻松扩展数以百计的服务器(水平扩展), 用于存储和处理数据. 它可以在很短的时间内存储、搜索和分析海量数据, 通常被作为复杂搜索场景下的核心引擎.
由于Lucene提供的API操作起来非常繁琐, 需要编写大量的代码, Elasticsearch对Lucene进行了封装与优化, 并提供了REST风格的操作接口, 开箱即用, 很大程度上方便了开发人员的使用.
- elasticSearch主要特点
- 特点:全文检索,结构化检索,数据统计、分析,接近实时处理,分布式搜索(可部署数百台服务器),处理PB级别的数据,搜索纠错,自动完成
- 使用场景:日志搜索,数据聚合,数据监控,报表统计分析
- 国内外使用者:维基百科,Stack Overflow,GitHub
11.3-SpringBoot整合ElasticSearch
- 添加依赖
<!--elasticsearch--> |
- 配置文件
spring: |
- 创建实体类bean
|
@Document注解
public Document {
String indexName();//索引库的名称,个人建议以项目的名称命名
String type() default "";//类型,个人建议以实体的名称命名
short shards() default 5;//默认分区数
short replicas() default 1;//每个分区默认的备份数
String refreshInterval() default "1s";//刷新间隔
String indexStoreType() default "fs";//索引文件存储类型
}@Document
作用于类上,经测试代码初始化时若es中没有对应的索引,则会在es中创建一个。@Field注解
public Field {
FieldType type() default FieldType.Auto;#自动检测属性的类型
FieldIndex index() default FieldIndex.analyzed;#默认情况下分词
DateFormat format() default DateFormat.none;
String pattern() default "";
boolean store() default false;#默认情况下不存储原文
String searchAnalyzer() default "";#指定字段搜索时使用的分词器
String indexAnalyzer() default "";#指定字段建立索引时指定的分词器
String[] ignoreFields() default {};#如果某个字段需要被忽略
boolean includeInParent() default false;
}@Field
作用于属性上,经测试该注解的属性有时会与现有的属性冲突,造成异常,错误信息如下,所以建议es中映射已建立的情况下,不要使用该注解。@Id
和@Version
分别用来绑定es中的_id
和_version
字段。
- 创建Repository(接口继承ElasticSearchRepository)
|
es的操作主要通过自定义的Repository对象完成,该对象可以通过继承模板接口ElasticsearchRepository
实现,该模板提供了save
、findById
、findAll
和search
等通用方法的实现,同时还支持通过规定的名称格式自定义操作方法.
- 使用
|
- Pageable对象
该对象可以帮助我们完成分页和排序操作,有手动和自动两种方式实现:
手动
Sort sort = new Sort(Sort.Direction.ASC,"name");
Pageable page = PageRequest.of(0,10,sort);自动
public Page<TestGoodsBo> search(String name, Pageable pageable)自动方式可以在request传参的同时就根据传入的参数来组装
Pageable
对象,同时还能使用@PageableDefault
注解设定默认值,因此更推荐使用。Spring支持的request参数如下:
page,第几页,从0开始,默认为第0页
size,每一页的大小,默认为20
sort,排序相关的信息,例如sort=firstname&sort=lastname,desc表示在按firstname正序排列基础上按lastname倒序排列
12-消息队列介绍和SpringBoot整合RockketMQ、ActiveMQ
12.1-JMS介绍和使用场景及基础编程模型
什么是JMS?
Java消息服务(Java Message Service),Java平台中关于面向消息中间件的接口
JMS是一种与厂商无关的 API,用来访问消息收发系统消息,它类似于JDBC(Java Database Connectivity)。这里,JDBC 是可以用来访问许多不同关系数据库的 API
使用场景
- 跨平台
- 多语言
- 多项目
- 解耦
- 分布式事务
- 流量控制
- 最终一致性
- RPC调用
概念
- JMS提供者:Apache ActiveMQ、RabbitMQ、Kafka、Notify、MetaQ、RocketMQ
- JMS生产者(Message Producer)
- JMS消费者(Message Consumer)
- JMS消息
- JMS队列
- JMS主题
JMS消息通常有两种类型:
点对点
、发布/订阅
编程模型
MQ中需要用的一些类
ConnectionFactory :连接工厂,JMS 用它创建连接 Connection :JMS 客户端到JMS Provider 的连接 Session: 一个发送或接收消息的线程 Destination :消息的目的地;消息发送给谁 MessageConsumer / MessageProducer: 消息接收者,消费者
12.2-ActiveMQ消息队列基础介绍
ActiveMQ是一种开源的基于JMS规范的一种消息中间件的实现,ActiveMQ的设计目标是提供标准的,面向消息的,能够跨越多语言和多系统的应用集成消息通信中间件。
特点:
- 支持来自Java,C,C ++,C#,Ruby,Perl,Python,PHP的各种跨语言客户端和协议
- 支持许多高级功能,如消息组,虚拟目标,通配符和复合目标
- 完全支持JMS 1.1和J2EE 1.4,支持瞬态,持久,事务和XA消息
- Spring支持,ActiveMQ可以轻松嵌入到Spring应用程序中,并使用Spring的XML配置机制进行配置
- 支持在流行的J2EE服务器(如TomEE,Geronimo,JBoss,GlassFish和WebLogic)中进行测试
- 使用JDBC和高性能日志支持非常快速的持久化
12.3-SpringBoot2整合ActiveMQ实战之点对点消息
添加依赖
<!-- 整合消息队列ActiveMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!-- 如果配置线程池则加入 -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
</dependency>配置文件
#整合jms测试,安装在别的机器,防火墙和端口号记得开放
tcp://127.0.0.1:61616 =
#集群配置
#spring.activemq.broker-url=failover:(tcp://localhost:61616,tcp://localhost:61617)
admin =
admin =
#下列配置要增加依赖
true =
100 =启动类添加@EnableJms注解
//启动消息队列
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}QueueConfig定义消息队列
import javax.jms.Queue;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class QueueConfig {
//定义存放消息的队列
public Queue queue() {
return new ActiveMQQueue("ActiveMQQueue");
}
}ProviderController测试
public class ProductController {
//注入存放消息的队列,用于下列方法一
private Queue queue;
//注入springboot封装的工具类
private JmsMessagingTemplate jmsMessagingTemplate;
public void send(String message) {
//方法一:添加消息到消息队列
jmsMessagingTemplate.convertAndSend(queue, message);
//方法二:这种方式不需要手动创建queue,系统会自行创建名为test的队列
//jmsMessagingTemplate.convertAndSend("test", message);
}
}消费者应用
application.properties 和 ConsumerApplication 同 provider类似,如下为不同的ActiveConsumer:
public class ActiveConsumer {
private JmsMessagingTemplate jmsMessagingTemplate;
// 使用JmsListener配置消费者监听的队列,其中message是接收到的消息
// SendTo 会将此方法返回的数据, 写入到 OutQueue 中去.
public String handleMessage(String message) {
System.out.println("成功接受message" + message);
return "成功接受message" + message;
}
}模拟请求
localhost:8080/send?msg=123
12.4-SpringBoot整合ActiveMQ实战之发布订阅模式
需要加入配置文件,支持发布订阅模型,默认只支持点对点
#default point to point
#默认消费者并不会消费订阅发布类型的消息,这是由于springboot默认采用的是p2p模式进行消息的监听
true =新建JMS配置类
public class JmsConfig {
public final static String TOPIC = "springboot.topic.test";
public final static String QUEUE = "springboot.queue.test";
public Topic topic() {
return new ActiveMQTopic(TOPIC);
}
public Queue queue() {
return new ActiveMQQueue(QUEUE);
}
// topic模式的ListenerContainer
public JmsListenerContainerFactory<?> jmsListenerContainerTopic(ConnectionFactory activeMQConnectionFactory) {
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
bean.setPubSubDomain(true);
bean.setConnectionFactory(activeMQConnectionFactory);
return bean;
}
// queue模式的ListenerContainer
public JmsListenerContainerFactory<?> jmsListenerContainerQueue(ConnectionFactory activeMQConnectionFactory) {
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
bean.setConnectionFactory(activeMQConnectionFactory);
return bean;
}
}生产者
public class Producer {
private JmsMessagingTemplate jmsTemplate;
// 发送消息,destination是发送到的队列,message是待发送的消息
public void sendMessage(Destination destination, final String message){
jmsTemplate.convertAndSend(destination, message);
}
}消费者类
public class Consumer {
private final static Logger logger = LoggerFactory.getLogger(JMSConsumer3.class);
public void onTopicMessage1(String msg) {
logger.info("接收到topic消息:{}",msg);
}
public void onTopicMessage2(String msg) {
logger.info("接收到topic消息:{}",msg);
}
public void onQueueMessage(String msg) {
logger.info("接收到queue消息:{}",text);
}
}测试
private Topic topic;
private Queue queue;
public void testJms() {
for (int i=0;i<10;i++) {
jmsProducer.sendMessage(queue,"queue,world!" + i);
jmsProducer.sendMessage(topic, "topic,world!" + i);
}
}
12.5-RocketMQ消息队列介绍
RocketMQ 是一款分布式、队列模型的消息中间件
特点:
- 在高压下1毫秒内响应延迟超过99.6%。
- 适合金融类业务,高可用性跟踪和审计功能。
- 支持发布订阅模型,和点对点
- 支持拉pull和推push两种消息模式
- 单一队列百万消息
- 支持单master节点,多master节点,多master多slave节点
概念:
- Producer:消息生产者
- Producer Group:消息生产者组,发送同类消息的一个消息生产组
- Consumer:消费者
- Consumer Group:消费同个消息的多个实例
- Tag:标签,子主题(二级分类),用于区分同一个主题下的不同业务的消息
- Topic:主题
- Message:消息
- Broker:MQ程序,接收生产的消息,提供给消费者消费的程序
- Name Server:给生产和消费者提供路由信息,提供轻量级的服务发现和路由
官网地址:http://rocketmq.apache.org/
12.6-Springboot2整合RocketMQ4.x实战
添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>配置文件
rocketmq:
# 生产者配置
producer:
isOnOff: on
# 发送同一类消息的设置为同一个group,保证唯一
groupName: FeePlatGroup
# 服务地址
namesrvAddr: 10.1.1.207:9876
# 消息最大长度 默认1024*4(4M)
maxMessageSize: 4096
# 发送消息超时时间,默认3000
sendMsgTimeout: 3000
# 发送消息失败重试次数,默认2
retryTimesWhenSendFailed: 2
# 消费者配置
consumer:
isOnOff: on
# 官方建议:确保同一组中的每个消费者订阅相同的主题。
groupName: FeePlatGroup
# 服务地址
namesrvAddr: 10.1.1.207:9876
# 接收该 Topic 下所有 Tag
topics: FangPlatTopic~*;
consumeThreadMin: 20
consumeThreadMax: 64
# 设置一次消费消息的条数,默认为1条
consumeMessageBatchMaxSize: 1
# 配置 Group Topic Tag
fang-plat:
fang-plat-group: FangPlatGroup
fang-plat-topic: FangPlatTopic
fang-account-tag: FangAccountTag生产者配置
//RocketMQ生产者配置
public class ProducerConfig {
private static final Logger LOG = LoggerFactory.getLogger(ProducerConfig.class) ;
private String groupName;
private String namesrvAddr;
private Integer maxMessageSize ;
private Integer sendMsgTimeout;
private Integer retryTimesWhenSendFailed;
public DefaultMQProducer getRocketMQProducer() {
DefaultMQProducer producer;
producer = new DefaultMQProducer(this.groupName);
producer.setNamesrvAddr(this.namesrvAddr);
//如果需要同一个jvm中不同的producer往不同的mq集群发送消息,需要设置不同的instanceName
if(this.maxMessageSize!=null){
producer.setMaxMessageSize(this.maxMessageSize);
}
if(this.sendMsgTimeout!=null){
producer.setSendMsgTimeout(this.sendMsgTimeout);
}
//如果发送消息失败,设置重试次数,默认为2次
if(this.retryTimesWhenSendFailed!=null){
producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
}
try {
producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
return producer;
}
}消费者配置
//消费者配置
public class ConsumerConfig {
private static final Logger LOG = LoggerFactory.getLogger(ConsumerConfig.class) ;
private String namesrvAddr;
private String groupName;
private int consumeThreadMin;
private int consumeThreadMax;
private String topics;
private int consumeMessageBatchMaxSize;
private RocketMsgListener msgListener;
public DefaultMQPushConsumer getRocketMQConsumer(){
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
consumer.setNamesrvAddr(namesrvAddr);
consumer.setConsumeThreadMin(consumeThreadMin);
consumer.setConsumeThreadMax(consumeThreadMax);
consumer.registerMessageListener(msgListener);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
try {
String[] topicTagsArr = topics.split(";");
for (String topicTags : topicTagsArr) {
String[] topicTag = topicTags.split("~");
consumer.subscribe(topicTag[0],topicTag[1]);
}
consumer.start();
}catch (MQClientException e){
e.printStackTrace();
}
return consumer;
}
}消费者监听配置
//消费监听配置
public class RocketMsgListener implements MessageListenerConcurrently {
private static final Logger LOGGER = LoggerFactory.getLogger(RocketMsgListener.class) ;
private ParamConfigService paramConfigService ;
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
if (CollectionUtils.isEmpty(list)){
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
MessageExt messageExt = list.get(0);
LOG.info("接受到的消息为:"+new String(messageExt.getBody()));
int reConsume = messageExt.getReconsumeTimes();
// 消息已经重试了3次,如果不需要再次消费,则返回成功
if(reConsume ==3){
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
if(messageExt.getTopic().equals(paramConfigService.feePlatTopic)){
String tags = messageExt.getTags() ;
switch (tags){
case "FeeAccountTag":
LOGGER.info("开户 tag == >>"+tags);
break ;
default:
LOGGER.info("未匹配到Tag == >>"+tags);
break;
}
}
// 消息消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}配置参数绑定
public class ParamConfigService {
public String fangPlatGroup ;
public String fangPlatTopic ;
public String fangAccountTag ;
}测试
public class FeePlatMqServiceImpl implements FeePlatMqService {
private DefaultMQProducer defaultMQProducer;
private ParamConfigService paramConfigService ;
public SendResult openAccountMsg(String msgInfo) {
// 可以不使用Config中的Group
defaultMQProducer.setProducerGroup(paramConfigService.fangPlatGroup);
SendResult sendResult = null;
try {
Message sendMsg = new Message(paramConfigService.fangPlatTopic,
paramConfigService.fangAccountTag,
"fang_open_account_key", msgInfo.getBytes());
sendResult = defaultMQProducer.send(sendMsg);
} catch (Exception e) {
e.printStackTrace();
}
return sendResult ;
}
}
13-SpringBoot多环境配置
13.1-SpringBoot多环境配置介绍
- 不同环境使用不同配置
例如数据库配置,在开发的时候,我们一般用开发数据库,而在生产环境的时候,我们是用正式的数据 - 配置文件存放路径
classpath
根目录的“/config”包下classpath
的根目录下 - spring boot允许通过命名约定按照一定的格式
(application-{profile}.properties)
来定义多个配置文件 spring.profiles.active=dev
来指定加载哪个环境的配置文件
14-SpringBoot2.0响应式编程(Webflux)
14.1-什么是reactive响应式编程(反应式编程)?
是一种异步编程范式,它关注数据流和变化的传播。这意味着可以通过使用编程语言轻松地表示静态(例如数组)和动态(例如事件发射器)数据流。
响应式编程是一种流行的编程方法,编写代码是基于对变化的反应。它的灵感来自于我们的日常生活,也即我们如何采取行动以及与他人沟通。
我们在执行日常生活活动时,我们会尽可能多任务,但大脑无法处理多任务,不管我们如何努力去做。我们人类实现多任务的唯一办法是在时间线上在任务之间切换。事实上,我们总是切换任务,即使我们没有意识到它。
例如,要执行一个任务:在星巴克喝一杯咖啡饮料,你需要发出一个命令,等待它准备好,然后接受你的饮料。当你在等待的时候,你很可能会找到别的事情做。这是最简单的执行任务的反应(响应)形式,你会在你等待来自咖啡师的“响应”时做别的事情,当你的咖啡已经准备好后,会叫你的名字时。
响应编程能够简化编程,它依赖于事件,代码运行的顺序不是代码行的顺序,而是和一个以上的事件有关,这些事件发生是以随着时间的推移的序列。我们把这一系列事件称为“流”。
何为事件?例如,你知道某个名人总是在发送有趣微博,每次他推发一条微博我们可以称之为一个“事件”。如果你看看这位名人微博系列,你会发现其实是一个随着时间的推移(一系列的事件)发生的一序列的“事件”,响应式编程就是因为我们得“响应”这些事件而得以命名。
- 依赖于事件,事件驱动(Event-driven)
- 一系列事件称为“流”
- 异步
- 非阻塞
- 观察者模式
例子
int A = B + C;
A被赋值为B和C的值。这时,如果我们改变B的值,A的值并不会随之改变。而如果我们运用一种机制,当B或者C的值发现变化的时候,A的值也随之改变,这样就实现了”响应式“。
14.2-SpringBoot2.x响应式编程webflux介绍
Spring WebFlux是Spring Framework 5.0中引入的新的反应式Web框架,与Spring MVC不同,它不需要Servlet API,完全异步和非阻塞,并 通过Reactor项目实现Reactive Streams规范。
传统的Servlet
servlet由servlet container进行生命周期管理。container启动时构造servlet对象并调用servlet init()进行初始化;container关闭时调用servlet destory()销毁servlet;container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。
缺点:
servlet是一个简单的网络编程模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的,但是一旦并发上升,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型就较为吃力。
例如:
spring webmvc是基于servlet之上的一个路由模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet),并由该servlet进行路由。所以spring webmvc无法摆脱servlet模型的弊端。
Webflux
Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的work线程中执行,而不阻塞的操作依然可以在Loop线程中进行处理,大大提高了Loop线程的利用率。
Spring WebFlux是Spring Framework 5.0中引入的新的反应式Web框架,与Spring MVC不同,它不需要Servlet API,完全异步和非阻塞,并 通过Reactor项目实现Reactive Streams规范。
响应式与非响应式区别:
Flux和Mono
简单业务而言:和其他普通对象差别不大,复杂请求业务,就可以提升性能
通俗理解:
Mono 表示的是包含 0 或者 1 个元素的异步序列
mono->单一对象 User 如: redis->用户ID->唯一的用户Mono<User>
Flux 表示的是包含 0 到 N 个元素的异步序列
flux->数组列表对象 List<User> 如: redis->男性用户->Flux<User>
Flux 和 Mono 之间可以进行转换
Spring WebFlux有两种风格:基于功能和基于注解的。基于注解非常接近Spring MVC模型
基于注解的方式
业务层Service:调用了ReactiveRedisTemplate对数据进行操作
public class ReactiveServiceImpl implements ReactiveService {
private ReactiveRedisTemplate reactiveRedisTemplate;
private static final String USER_KEY = "entity:user";
public Flux<User> findAll() {
return reactiveRedisTemplate.opsForHash().values(USER_KEY);
}
public Mono<User> queryByUUID(String name) {
return reactiveRedisTemplate.opsForHash().get(USER_KEY,name);
}
public Mono<Boolean> add(User user) {
String uuid = generateUUID();
user.setUuid(uuid);
return reactiveRedisTemplate.opsForHash().put(USER_KEY,user.getUuid(),user);
}
private String generateUUID() {
return UUID.randomUUID().toString().replace("-","");
}
public Mono<Boolean> update(User user) {
return reactiveRedisTemplate.opsForHash().put(USER_KEY,user.getUuid(),user);
}
public Mono<Boolean> delete(String uuid) {
return reactiveRedisTemplate.opsForHash().delete(uuid);
}
}Controller层:
public class ReactiveController {
private ReactiveService reactiveService;
// 查询所有
public Flux<User> findAll() {
return reactiveService.findAll();
}
// 查询单个
public Mono<User> queryByName( String uuid){
return reactiveService.queryByUUID(uuid);
}
// 添加用户
public Mono<Boolean> add( User user){
return reactiveService.add(user);
}
// 更新用户
public Mono<Boolean> update( User user){
return reactiveService.update(user);
}
// 删除用户
public Mono<Boolean> delete( String uuid){
return reactiveService.delete(uuid);
}
}和使用mvc没有任何的区别,唯一的区别在于返回的对象是
Mono
和Flux
,简单点理解,返回单个数据就是Mono
,多个就使用Flux
。启动项目可以看到实际上使用的是
Netty
服务器基于功能(函数式)
处理请求的类,实现具体的业务逻辑,接口
ServerRequest
表示的是一个 HTTP 请求体。通过ServerRequest 对象可获取到请求的相关信息,如请求路径、查询参数和请求内容等。方法 的返回值是一个 Mono对象。接口 ServerResponse
用来表示 HTTP 响应。ServerResponse 中包含了很多静态方法来创建不同 HTTP 状态码的响应对象。涉及几个比较重要的类如:
RouterFunction、HandlerFunction和DispatcherHandler
RouterFunction
就是一个路由函数,可以理解为将请求和具体的HandlerFunction
做一个映射;先创建
RouterFunction
public class UserFunctionRouter {
private UserHandler userHandler;
public RouterFunction router() {
RouterFunction<ServerResponse> routerFunction = route()
.GET("/user/find/all", accept(MediaType.APPLICATION_JSON_UTF8), userHandler::findAll)
.GET("/user/query/{uuid}", accept(MediaType.APPLICATION_JSON), userHandler::queryByName)
.POST("/user/add", accept(MediaType.APPLICATION_JSON_UTF8),userHandler::add)
.PUT("/user/update", accept(MediaType.APPLICATION_JSON_UTF8),userHandler::update)
.DELETE("/user/delete/{uuid}",accept(MediaType.APPLICATION_JSON_UTF8), userHandler::delete)
.build();
return routerFunction;
}
}将具体的请求路径和具体的handler做了映射,这样会根据用户具体的请求路径找具体的handler,其实就是具体的方法。和mvc的@RequestMapping功能上是一样的。但是这个需要注意的是返回的结果是ServerResponse,请求是ServerRequest,这个也可以和mvc的HttpServletRequest、HttpServletResponse对应起来,都是封装用户的请求信息,其实和mvc都还是能对应起来的,只是编程方式不太一样。
然后创建
HandlerFunction
public class UserHandler {
private UserRepository userRepository;
public Mono findAll(ServerRequest serverRequest) {
Flux<User> flux = userRepository.findAll();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(flux, User.class);
}
// 查询单个
public Mono queryByUUID(ServerRequest serverRequest) {
String uuid = serverRequest.pathVariable("uuid");
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userRepository.queryByUUID(uuid),User.class);
}
// 添加用户
public Mono add(ServerRequest serverRequest) {
// 将请求体转成指定Momo对象
Mono<User> mono = serverRequest.bodyToMono(User.class);
String uuid = generateUUID();
// 方法2
Mono<Object> safeUser = mono.doOnNext(u -> u.setUuid(uuid)).map(user -> {return userRepository.saveUser(user);});
// User user = createUser(serverRequest);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(safeUser,Object.class);
//方法1
// Mono<User> userMono = mono.doOnNext(u -> u.setUuid(uuid)).doOnSuccess(user -> userRepository.saveNoReturn(user));
// return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userMono,User.class);
}
private User createUser(ServerRequest serverRequest) {
User user = new User();
Optional<String> userId = serverRequest.queryParam("userId");
Optional<String> userName = serverRequest.queryParam("userName");
Optional<String> age = serverRequest.queryParam("age");
Optional<String> sex = serverRequest.queryParam("sex");
Optional<String> uuid = serverRequest.queryParam("uuid");
if (userId.isPresent()) user.setUserId(userId.get());
if (userName.isPresent()) user.setUserName(userName.get());
if (age.isPresent()) user.setAge(Integer.valueOf(age.get()));
if (sex.isPresent()) user.setSex(sex.get());
if (uuid.isPresent()) {
user.setUuid(uuid.get());
} else {
user.setUuid(generateUUID());
}
return user;
}
// 更新用户
public Mono update(ServerRequest serverRequest) {
Mono<User> mono = serverRequest.bodyToMono(User.class);
User user = createUser(serverRequest);
return ServerResponse.ok().body(userRepository.update(user),Boolean.class);
}
// 删除用户
public Mono delete(ServerRequest serverRequest) {
String uuid = serverRequest.pathVariable("uuid");
return ServerResponse.ok().body(userRepository.delete(uuid),Long.class);
}
private String generateUUID() {
return UUID.randomUUID().toString().replace("-","");
}
}
Spring WebFlux应用程序不严格依赖于Servlet API,因此它们不能作为war文件部署,也不能使用src/main/webapp目录
可以整合多个模板引擎
除了REST Web服务外,您还可以使用Spring WebFlux提供动态HTML内容。Spring WebFlux支持各种模板技术,包括Thymeleaf,FreeMarker。
14.3-SpringBoot2.x webflux实战
WebFlux中,请求和响应不再是WebMVC中的ServletRequest和ServletResponse,而是ServerRequest和ServerResponse
加入依赖,如果同时存在spring-boot-starter-web,则会优先用spring-boot-starter-web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>编写测试类UserController
public class UserController {
public Mono<String> test() {
return Mono.just("hello webflux!");
}
}启动方式默认是Netty,8080端口
测试:localhost:8080/api/v1/user/test
14.4-WebFlux客户端WebClient
WebClient是一个响应式客户端,它提供了RestTemplate的替代方法。它公开了一个功能齐全、流畅的API,并依赖于非阻塞I / O,使其能够比RestTemplate更高效地支持高并发性。WebClient非常适合流式的传输方案,并且依赖于较低级别的HTTP客户端库来执行请求,是可插拔的。
与RestTemplate相比,WebClient是:
- 非阻塞,Reactive的,并支持更高的并发性和更少的硬件资源。
- 提供利用Java 8 lambdas的函数API。
- 支持同步和异步方案。
- 支持从服务器向上或向下流式传输。
RestTemplate不适合在非阻塞应用程序中使用,因此Spring WebFlux应用程序应始终使用WebClient。在大多数高并发场景中,WebClient也应该是Spring MVC中的首选,并且用于编写一系列远程,相互依赖的调用。
Reactive方法:
|
15-SpringBoot2.0服务器端主动推送SSE技术
15.1-服务端推送常用技术介绍
客户端轮询:ajax定时拉取
ajax长时间和服务端保持通讯太占内存
服务端主动推送:WebSocket
全双工的,本质上是一个额外的tcp连接,建立和关闭时握手使用http协议,其他数据传输不使用http协议 更加复杂一些,适用于需要进行复杂双向数据通讯的场景
websocket可以进行服务端和前端双向通讯,写法较为复杂
服务端主动推送:SSE (Server Send Event)
html5新标准,用来从服务端实时推送数据到浏览器端, 直接建立在当前http连接上,本质上是保持一个http长连接,轻量协议 简单的服务器数据推送的场景,使用服务器推送事件 学习资料:http://www.w3school.com.cn/html5/html_5_serversentevents.asp
15.2-SpringBoot2.x服务端主动推送SSE
后端代码
public class SSEController {
//produces = "text/event-stream;charset=UTF-8"一定要带上
public String push() {
try {
Thread.sleep(1000);
}catch (Exception e) {
e.printStackTrace();
}
double moeny = Math.random()*10;
System.out.println(String.format("%.2f",moeny));
DecimalFormat df = new DecimalFormat(".00");
String price = df.format(moeny);
//!!!注意,EventSource返回的参数必须以data:开头,"\n\n"结尾,不然onmessage方法无法执行。
return "data:猪肉价格行情:" + price +"元"+ "\n\n";
}
}前段代码
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
//需要判断浏览器支不支持,可以去w3c进行查看
var source = new EventSource('/get_data');
source.onmessage = function (event) {
console.info(event.data);
document.getElementById('result').innerText = event.data
};
</script>
</head>
<body>
<div id="result"></div>
</body>
</html>
16-SpringBoot2.x监控Actuator
Spring Boot Actuator
是spring boot
项目一个监控模块,提供了很多原生的端点,包含了对应用系统的自省和监控的集成功能,可以查看应用配置的详细信息,比如应用程序上下文里全部的Bean、健康指标、环境变量及各类重要度量指标等等,这些都是使用可HTTP
进行请求访问。通过这些监控信息,我们就能随时了解应用的运行情况了。
Actuator 是 Spring Boot 提供的对应用系统的自省和监控功能。通过 Actuator,可以使用数据化的指标去度量应用的运行情况,比如查看服务器的磁盘、内存、CPU等信息,系统的线程、gc、运行状态等等。
Actuator 通常通过使用 HTTP 和 JMX 来管理和监控应用,大多数情况使用 HTTP 的方式。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>添加配置
server:
port: 8080
servlet:
context-path: /demo
# actuator 监控配置
management:
#actuator端口 如果不配置做默认使用上面8080端口
server:
port: 9090
endpoints:
web:
exposure:
#默认值访问health,info端点 用*可以包含全部端点
include: "*"
#修改访问路径 2.0之前默认是/; 2.0默认是/actuator可以通过这个属性值修改
base-path: /actuator配置完成启动项目后就可以通过postman或者直接在预览器输入路径等方式来查看应用的运行状态了。
当项目启动时,访问[http://127.0.0.1:9090/actuator]
地址注意:如果没有配置 actuator端口,采用默认访问地址:http://127.0.0.1:8080/demo/actuator
如果看到类似下面的内容,说明actuator已经生效了
{
"_links": {
"self": {
"href": "http://127.0.0.1:9090/actuator",
"templated": false
},
"auditevents": {
"href": "http://127.0.0.1:9090/actuator/auditevents",
"templated": false
},
"beans": {
"href": "http://127.0.0.1:9090/actuator/beans",
"templated": false
},
"caches-cache": {
"href": "http://127.0.0.1:9090/actuator/caches/{cache}",
"templated": true
},
"caches": {
"href": "http://127.0.0.1:9090/actuator/caches",
"templated": false
},
"health": {
"href": "http://127.0.0.1:9090/actuator/health",
"templated": false
},
"health-component": {
"href": "http://127.0.0.1:9090/actuator/health/{component}",
"templated": true
},
"health-component-instance": {
"href": "http://127.0.0.1:9090/actuator/health/{component}/{instance}",
"templated": true
},
"conditions": {
"href": "http://127.0.0.1:9090/actuator/conditions",
"templated": false
},
"configprops": {
"href": "http://127.0.0.1:9090/actuator/configprops",
"templated": false
},
"env": {
"href": "http://127.0.0.1:9090/actuator/env",
"templated": false
},
"env-toMatch": {
"href": "http://127.0.0.1:9090/actuator/env/{toMatch}",
"templated": true
},
"info": {
"href": "http://127.0.0.1:9090/actuator/info",
"templated": false
},
"loggers": {
"href": "http://127.0.0.1:9090/actuator/loggers",
"templated": false
},
"loggers-name": {
"href": "http://127.0.0.1:9090/actuator/loggers/{name}",
"templated": true
},
"heapdump": {
"href": "http://127.0.0.1:9090/actuator/heapdump",
"templated": false
},
"threaddump": {
"href": "http://127.0.0.1:9090/actuator/threaddump",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://127.0.0.1:9090/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://127.0.0.1:9090/actuator/metrics",
"templated": false
},
"scheduledtasks": {
"href": "http://127.0.0.1:9090/actuator/scheduledtasks",
"templated": false
},
"httptrace": {
"href": "http://127.0.0.1:9090/actuator/httptrace",
"templated": false
},
"mappings": {
"href": "http://127.0.0.1:9090/actuator/mappings",
"templated": false
}
}
}建议
只能访问几个url
访问的url在SpringBoot2.0版本需要加上actuator
需要在配置文件中加入下列配置
management.endpoints.web.exposure.include=*
原因:
出于安全考虑,除/ health和/ info之外的所有执行器默认都是禁用的。management.endpoints.web.exposure.include
属性可用于启用执行器建议
在设置management.endpoints.web.exposure.include之前,请确保暴露的执行器不包含敏感信息和/
或通过将其放置在防火墙进行控制,不对外进行使用禁用的端点将从应用程序上下文中完全删除。如果您只想更改端点所暴露的技术,请改用 include和exclude属性
例子:开启全部:`management.endpoints.web.exposure.include=*` 开启某个:`management.endpoints.web.exposure.include=metrics` 关闭某个:`management.endpoints.web.exposure.exclude=metrics`
或者用springadmin进行管理
相关资料:https://www.cnblogs.com/ityouknow/p/8440455.html
或者用自己编写脚本监控
CPU、内存、磁盘、nginx的http响应状态码200,404,5xx
介绍常用的几个
`/health` 查看应用健康指标 `/actuator/metrics` 查看应用基本指标列表 `/actuator/metrics/{name}` 通过上述列表,查看具体 查看具体指标 `/actuator/env` 显示来自Spring的 ConfigurableEnvironment的属性