Spring的事件监听机制
在讲解事件监听机制前,我们先回顾下设计模式中的观察者模式,因为事件监听机制可以说是在典型观察者模式基础上的进一步抽象和改进。我们可以在JDK或者各种开源框架比如Spring中看到它的身影,从这个意义上说,事件监听机制也可以看做是一种对传统观察者模式的具体实现,不同的框架对其实现方式会有些许差别。
典型的观察者模式将有依赖关系的对象抽象为了观察者和主题两个不同的角色,多个观察者同时观察一个主题,两者只通过抽象接口保持松耦合状态,这样双方可以相对独立的进行扩展和变化:比如可以很方便的增删观察者,修改观察者中的更新逻辑而不用修改主题中的代码。但是这种解耦进行的并不彻底,这具体体现在以下几个方面:
- ① 抽象主题需要依赖抽象观察者,而这种依赖关系完全可以去除。
- ② 主题需要维护观察者列表,并对外提供动态增删观察者的接口。
- ③ 主题状态改变时需要由自己去通知观察者进行更新。
我们可以把主题(Subject)替换成事件(event),把对特定主题进行观察的观察者(Observer)替换成对特定事件进行监听的监听器(EventListener),而把原有主题中负责维护主题与观察者映射关系以及在自身状态改变时通知观察者的职责从中抽出,放入一个新的角色事件发布器(EventPublisher)中,事件监听模式的轮廓就展现在了我们眼前,如下图所示:
常见事件监听机制的主要角色如下:
- 事件及事件源:对应于观察者模式中的主题。事件源发生某事件是特定事件监听器被触发的原因。
- 事件监听器:对应于观察者模式中的观察者。监听器监听特定事件,并在内部定义了事件发生后的响应逻辑。
- 事件发布器:事件监听器的容器,对外提供发布事件和增删事件监听器的接口,维护事件和事件监听器之间的映射关系,并在事件发生时负责通知相关监听器。
Spring框架对事件的发布与监听提供了相对完整的支持,它扩展了JDK中对自定义事件监听提供的基础框架,并与Spring的IOC特性作了整合,使得用户可以根据自己的业务特点进行相关的自定义,并依托Spring容器方便的实现监听器的注册和事件的发布。因为Spring的事件监听依托于JDK提供的底层支持,为了更好的理解,先来看下JDK中为用户实现自定义事件监听提供的基础框架。
JDK中对事件监听机制的支持
JDK为用户实现自定义事件监听提供了两个基础的类。一个是代表所有可被监听事件的事件基类java.util.EventObject,所有自定义事件类型都必须继承该类,类结构如下所示:
public class EventObject implements java.io.Serializable {
private static final long serialVersionUID = 5516075349620653480L;
/**
* The object on which the Event initially occurred.
*/
protected transient Object source;
/**
* Constructs a prototypical Event.
*
* @param source The object on which the Event initially occurred.
* @exception IllegalArgumentException if source is null.
*/
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");
this.source = source;
}
/**
* The object on which the Event initially occurred.
*
* @return The object on which the Event initially occurred.
*/
public Object getSource() {
return source;
}
/**
* Returns a String representation of this EventObject.
*
* @return A a String representation of this EventObject.
*/
public String toString() {
return getClass().getName() + "[source=" + source + "]";
}
}
该类内部有一个Object类型的source变量,逻辑上表示发生该事件的事件源,实际中可以用来存储包含该事件的一些相关信息。
另一个则是对所有事件监听器进行抽象的接口java.util.EventListener,这是一个标记接口,内部没有任何抽象方法,所有自定义事件监听器都必须实现该标记接口。
/**
* A tagging interface that all event listener interfaces must extend.
* @since JDK1.1
*/
public interface EventListener {
}
以上就是JDK为我们实现自定义事件监听提供的底层支持。针对具体业务场景,我们通过扩展java.util.EventObject来自定义事件类型,同时通过扩展java.util.EventListener来定义在特定事件发生时被触发的事件监听器。当然,不要忘了还要定义一个事件发布器来管理事件监听器并提供发布事件的功能。
基于JDK实现对任务执行结果的监听
想象我们正在做一个关于Spark的任务调度系统,我们需要把任务提交到集群中并监控任务的执行状态,当任务执行完毕(成功或者失败),除了必须对数据库进行更新外,还可以执行一些额外的工作:比如将任务执行结果以邮件的形式发送给用户。这些额外的工作后期还有较大的变动可能:比如还需要以短信的形式通知用户,对于特定的失败任务需要通知相关运维人员进行排查等等,所以不宜直接写死在主流程代码中。最好的方式自然是以事件监听的方式动态的增删对于任务执行结果的处理逻辑。为此我们可以基于JDK提供的事件框架,打造一个能够对任务执行结果进行监听的弹性系统。
任务结束事件的事件源
因为要对任务执行结束这一事件进行监听,所以必须对任务这一概念进行定义,如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {
private String name;
private TaskFinishStatus status;
}
任务包含任务名和任务状态,其中任务状态是个枚举常量,只有成功和失败两种取值。
public enum TaskFinishStatus {
SUCCEED,
FAIL;
}
任务结束事件TaskFinishEvent
自定义事件类型TaskFinishEvent继承自JDK中的EventObject,构造时会传入Task作为事件源。
public class TaskFinishEvent extends EventObject {
public TaskFinishEvent(Object source) {
super(source);
}
}
该事件的监听器抽象
继承标记接口EventListner表示该接口的实现类是一个监听器,同时在内部定义了事件发生时的响应方法onTaskFinish(event),接收一个TaskFinishEvent作为参数。
public interface TaskFinishEventListner extends EventListener {
void onTaskFinish(TaskFinishEvent event);
}
邮件服务监听器
该邮件服务监听器将在监听到任务结束事件时将任务的执行结果发送给用户。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MailTaskFinishListener implements TaskFinishEventListener {
private String email;
@Override
public void onTaskFinish(TaskFinishEvent event) {
System.out.println("Send Email to " + email + " Task:" + event.getSource());
}
}
自定义事件发布器
public class TaskFinishEventPublisher {
private List<TaskFinishEventListener> listeners = new ArrayList();
//注册监听器
public synchronized void register(TaskFinishEventListener listner) {
if (!listeners.contains(listner)) {
listeners.add(listner);
}
}
//移除监听器
public synchronized boolean remove(TaskFinishEventListener listener) {
return listeners.remove(listener);
}
//发布任务结束事件
public void publishEvent(TaskFinishEvent event) {
for (TaskFinishEventListener listener : listeners) {
listener.onTaskFinish(event);
}
}
}
测试代码如下
public class TestTaskFinishListener {
public static void main(String[] args) {
//------------- 1、事件源和事件
//事件源
Task source = new Task("用户统计", TaskFinishStatus.SUCCEED);
//任务结束事件(即事件源会发生某事件)
TaskFinishEvent event = new TaskFinishEvent(source);
//------------- 2、监听器
//邮件服务监听器
MailTaskFinishListener mailListener = new MailTaskFinishListener("harvey@163.com");
//------------- 3、事件发布器、注册监听器、发布事件
//事件发布器(用来发布事件)
TaskFinishEventPublisher publisher = new TaskFinishEventPublisher();
//注册邮件服务监听器
publisher.register(mailListener);
//发布事件(由于有前一步的注册服务监听器,所以发布事件其实就是间接的是调用监听器的方法)
publisher.publishEvent(event);
}
}
如果后期因为需求变动需要在任务结束时将结果以短信的方式发送给用户,则可以再添加一个短信服务监听器:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsTaskFinishListener implements TaskFinishEventListener {
private String address;
@Override
public void onTaskFinish(TaskFinishEvent event) {
System.out.println("Send Message to " + address + " Task:" + event.getSource());
}
}
在测试代码中添加如下代码向事件发布器注册该监听器:
SmsTaskFinishListener smsListener = new SmsTaskFinishListener("123456789");
//注册短信服务监听器
publisher.register(smsListener);
基于JDK的支持要实现对自定义事件的监听还是比较麻烦的,要做的工作比较多。而且自定义的事件发布器也不能提供对所有事件的统一发布支持。基于Spring框架实现自定义事件监听则要简单很多,功能也更加强大。
Spring容器对事件监听机制的支持
基于SpringBoot 2.6.2
Spring容器,具体而言是ApplicationContext接口定义的容器提供了一套相对完善的事件发布和监听框架,其遵循了JDK中的事件监听标准,并使用容器来管理相关组件,使得用户不用关心事件发布和监听的具体细节,降低了开发难度也简化了开发流程。下面看看对于事件监听机制中的各主要角色,Spring框架中是如何定义的,以及相关的类体系结构。
事件
Spring为容器内事件定义了一个抽象类ApplicationEvent,该类继承了JDK中的事件基类EventObject。因而自定义容器内事件除了需要继承ApplicationEvent之外,还要传入事件源作为构造参数。
事件监听器
Spring定义了一个ApplicationListener接口作为事件监听器的抽象,接口定义如下:
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);
}
(1)该接口继承了JDK中表示事件监听器的标记接口EventListener,内部只定义了一个抽象方法onApplicationEvent(evnt),当监听的事件在容器中被发布,该方法将被调用。
(2)同时,该接口是一个泛型接口,其实现类可以通过传入泛型参数指定该事件监听器要对哪些事件进行监听。这样有什么好处?这样所有的事件监听器就可以由一个事件发布器进行管理,并对所有事件进行统一发布,而具体的事件和事件监听器之间的映射关系,则可以通过反射读取泛型参数类型的方式进行匹配,稍后我们会对原理进行讲解。
(3)最后,所有的事件监听器都必须向容器注册,容器能够对其进行识别并委托容器内真正的事件发布器进行管理。
事件发布器
ApplicationContext接口继承了ApplicationEventPublisher接口,从而提供了对外发布事件的能力,如下所示:
那么是否可以说ApplicationContext,即容器本身就担当了事件发布器的角色呢?其实这是不准确的,容器本身仅仅是对外提供了事件发布的接口,真正的工作其实是委托给了具体容器内部一个ApplicationEventMulticaster对象,其定义在AbstractApplicationContext抽象类内部,如下所示:
/** Helper class used in event publishing */
private ApplicationEventMulticaster applicationEventMulticaster;
所以,真正的事件发布器是ApplicationEventMulticaster,这是一个接口,定义了事件发布器需要具备的基本功能:管理事件监听器以及发布事件。其默认实现类是
SimpleApplicationEventMulticaster,该组件会在容器启动时被自动创建,并以单例的形式存在,管理了所有的事件监听器,并提供针对所有容器内事件的发布功能。
org.springframework.context.support.AbstractApplicationContext#refresh方法中的initApplicationEventMulticaster()步骤会判断是否存在,否则就实例化一个SimpleApplicationEventMulticaster。
基于Spring实现对任务执行结果的监听
基于Spring框架来实现对自定义事件的监听流程十分简单,只需要三步:
- ① 自定义事件类
- ② 自定义事件监听器并向容器注册
- ③ 发布事件
自定任务结束事件
定义一个任务结束事件TaskFinishEvent2,该类继承抽象类ApplicationEvent来遵循容器事件规范。
public class TaskFinishEvent2 extends ApplicationEvent {
public TaskFinishEvent2(Object source) {
super(source);
}
}
自定义邮件服务监听器并向容器注册
该类实现了容器事件规范定义的监听器接口,通过泛型参数指定对上面定义的任务结束事件进行监听,通过@Component注解向容器进行注册。
@Component
public class MailTaskFinishListener2 implements ApplicationListener<TaskFinishEvent2> {
private String email = "harvey@163.com";
@Override
public void onApplicationEvent(TaskFinishEvent2 event) {
System.out.println("Send Email to " + email + " Task:" + event.getSource());
}
}
发布事件
从上面对Spring事件监听机制的类结构分析可知,发布事件的功能定义在ApplicationEventPublisher接口中,而ApplicationContext继承了该接口,所以最好的方法是通过实现ApplicationContextAware接口获取ApplicationContext实例,然后调用其发布事件方法。如下所示定义了一个发布容器事件的代理类:
@Component
public class EventPublisher implements ApplicationContextAware {
private ApplicationContext applicationContext;
//发布事件
public void publishEvent(ApplicationEvent event) {
applicationContext.publishEvent(event);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
在此基础上,还可以自定义一个短信服务监听器,在任务执行结束时发送短信通知用户。过程和上面自定义邮件服务监听器类似:实现ApplicationListner接口并重写抽象方法,然后通过注解或者xml的方式向容器注册。
测试发布事件
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {WebApp.class})
public class TestEventPublisher {
@Autowired
private EventPublisher eventPublisher;
@Test
public void test() {
//事件源
Task source = new Task("用户统计", TaskFinishStatus.SUCCEED);
TaskFinishEvent2 event2 = new TaskFinishEvent2(source);
eventPublisher.publishEvent(event2);
}
}
Spring事件监听源码解析
spring事件监听机制离不开容器IOC特性提供的支持,比如容器会自动创建事件发布器,自动识别用户注册的监听器并进行管理,在特定的事件发布后会找到对应的事件监听器并对其监听方法进行回调。Spring帮助用户屏蔽了关于事件监听机制背后的很多细节,使用户可以专注于业务层面进行自定义事件开发。然而我们还是忍不住对其背后的实现原理进行一番探讨,比如:
- 事件发布器ApplicationEventMulticaster是何时被初始化的,初始化过程中都做了什么?
- 注册事件监听器的过程是怎样的,容器怎么识别出它们并进行管理?
- 容器发布事件的流程是怎样的?它如何根据发布的事件找到对应的事件监听器,事件和由该事件触发的监听器之间的匹配规则是怎样的?
初始化事件发布器流程
真正的事件发布器是ApplicationEventMulticaster,它定义在AbstractApplicationContext中,并在ApplicationContext容器启动的时候进行初始化。在容器启动的refrsh()方法中可以找到初始化事件发布器的入口方法,如下图所示:
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
// 判断beanFactory里是否定义了id为applicationEventMulticaster的bean,默认是没有的
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isDebugEnabled()) {
logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
} else {
//一般情况会走这里,创建一个SimpleApplicationEventMulticaster并交由容器管理
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate ApplicationEventMulticaster with name '" +
APPLICATION_EVENT_MULTICASTER_BEAN_NAME +
"': using default [" + this.applicationEventMulticaster + "]");
}
}
}
这里会根据核心容器beanFactory中是否有id为applicationEventMulticaster的bean分两种情况:
(1)容器中已有id为applicationEventMulticaster的bean:直接从容器缓存获取或是创建该bean实例,并交由成员变量applicationEventMulticaster保存。当用户自定义了事件发布器并向容器注册时会执行该流程。
(2)容器中不存在applicationEventMulticaster的bean:这是容器默认的执行流程,会创建一个SimpleApplicationEventMulticaster,其仅在实现事件发布器基本功能(管理事件监听器以及发布容器事件)的前提下,增加了可以设置任务执行器Executor和错误处理器ErrorHandler的功能,当设置Executor为线程池时,则会以异步的方式对事件监听器进行回调,而ErrorHandler允许我们在回调方法执行错误时进行自定义处理。默认情况下,这两个变量都为null。
之后会调用beanFactory.registerSingleton方法将创建的SimpleApplicationEventMulticaster实例注册为容器的单实例bean。
初始化事件发布器的工作非常简单,一句话总结:由容器实例化用户自定义的事件发布器或者由容器帮我们创建一个简单的事件发布器并交由容器管理。
注册事件监听器流程
注册事件监听器的流程在初始化事件发布器之后,如下图所示:
其关键代码如下所示:
protected void registerListeners() {
// 首先注册静态指定的监听器。
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}
// 不要在这里初始化FactoryBeans:我们需要保留所有常规Bean
// 未初始化以允许后处理器应用于它们!
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
// 发布早期应用程序事件
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (!CollectionUtils.isEmpty(earlyEventsToProcess)) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
容器事件发布流程
org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object)
org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, ResolvableType)
前面我们介绍了,在启动的时候如果没有一个beanName叫做applicationEventMulticaster的ApplicationEventMulticaster,那使用的就是SimpleApplicationEventMulticaster,该组件会在容器启动时被自动创建,并以单例的形式存在,管理了所有的事件监听器,并提供针对所有容器内事件的发布功能。
org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(ApplicationEvent, ResolvableType)
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
//获取事件类型
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
//获取事件发布器内的任务执行器,默认该方法返回null
Executor executor = getTaskExecutor();
//遍历所有和事件匹配的事件监听器
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
//异步回调监听方法
executor.execute(() -> invokeListener(listener, event));
}
else {
//同步回调监听方法
invokeListener(listener, event);
}
}
}
那就是我们就有个疑问:如何根据事件类型找到匹配的所有事件监听器?这部分逻辑在org.springframework.context.event.AbstractApplicationEventMulticaster#getApplicationListeners(ApplicationEvent, ResolvableType)
protected Collection<ApplicationListener<?>> getApplicationListeners(
ApplicationEvent event, ResolvableType eventType) {
//获取事件中的事件源对象
Object source = event.getSource();
//获取事件源类型
Class<?> sourceType = (source != null ? source.getClass() : null);
//以事件类型和事件源类型为参数构建一个cacheKey,用于从缓存map中获取与之匹配的监听器列表
ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);
// Potential new retriever to populate
CachedListenerRetriever newRetriever = null;
//根据cacheKey从缓存中获取CachedListenerRetriever
CachedListenerRetriever existingRetriever = this.retrieverCache.get(cacheKey);
if (existingRetriever == null) {
if (this.beanClassLoader == null ||
(ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
//不存在则创建一个全新的CachedListenerRetriever,并保存到缓存中
newRetriever = new CachedListenerRetriever();
//如果存在重复的cacheKey,那么putIfAbsent不会放入值,如果存在则返回cacheKey对应的值
existingRetriever = this.retrieverCache.putIfAbsent(cacheKey, newRetriever);
if (existingRetriever != null) {
newRetriever = null; // no need to populate it in retrieveApplicationListeners
}
}
}
//如果查到CachedListenerRetriever,则从其实例中获取到所有的监听器直接返回
if (existingRetriever != null) {
Collection<ApplicationListener<?>> result = existingRetriever.getApplicationListeners();
if (result != null) {
return result;
}
// If result is null, the existing retriever is not fully populated yet by another thread.
// Proceed like caching wasn't possible for this current local attempt.
}
//遍历所有的事件监听器,并根据事件类型和事件源类型进行匹配。
return retrieveApplicationListeners(eventType, sourceType, newRetriever);
}
如果事件是第一次发布,会遍历所有的事件监听器,并根据事件类型和事件源类型进行匹配:org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners
private Collection<ApplicationListener<?>> retrieveApplicationListeners(
ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable CachedListenerRetriever retriever) {
//这是存放匹配的监听器的列表
List<ApplicationListener<?>> allListeners = new ArrayList<>();
Set<ApplicationListener<?>> filteredListeners = (retriever != null ? new LinkedHashSet<>() : null);
Set<String> filteredListenerBeans = (retriever != null ? new LinkedHashSet<>() : null);
Set<ApplicationListener<?>> listeners;
Set<String> listenerBeans;
synchronized (this.defaultRetriever) {
listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
}
//遍历所有的监听器
for (ApplicationListener<?> listener : listeners) {
//判断该事件监听器是否匹配
if (supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
filteredListeners.add(listener);
}
//将匹配的监听器加入列表
allListeners.add(listener);
}
}
//这部分可以跳过
if (!listenerBeans.isEmpty()) {
ConfigurableBeanFactory beanFactory = getBeanFactory();
for (String listenerBeanName : listenerBeans) {
try {
if (supportsEvent(beanFactory, listenerBeanName, eventType)) {
ApplicationListener<?> listener =
beanFactory.getBean(listenerBeanName, ApplicationListener.class);
if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
if (beanFactory.isSingleton(listenerBeanName)) {
filteredListeners.add(listener);
}
else {
filteredListenerBeans.add(listenerBeanName);
}
}
allListeners.add(listener);
}
}
else {
// Remove non-matching listeners that originally came from
// ApplicationListenerDetector, possibly ruled out by additional
// BeanDefinition metadata (e.g. factory method generics) above.
Object listener = beanFactory.getSingleton(listenerBeanName);
if (retriever != null) {
filteredListeners.remove(listener);
}
allListeners.remove(listener);
}
}
catch (NoSuchBeanDefinitionException ex) {
// Singleton listener instance (without backing bean definition) disappeared -
// probably in the middle of the destruction phase
}
}
}
//对匹配的监听器列表进行排序
AnnotationAwareOrderComparator.sort(allListeners);
if (retriever != null) {
if (filteredListenerBeans.isEmpty()) {
retriever.applicationListeners = new LinkedHashSet<>(allListeners);
retriever.applicationListenerBeans = filteredListenerBeans;
}
else {
retriever.applicationListeners = filteredListeners;
retriever.applicationListenerBeans = filteredListenerBeans;
}
}
return allListeners;
}
判断监听器是否匹配的逻辑在supportsEvent(listener, eventType, sourceType)中:org.springframework.context.event.AbstractApplicationEventMulticaster#supportsEvent(ConfigurableBeanFactory, java.lang.String, ResolvableType)
梳理下容器事件发布的整个流程,可以总结如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?