同以前的properties用法
优先级高于yml的方式。
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。
非常适合用来做以数据为中心的配置文件
总结:properties和yml的区别
properties 和 yml 都是 Spring Boot 支持的两种配置文件,其中 yml 格式的配置文件可以看作是对 properties 配置文件的升级。它们的主要区别有 4 点
:定义和定位不同、语法不同:yml 的语法更简单,且可读性更高、yml 可以更好的配置多种数据类型,比如对象和集合、yml 可以跨语言使用,通用性更好。
左对齐
即可'#'表示注释
,并且为单行注释(即:一个#只能注释一行)。字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义
k: v
行内写法: k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3
行内写法: k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
//读取yml类型文件中的内容。
@ConfigurationProperties(prefix ="person")
@Component
@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> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
-----------------------------------
@Data
public class Pet {
private String name;
private Double weight;
}
person:
# 单引号会将 \n作为字符串输出 双引号会将\n 作为换行输出
# 双引号不会转义,单引号会转义
userName: zhangsan #字面量
boss: true
birth: 2019/12/9
age: 18
# interests: [篮球,足球] #数组
interests:
- 篮球
- 足球
- 18
animal: [阿猫,阿狗] # list
# score: # map
# english: 80
# math: 90
score: {english:80,math:90}
salarys: #set
- 9999.98
- 9999.99
pet: # 对象
name: 阿狗
weight: 99.99
allPets: # Map> allPets
sick:
- {name: 阿狗,weight: 99.99}
- name: 阿猫
weight: 88.88
- name: 阿虫
weight: 77.77
health:
- {name: 阿花,weight: 199.99}
- {name: 阿明,weight: 199.99}
#spring:
# banner:
# image:
# bitdepth: 4
# cache:
# type: redis
# redis:
# time-to-live: 11000
控制层代码:
package com.atguigu.boot.controller;
import com.atguigu.boot.bean.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
Person person;
@RequestMapping("/person")
public Person person(){
String userName = person.getUserName();
System.out.println(userName);
return person;
}
}
启动主启动类进行访问测试:
说明:在yml文件中,配置springboot提供的属性有提示,而自己写的属性没有提示,不好。如何让自己写的属性也有提示呢???
自定义的类和配置文件绑定一般没有提示。
解决:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
covered later in this document
).covered later in this document
).covered later in this document
).covered later in this document
).covered later in this document
).If you want to keep those Spring Boot MVC customizations and make more
MVC customizations
(interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则
If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
声明 WebMvcRegistrations 改变默认底层组件
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.
使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
创建项目:
添加依赖:
只要静态资源放在类路径(resources )
下:
访问 : 当前项目根路径/ + 静态资源名
原理: 静态映射/**。
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
默认在以上这4个目录下,想要改变默认的静态资源路径需以下配置:这样现在的静态资源文件只能放在haha目录下,放在默认的4个目录下就访问不了了。
spring:
web:
resources:
static-locations: [classpath:/haha/]
#因为是数组,所以可以配置多个,用逗号隔开如: [classpath:/haha/,/aa/]
说明:建议静态资源访问时加上前缀,比如:一个web应用有很多静态资源和动态资源,项目中有很多拦截器,只有登录后才能访问一些动态请求,拦截器拦截/**
相当于是把静态资源也拦截了,为了拦截器配置方便,把静态资源访问都加上前缀,这样拦截器放行以指定前缀路径开始的所求请求,这样就非常方便过滤掉静态资源。
默认无前缀
spring:
mvc:
static-path-pattern: /res/**
#static-path-pattern: /** 默认无前缀
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
说明:它把js、css封装成了一个个的jar包,我们想用的时候只需要导入依赖即可。
自动映射 /webjars/**
官方地址:https://www.webjars.org/
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>jqueryartifactId>
<version>3.5.1version>
dependency>
访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js
后面地址要按照依赖里面的包路径
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
web:
resources:
static-locations: [classpath:/haha/]
index.html页面:
说明:连资源名都不用写了。
说明:访问网站时的小图标。
把一个图片放在静态资源目录下,名字为固定写法:favicon.ico
。
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath
//ServletRegistrationBean 给应用注册Servlet、Filter....
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
//webjars的规则
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
//
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
spring:
web:
resources:
add-mappings: false #禁用所有静态资源规则,默认为true
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
//要用欢迎页功能,必须是/**
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 调用Controller /index
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
@xxxMapping;
Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
○ 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
○ 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
○ 核心Filter;HiddenHttpMethodFilter过滤器
springmvc需要在xml中配置HiddenHttpMethodFilter过滤器,而在SpringBoot中默认配置的有过滤器,但是要在yml/properties配置文件中手动开启
。)○ 扩展:如何把_method 这个名字换成我们自己喜欢的。
测试:
index.html页面:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>atguigu,欢迎您h1>
测试REST风格;
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
form>
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit"/>
form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete"/>
<input name="_m" type="hidden" value="delete"/>
<input value="REST-DELETE 提交" type="submit"/>
form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT"/>
<input value="REST-PUT 提交" type="submit"/>
form>
<hr/>
测试基本注解:
<ul>
<a href="car/3/owner/lisi?age=18&inters=basketball&inters=game">car/{id}/owner/{username}a>
<li>@PathVariable(路径变量)li>
<li>@RequestHeader(获取请求头)li>
<li>@RequestParam(获取请求参数)li>
<li>@CookieValue(获取cookie值)li>
<li>@RequestBody(获取请求体[POST])li>
<li>@RequestAttribute(获取request域属性)li>
<li>@MatrixVariable(矩阵变量)li>
ul>
/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;<br/>
/cars/sell;low=34;brand=byd,audi,yd ;矩阵变量 <br/>
页面开发,cookie禁用了,session里面的内容怎么使用;
session.set(a,b)---> jsessionid ---> cookie ----> 每次发请求携带。
url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递.
/boss/1/2
/boss/1;age=20/2;age=20
<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)a>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)a>
<a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}a>
<br/>
<form action="/save" method="post">
测试@RequestBody获取数据 <br/>
用户名:<input name="userName"/> <br>
邮箱:<input name="email"/>
<input type="submit" value="提交"/>
form>
<ol>
<li>矩阵变量需要在SpringBoot中手动开启li>
<li>根据RFC3986的规范,矩阵变量应当绑定在路径变量中!li>
<li>若是有多个矩阵变量,应当使用英文符号;进行分隔。li>
<li>若是一个矩阵变量有多个值,应当使用英文符号,进行分隔,或之命名多个重复的key即可。li>
<li>如:/cars/sell;low=34;brand=byd,audi,ydli>
ol>
<hr/>
测试原生API:
<a href="/testapi">测试原生APIa>
<hr/>
测试复杂类型:<hr/>
测试封装POJO;
<form action="/saveuser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
宠物: <input name="pet" value="啊猫,3"/>
<input type="submit" value="保存"/>
form>
<br>
body>
html>
控制层方法:
package com.atguigu.boot.controller;
import org.springframework.web.bind.annotation.*;
@RestController
public class HelloController {
// @RequestMapping(value = "/user",method = RequestMethod.GET)
@GetMapping("/user")//简化写法
public String getUser(){
return "GET-张三";
}
// @RequestMapping(value = "/user",method = RequestMethod.POST)
@PostMapping("/user")
public String saveUser(){
return "POST-张三";
}
// @RequestMapping(value = "/user",method = RequestMethod.PUT)
@PutMapping("/user")
public String putUser(){
return "PUT-张三";
}
@DeleteMapping("/user")
// @RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
//扩展点:如何把 _method 这个名字换成我们自己喜欢的
}
----------------------------底层源码------------
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
Rest原理(表单提交要使用REST的时候)
_method=PUT
_method
的值。Restful使用客户端工具,
# 说明:这一项配置是选择性开启,因为如果是前后端分离,我们不做前端开发,只需要返回json数据就行了,也就不需要配置。
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能,把post请求解析为put.delete
访问页面:
此外还可以修改这个参数_method为其它自定义的参数。
添加配置类:
package com.atguigu.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
@Configuration(proxyBeanMethods = false)//组件没有依赖关系,快速放
public class WebConfig /*implements WebMvcConfigurer*/ {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}
测试index.html页面必须换为_m
才能访问成功。
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet-》doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
所有的请求映射都在HandlerMapping中。
/
会被省略掉); protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
index.html页面:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>atguigu,欢迎您h1>
测试REST风格;
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
form>
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit"/>
form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete"/>
<input name="_m" type="hidden" value="delete"/>
<input value="REST-DELETE 提交" type="submit"/>
form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT"/>
<input value="REST-PUT 提交" type="submit"/>
form>
<hr/>
测试基本注解:
<ul>
<a href="car/3/owner/lisi?age=18&inters=basketball&inters=game">car/{id}/owner/{username}a>
<li>@PathVariable(路径变量)li>
<li>@RequestHeader(获取请求头)li>
<li>@RequestParam(获取请求参数)li>
<li>@CookieValue(获取cookie值)li>
<li>@RequestBody(获取请求体[POST])li>
<li>@RequestAttribute(获取request域属性)li>
<li>@MatrixVariable(矩阵变量)li>
ul>
/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;<br/>
/cars/sell;low=34;brand=byd,audi,yd ;矩阵变量 <br/>
页面开发,cookie禁用了,session里面的内容怎么使用;
session.set(a,b)---> jsessionid ---> cookie ----> 每次发请求携带。
url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递.
/boss/1/2
/boss/1;age=20/2;age=20
<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)a>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)a>
<a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}a>
<br/>
<form action="/save" method="post">
测试@RequestBody获取数据 <br/>
用户名:<input name="userName"/> <br>
邮箱:<input name="email"/>
<input type="submit" value="提交"/>
form>
<ol>
<li>矩阵变量需要在SpringBoot中手动开启li>
<li>根据RFC3986的规范,矩阵变量应当绑定在路径变量中!li>
<li>若是有多个矩阵变量,应当使用英文符号;进行分隔。li>
<li>若是一个矩阵变量有多个值,应当使用英文符号,进行分隔,或之命名多个重复的key即可。li>
<li>如:/cars/sell;low=34;brand=byd,audi,ydli>
ol>
<hr/>
测试原生API:
<a href="/testapi">测试原生APIa>
<hr/>
测试复杂类型:<hr/>
测试封装POJO;
<form action="/saveuser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
宠物: <input name="pet" value="啊猫,3"/>
<input type="submit" value="保存"/>
form>
<br>
body>
html>
说明:在SpringMvc学习时获取的是单个值,现在学习的是可以一次性获取所有值。
ParameterTestController类:
package com.atguigu.boot.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 ParameterTestController {
// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
//@PathVariable:获取restful风格的请求参数:
// 1.可以单个的获取
// 2.也可以直接获取一个Map集合
//@RequestHeader:获取请求头信息
// 1.可以单个的获取
// 2.也可以一次性获取所有的请求头信息
//@RequestParam:获取普通的请求参数,如果请求参数名和方法的形参名相同时可以省略。
// 1.获取一个参数
// 2.获取的同名的参数有多个值。通过List集合或可变形参或数组
// 3.获取所有的请求参数,包括参数只有一个值和参数有多个值
//@CookieValue:获取Cookie里面的值:
// 1.单独获取一个Cookie里面Idea-9ccb666a的value值
// 2.获取整个Cookie信息
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("Idea-9ccb666a") String idea,
@CookieValue("Idea-9ccb666a") Cookie cookie
){
Map<String,Object> map = new HashMap<>();
// map.put("id",id);
// map.put("name",name);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
System.out.println(cookie.getName()+"===>"+cookie.getValue());
return map;
}
//@RequestBody:获取请求体的信息
@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
}
输出cookie信息:
获取请求体信息:
RequestController类:
package com.atguigu.boot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Controller
public class RequestController {
@GetMapping("/goto")
public String goToPage(HttpServletRequest request){
request.setAttribute("msg","成功了...");
request.setAttribute("code",200);
//现在还没有学习模板引擎,不能进行前端开发,所以模拟一个跳转页面
return "forward:/success"; //转发到 /success请求
}
@ResponseBody
@GetMapping("/success")
//@RequestAttribute:获取request域属性的值:
// 1.可以在方法的参数上,使用@RequestAttribute注解
// 2.也可以直接通过原生的servletApi的方式获取。因为是请求转发用的是同一个域对象
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false)Integer code,
HttpServletRequest request){
Object msg1 = request.getAttribute("msg");
Map<String,Object> map = new HashMap<>();
map.put("reqMethod_msg",msg1);
map.put("annotation_msg",msg);
return map;
}
}
package com.atguigu.boot.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 ParameterTestController {
//@MatrixVariable:获取矩阵变量的值
//1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启(2种):原理。对于路径的处理。UrlPathHelper进行解析。
// removeSemicolonContent(移除分号内容)支持矩阵变量的
//3、矩阵变量必须有url路径变量才能被解析
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}
// 矩阵变量获取相同名字的参数:/boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}
}
配置类:
package com.atguigu.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;
@Configuration(proxyBeanMethods = false)//组件没有依赖关系,快速放
public class WebConfig /*implements WebMvcConfigurer*/ {
//方式一:使用@Bean 的方式返回一个 WebMvcConfigurer类型的组件
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除;后面的内容。矩阵变量功能就可以生效urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
//方式二:实现WebMvcConfigurer接口,重写对应的方法。
// @Override
// public void configurePathMatch(PathMatchConfigurer configurer) {
//
// UrlPathHelper urlPathHelper = new UrlPathHelper();
// // 不移除;后面的内容。矩阵变量功能就可以生效
// urlPathHelper.setRemoveSemicolonContent(false);
// configurer.setUrlPathHelper(urlPathHelper);
// }
}
说明:控制器方法可以写Servlet API原生方式的参数有以下几种。
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
ServletRequestMethodArgumentResolver参数解析器 解析以上的部分参数
//源码:
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
Map、Model(map、model里面的数据会被放在request的请求域 相当于 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据,
request.getAttribute();
Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map
mavContainer.getModel(); 获取到值的
可以自动类型转换与格式化,可以级联封装。
/** 页面:
* 姓名:
* 年龄:
* 生日:
* 宠物姓名:
* 宠物年龄:
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
result
0 - 支持方法上标注@RequestMapping
1 - 支持函数式编程的
xxxxxx
// Actually invoke the handler.
//DispatcherServlet -- doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法
//ServletInvocableHandlerMethod
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
确定将要执行的目标方法的每一个参数的值是什么;
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
============InvocableHandlerMethod==========================
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
ServletModelAttributeMethodProcessor 这个参数处理器支持是否为简单类型。
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
byte – > file
@FunctionalInterfacepublic interface Converter
未来我们可以给WebDataBinder里面放自己的Converter;
private static final class StringToNumber
自定义 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;
}
});
}
};
}
将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据。
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
InternalResourceView:
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
rd.forward(request, response);
}
}
暴露模型作为请求域属性
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {
//model中的所有数据遍历挨个放在请求域中
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}
说明:
说明:使用SpringBoot返回JSON数据的步骤:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jsonartifactId>
<version>2.3.4.RELEASEversion>
<scope>compilescope>
dependency>
进入到web场景底层可以看到引入了json依赖:
给前端自动返回json数据;
(1.1.1)返回值解析器
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
RequestResponseBodyMethodProcessor
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
// 使用消息转换器进行写出操作
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
(1.1.2)返回值解析器原理
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person
0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的。
最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)
根据客户端接收能力不同,返回不同媒体类型的数据。
说明:响应xml需要引入依赖。
<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-xmlartifactId>
dependency>
说明:可以使用 postman测试发送请求,返回json数据或者xml数据。
Accept字段
为:application/xmlAccept字段
为:application/json说明:为了方便内容协商,开启基于请求参数的内容协商功能。
开启请求参数内容协商模式,之后在url上携带请求参数:format=json或者format=xml
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
发请求:
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
原理:
确定客户端接收什么样的内容类型;
1、Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
2、最终进行内容协商返回给客户端json即可。
1、判断当前响应头中是否已经有确定的媒体类型。MediaType
2、获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】
○ contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
○ HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
3、遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。
5、客户端需要【application/xml】。服务端能力【10种、json、xml】
6、进行内容协商的最佳匹配媒体类型
7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
导入了jackson处理xml的包,xml的converter就会自动进来
WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
实现多协议数据兼容。json、xml、x-guigu
0、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
1、Processor 处理方法返回值。通过 MessageConverter 处理
2、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter;
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
}
}
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
官网:https://www.thymeleaf.org/
缺点:thymeleaf模板引擎并不是一个高并发的引擎,对于开发高并发的应用进行页面跳转,应该是做一个前后端分离,让职业的前端人员来做。或者做一个高并发的后台管理系统,我们应该选择其它的模板引擎。但是对于一个简单的单体应用就可以使用它了。
总结:thymeleaf用于做一个前后端不分离的简单地单体架构。
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
文本值: ‘one text’ , ‘Another one!’ ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false
空值: null
变量: one,two,… 变量不能有空格
字符串拼接: +
变量替换: |The name is ${name}|
运算符: + , - , * , / , %
运算符: and , or
一元运算: ! , not
比较: > , < , >= , <= ( gt , lt , ge , le )等式: == , != ( eq , ne )
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
无操作: _
设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
fieldset>
form>
设置多个值
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法 th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
所有h5兼容的标签写法:
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onionstd>
<td th:text="${prod.price}">2.41td>
<td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onionstd>
<td th:text="${prod.price}">2.41td>
<td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">viewa>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administratorp>
<p th:case="#{roles.manager}">User is a managerp>
<p th:case="*">User is some other thingp>
div>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
底层源码:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }
自动配好的策略
页面默认放的位置:必须放在templates目录下,资源名必须是以.html后缀为结尾。
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1 th:text="${msg}">哈哈h1>
<h2>
<a href="www.atguigu.com" th:href="${link}">去百度a> <br/>
<a href="www.atguigu.com" th:href="@{link}">去百度2a>
h2>
body>
html>
配置文件:添加项目的前置访问路径
# 添加项目的前置访问路径
server:
servlet:
context-path: /world
控制层方法:
@Controller
public class ViewTestController {
@GetMapping("/atguigu")
public String atguigu(Model model){
//model中的数据会被放在请求域中 request.setAttribute("a",aa)
model.addAttribute("msg","你好 guigu");
model.addAttribute("link","http://www.baidu.com");
return "success";
}
}
启动主启动类进行测试:
thymeleaf、web-starter、devtools、lombok
自动配置好,我们只需要把所有静态资源放到 static 文件夹下
th:action=“@{/login}”
th:insert/replace/include
@Controller
public class IndexController {
/**
* 1.浏览器发送请求,跳转到登录页面
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){
return "login";
}
/**
* 2.在登录页面输入用户名密码,点击登录通过表单发送请求: th:action="@{/login}"
* 登录成功后跳转到后端管理的首页
* 但是这样用请求转发跳转会有表单重复提交问题,所以再写一个方法,通过重定向跳转来解决。
*/
@PostMapping("/login")
public String main(User user, HttpSession session, Model model){ //模拟登录成功后才能访问
if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//return "main"; 直接使用请求转发会有表单重复提交问题
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}
/**
* 3.去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session,Model model){
//在登录成功后的页面判断是否登录:
// 方式一:每个方法写一个判断,但是这样太麻烦。
// 方式二:可以直接使用拦截器,过滤器统一处理。
//方式一写法:
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "main";
}else {
//回到登录页面
model.addAttribute("msg","请重新登录");
return "login";
}
}
}
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
//表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
model.addAttribute("users",users);
return "table/dynamic_table";//因为页面是放在templates目录下的table目录下
}
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>#th>
<th>用户名th>
<th>密码th>
tr>
thead>
<tbody>
<tr class="gradeX" th:each="user,stats:${users}">
<td th:text="${stats.count}">Tridenttd>
<td th:text="${user.userName}">Internettd>
<td >[[${user.password}]]td>
tr>
tbody>
table>
/**
* 登录检查
* 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;//true:代表放行
}
//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");
request.getRequestDispatcher("/").forward(request,response);
return false;//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);
}
}
/**
* 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/**"); //放行的请求
}
}
(3)拦截器原理
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
form>
package com.atguigu.admin.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/**
* 文件上传测试
*/
@Slf4j
@Controller
public class FormTestController {
//跳转到文件提交页面
@GetMapping("/form_layouts")
public String form_layouts(){
return "form/form_layouts";
}
/**
*获取表单普通项:email、username
*获取文件表单项:MultipartFile 自动封装上传过来的文件:
* 1.通过MultipartFile, 获取单文件上传
* 2.通过MultipartFile[]数组, 获取多文件上传(表单项一次性可以上传多个文件)
*
* @param email
* @param username
* @param headerImg
* @param photos
* @return
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
//headerImg.getSize()获取上传文件的大小
//photos.length获取有几个上传文件,即数组的长度。
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email,username,headerImg.getSize(),photos.length);
if(!headerImg.isEmpty()){ //文件不为空才有文件
//保存到文件服务器,OSS服务器(阿里云对象存储服务器)
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("D:\\cache\\"+originalFilename));
}
//遍历获取的文件数组
if(photos.length > 0){//大于0,数组有文件
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){//每个文件不为空
String originalFilename = photo.getOriginalFilename();//获取上传的文件名
photo.transferTo(new File("D:\\cache\\"+originalFilename));//用的本地磁盘
}
}
}
return "main";
}
}
(3)配置文件:
#配置单个文件上传的最大值,默认为1MB
spring.servlet.multipart.max-file-size=10MB
#配置总请求上传的文件量的最大值,默认为10MB
spring.servlet.multipart.max-request-size=100MB
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos)
默认情况下,Spring Boot提供/error处理所有错误的映射
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
要对其进行自定义,添加View解析为error(即:可以自定义返回的错误页面)
要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
error/下的4xx,5xx页面会被自动解析;
自定义错误页
○ error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
@ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的
@ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error
Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
○ response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
ErrorViewResolver 实现自定义处理异常;
○ response.sendError 。error请求就会转给controller
○ 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
○ basicErrorController 要去的页面地址是
ErrorViewResolver ;
ErrorMvcAutoConfiguration 自动配置异常处理规则
○ 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
○ 容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException
2、进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;
○ 3.2.1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
○ 3.2.2、默认没有任何人能处理异常,所以异常会被抛出
3.2.2.1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
3.2.2.2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
3.2.2.3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
4、模板引擎最终响应这个页面 error/500.html
@ServletComponentScan(basePackages = “com.atguigu.admin”) :指定原生Servlet组件都放在那里
注册servlet:@WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器?
注册过滤器:@WebFilter(urlPatterns={“/css/“,”/images/”})
注册监听器:@WebListener
测试1:注册servlet:@WebServlet+@ServletComponentScan
package com.atguigu.admin.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//servlet3.0提供的注解,配置能够处理的路径
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
private static final long serialVersionUID = -3250793476589229955L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
package com.atguigu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan(basePackages = "com.atguigu.admin") //spring提供的功能,可以把自己写的servlet扫描进来
public class Boot05WebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(Boot05WebAdminApplication.class, args);
}
}
测试2:注册Filter:@WebFilter+@ServletComponentScan
package com.atguigu.admin.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
package com.atguigu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan(basePackages = "com.atguigu.admin") //spring提供的功能,可以把自己写的servlet扫描进来
public class Boot05WebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(Boot05WebAdminApplication.class, args);
}
}
测试3:注册Listener:@WebListener+@ServletComponentScan
package com.atguigu.admin.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@Slf4j
@WebListener
public class MySwervletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目销毁");
}
}
package com.atguigu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan(basePackages = "com.atguigu.admin") //spring提供的功能,可以把自己写的servlet扫描进来
public class Boot05WebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(Boot05WebAdminApplication.class, args);
}
}
扩展:DispatchServlet 如何注册进来
ServletRegistrationBean
把 DispatcherServlet 配置进来。spring.mvc.servlet.path=/mvc/
,一般也不需要修改,没有啥意义。Tomcat-Servlet;
多个Servlet都能处理到同一层路径,精确优选原则
A: /my/
B: /my/1
说明:使用配置类代替原来的:3个对应的注解+主启动类注解的方式。
步骤一:把原来的注解方式注释掉,但是这几个类还是要有的。
步骤二:编写配置类
package com.atguigu.admin.config;
import com.atguigu.admin.servlet.MyFilter;
import com.atguigu.admin.servlet.MyServlet;
import com.atguigu.admin.servlet.MySwervletContextListener;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
/**
* 1、MyServlet --> /my
* 2、DispatcherServlet --> /
*/
// (proxyBeanMethods = true):保证依赖的组件始终是单实例的,默认就是true单实例
@Configuration(proxyBeanMethods = true)
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();
//写法一:拦截和servlet请求相同的路径
// 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);
}
}
说明:即springboot默认提供的tomact服务器,当然可以进行切换。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-undertowartifactId>
dependency>
说明:可以修改服务器的一些规则,如:端口号,字符编码等。不但是tomact服务器,其它的web服务器同样可以修改。
WebServerFactoryCustomizer
xxxxxCustomizer:定制化器,可以改变xxxx的默认规则
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
@Configuration
public class AdminWebConfig implements WebMvcConfigurer
场景starter
- xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项
说明:在SpringBoot中想要操作数据库完成增删改差,按照以往的经验:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>
说明:导入jdbc场景会自动引入以下依赖:
问题:操作数据库还需要有数据库驱动,那为什么导入JDBC场景,官方不导入驱动数据库驱动呢???
导入MySQL的数据库驱动:用谁的数据库就导入谁的驱动。
8.0.22
,所以在导入驱动时就不需要写版本号了。即:数据库版本为8.0,那么数据库驱动可以使用8.0以及8.0以下的版本
)想要修改版本:
方式一:直接依赖引入具体版本(maven的就近依赖原则)
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
方式二:重新声明版本(maven的属性的就近优先原则)
<properties>
<java.version>1.8java.version>
properties>
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration
因为此时没有整合MyBatis等第三方的持久层框架,这是spring官方提供的一个crud的小工具
)
# 数据库连接地址、用户名、密码、数据库驱动的名字、数据库的类型不用指定默认就是HikariDataSource
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_db
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
# type: com.zaxxer.hikari.HikariDataSource
# 配置sprig下的jdbc下的template属性:查询的超时时间 单位:秒 表示3秒之内没有查询到数据则超时
jdbc:
template:
query-timeout: 3
package com.atguigu;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
//使用spring提供的工具jdbcTemplate来进行crud操作。
@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Autowired //这个组件spring容器注册好了,直接注入即可。
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
// jdbcTemplate.queryForObject("select * from account_tbl")
// jdbcTemplate.queryForList("select * from account_tbl",)
Long aLong = jdbcTemplate.queryForObject("select count(*) from t_account", Long.class);
log.info("记录总数:{}",aLong);
}
}
说明:这是阿里巴巴提供的数据源。
https://github.com/alibaba/druid
整合第三方数据库连接池技术的两种方式
添加依赖:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.17version>
dependency>
spring原生方式:使用xml文件注册组件并进行配置属性。现在使用配置类进行代替。
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />
配置文件:
# 数据库连接地址、用户名、密码、数据库驱动的名字、数据库的类型不用指定 默认就是HikariDataSource
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_db
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
# type: com.zaxxer.hikari.HikariDataSource
#配置sprig下的jdbc下的template属性:查询的超时时间 单位:秒 表示3秒之内没有查询到数据则超时
jdbc:
template:
query-timeout: 3
配置类:
@Configuration
public class MyDataSourceConfig {
// 默认的自动配置是判断容器中没有才会配@ConditionalOnMissingBean(DataSource.class)
@ConfigurationProperties("spring.datasource")//方式二:设置连接数据库的信息,读取配置文件设置的连接信息
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
//设置连接数据库的信息,方式一:这是写死的写法
// druidDataSource.setUrl();
// druidDataSource.setUsername();
// druidDataSource.setPassword();
return druidDataSource;
}
}
测试类:
package com.atguigu;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
//使用spring提供的工具jdbcTemplate来进行crud操作。
@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Autowired
DataSource dataSource;
@Test
void contextLoads() {
log.info("数据源类型:{}",dataSource.getClass());
}
}
StatViewServlet的用途包括:
web.xml原生方式:配置监控功能、
<servlet>
<servlet-name>DruidStatViewservlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>DruidStatViewservlet-name>
<url-pattern>/druid/*url-pattern>
servlet-mapping>
现在使用配置类:
package com.atguigu.admin.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Arrays;
@Configuration
public class MyDataSourceConfig {
// 默认的自动配置是判断容器中没有才会配@ConditionalOnMissingBean(DataSource.class)
@ConfigurationProperties("spring.datasource")//方式二:设置连接数据库的信息,读取配置文件设置的连接信息
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
//设置连接数据库的信息,方式一:这是写死的写法
// druidDataSource.setUrl();
// druidDataSource.setUsername();
// druidDataSource.setPassword();
// 加入监控功能
druidDataSource.setFilters("stat");
return druidDataSource;
}
/**
* 配置 druid的监控页功能
* @return
*/
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
//配置拦截的路径
ServletRegistrationBean<StatViewServlet> registrationBean =
new ServletRegistrationBean<>(statViewServlet, "/druid/*");
//配置监控页的用户名和密码
registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","123456");
return registrationBean;
}
}
控制层方法:
package com.atguigu.admin.controller;
import com.atguigu.admin.bean.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
@Controller
public class IndexController {
@Autowired
JdbcTemplate jdbcTemplate;
@ResponseBody
@GetMapping("/sql")
public String queryFromDb(){
Long aLong = jdbcTemplate.queryForObject("select count(*) from t_account", Long.class);
return aLong.toString();
}
}
启动主启动类:
访问监控页:http://localhost:8080/druid/index.html
用于统计监控信息;如SQL监控、URI监控
需要给数据源中配置如下属性;可以允许多个filter,多个用,分割;如:
<property name="filters" value="stat,slf4j" />
系统中所有filter:
慢SQL记录配置
<bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
<property name="slowSqlMillis" value="10000" />
<property name="logSlowSql" value="true" />
bean>
使用 slowSqlMillis 定义慢SQL的时长
WebStatFilter用于采集web-jdbc关联监控的数据。
web.xml方式配置:
<filter>
<filter-name>DruidWebStatFilterfilter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilterfilter-class>
<init-param>
<param-name>exclusionsparam-name>
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*param-value>
init-param>
filter>
<filter-mapping>
<filter-name>DruidWebStatFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
配置类方式:
/**
* WebStatFilter 用于采集web-jdbc关联监控的数据。
*/
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));//拦截所有路径
//排除不需要监控的资源
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
启动主启动类访问测试:
(2.2.4)防火墙
配置类:
说明:把方式一的自定义方式注释掉。
注释掉依赖:
配置类标识为过时,并注释掉。
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.17version>
dependency>
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
spring:
datasource: #配置数据库连接池的基本信息
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
druid: #配置druid的监控页功能
aop-patterns: com.atguigu.admin.* # 监控SpringBean的范围
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)
stat-view-servlet: # 配置监控页功能
enabled: true # 默认false,需要开启
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 # sql超过1000毫秒的查询都是慢查询
logSlowSql: true # 日志是否要记录慢查询的sql
enabled: true # 监控的功能要不要开启
wall:
enabled: true # 防火墙的功能要不要开启
config:
drop-table-allow: false # 所有的删除操作都会被防火墙拦截
SpringBoot配置示例
https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
配置项列表
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8
MyBatis官方整合SpringBoot场景的github地址:https://github.com/mybatis
说明:不能导入原来的MyBatis依赖,现在使用的是SpringBoot整合MyBatis后的依赖。
spring-boot-starter-*
*-spring-boot-starter
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.4version>
dependency>
说明:引入了mybatis-spring-boot-starter场景依赖,就会自动引入:
说明:以前的配置方式需要在配置文件中自己一个个的写,现在底层自动配置好了。
@EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{}
@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties
可以修改配置文件中 mybatis 开头的所有:
yml配置文件:配置核心配置文件和映射文件的路径。
# 配置mybatis规则
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置
核心配置文件:
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
settings>
configuration>
控制层,service层略。
接口:注意加上@Mapper
注解
package com.atguigu.admin.mapper;
import com.atguigu.admin.bean.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Mapper;
/**
* @Mapper 用于描述(做标记)数据层访问接口,用于告诉mybatis框架,
* 使用此注解描述的接口要由底层为创建实现类.在实现类中基于mybatis
* API实现与数据库的交互.这个类的对象最后会交给spring管理.
*/
@Mapper
public interface AccountMapper {
public Account getAcct(Long id);
}
映射文件:
Mapper接口--->绑定Xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.admin.mapper.AccountMapper">
<select id="getAcct" resultType="com.atguigu.admin.bean.Account">
select * from account_tbl where id=#{id}
select>
mapper>
说明:
注意:
# 配置mybatis规则
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml #注释掉 方式一的全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置
configuration: # 指定mybatis全局配置文件中的相关配置项
map-underscore-to-camel-case: true #开启驼峰规则映射,一旦使用这种方式,那么必须要注释掉xml核心配置文件的方式。
总结: 可以不写全局配置文件,所有全局配置文件的配置都放yml/properties中的configuration配置项中即可
说明:使用注解方式代替映射文件方式写sql,适合简单地sql语句
。
注解方式获取插入数据的自增主键值:
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getById(Long id);
@Insert("insert into city(`name`,`state`,`country`) values(#{name},#{state},#{country})")
@Options(useGeneratedKeys = true,keyProperty = "id")//注解方式获取插入数据的自增主键值。
public void insert(City city);
}
说明:注解方式和xml方式可以同时使用。
接口:
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getById(Long id);
public void insert(City city);
}
映射文件:xml方式获取插入数据的自增主键值。
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.admin.mapper.CityMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into city(`name`,`state`,`country`) values(#{name},#{state},#{country})
insert>
mapper>
详情查看:MyBatis-Plus的篇章。
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
mybatis plus 官网:https://baomidou.com/
建议安装 MybatisX 插件 :点击接口方法上的小鸟图标,会直接跳到对应的xml文件,开发时来回跳转比较方便,还有逆向生成代码,代码提示等功能。
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.4version>
dependency>
自动配置
classpath*:/mapper/**/*.xml
;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件
。 建议以后sql映射文件,放在 mapper下(当然也可以自定义修改)优点:
package com.atguigu.admin.mapper;
import com.atguigu.admin.bean.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* 泛型:数据库对应的实体类类型。
*/
public interface UserMapper extends BaseMapper<User> {
}
控制层:
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1")Integer pn,
RedirectAttributes ra){
userService.removeById(id);
ra.addAttribute("pn",pn);
return "redirect:/dynamic_table";
}
@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
//表格内容的遍历
// response.sendError
// List users = Arrays.asList(new User("zhangsan", "123456"),
// new User("lisi", "123444"),
// new User("haha", "aaaaa"),
// new User("hehe ", "aaddd"));
// model.addAttribute("users",users);
//
// if(users.size()>3){
// throw new UserTooManyException();
// }
//从数据库中查出user表中的用户进行展示
//构造分页参数
Page<User> page = new Page<>(pn, 2);
//调用page进行分页
Page<User> userPage = userService.page(page, null);
// userPage.getRecords()
// userPage.getCurrent()
// userPage.getPages()
model.addAttribute("users",userPage);
return "table/dynamic_table";
}
业务层:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
}
public interface UserService extends IService<User> {
}
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)。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
自动配置:
redis环境搭建
1、阿里云按量付费redis。经典网络
2、申请redis的公网连接地址
3、修改白名单 允许0.0.0.0/0 访问
@Test
void testRedis(){
ValueOperations<String, String> operations = redisTemplate.opsForValue();
operations.set("hello","world");
String hello = operations.get("hello");
System.out.println(hello);
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
spring:
redis:
host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com
port: 6379
password: lfy:Lfy123456
client-type: jedis
jedis:
pool:
max-active: 10
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
步骤1:添加单元测试依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
会自动依赖一下场景
注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)
JUnit 5’s Vintage Engine Removed from spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage
总结:
<dependency>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.hamcrestgroupId>
<artifactId>hamcrest-coreartifactId>
exclusion>
exclusions>
dependency>
以前:使用单元测试需要添加很多注解
@SpringBootTest + @RunWith(SpringTest.class)
步骤2:
现在版本:
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Test
void contextLoads() {
}
}
总结使用测试类的步骤:SpringBoot整合Junit以后。
JUnit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
package com.atguigu.admin;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.concurrent.TimeUnit;
/**
* @SpringBootTest是复合注解:
* @BootstrapWith(SpringBootTestContextBootstrapper.class)
* @ExtendWith(SpringExtension.class)
*/
//@ExtendWith 为测试类或测试方法提供扩展类引用,@SpringBootTest本身就包含它。
@SpringBootTest //想要使用spring的容器注入功能就需要加上此注解,如果不需要使用注入功能可以不写。
@DisplayName("junit5功能测试类")
public class Junit5Test { //在类上运行单元测试,整个类上的测试方法都会执行一遍
@Autowired
JdbcTemplate jdbcTemplate;
//为测试类或者测试方法设置展示名称,运行的时候知道是那个测试方法在运行
@DisplayName("测试DisplayName注解")
@Test
void testDisplayName(){
System.out.println("111111");
System.out.println(jdbcTemplate);
}
@Disabled //表示测试类或测试方法不执行
@DisplayName("测试方法2")
@Test
void test2() {
System.out.println(2);
}
@RepeatedTest(5) //表示方法可重复执行,此测试方法可以执行5次
@Test
void test3() {
System.out.println(5);
}
/**
* 规定方法超时时间。超出时间测试出异常
* 可以指定单位:MILLISECONDS毫秒
* @throws InterruptedException
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
@BeforeEach //表示在每个单元测试之前执行
void testBeforeEach() {
System.out.println("测试就要开始了...");
}
@AfterEach //表示在每个单元测试之后执行
void testAfterEach() {
System.out.println("测试结束了...");
}
//必须是静态方法,或表示@TestInstance.因为这个方法只会被调用一次
@BeforeAll //表示在所有单元测试之前执行
static void testBeforeAll() {
System.out.println("所有测试就要开始了...");
}
//必须是静态方法,或表示@TestInstance。因为这个方法只会被调用一次
@AfterAll //表示在所有单元测试之后执行
static void testAfterAll() {
System.out.println("所有测试以及结束了...");
}
}
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后,会有一个详细的测试报告;
断言:即断定某件事情一定会发生,如果这件事情没有发生表示出现了问题。用来代替system输出方法,相比输出方法会有一个详细的测试报告。
用来对单个值进行简单的验证。如:
package com.atguigu.admin;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class Juint5Test01 {
/**
* 断言:前面断言失败,后面的代码都不会执行
*/
@DisplayName("测试简单断言")
@Test
void testSimpleAssertions() {
int cal = cal(3, 2);
//判断:传入的期望值和真实值是否相等
//参数:期望值、传入的真实值、自定义断言失败的提示信息
assertEquals(5, cal, "业务逻辑计算失败");
Object obj1 = new Object();
Object obj2 = new Object();
//判断两个对象引用是否指向同一个对象,即地址值是否相等
assertSame(obj1, obj2, "两个对象不一样");
}
int cal(int i, int j) {
return i + j;
}
}
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等
@Test
@DisplayName("array assertion")
public void array() {
//比较的是2个数组里面的值
//前面用的永远是期望的值,后面传的才是真正的值
//每个断言都可以自定义异常信息
assertArrayEquals(new int[]{1, 1}, new int[] {1, 2},"数组内容不相等");
}
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
@Test
@DisplayName("组合断言")
void all() {
/**
* 所有断言全部需要成功,才会向下执行。
*/
//参数1:给组合断言是哪一个组起一个名
//参数2:可变参数可以出传多个断言
assertAll("test",
() -> assertTrue(true && true, "结果不为true"),
() -> assertEquals(1, 1, "结果不是1"));
System.out.println("=====");
}
在JUnit4时期,想要测试方法的异常情况时,需要用 @Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。
@DisplayName("异常断言")
@Test
void testException() {
//断定业务逻辑一定出现异常,没有出现异常所说明这个业务逻辑是有问题的。
//参数:异常类型,可执行的方法,没有抛出异常的提示信息
assertThrows(ArithmeticException.class, () -> {
int i = 10 / 2;
}, "业务逻辑居然正常运行?");
}
Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
通过 fail 方法直接使得测试失败
@DisplayName("快速失败")
@Test
void testFail(){
//xxxxx
//在前面xxxx中单元测试了一大堆,之后想要直接结束,不想要在测试了。
if(1 == 2){
fail("测试失败");
}
}
说明:在项目上线之前,跑一遍单元测试,会有一个完整的单元测试报告。它会执行每一个单元测试方法,那个方法执行成功,那个执行失败 失败的原因是什么,会有一个汇总报告,方便我们定位。
JUnit 5 中的前置条件(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")
);
}
/**
* 测试前置条件
*/
@DisplayName("测试前置条件")
@Test
void testassumptions(){
//是true才会向下执行,如果不是true会有个提示信息。
Assumptions.assumeTrue(false,"结果不是true");
System.out.println("111111");
}
}
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
package com.atguigu.admin;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.EmptyStackException;
import java.util.Stack;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("嵌套测试")
public class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
//嵌套测试情况下,外层的Test不能驱动内层的Before(After)Each/All之类的方法提前/之后运行
assertNull(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可以驱动外层的Before(After)Each/All之类的方法提前/之后运行
*/
@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());
}
}
}
}
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource
等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@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");
}
在进行迁移的时候需要注意如下的变化:
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
http://localhost:8080/actuator/beans
http://localhost:8080/actuator/configprops
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/jvm.gc.pause
http://localhost:8080/actuator/endpointName/detailPath
。。。。。。
https://github.com/codecentric/spring-boot-admin
ID | 描述 |
---|---|
auditevents | 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件。 |
beans | 显示应用程序中所有Spring Bean的完整列表。 |
caches | 暴露可用的缓存。 |
conditions | 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
configprops | 显示所有@ConfigurationProperties。 |
env | 暴露Spring的属性ConfigurableEnvironment |
flyway | 显示已应用的所有Flyway数据库迁移。需要一个或多个Flyway组件。 |
health | 显示应用程序运行状况信息。 |
httptrace | 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。 |
info | 显示应用程序信息。 |
integrationgraph | 显示Spring integrationgraph 。需要依赖spring-integration-core。 |
loggers | 显示和修改应用程序中日志的配置。 |
liquibase | 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。 |
metrics | 显示当前应用程序的“指标”信息。 |
mappings | 显示所有@RequestMapping路径列表。 |
scheduledtasks | 显示应用程序中的计划任务。 |
sessions | 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 |
shutdown | 使应用程序正常关闭。默认禁用。 |
startup | 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup。 |
threaddump | 执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump | 返回hprof堆转储文件。 |
jolokia | 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core。 |
logfile | 返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。 |
prometheus | 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus。 |
最常用的Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
management.endpoint..enabled = true
management:
endpoint:
beans:
enabled: true
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
支持的暴露方式
ID | JMX | Web |
---|---|---|
auditevents | Yes | No |
beans | Yes | No |
caches | Yes | No |
conditions | Yes | No |
configprops | Yes | No |
env | Yes | No |
flyway | Yes | No |
health | Yes | Yes |
heapdump | N/A | No |
httptrace | Yes | No |
info | Yes | Yes |
integrationgraph | Yes | No |
jolokia | N/A | No |
logfile | N/A | No |
loggers | Yes | No |
liquibase | Yes | No |
metrics | Yes | No |
mappings | Yes | No |
prometheus | N/A | No |
scheduledtasks | Yes | No |
sessions | Yes | No |
shutdown | Yes | No |
startup | Yes | No |
threaddump | Yes | No |
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}
}
构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
/**
* 真实的检查方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb。 获取连接进行测试
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 2){
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}
builder.withDetail("code",100)
.withDetails(map);
}
}
常用两种方式
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@
import java.util.Collections;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
@Component
public class ExampleInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}
}
http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}
public void hello() {
counter.increment();
}
}
//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
@Component
@Endpoint(id = "container")
public class DockerEndpoint {
@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}
}
场景:开发ReadinessEndpoint来管理程序是否就绪,或者LivenessEndpoint来管理程序是否存活;
当然,这个也可以直接使用 https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes
为了方便多环境适配,springboot简化了profile功能。
使用场景:开发一个微服务项目,在配置文件中配置了很多的信息。
说明:指定使用的是哪个配置环境。
执行规则:
任何时候
都会加载java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
创建springboot项目,打包方式为jar,添加web依赖。
如何修改文件名:
默认配置文件:即 不带任何标识的文件,application.properties/yml,默认配置文件永远都会加载。
application.properties:项目创建好默认生成的配置文件
person.name=张三
application-prod.yml:手动创建生产环境的配置文件
person:
name: prod-张三 # 这是生产环境下的配置
#如何告诉SpringBoot这是生产环境下配置文件呢??
#比如:在配置文件的名字加上环境标识(-xx),application-xxx.yml xxx环境标识名字随便写
application-test.yml:手动创建测试环境的配置文件
person:
name: test-张三 # 这是测试环境下的配置
#如何告诉SpringBoot这是生产环境下配置文件呢??
#比如:在配置文件的名字加上环境标识(-xx),application-xxx.yml xxx环境标识名字随便写
控制层方法:
package com.cn.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
//默认的properties直接就可以读取到了,如果是自定义的properties文件还需要指定路径
//@PropertySource("classpath:/application.properties")
public class HelloController {
//如果读取不到使用默认值小李 也可以使用绑定配置文件的方式@ConfigurationProperties
@Value("${person.name:小李}")
private String name; //读取配置文件中的值
@GetMapping("/")
public String name(){
return "Hello"+ name;
}
}
启动运行项目,在页面输入地址访问此方法,输出的结果为:Hello张三
说明:只设置了生产环境和测试环境的标识格式,但没有指定使用的是哪个环境的配置文件,项目运行时使用的是,默认的配置文件application.properties(项目创建好后自动生成的),yml也可以只要名字是application前缀就行。
说明:想要指定使用哪个环境,需要在默认的配置文件中设置激活哪个环境。
#值为环境标识, 注意如果注释加在这一行的后面,会让此配置active不会生效
spring.profiles.active=prod
运行结果:输出的是生产环境下设置的名字
说明:在指定环境标识的情况下,3个配置文件出现了同名的配置,运行时以指定的环境标识下的配置文件为准。
总结执行流程:项目启动永远是先加载默认的配置文件,如果在默认的配置文件中设置了要加载那个环境的配置,那么之后就加载有环境标识的文件。如果默认的配置文件和指定的标识环境文件中出现了相同的配置,则以指定环境标识的文件为准。
说明:打包后因为配置文件里面配置的是生产环境prod,所以运行时使用的是生产环境prod,此时想要使用别的环境该如何办呢???
java -jar xxx.jar
:运行打包后的jar包项目java -jar xxx.jar --spring.profiles.active=prod
:执行项目并且指定使用那个环境java -jar xxx.jar --spring.profiles.active=test --person.name=haha
:执行项目,指定使用那个环境,并修改name属性值。
java -jar profile-test-0.0.1-SNAPSHOT.jar --spring.profiles.active=test --person.name=haha
说明:使用 @Profile注解可以按条件注入属性,即符合@Profile注解的值配置才生效。
业务:如果当前是生成环境就注入到Boss中,是测试环境就注入到Worker中
application.properties:
person.name=张三
#值为环境标识, 注意如果注释加在这一行的后面,会让此配置active不会生效
spring.profiles.active=prod
server.port=8080
person:
name: prod-张三 # 这是生产环境下的配置
age: 18 #如何告诉SpringBoot这是生产环境下配置文件呢??
#比如:在配置文件的名字加上环境标识(-xx),application-xxx.yml xxx环境标识名字随便写
server:
port: 8000
创建4个对象:
说明:
Person接口:
package com.cn.bean;
public interface Person {
String getName();//接口中的方法 连{}也没有,默认加上public abstract
Integer getAge();
}
Boss实现类:
package com.cn.bean;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
//业务:如果当前是生成环境就注入到Boss中,是测试环境就注入到Worker中
@Profile("prod") //指定prod环境才生效
@Component
@ConfigurationProperties("preperson")//绑定配置文件prod中的以person为前缀的值,因为指定了prod环境。交给Spring容器管理才生效
@Data
public class Boss implements Person {
private String name; //可以获取配置文件中的值
private Integer age;
}
Work实现类:
package com.cn.bean;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
//业务:如果当前是生成环境就注入到Boss中,是测试环境就注入到Worker中
@Profile("test") //指定test环境才生效
@Component
@ConfigurationProperties(prefix = "person")//绑定配置文件prod中的以person为前缀的值,因为指定了prod环境。 交给Spring容器管理才生效
@Data
public class Worker implements Person {
private String name;
private Integer age;
}
Color:
package com.cn.bean;
public class Color {
}
配置类:
package com.cn.config;
import com.cn.bean.Color;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class MyConfig {
@Profile("prod")//当环境是prod时才生效
@Bean
public Color red(){
return new Color();
}
@Profile("test") //当环境是test时才生效
@Bean
public Color green(){
return new Color();
}
}
package com.cn.controller;
import com.cn.bean.Color;
import com.cn.bean.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
//默认的properties直接就可以读取到了,如果是自定义的properties文件还需要指定路径
//@PropertySource("classpath:/application.properties")
public class HelloController {
//如果读取不到使用默认值小李 也可以使用绑定配置文件的方式@ConfigurationProperties
@Value("${person.name:小李}")
private String name; //读取配置文件中的值
@Autowired
private Person person;
@Autowired
private Color color;
@GetMapping("/")
public String name(){
//反射获取 普通对象获取
return "第一个"+person.getClass().toString()+"第二个"+color.toString();
}
}
运行测试:
可以看到因为在配置文件激活的是prod环境,所以只有是满足@Profile(“prod”)注解标识的方法或类才能够生效。
说明:使用profile分组,可以同时激活同一个组下的多个环境。
application.properties:
在默认的配置文件中设置分组,并设置需要激活的分组。
#值为环境标识, 注意如果注释加在这一行的后面,会让此配置active不会生效
# 此时激活的是这个分组下的所有环境:ppd prod
spring.profiles.active=myprod
#定义生产的分组
spring.profiles.group.myprod[0]=ppd
spring.profiles.group.myprod[1]=prod
# 定义测试的分组
spring.profiles.group.mytest[0]=test
application-ppd.yaml
person:
age: 20
application-prod.yaml
person:
name: prod-张三 # 这是生产环境下的配置
#如何告诉SpringBoot这是生产环境下配置文件呢??
#比如:在配置文件的名字加上环境标识(-xx),application-xxx.yml xxx环境标识名字随便写
server:
port: 8000
说明:在默认的配置文件中使用分组同时激活ppd prod,此时2个配置文件的值注入到了此类的name、age属性中(因为使用注解绑定了配置文件)。
@GetMapping("/person")
public Person person(){
return person;
}
可以看到:来自同一分组下的 ppd环境中的age: 20 , prod环境中的name: prod-张三,都被输出了。
说明:
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
说明:外部配置源可以来自不同的地方。
常用:Java属性文件、YAML文件、环境变量、命令行参数(Dos窗口);
测试:配置信息来自环境变量
控制层代码:
@RestController
public class HelloController {
@Value("${MAVEN_HOME}")//获取Maven的安装目录
private String msg;
@Value("${os.name}") //获取操作系统
private String osName;
@GetMapping("/msg")
public String getMsg(){
return msg+"==>"+osName;
}
}
访问:
主启动类输出系统的环境变量所有信息:启动项目后会自动输出
package com.atguigu.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.Map;
@SpringBootApplication
public class Boot09FeaturesProfileApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Boot09FeaturesProfileApplication.class, args);
//获取当前的环境
ConfigurableEnvironment environment = run.getEnvironment();
//系统的环境变量
Map<String, Object> systemEnvironment = environment.getSystemEnvironment();
//系统的属性
Map<String, Object> systemProperties = environment.getSystemProperties();
System.out.println(systemEnvironment);
System.out.println(systemProperties);
}
}
说明:配置文件的位置可以有以下5种种方式。仍然有优先级(先执行根路径下的–>在执行根路径下的config目录中的配置文件—>…),如果后执行的配置文件中有同名的配置信息,则后面的会覆盖前面的配置信息。
classpath 根路径(类路径下的根路径:即源码标识的路径)
classpath 根路径下config目录
jar包当前目录
即:项目打成jar包发布到服务器的某一个位置,那么这个jar包所在的目录里面的配置文件也能拿到。
jar包当前目录的config目录
即:在config目录下写配置文件
linux根目录下的/config子目录的直接子目录(针对于linux系统)
即:在v1目录下写配置文件,这里是在windows下测试用的,执行不成功。
说明:这个指的是都是用配置文件的方式进行配置,多个配置文件的加载顺序。
先执行jar内部的默认配置文件—>jar包内部的指定的配置文件---->jar包外部的默认配置文件---->jar包外部的指定配置文件。
如果后面有同名的配置信息,后面会覆盖前面的,即后面的优先级高于前面的。(优先级指的是如果出现同名的配置后面的会覆盖前面的,而不是文件的加载顺序。
)
说明:这14种指的是不同种类的配置方式,比如使用配置文件配置端口号,使用命令行配置端口号,后面的命令行配置方式优先级高于配置文件的方式。
@PropertySource
annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.说明:springboot为了简化开发,常用的场景都帮我们抽取出来了,即使这样也不能模拟所有的开发场景。比如我们现在有一个功能非常常用,我们把它抽取出来,其它的模块经常引用,而且引用过来一切场景都要配置好,此时我们就需要把这个功能抽取为自定义的starter。别人一引用这个自定义的stater,所有东西全配置好只需要改下配置文件就能用。
META-INF/spring.factories
中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类引入starter --- xxxAutoConfiguration --- 容器中放入组件 ---- 绑定xxxProperties ---- 配置项
atguigu-hello-spring-boot-starter(启动器)
atguigu-hello-spring-boot-starter-autoconfigure(自动配置包)
一、创建场景启动器项目:以maven为例(场景启动器不需要写代码只需要引入依赖即可,而使用Initailizr创建会帮你创建很多的东西)
<dependency>
<groupId>com.atguigugroupId>
<artifactId>atguigu-hello-spring-boot-starter-autoconfigureartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
可以看到添加依赖后,场景启动器会自动依赖自动配置类项目:
其次:在自动配置项目中编写功能:
业务说明:有一个组件的功能经常使用,所以把此功能抽取出来 ,可以实现这个功能场景能够自动配置。
HelloProperties:此类的作用是绑定配置文件读取里面的内容:
package com.atguigu.hello.bean;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("atguigu.hello")//绑定配置文件以atguigu.hello为前缀的属性 加入到容器中才生效
//因为实体类可能用的是第三方包里面的,可能没有加@Component,你也不能擅自加上,此时就可以使用
// 在配置类中添加 @EnableConfigurationProperties注解:开启属性绑定功能,把组件放到容器中。
public class HelloProperties {
private String prefix;
private String 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;
}
@Override
public String toString() {
return "HelloProperties{" +
"prefix='" + prefix + '\'' +
", suffix='" + suffix + '\'' +
'}';
}
}
HelloService:编写业务功能,用户传入一个人名,会自动拼接上前缀后缀成一个礼貌用语,这个前缀后缀希望是动态的可配置的
package com.atguigu.hello.service;
import com.atguigu.hello.bean.HelloProperties;
import org.springframework.beans.factory.annotation.Autowired;
/**希望有一个功能场景能够自动配置
* 说明:这个组件的功能经常使用,所以把此功能抽取出来
* 这个组件HelloService默认不要在这个地方放在容器中,因为我们可能会进行配置。可以使用配置类的方式,在这里配置好后
* 使用配置类判断如果容器中没有此组件在进行配置。
*/
public class HelloService {
//业务流程:HelloProperties获取到配置文件的值,一旦在配置类中把HelloProperties获取到配置文件的值组件注册到容器中,此时
//就可以在这里注入了。
@Autowired
private HelloProperties helloProperties;
//业务需求:用户传入一个人名,会自动拼接上前缀后缀成一个礼貌用语,这个前缀后缀希望是动态的可配置的
public String sayHello(String userName){
return helloProperties.getPrefix() + ":"+userName+"》"+helloProperties.getSuffix();//前后缀希望是可变的
}
}
HelloServiceAutoConfiguration配置类:把HelloProperties、HelloService都注册到容器中。
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
@EnableConfigurationProperties(HelloProperties.class)//开启属性绑定功能,并会把组件放到容器中
public class HelloServiceAutoConfiguration {
@ConditionalOnMissingBean(HelloService.class)//当容器中没有配HelloService时,此配置类才会生效
@Bean
public HelloService helloService(){
HelloService helloService =new HelloService();
return helloService;
}
}
自动配置场景中的配置类不会自动生效,需要在META-INF/spring.factories中配置哪些类在项目启动时加载。
spring.factories文件:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.atguigu.hello.auto.HelloServiceAutoConfiguration
说明:开发场景依赖于----》场景启动器依赖于----》依赖自动配置包
所以首先需要打包自动依赖配置项目,在打包场景启动器项目,作用是把打包后的项目安装到本地仓库里面。之后开发场景在pom.xml文件中添加坐标后只需要更新依赖就可以使用了。
一般是先clean:删除本地仓库中之前可能打包过的同名项目,因为项目的代码可能修修改了所以你需要先删除在重写打包。之后在install打包安装到本地仓库。
在开发场景引入场景启动器,编写配置文件,控制层方法
<dependency>
<groupId>com.atguigugroupId>
<artifactId>atguigu-hello-spring-boot-starterartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
可以看到开发场景依赖了场景启动器,场景启动器又依赖了自动配置包。
application.properties:
atguigu.hello.prefix=nihao
atguigu.hello.suffix=6666
HelloController:
package com.atguigu.controller;
import com.atguigu.hello.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/")
public String sayHello(){
String say = helloService.sayHello("小明");
return say;
}
}
启动当前开发场景项目,在页面输入url进行访问:
总结执行流程:
说明:可以在开发场景项目中自定义组件,此时用的就是自己的组件,而不是原先引入的自动配置包的HelloService组件了。
package com.atguigu.config;
import com.atguigu.hello.service.HelloService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HelloConfig {
@Bean
public HelloService helloService(){
HelloService helloService= new HelloService();
return helloService;
}
}
Spring原理【Spring注解】、SpringMVC原理、自动配置原理、SpringBoot原理
public interface Bootstrapper {
/**
* Initialize the given {@link BootstrapRegistry} with any required registrations.
* @param registry the registry to initialize
*/
void intitialize(BootstrapRegistry registry);
}
@FunctionalInterface
public interface ApplicationRunner {
/**
* Callback used to run the bean.
* @param args incoming application arguments
* @throws Exception on error
*/
void run(ApplicationArguments args) throws Exception;
}
@FunctionalInterface
public interface CommandLineRunner {
/**
* Callback used to run the bean.
* @param args incoming main method arguments
* @throws Exception on error
*/
void run(String... args) throws Exception;
}
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners
ApplicationContextInitializer
ApplicationListener
SpringApplicationRunListener