一、简介

  • Spring早期是通过实现ApplicationListener接口来定义监听事件,在spring4.2的时候开始我们可以通过@EventListener注解来定义监听事件,ApplicationListener接口定义如下:
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {/*** 处理Spring监听事件,监听 ApplicationEvent 及其下面的子事件* 这里的ApplicationListener接口相当于观察者模式中的观察者Obverse接口,* 且此处使用的是观察者模式的“推-拉”模型中的“拉”模型, 主题对象在通知观察者* 的时候,只传递少量信息。如果观察者需要更具体的信息, 由观察者主动到主题对* 象中获取,相当于是观察者从主题对象中拉数据。一般这种 模型的实现中,会把主* 题对 象自身通过update()方法传递给观察者,这样在观察 者需要获取数据的时候,* 就可以通过这个引用来获取了。* Handle an application event.* @param event the event to respond to*/void onApplicationEvent(E event);
}
  • Spring为我们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式(拉模型);为的就是业务系统逻辑的解耦,提高可扩展性以及可维护性。事件发布者并不需要考虑谁去监听,监听具体的实现内容是什么,发布者的工作只是为了发布事件而已。
  • 比如在我们的系统中,我们需要记录某一些比较重要方法的调用日志,我们就可以通过自定义注解+实现自定义监听事件即可。
  • 本篇文章我们通过@EventListener注解来分析Spring的事件注册及监听流程

二、使用@EventListener注解

  1. 建立事件对象,当调用publishEvent方法是会通过这个bean对象找对应事件的监听。
package com.asiainfo.gridtask.event;import com.asiainfo.gridtask.entity.log.SysLog;
import org.springframework.context.ApplicationEvent;/*** 系统日志事件,ApplicationEvent相当于观察者模式中的Subject主题对象,Spring容器* 发布监听事件后,* @author Jack.Cheng* @date 2020/1/7 15:04**/
public class SysLogEvent extends ApplicationEvent {public SysLogEvent(SysLog sysLog) {super(sysLog);}
}
  • 看一看SysLogEvent类的继承图,SysLogEvent继承至ApplicationEvent,ApplicationEvent 继承至EventObject,EventObject对象中定义了一个Object类型的source变量用于存放事件的消息。
  • 新增对应的监听类
package com.asiainfo.gridtask.event;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.asiainfo.gridtask.common.constant.CommonConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;/*** @author Jack.Cheng* @date 2020/1/7 15:08**/
@Slf4j
@Component
public class SysLogListener {@EventListener(SysLogEvent.class)public void saveSysLog(SysLogEvent event) {log.info("收到调用日志信息:info:{}" , JSON.toJSONString(event));}
}
  • 建立对应的测试类
package com.asiainfo.gridtask.controller;import com.asiainfo.gridtask.entity.log.SysLog;
import com.asiainfo.gridtask.event.SysLogEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author Jack.Cheng* @date 2020/4/7 16:34**/
@RestController
public class TestController {@Autowiredprivate ApplicationContext applicationContext;@GetMapping("testEvent.do")public void testEvent(){SysLog sysLog = new SysLog();sysLog.setLogId("123456789").setStaffCode("jack").setPhoneNo("13378224441");applicationContext.publishEvent(new SysLogEvent(sysLog));}
}
  • 调用Restful接口后结果如下:

三、源码解析

  • AnnotationConfigUtils#registerAnnotationConfigProcessors注册了EventListenerMethodProcessor 的BeanDefinition信息, 初始化SpringIOC容器的时候会将EventListenerMethodProcessor注册到容器中。
  • AnnotationConfigUtils是在AnnotationConfigServletWebServerApplicationContext构造方法里被加载。AnnotationConfigServletWebServerApplicationContext,他是spring boot启动入口的重要类(我这里用的是spring boot所以是这个类),可以相当于用xml的ClassPathXmlApplicationContext。
