四、另一件武器:Spring容器的事件监听机制

过去,事件监听机制多用于图形界面编程,比如:点击按钮、在文本框输入内容等操作被称为事件,而当事件触发时,应用程序作出一定的响应则表示应用监听了这个事件,而在服务器端,事件的监听机制更多的用于异步通知以及监控和异常处理。Java提供了实现事件监听机制的两个基础类:自定义事件类型扩展自java.util.EventObject、事件的监听器扩展自java.util.EventListener。来看一个简单的实例:简单的监控一个方法的耗时。

首先定义事件类型,通常的做法是扩展EventObject,随着事件的发生,相应的状态通常都封装在此类中:

public class MethodMonitorEvent extends EventObject {// 时间戳,用于记录方法开始执行的时间public long timestamp;public MethodMonitorEvent(Object source) {super(source);}
}

事件发布之后,相应的监听器即可对该类型的事件进行处理,我们可以在方法开始执行之前发布一个begin事件,在方法执行结束之后发布一个end事件,相应地,事件监听器需要提供方法对这两种情况下接收到的事件进行处理:

// 1、定义事件监听接口
public interface MethodMonitorEventListener extends EventListener {// 处理方法执行之前发布的事件public void onMethodBegin(MethodMonitorEvent event);// 处理方法结束时发布的事件public void onMethodEnd(MethodMonitorEvent event);
}
// 2、事件监听接口的实现:如何处理
public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener {@Overridepublic void onMethodBegin(MethodMonitorEvent event) {// 记录方法开始执行时的时间event.timestamp = System.currentTimeMillis();}@Overridepublic void onMethodEnd(MethodMonitorEvent event) {// 计算方法耗时long duration = System.currentTimeMillis() - event.timestamp;System.out.println("耗时:" + duration);}
}

事件监听器接口针对不同的事件发布实际提供相应的处理方法定义,最重要的是,其方法只接收MethodMonitorEvent参数,说明这个监听器类只负责监听器对应的事件并进行处理。有了事件和监听器,剩下的就是发布事件,然后让相应的监听器监听并处理。通常情况,我们会有一个事件发布者,它本身作为事件源,在合适的时机,将相应的事件发布给对应的事件监听器:

public class MethodMonitorEventPublisher {private List<MethodMonitorEventListener> listeners = new ArrayList<MethodMonitorEventListener>();public void methodMonitor() {MethodMonitorEvent eventObject = new MethodMonitorEvent(this);publishEvent("begin",eventObject);// 模拟方法执行:休眠5秒钟TimeUnit.SECONDS.sleep(5);publishEvent("end",eventObject);}private void publishEvent(String status,MethodMonitorEvent event) {// 避免在事件处理期间,监听器被移除,这里为了安全做一个复制操作List<MethodMonitorEventListener> copyListeners = ➥ new ArrayList<MethodMonitorEventListener>(listeners);for (MethodMonitorEventListener listener : copyListeners) {if ("begin".equals(status)) {listener.onMethodBegin(event);} else {listener.onMethodEnd(event);}}}public static void main(String[] args) {MethodMonitorEventPublisher publisher = new MethodMonitorEventPublisher();publisher.addEventListener(new AbstractMethodMonitorEventListener());publisher.methodMonitor();}// 省略实现public void addEventListener(MethodMonitorEventListener listener) {}public void removeEventListener(MethodMonitorEventListener listener) {}public void removeAllListeners() {}

对于事件发布者(事件源)通常需要关注两点:

  1. 在合适的时机发布事件。此例中的methodMonitor()方法是事件发布的源头,其在方法执行之前和结束之后两个时间点发布MethodMonitorEvent事件,每个时间点发布的事件都会传给相应的监听器进行处理。在具体实现时需要注意的是,事件发布是顺序执行,为了不影响处理性能,事件监听器的处理逻辑应尽量简单。

  2. 事件监听器的管理。publisher类中提供了事件监听器的注册与移除方法,这样客户端可以根据实际情况决定是否需要注册新的监听器或者移除某个监听器。如果这里没有提供remove方法,那么注册的监听器示例将一直被MethodMonitorEventPublisher引用,即使已经废弃不用了,也依然在发布者的监听器列表中,这会导致隐性的内存泄漏。

Spring容器内的事件监听机制

Spring的ApplicationContext容器内部中的所有事件类型均继承自org.springframework.context.AppliationEvent,容器中的所有监听器都实现org.springframework.context.ApplicationListener接口,并且以bean的形式注册在容器中。一旦在容器内发布ApplicationEvent及其子类型的事件,注册到容器的ApplicationListener就会对这些事件进行处理。

你应该已经猜到是怎么回事了。

ApplicationEvent继承自EventObject,Spring提供了一些默认的实现,比如:ContextClosedEvent表示容器在即将关闭时发布的事件类型,ContextRefreshedEvent表示容器在初始化或者刷新的时候发布的事件类型……

容器内部使用ApplicationListener作为事件监听器接口定义,它继承自EventListener。ApplicationContext容器在启动时,会自动识别并加载EventListener类型的bean,一旦容器内有事件发布,将通知这些注册到容器的EventListener。

ApplicationContext接口继承了ApplicationEventPublisher接口,该接口提供了void publishEvent(ApplicationEvent event)方法定义,不难看出,ApplicationContext容器担当的就是事件发布者的角色。如果有兴趣可以查看AbstractApplicationContext.publishEvent(ApplicationEvent event)方法的源码:ApplicationContext将事件的发布以及监听器的管理工作委托给ApplicationEventMulticaster接口的实现类。在容器启动时,会检查容器内是否存在名为applicationEventMulticaster的ApplicationEventMulticaster对象实例。如果有就使用其提供的实现,没有就默认初始化一个SimpleApplicationEventMulticaster作为实现。

最后,如果我们业务需要在容器内部发布事件,只需要为其注入ApplicationEventPublisher依赖即可:实现ApplicationEventPublisherAware接口或者ApplicationContextAware接口(Aware接口相关内容请回顾上文)。

五、出神入化:揭秘自动配置原理

典型的Spring Boot应用的启动类一般均位于src/main/java根路径下,比如MoonApplication类:

@SpringBootApplication
public class MoonApplication {public static void main(String[] args) {SpringApplication.run(MoonApplication.class, args);}
}

其中@SpringBootApplication开启组件扫描和自动配置,而SpringApplication.run则负责启动引导应用程序。@SpringBootApplication是一个复合Annotation,它将三个有用的注解组合在一起:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {// ......
}

@SpringBootConfiguration就是@Configuration,它是Spring框架的注解,标明该类是一个JavaConfig配置类。而@ComponentScan启用组件扫描,前文已经详细讲解过,这里着重关注@EnableAutoConfiguration

@EnableAutoConfiguration注解表示开启Spring Boot自动配置功能,Spring Boot会根据应用的依赖、自定义的bean、classpath下有没有某个类 等等因素来猜测你需要的bean,然后注册到IOC容器中。那@EnableAutoConfiguration是如何推算出你的需求?首先看下它的定义:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {// ......
}

你的关注点应该在@Import(EnableAutoConfigurationImportSelector.class)上了,前文说过,@Import注解用于导入类,并将这个类作为一个bean的定义注册到容器中,这里它将把EnableAutoConfigurationImportSelector作为bean注入到容器中,而这个类会将所有符合条件的@Configuration配置都加载到容器中,看看它的代码:

public String[] selectImports(AnnotationMetadata annotationMetadata) {// 省略了大部分代码,保留一句核心代码// 注意:SpringBoot最近版本中,这句代码被封装在一个单独的方法中// SpringFactoriesLoader相关知识请参考前文List<String> factories = new ArrayList<String>(new LinkedHashSet<String>(  SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader)));
}

这个类会扫描所有的jar包,将所有符合条件的@Configuration配置类注入的容器中,何为符合条件,看看META-INF/spring.factories的文件内容:

// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories
// 配置的key = EnableAutoConfiguration,与代码中一致
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\
.....

DataSourceAutoConfiguration为例,看看Spring Boot是如何自动配置的:

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration {
}

分别说一说:

  • @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }):当Classpath中存在DataSource或者EmbeddedDatabaseType类时才启用这个配置,否则这个配置将被忽略。

  • @EnableConfigurationProperties(DataSourceProperties.class):将DataSource的默认配置类注入到IOC容器中,DataSourceproperties定义为:

// 提供对datasource配置信息的支持,所有的配置前缀为:spring.datasource
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties  {private ClassLoader classLoader;private Environment environment;private String name = "testdb";......
}
  • @Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class }):导入其他额外的配置,就以DataSourcePoolMetadataProvidersConfiguration为例吧。

