注:里面的各种原理不求完全掌握,可以看看最后一个章节,懂懂流程就行。
4 配置文件
配置文件两种类型,一种是properties,一种是yaml
yaml语法:
自定义的类写配置文件的时候没有提示,要提示需要依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
5 web开发
5.1 自动配置概览
这个不求全懂,有个印象:
5.2 静态资源、index页、Favicon
5.2.1 静态资源
静态资源默认的存放地址:
访问:
当前项目根路径/ + 静态资源名
修改配置:
spring:
mvc:
#静态资源访问前缀
static-path-pattern: /res/**
resources:
#静态资源路径修改
static-locations: [classpath:/haha/]
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找。
前后端分离后这种情况很少见了,静态资源放到前端工程的static里了。
5.2.2 欢迎页index.html
静态资源路径下放个index.html,访问项目路径直接可以跳到index页
5.2.3 Favicon图标
favicon.ico 放在静态资源目录下即可。
5.2.4 静态资源配置原理
SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类),SpringMVC功能的自动配置类 WebMvcAutoConfiguration,所以我们debug看WebMvcAutoConfiguration这个类。
这里就可以看到默认的静态路径为什么是那4个:
5.3 请求参数
5.3.0 rest
rest风格:
rest使用条件:
- 表单method=post,隐藏域 _method=put
- yaml配置开启hiddenmethodfilter
spring:
mvc:
hiddenmethod:
filter:
enabled: true
rest风格就是通过HiddenHttpMethodFilter类中的方法,将post请求的_method属性的值(PUT/DELETE)替换请求类型,post变put/delete,然后放行给后面的dispatcherservlet,让它调具体的controller。
定制化"_method":
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
//更改rest风格需要的默认属性名:
methodFilter.setMethodParam("_m");
return methodFilter;
}
5.3.1 普通参数和基本注解
注解
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
以上注解的具体描述:
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>测试基本注解</h2>
<a href="/car/1/中文name?age=18&inters=basketball&inters=羽毛球">/car/1/中文name?age=18&inters=basketball&inters=羽毛球</a>
</body>
</html>
controller:
package com.atguigu.controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
public class ParamTestController {
@GetMapping("/car/{id}/{name}")
public Map<String, Object> getCar(@PathVariable("id") Integer id,
@PathVariable("name") String name,
@PathVariable Map<String, String> pathVariableMap,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String, String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String, String> requestParamMap
//@CookieValue("_ga") String ga (获取cookie,chrome浏览器没发cookie,这里就不获取了,免得报错)
) {
HashMap<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", name);
map.put("pathVariableMap", pathVariableMap);
map.put("userAgent", userAgent);
map.put("header", header);
map.put("age", age);
map.put("inters", inters);
map.put("requestParamMap",requestParamMap);
return map;
}
}
返回的map->json结果:
post请求可以用@RequestBody,得到原始的请求体信息:
html:
controller:
返回结果:
@RequestAttribute,得到request请求域中的特定属性数据:
矩阵变量(了解):
Servlet API
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
复杂参数
Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
参数处理原理
● HandlerMapping中找到能处理请求的Handler(Controller.method())
● 为当前Handler 找一个适配器 HandlerAdapter
● 通过该适配器下不同的参数解析器resolvers解析不同的参数,然后适配器执行目标方法
5.3.2 pojo封装
可以自动类型转换与格式化,可以级联封装。
pojo封装原理
首先创建个空的对象,然后通过绑定器把k-v和空对象一一对应,值填入空对象,值填入空对象之前用转换器把值变成对象需要的数据类型。
5.4 数据响应与内容协商
数据响应,分响应页面和响应数据,响应页面用的少,前后端分离后一般是后台给前台响应数据,怎么显示是前台(vue,jsp,html)的事。
内容协商是说,根据客户端接收能力不同,返回不同媒体类型的数据。
5.4.0 相应页面
客户端跳转(重定向redirect)和服务器端跳转:
服务端跳转:
客户端跳转:
区别:
- 服务器端跳转时,浏览器地址栏中的URL不会改变(客户端并不知道页面进行了跳转);客户端跳转时,则地址栏会改变为第二次请求的URL。
- 服务器端跳转时,未超出request的属性范围,request属性能够保存到跳转页;客户端跳转时,则超出了request的属性范围,无法进行其属性的传递。
5.4.1 响应json
jackson.jar+@ResponseBody:
web场景启动器里有json相关的jar包,然后再加@ResponseBody注解,返回的就是json。
原理:
springmvc支持的返回值类型:
5.4.2 内容协商
(1)基于header请求头的内容协商
浏览器能解析什么类型数据,一般默认由请求头信息说明,服务器给浏览器返回该类型的数据。
下面模拟返回xml和json两种不同数据:
(由于返回json方式在jackson的jar包中已经有了,所以这里添加个对应的能返回xml格式数据的jar包)
1 引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2 postman分别测试返回json和xml
这里使用postman改变header请求头中Accept的值来让服务器得知返回什么类型的数据。
接收所有格式的k-v参数:
(2)基于请求参数的内容协商
除了默认的基于请求头,springmvc还自带一个基于请求参数的内容协商,只需要yaml配置一下就行:
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
然后路径中带format的参数:
这个操作有时候不起效不知道为什么,本人还是喜欢用前端ajax请求的时候在header中规定数据格式。
5.4.3 内容协商原理
5.5 视图解析
5.6 拦截器
拦截器是使用的springmvc的拦截器,可以参考之前的博客:springmvc博客。
定义拦截器就实现HandlerInterceptor接口,实现里面三个方法就行:
比如创建一个可以在登录的controller执行前验证登录信息的拦截器:
然后将拦截器注册:
多拦截器执行:
5.7 文件上传
文件上传参照springmvc博客
文件上传自动配置内容:
5.8 异常处理
5.8.1 默认异常处理规则
- 默认情况下,Spring Boot提供/error处理所有错误的映射
- 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。
对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
5.8.2 异常处理流程
异常处理流程:
5.8.3 异常处理的定义使用
5.9 web原生组件注入
servlet、filter、listener,使用springmvc和springboot后,其实原生的组件用的少了,本人觉得5.9这节没啥用。
5.9.1 servlet
三大原生组件都需要这个@servletcomponentscan来扫描:
5.9.2 filter
5.9.3 listener
5.9.4 使用registrationbean注解
最好单实例:
使用这三个注解就不用加@webservlet之类的注解了。
5.10 嵌入式Servlet容器
servlet容器就是指tomcat之类的。
5.10.1 切换servlet容器
切换容器就是排除tomcat,然后导入别的服务器jar包:
5.10.2 修改容器属性
yaml里修改:server.xxxx
5.11 定制化原理
5.11.1 定制化常见方式
方法1:最简单的,直接改yaml
方法2:xxxxCustomizer 定制化器
方法3:编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件
5.11.2 原理分析套路
导入组件是在xxxAutoConfiguration里面的@Bean操作导入的。
6 数据访问
6.1 SQL
jdbc、mybatis、druid的关系:
6.1.1 整合druid+mybatis:
整合某些东西一般需要自定义或者用starter,这里使用starter。
- 引入druid的starter依赖和yaml配置
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<!--springboot不知道你要用什么数据库,所以数据库驱动自己额外加,注意默认的驱动版本-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
spring:
datasource:
url: jdbc:mysql://localhost:3306/guns211030?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
aop-patterns: com.atguigu.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)
stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin
login-password: admin
resetEnable: false
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
- mybatis依赖和yaml:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
# 配置mybatis规则
mybatis:
#全局配置文件,autoconfig都配好了,可以不配
#config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true #开启数据库表里面的字段名xxx_yyy和实体类里面的属性名xxxYyy的自动驼峰对应。
- 使用:
数据库里搞个表 show_jflow:
弄两个数据:
写个对应的实体类,加上get/set方法:
写个dao层的interface,标上mybatis独有的@Mapper注解:
在resources/mybatis/mapper里建个JflowMapper.xml:
<?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.atguigu.dao.JflowMapper">
<select id="getAll" resultType="com.atguigu.bean.JworkFlow">
select * from show_jflow
</select>
</mapper>
搞个controller,service层这里就不弄了,直接注入dao层的mapper,测试从简:
运行起来访问有数据,可以了:
注:注解方式可以不用写xml,简单的查询可以,复杂查询建议还是xml走起,可以xml+注解
6.1.2 整合mybatisplus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
不引入mybatis,只引入mybatisplus,因为后者包括了前者的jar包:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
只需要我们的Mapper继承 BaseMapper 就可以拥有crud能力(图片里是user类),甚至如果没有自定义的查询语句需求的话,xml都可以不要:
因为我们的实体类名和表名不同,所以要使用注解对应:
写个controller:
测试:
6.2 NoSQL
这里以redis为例。
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。
6.2.1 整合redis
操作redis数据库的框架官方推荐的java客户端只有Jedis、lettuce、Redisson。
- 安装redis,安装可以参照:how2j.cn的REDIS系列教材,然后按照教程运行起来redis服务。
- 导入redis的starter,配置yaml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
可以看到,自带一个底层是用通信框架netty写的lettuce:
测试:
6.2.2 切换操作redis的框架lettuce–>jedis
在原有的starter依赖基础上加jedis依赖,然后yaml指定jedis框架,
测试类写法不动,测试依旧:
7 单元测试
7.1 junit5新特性
junit5使用:
7.2 常用注解
7.3 断言
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。
异常断言例子:
项目上线前,先跑几遍测试,看看汇总报告,确保没问题再上线:
7.4 前置条件
7.5 嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
7.6 参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
8 指标监控
8.1 springboot Actuator
8.1.1 actuator介绍
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
8.1.2 Actuator使用
首先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后yaml配置:
management:
endpoints:
enabled-by-default: true #暴露所有监控端点信息
web:
exposure:
include: '*' #以web方式暴露所有端点
测试:
访问 http://localhost:8080/actuator/**,
可用的路径一般有:
这些beans、configprops等都是监控端点(Actuator Endpoint),具体有哪些监控端点可以查官网。
全代码看着费劲,下面介绍可视化的工具。
8.1.3 可视化
这里使用这个可视化框架:https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started
服务端:
1.初始化一个springboot项目,然后导依赖:
注意版本
2.启动程序上加个注解:
客户端:
(注意springboot的版本都是2.3.4;客户端端口号不要和服务端冲突了)
依赖:
yaml:
都启动,然后测试:
8.2 Actuator Endpoint
最常用的Endpoint
● Health:监控状况
● Metrics:运行时指标
● Loggers:日志记录
8.2.1 Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
● health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
● 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
● 可以很容易的添加自定义的健康检查机制
看到默认的health端点不够详细:
不详细就设置一下:
设置后的详细health:
8.2.2 Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
● 通过Metrics对接多种监控系统
● 简化核心Metrics开发
● 添加自定义Metrics或者扩展已有Metrics
8.2.3 管理Endpoints
● 默认所有的Endpoint除过shutdown都是开启的。
● 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint..enabled = true
8.3 定制 Endpoint
8.3.1 定制health
8.3.2 定制info
http://localhost:8080/actuator/info 会输出返回的所有info信息,默认是空的。
常用两种方式,yaml方式和java方式.
第二种方式在一些信息只能通过方法返回得到的场景会使用。
会返回方法1和方法2所有的合并结果:
8.3.3 定制Metrics信息
定制指标:
这样代码太耦合了,笔者现有知识推荐用aop。
查看:
8.3.4 定制(新增)endpoint
9 原理解析
9.1 profile功能
为了方便多环境适配,springboot简化了profile功能。
首先建三个配置:
依次配三个server.port,分别为8080,8081,8082
启动,观察发现第一个yaml生效了:
在application.yaml里加入:
spring:
profiles:
active: prod
发现prod的生效了:
结论:默认配置文件(application.yaml)永远加载,在里面指定加载的配置文件之后加载,有冲突的地方凭后加载的为准。
打jar包以后配置就固定了,如果要改配置,就java -jar xxx.jar --xxx.xxx.xxx=xxx,比如改profile配置:
还可以条件装配组件:
profile还能批量加载分组配置:
9.2 外部化配置
9.2.1 配置来源
将配置抽取出来放在外部,加载的时候让springboot调配置加载。
外部化配置来源:
1…yaml/properties文件
2.来自环境、系统变量
主要能访问的东西都来源于系统的环境变量和系统属性:
3.启动时命令行额外带着的配置
9.2.2 配置查找位置
(后面的覆盖前面的)
9.2.3 配置文件加载顺序
上节第3点config data 配置文件的查找位置:
classpath类路径:java路径、resources路径都是类路径。
其实也不是优先级低,而是下面的后加载,导致覆盖了上面的配置。
9.2.4 总结
指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
9.3 自定义starter
9.3.1 starter启动原理
9.3.2 自定义starter案例
- 新建一个Empty project,在里面新建两个模块,一个maven模块叫atguigu-hello-spring-boot-starter,另一个springboot模块叫atguigu-hello-spring-boot-starter-autoconfigure。
- 在starter模块中引入autoconfigure模块的依赖:
- autoconfigure模块配置:
HelloServiceAutoConfiguration:
package com.atguigu.hello.auto;
import com.atguigu.hello.bean.HelloProperties;
import com.atguigu.hello.service.HelloService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnMissingBean(HelloService.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
@Bean
public HelloService helloService() {
HelloService helloService = new HelloService();
return helloService;
}
}
HelloProperties:
package com.atguigu.hello.bean;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("atguigu.hello")
public class HelloProperties {
private String prefix="default prefix";
private String suffix="default suffix";
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
HelloService:
package com.atguigu.hello.service;
import com.atguigu.hello.bean.HelloProperties;
import org.springframework.beans.factory.annotation.Autowired;
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayhello(String userName) {
return helloProperties.getPrefix() + ":" + userName + "," + helloProperties.getSuffix();
}
}
spring.factories把我们的自动配置类加上,其他保持springboot的自动配置类,(其实可以不用加springboot原来的,他应该是额外增加,不是替换):
4. clean-install:
先auto,再starter,作用是加到maven仓库里?
5. 测试
测试就是找个项目,pom里加上starter,然后测试类测试:
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>atguigu-hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
@SpringBootTest(classes = MainApplication.class)
public class TestApplication {
@Autowired
HelloService helloService;
@Test
void test1(){
String s=helloService.sayhello("coderhao");
System.out.println(s);
}
}
yaml里配置点东西再测:
9.4 springboot原理
9.4.1 启动过程
两步,创建SpringApplication,运行SpringApplication
还可以参考:
springboot启动原理
SPRINGBOOT启动流程及其原理
9.4.2 @SpringbootApplication注解如何将启动过程中需要的信息都联系起来的?
其中@EnableAutoConfiguration那条线组件的创建过程:
9.4.3 springmvc处理请求原理
9.4.4 springboot中bean的生命周期
资料来自于Spring基本用法5——容器中Bean的生命周期
对于prototype作用域的Bean,Spring容器仅仅负责创建,当容器创建了Bena实例之后,Bean实例完全交给客户端代码管理,容器不再跟踪其生命周期。
对于singleton作用域的Bean,每次客户端代码请求时,都返回同一个贡献实例,客户端代码不能控制Bean的销毁,Spring容器负责跟踪Bean实例的产生、销毁。
我们将Spring容器中Bean的生命周期级别分为四级,分别是:Bean自身方法、Bean级生命周期接口方法、容器级生命周期接口方法、工厂后处理器接口方法。
- Bean自身的方法,包括:
- Bean本身调用的方法
- 通过配置文件中的init-method和destroy-method指定的方法;
- Bean级生命周期接口方法,包括:
- InitializingBean接口
- DiposableBean接口
- BeanNameAware接口
- ApplicationContextAware接口
- BeanFactoryAware接口
- 其他
- 容器级生命周期接口方法,包括:
- InstantiationAwareBeanPostProcessor接口实现
- BeanPostProcessor 接口实现
- 一般称它们的实现类为“后处理器”
- 工厂级生命周期口方法(BeanFactoryPostProcessor接口的实现类)
- AspectJWeavingEnabler
- ConfigurationClassPostProcessor
- CustomAutowireConfigurer等
- 工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。
9.4.5 自定义事件监听组件
将9.4.1中黄色标注的接口实现,然后重写方法,再将MyApplicationRunner,MyCommandLineRunner加@component注解,因为这两个方法是在容器中找的。再在spring.factories里注册剩下3个类,运行看看。
实现runlistener的类要加个构造器,不然报错,至于为什么这样写,是因为实现这个接口的自带的类是这么写的:
可以观察调用顺序: