【栖梧-源码-spring】@Bean从解析到注册到beanDefinitionMap

  • 序幕
    • 源码阅读技巧
    • 本文说明
    • 类 ConfigurationClassParser#doProcessConfigurationClass
    • 方法retrieveBeanMethodMetadata
    • 类 BeanMethod
    • 类ConfigurationClassBeanDefinitionReader
    • 方法loadBeanDefinitionsForBeanMethod
    • 类DefaultListableBeanFactory
    • 总结

序幕

本文是我的第一篇博客,自我研读spring源码以来,收获颇丰。自工作以来,疑惑颇多,于是通过网络、同事、领导、技术网友等益友的分享和指导,解决了工作中许多疑难杂症,所以我也非常希望把我工作中、学习中的收获分享给所有需要的人。同时受水平所限,其中必有缺漏不足之处,还望指出。

"道友"解释->志同道合者;

源码阅读技巧

书山有路勤为径,学海无涯需持久!
源码阅读是一件枯燥乏味的事情,这种说法是对没有时间、没有兴趣、没有好奇心的道友来讲的。要想深入理解一个框架的原理,必须了解其实现细节,所以花费大量的时间、精力来吸取里面的营养是必须的。

<源码-spring>系列文章均涉及大量源码,且自建简单的项目是基于springboot 2的版本,如果其他版本的源码可能稍有差异,属正常情况。
如果道友你从来没有看过spring源码,阅读起来将会吃力,请悉知!

本文说明

本文核心讲解的是@Bean注解从解析bean到beanFactory的beanDefiniton中

类 ConfigurationClassParser#doProcessConfigurationClass

package org.springframework.context.annotation;

当spring启动后,走到高级AbstractApplicationContext这个高级容器的最核心的 refresh() 方法后,再进入invokeBeanFactoryPostProcessors(beanFactory)调用后置处理器方法,就会调用静态方法PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()) 开始解析所有的BeanDefinition了,由于本文主要讲解@Bean,所以其他的暂时不详解。

 //此类就是解析配置类的入口@Nullableprotected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)throws IOException {//先判断是否有@Component注解,if (configClass.getMetadata().isAnnotated(Component.class.getName())) {//首先递归处理任何成员(嵌套)类// Recursively process any member (nested) classes firstprocessMemberClasses(configClass, sourceClass);}//处理带有@PropertySource的类,然后将里面变量存到全局的Environment中去for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class,org.springframework.context.annotation.PropertySource.class)) {if (this.environment instanceof ConfigurableEnvironment) {processPropertySource(propertySource);}else {logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +"]. Reason: Environment must implement ConfigurableEnvironment");}}// 处理 @ComponentScan 注解,注意里面是多个递归解析(如果不清楚是递归的道友进入这个方法可能会晕掉的),这//里面就会解析所有的@Component注解的,包括@Controller、@Service等这些被@Component注解过的注解,解析后的//bean全部放到ConfigurationClassParser这个对象的 configurationClasses 这个map里面进行后续处理Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);if (!componentScans.isEmpty() &&!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {for (AnnotationAttributes componentScan : componentScans) {// The config class is annotated with @ComponentScan -> perform the scan immediatelySet<BeanDefinitionHolder> scannedBeanDefinitions =this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());// Check the set of scanned definitions for any further config classes and parse recursively if neededfor (BeanDefinitionHolder holder : scannedBeanDefinitions) {BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();if (bdCand == null) {bdCand = holder.getBeanDefinition();}if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {parse(bdCand.getBeanClassName(), holder.getBeanName());}}}}//处理@Import的注解processImports(configClass, sourceClass, getImports(sourceClass), true);// Process any @ImportResource annotationsAnnotationAttributes importResource =AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);if (importResource != null) {String[] resources = importResource.getStringArray("locations");Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");for (String resource : resources) {String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);configClass.addImportedResource(resolvedResource, readerClass);}}// 关键来了,本文重点就是分析 @Bean的解析过程,。先获取被@Bean了的方法,下面会讲解此方法Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);for (MethodMetadata methodMetadata : beanMethods) {//然后把@Bean注解的方法放入到 ConfigurationClass 这个对象里面的 beanMethods这个/Set<BeanMethod> 里面//到此,解析@Bean的bean第一步-先把@Bean找出来,就在这完成了,下面会简单说一下装@Bean的类BeanMethodconfigClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));}// Process default methods on interfacesprocessInterfaces(configClass, sourceClass);// Process superclass, if anyif (sourceClass.getMetadata().hasSuperClass()) {String superclass = sourceClass.getMetadata().getSuperClassName();if (superclass != null && !superclass.startsWith("java") &&!this.knownSuperclasses.containsKey(superclass)) {this.knownSuperclasses.put(superclass, configClass);// Superclass found, return its annotation metadata and recursereturn sourceClass.getSuperClass();}}// No superclass -> processing is completereturn null;}