@Configuration
public class DataSourcePoolMetadataProvidersConfiguration {@Configuration@ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)static class TomcatDataSourcePoolMetadataProviderConfiguration {@Beanpublic DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() {.....}}......
}

DataSourcePoolMetadataProvidersConfiguration是数据库连接池提供者的一个配置类,即Classpath中存在org.apache.tomcat.jdbc.pool.DataSource.class,则使用tomcat-jdbc连接池,如果Classpath中存在HikariDataSource.class则使用Hikari连接池。

这里仅描述了DataSourceAutoConfiguration的冰山一角,但足以说明Spring Boot如何利用条件话配置来实现自动配置的。回顾一下,@EnableAutoConfiguration中导入了EnableAutoConfigurationImportSelector类,而这个类的selectImports()通过SpringFactoriesLoader得到了大量的配置类,而每一个配置类则根据条件化配置来做出决策,以实现自动配置。

整个流程很清晰,但漏了一个大问题:EnableAutoConfigurationImportSelector.selectImports()是何时执行的?其实这个方法会在容器启动过程中执行:AbstractApplicationContext.refresh(),更多的细节在下一小节中说明。

六、启动引导:Spring Boot应用启动的秘密

6.1 SpringApplication初始化

SpringBoot整个启动流程分为两个步骤:初始化一个SpringApplication对象、执行该对象的run方法。看下SpringApplication的初始化流程,SpringApplication的构造方法中调用initialize(Object[] sources)方法,其代码如下:

