• Spring Cloud(十三):Spring 扩展


    • Spring扩展点
      • Bean的生命周期中的BeanPostProcessor扩展点
      • Spring扩展点梳理
    • Spring扩展点应用场景
      • 整合Nacos 服务注册
        • ApplicationListener扩展场景——监听容器中发布的事件
        • Lifecycle Nacos 发布订阅 & Eureka 服务启动、同步、剔除
        • Lifecycle扩展场景——管理具有启动、停止生命周期需求的对象
      • 整合Nacos 发布订阅 NacosWatch
      • 整合Eureka 注册
        • 扩展: Eureka Server端上下文的初始化是在SmartLifecycle#start中实现的
      • 整合Ribbon
        • SmartInitializingSingleton扩展场景—— 对容器中的Bean对象进行定制处理
      • 整合Feign
        • FactoryBean的扩展场景——将接口生成的代理对象交给Spring管理
      • 整合sentinel
        • HandlerInterceptor扩展场景——对mvc请求增强
        • SmartInitializingSingleton&FactoryBean结合场景——根据类型动态装配对象
      • 整合seata
        • AbstractAutoProxyCreator&MethodInterceptor结合场景——实现方法增强

    Spring扩展点

    Bean的生命周期中的BeanPostProcessor扩展点

    1. bean create before
      resolveBeforeInstantiation(beanName, mbdToUse); => InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation(beanClass, beanName);

    2. bean instance before
      determineConstructorsFromBeanPostProcessors(beanClass, beanName); => SmartInstantiationAwareBeanPostProcessor.determineCandidateConstructors(beanClass, beanName); @Autowired 构造器

    3. bean instance after

      • bean instance after merge bean query
        applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); => MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition(mbd, beanType, beanName);

        1. AutowiredAnnotationBeanPostProcessor @Autowired @Value
        2. CommonAnnotationBeanPostProcessor @Resource @PostConstruct @PreDestroy
      • bean instance after earlybean
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); => SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(exposedObject, beanName);

    4. bean field setting
      属性填充 populateBean(beanName, mbd, instanceWrapper);

      • 属性填充 before => InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)
      • 属性填充 after => field属性 和 setter方式
        InstantiationAwareBeanPostProcessor.postProcessProperties(pvs, bw.getWrappedInstance(), beanName)
        InstantiationAwareBeanPostProcessor.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
    5. bean initialize

      • bean initialize before
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); =>

        1. InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(result, beanName); initmethod @PostConstruct
        2. ApplicationContextAwareProcessor.postProcessBeforeInitialization(result, beanName); Aware: ApplicationContextAware、ApplicationEventPublisherAware、EnvironmentAware
      • bean initialize before
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); =>

        1. AbstractAutoProxyCreator.postProcessAfterInitialization(result, beanName); AOP (wrapIfNecessary->createAopProxy().getProxy(classLoader); Cglib/Jdk)
      • bean initialize register distroy
        registerDisposableBeanIfNecessary(beanName, bean, mbd); => DestructionAwareBeanPostProcessor

    6. bean create after
      destroySingleton(beanName) => DestructionAwareBeanPostProcessor#postProcessBeforeDestruction(this.bean, this.beanName);

    Spring扩展点梳理

    • BeanFactoryPostProcessor
      • BeanDefinitionRegistryPostProcessor
        • postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) # 注册Bean 定义
    • BeanPostProcessor
      • InstantiationAwareBeanPostProcessor
        • postProcessBeforeInstantiation
        • postProcessAfterInstantiation
        • postProcessProperties
        • postProcessPropertyValues
      • AbstractAutoProxyCreator
        • postProcessAfterInitialization # 实例化之后AOP支持
    • @Import
      • ImportBeanDefinitionRegistrar
        • registerBeanDefinitions
      • ImportSelector
        • String[] selectImports(AnnotationMetadata importingClassMetadata)
        • default Predicate getExclusionFilter() { return null;}
    • Aware
      • ApplicationContextAware
        • setApplicationContext(ApplicationContext applicationContext)
      • BeanFactoryAware
        • setBeanFactory(BeanFactory beanFactory)
    • InitializingBean || @PostConstruct
    • FactoryBean (动态创建Bean)(&beanName factoryBean.getObject | beanName beanFactory.getBean)(Mybatis#mapper、Openfeign)
      • T getObject() throws Exception;
      • Class getObjectType();
      • default boolean isSingleton() { return true; }
    • SmartInitializingSingleton Bean
      • void afterSingletonsInstantiated(); # 初始化之后调用 过滤器…
    • ApplicationListener
      • void onApplicationEvent(E event);
    • Lifecycle
      • SmartLifecycle
        • default boolean isAutoStartup() { return true; }
        • default void stop(Runnable callback)
      • LifecycleProcessor
    • HandlerInterceptor
      • default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
      • default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception
      • default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,@Nullable Exception ex) throws Exception
    • MethodInterceptor
      • Object invoke(MethodInvocation invocation) throws Throwable;

    Spring扩展点应用场景

    整合Nacos 服务注册

    ApplicationListener扩展场景——监听容器中发布的事件

    NamingService -> NacosNamingService => registerInstance

    思考: 为什么整合Nacos注册中心后,服务启动就会自动注册,Nacos是如何实现自动服务注册的?

    1. Tomcat启动之后, 在ServletWebServerApplicationContext中,发布事件 ServletWebServerInitializedEvent

    2. spring-cloud-commons 包 AbstractAutoServiceRegistration 监听 WebServerInitializedEvent

    public abstract class AbstractAutoServiceRegistration<R extends Registration>
    		implements AutoServiceRegistration, ApplicationContextAware,
    		ApplicationListener<WebServerInitializedEvent> {
    
    	public void onApplicationEvent(WebServerInitializedEvent event) {
    		bind(event); // .... register();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. Nacos 实现ServiceRegistry.register

    Lifecycle Nacos 发布订阅 & Eureka 服务启动、同步、剔除

    Lifecycle扩展场景——管理具有启动、停止生命周期需求的对象

    start => AbstractApplicationContext.refresh -> finishRefresh() -> getLifecycleProcessor().onRefresh(); -> start()

    整合Nacos 发布订阅 NacosWatch

    public class NacosWatch
    		implements ApplicationEventPublisherAware, SmartLifecycle, DisposableBean {
    
    
    	@Override
    	public void start() {
    			...
    			NamingService namingService = nacosServiceManager
    					.getNamingService(properties.getNacosProperties());
    			try {
    				namingService.subscribe(properties.getService(), properties.getGroup(),
    						Arrays.asList(properties.getClusterName()), eventListener);
    			}
    			catch (Exception e) {
    				log.error("namingService subscribe failed, properties:{}", properties, e);
    			}
    
    			this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
    					this::nacosServicesWatch, this.properties.getWatchDelay());
    		}
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    整合Eureka 注册

    扩展: Eureka Server端上下文的初始化是在SmartLifecycle#start中实现的

    EurekaServerInitializerConfiguration

    整合Ribbon

    SmartInitializingSingleton扩展场景—— 对容器中的Bean对象进行定制处理

    所有的单例bean实例化之后调用

    思考:为什么@Bean修饰的RestTemplate加上@LoadBalanced就能实现负载均衡功能?

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    LoadBalancerAutoConfiguration
    对SmartInitializingSingleton的扩展,为所有用@LoadBalanced修饰的restTemplate(利用了@Qualifier 限定符)绑定实现了负载均衡逻辑的拦截器LoadBalancerInterceptor

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();
    
    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
    		final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
    	return () -> restTemplateCustomizers.ifAvailable(customizers -> {
    		for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
    			for (RestTemplateCustomizer customizer : customizers) {
    				customizer.customize(restTemplate);
    			}
    		}
    	});
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    /**
     * Annotation to mark a RestTemplate or WebClient bean to be configured to use a
     * LoadBalancerClient.
     * @author Spencer Gibb
     */
    @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Qualifier //Qualifier
    public @interface LoadBalanced {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    LoadBalancerInterceptor

    	@Configuration(proxyBeanMethods = false)
    	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    	static class LoadBalancerInterceptorConfig {
    
    		@Bean
    		public LoadBalancerInterceptor loadBalancerInterceptor(
    				LoadBalancerClient loadBalancerClient,
    				LoadBalancerRequestFactory requestFactory) {
    			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    		}
    
    		@Bean
    		@ConditionalOnMissingBean
    		public RestTemplateCustomizer restTemplateCustomizer(
    				final LoadBalancerInterceptor loadBalancerInterceptor) {
    			return restTemplate -> {
    				List<ClientHttpRequestInterceptor> list = new ArrayList<>(
    						restTemplate.getInterceptors());
    				list.add(loadBalancerInterceptor);
    				restTemplate.setInterceptors(list);
    			};
    		}
    
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    整合Feign

    FactoryBean的扩展场景——将接口生成的代理对象交给Spring管理

    思考:为什么Feign接口可以通过@Autowired直接注入使用?Feign接口是如何交给Spring管理的?

    @FeignClient(value = "order-service", path = "/order")
    public interface OrderFeignService {
    
        @GetMapping(value = "/info/{productId}")
        public String findOrderInfoByProductId(@PathVariable("productId") Integer productId);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    @SpringBootApplication
    @EnableFeignClients(basePackages = "com.mx.use.feign")
    @EnableHystrix
    @EnableHystrixDashboard //开启 Hystrix 监控功能
    public class OrderServiceApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(OrderServiceApplication.class, args);
        }
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    FactoryBean => FeignClientsRegistrar -> scan(FeignClient) -> registerFeignClient -> FeignClientFactoryBean

    在这里插入图片描述

    @Override
    public Object getObject() {
    	return getTarget(); //
    }
    
    • 1
    • 2
    • 3
    • 4

    整合sentinel

    HandlerInterceptor扩展场景——对mvc请求增强

    AbstractSentinelInterceptor#preHandle

    public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
        ...
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
            try {
                String resourceName = getResourceName(request);
    
                if (StringUtil.isEmpty(resourceName)) {
                    return true;
                }
                
                if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
                    return true;
                }
                
                // Parse the request origin using registered origin parser.
                String origin = parseOrigin(request);
                String contextName = getContextName(request);
                ContextUtil.enter(contextName, origin);
                //Sentinel入口
                Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
                request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
                return true;
            } catch (BlockException e) {
                try {
                    handleBlockException(request, response, e);
                } finally {
                    ContextUtil.exit();
                }
                return false;
            }
        }
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    SmartInitializingSingleton&FactoryBean结合场景——根据类型动态装配对象

    SentinelDataSourceHandler

    #Sentinel持久化读数据源设计,利用了SmartInitializingSingleton扩展点
    SentinelDataSourceHandler#afterSingletonsInstantiated
    # 注册一个FactoryBean类型的数据源 
    》SentinelDataSourceHandler#registerBean
    》》NacosDataSourceFactoryBean#getObject
    # 利用FactoryBean获取到读数据源
    》》new NacosDataSource(properties, groupId, dataId, converter)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    NacosDataSourceFactoryBean

    public class NacosDataSourceFactoryBean implements FactoryBean<NacosDataSource> {
    	...
    	@Override
    	public NacosDataSource getObject() throws Exception {
    		...
    		return new NacosDataSource(properties, groupId, dataId, converter);
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    SentinelDataSourceHandler

    public class SentinelDataSourceHandler implements SmartInitializingSingleton {
    	...
    	@Override
    	public void afterSingletonsInstantiated() {
    		...
    		//sentinel.nacos.config.serverAddr=${sentinel.nacos.config.serverAddr}
    		//NacosDataSource
    		AbstractDataSourceProperties abstractDataSourceProperties = dataSourceProperties.getValidDataSourceProperties();
    		abstractDataSourceProperties.setEnv(env);
    		abstractDataSourceProperties.preCheck(dataSourceName);
    		registerBean(abstractDataSourceProperties, dataSourceName+ "-sentinel-" + validFields.get(0) + "-datasource"); //NacosDataSource
    		...
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    整合seata (Spring Aop)

    AbstractAutoProxyCreator&MethodInterceptor结合场景——实现方法增强

    GlobalTransactionScanner

    在这里插入图片描述

    GlobalTransactionalInterceptor

    在这里插入图片描述

    public class GlobalTransactionalInterceptor implements ConfigurationChangeListener, MethodInterceptor, SeataInterceptor {
    	...
    
        @Override
        public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
            Class<?> targetClass =
                methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null;
            Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
            if (specificMethod != null && !specificMethod.getDeclaringClass().equals(Object.class)) {
                final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);
                final GlobalTransactional globalTransactionalAnnotation =
                    getAnnotation(method, targetClass, GlobalTransactional.class);
                final GlobalLock globalLockAnnotation = getAnnotation(method, targetClass, GlobalLock.class);
                boolean localDisable = disable || (degradeCheck && degradeNum >= degradeCheckAllowTimes);
                if (!localDisable) {
                    if (globalTransactionalAnnotation != null || this.aspectTransactional != null) {
                        AspectTransactional transactional;
                        if (globalTransactionalAnnotation != null) {
                            transactional = new AspectTransactional(globalTransactionalAnnotation.timeoutMills(),
                                globalTransactionalAnnotation.name(), globalTransactionalAnnotation.rollbackFor(),
                                globalTransactionalAnnotation.noRollbackForClassName(),
                                globalTransactionalAnnotation.noRollbackFor(),
                                globalTransactionalAnnotation.noRollbackForClassName(),
                                globalTransactionalAnnotation.propagation(),
                                globalTransactionalAnnotation.lockRetryInterval(),
                                globalTransactionalAnnotation.lockRetryTimes());
                        } else {
                            transactional = this.aspectTransactional;
                        }
                        // 执行事务 transactionalTemplate.execute()
                        return handleGlobalTransaction(methodInvocation, transactional);
                    } else if (globalLockAnnotation != null) {
                        return handleGlobalLock(methodInvocation, globalLockAnnotation);
                    }
                }
            }
            return methodInvocation.proceed();
        }
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    public class TransactionalTemplate {
        public Object execute(TransactionalExecutor business) throws Throwable {
            // 1. Get transactionInfo
            TransactionInfo txInfo = business.getTransactionInfo();
            if (txInfo == null) {
                throw new ShouldNeverHappenException("transactionInfo does not exist");
            }
            // 1.1 Get current transaction, if not null, the tx role is 'GlobalTransactionRole.Participant'.
            GlobalTransaction tx = GlobalTransactionContext.getCurrent();
    
            // 1.2 Handle the transaction propagation.
            Propagation propagation = txInfo.getPropagation();
            SuspendedResourcesHolder suspendedResourcesHolder = null;
            try {
                switch (propagation) {
                    case NOT_SUPPORTED:
                        // If transaction is existing, suspend it.
                        if (existingTransaction(tx)) {
                            suspendedResourcesHolder = tx.suspend();
                        }
                        // Execute without transaction and return.
                        return business.execute();
                    case REQUIRES_NEW:
                        // If transaction is existing, suspend it, and then begin new transaction.
                        if (existingTransaction(tx)) {
                            suspendedResourcesHolder = tx.suspend();
                            tx = GlobalTransactionContext.createNew();
                        }
                        // Continue and execute with new transaction
                        break;
                    case SUPPORTS:
                        // If transaction is not existing, execute without transaction.
                        if (notExistingTransaction(tx)) {
                            return business.execute();
                        }
                        // Continue and execute with new transaction
                        break;
                    case REQUIRED:
                        // If current transaction is existing, execute with current transaction,
                        // else continue and execute with new transaction.
                        break;
                    case NEVER:
                        // If transaction is existing, throw exception.
                        if (existingTransaction(tx)) {
                            throw new TransactionException(
                                String.format("Existing transaction found for transaction marked with propagation 'never', xid = %s"
                                        , tx.getXid()));
                        } else {
                            // Execute without transaction and return.
                            return business.execute();
                        }
                    case MANDATORY:
                        // If transaction is not existing, throw exception.
                        if (notExistingTransaction(tx)) {
                            throw new TransactionException("No existing transaction found for transaction marked with propagation 'mandatory'");
                        }
                        // Continue and execute with current transaction.
                        break;
                    default:
                        throw new TransactionException("Not Supported Propagation:" + propagation);
                }
    
                // 1.3 If null, create new transaction with role 'GlobalTransactionRole.Launcher'.
                if (tx == null) {
                    tx = GlobalTransactionContext.createNew();
                }
    
                // set current tx config to holder
                GlobalLockConfig previousConfig = replaceGlobalLockConfig(txInfo);
    
                try {
                    // 2. If the tx role is 'GlobalTransactionRole.Launcher', send the request of beginTransaction to TC,
                    //    else do nothing. Of course, the hooks will still be triggered.
                    beginTransaction(txInfo, tx);
    
                    Object rs;
                    try {
                        // Do Your Business
                        rs = business.execute();
                    } catch (Throwable ex) {
                        // 3. The needed business exception to rollback.
                        completeTransactionAfterThrowing(txInfo, tx, ex);
                        throw ex;
                    }
    
                    // 4. everything is fine, commit.
                    commitTransaction(tx);
    
                    return rs;
                } finally {
                    //5. clear
                    resumeGlobalLockConfig(previousConfig);
                    triggerAfterCompletion();
                    cleanUp();
                }
            } finally {
                // If the transaction is suspended, resume it.
                if (suspendedResourcesHolder != null) {
                    tx.resume(suspendedResourcesHolder);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
  • 相关阅读:
    使用Nginx搭建流媒体
    体验亚马逊的 CodeWhisperer 感觉
    电脑提示iutils.dll丢失的解决方法,分享5种靠谱的解决方法
    断点检测学习
    作为高级架构师,你居然看不透大型网站技术架构的核心问题?
    【scikit-learn基础】--『回归模型评估』之损失分析
    MySQL理论基础篇
    linux驱动开发 使用设备树编写一个led驱动程序
    【windows Docker 安装mysql:只需3条命令】
    Centos7 安装部署Kubernetes(k8s)集群实现过程
  • 原文地址:https://blog.csdn.net/menxu_work/article/details/128000900