SpringBoot(1.5.6.RELEASE)源码解析(三)
请尊重作者劳动成果,转载请标明原文链接:http://www.cnblogs.com/dylan-java/p/7455699.html
上一篇分析了SpringApplication的初始化,接下来看一下它的run方法
1 publicConfigurableApplicationContext run(String... args) {2 StopWatch stopWatch = newStopWatch();3 stopWatch.start();4 ConfigurableApplicationContext context = null;5 FailureAnalyzers analyzers = null;6 configureHeadlessProperty();7 SpringApplicationRunListeners listeners =getRunListeners(args);8 listeners.starting();9 try{10 ApplicationArguments applicationArguments = newDefaultApplicationArguments(11 args);12 ConfigurableEnvironment environment =prepareEnvironment(listeners,13 applicationArguments);14 Banner printedBanner =printBanner(environment);15 context =createApplicationContext();16 analyzers = newFailureAnalyzers(context);17 prepareContext(context, environment, listeners, applicationArguments,18 printedBanner);19 refreshContext(context);20 afterRefresh(context, applicationArguments);21 listeners.finished(context, null);22 stopWatch.stop();23 if (this.logStartupInfo) {24 new StartupInfoLogger(this.mainApplicationClass)25 .logStarted(getApplicationLog(), stopWatch);26 }27 returncontext;28 }29 catch(Throwable ex) {30 handleRunFailure(context, listeners, analyzers, ex);31 throw newIllegalStateException(ex);32 }33 }
首先创建一个StopWatch对象并调用它的start方法,该类是Spring提供的一个计时器类,与本篇要讨论的东西无关,所在在这里不对它进行分析
第6行调用了configureHeadlessProperty方法,该方法只有一行代码,就是设置系统属性java.awt.headless,这里设置为true,表示运行在服务器端,在没有显示器和鼠标键盘的模式下工作,模拟输入输出设备功能
第7行通过getRunListeners方法获取SpringApplicationRunListeners对象,这个对象是一个SpringBoot事件广播器的管理者,它可以包含多个SpringApplicationRunListener,SpringApplication类中使用它们来间接调用ApplicationListener
在classpath下的JAR文件中包含的/META/spring.factories文件里找到org.springframework.boot.SpringApplicationRunListener对应的属性,然后实例化并排序
在jar:file:/C:/Users/guiqingqing/.m2/repository/org/springframework/boot/spring-boot/1.5.6.RELEASE/spring-boot-1.5.6.RELEASE.jar!/META-INF/spring.factories文件找到如下配置
# Run Listeners org.springframework.boot.SpringApplicationRunListener=\ org.springframework.boot.context.event.EventPublishingRunListener
这里只有一个,就是EventPublishingRunListener,然后作为参数调用SpringApplicationRunListeners的构造函数,创建对象并返回,EventPublishingRunListener对象的实例被添加到SpringApplicationRunListeners对象的listeners属性中
看一下EventPublishingRunListener的构造函数
1 publicEventPublishingRunListener(SpringApplication application, String[] args) {2 this.application =application;3 this.args =args;4 this.initialMulticaster = newSimpleApplicationEventMulticaster();5 for (ApplicationListener<?>listener : application.getListeners()) {6 this.initialMulticaster.addApplicationListener(listener);7 }8 }
内部会创建一个Spring广播器SimpleApplicationEventMulticaster对象,它实际上是一个事件广播器,EventPublishingRunListener实现了SpringApplicationRunListener接口,我们看一下SpringApplicationRunListener接口的定义
1 public interfaceSpringApplicationRunListener {2 voidstarting();3 4 voidenvironmentPrepared(ConfigurableEnvironment environment);5 6 voidcontextPrepared(ConfigurableApplicationContext context);7 8 voidcontextLoaded(ConfigurableApplicationContext context);9 10 voidfinished(ConfigurableApplicationContext context, Throwable exception);11 }
该接口规定了SpringBoot的生命周期,在各个生命周期广播相应的事件,调用实际的ApplicationListener的onApplicationEvent方法
回到SpringApplication的run方法
第8行调用listeners的starting方法
1 public voidstarting() {2 for (SpringApplicationRunListener listener : this.listeners) {3 listener.starting();4 }5 }
该方法遍历所有的listeners,当然这里只有一个(EventPublishingRunListener),调用它的starting方法
1 @Override2 @SuppressWarnings("deprecation")3 public voidstarting() {4 this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args));5 }
这里会使用广播器(SimpleApplicationEventMulticaster)去广播事件,创建一个ApplicationStartedEvent对象作为参数
1 @Override2 public voidmulticastEvent(ApplicationEvent event) {3 multicastEvent(event, resolveDefaultEventType(event));4 }5 6 @Override7 public void multicastEvent(finalApplicationEvent event, ResolvableType eventType) {8 ResolvableType type = (eventType != null ?eventType : resolveDefaultEventType(event));9 for (final ApplicationListener<?>listener : getApplicationListeners(event, type)) {10 Executor executor =getTaskExecutor();11 if (executor != null) {12 executor.execute(newRunnable() {13 @Override14 public voidrun() {15 invokeListener(listener, event);16 }17 });18 }19 else{20 invokeListener(listener, event);21 }22 }23 }
可以看出multicastEvent方法会找到ApplicationListener的集合,然后依次调用invokeListener方法,而invokeListener方法内部则会调用ApplicationListener对象的onApplicationEvent方法
第10行创建一个DefaultApplicationArguments对象,它持有着args参数,就是main函数传进来的参数
第12行调用prepareEnvironment方法,该方法准备运行的环境,比如开发环境dev,测试环境test,还是生产环境prd,然后根据环境解析不同的配置文件
1 privateConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {2 //Create and configure the environment 3 ConfigurableEnvironment environment =getOrCreateEnvironment();4 configureEnvironment(environment, applicationArguments.getSourceArgs());5 listeners.environmentPrepared(environment);6 if (!this.webEnvironment) {7 environment = newEnvironmentConverter(getClassLoader())8 .convertToStandardEnvironmentIfNecessary(environment);9 }10 returnenvironment;11 }
getOrCreateEnvironment方法会检查之前设置的webEnvironment属性,如果是web程序,那么创建一个StandardServletEnvironment对象并返回,如果不是web程序,那么创建一个StandardEnvironment对象并返回
接下来是configureEnvironment方法,该方法会对上一步返回的environment做进一步的配置,调用configurePropertySources和configureProfiles,比如main函数传进来参数"--spring.profiles.active=dev",那么会在configurePropertySources方法里被添加到CommandLineArgs对象的optionArgs属性,不是以"--"开头的参数会被添加到nonOptionArgs属性。而configureProfiles方法这个时候就会获取到dev作为当前有效的profile并添加到environment的activeProfiles属性中,本例不传参数,所以基本上configureEnvironment方法什么都不做
然后调用listeners的environmentPrepared方法,发布一个ApplicationEnvironmentPreparedEvent事件,通过事件广播器,依次调用每个ApplicationListener对象的onApplicationEvent方法,这里我们重点分析ConfigFileApplicationListener的onApplicationEvent方法
1 @Override2 public voidonApplicationEvent(ApplicationEvent event) {3 if (event instanceofApplicationEnvironmentPreparedEvent) {4 onApplicationEnvironmentPreparedEvent(5 (ApplicationEnvironmentPreparedEvent) event);6 }7 if (event instanceofApplicationPreparedEvent) {8 onApplicationPreparedEvent(event);9 }10 }
1 private voidonApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {2 List<EnvironmentPostProcessor> postProcessors =loadPostProcessors();3 postProcessors.add(this);4 AnnotationAwareOrderComparator.sort(postProcessors);5 for(EnvironmentPostProcessor postProcessor : postProcessors) {6 postProcessor.postProcessEnvironment(event.getEnvironment(),7 event.getSpringApplication());8 }9 }
首先loadPostProcessors方法会去classpath下的JAR文件中包含的/META/spring.factories文件里找到org.springframework.boot.env.EnvironmentPostProcessor对应的属性,然后实例化并排序,设置到postProcessors变量,在把当前对象(ConfigFileApplicationListener的实例)也添加到postProcessors变量中,然后对postProcessors排序,遍历postProcessors,依次调用它们的postProcessEnvironment方法。我们来看ConfigFileApplicationListener的postProcessEnvironment方法
1 @Override2 public voidpostProcessEnvironment(ConfigurableEnvironment environment,3 SpringApplication application) {4 addPropertySources(environment, application.getResourceLoader());5 configureIgnoreBeanInfo(environment);6 bindToSpringApplication(environment, application);7 }
addPropertySources方法最终会调用到ConfigFileApplicationListener的内部类Loader的load方法,这个方法是解析我们配置的application.yml和application-dev.yml文件的实现所在
1 public voidload() {2 this.propertiesLoader = newPropertySourcesLoader();3 this.activatedProfiles = false;4 this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());5 this.processedProfiles = new LinkedList<Profile>();6 7 //Pre-existing active profiles set via Environment.setActiveProfiles()8 //are additional profiles and config files are allowed to add more if9 //they want to, so don't call addActiveProfiles() here. 10 Set<Profile> initialActiveProfiles =initializeActiveProfiles();11 this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));12 if (this.profiles.isEmpty()) {13 for (String defaultProfileName : this.environment.getDefaultProfiles()) {14 Profile defaultProfile = new Profile(defaultProfileName, true);15 if (!this.profiles.contains(defaultProfile)) {16 this.profiles.add(defaultProfile);17 }18 }19 }20 21 //The default profile for these purposes is represented as null. We add it22 //last so that it is first out of the queue (active profiles will then23 //override any settings in the defaults when the list is reversed later). 24 this.profiles.add(null);25 26 while (!this.profiles.isEmpty()) {27 Profile profile = this.profiles.poll();28 for(String location : getSearchLocations()) {29 if (!location.endsWith("/")) {30 //location is a filename already, so don't search for more31 //filenames 32 load(location, null, profile);33 }34 else{35 for(String name : getSearchNames()) {36 load(location, name, profile);37 }38 }39 }40 this.processedProfiles.add(profile);41 }42 43 addConfigurationProperties(this.propertiesLoader.getPropertySources());44 }
25行之前基本上是一些变量的实例化,会往profiles集合中添加default和null,直接跳过,接下来从profiles集合中取出对应的值进行解析,首先取出null
第28行getSearchLocations方法,首先会检查当前环境是否配置了spring.config.location,如果没有,那么会使用默认的搜索路径classpath:/,classpath:/config/,file:./,file:./config/,这里调用了Collections.reverse方法进行倒序,也就是先查找file:./config/,在查找file:./,再是classpath:/config/,最后查找classpath:/。笔者的application.yml和application-dev.yml都是直接在src/main/resources目录下,所以对应的是在location为classpath:/时调用load(String location, String name, Profile profile)方法(在没有配置spring.config.name的情况下,name默认为application)
1 private voidload(String location, String name, Profile profile) {2 String group = "profile=" + (profile == null ? "": profile);3 if (!StringUtils.hasText(name)) {4 //Try to load directly from the location 5 loadIntoGroup(group, location, profile);6 }7 else{8 //Search for a file with the given name 9 for (String ext : this.propertiesLoader.getAllFileExtensions()) {10 if (profile != null) {11 //Try the profile-specific file 12 loadIntoGroup(group, location + name + "-" + profile + "." +ext,13 null);14 for (Profile processedProfile : this.processedProfiles) {15 if (processedProfile != null) {16 loadIntoGroup(group, location + name + "-" 17 + processedProfile + "." +ext, profile);18 }19 }20 //Sometimes people put "spring.profiles: dev" in21 //application-dev.yml (gh-340). Arguably we should try and error22 //out on that, but we can be kind and load it anyway. 23 loadIntoGroup(group, location + name + "-" + profile + "." +ext,24 profile);25 }26 //Also try the profile-specific section (if any) of the normal file 27 loadIntoGroup(group, location + name + "." +ext, profile);28 }29 }30 }
调用this.propertiesLoader.getAllFileExtensions()方法获取配置文件后缀名,分别是在PropertiesPropertySourceLoader类和YamlPropertySourceLoader类中getFileExtensions方法定义的
1 public class PropertiesPropertySourceLoader implementsPropertySourceLoader {2 @Override3 publicString[] getFileExtensions() {4 return new String[] { "properties", "xml"};5 }6 }
1 public class YamlPropertySourceLoader implementsPropertySourceLoader {2 @Override3 publicString[] getFileExtensions() {4 return new String[] { "yml", "yaml"};5 }6 }
第23行调用的loadIntoGroup方法里又调用doLoadIntoGroup方法,这个方法会检查配置文件是否存在,比如file:./config/下不存在,或是classpath:/下的application.properties不存在,最后发现classpath:/下的application.yml存在,然后对其进行解析
1 public PropertySource<?>load(Resource resource, String group, String name,2 String profile) throwsIOException {3 if(isFile(resource)) {4 String sourceName =generatePropertySourceName(name, profile);5 for (PropertySourceLoader loader : this.loaders) {6 if(canLoadFileExtension(loader, resource)) {7 PropertySource<?> specific =loader.load(sourceName, resource,8 profile);9 addPropertySource(group, specific, profile);10 returnspecific;11 }12 }13 }14 return null;15 }
这里有2个loader,PropertiesPropertySourceLoader和YamlPropertySourceLoader,先检查是否该由此loader去解析,检查的规则就是看文件后缀名是否在getFileExtensions方法返回的数组中存在,很显然,这里应该由YamlPropertySourceLoader去解析,笔者的application.yml很简单,内容如下
spring:profiles:active: dev
解析完成之后,调用了一个handleProfileProperties方法,这个方法里又会调用maybeActivateProfiles方法,此方法的addProfiles方法会把解析到的dev添加到profiles中去,removeUnprocessedDefaultProfiles方法会删除之前添加的default(此时只剩下dev),如果配置了include,也会被解析
然后再次遍历file:./config/,file:./,classpath:/config/,classpath:/,这次是查找到application-dev.yml并解析,过程就不重复分析了
回到SpringApplication的run方法
第14行调用printBanner方法,打印SpringBoot的LOGO
1 privateBanner printBanner(ConfigurableEnvironment environment) {2 if (this.bannerMode ==Banner.Mode.OFF) {3 return null;4 }5 ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader6 : newDefaultResourceLoader(getClassLoader());7 SpringApplicationBannerPrinter bannerPrinter = newSpringApplicationBannerPrinter(8 resourceLoader, this.banner);9 if (this.bannerMode ==Mode.LOG) {10 return bannerPrinter.print(environment, this.mainApplicationClass, logger);11 }12 return bannerPrinter.print(environment, this.mainApplicationClass, System.out);13 }
方法内部会创建一个SpringApplicationBannerPrinter的对象,并调用print方法
1 public Banner print(Environment environment, Class<?>sourceClass, PrintStream out) {2 Banner banner = getBanner(environment, this.fallbackBanner);3 banner.printBanner(environment, sourceClass, out);4 return newPrintedBanner(banner, sourceClass);5 }
通过getBanner方法获取Banner对象
1 privateBanner getBanner(Environment environment, Banner definedBanner) {2 Banners banners = newBanners();3 banners.addIfNotNull(getImageBanner(environment));4 banners.addIfNotNull(getTextBanner(environment));5 if(banners.hasAtLeastOneBanner()) {6 returnbanners;7 }8 if (this.fallbackBanner != null) {9 return this.fallbackBanner;10 }11 returnDEFAULT_BANNER;12 }
第一步,调用Banners的无参构造创建一个Banners的对象,然后我们先看一下SpringApplicationBannerPrinter类里定义的几个静态属性
1 static final String BANNER_LOCATION_PROPERTY = "banner.location";2 3 static final String BANNER_IMAGE_LOCATION_PROPERTY = "banner.image.location";4 5 static final String DEFAULT_BANNER_LOCATION = "banner.txt";6 7 static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };
先看是否配置了系统属性banner.image.location,没有配置则在classpath中查找banner.gif,banner.jpg,banner.png,如果找到,则创建一个ImageBanner对象并添加到Banners对象的banners属性中,该属性是一个List类型,接下来看是否配置了banner.location属性,如果没有,则用默认的banner.txt,然后在classpath中查找banner.txt,如果找到,则创建一个ResourceBanner对象并添加到Banners对象的banners属性中,最后,如果Banners对象的banners不为空,也就是至少找到了banner.gif,banner.jpg,banner.png,banner.txt其中的一个,那么返回该Banners对象,否则返回默认的SpringBootBanner对象
接下来根据返回的不同Banners类型的对象调用具体的printBanner实现方法,所以如果要想打印自定义的LOGO,要么你在classpath下添加图片,要么添加banner.txt的文本文件,默认的SpringBootBanner会打印如下图
回到SpringApplication的run方法
第15行调用createApplicationContext方法,该方法创建SpringBoot的上下文
1 protectedConfigurableApplicationContext createApplicationContext() {2 Class<?> contextClass = this.applicationContextClass;3 if (contextClass == null) {4 try{5 contextClass = Class.forName(this.webEnvironment6 ?DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);7 }8 catch(ClassNotFoundException ex) {9 throw newIllegalStateException(10 "Unable create a default ApplicationContext, " 11 + "please specify an ApplicationContextClass",12 ex);13 }14 }15 return(ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);16 }
通过判断当前是否是web环境决定创建什么类,如果是web程序,那么创建org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext的实例,否则创建org.springframework.context.annotation.AnnotationConfigApplicationContext的实例,这些都是定义在SpringApplication类的静态属性中
第16行创建FailureAnalyzers的对象,FailureAnalyzers的构造函数里调用了loadFailureAnalyzers方法,还是老规矩,在classpath下的JAR文件中包含的/META/spring.factories文件中找到org.springframework.boot.diagnostics.FailureAnalyzer对应的属性,实例化并排序,赋值给FailureAnalyzers对象的analyzers属性,主要是用来处理启动时发生一些异常时的一些分析
第17行调用prepareContext方法,准备上下文
1 private voidprepareContext(ConfigurableApplicationContext context,2 ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,3 ApplicationArguments applicationArguments, Banner printedBanner) {4 context.setEnvironment(environment);5 postProcessApplicationContext(context);6 applyInitializers(context);7 listeners.contextPrepared(context);8 if (this.logStartupInfo) {9 logStartupInfo(context.getParent() == null);10 logStartupProfileInfo(context);11 }12 13 //Add boot specific singleton beans 14 context.getBeanFactory().registerSingleton("springApplicationArguments",15 applicationArguments);16 if (printedBanner != null) {17 context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);18 }19 20 //Load the sources 21 Set<Object> sources =getSources();22 Assert.notEmpty(sources, "Sources must not be empty");23 load(context, sources.toArray(newObject[sources.size()]));24 listeners.contextLoaded(context);25 }
先给上下文设置环境,然后调用postProcessApplicationContext方法设置上下文的beanNameGenerator和resourceLoader(如果SpringApplication有的话)
接下来调用applyInitializers方法,拿到之前实例化SpringApplication对象的时候设置的ApplicationContextInitializer,调用它们的initialize方法,对上下文做初始化
介绍几个ApplicationContextInitializer,其他的不过多介绍,有兴趣的可以自己DEBUG
DelegatingApplicationContextInitializer: 从环境中(配置的context.initializer.classes)取出所有的ApplicationContextInitializer并执行
ContextIdApplicationContextInitializer: 设置上下文的id(name + ":" + profiles + ":" + index),笔者调试的id为(application:dev:8080)
下面一行调用listeners的contextPrepared方法,跟之前调用listeners的starting方法一样,只是EventPublishingRunListener的contextPrepared方法是个空实现
然后打印启动日志
2017-09-02 22:08:07.196 [INFO ][main]:com.dylan.java.springboot.template.Application[logStarting:48] - Starting Application on Dylan-PC with PID 5764 (F:\Dylan\workspace\template\target\classes started by Dylan in F:\Dylan\workspace\template) 2017-09-02 22:08:12.388 [INFO ][main]:com.dylan.java.springboot.template.Application[logStartupProfileInfo:597] - The following profiles are active: dev
打印完日志往上下文的beanFactory中注册一个singleton的bean,bean的名字是springApplicationArguments,bean的实例是之前实例化的ApplicationArguments对象
如果之前获取的printedBanner不为空,那么往上下文的beanFactory中注册一个singleton的bean,bean的名字是springBootBanner,bean的实例就是这个printedBanner
prepareContext方法的倒数第2行调用load方法注册启动类的bean定义,也就是调用SpringApplication.run(Application.class, args);的类,SpringApplication的load方法内会创建BeanDefinitionLoader的对象,并调用它的load()方法
1 public intload() {2 int count = 0;3 for (Object source : this.sources) {4 count +=load(source);5 }6 returncount;7 }
对所有的source(这里只有一个: class com.dylan.java.springboot.template.Application)都执行一次load(Object source)方法,这个方法又会调用load(Class<?> source)方法
1 private int load(Class<?>source) {2 if(isGroovyPresent()) {3 //Any GroovyLoaders added in beans{} DSL can contribute beans here 4 if (GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {5 GroovyBeanDefinitionSource loader =BeanUtils.instantiateClass(source,6 GroovyBeanDefinitionSource.class);7 load(loader);8 }9 }10 if(isComponent(source)) {11 this.annotatedReader.register(source);12 return 1;13 }14 return 0;15 }
由于@SpringBootApplication注解继承了@SpringBootConfiguration注解,@SpringBootConfiguration注解继承了@Configuration注解,@Configuration注解又继承了@Component注解,所以上面代码的第10行返回true,于是执行第11行和第12行代码
1 classBeanDefinitionLoader {2 private finalObject[] sources;3 4 private finalAnnotatedBeanDefinitionReader annotatedReader;5 6 private finalXmlBeanDefinitionReader xmlReader;7 8 privateBeanDefinitionReader groovyReader;9 10 ...11 }
可以看出BeanDefinitionLoader中有多个加载BeanDefinition的Reader类,这里针对@SpringBootApplication注解使用了annotatedReader,调用register方法,因为启动类没有@Conditional注解,所以不能跳过注册的步骤
那么就老老实实的注册该BeanDefinition吧,没有设置bean是singleton还是prototype,那么默认使用singleton,而bean的名字,则默认是把类名的首字母变小写,也就是application,然后检查是否有@Lazy、@Primary、@DependsOn注解并设置AnnotatedBeanDefinition的属性,如果是AbstractBeanDefinition,还要检查是否有@Role、@Description注解并设置其属性,最后通过BeanDefinitionReaderUtils类的registerBeanDefinition方法注册BeanDefinition
最后调用listeners的contextLoaded方法,说明上下文已经加载,该方法先找到所有的ApplicationListener,遍历这些listener,如果该listener继承了ApplicationContextAware类,那么在这一步会调用它的setApplicationContext方法,设置context
遍历完ApplicationListener之后,创建ApplicationPreparedEvent事件对象,并广播出去,也就是调用所有ApplicationListener的onApplicationEvent方法
最后再回到SpringApplication的run方法,第19行调用refreshContext方法,该方法内容较多,我们放下一篇再分析
转载于:https://www.cnblogs.com/dylan-java/p/7455699.html
SpringBoot(1.5.6.RELEASE)源码解析(三)相关推荐
- SpringBoot(1.5.6.RELEASE)源码解析(一)
启动SpringBoot,需要在入口函数所在的类上添加@SpringBootApplication注解 1 @SpringBootApplication 2 public class Applicat ...
- Disruptor源码解析三 RingBuffer解析
目录 系列索引 前言 主要内容 RingBuffer的要点 源码解析 系列索引 Disruptor源码解析一 Disruptor高性能之道 Disruptor源码解析二 Sequence相关类解析 D ...
- OkHttp3源码解析(三)——连接池复用
OKHttp3源码解析系列 OkHttp3源码解析(一)之请求流程 OkHttp3源码解析(二)--拦截器链和缓存策略 本文基于OkHttp3的3.11.0版本 implementation 'com ...
- 并发编程与源码解析 (三)
并发编程 (三) 1 Fork/Join分解合并框架 1.1 什么是fork/join Fork/Join框架是JDK1.7提供的一个用于并行执行任务的框架,开发者可以在不去了解如Thread.R ...
- ReactiveSwift源码解析(三) Signal代码的基本实现
上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...
- 前端入门之(vuex源码解析三)
上两节前端入门之(vuex源码解析二)我们把vuex的源码大概的撸了一遍,还剩下(插件.getters跟module),我们继续哈~ 插件童鞋们可以去看看vuex在各个浏览器的状态显示插件,小伙伴可以 ...
- 拆轮子-RxDownload2源码解析(三)
本文为博主原创文章,未经允许不得转载 造轮子者:Season_zlc 轮子用法请戳作者链接 ↑ 前言 本文主要讲述 RxDownload2 的多线程断点下载技术. 断点下载技术前提 服务器必须支持按 ...
- 深度理解springboot集成cache缓存之源码解析
一.案例准备 1.创建数据表(employee表) 2.创建Employee实体类封装数据库中的数据 @AllArgsConstructor @NoArgsConstructor @Data @ToS ...
- Tomcat源码解析三:tomcat的启动过程
Tomcat组件生命周期管理 在Tomcat总体结构 (Tomcat源代码解析之二)中,我们列出了Tomcat中Server,Service,Connector,Engine,Host,Context ...
最新文章
- Google、Facebook、亚马逊、Uber等硅谷顶尖AI专家团北京聚首 ,这场AI开发者盛会不可错过
- unet_3plus
- java后端 返回json_Java后端返回Json数据
- 用Excel制作改进前后漏斗模型图比较
- hdu3594 强连通 tarjan
- Netflix Curator 使用 Zookeeper 编程
- python小结_python简单小结
- 12v小型电机型号大全_伊藤8KW静音柴油发电机YT8100T型号规格
- Ruby编程语言学习笔记4
- 机器学习之几个好用的数据下载网站
- spring boot配置虚拟路径(替代docBase配置)访问本地图片
- SDOI2017R1(姑且是游记)
- 【MaxScript】删除所选择的骨骼的位置和缩放
- Python爬虫学习框架介绍
- useful websites for constructing your own website
- 南邮转专业计算机科学与技术,南京邮电大学本科生转专业管理办法(2020年10月9日修订)...
- 多图预警~ 华为 ECS 与 阿里云 ECS 对比实战
- WIN10彻底禁用UAC(用户账户控制)
- Unity Shader入门精要学习笔记 - 第11章 让画面动起来
- 解决SublimeText中文注释乱码
热门文章
- flask实现后台java实现前端页面_java实现telnet功能,待实现windows下远程多机自动化发布软件后台代码...
- linux版本信息i686,Linux下如何查看版本信息
- 投影仪幕布增益_当贝投影带你看投影仪使用幕布和白墙的区别实测!
- 小程序与UC浏览器打通
- Buffer内存分配
- 关于php正则表达式得选择题,经典PHP笔试题
- vfp 调用接口取数据_2分钟教你调用全国天气预报数据接口
- oracle数据库静态启动,ORACLE数据库的连接
- VB提取字符串中的日期
- 消耗性缺口_衰竭缺口分析