private void initialize(Object[] sources) {if (sources != null && sources.length > 0) {this.sources.addAll(Arrays.asList(sources));}// 判断是否是Web项目this.webEnvironment = deduceWebEnvironment();setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 找到入口类this.mainApplicationClass = deduceMainApplicationClass();
}

初始化流程中最重要的就是通过SpringFactoriesLoader找到spring.factories文件中配置的ApplicationContextInitializerApplicationListener两个接口的实现类名称,以便后期构造相应的实例。ApplicationContextInitializer的主要目的是在ConfigurableApplicationContext做refresh之前,对ConfigurableApplicationContext实例做进一步的设置或处理。ConfigurableApplicationContext继承自ApplicationContext,其主要提供了对ApplicationContext进行设置的能力。

实现一个ApplicationContextInitializer非常简单,因为它只有一个方法,但大多数情况下我们没有必要自定义一个ApplicationContextInitializer,即便是Spring Boot框架,它默认也只是注册了两个实现,毕竟Spring的容器已经非常成熟和稳定,你没有必要来改变它。

ApplicationListener的目的就没什么好说的了,它是Spring框架对Java事件监听机制的一种框架实现,具体内容在前文Spring事件监听机制这个小节有详细讲解。这里主要说说,如果你想为Spring Boot应用添加监听器,该如何实现?

Spring Boot提供两种方式来添加自定义监听器:

  • 通过SpringApplication.addListeners(ApplicationListener… listeners)或者SpringApplication.setListeners(Collection&gt; listeners)两个方法来添加一个或者多个自定义监听器

  • 既然SpringApplication的初始化流程中已经从spring.factories中获取到ApplicationListener的实现类,那么我们直接在自己的jar包的META-INF/spring.factories文件中新增配置即可:

org.springframework.context.ApplicationListener=\
cn.moondev.listeners.xxxxListener\

关于SpringApplication的初始化,我们就说这么多。

6.2 Spring Boot启动流程

Spring Boot应用的整个启动流程都封装在SpringApplication.run方法中,其整个流程真的是太长太长了,但本质上就是在Spring容器启动的基础上做了大量的扩展,按照这个思路来看看源码:

