SpringBoot
SpringBoot核心基础
SpringBoot2的核心功能:
-
配置文件
-
web开发
-
数据访问
-
Junit5单元测试
-
生产指标监控
-
SpringBoot核心原理解析
SpringBoot2场景整合:
-
虚拟化技术
-
安全技术
-
缓存技术
-
消息中间件
-
分布式入门
SpringBoot能干吗?
SpringBoot的优点:
-
创建独立的Spring应用
-
内嵌web服务器
-
自动starter依赖,简化构建配置
-
自动配置Spring以及第三方功能
-
提供生产级别的监控,健康检查及外部化配置
-
无代码生成,无需编写XML
SpringBoot的自动配置原理
SpringBoot有哪些自动配置❓
- 自动配置tomcat
- 自动配置SpringMVC
- 自动配置Web常用功能,如:字符编码
- 默认包结构:即与主程序同级目及其子包都会被默认扫描进来。如需更改扫描路径
@SpringBootApplication(scanBasePackages=**"类路径"**)
- 各种配置拥有默认值
- 按需加载所有自动配置项,引入了 那个场景的自动配置才会开启,所有自动配置功能都在
spring-boot-autoconfigurtion
包里面。
容器功能:
- @configurtion:
@configurtion(**proxyBeanMethods="true")
:当proxyBeanMethods = true时,保证每个@Bean方法被调用多少次返回的组件都是单实例的,即为full模式**。当proxyBeanMethods = fals时,每个@Bean方法被调用多少次返回的组件都是新创建的,即为***Lite模式***。组件依赖必须使用Full模式默认。其他默认是否Lite模式。
创建SpringBoot应用
配置Maven:
<-- 配置下载地址源 !-->
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
<-- 配置编译环境 !-->
<profiles>
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>
</profiles>
引入SpringBoot所需的Pom:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<-- 创建一个可执行Jar !-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
配置绑定
即读取到Properties文件中的配置
配置绑定方式一:@Component+@ConfigurationProperties(perfix=""):
将properties配置项中匹配的前缀进行属性的绑定。即将配置属性中的值注入到字段中。
-
properties:
ikart.name = ikart ikart.age = 20
-
类
@Data //将组件添加到容器中 @Component //匹配配置文件中以ikart开头的 @ConfigurationProperties(prefix = "ikart") public class Cat { String name; int age; }
@EnableConfigurationProperties+@ConfigurationProperties:
-
@EnableConfigurationProperties:
- 开启配置绑定功能
- 把配置的类当做组件注册到容器中
- @EnableConfigurationProperties注解必须搭配@ConfigurationProperties注解使用
配置提示:
自定义的类和配置文件绑定一般没有提示。
-
pom
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <-- package时不会将spring-boot-configuration-processor.jar打包 !--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <proc>none</proc> </configuration> </plugin>
配置提示文档:
自动配置原理入门
引导加载自动配置类:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {}
-
@SpringBootConfigruation:代表当前是一个配置类。
-
@ComponentScan:指定扫描范围
-
@EnableConfiguration:
@AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration {}
-
@AutoConfigruationPackage:自动配置包,指定了默认的包规则。
@Import(AutoConfigurationPackages.Registrar.class) //给容器中导入一个组件 public @interface AutoConfigurationPackage {} //利用Registrar给容器中导入一系列组件 //将指定的一个包下的所有组件导入进来?MainApplication 所在包下。
-
@Import(AutoConfigurationImportSelector.class):
-
利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
-
调用List
configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类 -
利用工厂加载 Map<String, List
> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件 -
从META-INF/spring.factories位置来加载一个文件。默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories、该文件里面写死了spring-boot一启动就要给容器中加载的所有配置类
-
-
按需开启自动配置项:
- 虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
- 按照条件装配规则(@Conditional),最终会按需配置。
修改默认配置规则:
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先。即定制化配置
- 通过修改配置文件
- 通过@Bean替换底层的组件
自动配置总结:
- SpringBoot先加载所有自动配置类,如:xxAutoConfiguration
- 每个自动配置类按照条件生效,默认都会绑定配置文件指定的值,xxxProperties里面拿,xxxProperties和配置文件进行了绑定。
- 生效的配置类就会给容器中装配很多组件,只有容器中有这些组件,相当于什么功能就有了。
- 定制化配置,就用户自行修改默认配置规则。
实践:
-
引入场景依赖(参考SpingBoot整合的场景依赖)
-
如何查看自动配置那些场景❓
- 自己分析,当引入相应的场景,对应的自动配置一般都生效了
- properties中开启
debug=true
模式
-
如何修改SpringBoot底层配置❓参考如下官网,罗列了SpringBoot能够修改的配置项:
- 也可以通过添加、替换底层组件的方式
注意项:
- 导入了场景后需要完成场景的配置,否则可能不能正常启动;
- @Deprecated(不推荐使用注解) : 类添加了此注解表明已过时,不推荐使用
- 配置filters: stat(sql监控),wall(防火墙)
SpringBoot核心功能
配置文件
Yaml:
YAML 是 "YAML Ain't Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。适合用来做以数据为中心的配置文件。
- 基本语法:
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- '#'表示注释
- 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义
- 数据类型:
-
字面量:单个的、不可再分的值。如date、boolean、string、number、null
k: v
-
对象:键值对的集合。map、hash、set、object
#行内写法: k: {k1:v1,k2:v2,k3:v3} #或 k: k1: v1 k2: v2 k3: v3
-
数组:一组按次序排列的值。array、list、queue
#行内写法: k: [v1,v2,v3] #或者 k: - v1 - v2
-
Web开发
静态资源目录:
-
只要静态资源放在类路径下: called
/static
(or/public
or/resources
or/META-INF/resources
- 访问 : 当前项目根路径/ + 静态资源名
-
配置静态资源访问前缀(默认无前缀):
spring: mvc: static-path-pattern: /res/** #添加该配置后,所有访问静态资源都需要加上前缀res
-
配置静态资源目录
spring: web: resources: static-locations: [classpath:/ikart/]
-
Favicon:存放在静态目录,且名为
favicon.ico
的文件 -
欢迎页:即访问
/
时默认为静态资源目录下index.html
的文件 -
禁用所有静态资源规则
spring: # mvc: # static-path-pattern: /res/** resources: add-mappings: false 禁用所有静态资源规则
静态资源配置原理:
-
SpringBoot启动自动加载xxxAutoConfiguration类(自动配置类)
-
SpringMVC功能的自动配置类WebMvcAutoConfiguration,生效
@Configuration( proxyBeanMethods = false ) @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class); private final Resources resourceProperties; private final WebMvcProperties mvcProperties; private final ListableBeanFactory beanFactory; private final ObjectProvider<HttpMessageConverters> messageConvertersProvider; private final ObjectProvider<DispatcherServletPath> dispatcherServletPath; private final ObjectProvider<ServletRegistrationBean<?>> servletRegistrations; private final WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; private ServletContext servletContext; //只有一个有参构造器所有的参数的值都会从容器中获取。 public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) { this.resourceProperties = (Resources)(resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources()); this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.dispatcherServletPath = dispatcherServletPath; this.servletRegistrations = servletRegistrations; this.mvcProperties.checkConfiguration(); } .... }
WebMvcAutoConfigurationAdapter说明
-
给容器添加了那些配置:
@Configuration(proxyBeanMethods = false) @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}
- 配置文件的相关属性和xxx进行了绑定。WebMvcProperties==**spring.mvc、**ResourceProperties==spring.resources
-
请求参数处理
请求映射:Rest使用与原理:
Rest风格:以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户...来区分操作。使用Rest无须修改访问路径来区分如何操作,而是根据请求的类型来区分不同的操作。如/user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户。
-
开启页面表单的Rest功能(SpringBoot默认为关闭)
spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能
-
Rest原理(表单提交要使用Rest的时候)
- 表单提交会带上
_mothod=请求方式
- 请求过来被**
HiddenHttpMethodFilter
**拦截- 请求是否正常,并且为指定的请求方式(列如delete、post)
-
获取到_method的值。_mothod的值可为PUT,DELETE,PATCH
-
原生request请求,包装模式requestWrapper重写的getMethod方法,返回的是传入的值。
-
过滤链放行的时候用wrapper。以后的方法调用getMethod是调用requestWapper的。
-
- 请求是否正常,并且为指定的请求方式(列如delete、post)
- 表单提交会带上
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);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 找到当前请求使用哪个Handler(Controller的方法)处理
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
- RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
- SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
- 欢迎页处理器:ParameterizableViewController [view="forward:index.html"]
- SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
- 参数解析器-HandlerMethodArgumentResolver
-
确定将要执行的目标方法的每一个参数的值是什么;
-
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
-
普通参数与基本注解
注解:@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
基本注解:
-
@PathVariable:路径变量、将路径上面的变量获取到赋值给方法形参。如果方法参数是Map<String, String>,那么Map将填充所有路径变量名称和值到Map中。
-
@RequestHeader:获取请求头,如果方法参数是Map<String, String>,那么Map将填充所有请求头参数put到Map中。
-
@RequestParam:获取请求参数,如果方法参数为Map<String, String> 或 MultiValueMap<String, String>且未指定参数名称,则 map 参数填充了所有请求参数名称和值。
-
@MatrixVariable:矩阵变量,如果方法参数为Map<String, String> 或 MultiValueMap<String, String>且未指定变量名称,则map填充了所有矩阵变量名称和值。
-
矩阵变量必须有url路径变量才能被解析
-
@MatrixVariable功能默认关闭,开启:
@Configuration(proxyBeanMethods = false) public class MyConfiguration implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }
-
-
@CookieValue:获取cookie值
Servlet API:
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
复杂参数:
Map、Model(map、model里面的数据会被放在request的请求域),可以通过request.getAttribute()获取到。
-
代码:
@RequestMapping("/params") public String params(Map<String, Object> map, Model model, HttpServletRequest request) { map.put("ikart", "ikart"); model.addAttribute("c1", "v1"); request.setAttribute("attribute", "--value--"); return "forward:/success2"; } @ResponseBody @RequestMapping("/success2") public Map<String, Object> success2(HttpServletRequest request) { Map<String, Object> map = new HashMap<>(); map.put("a1", request.getAttribute("ikart")); map.put("a2", request.getAttribute("c1")); map.put("a3", request.getAttribute("attribute")); return map; }
-
访问结果:
- Model即使Model也是Map,属于同一实例对象
- mavContainer包含了视图页面和Model数据
- Forward:/path处理:
参数处理原理:
- HandlerMapping中找到能处理请求的Handler(Controller.method())
- 为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
- 适配器执行目标方法并确定方法参数的每一个值
-
HandlerAdapter:
//找到这个请求对应的handler HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
- 在以下4个适配器里面挨个遍历,找到处理请求对应的handler
RequestMappingHandlerAdpter
:标注@RequestMapping注解请求处理适配器,当有对应的请求处理方法时会调用这个适配器HandlerFunctionAdapter
:函数式编程处理适配器HttpRequestHandlerAdpter
:当找不到请求处理方法时,返回404会使用该适配器SimpleControllerHandlerAdpter
:简单控制处理适配器,页面刷新时会使用该适配器
- 在以下4个适配器里面挨个遍历,找到处理请求对应的handler
-
执行目标方法:
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);
-
参数解析器
**HandlerMethodArgumentResolver**
-
确定将要执行的目标方法的每一个参数的值是什么;
-
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
疑问❓在分析源码中每一个对应的参数解析器都对应着处理某一个注解,那么是否意味着在方法可标注的注解方式有27个?
- 参数处理解析器
- 当前解析器是否支持解析这种参数
- 支持就调用resolveArgument
-
-
返回值处理器
-
如何确定目标方法每一个参数❓
-
InvocableHandlerMethod.getMethodArgumentValues
:来获取方法的参数值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; }
-
HandlerMethodArgumentResolverComposite.getArgumentResolver
:获取参数解析器- 先从缓存中找是否有支持的参数解析器,如果没有则在自身中找参数处理器
- 自身有27个参数处理器,诶个遍历参数处理器,那个能处理就交由那个参数处理器处理。
@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; }
-
Pojo封装过程:自定义对象参数
可以自动类型转换与格式化、可以级联封装。即当参数类型是我们自定义的类时,SpringBoot会根据将请求(GET、POST)中的参数绑定到我们的对象中。当然我们需要遵守SpringBoot的自动注入规则。我们也可以自定义注入规则。
- 数据绑定:页面提交的请求数据(
GET、POST
)都可以和对象属性进行绑定-
前端发送
测试封装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.name" value="阿猫"/><br/> 宠物年龄:<input name="pet.age" value="5"/> <!-- 宠物: <input name="pet" value="啊猫,3"/>--> <input type="submit" value="保存"/> </form>
-
后端封装:
@ResponseBody @PostMapping("/saveuser") public Man saveUser(Man man) { return man; }
-
-
自定义Convert:在配置类中实线
WebMvcConfigurer
类重新实现Convert@Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter<String, Pet>() { @Override public Pet convert(String source) { if (!StringUtils.isEmpty(source)){ Pet pet = new Pet(); String[] split = source.split(","); pet.setName(split[0]); pet.setAge((split[1])); return pet; } return null; } }); }
-
Pojo封装由参数处理器
ServletModelAttributeMethodProcessor
完成-
ModelAttributeMethodProcessor.resolveArgument
- 创建属性实例:attribute = createAttribute(...);创建属性实例,此时为空对象
- 创建Web数据绑定器:WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
- Web数据绑定器将请求参数的值绑定到指定的JavaBean里面
- Web数据绑定器利用内部的Converter将请求数据转成指定类型。再次封装到JavaBean中
- GenerricConverterService:在设置每一个值的时候,找内部里面所有converter能够将这个数据类型(request带来参数的字符串)转换为指定的类型。
@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(); } else { attribute = ex.getTarget(); } 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; }
-
由createAttribute创建目标对象
protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception { MethodParameter nestedParameter = parameter.nestedIfOptional(); Class<?> clazz = nestedParameter.getNestedParameterType(); Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz); Object attribute = constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest); if (parameter != nestedParameter) { attribute = Optional.of(attribute); } return attribute; }
- 自定义Converter
- new Converter<来源类型,转换目标类型>():
-
目标方法执行完成:
-
将所有的数据都放在 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);
}
}
- exposeModelAsRequestAttributes(model, request):暴露模型作为请求域属性
Web开发-数据响应与内容协商
响应Json:
-
Jackson+@ResponseBody:
spring-boot-starter-web
自动引入了Json场景,自动给前端返回Json数据。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> <version>2.5.4</version> <scope>compile</scope> </dependency>
-
返回值解析器:
-
选择能够处理方法返回值的处理器:
try { this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); }
@Nullable private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { boolean isAsyncValue = isAsyncReturnValue(value, returnType); for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { continue; } if (handler.supportsReturnType(returnType)) { return handler; } } return null; }
@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); }
-
-
返回值解析原理:
- 返回值处理器判断是否支持这种类型的返回值supportReturnType
- 返回值处理器调用handleReturnValue进行处理
- RequestResponseBodyMethodProcessor可以处理返回值标了@RequestBody注解的。
- 利用MessageConverter进行处理,将数据写为Json
- 内容协商(浏览器默认会以请求头的方式告诉服务器它能接受什么样的数据类型)
- 服务器最终根据自己自身的能力,决定服务器能产生什么样的内容类型的数据
- Spring MVC会诶个遍历容器底层的HttpMessageConverter,找到能处理的Converter。
-
得到MappingJackson2HttpMessageConverter可以将对象写为Json
-
利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
- 浏览器发送请求携带的数据头
- 服务器接受到请求后诶个遍历容器底层的HttpMessageConverter,找到能处理的Converter
-
- 利用MessageConverter进行处理,将数据写为Json
-
Web MVC支持处理那些返回值?
ModelAndView Model View ResponseEntity ResponseBodyEmitter StreamingResponseBody HttpEntity HttpHeaders Callable DeferredResult ListenableFuture CompletionStage WebAsyncTask 有 @ModelAttribute 且为对象类型的 @ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
HttpMessageConverter原理:
-
HttpMessageConverter接口规范:看是否支持将此Class类型的对象,转为MediaType类型的数据。例如将Person对象转为Json,或Json转为Person。
-
默认的MessageConverter:
-
获取服务器能够产生的媒体类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
-
诶个判断那个MessageConverter能够处理Class对象转为浏览器能够接受的数据
@SuppressWarnings("unchecked") protected List<MediaType> getProducibleMediaTypes( HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) { Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } List<MediaType> result = new ArrayList<>(); for (HttpMessageConverter<?> converter : this.messageConverters) { if (converter instanceof GenericHttpMessageConverter && targetType != null) { if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) { result.addAll(converter.getSupportedMediaTypes(valueClass)); } } else if (converter.canWrite(valueClass, null)) { result.addAll(converter.getSupportedMediaTypes(valueClass)); } } return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); }
-
0-支持处理Byte类型的Message
-
1-String
-
2-String
-
3-Resource
-
4-ResuoreceRegion
-
5-支持处理以下类型的Message
DOMSource.class SAXSource.class StAXSource.class StreamSource.class Source.class
-
6-MultiValueMap
-
7-能够处理所有的类型,将Message转为json
-
8-能够处理所有的类型,将Message转为json
-
9-支持注解方式的xml处理
-
-
-
将浏览器能够接收的媒体类型和服务器能够生产的类型进行匹配,以相匹配的方式将结果返回给浏览器。
List<MediaType> mediaTypesToUse = new ArrayList<>(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } }
内容协商:
根据客户端接收能力不同,返回不同媒体类型的数据。
-
导入Jar:SpringBoot自动配置了版本,导入即可。
<!--内容协商--> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
-
导入jar后,会多一个MessageConverter
-
jackson-dataformat-xml
能够返回的数据类型为:
-
-
访问:页面内容类型为xhtml+xml
-
使用PostMan发送
-
开启浏览器参数方式内容协商功能:
#开启内容协商 spring: contentnegotiation: favor-parameter: true
- 在发送访问请求时在参数添加format = 你要接受的数据类型。例如:
-
确定客户端接收什么样的内容类型:
-
AbstractMessageConverterMethodProcessor:
- 从请求中获取请求参数中指定的内容类型
acceptableTypes = getAcceptableMediaTypes(request);
-
Parmater策略优先返回json数据(获取请求头中format的值)
-
最终进行内容协商返回给客户端json即可。
-
内容协商原理:
- 判断当前响应头中是否已经有确定的媒体类型。MediaType
- 获取客户端(PostMan、浏览器...)支持接受的内容类型(获取Accept请求头字段)
-
contentNegotiationManager:内容协商管理器,默认使用基于请求头的策略,即HeaderContentNegotiationStrategy。
- HeaderContentNegotiationStrategy 从请求头中确定客户端可以接收的内容类型
- ParameterContentNegotiationStrategy 从请求参数中获取客户端可以接受的内容类型。默认支持xml和json
-
- 遍历循环当前系统的MessageConverter,看谁支持操作这个对象(Man)
- 找到支持Man的converter,把converter支持的媒体类型统计出来。
- 客户端需要类型与服务端能够产生的类型进行对比
- 找到支持处理客户端要求的内容类型的converter(进行内容协商的最佳匹配媒体类型)。
- 用支持将对象转为最佳媒体匹配类型的converter。调用它进行转化。
自定义MessageConverter:
- 实现多协议数据兼容。Json、xml...
- @ResponseBody响应数据出去调用RequestResponseBodyMethodProcessor处理
- Processor处理方法返回值。通过MessageConverter处理
- 所有MessageConverter合起来可以通过支持各种媒体类型数据的操作(读、写)
- 内容协商找到最终的messageConverter
- 自定义MessageConverter:
-
配置类继承
WebMvcConfigurer
-
重写extendMessageConverters方法
@Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converter.add(new OwnMessageConverter()) } } }
-
converter.add()添加自己的MessageConverter。自己的converter需要实现HttpMessageConverter接口。
-
Web开发-视图解析与模板引擎
视图解析:SpringBoot默认不支持JSP、需要引入第三方模板引擎技术实现页面渲染。
视图解析原理流程:
- 目标方法处理的过程中,所有的数据都会在
**ModelAndViewContainer**
里面。包括数据和视图地址。 - 方法的参数是一个自定义类型对象(从请求中确定),把他重新放在ModelAndViewContainer。
- 任何目标方法执行完成以后都会返回ModelAndView(数据视图地址)。
- processDispatch处理派发结果(即页面如何响应)
- 根据方法的String返回值得到View对象(定义了页面渲染逻辑)
- 得到了
redirect: /页面
由thymeleaf来new RedirectView() - ContentNegotiationViewResolver里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
- view.render();视图对象调用自定义的render进行页面渲染工作。
- RedirectView如何渲染(重定向到一个页面)
- 获取目标url地址
- response.sendRedirect();
- 视图解析:
- 返回值以forward开始:new InternalResourceView(forwardUrl);
- 返回值以redirect开始:new RedirectView(),render就是重定向
- 返回值为普通字符串:new ThymeleafView()
模板引擎-Thymeleaf:
Thymeleaf是一个现代的服务器端Java模板引擎,适用于web和独立的环境,能够处理HTML, XML, JavaScript, CSS甚至纯文本。
-
基本语法
-
表达式:
-
-
Thymeleaf的使用:
-
引入Starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
-
Thymeleaf的自动配置都在
ThymeleafAutoConfiguration
中@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class }) @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration {...}
-
自动配置好的策略
- 所有thymeleaf的配置值在ThymeleafProperties
- 配置好了SpringTemplateEngine
- 配置好了ThymeleafViewResolver
只需直接开发页面即可。
-
引入Thymeleaf命名空间:
xmlns:th="http://www.thymeleaf.org"
-
-
Themleaf参考文档:
使用Thymeleaf构建后台管理系统:
-
项目创建:thymeleaf、web-starter、devtools、lombok
-
静态资源处理:thymeleaf自动配置好了,只需将所有的静态资源放到static文件下
-
页面跳转:
@PostMapping("/login") public String main(User user, HttpSession session, Model model){ if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){ //把登陆成功的用户保存起来 session.setAttribute("loginUser",user); //登录成功重定向到main.html; 重定向防止表单重复提交 return "redirect:/main.html"; }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"; }
-
页面展示:
<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}">Trident</td> <td th:text="${user.userName}">Internet</td> <td >[[${user.password}]]</td> </tr> </tbody> </table>
拦截器:
实现HandlerInterceptor来定义拦截规则,并将拦截器添加进容器中即可。
-
实现:
public class LoginInterceptor implements HandlerInterceptor { ... }
-
加入容器:
public class AdminWebConfig implements WebMvcConfigurer { /** * 添加自定义拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**", "/aa/**"); } }
拦截器原理:
- 根据当前请求,找到HandlerExecutionChain(可以处理请求的handler以及handler的所有拦截器)
- 先来顺序执行所有拦截器的preHandle方法
- 如果当前拦截器preHandle返回为true,则执行下一个拦截器preHandle
- 如果当前拦截器返回为false,则直接倒序执行所有已经执行了的拦截器的afterCompletion。
- 如果任何一个拦截器返回false。直接跳出不执行目标方法。
- 所有拦截器都执行为true。执行目标方法。
- 倒序执行所有拦截器的postHandle方法。
- 前面的步骤有任何异常都会直接倒序触发afterComletion
- 页面成功渲染完成以后,也会倒序触发afterCompletion。
文件上传:
-
前端代码:
<form role="form" method="post" th:action="@{/upload}" enctype="multipart/form-data"> <div class="form-group"> <label for="multipartFile">生活照</label> <input type="file" name="photos" id="multipartFile" multiple> </div> </form>
-
数据渲染:
-
MultipartFile 自动封装上传过来的文件
@PostMapping("/upload") public String upload(@RequestParam("email") String email, @RequestParam("password") String password, @RequestPart("profile") MultipartFile profile, @RequestPart("photos") MultipartFile[] photos) throws IOException { log.info("email为:{},password为:{},profile文件名为:{},photos文件个数为:{}个", email, password, profile.getOriginalFilename(), Arrays.stream(photos).count()); if (!profile.isEmpty()) { profile.transferTo(new File("D:\\tmp\\" + profile.getOriginalFilename())); } if (photos.length > 0) { for (MultipartFile photo : photos) { if (!photo.isEmpty()) { photo.transferTo(new File("D:\\tmp\\photos\\" + photo.getOriginalFilename())); } } } return "form/form_layouts"; }
-
自动配置原理:
- 文件上传自动配置类MultipartAutoConfiguration
-
自动配置了StandardServletMultipartResolver【标准类型文件上传解析器】
-
原理步骤:
-
请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求。
-
参数解析器来解析请求中的文件内容封装成MultipartFile
-
将request中文件信息封装为一个Map;MultiValueMap<String,MultipartFile>
-
-
FileCopyUtils:实现文件流的拷贝。
-
异常处理:
-
错误处理:默认情况
-
默认情况下,SpringBoot提供/error处理所有错误的映射。
-
对于机器客户端,它将生成Json响应,其中包含数据,Http状态和异常消息的详细信息,对于浏览器客户端,响应一个“whileable”错误试图,以Html格式呈现相同的数据。
-
-
错误处理:自定义页面
- 添加View解析为error
- 要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes
类型的组件以使用现有机制但替换其内容。 - error/下的4xx,5xx页面会被自动解析;
- 定制错误处理逻辑:
- 自定义错误页面:
- error/404.html error/5xx.html;有精确的错误状态码页面就精确匹配,没有就找4xx.html;如果都没有就触发空白页。
- @ControllerAdvice+@ExceptionHandler处理全局异常;底层是ExceptionHandlerExcpetionResovler支持的。
- @ResponseStatus+自定义异常;底层是ResponseStatusExceptionResovler,把responsestatus注解的信息调用response.sendError(statusCode,resolverReason)【tomcat发送的/error】;
- Spring底层异常,如参数类型转换异常;DefaultHandlerExceptionResovler处理框架底层的异常。
- 自定义错误页面:
- 自定义实现
HandlerExceptionResolver
处理异常;可以作为默认的全局异常处理规则-
效果:
-
代码实现:
// 设置最高优先级,当发生错误时优先使用此异常解析器 @Order(value = Ordered.HIGHEST_PRECEDENCE) @Component public class OwnHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { response.sendError(567, "是我喜欢的错误!"); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } }
-
Web原生组件注入:
-
使用Servlet Api:
- @ServletComponentScan(basePackages="包路径"):指定原生Servlet组件都放在哪里
- @WebServlet(urlPatterns=""):直接响应,不会经过Spring的拦截器。
- @WebFilter(urlPatterns={拦截路径}):拦截请求
- @WebListener
-
演示:
-
方式一:通过@ServletComponentScan+@WebServlet/@WebFilter/@WebListener实现原生组件的配置。
-
主启动类:
@MapperScan("cn.ikarts.springboot.mapper") @ServletComponentScan(basePackages = "cn.ikarts.springboot.servlet") @SpringBootApplication() public class SpringbootApplication { ... }
-
自定义Servlet:
@WebServlet(urlPatterns = "/my") public class OwnServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setCharacterEncoding("GBK"); resp.getWriter().write("这是自定义的Servlet~"); } }
-
自定义Fileter:
@WebFilter(urlPatterns = "/my") public class OwnFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String name = request.getParameter("name"); if ("ikart".equals(name)){ chain.doFilter(request,response); } } @Override public void destroy() { Filter.super.destroy(); } }
-
自定义监听器:
@Slf4j @WebListener public class OwnListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { log.info("contextInitialized执行了..."); } @Override public void contextDestroyed(ServletContextEvent sce) { log.info("contextDestroyed执行了..."); } }
-
-
方式二:通过@Configuration配置类将原生组件加入容器:
@Configuration(proxyBeanMethods = true) public class RegisterConfig { /** * 将原生Servlet注册进容器 */ @Bean public ServletRegistrationBean<OwnServlet> servletServletRegistrationBean() { return new ServletRegistrationBean<>(new OwnServlet(), "/my"); } @Bean FilterRegistrationBean<OwnFilter> filterRegistrationBean() { OwnFilter filter = new OwnFilter(); FilterRegistrationBean<OwnFilter> registrationBean = new FilterRegistrationBean<>(filter); registrationBean.addUrlPatterns("/my"); return registrationBean; } @Bean ServletListenerRegistrationBean<OwnListener> listenerRegistrationBean() { return new ServletListenerRegistrationBean<>(new OwnListener()); } }
-
-
DispatchServlet如何注册进来?
-
容器中自动配置了DispatchServlet属性绑定到WebServletProperties;对应的配置文件项是spring.mvc
-
通过ServletRegistrationBean
把DispatchServlet配置进来。 -
默认映射的路径是/
-
当请求进来tomcat会匹配那个Servlet能够处理。如果都匹配的Servlet能够处理优先交给匹配的Servlet,没有Servlet能够处理就统一交由DispatchServlet处理。即交给Spring处理。
-
多个Servlet都能处理到同一层路径,精确优选原则
-
嵌入式Servlet容器:
Servlet容器:即web服务器。例如:Tomcat、Jetty、Undertow
-
默认支持的webServer:Tomcat、Jetty、Undertow
- ServletWebServerApplicationContext容器启动寻找ServletWebServerFactory并引导创建服务器。
-
切换服务器
-
排除默认的服务器并导入指定服务器的POM:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency>
-
-
原理:
- SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
- web应用会创建一个web版的ioc容器
ServletWebServerApplicationContext
ServletWebServerApplicationContext
启动的时候寻找 **ServletWebServerFactory**(Servlet 的web服务器工厂---> Servlet 的web服务器)
- SpringBoot底层默认有很多的WebServer工厂;
TomcatServletWebServerFactory
,JettyServletWebServerFactory
, orUndertowServletWebServerFactory
- 底层直接会有一个自动配置类。
ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration
导入了ServletWebServerFactoryConfiguration
(配置类)ServletWebServerFactoryConfiguration
配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有TomcatServletWebServerFactory
TomcatServletWebServerFactory
创建出Tomcat服务器并启动;TomcatWebServer
的构造器拥有初始化方法initialize---this.tomcat.start();
- 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
定制Servlet容器:
-
实现
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
-
把配置文件的值和ServletWebServerFactory 进行绑定
-
修改配置文件 server.xxx
-
直接自定义 ConfigurableServletWebServerFactory
@Configuration public class ServerConfig implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> { @Override public void customize(ConfigurableWebServerFactory factory) { factory.setPort(9999); } }
定制化原理:
- 定制化的常见方式:
-
修改配置文件;
-
实现xxxCustomizer
-
编写自定义的配置类@Configuration+@Bean替换、增加容器中默认组件、视图解析器
-
Web引用编写一个配置类实现WebMvcConfiguration即可定制化web功能,也可以扩展功能。将我们自己的逻辑@Bean添加进容器即可。
@Configuration public class AdminWebConfig implements WebMvcConfigurer
-
@EnableWebMvc+WebMvcConfiguration可以全面接管SpringMvc,所有规则全部自己重新配置,实现定制和扩展功能。强烈不建议这种方式。
- 原理:
- WebMvcAutoConfiguration默认的SpringMvc的自动配置类。静态资源、欢迎页等等
- 当使用@EnableWebMvc会@Import(DelegatingWebMvcConfiguration.class)
- DelegatingWebMvcConfiguration:只保证SpringMvc的基本使用
- 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
- 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
- public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
- WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
- @EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
- DelegatingWebMvcConfiguration:只保证SpringMvc的基本使用
- 原理:
-
数据访问
数据源的自动配置-HikariDataSource:
-
导入jdbc场景:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency>
-
导入数据库驱动:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
-
分析自动配置:
- 自动配置的类:
DatasourceAutoConfiguration
:数据源的自动配置。-
修改数据源相关的配置:spring.datasource
-
数据库连接池的配置,是自己容器中没有Datasource才自动配置的。
-
底层配置好的连接池是:HikariDatasource
-
DatasourceTransactionManagerAutoConfiguration:事务管理器的自动配置。
-
JdbcTemplateAutoConfiguration:JdbcTemplate的自动配置,可以对数据库进行crud。
- 可以修改配置文件中的spring.jdbc来修改JdbcTemplate
-
JndiDataSourceAutoConfiguration:jndi的自动配置
-
XADataSourceAutoConfiguration:分布式事务相关的
-
- 自动配置的类:
-
如何修改默认数据源?
- 导入数据源pom
- 修改配置文件
spring.datasource.type
来指定数据源类型。
使用Druid数据源:
-
druid官方文档:
-
开启durid监控面板:
druid: stat-view-servlet: enabled: true
-
StatFilter:用于统计监控信息;如SQL监控、URL监控。
druid: stat-view-servlet: enabled: true #url监控配置 web-stat-filter: enabled: true url-pattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico' session-stat-enable: true
-
分析Druid自动配置:
- 扩展配置项 spring.datasource.druid
- DruidSpringAopConfiguration.class, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
- DruidStatViewServletConfiguration.class, 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
- DruidWebStatFilterConfiguration.class, web监控配置;spring.datasource.druid.web-stat-filter;默认开启
- DruidFilterConfiguration.class}) 所有Druid自己filter的配置
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";
-
Druid配置实例
spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* #监控SpringBean filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: # 对上面filters里面的stat的详细配置 slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false
整合Mybatis:
-
导入pom
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency>
-
配置模式(即通过配置xml来完成sql操作)
- 全局配置文件
- SqlSessionFactory: 自动配置好了
- SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
- @Import(AutoConfiguredMapperScannerRegistrar.class);
- Mapper: 只要我们写的操作MyBatis的接口标准了 @Mapper 就会被自动扫描进来
-
配置文件中配置Mybatis
# 配置mybatis规则 mybatis: config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置 mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置 configuration: map-underscore-to-camel-case: true #开启驼峰匹配
-
注解模式(通过注解完成sql操作):
@Mapper public interface CityMapper { @Select("select * from city where id=#{id}") public City getById(Long id); public void insert(City city); }
-
混合模式(即注解模式和配置模式结合使用):
@Mapper public interface CityMapper { @Select("select * from city where id=#{id}") public City getById(Long id); public void insert(City city); }
-
总结:Mybatis使用流程
- 引入mybatis-starter
- 配置application.yaml中,指定mapper-location位置即可
- 编写Mapper接口并标注@Mapper注解
- 简单方法直接注解方式
- 复杂方法编写mapper.xml进行绑定映射
- @MapperScan("com.atguigu.admin.mapper") 简化,其他的接口就可以不用标注@Mapper注解
整合 MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
-
导入pom:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency>
-
自动配置:
- MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。mybatis-plus:xxx 就是对mybatis-plus的定制
- SqlSessionFactory 自动配置好。底层是容器中默认的数据源
- mapperLocations 自动配置好的。有默认值。
classpath*:/mapper/**/*.xml
;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下 - 容器中也自动配置好了 SqlSessionTemplate
- @Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan("cn.ikarts.admin.mapper") 批量扫描就行
优点:
- 只需要我们的Mapper继承 BaseMapper 就可以拥有crud能力
整合Redis:
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
-
导入pom
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
自动配置:
- RedisAutoConfiguration 自动配置类。RedisProperties 属性类
- 连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration
- 自动注入了RedisTemplate<Object, Object> : xxxTemplate;
- 自动注入了StringRedisTemplate;k:v都是String
- key:value
- 底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis
-
切换至Jedis
<!--导入jedis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
spring: redis: host: service.ikarts.cn port: 6379 client-name: ikart-computer client-type: jedis
单元测试
JUnit5 的变化
**Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,**作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。
Junit5组成
-
注意:
-
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)vintage
<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> </dependency>
-
-
引入单元测试:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
SpringBoot整合Junit以后。
- 编写测试方法:@Test标注(注意需要使用junit5版本的注解,即
org.junit.jupiter.api.Test
) - Junit类具有Spring的功能,例如:@Autowried的自动注入。
- **@Test :**表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
- **@ParameterizedTest :**表示方法是参数化测试
- **@RepeatedTest :**表示方法可重复执行
- **@DisplayName :**为测试类或者测试方法设置展示名称
- **@BeforeEach :**表示在每个单元测试之前执行
- **@AfterEach :**表示在每个单元测试之后执行
- **@BeforeAll :**表示在所有单元测试之前执行
- **@AfterAll :**表示在所有单元测试之后执行
- **@Tag :**表示单元测试类别,类似于JUnit4中的@Categories
- **@Disabled :**表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- **@Timeout :**表示测试方法运行如果超过了指定时间将会返回错误
- **@ExtendWith :**为测试类或测试方法提供扩展类引用
断言:
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告;
断言案例:
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "不符合预期值");
assertNotEquals(3, 1 + 1,"错误:不期望值与期望值相等");
assertNotSame(new Object(), new Object(),"错误:这两个为同一对象");
Object obj = new Object();
assertSame(obj, obj),"两个不为同一对象");
assertFalse(1 > 2);
assertTrue(1 < 2);
assertNull(null);
assertNotNull(new Object());
}
-
数组断言:
@Test @DisplayName("array assertion") public void array() { assertArrayEquals(new int[]{1, 2}, new int[]{1, 2}, "两数组不相等"); }
-
组合测试:
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言.所有断言成功才算测试成功
@Test @DisplayName("assert all") public void all() { assertAll("Math", () -> assertEquals(2, 1 + 1), () -> assertTrue(1 > 0) ); }
-
断言测试:
/** * 断言异常,断定你会抛出指定的异常,如果没有抛出异常则测试失败 */ @Test @DisplayName("异常测试") public void exceptionTest() { Exception exception = Assertions.assertThrows( //扔出断言异常 Exception.class, () -> System.out.println(1 % 0)); }
-
前置条件:
@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") ); } }
-
快速失败:
/** * 快速失败 */ @Test @DisplayName("fail") public void shouldFail() { fail("This should fail"); }
-
嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
@DisplayName("A stack") class TestingAStackDemo { Stack<Object> stack; @Test @DisplayName("is instantiated with new Stack()") void isInstantiatedWithNew() { new Stack<>(); } @Nested @DisplayName("when new") class WhenNew { @BeforeEach void createNewStack() { stack = new Stack<>(); } @Test @DisplayName("is empty") void isEmpty() { assertTrue(stack.isEmpty()); } @Test @DisplayName("throws EmptyStackException when popped") void throwsExceptionWhenPopped() { assertThrows(EmptyStackException.class, stack::pop); } @Test @DisplayName("throws EmptyStackException when peeked") void throwsExceptionWhenPeeked() { assertThrows(EmptyStackException.class, stack::peek); } @Nested @DisplayName("after pushing an element") class AfterPushing { String anElement = "an element"; @BeforeEach void pushAnElement() { stack.push(anElement); } @Test @DisplayName("it is no longer empty") void isNotEmpty() { assertFalse(stack.isEmpty()); } @Test @DisplayName("returns the element when popped and is empty") void returnElementWhenPopped() { assertEquals(anElement, stack.pop()); assertTrue(stack.isEmpty()); } @Test @DisplayName("returns the element when peeked but remains not empty") void returnElementWhenPeeked() { assertEquals(anElement, stack.peek()); assertFalse(stack.isEmpty()); } } } }
-
参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
- @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
- @NullSource: 表示为参数化测试提供一个null的入参
- @EnumSource: 表示为参数化测试提供一个枚举入参
- @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
- @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
/** * 参数化测试 */ @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场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
简介:
-
POM:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
1.x与2.x的不同:
如何使用?
-
引入场景
-
配置文件暴露监控信息
-
访问http//localhost:8080/acuator/
management: endpoints: enabled-by-default: true #暴露所有端点信息 web: exposure: include: '*' #以web方式暴露
-
可视化面板:
-
项目地址:
GitHub - codecentric/spring-boot-admin: Admin UI for administration of spring boot applications
-
官方文档:
-
Pom
<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.3.1</version> </dependency>
-
-
常用暴露端点信息
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:监控状况
- Metrics:运行时指标
- Loggers:日志记录
Health Endpoint:
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
- 重要的几点:
- health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
- 可以很容易的添加自定义的健康检查机制
Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到。
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或者扩展已有Metrics
管理Endpoint
- 开启与禁用Endpoint
-
默认所有的Endpoint除过shutdown都是开启的。
-
需要开启或者警用某个Endpoint。配置模式为
Management.endpoint.**${endpointName}**.enable=true
即可开启management: endpoint: beans: enabled: true
-
或者禁用所有的Endpoint然后手动开启指定的Endpoint
management: endpoints: enabled-by-default: false endpoint: beans: enabled: true health: enabled: true
-
暴露Endpoints
- 支持的暴露方式
-
HTTP:默认只暴露health和info Endpoint
-
JMX:默认暴露所有Endpoint
-
除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
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 No No httptrace Yes No info Yes Yes integrationgraph Yes No jolokia No No logfile No No loggers Yes No liquibase Yes No metrics Yes No mappings Yes No prometheus No No scheduledtasks Yes No sessions Yes No shutdown Yes No startup Yes No threaddump Yes
-
定制Info:
-
修改配置文件:
info: webside: @groupId@ mysql-version: @mysql.version@ applicationName: ${spring.application.name}
-
访问:http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息
-
原理解析
Profile功能
为了方便多环境适配,springboot简化了profile功能。
application-profile功能:
- 默认配置文件 application.yaml;任何时候都会加载
- 指定环境配置文件 application-.yaml
- 激活指定环境
- 配置文件激活
- 命令行激活:
java -jar xxx.jar --**spring.profiles.active=prod**
- 修改配置文件的任意值,命令行优先
- **默认配置与环境配置同时生效,同名配置项,profile配置优先,**即当环境配置中配置的会替换默认配置,如果没有配置则使用默认配置。
条件装配:
@Profile("环境")
- 根据不同的环境决定那些类、方法生效。
- 可以标注在方法或类上,作用域根据标注位置而定。
Profile分组:
- 指定prod组,此时dev,test环境配置同时生效;
spring:
profiles:
group:
prod[0]: dev
prod[1]: test
active: prod
外部配置源
常用:Java属性文件、YAML文件、环境变量、命令行参数;
配置文件查找位置
(1) classpath 根路径
(2) classpath 根路径下config目录
(3) jar包当前目录
(4) jar包当前目录的config目录
(5) /config子目录的直接子目录
配置文件加载顺序:
- 当前jar包内部的application.properties和application.yml
- 当前jar包内部的application-.properties 和 application-.yml
- 引用的外部jar包的application.properties和application.yml
- 引用的外部jar包的application-.properties 和 application-.yml
Starter启动原理
-
starter-pom引入 autoconfigurer 包
-
autoconfigure包中配置使用
META-INF/spring.factories
中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类 -
编写自动配置类 xxxAutoConfiguration -> xxxxProperties
-
@Configuration
-
@Conditional
-
@EnableConfigurationProperties
-
@Bean
引入starter -- xxxAutoConfiguration --- 容器中放入组件 ---- 绑定xxxProperties ---- 配置项
注解
- 关于@PostConstruct
@Resource和@Autowired的区别:
@Resource和@Autowired都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入。
共同点:
- 两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。
不同点:
- @Autowired:@Autowired为Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired;只按照byType注入。@Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用。如下
public class TestServiceImpl {
@Autowired
@Qualifier("userDao")
private UserDao userDao;
}
- @Resource:@Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。
SpringBoot使用中注意点:
单词
- locale : /loʊˈkæl/ n. (事情发生的)地点,现场
- registry:/ˈredʒɪstri/ n. 注册;登记处;挂号处;船舶的国籍
- merge:v.(使)合并、融合、融入
- dispatch:v.派遣、发送
- matrix:/ˈmeɪtrɪks/ n.矩阵、模型
- negotiation:/nɪˌɡoʊʃiˈeɪʃn/ n.协商、谈判
- trigger:n.触发、诱因、触发器
参考文档
-
参考笔记:
-
Banner图在线生成器:
-
SpringBoot官方文档:
Q.E.D.