方法retrieveBeanMethodMetadata

此方法是获取当前类所有的@Bean注解的方法

 //此方法是检索所有@Bean方法的元数据private Set<MethodMetadata> retrieveBeanMethodMetadata(SourceClass sourceClass) {//获取元数据信息AnnotationMetadata original = sourceClass.getMetadata();//获取所有的@Bean方法Set<MethodMetadata> beanMethods = original.getAnnotatedMethods(Bean.class.getName());if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {//尝试通过ASM读取类文件以确定性声明顺序... 不幸的是,JVM的标准反射以任意//顺序返回方法,甚至在同一JVM上//同一应用程序的不同运行之间。try {AnnotationMetadata asm =this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Bean.class.getName());if (asmMethods.size() >= beanMethods.size()) {Set<MethodMetadata> selectedMethods = new LinkedHashSet<>(asmMethods.size());for (MethodMetadata asmMethod : asmMethods) {for (MethodMetadata beanMethod : beanMethods) {if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {selectedMethods.add(beanMethod);break;}}}if (selectedMethods.size() == beanMethods.size()) {// All reflection-detected methods found in ASM method set -> proceedbeanMethods = selectedMethods;}}}catch (IOException ex) {logger.debug("Failed to read class file via ASM for determining @Bean method order", ex);// No worries, let's continue with the reflection metadata we started with...}}return beanMethods;}
以上操作就会把所有的@Bean方法找出来

类 BeanMethod

package org.springframework.context.annotation;

上面的源码中,看见new BeanMethod(),这个BeanMethod继承了ConfigurationMethod类,ConfigurationMethod它有两个字段MethodMetadata和ConfigurationClass这两个对象,而BeanMethod集成后没有单独的字段,仅仅覆写了validate此方法和有一个私有的非静态的内部类,