public ConfigurableApplicationContext run(String... args) {StopWatch stopWatch = new StopWatch();stopWatch.start();ConfigurableApplicationContext context = null;FailureAnalyzers analyzers = null;configureHeadlessProperty();// ①SpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting();try {// ②ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);// ③Banner printedBanner = printBanner(environment);// ④context = createApplicationContext();// ⑤analyzers = new FailureAnalyzers(context);// ⑥prepareContext(context, environment, listeners, applicationArguments,printedBanner);// ⑦ refreshContext(context);// ⑧afterRefresh(context, applicationArguments);// ⑨listeners.finished(context, null);stopWatch.stop();return context;}catch (Throwable ex) {handleRunFailure(context, listeners, analyzers, ex);throw new IllegalStateException(ex);}}

① 通过SpringFactoriesLoader查找并加载所有的SpringApplicationRunListeners,通过调用starting()方法通知所有的SpringApplicationRunListeners:应用开始启动了。SpringApplicationRunListeners其本质上就是一个事件发布者,它在SpringBoot应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。还记得初始化流程中,SpringApplication加载了一系列ApplicationListener吗?这个启动流程中没有发现有发布事件的代码,其实都已经在SpringApplicationRunListeners这儿实现了。

简单的分析一下其实现流程,首先看下SpringApplicationRunListener的源码:

public interface SpringApplicationRunListener {// 运行run方法时立即调用此方法,可以用户非常早期的初始化工作void starting();// Environment准备好后,并且ApplicationContext创建之前调用void environmentPrepared(ConfigurableEnvironment environment);// ApplicationContext创建好后立即调用void contextPrepared(ConfigurableApplicationContext context);// ApplicationContext加载完成,在refresh之前调用void contextLoaded(ConfigurableApplicationContext context);// 当run方法结束之前调用void finished(ConfigurableApplicationContext context, Throwable exception);}

SpringApplicationRunListener只有一个实现类:EventPublishingRunListener。①处的代码只会获取到一个EventPublishingRunListener的实例,我们来看看starting()方法的内容:

public void starting() {// 发布一个ApplicationStartedEventthis.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args));
}

顺着这个逻辑,你可以在②处的prepareEnvironment()方法的源码中找到listeners.environmentPrepared(environment);即SpringApplicationRunListener接口的第二个方法,那不出你所料,environmentPrepared()又发布了另外一个事件ApplicationEnvironmentPreparedEvent。接下来会发生什么,就不用我多说了吧。

② 创建并配置当前应用将要使用的Environment,Environment用于描述应用程序当前的运行环境,其抽象了两个方面的内容:配置文件(profile)和属性(properties),开发经验丰富的同学对这两个东西一定不会陌生:不同的环境(eg:生产环境、预发布环境)可以使用不同的配置文件,而属性则可以从配置文件、环境变量、命令行参数等来源获取。因此,当Environment准备好后,在整个应用的任何时候,都可以从Environment中获取资源。

总结起来,②处的两句代码,主要完成以下几件事:

  • 判断Environment是否存在,不存在就创建(如果是web项目就创建StandardServletEnvironment,否则创建StandardEnvironment

  • 配置Environment:配置profile以及properties

  • 调用SpringApplicationRunListener的environmentPrepared()方法,通知事件监听者:应用的Environment已经准备好

③、SpringBoot应用在启动时会输出这样的东西:

  .   ____          _            __ _ _/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\\/  ___)| |_)| | | | | || (_| |  ) ) ) )'  |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/:: Spring Boot ::        (v1.5.6.RELEASE)

如果想把这个东西改成自己的涂鸦,你可以研究以下Banner的实现,这个任务就留给你们吧。

④、根据是否是web项目,来创建不同的ApplicationContext容器。

⑤、创建一系列FailureAnalyzer,创建流程依然是通过SpringFactoriesLoader获取到所有实现FailureAnalyzer接口的class,然后在创建对应的实例。FailureAnalyzer用于分析故障并提供相关诊断信息。

⑥、初始化ApplicationContext,主要完成以下工作:

  • 将准备好的Environment设置给ApplicationContext

  • 遍历调用所有的ApplicationContextInitializer的initialize()方法来对已经创建好的ApplicationContext进行进一步的处理

  • 调用SpringApplicationRunListener的contextPrepared()方法,通知所有的监听者:ApplicationContext已经准备完毕

  • 将所有的bean加载到容器中

  • 调用SpringApplicationRunListener的contextLoaded()方法,通知所有的监听者:ApplicationContext已经装载完毕

⑦、调用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序。从名字上理解为刷新容器,那何为刷新?就是插手容器的启动,联系一下第一小节的内容。那如何刷新呢?且看下面代码:

// 摘自refresh()方法中一句代码
invokeBeanFactoryPostProcessors(beanFactory);

看看这个方法的实现:

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());......
}

获取到所有的BeanFactoryPostProcessor来对容器做一些额外的操作。BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做一些额外的操作。这里的getBeanFactoryPostProcessors()方法可以获取到3个Processor:

ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor
SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor
ConfigFileApplicationListener$PropertySourceOrderingPostProcessor

不是有那么多BeanFactoryPostProcessor的实现类,为什么这儿只有这3个?因为在初始化流程获取到的各种ApplicationContextInitializer和ApplicationListener中,只有上文3个做了类似于如下操作:

public void initialize(ConfigurableApplicationContext context) {context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));
}

然后你就可以进入到PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,这个方法除了会遍历上面的3个BeanFactoryPostProcessor处理外,还会获取类型为BeanDefinitionRegistryPostProcessor的bean:org.springframework.context.annotation.internalConfigurationAnnotationProcessor,对应的Class为ConfigurationClassPostProcessorConfigurationClassPostProcessor用于解析处理各种注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。当处理@import注解的时候,就会调用<自动配置>这一小节中的EnableAutoConfigurationImportSelector.selectImports()来完成自动配置功能。其他的这里不再多讲,如果你有兴趣,可以查阅参考资料6。