AnnotationConfigUtils类/*** 内部管理@EventListener注解处理器的bean名称* The bean name of the internally managed @EventListener annotation processor.*/public static final String EVENT_LISTENER_PROCESSOR_BEAN_NAME ="org.springframework.context.event.internalEventListenerProcessor";public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {...........省略...............// 注册EventListenerMethodProcessor对象if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));}if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));}return beanDefs;}
  • 注册的EventListenerMethodProcessor对象会在初始化非懒加载对象的时候执行它的afterSingletonsInstantiated方法。这里通过AbstractApplicationContext类的refresh() 方法中的 finishBeanFactoryInitialization(beanFactory) 去做初始化。
  • AbstractApplicationContext#refresh()
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// 记录容器的启动时间、标记“已启动”状态、检查环境变量prepareRefresh();// 初始化BeanFactory容器(DefaultListableBeanFactory)、注册BeanDefinitionConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// 设置 BeanFactory 的类加载器,添加几个 BeanPostProcessor,手动注册几个特殊的 beanprepareBeanFactory(beanFactory);try {// 扩展点,具体逻辑由子类去实现postProcessBeanFactory(beanFactory);// 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法invokeBeanFactoryPostProcessors(beanFactory);// 注册 BeanPostProcessor 的实现类registerBeanPostProcessors(beanFactory);// 初始化MessageSourceinitMessageSource();// 注册Spring事件派发多播器initApplicationEventMulticaster();// 扩展点,交由子类实现onRefresh();// 注册事件监听器registerListeners();// 初始化所有的 singleton beansfinishBeanFactoryInitialization(beanFactory);// 广播事件finishRefresh();}catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("Exception encountered during context initialization - " +"cancelling refresh attempt: " + ex);}// 销毁已经初始化的的BeandestroyBeans();// 设置 'active' 状态cancelRefresh(ex);throw ex;}finally {// 清除缓存resetCommonCaches();}}
}
  • 这里我们重点关注refresh()#finishBeanFactoryInitialization(beanFactory)#preInstantiateSingletons() 方法
 protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {// Initialize conversion service for this context.if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {beanFactory.setConversionService(beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));}// Register a default embedded value resolver if no bean post-processor// (such as a PropertyPlaceholderConfigurer bean) registered any before:// at this point, primarily for resolution in annotation attribute values.if (!beanFactory.hasEmbeddedValueResolver()) {beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));}// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);for (String weaverAwareName : weaverAwareNames) {getBean(weaverAwareName);}// Stop using the temporary ClassLoader for type matching.beanFactory.setTempClassLoader(null);// Allow for caching all bean definition metadata, not expecting further changes.beanFactory.freezeConfiguration();// 初始化非懒加载对象beanFactory.preInstantiateSingletons();}
  • DefaultListableBeanFactory#preInstantiateSingletons()
 @Overridepublic void preInstantiateSingletons() throws BeansException {..........省略非必要代码...........// 将注册的beanDefinition类信息封装到集合中List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);..........省略非必要代码...........// 触发所有适用bean的初始化后回调 主要是afterSingletonsInstantiated方法for (String beanName : beanNames) {Object singletonInstance = getSingleton(beanName);/*** 处理SmartInitializingSingleton的实现类,调用其afterSingletonsInstantiated()方法,该方法* 会将带有EventListener注解的方法包装为ApplicationListenerMethodAdapter类,Spring容器发布* 事件后将通过多播器触发调用这个类的onApplicationEvent(ApplicationEvent event)方法,这个方法* 最终会通过反射的方式对应的调用我们加了EventListener注解的方法,最终完成事件的发布调用流程*/if (singletonInstance instanceof SmartInitializingSingleton) {final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;if (System.getSecurityManager() != null) {AccessController.doPrivileged((PrivilegedAction<Object>) () -> {smartSingleton.afterSingletonsInstantiated();return null;}, getAccessControlContext());}else {// 调用afterSingletonsInstantiated方法(EventListenerMethodProcessor类)smartSingleton.afterSingletonsInstantiated();}}}}
  • EventListenerMethodProcessor类图如下,这里可以看到其实现了SmartInitializingSingleton接口
  • EventListenerMethodProcessor#afterSingletonsInstantiated,敲黑板,这里开始注册带有@EventListener注解的方法了
 @Overridepublic void afterSingletonsInstantiated() {// 获取EventListenerFactory工厂类ConfigurableListableBeanFactory beanFactory = this.beanFactory;Assert.state(this.beanFactory != null, "No ConfigurableListableBeanFactory set");String[] beanNames = beanFactory.getBeanNamesForType(Object.class);for (String beanName : beanNames) {if (!ScopedProxyUtils.isScopedTarget(beanName)) {Class<?> type = null;try {type = AutoProxyUtils.determineTargetClass(beanFactory, beanName);}catch (Throwable ex) {// An unresolvable bean type, probably from a lazy bean - let's ignore it.if (logger.isDebugEnabled()) {logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);}}if (type != null) {if (ScopedObject.class.isAssignableFrom(type)) {try {Class<?> targetClass = AutoProxyUtils.determineTargetClass(beanFactory, ScopedProxyUtils.getTargetBeanName(beanName));if (targetClass != null) {type = targetClass;}}catch (Throwable ex) {// An invalid scoped proxy arrangement - let's ignore it.if (logger.isDebugEnabled()) {logger.debug("Could not resolve target bean for scoped proxy '" + beanName + "'", ex);}}}try {// 重点是这个方法处理BeanprocessBean(beanName, type);}catch (Throwable ex) {throw new BeanInitializationException("Failed to process @EventListener " +"annotation on bean with name '" + beanName + "'", ex);}}}}}
  • EventListenerMethodProcessor#processBean,这里会将带有EventListener注解的方法包装为ApplicationListenerMethodAdapter类,Spring容器在发布事件后会通过多播器触发调用这个类的onApplicationEvent(ApplicationEvent event)方法,这个方法最终会通过反射的方式对应的调用我们加了EventListener注解的方法,最终完成整个事件的发布调用流程。
private void processBean(final String beanName, final Class<?> targetType) {if (!this.nonAnnotatedClasses.contains(targetType) && !isSpringContainerClass(targetType)) {Map<Method, EventListener> annotatedMethods = null;try {// 拿到使用了@EventListener注解的方法annotatedMethods = MethodIntrospector.selectMethods(targetType,(MethodIntrospector.MetadataLookup<EventListener>) method ->AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class));}catch (Throwable ex) {// An unresolvable type in a method signature, probably from a lazy bean - let's ignore it.if (logger.isDebugEnabled()) {logger.debug("Could not resolve methods for bean with name '" + beanName + "'", ex);}}if (CollectionUtils.isEmpty(annotatedMethods)) {this.nonAnnotatedClasses.add(targetType);if (logger.isTraceEnabled()) {logger.trace("No @EventListener annotations found on bean class: " + targetType.getName());}}else {// Non-empty set of methodsConfigurableApplicationContext context = this.applicationContext;Assert.state(context != null, "No ApplicationContext set");List<EventListenerFactory> factories = this.eventListenerFactories;Assert.state(factories != null, "EventListenerFactory List not initialized");for (Method method : annotatedMethods.keySet()) {for (EventListenerFactory factory : factories) {// 判断是否支持该方法  这里用的DefaultEventListenerFactory spring5.0.8 写死的返回trueif (factory.supportsMethod(method)) {// 获取类上标注了@EventListener注解的方法Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));// 将该类和方法包装为ApplicationListenerMethodAdapter对象ApplicationListener<?> applicationListener = factory.createApplicationListener(beanName, targetType, methodToUse);if (applicationListener instanceof ApplicationListenerMethodAdapter) {// 如果是ApplicationListenerMethodAdapter对象 就把context和evaluator传进去((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator);}// 添加到ApplicationListener事件Set集合中去context.addApplicationListener(applicationListener);break;}}}if (logger.isDebugEnabled()) {logger.debug(annotatedMethods.size() + " @EventListener methods processed on bean '" +beanName + "': " + annotatedMethods);}}}
}// 封装ApplicationListenerMethodAdapter对象
public ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) {return new ApplicationListenerMethodAdapter(beanName, type, method);
}
  • ApplicationListenerMethodAdapter类的UML图
  • ApplicationListenerMethodAdapter类的属性及部分关键方法。
public class ApplicationListenerMethodAdapter implements GenericApplicationListener {protected final Log logger = LogFactory.getLog(getClass());// 当前监听器在容器中类名,Spring容器可以通过beanName获取该类private final String beanName;// 监听器中被@EventListener注解修饰的方法private final Method method;private final Method targetMethod;private final AnnotatedElementKey methodKey;private final List<ResolvableType> declaredEventTypes;@Nullableprivate final String condition;private final int order;@Nullableprivate ApplicationContext applicationContext;@Nullableprivate EventExpressionEvaluator evaluator;.........省略不相关方法.........../*** 该方法是实现了ApplicationListener接口的onApplicationEvent方法,当ApplicationContext容器* publishEvent事件后,最后具体执行的方法,相当于观察者模式中的ConcreteObverse对象实现* Obverse接口的方法。*/@Overridepublic void onApplicationEvent(ApplicationEvent event) {processEvent(event);}/*** Process the specified {@link ApplicationEvent}, checking if the condition* match and handling non-null result, if any.*/public void processEvent(ApplicationEvent event) {// 解析ApplicationContext发布的事件参数信息Object[] args = resolveArguments(event);if (shouldHandle(event, args)) {// 通过反射的形式执行通过@EventListener注解修饰的方法Object result = doInvoke(args);if (result != null) {handleResult(result);}else {logger.trace("No result object given - no result to handle");}}}@Nullableprotected Object doInvoke(Object... args) {//获取@EventListener注解修饰方法所在的BeanObject bean = getTargetBean();//将@EventListener注解修饰方法的权限设置可访问ReflectionUtils.makeAccessible(this.method);try {//通过反射执行该方法return this.method.invoke(bean, args);}catch (IllegalArgumentException ex) {assertTargetBean(this.method, bean, args);throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex);}catch (IllegalAccessException ex) {throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex);}catch (InvocationTargetException ex) {// Throw underlying exceptionThrowable targetException = ex.getTargetException();if (targetException instanceof RuntimeException) {throw (RuntimeException) targetException;}else {String msg = getInvocationErrorMessage(bean, "Failed to invoke event listener method", args);throw new UndeclaredThrowableException(targetException, msg);}}}// 获取目标类protected Object getTargetBean() {Assert.notNull(this.applicationContext, "ApplicationContext must no be null");return this.applicationContext.getBean(this.beanName);}
}
  • 最后面就是触发事件监听了AbstractApplicationContext#publishEvent
public abstract class AbstractApplicationContext extends DefaultResourceLoaderimplements ConfigurableApplicationContext {.........省略非必要代码........@Overridepublic void publishEvent(ApplicationEvent event) {publishEvent(event, null);}/*** Publish the given event to all listeners.* @param event the event to publish (may be an {@link ApplicationEvent}* or a payload object to be turned into a {@link PayloadApplicationEvent})* @param eventType the resolved event type, if known* @since 4.2*/protected void publishEvent(Object event, @Nullable ResolvableType eventType) {Assert.notNull(event, "Event must not be null");// Decorate event as an ApplicationEvent if necessaryApplicationEvent applicationEvent;if (event instanceof ApplicationEvent) {applicationEvent = (ApplicationEvent) event;}else {applicationEvent = new PayloadApplicationEvent<>(this, event);if (eventType == null) {eventType = ((PayloadApplicationEvent) applicationEvent).getResolvableType();}}// Multicast right now if possible - or lazily once the multicaster is initializedif (this.earlyApplicationEvents != null) {this.earlyApplicationEvents.add(applicationEvent);}else {// 进入multicastEventgetApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);}// Publish event via parent context as well...if (this.parent != null) {if (this.parent instanceof AbstractApplicationContext) {((AbstractApplicationContext) this.parent).publishEvent(event, eventType);}else {this.parent.publishEvent(event);}}}
}
  • SimpleApplicationEventMulticaster#multicastEvent->invokeListener->doInvokeListener
 @Overridepublic void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));//获取所有监听器,遍历,广播事件for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {//如果自定义实现了SimpleApplicationEventMulticaster类,并设置了线程池//则通过线程池异步的广播事件Executor executor = getTaskExecutor();if (executor != null) {executor.execute(() -> invokeListener(listener, event));}else {//未实现线程池,同步的执行广播事件invokeListener(listener, event);}}}//invokeListener方法,调用此类的doInvokeListener方法protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {ErrorHandler errorHandler = getErrorHandler();if (errorHandler != null) {try {doInvokeListener(listener, event);}catch (Throwable err) {errorHandler.handleError(err);}}else {doInvokeListener(listener, event);}}@SuppressWarnings({"unchecked", "rawtypes"})private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {try {//由实现了ApplicationListener接口的类执行该方法,这里是//ApplicationListenerMethodAdapter类,该方法在上面已经详细讲解过了listener.onApplicationEvent(event);}catch (ClassCastException ex) {String msg = ex.getMessage();if (msg == null || matchesClassCastMessage(msg, event.getClass())) {// Possibly a lambda-defined listener which we could not resolve the generic event type for// -> let's suppress the exception and just log a debug message.Log logger = LogFactory.getLog(getClass());if (logger.isDebugEnabled()) {logger.debug("Non-matching event type for listener: " + listener, ex);}}else {throw ex;}}}
  • 到这里整个事件监听的方法都已执行完毕,本篇内容为博主的第一篇博客,限于博主知识水平有限,如有错误欢迎大家及时指正,谢谢大家。

Spring事件监听流程分析【源码浅析】相关推荐

  1. Spring5源码 - 11 Spring事件监听机制_源码篇

    文章目录 pre 事件监听机制的实现原理[观察者模式] 事件 ApplicationEvent 事件监听者 ApplicationEvent 事件发布者 ApplicationEventMultica ...

  2. Spring5源码 - 12 Spring事件监听机制_异步事件监听应用及源码解析

    文章目录 Pre 实现原理 应用 配置类 Event事件 事件监听 EventListener 发布事件 publishEvent 源码解析 (反推) Spring默认的事件广播器 SimpleApp ...

  3. 监听返回app_基于 Redis 消息队列实现 Laravel 事件监听及底层源码探究

    在 Laravel 中,除了使用 dispatch 辅助函数通过 Illuminate\Bus\Dispatcher 显式推送队列任务外,还可以通过事件监听的方式隐式进行队列任务推送,在这个场景下,事 ...

  4. Spring5源码 - 13 Spring事件监听机制_@EventListener源码解析

    文章目录 Pre 概览 开天辟地的时候初始化的处理器 @EventListener EventListenerMethodProcessor afterSingletonsInstantiated 小 ...

  5. spring 事件监听

    用一个简单的例子来实现spring事件监听的功能 这个例子主要功能是,记录那些用户是第一次登入系统,如果用户是第一次登入系统,则调用spring的事件监听,记录这些用户. 主要用到的spring的类和 ...

  6. Spring事件监听原理

    1 简述Spring的生命周期 不论是Spring的监听机制原理还是Spring AOP的原理,都是依托于Spring的生命周期,所以要了解Spring的监听机制原理就需要先了解Spring的生命周期 ...

  7. Spring5源码 - 10 Spring事件监听机制_应用篇

    文章目录 Spring事件概览 事件 自定义事件 事件监听器 基于接口 基于注解 事件广播器 Spring事件概览 Spring事件体系包括三个组件:事件,事件监听器,事件广播器 事件 Spring的 ...

  8. Struts流程分析+源码分析

    1.初始化工作 读取配置---转换器-----读取插件 当struts-config.xml配置文件加载到内存,则会创建两个map:ActionConfigs,FromBeans.这两个map都交由M ...

  9. spring 扫描所有_自定义Spring事件监听机制

    开头提醒一下大家: 尽管我简化了Spring源码搞了个精简版的Spring事件机制,但是没接触过Spring源码的朋友阅读起来还是有很大难度,请复制代码到本地,边Debug边看 既然要简化代码,所以不 ...

最新文章

  1. 使用Wireshark进行DNS协议解析
  2. Unix环境高级编程(二十一)数据库函数库
  3. 二进制的mysql怎么装_使用二进制演示MySQL安装步骤
  4. 【大牛疯狂教学】java程序员大专找不到工作
  5. openstack 重启mysql_突然断电导致mariadb数据库无法启动(openstack 命令无法使用)...
  6. MVC模式 在Java Web应用程序中的实现
  7. linux网站权限一直自动关闭,奇妙伞-解决SELinux对网站目录权限控制的不当的问题--网上摘抄集合,记录使用...
  8. 现在当兵有什么待遇复原以后_当兵多少年最好呢?这些关键点会影响在部队发展,很重要、很实用...
  9. 第五章 列表、元组和字符串[DDT书本学习 小甲鱼]【8】
  10. 玩转Spring Boot 集成Dubbo
  11. 外设、总线、接口概念辨析
  12. Subclipse更新地址
  13. PreScan快速入门到精通第三讲快速搭建第一个自动驾驶仿真模型
  14. visual studio 总是和搜狗输入法冲突
  15. android开发塔防游戏机,上手快又耐玩 五款Android平台塔防类游戏推荐
  16. Deepfake——深度造假视频在智能城市中的风险
  17. Python实现rosbag转换成video
  18. 纸质合同为什么要升级为电子合同?区别在哪?
  19. Mybatis方法入参处理
  20. 美国CPSIA关于玩具和儿童产品的测试要求,CPC证书要求

热门文章

  1. Jmeter 安装及使用教程
  2. 失信企业查询_在不丢失信标的情况下找到信标:我进入Android低功耗蓝牙领域的旅程...
  3. 群多多社群人脉H5-2.1.4多开插件+小程序独立前端+搭建教程
  4. vue项目webpack配置全局变量
  5. 三分钟快速了解什么是MES系统
  6. 服务器上搭建MSSQL 服务器
  7. 血氧饱和检测仪————TFT方案设计
  8. 用计算机打字打错了怎么办,笔记本键盘输入错误怎么办
  9. 实时音视频技术难点及解决方案
  10. 谈谈Windows程序中的字符编码