final class BeanMethod extends ConfigurationMethod {public BeanMethod(MethodMetadata metadata, ConfigurationClass configurationClass) {super(metadata, configurationClass);}@Overridepublic void validate(ProblemReporter problemReporter) {if (getMetadata().isStatic()) {// static @Bean methods have no constraints to validate -> return immediatelyreturn;}if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) {if (!getMetadata().isOverridable()) {// instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIBproblemReporter.error(new NonOverridableMethodError());}}}//这个BeanMethod.NonOverridableMethodError内部类的作用仅仅是处理@Bean方法 “ must not be private or final ”private class NonOverridableMethodError extends Problem {public NonOverridableMethodError() {super(String.format("@Bean method '%s' must not be private or final; change the method's modifiers to continue",getMetadata().getMethodName()), getResourceLocation());}}
}

上面将bean解析成ConfigurationClass后,接下来就需要把ConfigurationClass里面的beanMethods注册成为ConfigurationClassBeanDefinition

类ConfigurationClassBeanDefinitionReader

package org.springframework.context.annotation;

private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {//判断是否需要跳过注册if (trackedConditionEvaluator.shouldSkip(configClass)) {String beanName = configClass.getBeanName();if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {this.registry.removeBeanDefinition(beanName);}this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());return;}//如果是if (configClass.isImported()) {registerBeanDefinitionForImportedConfigurationClass(configClass);}//此方法就是把@Bean注册到beanFactory的入口for (BeanMethod beanMethod : configClass.getBeanMethods()) {loadBeanDefinitionsForBeanMethod(beanMethod);}loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());//这里面处理实现了ImportBeanDefinitionRegistrar的类,此时会调用ImportBeanDefinitionRegistrar的registerBeanDefinitions方法loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());}

方法loadBeanDefinitionsForBeanMethod

 private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {//获取@Bean方法的类的信息ConfigurationClass configClass = beanMethod.getConfigurationClass();//获取@Bean的元数据信息MethodMetadata metadata = beanMethod.getMetadata();//获取方法名,也就是默认的bean的名称String methodName = metadata.getMethodName();// 我们是否需要将bean标记为跳过它的状态if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {configClass.skippedBeanMethods.add(methodName);return;}if (configClass.skippedBeanMethods.contains(methodName)) {return;}//把@Bean注解里面的信息解析出来AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);Assert.state(bean != null, "No @Bean annotation attributes");// 取出别名List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));String beanName = (!names.isEmpty() ? names.remove(0) : methodName);// 将别名注册到beanFactoryfor (String alias : names) {this.registry.registerAlias(beanName, alias);}if (isOverriddenByExistingDefinition(beanMethod, beanName)) {if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) {throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() +"' clashes with bean name for containing configuration class; please make those names unique!");}return;}//ConfigurationClassBeanDefinition它是ConfigurationClassBeanDefinitionReader的内部的私有的静态类//ConfigurationClassBeanDefinition它继承了RootBeanDefinition,RootBeanDefinition的层级关系这里就不罗列了哈//为什么要用ConfigurationClassBeanDefinition把bean信息包装一层?  因为beanFactory的beanDefinitionMap必须是BeanDefinition的子类//实现了它才是标准的BeanDefinition,spring实例化bean只能实例化BeanDefinitionConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata);beanDef.setResource(configClass.getResource());beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));//静态的@Bean和非静态的处理方式有一点点区别if (metadata.isStatic()) {// 静态 @Bean 方法,这里设置的是setBeanClassNamebeanDef.setBeanClassName(configClass.getMetadata().getClassName());beanDef.setFactoryMethodName(methodName);}else {// 实例 @Bean 方法setFactoryBeanNamebeanDef.setFactoryBeanName(configClass.getBeanName());beanDef.setUniqueFactoryMethodName(methodName);}//下面都是设置@Bean注解里面属性beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);Autowire autowire = bean.getEnum("autowire");if (autowire.isAutowire()) {beanDef.setAutowireMode(autowire.value());}boolean autowireCandidate = bean.getBoolean("autowireCandidate");if (!autowireCandidate) {beanDef.setAutowireCandidate(false);}String initMethodName = bean.getString("initMethod");if (StringUtils.hasText(initMethodName)) {beanDef.setInitMethodName(initMethodName);}String destroyMethodName = bean.getString("destroyMethod");beanDef.setDestroyMethodName(destroyMethodName);ScopedProxyMode proxyMode = ScopedProxyMode.NO;AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);if (attributes != null) {beanDef.setScope(attributes.getString("value"));proxyMode = attributes.getEnum("proxyMode");if (proxyMode == ScopedProxyMode.DEFAULT) {proxyMode = ScopedProxyMode.NO;}}// 如有必要,将原始bean定义替换为目标bean定义BeanDefinition beanDefToRegister = beanDef;if (proxyMode != ScopedProxyMode.NO) {BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(new BeanDefinitionHolder(beanDef, beanName), this.registry,proxyMode == ScopedProxyMode.TARGET_CLASS);beanDefToRegister = new ConfigurationClassBeanDefinition((RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata);}if (logger.isTraceEnabled()) {logger.trace(String.format("Registering bean definition for @Bean method %s.%s()",configClass.getMetadata().getClassName(), beanName));}//这里调用了真正的、幕后的BeanFactory->DefaultListableBeanFactory的registerBeanDefinition方法,下面继续看源码this.registry.registerBeanDefinition(beanName, beanDefToRegister);}

类DefaultListableBeanFactory

package org.springframework.beans.factory.support;

躲在层层包装之后的beanFactory就是它了,spring的beanFactory的核心类之一,DefaultListableBeanFactory

 //这个方法是BeanDefinitionRegistry接口的实现@Overridepublic void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)throws BeanDefinitionStoreException {//老规矩,先验证合法性Assert.hasText(beanName, "Bean name must not be empty");Assert.notNull(beanDefinition, "BeanDefinition must not be null");if (beanDefinition instanceof AbstractBeanDefinition) {try {((AbstractBeanDefinition) beanDefinition).validate();}catch (BeanDefinitionValidationException ex) {throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,"Validation of bean definition failed", ex);}}BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);//如果beanDefinitionMap已经存在了,就验证是否两个对象相同然后打印一下废话日志等等,反正最后要把它给覆盖了,哈哈哈if (existingDefinition != null) {if (!isAllowBeanDefinitionOverriding()) {throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);}else if (existingDefinition.getRole() < beanDefinition.getRole()) {// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTUREif (logger.isInfoEnabled()) {logger.info("Overriding user-defined bean definition for bean '" + beanName +"' with a framework-generated bean definition: replacing [" +existingDefinition + "] with [" + beanDefinition + "]");}}else if (!beanDefinition.equals(existingDefinition)) {if (logger.isDebugEnabled()) {logger.debug("Overriding bean definition for bean '" + beanName +"' with a different definition: replacing [" + existingDefinition +"] with [" + beanDefinition + "]");}}else {if (logger.isTraceEnabled()) {logger.trace("Overriding bean definition for bean '" + beanName +"' with an equivalent definition: replacing [" + existingDefinition +"] with [" + beanDefinition + "]");}}//管你已经占着茅坑在拉屎,现在我是老大一脚把你踢开让我来,哈哈哈哈this.beanDefinitionMap.put(beanName, beanDefinition);}else {//检查此工厂的bean创建阶段是否已经开始,即是否在此期间将任何bean标记为已创建,说白了就是//目前为止 IOC 容器已经有病了(有bean),哈哈哈//这里为什么要整一个判断 IOC 容器是否已经启动了呢????//情况成员变量: private volatile List<String> beanDefinitionNames = new ArrayList<>(256);//明白了没?    size = 256 !!!   如果容器已经启动了,那么有可能 beanDefinitionNames已经满了,装不下了,数组越界,所以//需要新建一个数组,容量加一个,然后重新引用。。。。if (hasBeanCreationStarted()) {// 无法再修改启动时集合元素(用于稳定迭代),这里就最终的开始将@Bean的bean注册到beanDefinitionMap了synchronized (this.beanDefinitionMap) {this.beanDefinitionMap.put(beanName, beanDefinition);//上面说的list容量加一,就是这里哈List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);updatedDefinitions.addAll(this.beanDefinitionNames);updatedDefinitions.add(beanName);this.beanDefinitionNames = updatedDefinitions;if (this.manualSingletonNames.contains(beanName)) {Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);updatedSingletons.remove(beanName);this.manualSingletonNames = updatedSingletons;}}}else {//仍在启动注册阶段,意思就是目前为止 IOC 没病(bean),容器还是空的this.beanDefinitionMap.put(beanName, beanDefinition);this.beanDefinitionNames.add(beanName);this.manualSingletonNames.remove(beanName);}this.frozenBeanDefinitionNames = null;}if (existingDefinition != null || containsSingleton(beanName)) {resetBeanDe  finition(beanName);}}

至此,@Bean的bean从配置类里面已经成功从一个小妹妹变成了一个大菇凉了。

总结

spring在启动时,在调用后置处理器PostProcessor的时候,将会把所有的bean包括@Bean的bean注册到beanFactory的beanDefiniton里面去,先将@Bean解析到父ConfigurationClass里面去,然后再一个一个的加载ConfigurationClass里面的bean定义。最后再告诉道友一个小秘密哦,这里面的所有操作都在 ConfigurationClassPostProcessor 这个 PostProcessor里面的invokeBeanDefinitionRegistryPostProcessors静态方法里面完成的,哈哈哈哈

【栖梧-源码-spring】@Bean从解析到注册到beanDefinitionMap相关推荐

  1. Spring-bean的循环依赖以及解决方式___Spring源码初探--Bean的初始化-循环依赖的解决

    本文主要是分析Spring bean的循环依赖,以及Spring的解决方式. 通过这种解决方式,我们可以应用在我们实际开发项目中. 什么是循环依赖? 怎么检测循环依赖 Spring怎么解决循环依赖 S ...

  2. MyBatis 源码分析 - 映射文件解析过程

    1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...

  3. 【flink】Flink 1.12.2 源码浅析 : yarn-per-job模式解析 TaskMasger 启动

    1.概述 转载:Flink 1.12.2 源码浅析 : yarn-per-job模式解析 [四] 上一篇: [flink]Flink 1.12.2 源码浅析 : yarn-per-job模式解析 Jo ...

  4. 【flink】Flink 1.12.2 源码浅析 : yarn-per-job模式解析 JobMasger启动 YarnJobClusterEntrypoint

    1.概述 转载:Flink 1.12.2 源码浅析 : yarn-per-job模式解析 [三] 上一章:[flink]Flink 1.12.2 源码浅析 : yarn-per-job模式解析 yar ...

  5. 【flink】Flink 1.12.2 源码浅析 : yarn-per-job模式解析 yarn 提交过程解析

    1.概述 转载:Flink 1.12.2 源码浅析 : yarn-per-job模式解析 [二] 请大家看原文去. 接上文Flink 1.12.2 源码分析 : yarn-per-job模式浅析 [一 ...

  6. 【flink】Flink 1.12.2 源码浅析 : yarn-per-job模式解析 从脚本到主类

    1.概述 转载:Flink 1.12.2 源码浅析 : yarn-per-job模式解析 [一] 可以去看原文.这里是补充专栏.请看原文 2. 前言 主要针对yarn-per-job模式进行代码分析. ...

  7. Android Fragment 从源码的角度去解析(上)

    ###1.概述 本来想着昨天星期五可以早点休息,今天可以早点起来跑步,可没想到事情那么的多,晚上有人问我主页怎么做到点击才去加载Fragment数据,而不是一进入主页就去加载所有的数据,在这里自己就对 ...

  8. [darknet源码系列-2] darknet源码中的cfg解析

    [darknet源码系列-2] darknet源码中的cfg解析 FesianXu 20201118 at UESTC 前言 笔者在[1]一文中简单介绍了在darknet中常见的数据结构,本文继续上文 ...

  9. 手机QQ侧滑菜单_从源码上一步步解析效果的实现

    本文思想来自洪洋大哥,本来写的原创的,有些朋友看到标题后认为是照搬翔哥的例子,仔细看看,会有不同,不过其中的主要思想还是翔哥的,滑动方面的算法还真是有些区别的,看完了就知道不一样,而且我这人比较啰嗦, ...

最新文章

  1. enumerate在python中的意思_Python中enumerate用法详解
  2. 60 Permutation Sequence
  3. 一起学习手撕包菜如何做 - 生活至上,美容至尚!
  4. Activiti学习——Activiti与Spring集成
  5. 【推荐系统】推荐系统主流召回方法综述
  6. codeforces1435 D. Shurikens
  7. 编写干净的测试–提防魔术
  8. 前端学习(14):相对路径和绝对路径
  9. 嵌入式 boa服务器移植
  10. 以后出去找工作,只能说自己是产品策划了
  11. CentOs基础操作指令(进程管理)
  12. 腾讯云服务器CentOS 7安装JAVA JDK并运行class文件
  13. 穷举法求最大公共子序列C语言,算法--最长公共子序列(LongestCommon Subsequence, LCS)...
  14. MyCat分片规则之程序指定分片
  15. oracle grant的用法,oracle grant总结
  16. MSN无法登陆错误汇总
  17. 遥感水文前景_【充电】学遥感必读的十本专业书
  18. Ant Design表格插入图片
  19. 不积跬步,无以至千里
  20. qt在表格中如何画线_如何在电子表格中的某单元格内画一根长线

热门文章

  1. 计算机社团成立大会主持稿四个主持人,团委成立大会主持词_社团成立大会主持词范文...
  2. python下wordpdf转换总结
  3. 图解电动汽车:电动汽车简介
  4. 51单片机蜂鸣器播放音乐C语言程序实例,51单片机蜂鸣器音乐之八月桂花播放源程序...
  5. wphone 开发 跳转应用市场 全景透视效果
  6. is word 无法启动转换器 mswrd632.wpc
  7. RS485远程无线抄表系统方案及工作原理Wireless Infrared Meter Reading controller
  8. 调用haoya解压文件
  9. wireshark 的用处、下载和安装
  10. V4L2开发应用流程的各类超实用VIDIOC命令及其结构体集锦