松鼠乐园 松鼠乐园
  • 注册
  • 登录
  • 首页
  • 快捷入口
    • Vue
    • Tensorflow
    • Springboot
    • 语言类
      • CSS
      • ES5
      • ES6
      • Go
      • Java
      • Javascript
    • 工具类
      • Git
      • 工具推荐
    • 服务器&运维
      • Centos
      • Docker
      • Linux
      • Mac
      • MySQL
      • Nginx
      • Redis
      • Windows
    • 资源类
      • 论文
      • 书籍推荐
      • 后端资源
      • 前端资源
      • html网页模板
      • 代码
    • 性能优化
    • 测试
  • 重大新闻
  • 人工智能
  • 开源项目
  • Vue2.0从零开始
  • 广场
首页 › Springboot › JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

迦娜王
10月前Springboot
169 0 0
JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

四、另一件武器: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 {
 @Override
 public void onMethodBegin(MethodMonitorEvent event) {
 // 记录方法开始执行时的时间
 event.timestamp = System.currentTimeMillis();
 }
 @Override
 public void onMethodEnd(MethodMonitorEvent event) {
 // 计算方法耗时
 long duration = System.currentTimeMillis() - event.timestamp;
 System.out.println("耗时:"   duration);
 }
}

事件监听器接口针对不同的事件发布实际提供相应的处理方法定义,最重要的是,其方法只接收MethodMonitorEvent参数,说明这个监听器类只负责监听器对应的事件并进行处理。

有了事件和监听器,剩下的就是发布事件,然后让相应的监听器监听并处理。通常情况,我们会有一个事件发布者,它本身作为事件源,在合适的时机,将相应的事件发布给对应的事件监听器:

public class MethodMonitorEventPublisher {
 private Listlisteners = new ArrayList();
 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) {
 // 避免在事件处理期间,监听器被移除,这里为了安全做一个复制操作
 ListcopyListeners = ➥ new ArrayList(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() {}
JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

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

在合适的时机发布事件。

此例中的methodMonitor()方法是事件发布的源头,其在方法执行之前和结束之后两个时间点发布MethodMonitorEvent事件,每个时间点发布的事件都会传给相应的监听器进行处理。

在具体实现时需要注意的是,事件发布是顺序执行,为了不影响处理性能,事件监听器的处理逻辑应尽量简单。

JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

事件监听器的管理。

publisher类中提供了事件监听器的注册与移除方法,这样客户端可以根据实际情况决定是否需要注册新的监听器或者移除某个监听器。

如果这里没有提供 remove 方法,那么,注册的监听器示例将一直被 MethodMonitorEventPublisher 引用,即使已经废弃不用了,也依然在发布者的监听器列表中,这会导致隐性的内存泄漏。

JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

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 接口,该接口提供了 voidpublishEvent (ApplicationEvent event) 方法定义,不难看出, ApplicationContext 容器担当的就是事件发布者的角色。

如果有兴趣可以查看 AbstractApplicationContext.publishEvent (ApplicationEvent event) 方法的源码:ApplicationContext将事件的发布以及监听器的管理工作委托给ApplicationEventMulticaster接口的实现类。

在容器启动时,会检查容器内是否存在名为applicationEventMulticaster的ApplicationEventMulticaster对象实例。

如果有就使用其提供的实现,没有就默认初始化一个SimpleApplicationEventMulticaster作为实现。

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

JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

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

典型的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相关知识请参考前文
 Listfactories = new ArrayList(new LinkedHashSet( 
 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 {
 @Bean
 public 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(),更多的细节在下一小节中说明。

JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

六、启动引导: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 文件中配置的 ApplicationContextInitializer 和 ApplicationListener 两个接口的实现类名称,以便后期构造相应的实例。

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> listeners)两个方法来添加一个或者多个自定义监听器
  • 既然SpringApplication的初始化流程中已经从spring.factories中获取到ApplicationListener的实现类,那么我们直接在自己的jar包的META-INF/spring.factories文件中新增配置即可:
org.springframework.context.ApplicationListener=\
cn.moondev.listeners.xxxxListener\

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

JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

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() {
 // 发布一个ApplicationStartedEvent
 this.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为ConfigurationClassPostProcessor 。

ConfigurationClassPostProcessor 用于解析处理各种注解,包括: @Configuration 、 @ComponentScan 、 @Import 、 @PropertySource 、 @ImportResource 、 @Bean 。

当处理 @import 注解的时候,就会调用<自动配置>这一小节中的 EnableAutoConfigurationImportSelector.selectImports() 来完成自动配置功能。

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

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

这就是Spring Boot的整个启动流程,其核心就是在Spring容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener以及各种BeanFactoryPostProcessor等等。

JAVA学习一份超详细 Spring Boot 知识清单是不是你需要的那份!

你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。

整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。

言而总之,Spring才是核心,理解清楚Spring容器的启动流程,那Spring Boot启动流程就不在话下了。

SpringBoot
0
干货!没学过 Spring Boot?送你一份超详细的知识清单
上一篇
有哪些值得学习的spring boot开源项目?
下一篇
评论 (0)

请登录以参与评论。

现在登录
聚合文章
在Gitee收获近 5k Star,更新后的Vue版RuoYi有哪些新变化?
2月前
vue3.x reactive、effect、computed、watch依赖关系及实现原理
2月前
Vue 3 新特性:在 Composition API 中使用 CSS Modules
2月前
新手必看的前端项目去中心化和模块化思想
2月前
标签
AI AI项目 css docker Drone Elaticsearch es5 es6 Geometry Go gru java Javascript jenkins lstm mysql mysql优化 mysql地理位置索引 mysql索引 mysql规范 mysql设计 mysql配置文件 mysql面试题 mysql高可用 nginx Redis redis性能 rnn SpringBoot Tensorflow tensorflow2.0 UI设计 vue vue3.0 vue原理 whistle ZooKeeper 开源项目 抓包工具 日志输出 机器学习 深度学习 神经网络 论文 面试题
相关文章
两年摸爬滚打 Spring Boot,总结了这 16 条最佳实践
「开源」10个优秀的SpringBoot项目,总有你喜欢的
给你一份超详细Spring Boot知识清单
Spring Boot 核心知识,深入剖析,请收藏
松鼠乐园

资源整合,创造价值

小伙伴
墨魇博客 无同创意
目录
重大新闻 Centos CSS Docker ES5 ES6 Go Java Javascript Linux Mac MySQL Nginx Redis Springboot Tensorflow Vue Vue2.x从零开始 Windows 书籍推荐 人工智能 前端资源 后端资源 壁纸 开源项目 测试 论文
Copyright © 2018-2021 松鼠乐园. Designed by nicetheme. 浙ICP备15039601号-4
  • 重大新闻
  • Centos
  • CSS
  • Docker
  • ES5
  • ES6
  • Go
  • Java
  • Javascript
  • Linux
  • Mac
  • MySQL
  • Nginx
  • Redis
  • Springboot
  • Tensorflow
  • Vue
  • Vue2.x从零开始
  • Windows
  • 书籍推荐
  • 人工智能
  • 前端资源
  • 后端资源
  • 壁纸
  • 开源项目
  • 测试
  • 论文
热门搜索
  • jetson nano
  • vue
  • java
  • mysql
  • 人工智能
  • 人脸识别
迦娜王
坚持才有希望
1224 文章
33 评论
233 喜欢
  • 0
  • 0
  • Top