Spring Boot自动装配原理详解
目录
1.环境和依赖
1.1.spring boot版本
1.2.依赖管理
2.自动装配
2.1.流程概述
2.2.三大步前的准备工作
2.2.1.注解入口
2.2.2.获取所有配置类
2.3.获取过滤列表
2.3.1.目的
2.3.2.过程
2.4.装载
2.4.1.目的
2.4.2.过程
2.5.自动配置
3.启动过程
3.1.整体流程
3.2.创建环境信息对象
3.3.创建应用上下文对象
3.4.刷新应用上下文对象
3.4.1.准备刷新
3.4.2.刷新
1.环境和依赖
1.1.spring boot版本
springboot 2.2.X版本采用的maven构建,2.3.X采用gradle构建,因此采用2.2.X,mavan构建的便于源码阅读。本文以2.2.9为例进行Spring Boot自动装配原理的解析。
1.2.依赖管理
引入Spring Boot的方式有两种
- 引入spring-boot-dependencies的pom文件
- 将spring-boot-starter-parent作为父级pom
这两种方式的底层都是都是一样的,都是引入了spring-boot-dependencies这个pom文件来管理Spring Boot的所有依赖。
SpringBoot中将一类场景要用到的依赖封装成一个starter,spring-boot-dependencies中包含了J2EE中所有场景(starter)的依赖,并声明了依赖的版本号。
2.自动装配
2.1.流程概述
首先所有JAVA程序的入口都是main方法,Spring Boot也不例外,只有main方法执行时,所有流程步骤才会执行,此处我们只是从启动流程中剥离出和自动装配相关的流程来进行单独解析。只需要大致知道自动装配流程有几步即可,如果有其它疑惑看后文的启动过程解析,就能豁然开朗。
自动装配的整个流程可以分为三大步
- 获取过滤列表
- 获取自动配置类列表
- 比对移除、封装返回
1.获取条件列表
获取类自动装载的条件列表。
2.获取自动配置列表
获取自动装载类的列表。
3.比对移除、封装返回
按照条件列表,将不满足被自动装载条件的类移除掉,返回满足条件的类列表。
2.2.三大步前的准备工作
2.2.1.注解入口
@SpringBootApplication
该注解是个复合注解:
@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 {
@EnableAutoConfiguration启动自动装配:
@Import(AutoConfigurationImportSelector.class) ,AutoConfigurationImportSelector会完成所有配置类的获取以及相关的准备工作。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
2.2.2.获取所有配置类
AutoConfigurationImportSelector被加载后,经过层层调用,最终会调用到DeferredImportSelector中:
会去扫描所有@Configuration封装成一个列表返回。
public Iterable<Entry> getImports() {Iterator var1 = this.deferredImports.iterator();while(var1.hasNext()) {ConfigurationClassParser.DeferredImportSelectorHolder deferredImport = (ConfigurationClassParser.DeferredImportSelectorHolder)var1.next();this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector());}//将得到的自动配置类按照@order进行排序return this.group.selectImports();}
2.3.获取过滤列表
2.3.1.目的
获取过滤列表,即去获取META-INF/spring-autoconfigure-metadata.properties这一文件。这个文件中会详细记录Spring Boot自带的各大J2EE场景的自动配置类(@Configuration)各自被自动装载生效的前提条件是什么。
2.3.2.过程
DeferredImportSelector.Group.process()中会首先获取自动装配的过滤条件列表,该列表中记录了待装配的类的装配条件。获取的核心方法是getAutoConfigurationMetadata(),该方法会根据传过来的ClassLoader去遍历加载classpath下的所有依赖,获取依赖中的META-INF/spring-autoconfigure-metadata.properties文件。
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,() -> String.format("Only %s implementations are supported, got %s",AutoConfigurationImportSelector.class.getSimpleName(),deferredImportSelector.getClass().getName()));//获取自动配置类AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);this.autoConfigurationEntries.add(autoConfigurationEntry);//解析存放自动配置类for (String importClassName : autoConfigurationEntry.getConfigurations()) {this.entries.putIfAbsent(importClassName, annotationMetadata);}}private AutoConfigurationMetadata getAutoConfigurationMetadata() {if (this.autoConfigurationMetadata == null) {this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);}return this.autoConfigurationMetadata;}
final class AutoConfigurationMetadataLoader {protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";private AutoConfigurationMetadataLoader() {}static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {return loadMetadata(classLoader, PATH);}static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {try {Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path): ClassLoader.getSystemResources(path);Properties properties = new Properties();while (urls.hasMoreElements()) {properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));}return loadMetadata(properties);}catch (IOException ex) {throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);}}
过滤列表中会以KV键值对的方式记录装配条件,例如:
org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.annotation.EnableRabbit
2.4.装载
2.4.1.目的
获取自动配置列表
对比过滤列表,移除不满足自动装载的类
封装返回
2.4.2.过程
process()方法中会调用getAutoConfigurationEntry()方法,并将过滤列表传和ClassLoader传过去,在getCandidateConfigurations()方法中通过传递的ClassLoader获取自动装配的列表"META-INF/spring.factories",然后比对过滤列表,将满足条件的待装配类的全路径记录在AutoConfigurationImportSelector.AutoConfigurationGroup的一个叫autoConfigurationEntries的List中。
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}AnnotationAttributes attributes = getAttributes(annotationMetadata);//从spring.factories中加载所有自动配置类List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);//移除重复配置类configurations = removeDuplicates(configurations);//得到指定要移除的类(@SpringBootApplication(exclude=FreeMarkerAutoConfiguration.class))Set<String> exclusions = getExclusions(annotationMetadata, attributes);//检查指定要移除的类,如果不是配置类,抛出异常checkExcludedClasses(configurations, exclusions);//移除指定要移除的自动配置类configurations.removeAll(exclusions);//获取满足条件的自动配置类列表configurations = filter(configurations, autoConfigurationMetadata);//记录下符合条件的对象,并封装在实体中返回fireAutoConfigurationImportEvents(configurations, exclusions);return new AutoConfigurationEntry(configurations, exclusions);
}
一切执行完毕后会回到入口出继续向下执行this.group.selectImports(),最终会调用到AutoConfigurationImportSelector的selectImports()方法,在该方法中会根据@order对自动配置类进行排序。
public Iterable<Entry> selectImports() {if (this.autoConfigurationEntries.isEmpty()) {return Collections.emptyList();}Set<String> allExclusions = this.autoConfigurationEntries.stream().map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());Set<String> processedConfigurations = this.autoConfigurationEntries.stream().map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));processedConfigurations.removeAll(allExclusions);return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream().map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName)).collect(Collectors.toList());
}
2.5.自动配置
在自动装载步骤中已经获得需要加载的自动配置类的全路径,接下来就是自动配置。
以随便一个AutoConfiguration类为例:
头上的一大串@Conditional注解其实就是过滤时的过滤条件,过滤列表其实就是通过这些条件注解生成的。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HttpProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {}
这个@Configuration满足条件后其中的@Bean都会被自动装入IOC。
3.启动过程
3.1.整体流程
Spring boot的启动过程就是围绕上下文的创建、准备、刷新(填充)展开的。
spring应用上下文和servletContext不是一个东西,servlet上下文用来维系当前应用的一块共享空间,目的是实现资源和数据在应用中的全局共享。spring的上下文是一个维护Bean定义以及对象之间协作关系的高级接口,目的是维护好整个spring中的资源,如配置文件、Bean对象等,其涵盖了IOC,但不只有IOC,可以理解为Spring应用的一个抽象。
在SpringApplication的run()方法中创建应用上下文,整个SpringApplication的run方法主要完成四个核心动作:
prepareEnvironment
创建环境信息对象,解析环境参数,包含配置文件、命令行传参等。
createApplicationContext
创建应用上下文对象
prepareContext
准备应用上下文对象
refreshContext
刷新应用上下文对象
// 类 SpringApplication 代码片段public ConfigurableApplicationContext run(String... args) {StopWatch stopWatch = new StopWatch();stopWatch.start();ConfigurableApplicationContext context = null;Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();configureHeadlessProperty();SpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting();try {// 包装通过命令行传入的名命令行参数ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 结合命令行参数 准备环境对象,该环境对象将会被设置到应用上下文对象 ApplicationContext 上 ,// 环境对象通常包含如下信息 : // 1. profile// 2. system properties// 3. system environment// 4. commandline arguments// 5. spring 配置文件// 6. 一个随机值属性源 random// 对于当前 WebFlux 应用来讲,这里实现类会使用 StandardReactiveWebEnvironmentConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);configureIgnoreBeanInfo(environment);Banner printedBanner = printBanner(environment);// 创建应用上下文对象 ApplicationContext // 实现类会采用 : AnnotationConfigReactiveWebServerApplicationContextcontext = createApplicationContext();exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);// 准备应用上下文对象 ApplicationContext // 1. 关联环境信息对象到应用上下文对象// 2. 对象创建后置处理 : 设置容器的类型转换服务// 3. 初始化应用上下文对象:调用各个 ApplicationContextInitializer// 4. 广播事件 : ApplicationContextInitializedEvent// 5. 将应用程序参数作为一个 bean 注册到容器 : springApplicationArguments// 6. 将应用程序入口类作为 bean 注册到容器 (load)// 7. 上下文加载完成生命周期事件回调,为各个实现了 接口 ApplicationContextAware 的 // ApplicationListener 设置应用上下文对象属性, 并广播事件 : ApplicationPreparedEventprepareContext(context, environment, listeners, applicationArguments, printedBanner);// 刷新应用上下文对象 ApplicationContext // 主要是调用应用上下文对象 ApplicationContext 自身的 refresh 方法refreshContext(context);afterRefresh(context, applicationArguments);stopWatch.stop();if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}listeners.started(context);// 应用程序上下文对象 ApplicationContext 已经准备就绪,// 现在调用各种开发人员或者框架其他部分定义的 // ApplicationRunner 或者 CommandLineRunnercallRunners(context, applicationArguments);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, listeners);throw new IllegalStateException(ex);}try {listeners.running(context);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, null);throw new IllegalStateException(ex);}return context;}
3.2.创建环境信息对象
// SpringApplication 代码片段private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments) {// Create and configure the environment// 创建环境信息对象 environmentConfigurableEnvironment environment = getOrCreateEnvironment();// 将应用程序参数关联到环境信息对象 environmentconfigureEnvironment(environment, applicationArguments.getSourceArgs());// 发布应用程序事件 : 环境信息对象准备好了 ,// 同步调用各个事件监听器listeners.environmentPrepared(environment);bindToSpringApplication(environment);if (!this.isCustomEnvironment) {environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());} ConfigurationPropertySources.attach(environment);return environment;}
普通web应用和reactive创建的环境信息对象类型不同,但是实际功能相同,并没有什么太大区别。
// SpringApplication 代码片段private ConfigurableEnvironment getOrCreateEnvironment() {if (this.environment != null) {return this.environment;}switch (this.webApplicationType) {case SERVLET:return new StandardServletEnvironment();case REACTIVE:return new StandardReactiveWebEnvironment();default:return new StandardEnvironment();}}
3.3.创建应用上下文对象
根据之前环境推断中得到的当前应用的环境类型来创建不同类型的应用上下文。
// SpringApplication 代码片段protected ConfigurableApplicationContext createApplicationContext() {Class<?> contextClass = this.applicationContextClass;if (contextClass == null) {try {// 根据 this.webApplicationType 确定应用上下文实现类switch (this.webApplicationType) {case SERVLET:// DEFAULT_SERVLET_WEB_CONTEXT_CLASS 常量值为 : // org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext// 对应 Spring MVC Servlet Web 环境contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);break;case REACTIVE:// DEFAULT_REACTIVE_WEB_CONTEXT_CLASS 常量值为 : // org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext// 对应 Spring WebFlux Reactive Web 环境contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);break;default:// DEFAULT_CONTEXT_CLASS 常量值为 : // org.springframework.context.annotation.AnnotationConfigApplicationContext// 不对应任何 Web 环境contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);}}catch (ClassNotFoundException ex) {throw new IllegalStateException("Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass",ex);}}// 确定应用上下文实现类之后,实例化应用上下文对象 return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);}
3.4.刷新应用上下文对象
3.4.1.准备刷新
这一步主要是完成刷新前的准备工作,将除IOC相关的一切context中的东西全部赋值初始化好。
主要完成以下动作:
关联环境信息
查找调用各种前置、后置处理器(自定义的、自带的)
调用各种回调
获取主启动类的路径,将主启动类封装成一个BeanDefinition
// SpringApplication 代码片段private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {// 1. 关联环境信息对象到应用上下文对象 context.setEnvironment(environment);// 2. 对象创建后置处理 : 设置容器的类型转换服务 postProcessApplicationContext(context);// 3. 初始化应用上下文对象:调用各个 ApplicationContextInitializer applyInitializers(context);// 4. 广播事件 : ApplicationContextInitializedEvent listeners.contextPrepared(context);if (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context);}// Add boot specific singleton beans// 5. 将应用程序参数作为一个 bean 注册到容器 : springApplicationArguments ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();beanFactory.registerSingleton("springApplicationArguments", applicationArguments);if (printedBanner != null) {beanFactory.registerSingleton("springBootBanner", printedBanner);}if (beanFactory instanceof DefaultListableBeanFactory) {((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);}// 获取主启动类的路径Set<Object> sources = getAllSources();Assert.notEmpty(sources, "Sources must not be empty");// 6. 将主启动类封装成一个BeanDefinition load(context, sources.toArray(new Object[0]));// 7. 上下文加载完成生命周期事件回调,为各个实现了 接口 ApplicationContextAware 的 // ApplicationListener 设置应用上下文对象属性, 并广播事件 : ApplicationPreparedEvent listeners.contextLoaded(context);}
3.4.2.刷新
主要是调用应用上下文对象 ApplicationContext 自身的 refresh 方法,这是上下文对象的初始化中最关键的一步,该步骤中会完成几个核心动作:
初始化IOC容器(即BeanFactory)
该步骤中就会扫描解析注解,触发自动装配.
初始化WebServer容器
刷新应用上下文的动作其实是在spring相关的jar中,因此首先要有个概念,在这一步之前spring boot的动作已经完成,真正与IOC相关的动作还是由spring来完成,所以说spring boot是对spring的二次封装。
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// 做一些初始化动作prepareRefresh();// 获取bean factoryConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// 初始化bean factory,为其成员属性赋一些值prepareBeanFactory(beanFactory);try {// 获取所有bean后置处理器postProcessBeanFactory(beanFactory);// **最核心的方法,注解的扫描,自动配置类的装载,IOC的初始化等全在这个方法中invokeBeanFactoryPostProcessors(beanFactory);// Register bean processors that intercept bean creation.registerBeanPostProcessors(beanFactory);// Initialize message source for this context.initMessageSource();// Initialize event multicaster for this context.initApplicationEventMulticaster();// Initialize other special beans in specific context subclasses.onRefresh();// Check for listener beans and register them.registerListeners();// Instantiate all remaining (non-lazy-init) singletons.finishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event.finishRefresh();}catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("Exception encountered during context initialization - " +"cancelling refresh attempt: " + ex);}// Destroy already created singletons to avoid dangling resources.destroyBeans();// Reset 'active' flag.cancelRefresh(ex);// Propagate exception to caller.throw ex;}finally {// Reset common introspection caches in Spring's core, since we// might not ever need metadata for singleton beans anymore...resetCommonCaches();}}}
初始化IOC:
创建容器其实没有什么说的,就是new一个web server(tomcat、netty或者jetty)出来。这里着重要说一下初始化IOC。
入口在invokeBeanFactoryPostProcessors(beanFactory)。
IOC容器的初始化分为三步:
Resource定位
定位到需要的各种路径:
BasePackage
这一步在准备刷新的时候就已经完成,并在封装在了主启动类封装为的BeanDefinition中。
基于BasePackage去扫描通过注解自定义的需要注入IOC的Bean。
自动配置类的全路径
这一步在刷新应用上下文的时候进行,即去获取factory.properties。
基于自动配置类的全路径去将对应自动配置类注入IOC。
BeanDefinition载入
将定位到的Resource记录的Class分别封装为一个个的Definition。
BeanDefinition注册
将Definition注册进IOC中。其实就是注入到一个ConcurrentHashMap中,IOC就是通过这个Map来持有这些BeanDefinition的。
IOC涉及的两个核心概念:
BeanDefinition
BeanFactory
IOC容器其实就是BeanFactory,BeanFactory就是IOC容器的规范接口,有多个实现,最典型的就是DefalutListableBeanFactory,IOC容器中有一个成员Map(BeanDefinitionMap),该Map持有所有的BeanDefinition,用来维护Bean的基本信息(class、作用域等)
Spring Boot自动装配原理详解相关推荐
- springboot 整合redis_springboot自动装配原理详解
1)传统ssm整合redis的时候 需要在xml的配置文件中 进行大量的配置Bean 我们在这里使用springboot来代替ssm的整合,只是通过xml的形式来整合redis 第一步:加入配置 &l ...
- Spring Boot 自动装配原理
本文已经收录到Github仓库,该仓库包含计算机基础.Java核心知识点.多线程.JVM.常见框架.分布式.微服务.设计模式.架构等核心知识点,欢迎star~ Github地址:https://git ...
- 芋道 Spring Boot 自动配置原理
转载自 芋道 Spring Boot 自动配置原理 1. 概述 友情提示:因为本文是分享 Spring Boot 自动配置的原理,所以需要胖友有使用过 Spring Boot 的经验.如果还没使用过 ...
- Spring Boot的启动器Starter详解
Spring Boot的启动器Starter详解 作者:chszs,未经博主允许不得转载.经许可的转载需注明作者和博客主页:http://blog.csdn.net/chszs Spring Boot ...
- 学习第三篇:【SpringBoot-Labs】芋道 Spring Boot 自动配置原理
本周(8.21-8.27)将学习芋道 Spring Boot的以下文章: 8.21: 快速入门 8.22:Spring Boot 自动配置原理 .Jar 启动原理 8.23:调试环境. 热部署入门.消 ...
- Spring Boot自动装配过程解析及简单Demo演示
文章目录 1.约定大于配置 2.自动装配原理 2.1.`@SpringBootApplication` 2.2.`@EnableAutoConfiguration` 2.3.`@Import` 2.4 ...
- Spring Boot自动配置原理、实战
Spring Boot自动配置原理 Spring Boot的自动配置注解是@EnableAutoConfiguration, 从上面的@Import的类可以找到下面自动加载自动配置的映射. org.s ...
- ElasticSearch——Spring Boot 集成 ES 操作详解
文章目录 ElasticSearch--Spring Boot 集成 ES 操作详解 1.SpringBoot 集成 ES 2.索引的API操作详解 3.文档的API操作详解 ElasticSearc ...
- 轻量级数据库sqlite,spring boot+sqlite的配置详解 (一)
spring boot+sqlite的配置,及成功运行详解 sqlite数据库的安装与调试 首先,通过sqlite官方地址下载对应的安装包 https://www.sqlite.org/downloa ...
最新文章
- C语言中 #pragma pack()
- 设置springboot日志级别_Spring Boot 日志框架实践
- linux 删除node进程,关于node.js:杀死Linux中的节点进程
- shell 文件路径有空格_Python学习第57课-shell入门之基本简单命令(一)
- H5炫酷特效系列1——canvas满屏幕变换爱心示例
- 看到的精彩的地方 分享下
- java和xampp_XAMPP和Bugfree详细教程
- [计算机网络] - 从英雄联盟,看数据包何去何从?
- 汪文君PowerMock实战视频
- sqlplus命令大全
- ORA-28003和ORA-20001解决办法
- 大道至简:软件工程实践者的思想(读后感想)
- 李开复:中国创业有四大优势
- Spring框架基础(2)----Bean的创建及标签属性
- scala使用log4j_将Twitter4j与Scala结合使用以执行用户操作
- 什么是utf8mb4和utf8mb3区别?
- 小综述 | 深度学习在数学文本相关领域的研究梳理
- all CUDA-capable devices are busy or unavailable in function ‘setDevice‘
- Cytoskeleton 磷酸盐测定试剂盒说明书
- 微软亚洲研究院电话面试