⑧、查找当前context中是否注册有CommandLineRunner和ApplicationRunner,如果有则遍历执行它们。

⑨、执行所有SpringApplicationRunListener的finished()方法。

这就是Spring Boot的整个启动流程,其核心就是在Spring容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener以及各种BeanFactoryPostProcessor等等。你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。

整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。言而总之,Spring才是核心,理解清楚Spring容器的启动流程,那Spring Boot启动流程就不在话下了。

给你一份长长长的 Spring Boot 知识清单(下)相关推荐

  1. 给你一份长长长的 Spring Boot 知识清单(上)

    预警:本文非常长,建议先mark后看,也许是最后一次写这么长的文章 说明:前面有4个小节关于Spring的基础知识,分别是:IOC容器.JavaConfig.事件监听.SpringFactoriesL ...

  2. 给你一份超详细 Spring Boot 知识清单

    转载自   给你一份超详细 Spring Boot 知识清单 在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架.或许从命名上就能看出这个框架的设计初衷:快速的启动Spri ...

  3. 给你一份超详细Spring Boot知识清单

    什么是 Spring Boot ? 解释一下:Spring Boot 可以构建一切.Spring Boot 设计之初就是为了最少的配置,最快的速度来启动和运行 Spring 项目.Spring Boo ...

  4. 11月22日云栖精选夜读 | 送你一份Spring Boot 知识清单

    在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架.或许从命名上就能看出这个框架的设计初衷:快速的启动Spring应用.因而Spring Boot应用本质上就是一个基于Sp ...

  5. 超详细 Spring Boot 知识清单

    2019独角兽企业重金招聘Python工程师标准>>> 超详细 Spring Boot 知识清单 在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架.或许 ...

  6. Spring Boot知识清单

    2019独角兽企业重金招聘Python工程师标准>>> Spring Boot知识清单 Spring Boot 应用本质上就是一个基于 Spring 框架的应用,它是 Spring ...

  7. 这篇Spring Boot知识清单,你值得拥有,带你快速入门

    因而 Spring Boot 应用本质上就是一个基于 Spring 框架的应用,它是 Spring 对"约定优先于配置"理念的最佳实践产物,它能够帮助开发者更快速高效地构建基于 S ...

  8. 值得收藏——超详细 Spring Boot 知识清单

    在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架.或许从命名上就能看出这个框架的设计初衷:快速的启动Spring应用.因而Spring Boot应用本质上就是一个基于Sp ...

  9. Spring Boot 终极清单

    一.Spring Boot 终极清单诞生原因 我上学那会主要学的是 Java 和 .Net 两种语言,当时对于语言分类这事儿没什么概念,恰好在2009年毕业那会阴差阳错的先找到了 .Net 的工作,此 ...

最新文章

  1. juniper交换机 mac地址和端口绑定
  2. jQuery的祖先遍历
  3. mathtype中批量修改公式的字号和大小
  4. [html] 举例说明写一个button的按钮的方法有哪些?
  5. vue开发 - 将方法绑定到window对象,给app端调用
  6. 不靠谱的副业,别碰!
  7. 电脑计算机无法安3.5,win10 net framework 3.5安装不了的完美解决办法
  8. c++ 多线程 类成员函数_C++11多线程
  9. 推荐几个e书下载地址
  10. LabVIEW网络数据传输远程控制编程与验证测试
  11. 及时复盘的好处_及时复盘,促进成长
  12. 2016 新学++ , 回顾过去展望未来
  13. 秦曾昌人工智能课程---4、梯度下降算法
  14. win7 android studio 升级HAXM后无法启动安卓模拟器
  15. DDR3不同型号不同位宽仿真记录
  16. 一个能在vue3中运行的JSON编辑器,能展示JSON数据的高亮,打开时有默认数据
  17. Android HID设备的连接
  18. 宝塔开启面板ssl后无法访问-解决方案
  19. 房地产神秘顾客调查方案
  20. 从离散傅里叶变换到离散余弦变换 —— 公式证明

热门文章

  1. 动手---sbt(2)
  2. 自己动手开发智能聊天机器人完全指南(附python完整源码)
  3. 安装phpcms时出现Warning: ob_start(): output handler \'ob_gzhandler\' conflicts with \'zlib
  4. ios 应用 开发流程。。。
  5. 【OpenCV】使用projectPoints实现透视图到俯视图的变化效果
  6. 双边滤波(bilateral filter)彩色图 matlab实现代码
  7. Linux下根据进程ID查看进程文件的路径
  8. h264.265裸流和音频(ALAW或PCM)封装为mp4
  9. How to install python packages
  10. 分享这两年从事Linux系统运维行业的感受