SpringBoot2 - 基础入门【一 ~ 五】,详见:
六、配置文件
6.1 properties文件格式
同之前的用法。
6.2 yaml文件格式【推荐】
YAML本意:"YAML"不是一种标记语言。但在开发中,实际把它理解为:"Yet Another Markup Language"(仍是一种标记语言)
戏称为:薛定谔的YAML
● 非常适合以数据为中心 的配置文件
● 后缀:.yaml 或 .yml
【语法】
(1) key: value
● 对于数组、List、Set 的每个元素:
● 行内 写法:[x1,x2,x3, ...]
● 层次 写法:每一层使用 - x1
● 对于Map、Object 的每个元素:
● 行内 写法:{key1: value1, key2: value2, ...}
● 层次 写法:空格缩进后,每一层直接写key : value
(2) 使用空格缩进表示层级关系
● 数量不重要,只要相同层级对齐即可
● 不允许使用tab,只允许空格,但在IDE中我们不用关心
(3) 字符串我们可以不用加引号
● '...' 单引号 :表示保留字符串原始内容 ,尽管是转义字符,也不会进行转义
● "..." 双引号 :表示会将转义字符转义
(4) 大小写敏感
(5) #表示注释
e.g
@Data
public class Person{
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animals;
private Map<String, Object> scores;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@Data
class Pet{
private String name;
private Double weight;
}
application.yml
person:
userName: zhangsan
boss: true
birth: 2019/12/9
age: 18
interests:
- 篮球
- 足球
- 18动漫
animals: [阿猫, 阿狗]
scores:
english: 80
math: 90
salarys:
- 9999.98
- 9999.99
pet:
name: 阿狗
weight: 99.99
allPets:
sick:
- {name: 阿狗, weight: 99.99}
- name: 阿猫
- weight: 88.88
- name: 阿虫
- weight: 77.77
health:
- {name: 阿花, weight:199.99}
- {name: 阿明, weight:11.55}
⭕ 自定义类绑定的配置提示(配置处理器)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<!-- 某个依赖项是可选的。这意味着当其他项目依赖于该项目时,该依赖项不会自动传递给其他项目,除非其他项目也显式地声明了该依赖项。 -->
<optional>true</optional>
</dependency>
<!-- 官方建议:移除这个配置处理器,新版本SpringBoot自动移除了 -->
<build>
<plugins>
<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>
</plugins>
</build>
七、Web开发
7.1 静态资源
7.1.1 存放静态资源的目录
(1) 默认情况下,当用户键入:当前项目根路径/ + 静态资源文件
SpringBoot就会在项目的**/static(或/public、/resources、/META-INF/resources)**目录下查找静态资源
● 原因:默认的静态资源映射路径为:/**
当请求进来后,先会去Controller处理器 中看看能不能处理,如果不能处理,则交给默认的静态资源处理器去查找。
(2) 修改存放静态资源的目录
SpringBoot会在你修改后的目录中找静态资源
application.yaml
spring:
resources:
static-locations: [classpath:/myResource/]
add-mappings: false # 这里可以禁用查找静态资源(默认是true)
7.1.2 静态资源的映射路径
(1) 默认情况下:无前缀(即/**)
(2) 修改静态资源的映射路径【推荐】
这样当我们后续设置Filter时,不会拦截到静态资源。
application.yaml
spring:
mvc:
static-path-pattern: /res/**
● 当用户键入:当前项目 + static-path-pattern + 静态资源名 = SpringBoot就会前往静态资源目录中寻找对应的资源
7.1.3 webjar(了解)
一些第三方静态资源也打成了jar包,当需要获取时,可以通过加上前缀:/webjar/...
访问jQuery示例:http://localhost:8080/webjars/jquery/3.5.1/jquery.js
7.1.4 欢迎页和自定义Favicon
(1) 欢迎页
当用户键入项目根目录时,Spring Boot会:
● 先找ReqeustMappingHandlerMapping 【保存了我们所有controller的映射规则】,即查找@RequestMapping("/")的处理器;
● 找不到,再去找WelcomePageHandlerMapping ,查找静态资源目录下的欢迎页 作为默认首页。还找不到,才会去寻找模板,即对应的Controller中写了@RequestMapping("/index")并返回"index"视图名的处理器方法 。
源码
⭕ 注意:当配置了自定义的静态资源的映射路径时,用户键入项目根路径将无法找到index.html这类静态资源。
(2) favicon
在静态资源目录下,放入名为"favicon.ico"的静态资源 ,则会自动的将icon显示,作为网页的图标。
⭕ 注意:当配置了自定义的静态资源的映射路径时,用户键入项目根路径将无法显示favicon.ico图标。
7.2 请求参数处理
7.2.1 支持Rest风格的HiddenHttpMethodFilter
在SpringBoot的WebMvcAutoConfiguration 自动配置类中,已经帮我们自动配置了一个该过滤器对象,但是它需要配置文件的支持!
底层源码
⭕ 如果是表单中使用,需要手动开启HiddenHttpMethodFilter!
application.yaml
spring:
mvc:
hiddenmethod:
filter:
enabled: true
因为有很多客户端(如Postman),可以发送DELETE、PUT等请求,这一项是选择性开启。
7.2.2 自定义转换器
(若干个)参数解析器 中大多都会有一个类WebDataBinder ,它用来做数据绑定 ,里面有一个conversionService 对象,注册了(若干)转换器converter ,用来做数据类型的转换。
⭕ 假设现在有一个需求:表单中提交name="pet",value="阿猫, 18" ;服务器(SpringBoot)的handler中参数接收一个Pet类型 的数据。
● 默认converter没有办法处理这种String→Pet的转换,会报错 。
● 我们自定义 一个converter,并把它注册进容器中。
//1、WebMvcConfigurer定制化SpringMVC的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除;后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
// 啊猫,3
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
};
}
7.3 数据响应和内容协商
数据响应分类
7.3.1 原理
HttpMessageConverter规范
SpringBoot【处理@ResponseBody响应】中,根据handler返回值类型(假设是自定义Person类)确定一个返回值处理器 (有若干个),在这个处理器内部,先获取浏览器可以接收的数据类型 ,然后去**(第一次)遍历HttpMessageConverter** ,根据**【canWrite方法】** ,拿到所有可以将Person写成别的数据类型 的转换器,将这些转换器支持的MediaType保存起来 ,即获取服务器可以写出的数据类型,然后做浏览器和服务器的【内容协商】 ,确定最终匹配的MediaType(可能有多个),最后,(第二次)遍历MessageConverter ,仍然根据**【canWrite方法】** ,找到含有最终匹配MediaType的这些转换器。然后按照优先级排序 ,选出**【第一个】** 可以将Person转换成最终的MediaType的转换器,使用它将Person转换为最终格式并返回给浏览器。
● 第一次遍历HttpMessageConverter:服务器具备写哪些数据格式的能力
● 第二次遍历HttpMessageConverter:浏览器和服务器协商后,找到可以对应的转换器
⭕ 另:canRead:带有@RequestBody的请求参数值是否可以读成handler形参中的类型。
7.3.2 如何响应Json和xml
响应json:
● json相关依赖 + @ResponseBody
(1) json相关依赖
引入spring-boot-starter-web场景启动器,内部就含有spring-boot-starter-json场景启动器,主要是通过jackson来实现的。
(2) 在handler方法或者类上标注@ResponseBody
响应xml:
(1) 引入xml依赖
由于spring-boot-starter-web中没有内置对xml的解析支持,所以需要引入额外的依赖。
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
(2) 在handler方法或者类上标注@ResponseBody
7.3.3 开启浏览器:使用请求参数,来进行内容协商
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
发请求:(带上键为"format"的请求参数 ,如:json)
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
内容协商管理器
参数内容协商管理器支持的数据类型(想要协商其他的可以自定义)
7.3.4 自定义MessageConverter(修改服务器需要的MediaType)
假定业务需求,有一种自定义的输出格式"application/myFormat",它要求将Person的属性值按照分号间隔,返回给客户端。
在@Confiugration配置类中添加WebMvcConfig的Bean组件,并在里面添加自定义的MessageConverter功能。
省略了Person的JaveBean
@RestController
public class testController{
@GetMapping("/test")
public Person getPerson(){
Person p = new Person("张三", 18);
return p;
}
}
// 自定义的转换器
class MyMessageConverter implements HttpMessageConverter<Person>{
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType){
return false; // 这部分是@requestBody需要做的,我们现在不关心
}
// 自定义判断是否可写
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType){
return clazz.isAssignableFrom(Person.class);
}
@Override
public List<MediaType> getSupportMediaTypes(){
return MediaType.parseMediaType("application/myFormat"); // 自定义MediaType,即可以处理的数据类型
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage)throws IOException{
return null; // 这部分是@requestBody需要做的,我们现在不关心
}
// 自定义如何将Person写出
@Override
public Person write(Class<? extends Person> clazz, HttpOutputMessage outputMessage)throws IOException{
String data = person.getUserName + ";" + person.getAge();
OutputStream os = outputMessage.getBody()
os.write(data.getBytes());
}
}
在我们的含有@Configuration配置类中自定义WebMvcConfig的Bean组件,对其中的功能(这里是添加MessageConverter)进行定制。
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
// 这个方法是追加MessageConverter,并不会覆盖掉原来的
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyMessageConverter()); // 添加自定义MessageConverter
}
}
}
7.3.5 自定义ContentNegotiationStrategy(通过携带自定义参数,修改浏览器需要的MediaType)
开启【基于参数format的】,在WebMvcConfigurer里配置一个新的ParameterContentNegotiationStrategy
在我们的含有@Configuration配置类中自定义WebMvcConfig的Bean组件,对其中的功能(这里是覆盖内容协商管理器的策略)进行定制。
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
// 配置自定义内容协商器
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> mediaTypes = new HashMap<>();
map.put("json", MediaType.APPLICATION_JSON);
map.put("xml", MediaType.APPLICAION_XML);
map.put("myFormat", MediaType.parseMediaType("application/myFormat")); // 自定义MediaType数据格式
ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(mediaTypes);
configurer.strategies(Arrays.asList(strategy)); //【这里会覆盖原有的内容协商管理器的策略,可以使用别的方法,实现新增】
// configurer.strategies(Arrays.asList(strategy, new HeaderContentNegotiationStrategy());
}
}
}
● 使用这种方法会覆盖原有的请求头内容协商策略,导致默认功能失效,我们尽量修改的**【原则是新增,而不是覆盖】**!(参照SpringBoot用别的方法开发自定义内容协商部分)
7.4 视图解析与模板引擎
SpringBoot默认不支持JSP (需要服务器解压,并提供Java编译器,带来额外的负担),需要引入第三方模板引擎技术(自身的模板引擎技术是支持JSP的)。
● 引入Thymeleaf模板引擎场景启动器
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
它帮我们配置了ThymeleafViewResolver,并绑定到了ThymeleafProperties类。
默认前缀、后缀
7.4.1 视图解析原理
(1) 所有的handler执行完成后会返回ModelAndView对象 ;
(2) DispatcherServlet调用processDispatchResult 方法,遍历各个视图解析器 ,根据视图名称 确定可以处理的解析器;
(3) 解析器会给我们返回View对象 (View是接口 ,里面有render方法定义了渲染逻辑 )
(4) View调用render方法渲染视图
e.g:
● RedirectView:render方法最终会调用:
response.sendRedirect("浏览器解析的路径");
● InternalResourceView:render方法最终会调用:
request.getRequestDispatcher("服务器解析的路径").forward(request, response);
● TymeleafView:有自己的渲染规则...
7.5 拦截器 Interceptor
通常情况下,我们除了登录页面 之外,其他页面的都需要用户登录后才能进入,我们会使用拦截器实现这个功能。
(1) 自定义拦截器
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
return true;
}
//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}
/**
* 页面渲染以后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}
(2) 将拦截器添加注册到WebMvcConfigurer中
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
}
拦截器的原理详见SpringMVC:框架3:SpringMVC - 简书 (jianshu.com)
7.6 文件上传
SpringBoot中帮我们自动配置了**【MultipartAutoConfiguration】文件上传自动配置类** ,它里面帮我们配置好了StandardServletMultipartResolver文件上传解析器 ,并和MultipartProperties绑定(我们可以通过修改application的全局配置文件来修改,诸如:单个文件上传最大大小、整个请求上传的最大大小...)。
● @RequestPart注解 :当有多个文件 需要上传时,显式指定提交表单项的"name"名称 ,用于确定哪一个上传的文件。
(当然如果页面中使用<input type="file" name="files" multiple/>,handler中可以使用形参MultipartFile[]接收)
7.6.1 文件上传源码分析
(1) DispatcherServlet中的doDispach()方法,接收请求,然后会使用multipartResolver文件上传解析器判断当前是不是一个分段请求(即文件上传请求) ,如果是:会将当前Request封装成新的MultipartHttpServletRequest对象 。
⭕ 具体的判断条件:
multipartResolver.isMultipart(request);
↓
StringUtils.startWithIgnoreCase(request.getContentType(), prefix:"multipart/");
(2) 然后DispatcherServlet再去找到对应的HandlerMapping,做后续的处理......
(3) 在使用HandlerAdapter处理器适配器去执行handler时,匹配到RequestPartMethodParameterResolver参数解析器 ,会把封装过的request对象中的文件封装成MultipartFile对象 ,放到handler的形参列表中,对应的handler就可以进行处理了。
7.7 异常处理
SpringBoot在默认的情况下,对于来自机器客户端 的错误,将生成JSON响应 ,其中包括错误、HTTP状态、异常消息的详细信息;对于浏览器 的错误,会响应一个**"whitelabel"错误视图** ,以HTML格式呈现同样的数据。
两种默认的响应方式
7.7.1 ErrorMvcAutoConfiguration
这是SpringBoot为异常处理提供的自动配置类。里面主要包含三大组件:
(1) DefaultErrorAttributes------handlerExceptionResolver实现类之一
● 作用:它主要是用来定义错误页面可以包含哪些数据 ,并将它保存到request域 中,以便在错误页面展示。
(2) BasicErrorController------handler
● 作用:可以理解为是一个普通的handler ,但他是专门处理默认的"/error"请求 。默认情况下(没有任何的自定义处理异常) ,会使用SpringBoot放在容器中的默认错误页,即id为"error"的View组件 【这就是浏览器错误白页的由来】。
(3) DefaultErrorViewResolver------视图解析器
● 作用:ErrorMvcAutoConfiguration默认配置,专门用来处理异常视图的解析器 。它首先会先根据状态响应码,对状态码进行匹配 。先进行精确匹配 ,如"404";如果匹配不到,则进行序列匹配 ,如"4xx"、"5xx"【可以在"/error"目录下,自定义4xx、5xx、400等html页面】。如果找不到 ,则解析上述的**"error"默认错误视图** 。
7.7.2 异常处理步骤
(1) 执行目标方法,如果方法运行期间出现异常,会被捕获,并标志请求结束。
(2) 进入视图解析阶段,调用processDispatchResult(request, response, handler, mv, exception)方法,派发返回页面。
● 遍历所有的handlerExceptionResolver,看谁能处理当前异常。【HandlerExceptionResolver是一个接口,它里面只定义了一个方法,即resolveException,如何处理异常,返回类型是ModelAndView】
系统默认的异常解析器
a) 第一个执行的解析器是 DefaultErrorAttributes ,它仅 定义了将哪些错误信息放到request域 ,供错误页面使用,返回ModelAndView为null。
b) 执行第二个解析器,该解析器中定义了三个自带的异常处理解析器。
● ExceptionHandlerExceptionResolver:携带了注解@ExceptionHandler ,并能够处理当前异常的解析器【它就是@ControllerAdvice + @ExceptionHandler 的底层支持】
● ResponseStatusExceptionResolver:携带了注解@ResponseStatus ,并能够处理当前异常的解析器【它就是@ResponseStatus 的底层支持】
● DefaultHandlerExceptionResolver:SpringBoot专门用来处理内部异常的解析器,如参数不存在...
c) 自己实现了HandlerExceptionResolver接口的解析器类【可以通过@Order来更改优先级】
● 只要上述任意一个异常处理解析器返回的ModelAndView对象不为空,则直接跳出。如果找到了解析器,那么就使用该解析器解析,处理异常;如果所有的异常处理解析器都无法处理 ,那么会直接将异常抛出去,给Tomcat处理 。但是SpringBoot在这里进行了封装,它不会直接由Tomcat的默认错误页处理,而是会转发"/error"请求,被SpringBoot中的BasicErrorController组件处理。
7.7.3 自定义异常处理
● 方式一:自定义错误页
**在静态资源目录,或者templates目录下,创建一个"/error"目录,里面存放"404"、"4xx"、"5xx"等以状态码命名的html页面。**DefaultErrorViewResolver会先找这样的错误模板页,如果找不到才使用默认的错误视图"error"。
● 方式二:@ControllerAdvice + @ExceptionHandler【推荐】 ⭐
它可以:**自定义如果解析异常信息,并返回ModelAndView。**底层由ExceptionHandlerExceptionResolver支持。
● 方式三:@ResponseStatus 自定义状态码,以及详细的错误信息
它不能定义如何解析这个异常以及返回ModelAndView,只能手动标识一个HTTP状态码和详细错误信息 ,【可以将注解加到控制器方法或异常类上】。 最终还是会调用response.sendError(statusCode, revolcedReason),即转发"/error"请求 (由BasicErrorController处理,由DefaultErrorViewResolver解析)。可以通过这个搭配自定义错误页(方式一)使用!底层由ResponseStatusExceptionResolver支持。
@ResponseStatus
● value/code:类型HttpStatus,它是由Spring框架提供的枚举类型,用于表示HTTP响应码
● reason:自定义的相应信息
(没有办法自己创建一个HttpStatus对象)
● 方式四:实现HandlerExceptionResolver接口的解析器类
实现里面的resolveException方法,并返回ModelAndView对象。【用的比较少】,因为它可以更改优先级,会改变SpringBoot默认的处理异常的顺序。
7.8 Web原生组件 和 嵌入式Servlet容器
7.8.1 注入Servlet、Filter、Listener
(1) 方式一:@ServletComponentScan(basePackage="...") + ( @WebServlet / @WebFilter / @WebListener )
如,可以像原生Servlet一样书写,并且在类上标注@WebServlet,然后在主程序类(主配置类)上加上@ServletComponentScan注解,如果不写basePackage,则默认是主配置类所在包目录
(2) 方式二:使用RegistrationBean
可以在@Configuration配置类中,添加@Bean组件------ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean
@Configuration
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
7.8.2 嵌入式Servlet容器
SpringBoot默认支持的WebServer:Tomcat、Jetty、Undertow
(1) 原理
● SpringBoot应用在启动时 ,检测到是Web应用 ,就会创建一个ServletWebServerApplicationContext的IOC容器 。
● 该容器启动时,会寻找ServletWebServerFactory 。
● SpringBoot底层拥有ServletWebServerFactoryAutoConfiguration自动配置类 。该配置类中装配了Tomcat、Jetty、Undertow这些Servlet容器。并不会全部生效!导入了哪个,生效哪个!
● 生效的ServletWebServerFactory会创建对应的Servlet容器 ,并启用它。
(2) 如何切换服务器
默认的web场景启动器,内置了Tomcat的Servlet容器。可以排除它,并引入其他的Servlet容器。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除Tomcat -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
</dependencies>
八、数据访问
SpringBoot中有关数据访问的场景启动器:【spring-boot-starter-data-*】
8.1 JDBC场景
8.1.1 引入 场景启动器 + 驱动 依赖
(1) 导入spring-boot-starter-data-jdbc
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
里面内置了HikariCp连接池这个数据源 ,以及Spring中支持jdbc、事务相关的包
jdbc场景
(2) 引入驱动的依赖
⭕ 为什么官方不给我们提供在jdbc场景中呢?------因为它不知道我们要使用哪个数据库!所以我们需要自己引入
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--<version>5.1.49</version>-->
</dependency>
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、依照parent的properties重新声明版本(maven的属性的就近优先原则)
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.49</mysql.version>
</properties>
**spring-boot-starter-parent里面给我们管理了主流的驱动依赖。**我们可以不用写版本,但是一定要看看驱动版本和我们自己用的数据库对不对应的上!
● 配置驱动的连接信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
(3) 测试
@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
// jdbcTemplate.queryForObject("select * from account_tbl")
// jdbcTemplate.queryForList("select * from account_tbl",)
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}",aLong);
}
}
8.1.2 DataSource的自动配置类
● DataSourceConfiguration
数据库连接池的自动配置类,和DataSourceProperties绑定(prefix="spring.datasource"),当容器中没有自己的DataSource时才自动配置
底层配置好的连接池:HikariDataSource
● DataSourceTransactionManagerAutoConfiguration
事务管理器的自动配置类
● JdbcTemplateAutoConfiguration
JdbcTemplate的自动配置类,可以用来对数据库进行crud操作。
8.2 使用Druid数据源(第三方)
8.2.1 自定义整合
(1) 引入Druid连接池数据源
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
(2) 给Spring容器中添加DruidDataSource组件
● 使用【@ConfigurationProperties】,指定application全局配置文件的前缀 ,会自动地为当前组件的属性注入值。【底层:反射,需要依赖于无参构造+set】
@Configuration
public class MyDataSourceConfiguration{
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
// druidDataSource.serUrl("...");
// druidDataSource.serUsername("...");
// druidDataSource.serPassword("...");
return druidDataSource;
}
}
(3) 添加Druid功能到我们的自动配置类
功能很多,比如:查询慢SQL记录等等...
● 基本原则:参照官方文档说明,只要有<bean>标签,就可以在配置类中添加一个@Bean组件进Spring容器,并在里面放好指定的属性,就可以完成功能。
官方链接:
https://github.com/alibaba/druid
8.2.2 starter整合
(1) 引入druid-spring-boot-starter
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
(2) 根据官方的自动配置类,配置文件
给个示例:
application.yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
druid:
aop-patterns: com.atguigu.admin.* #监控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
8.3 整合MyBatis
● 引入mybatis提供的场景启动器
mybatis-spring-boot-starter包含
可以在官方中查看pom.xml文件提取坐标信息,也可以使用Spring Initializer向导帮助我们创建MyBatis Spring Boot Application。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
8.3.0 回顾原生mybatis整合spring
(1) mybatis自己的配置
● 在mybatis-config.xml全局配置文件 中,指定mapper包路径
● 在XXXmapper.xml映射sql文件 中,编写sql语句,并在namespace、sql标签id上绑定对应接口
通过sqlSessionFactory,创建sqlSession对象,调用getMapper方法时传入接口类名,创建代理对象,执行sql。
(2) 整合配置
需要在Spring容器中(Spring配置文件或者Spring配置类中),放入SqlSessionFactory组件(里面配置了dataSource信息、通过configLocation配置了mybatis-config.xml全局配置文件路径) ,并开启Mapper接口的扫描。
而在springboot中,提供了三种模式供我们进行整合。
8.3.1 配置模式【重要】
(0) 导入mybatis官方starter
(1) 编写Mapper接口,标注@Mapper注解。【推荐】 或者在SpringBoot主启动类中标注@MapperScan("Mapper接口的包路径")【AutoConfiguredMapperScannerRegistrar自动帮我们扫描该路径下的Mapper接口】
使用 @Mapper 注解时,需要在每个 Mapper 接口上都标注 @Mapper 注解。如果应用中的 Mapper 接口比较多,这样做会显得繁琐。此时,可以使用 @MapperScan 注解指定 Mapper 接口的扫描路径,以避免重复标注 @Mapper 注解。
(2) 编写sql映射文件并通过namespace、sql标签id,分别绑定接口类名、接口方法名。
(3) 在application.yaml中指定mybatis-config.xml全局配置文件路径,和mapper映射文件位置。
application.yaml
# 配置mybatis规则
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml
# 这里配置了,mybatis全局配置中就不用指定mapper的包路径了,它的作用:创建代理对象时找到对应的接口
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
⭕ 注意:
● 在yaml中配置mybatis.configuration,相当于在mybatis-config.xml(MyBatis全局配置文件)中配置 。只能二选一 !
● (1)只是帮我们把实现了接口的代理对象作为组件放入Spring的IOC容器中;(2)、(3)是mybatis中的逻辑,需要指定sql映射文件的位置、指定全局配置文件路径、以及两处绑定接口的操作。
8.3.2 注解模式
直接省略Sql的映射文件,在接口方法上书写sql。
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getById(Long id);
}
8.3.3 混合模式(最佳实践)
● 简单方法直接注解方式
● 复杂方法编写mapper.xml进行绑定映射
8.4 整合MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
● 建议安装 MybatisX 插件
● 只需要我们的Mapper继承 BaseMapper 就可以拥有简单的crud能力
● 一些Service接口的实现类直接调用Dao方法,没有什么额外的业务逻辑。我们可以让它的接口实现 IService<T>,让该实现类继承 ServiceImpl<Mapper, Bean>(Mapper是操作数据库的Mapper接口,Bean是返回封装的JavaBean)
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
ServiceImpl<Mapper, Bean>是Mybatis-Plus提供给我们的
}
public interface UserService extends IService<User> {
IService是Mybatis-Plus提供给我们的
}
65、数据访问-整合MyBatisPlus操作数据库_哔哩哔哩_bilibili
8.5 整合Redis
Redis是一个开源的(BSD许可),内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。
● 引入redis的场景启动器
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
自动装配了RedisAutoConfiguration 配置类,绑定了RedisProperties 属性类(前缀为"spring.redis")
● 配置了 客户端连接工厂:Lettuce、Jedis (早期默认Jedis,高版本默认Lettuce)
● 配置了 RedisTemplate<Object, Object> 组件。redisTemplate 是 【Spring Framework 提供的】 一个用于【简化 Redis 操作的模板类】,它可以使用多种 Redis 客户端实现,包括 Jedis 和 Lettuce 等。
● 配置了 StringRedisTemplate组件,它的key、value都是String
● 高版本的redis场景启动器,默认的客户端是Lettuce,如果要切换客户端到Jedis
(1) 引入Jedis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- 这里可以排除Lettuce,不排除的话,相当于有两个客户端包。在application.yaml中配置一下使用的客户端为Jedis也可以 -->
</dependency>
<!--导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
(2) applcaition.yaml全局指定redis客户端
spring:
redis:
host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com
port: 6379
password: lfy:Lfy123456
client-type: jedis
jedis:
pool:
max-active: 10
● 简单测试
class Test{
@Test
void testRedis(){
ValueOperations<String, String> operations = redisTemplate.opsForValue();
operations.set("hello","world");
String hello = operations.get("hello");
System.out.println(hello);
}
}
九、单元测试
SpringBoot 2.2.0 版本开始引入JUnit5作为单元测试默认库。
SpringBoot 2.4 以上版本移除了默认对Vintage的依赖。如果需要兼容JUnit4,需要自行引入
● 引入测试场景启动器
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 以下是对Junit4的兼容 -->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
在JUnit4中,我们在SpringBoot中测试:@SpringBootTest + @RunWith(SpringRunner.class)
在JUnit5中,我们在SpringBoot中测试:@SpringBootTest 即可。JUnit类具有Spring的功能。如可以使用@Autowired自动注入,使用@Transactional标注事务,测试完成后会自动回滚......
9.1 常用的测试注解
(1) @Test:无参数测试
(2) @ParameterizedTest:参数化测试
参数化测试时JUnit5很重要的一个新特性,它使得不同的参数多次运行测试 成为了可能。
● @ValueSource
● @NullSource
● @EnumSource
● @CsvSource
● @MethodSource:表示读取指定方法的返回值作为参数化测试入参**(注意方法返回需要是一个流,并且方法是静态的)**
⭕ 另外:它可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
(3) @RepeatedTest(次数):表示方法可重复执行的次数
(4) @DisplayName("名字"):为测试类或者测试方法设置展示名字
(5) @BeforeEach:在【每个】单元测试方法之前执行
(6) @AfterEach:在【每个】单元测试方法之后执行
(7) @BeforeAll:在【所有】单元测试方法之前执行,只执行一次
(8) @AfterAll:在【所有】单元测试方法之后执行,只执行一次
(9) @Tag("标签名"):设置单元测试的类别,可以在运行的时候筛选
(10) @Disabled:测试类或者测试方法不执行(报告方法跳过,而不是错误)
(11) @Timeout:当测试方法运行超出了时间会返回错误
● 可以设置时间单位
● 设置具体的时间数
(12) @ExtendWith:给测试类或者测试方法提供【扩展类】的引用
例如:
● @SpringBootTest:复合注解
** ● @ExtendWith(SpringExtension.class)------对接了Spring的测试驱动**
● @BootstrapWith(SpringBootTestContextBootstrapper.class)
其他的注解:JUnit 5 User Guide
import org.junit.jupiter.api.Test; //注意这里使用的是jupiter的Test注解!!
@DisplayName("我的测试类")
public class TestDemo {
@Test
@DisplayName("第一次测试")
public void firstTest() {
System.out.println("hello world");
}
9.2 断言
断言(Assertions)是测试方法中的核心部分,是org.junit.jupiter.api.Assertions包下的静态方法。
在maven项目中进行test,会生成一个详细的测试报告。
(1) 简单断言
● 参数列表:期望值(Boolean类型的就不用期望值了)、实际运行值、String(message)(可以为空。当断言失败时,输出的消息)
简单断言
(2) 数组断言 :判断两个数组长度和内容上是否相等
● 参数列表:期望值、实际运行值、String(message)(可以为空。当断言失败时,输出的消息)
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
(3) 组合断言 :多个断言组合起来,只有全部断言成功,才成功;否则,报告所有失败的断言【即所有断言均会执行,无论是否存在失败】
● 参数列表:String(heading,指定一个组合名字用于测试报告显示)、Executable可变形参
Executable是一个接口,支持Lambda表达式编写多个断言
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
(4) 异常断言 :如果出现指定异常,才断言成功;否则失败
● 参数列表:期望值、Executable、String(message)
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> {
int i = 10 / 0;
}, message:"业务逻辑居然正常运行?");
}
(5) 超时断言 :断定方法会在指定时间内运行完成
● 参数列表:期望值、Executable、String(message)
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
(6) 快速失败
fail方法,直接使得测试失败
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}
9.3 前置条件
JUnit5 中的前置条件(Assumptions) 类似于断言,都会在不满足条件时,终止执行。
● 区别 在于:断言会报告方法失败 ,而前置条件不满足会报告方法被跳过。
@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}
● assumeTrue 和 assumeFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。
● assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象 。只有条件满足时,Executable 对象才会被执行。
9.4 嵌套测试
● @Nested:加在测试类的【内部类】上。
● 测试【内部类】的方法,【会】驱动 外部类的Before(After)Each/All方法;
● 测试【外部类】的方法,【不会】驱动内部类的Before(After)Each/All方法。
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
十、指标监控
未来每一个微服务在云上部署 以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
视频:77、指标监控-SpringBoot Actuator与Endpoint_哔哩哔哩_bilibili
十一、原理解析
11.1 多环境适配------Profile
生产环境、开发环境、测试环境等等...配置文件内容、配置类等 可能会随着环境的不同而有不同的设置。
11.1.1 配置文件application.yaml相关
如果我们每次都在一个配置文件上修改,显得太麻烦了。SpringBoot允许我们使用多个配置文件。
● 默认的配置文件:application.yaml------任何时候都会加载;
● 自定义环境配置文件:application-{env}.yaml
⭕ 激活 自定义环境配置文件
● 方式一:在默认的配置文件中指定
假定自定义环境配置文件:application-myEnv1.yaml、application-myEnv2.yaml
(1) 在默认配置文件中【激活某个】自定义的环境配置文件:
application.yaml
spring.profiles.active = myEnv1
(2) 在默认配置文件中,对profile进行分组,【激活组】:
spring.profiles.group.myEnv[0] = myEnv1
spring.profiles.group.myEnv[0] = myEnv2
如果在默认配置文件中:spring.profiles.active = myEnv,则两个配置文件都会生效
● 方式二:命令行激活 (我们的spring-boot项目最终都可以被打包成一个jar包部署到服务器)
可以在外部修改配置文件的任何信息
java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
11.1.2 @Profile条件装配
● 可以加在配置类 ,也可以加在方法上 。可以指定在某个环境生效。(application.yaml中spring.profiles.active指定)
application.yaml
spring.profile.active: production
@Configuration(proxyBeanMethods = false)
@Profile("production") 【默认是"default",可以写成数组形式】
public class ProductionConfiguration {
// ...
}
11.2 外部化配置
将数据不是写死在代码中,而是抽取成外部文件,集中管理。
SpringBoot中支持各种外部化配置,其中包括:Java Properties文件,YAML文件,环境变量和命令行参数。
举例:直接获取系统环境变量的值
11.3 自定义starter
模拟源码编写自定义starter。
● 步骤
(1) 编写一个maven项目,用来作为myservice-spring-boot-starter,只写pom.xml,里面引入了MyServiceAutoConfiguration自动配置类的依赖(除了pom文件以外,没有任何其他的Java代码、配置文件)
(2) 编写一个maven项目,用来作为MyServiceAutoConfiguration自动配置类 (被自定义starter引用),编写自动配置类(@Configuration + @EnableConfigurationProperties)、业务组件(@Bean + 条件装配)、MyServiceProperties类(@ConfigurationProperties + 绑定到自动配置类)。
(3) 在MyServiceAutoConfiguration项目中 的resources目录下准备META-INF目录,存放spring.factories文件 ,里面编写项目启动时自动加载的配置类**"xxx.xxx.EnableAutoConfiguration=MyServiceAutoConfiguration自动配置类的全类名"**
(4) 在外部项目中,引入my-spring-boot-starter即可使用我们自定义的功能。
● myservice-spring-boot-starter:maven项目
pom.xml
...
<!-- my-spring-boot-starter坐标 -->
<groupId>xxxx</groupId>
<artifactId>myservice-spring-boot-starter</artifactId>
<version>1.0</version>
...
<dependencies>
<dependency>
<!-- 引入MyAutoConfiguration自动配置类 -->
<groupId>xxx</groupId>
<artifactId>myservice-spring-boot-starter-autoconfigure</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
...
● MyServiceAutoConfiguration项目myservice-spring-boot-starter-autoconfigure:maven项目
pom.xml
...
<!-- 自己的坐标 -->
<groupId>xxx</groupId>
<artifactId>myservice-spring-boot-starter-autoconfigure</artifactId>
<version>1.0</version>
...
// 逻辑业务组件
@Service
public class MyService{
@Autowired
private MyServiceProperties myServiceProperties;
public string sayHello(String name){
return myServiceProperties.getHelloWord() + name;
}
}
@ConfigurationProperties(prefix="my.myService")
public class MyServiceProperties{
private String helloWord;
public String getHelloWord(){
return helloWord;
}
}
application.yaml
my:
myService:
helloWord: hello~
@Configuration
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration{
@ConditionalOnMissingBean(MyService.class)
@Bean
public MyService myService(){
MyService myService = new MyService();
return myService;
}
}
在项目resources文件夹下创建/META-INF,创建spring.factories文件
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
MyServiceAutoConfiguration全类名
● 自己测试myservice-spring-boot-starter,创建一个外部项目
pom.xml
...
<!-- 自己的坐标(略) -->
<!-- 引入我们自定义的starter -->
<dependencies>
<dependency>
<groupId>xxxx</groupId>
<artifactId>myservice-spring-boot-starter</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
...
@RestController
public class MyTestController{
@Autowired
private MyService myService;
@RequestMapping("/hello")
public String sayHello(){
return myService.sayHello("张三");
}
}
11.4 原理总结
【创建】并【运行】SpringApplication。
● 【创建】:主要是通过spring.factories 文件去创建一些关键组件。如:启动引导器(BootStrapppers)、IOC初始化器(ApplicationContextInitializer)、应用监听器(ApplicationListener)
● 【运行】:找到spring.factories中所有SpringApplicationRunListener,对每个阶段进行监听 。包括准备环境 (读取并绑定系统配置、外部配置源等等)、根据Web项目类型创建IOC容器 、刷新IOC容器(创建Bean组件) 、**Runner启动(应用启动后会调用run方法)**等等。
------以上ApplicationContextInitializer、ApplicationListener、SpringApplicationRunListener、Runner均可以自定义。
最后编辑于:2024-11-17 10:24:30
© 著作权归作者所有,转载或内容合作请联系作者
喜欢的朋友记得点赞、收藏、关注哦!!!