spring 能干什么
来spring官网看看,我发现我所学习的只是它的冰山一角。
微服务
支持模块化开发,把应用拆分成一个一个模块,好处其实有很多,我列举一些我自己能想到吧。
首先是解耦合,不至于每个模块的耦合很大,而且很方便程序员去设计数据库,因为需要考虑的东西确实是少了。
单从解耦合这个角度来看,我的各个模块可以独立部署,互不干扰,我测试或者更新其中一部分,对其他功能是没有任何影响的,这是单体应用无论如何都不可能做到的。
这个不得不说DDD,我觉得它里面有一个很好的思想,就是领域划分,如果我进行了领域划分,那么我在微服务拆分的时候,就会很方便去拆分。
在微服务场景下,肯定会产生很多问题,这个就需要我们有一种好的架构,能让它在单体成长的过程中,就有了微服务拆分的意识,当单体成长到一定的水平,就能很容易的划分开。所以,我得先学习微服务,然后在使用DDD的领域划分,我得在单体中去思考,拆分之后,这个要如何编写代码。
响应式
可以用更少的线程,更少的资源来实现跟高的并发,我以前只是简单的了解过,没想到它这里有这样的好处,打算尝试一下。
Serverless
没想到spring家族竟然支持这个,我一直以为这个是一个很新的技术,值得去了解一下,毕竟很多小功能函数没有必要设计一个web应用来使用
spring boot 2.x
基于java8 提供了接口的默认实现,在以前,比如我一个借口有5个函数,A类需要实现1,2函数,B类需要实现4,5函数,但是我在实现接口的时候,不得不实现其他必要的空接口,所以用一个java类来做适配,来实现空方法,但是现在不需要中间这层类了,但其实我觉得加一个类也挺好,毕竟抽象类的功能比接口要强大,但可惜java不支持多继承。
优点
- 内嵌web服务器
- 这个比tomcat放war包好多了,通过jar独立启动,让端口号各不相同,而且自定义jvm配置很方便,而且还能和pm2这种线程管理工具结合,确实太方便了
- 自动starter依赖,简化构建配置,自动配置Spring以及第三方功能
- 最喜欢的功能,没有之一,因为我个人觉得,配置这种东西本身就是让用户来定制需求的,也就是说,我们的应用要有能力,自动判断出用户的需求,约定大于配置,自动判断是我的设计理念
- 提供生产级别的监控、健康检查及外部化配置
- 这个我感觉很有用,我在项目上线的时候,才意识到,这个真的很重要,目前没有用过
- 无代码生成、无需编写XML
- 这个是简单配置最适合的地方了,我以前也分析过,面对不同的配置需求,不同的配置方法各有各自的优点,但是我也可以结合起来使用,使收益最大化,这个时间本身就不是什么非此即彼的世界
官方文档
看了一下,还是有一些东西的,打算学完后,梳理一下官方文档
还有看更新,没有什么比看官方文档更能快速并且准确的了解新的特性了,不过我一般是先看博客,因为他们会把重要的更新点摘出来,然后再看看官方,最后可以去看源码,没有什么比源码更加详细了,我指的是源码的文档。
yaml
字符串不需要加’’,“”,加的话是为了防止转义,比如on,off会变成ture或者false
使用细节
maven
版本号放在属性里面管理,说一下这样做的原因,我经常改的是依赖的版本,而依赖一般是不会切换的,因为切换依赖会涉及到大量代码的改动,然后是有的依赖需要版本的统一,它们共用一个版本
maven爆红的问题,这个一般是网络的问题,如果maven下载失败的话。
1. 用镜像
2. 删除下载到一半到失败文件
3. spring boot parent 爆红,可以重新创建一个spring boot版本最好和这个一样,然后尽量勾选有的依赖,然后你会发现这些都不爆红了,如果没有对应的版本,就选一个差不多的,比较个人使用其实区别并不大
4. 依赖项目爆红,如果有的不爆红,有的爆红,而且爆红的还是那种没有版本号的依赖,很简单,直接设置版本号,让maven根据这个下载就行
运行时参数
这个实现原理其实简单,就是我们平时项目启动的时候,传递到main方法里面的args参数。
组件管理
ConfigurableApplicationContext run = SpringApplication.run(HearuApplication.class, args);
String[] beanDefinitionNames = run.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
log.info(beanDefinitionName);
}
这个就是我们平时最容易忽视的spring boot启动类,它是有返回值的,通过这个返回值,我们可以获取到所有的bean。run.getBean(DispatcherServlet.class);
可以获取到bean里面的实例。
run里面的方法是很丰富的,肯定能支持你的所有对组件的要求,它不止是接口定义的那些,因为接口继承了好多,所以是好多接口方法的集合
创建新的类(包括它的包)
我们用idea创建一个类的时候,如果输入的是utils.WebUtil,那idea会自动帮助我们生成utils包,所以idea还是极其强大的,我们对它的功能了解的越多,意味着我们越能发挥出更大的能力
attribution
原来spring自己用attrbution的时候,用的都是以类名为key,这样就可以保证很用户自己定义的类不冲突
自动配置
依赖管理
原理是maven的spring-boot-starter-parent,这个里面主要负责了build
构建相关的,然后是spring-boot-dependencies,这个里面维护了大量的依赖和版本管理,到此版本管理解决了。那复杂的依赖关系怎么做?
你随便点进一个start,你就会发现这个里面维护了相关的依赖,而且还有版本,而且都以spring-boot-starter这个依赖为核心
自动装配
@SpringBootApplication
效果和下面三个结合起来是一样的,我突然就想到spring那个工具类了,因为我在想,如果我把注解标注在注解上,是扫码不到的,为什么spring中有这么多标注给注解的注解呢,它什么是怎么起作用的呢,原理仅仅是一个工具类,这个工具类在获取注解的时候,可以获取注解的注解。而且如果你看了AnnotationUtils.findAnnotation
的源码,就会值得,它不止会扫描注解的注解还会扫描接口和超类的注解,当然你不用担心效率,比较这个注解是我们自己标注的。
spring的工具类还是非常有用的,可以多看看源码学习一下,这样可以让自己少创造很多轮子,而且你能感受到他们对反射的加强,而且能够增强你代码的严谨性,毕竟我从来不会怀疑是spring的错,就像不会怀疑java一样
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
@Configuration
如果我们想配置bean,比如我们配置redis的bean,让它有string,object和各种序列化的能力,我们就可以重新配置一个。
或者,我们像mybatis plus的分页插件,需要额外配置,因为有的时候,可能并不需要开启这个功能,但是我个人感觉,这个应该作为默认实现。倒是像字段填充那种,才真正的需要额外的配置bean,而且这样比配置一个类方便的多了,但是mybatis plus采取了一种更合适的方法,就是利用接口。
还有一种很方便的就是组件的覆盖,比如objectmapper,我们加一个@Primary注解,来确保代替原来的组件,这个定制化增强它的功能,也就是说。
也就是说,如果你想增强一个组件或者定制化一个组件,就用@bean的方法,定制化的意思就是,这个对象比较常用,而且创建比较复杂,创建对象本身就是ioc的意义所在
但是你要注意几点,就是标注之后会被spring代理,成为一个代理对象,当你在执行对象方法的时候,已经不是原来对象的方法了,而是已经被增强过的方法。比如你通过这个bean获取的对象总是同一个,当然你可以通过修改注解的属性让spring不代理它。
@bean注解的方法是组件的id,然后返回的类型就是我们注入的类型,当然可以通过bean的属性来定义我们的id
Full模式与Lite模式
配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
依赖关系指的是组件之间会产生关系,一般不需要改变配置,因为产生依赖关系太正常了
@Import
默认以全类名为id,然后把这个类注册到组件里面
@Conditional
条件装配,按条件决定是否装配指定的组件
@ImportResoure
支持导入xml等其他资源文件,这里区别于配置
@ConfigurationProperties
这个必须配合@Component使用!而且我发现你得运行一下spring boot才会有提示,这下我最觉得不方便的问题就解决了,而且我现在学会了注入静态属性,这样可以定义很多东西了。也许你会问,为什么需要@Componet呢,因为有的时候,你并不一定需要把这些配置注入进去,所以还有另外一种方法开启这个注解的功能@EnableConfigurationProperties,就像是@EnableXXX一样
而你这个类名最好是XxxProperties,和注册bean的XxxConfiguration区别一下
源码分析
@EnableAutoConfiguration
自动配置的核心
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
@AutoConfigurationPackage
@Import(AutoConfigurationPackages.Registrar.class)
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//AnnotationMetadata是注解的元信息,我们可以看到这个注解标注在了哪个类上面
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
//new PackageImports(metadata).getPackageNames()获取到了我们的包名,说明它是为了注册我们在包内定义的所有注解,所以说,不要把java类写在启动类的包外边
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
利用上面这个类,注册我们自己定义的一系列组件
AutoConfigurationImportSelector
@Override
//选择要导入哪些组件
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);//明显这个是核心方法
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
怎么学习这个源码呢,最方便的方法就是,自己打断点看,但前提你得了解足够多的外部使用,当然,你连使用都不了解的话,学习源码就是一个笑话。
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);//明显关键代码是这个
configurations = removeDuplicates(configurations);//移除重复
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);//移除排除
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {//获取所有的可能的bean,然后再进行排除
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
我们直接来到核心方法,真的,看哪个方法是核心很容易,一般特点是获取这个结果后直接返回,或者对这个结果做一下处理后返回,说实在的,一个方法的目的不就是为了得到返回的结果吗,那我关注的肯定是这个结果。
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
//从缓存里面得到,你会发现是一个List<String>明显是为了支持不止一个bean,而且也能说明,有很多核心的类比如spring自己的组件,已经被提前加载了
if (result != null) {
return result;
}
//下面就是没有缓存初次加载的情况
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);//"META-INF/spring.factories"
//这个常量就是保存我们在这个文件下配置的所有类,这样就可以在spring boot外部启动,这个就是原理,你知道使用方法,一看原理的时候,很容易就懂了
while (urls.hasMoreElements()) {
URL url = urls.nextElement();//jar:file:/D:/java/maven/repository/org/springframework/spring-beans/5.3.15/spring-beans-5.3.15.jar!/META-INF/spring.factories
//不理解源码,还不理解例子吗,这就是debug的好处,能加强和方便我们看源码的能力,而且可以随处计算内存中的值,实在是太方便了,我以后尽量用debug看源码
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
主要是为了扫描spring-boot-autoconfigure-2.6.3.jar这个包下的spring.factories,它会扫描出所有的配置类,然后按需加载组件,所以,启动时间才会那么漫长
除此之外,你可以多学习学习org.springframework.boot.autoconfigure包下的自动配置是怎么写的,比如我看到了@Conditional可以标注一个类,然后类继承SpringBootCondition,进行复杂的代码级别的判断
那最常见的一个功能,就是如果你用户配置了,我就不配置,如果你用户配置了,我就使用你配置的,这种是怎么实现的呢?@ConditionalOnMissingBean。而且bean是可以传递参数的,参数自动从组件里面获取,然后赋值,就像@Autowire注入静态变量一样
小技巧,yaml开启debug=true可以查看自动配置的情况,甚至告诉你为什么这个配置没有生效
静态资源配置
WebMvcAutoConfiguration
@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{
}
@ConditionalOnProperty
prefix 前缀 用xx.xxx.xx分割指定
name 这个才是真正的配置属性
matchIfMissing = true 如果没有在yaml中配置,就按你配置了算
WebMvcAutoConfigurationAdapter
如果一个类上面有@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class }),明显是在说明,这个两个配置文件的信息就是为了这个配置类准备的
而且这个配置类只有一个有参构造器,说明所有的参数都从容器或获取,如果我们在spring中看到其他这样的写法,也说明是从容器中获取的。
缓存时间
spring boot 的静态资源也是支持缓存的,原理是通过配置请求头来实现的。但其实不推荐这样做,用nginx也可以实现。而且看官方的配置文档,或者使用demo也可以获取很多使用的方法。
其他
我们写的代码,可以进行配置的,要放到配置类里面,宁可多加一些判断,也不能写死,写死是最不好的方法了
Restful
我个人觉得不好,这种只是适合简单的后台操作,毕竟我们对一个资源怎么可能才只有四种简单的操作呢,spring boot默认是没有开启的,需要我们在yaml配置。前端传递参数用post,然后提供参数_method=PUT/Delete等,当然这种是表单传递参数,如果通过ajax工具,我感觉是不需要开启这个的,毕竟从源码层面看确实是这样的。
spring:
mvc:
hiddenmethod:
filter:
enabled: true
请求处理
这部分内容是对以前做的回顾和补充,不会太全面
try {
processedRequest = checkMultipart(request);//检查是否为文件上传
multipartRequestParsed = (processedRequest != request);
// 获取处理器,一般来说,就通过请求路径决定用哪个controller的方法
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 获取一个要执行方法的类,因为执行方法比较复杂,要解析注解标注的参数
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// get和head结合浏览器缓存处理
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;//你看这个就直接返回了,不处理
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 实际执行方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
RequestMappingHandlerMapping
本质上就是先决定用哪个controller去处理请求,设计思路是有多个Handlermapping,然后其中有一个是RequestMappingHandlerMapping,如果我们在Controller上面标注了它,我相信,它在扫描的时候,就会扫码到我们的controller注解,然后解析里面的方法,把他们都加到一个mappingRegistry里面,然后先根据路径筛选,然后是通过请求方法筛选,如果发现有多个,就会报错,这我也经历过。所以,我发现spring匹配的大部分思路都是,先获取全部,然后筛选有可能的,最后才是精确的筛选,这样的编码实现明显会简单很多,而且很多都是交给不同的类去处理的,这样就更简单了,而且简单有一个最大的好处就是,不容易出错。看起来代码多,其实这样代码都是化简使用的。
而且这个接口也设计的特别好,能够让我们自定义,比如接口有常见的V1,V2这种,如果我们一个controller里面写那么多v1,v2难免会很麻烦,不如自己自定义到不同的类执行
为什么需要Handler?
因为我们处理的不止有一种请求,还要处理静态资源请求,这个思想在tomcat中就体现过。
RequestMappingHandlerAdapter
invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod)
这个方法的定义很明显,是想把request里面的参数绑定在方法里面,而且spring对原始的method做了加强,这样的话,可以做很多自己想做的事情,真的是对java反射的史诗级加强啊。
if (this.argumentResolvers != null) {//参数解析
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {//返回值处理
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
参数解析
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);//是否支持解析这种参数
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;//具体解析规则
}
InvocableHandlerMethod
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
//获取方法的参数,这个是spring包装的一个类,这个类里面保存了方法的所有参数
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"));
//这个的实现是遍历判断,如果成功了就加入缓存,所以spring mvc执行的会越来越快,而且spring里面很多都是有缓存的,缓存其实一点也不能,难的是并发场景下的缓存
}
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;
}
具体的解析过程就是解析器的事情了,其实原理也不难,就是request里面获取各种各样的值,然后保存到request的attr里面,然后获取这个值,获取后放到arg数组里面,然后给传递给方法执行就可以了。那么我看几个关键注解的处理方法!
AbstractNamedValueMethodArgumentResolver
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {//如果解析完,发现是空的
if (namedValueInfo.defaultValue != null) {//如果defaultValue不是空的
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
//设置默认值
//这种情况就是我们不传递参数
}//当默认值就是空,而且还需要这个参数,而且参数还不是Optional类型
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);//肯定是抛出异常
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());//这个处理空值处理处理boolen值以外,其他都会给你抛出异常
}
//所以给一个结论,defaultValue的意义就是,前端没有传递,我把这个值设置为defalutValue,和required基本没有什么关系,然后就是required了,true的话,你没有传递,而且没有默认值会报错,false的话,就不管了,但你要在代码层面判断一下它为null,我觉得这个方法比我设置一个特殊的defalutValue好,这就是看源码的收获!!!
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
//这种情况就是告诉你,如果参数是空字符串,而且配置了默认值,我就会给你设置默认值,这个和我之前做的实验是一样的,不过看源码一下就能看出来,不然还要尝试特别多的组合
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (binderFactory != null) {//这个是对参数的特殊处理
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);//如果我们要对参数做特殊处理的话,就是比如日期类型的转换,我发现LocalDate用这个更好些,这个就是学习源码的意义
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
// Check for null value after conversion of incoming argument value
if (arg == null && namedValueInfo.defaultValue == null &&
namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
}
}
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
BeanUtils
无意间发现了spring boot的一个工具类,这个工具类竟然是一个抽象类,我一想,这样也可以啊,毕竟都是静态方法,而且不能直接创建对象,比用一个private修饰的构造器好多了,我以后也用这种方法弄工具类
HttpMessageConverter
这个也是spring boot用来转换的接口,被我在看requestBody源码的时候发现了
Model
虽然不是很常用,但是这些原理都是在BindingAwareModelMap一个单例中获取,而且数据传递的原理就是放在了request里面的attribution里面,比如转发请求就可以用这个来传递数据,不过在微服务场景下,消息队列明显更合适一些。
ModelAttributeMethodProcessor
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注解,因为不常用
else {
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;
}
自定义converter
@Component
public class StringToPOJOConverter implements Converter<String, POJO> {
@Override
public POJO convert(String source) {
POJO pojo = new POJO();
if(StringUtils.hasText(source)){
String[] split = source.split(",");
if(split.length!=2){
throw new IllegalArgumentException("格式为name,age");
}
pojo.setName(split[0]);
try {
pojo.setAge(Integer.valueOf(split[1]));
}catch (Exception e){
throw new IllegalArgumentException("年龄必须是整数");
}
return pojo;
}
throw new IllegalArgumentException("字符串不能为空");
}
}
响应处理
基本原理
和请求处理的类似,在处理方法之前,和确定参数赋值器的同一个函数,就确定了用哪个返回值处理器。利用的接口是HandlerMethodReturnValueHandler
RequestResponseBodyMethodProcessor
这个就是我们最常用的返回处理器,我在想,是不是自己写一个接口,能把这个变成RPC接口呢?
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
if (value instanceof CharSequence) {//如果返回的是字符串类型
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {//一般都是我们的包装类型
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
if (isResourceType(value, returnType)) {//判断是否为流数据
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&
outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
}
catch (IllegalArgumentException ex) {
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}
//下面是用来确定返回类型的,有的时候,浏览器想要json,有的时候想要xml,我们都需要确定这些内容
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> acceptableTypes;
try {
acceptableTypes = getAcceptableMediaTypes(request);
}
catch (HttpMediaTypeNotAcceptableException ex) {
int series = outputMessage.getServletResponse().getStatus() / 100;
if (body == null || series == 4 || series == 5) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring error response content (if any). " + ex);
}
return;
}
throw ex;
}
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
List<MediaType> mediaTypesToUse = new ArrayList<>();
//下面就是遍历自己能转换的和浏览器想要的,找一个最合适的
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);//看名字就知道,是在决定权重
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
//看看我们的消息转化器,按照优先级排序,能处理了哪一个
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
if (body != null) {
Set<MediaType> producibleMediaTypes =
(Set<MediaType>) inputMessage.getServletRequest()
.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) {
throw new HttpMessageNotWritableException(
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass()));
}
}
HttpMessageConverter
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
读写兼备,然其实就把对象转换成json,并且对response做了对应的处理
自定义
这个不错,可以弄一个返回excel的协议。
public class MyMessageConverter implements HttpMessageConverter<ExcelFile> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(ExcelFile.class);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/excel");
}
@Override
public List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return HttpMessageConverter.super.getSupportedMediaTypes(clazz);
}
@Override
public ExcelFile read(Class<? extends ExcelFile> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(ExcelFile file, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
OutputStream body = outputMessage.getBody();
body.write(file.getBytes());
}
}
@Configuration
public class MyMessageConverterConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyMessageConverter());
}
};
}
}
JUnit5 单元测试
spring boot 2.4以后,junit以前的一些测试不能使用了,当然你可以自己导入依赖配置上,这个就是为什么最新版的spring boot使用单元测试会出现错误
官网的示例也是非常多,而且通俗易懂的,可以去看看
注解
@Transactional
标注这个会自动回滚事务,非常适合我们做测试,这个真的很重要!
@DisplayName
可以给我们的测试方法和测试类起一个名称,方便我们去看测试的结果
@beforeEach 和 @afterEach
和aop是一样的,在每个测试方法的前后执行,会执行多次,有多少个测试执行多少次
@beforeAll 和 @afterAll
在所有测试方法的前后执行,只会执行一次,所以要设置成静态方法static
@Timeout
可以进行超时时间的测试,规定这个方法必须在多少单位时间内执行完成
@Disabled
禁用这个测试
@RepeatedTest
重复测试,设置重复次数,然后测试,比如我们要随机测试数据,当然希望测试的次数多一些,这样随机出来的值也多
@ParameterizedTest
可以通过ValueSource来为方法注入参数
断言
导包
import static org.junit.jupiter.api.Assertions.*;
因为这个静态方法都非常的常用,同时你要注意前面加了static这个标志。
普通断言
assertTrue(“a”.equals(“b”)); 可以额外加一个参数表示断言失败会显示什么信息
而且断言失败后,后面的代码是不会执行的
组合断言
很多时候,我们最终结果不是一个断言就能判断出来的
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
快速失败
fail(“测试失败”);
系统监控
yaml
yaml里面的@@语法可以获取到maven里面的配置,而且还有自动提示
使用
maven
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yaml
spring:
boot:
admin:
client:
url: http://ip:port/
application:
name: 项目名称
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
多环境
注意spring boot 2.4发送了较多的变化!
规则
- 不配置环境,就是默认环境,就是什么都不加的原始配置文件
- 默认配置文件永远都会加载
- 配置了相同的属性,选择了某个开发环境,其他环境的该属性不会生效
@Profile
根据环境决定装配规则
分组配置
spring:
profiles:
active: "dev"
group:
"dev": "dev-db,dev-mq"
"prod": "prod-db,prod-mq"
环境选择
默认环境的配置
spring:
profiles:
active: "dev"
运行时指定的参数
java -jar hearu-2.6.3.jar --spring.profiles.active=prod
以上参数都是经过测试,确保正确的!!!
自定义Starter
步骤
- 创建一个空项目
- 创建一个maven模块,场景启动器模块xxx-spring-boot-starter
- 利用springboot的场景启动器,初始化一个xxx-spring-boot-autoconfigure
- 在starter的maven里面依赖我们的自动配置包
- 删除autoconfigure的打包和测试依赖,yaml配置文件和主程序也删掉,本质就是创建一个含有spring-boot父工程的maven项目
- 然后你就可以在autoconfigure里面写代码了
注意
不要在组件上标注@compent之类的注解,因为我们的组件在主项目外,是扫描不到的,所以要通过自动配置类来帮助我们扫描,一般是条件装配和import
条件装配还有一个问题就是你要标注@configuration注解,然在里面根据条件注入bean
因为spring不知道哪个是你的自动装配类,所以你需要在类路径下创建一个META-INF/spring.factories配置你的自动装配类
拦截器
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
再获取处理器的过程中,就已经获取到拦截器链了,因为我们在配置拦截器的时候,配置了拦截器的路径,而我们在获取处理器的时候,又恰好知道这个请求路径,反过来想,路径不同的话,处理的拦截器链肯定是不同的。
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
上面的代码很明显的说明了preHandle执行返回true和false的不同结果,理论上,一旦返回false,后面的是不可能执行的,但是实际情况多少还是有点不一样的,当然我说的是出现异常的时候
preHandler
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
//按顺序执行preHandler
if (!interceptor.preHandle(request, response, this.handler)) {
//这是返回false的情况
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;//每次循环都标记了拦截器执行到的位置,所以下面才能出现逆序从哪个地方开始逆,这个时候我们不妨给出一个结论。就是如果我们拦截器走到某一步停止了以后,后面的拦截器的pre和after都是不会执行的
}
return true;
}
afterHandler
for (int i = this.interceptorIndex; i >= 0; i--) {//逆序执行
//现在我就清楚了,责任链是如何实现的了,其实就是一个顺序的问题,不一定是通过堆栈去模拟的,我们 的正逆循环也完全没有问题
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
//执行的是这个after
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
catch (Exception ex) {
//当我们在任意一个环节出现异常的时候,after也是会被执行的,其实看源码好多现象都不用去做实验
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
这个有一个特点,就是它是永远都会触发的,而且只会除非已经执行过的拦截器的逆序。永远会触发是因为我有异常捕获,已经执行过的,是因为我有一个指针,逆序就是i–
postHandler
applyDefaultViewName(processedRequest, mv);//明显在post执行前,视图有了一定程度的渲染,而且我们在post中,是可以看到视图参数mv被传递进去的
mappedHandler.applyPostHandle(processedRequest, response, mv);
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
//下面是倒序执行所有的,因为你能走到这一步,说明所有的pre都已经放行了,而且这里也说明了,只有所有的pre放行,它才有可能执行一个,而且肯定是逆序的全部执行
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
interceptor.postHandle(request, response, this.handler, mv);
}
}
这样我们对源码简单了解一下,对执行顺序的把握肯定要比看那些文档或者说明准确的多
异常处理机制
在我们的适配器执行方法的时候,无论如何都会保存一个webRequest.requestCompleted();
来标识这个方法已经被执行完了,如果我们遇到重复执行的问题,可以考虑一下这个,它在类RequestMappingHandlerAdapter
中。
我来说一下大致的思路,就是把你抛出的异常捕获了一下,然后保存在一个变量里面,这样spring就获取了这个异常的相关信息。
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
//我们一般执行的是下面这个异常
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
如果你没有为spring配置异常处理,或者没有能配置相关异常处理的内容,spring就会把你这个异常交给tomcat处理,而且交给tomcat的方式是请求/error页面,不是很推荐这样做,不可控性瞬间就变大了,所以,spring为我们做了一个controller
BasicErrorController
spring会把/error交给它自己写的controller去处理,所以我们在swagger里面就会扫描到这个controller,又联系起来一个
ResponseStatus
可以直接标注在异常上面!这样我们就不需要在全局异常处理器中处理这个异常了。这个原理其实使用了一个和全局异常处理同一接口的,然后去遍历的,不过呢,这个在全局异常处理器之后,如果全局异常处理器无法处理的话,才轮的到它。而且除了这个异常解析器,spring还为自己会产生的异常准备了一个默认的异常解析器,如果还是解析不了的话,你就真的可以看到tomcat的错误处理页面了
HandlerExceptionResolver
自定义异常处理器
Servlet原生
快速开始
@ServletComponentScan指定包的位置,然后标注@WebXXX指定url路径,注意一下这个是在拦截器之前的,spring的安全框架用的就是这个原理,但是你多多少少会感觉到不方便,因为这个是servlet的原生注释。
ServletRegistrationBean,这个使用方法就是new的时候设置一些信息,注册到容器中,其实也不怎么方便,尤其是我想要多个servlet的时候。不过我们的mvc核心类DispatcherServlet就是通过这个方法被自动配置进来的
boot 原理
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return (new SpringApplication(primarySources)).run(args);
}
明显是先创建spring boot应用,然后依据args参数运行
创建
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));//这个就是你的主启动类
this.webApplicationType = WebApplicationType.deduceFromClasspath();//判断是不是webflux环境,默认都是返回一个普通web环境,然后你观察枚举类WebApplicationType有非web环境,我们不做考虑,不过利用spring的种种机制,设计一个软件也是很方便的
this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();//SpringFactoriesLoader.loadFactoryNames从spring.factory中寻找相关的配置
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();//主程序创建,这个是主动抛出异常利用函数的调用堆栈,这个从思想上看确定有点意想不到哈
}
运行
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();//开始记录时间
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);//获取监听器
listeners.starting(bootstrapContext, this.mainApplicationClass);//执行监听器事件
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);//处理参数
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);//准备环境
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();//这个是比较重要的,其他其实看方法名也行
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();//启动成功,统计时间
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);//监听器被调用函数
callRunners(context, applicationArguments);//runner
}
catch (Throwable ex) {
//启动时的异常处理器
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
容器创建
try {
switch (webApplicationType) {//很明显,普通web和reactweb的容器都不一样
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
}
然后就是适当的时候,调用我们初始化器,调用监听器。
这个就是生命周期函数的调用,至于我们怎么使用它,这里需要注意的一点就是需要在spring.factory配置他们,因为他们有的是在ioc容器创建之前就要执行的。
Bootstrapper,ApplicationRunner,CommandLineRunner,ApplicationContextInitializer,ApplicationListener,SpringApplicationRunListener