驾一叶之扁舟 举匏樽以相属
寄蜉蝣于天地,渺沧海之一粟。哀吾生之须臾,羡长江之无穷。
挟飞仙以遨游,抱明月而长终。知不可乎骤得,托遗响于悲风。

从壹开始微服务 [ DDD ] 之十二 ║ 核心篇【下】:事件驱动EDA 详解

缘起

哈喽大家好,又是周二了,时间很快,我的第二个系列DDD领域驱动设计讲解已经接近尾声了,除了今天的时间驱动EDA(也有可能是两篇),然后就是下一篇的事件回溯,就剩下最后的权限验证了,然后就完结了,这两个月我也是一直在自学,然后再想栗子,个人感觉收获还是很大的,比如DDD领域分层设计、CQRS读写分离、CommandBus命令总线、EDA事件驱动、四色原理等等,如果大家真的能踏踏实实的看完,或者说多看看书,对个人的思想提高有很大的帮助,这里要说两点,可能会有一些小伙伴不开心,但是还是要说说:

1、很多小伙伴一直问我看什么书,我个人感觉,只要是书看就对了,与其纠结哪本,还不如踏踏实实先看一本。

2、还有小伙伴问,为啥还没有看到微服务的内容?

我想说,其实微服务是一个很宽泛的领域,比如.net core的深入学习,依赖注入的使用,仓储契约、DDD+事件总线的学习、中介者模式、Docker的学习、容器化设计等等等等,这些都属于微服务的范畴,如果这些基础知识不会的话,可能是学不好微服务的。

周末的时候,我又好好的整理了下我的Github上的代码,然后新建了一些分支(如果你不会使用Git命令,可以看我的一个文章:https://www.jianshu.com/p/2b666a08a3b5,会一直更新),主要是这样的(这个数字是对应的文章,比如今天的是第 12 ):

其实我这个系列所说的 DDD领域驱动设计,是一个很丰富的概念,里边包含了DDD的多层设计思想、CQRS、Bus、EDA、ES等等,所以如果你只想要其中的一部分,可以对应的分支进行Clone,比如你单纯想要一个干净的基于DDD四层设计的模板,可以克隆 Framework_DDD_8 这个分支,如果你想带有读写分离,可以克隆 CQRS_DDD_9 这个分支等等,也方便好好研究。

关于CQRS读写分离概念,请注意,分离不一定是分库,一个数据库也能实现读写分离,最简单的就是从Code上来区分。

 

前言

好啦,上边说了一些周末的思考,现在马上进入正文,不知道大家对上周的内容还有没有印象,主要用两篇文章来说明了命令总线的设计思想和执行过程《十 ║领域驱动【实战篇·中】:命令总线Bus分发(一)》、《十一 ║ 基于源码分析,命令分发的过程(二)》,咱们很好的实现了多个复杂模型间的解耦,成功的简化了API接口层和 Application应用服务层,把重心真正的转义到了领域层。

当然其中也有一些新的问题出现了,这个也可以当作今天的每篇一问:

首先,对领域通知的处理上,目前用的是通过一个 ErrorData 的key 来把错误通知放到了内存里,然后去读取,这样有一个很危险的问题,就是生命周期的问题,如果在当前实例中,没有及时删除,可能会出现错误通知的混乱,这是致命的,当然还有 key 的问题,因为几乎每一个 Command 都会有不同的信息,我们不能通过简简单单的人为取名字来实现这个逻辑,这是荒唐的。

其次,如果我们 Command 执行完成,是如何发布通知的,比如注册成功的邮件,短信分发,站内推送等等。

最后,不知道大家有没有深入的去学习,去了解 MediatR 中介者的两个模式:请求/响应模式 与 发布/订阅模式的区别和联系(详细的下边会说到)。

 你会说,很简单呀,我们直接在 CommandHandler 命令处理程序中处理不就行了,一步一步往下走就可以了呀,如果你现在还有这样的思维,那DDD可真的好好再学习了,为什么呢?很简单,我们当时为什么要把 contrller 的业务逻辑剥离到领域模型,就是为了业务独立化,不让多个不相干的业务缠绕(比如我们之前是把model 验证、错误返回、发邮件等,都是写在 controller 里的),那如果我们再把过多的业务逻辑写到命令处理程序中的话,那命令处理模型不就成为了第二个 controller 了么?我们为业务把 controller 剥离了一次,那今天咱们就继续从 命令处理程序中,再优化一次。

 

零、今天要实现右下角蓝色的部分

 

(周末有一个小伙伴问这个软件的地址:https://www.mindmeister.com,应该需要FQ)

 

一、领域事件驱动设计 —— EDA

1、什么是领域事件 

我们先看看官网,在《实现领域驱动设计》一书中对领域事件的定义如下:

领域专家所关心的发生在领域中的一些事件。

将领域中所发生的活动建模成一系列的离散事件。

每个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中所发生的事情。

领域事件:Domain Event,是针对某个业务来说的,或者说针对某个聚合的业务来说的,例如订单生成这种业务,它可以同时对应一种事件,比如叫做OrderGeneratorEvent,而你的零散业务可能随时会变,加一些业务,减一些业务,而对于订单生成这个事件来说,它是唯一不变的,而我们需要把这些由产生订单而发生变化的事情拿出来,而拿出来的这些业务就叫做"领域事件".其中的领域指的就是订单生成这个聚合;而事件指的就是那些零散业务的统称.

 

2、领域事件包含了哪些内容

如果你对上一篇命令总线很熟悉,这里就特别简单,几乎是一个模式,只不过总线发布的方式不一样罢了,如果你比较熟悉命令驱动,这里正好温习。如果不了解,这里就一起看吧,千万记得再回去看前两篇内容哟。

在面向对象的编程世界里,做这种事情我们需要几个抽象:

领域对象事件标示:标示接口,接口的一种,用来约束一批对象,IEvent(当前也可以使用抽象类,本文即是)

领域对象的处理方法行为:比如 StudentEventHandler。(我们的命令处理程序也是如此)

事件总线:事件处理核心类,承载了事件的发布,订阅与取消订阅的逻辑,EventBus(这个和我们的命令总线CommandBus很类似)

某个领域对象的事件:它是一个事件处理类,它实现了 EventHandler,它所处理的事情需要在Handle里去完成

一个领域事件可以理解为是发生在一个特定领域中的事件,是你希望在同一个领域中其他部分知道并产生后续动作的事件。一个领域事件必须对业务有价值,有助于形成完整的业务闭环,也即一个领域事件将导致进一步的业务操作。就比如我们今天说到的领域通知,就应该是一个事件,我们从命令中产生的错误提示,通过处理程序,引发到事件总线内,并返回到前台。

 

3、为什么需要领域事件

领域事件也是一种基于事件的架构(EDA)。事件架构的好处可以把处理的流程解耦,实现系统可扩展性,提高主业务流程的内聚性。

在咱们文章的开头,可说到了这个问题,不知道大家是否还记得,咱们再分析一下:

我们提交了一个添加Student 的申请,系统在完成保存后,可能还需要发送一个通知(当然这里错误信息,也有成功的),当然肯定还会会一些其他的后台服务的活动。如果把这一系列的动作放入一个处理过程中,会产生几个的明显问题:

1、一个是命令提交的的事务比较长,性能会有问题,甚至在极端情况下容易引发数据库的严重故障(服务器方面);

2、另外提交的服务内聚性差,可维护性差,在业务流程发生变更时候,需要频繁修改主程序(程序员方面)。

3、我们有时候只关心核心的流程,就比如添加Student,我们只关心是否添加成功,而且我们需要对这个成功有反馈,但是发邮件的功能,我们却不用放在主业务中,甚至发送成功与否,不影响 Student 的正常添加,这样我们就把后续的这些活动事件,从主业务中剥离开,实现了高内聚和低耦合(业务方面)。

还记得 MediatR 有两个中介者模式么:请求/响应 和 发布/订阅。在我们的系统中,添加一个学生命令,就是用到的请求/响应 IRequest 模式,因为我们需要等待当前操作完成,我们需要总线对我们的请求做出响应。

但是有时候我们不需要在同一请求/响应中立即执行一个动作的结果,只要异步执行这个动作,比如发送电子邮件。在这种情况下,我们使用发布/订阅模式,以异步方式发送电子邮件,并避免让用户等待发送电子邮件。

 

4、领域事件驱动是如何运行的呢?

这个时候,就用到之前我画的图了,中介者模式下,上半部的命令总线已经说完,今天说另一半事件总线:

 

当然这里也有一个网上的栗子,很不错:

 

 从图中我们也可以看到,事件驱动的工作流程呢,在命令模式下,主要是在我们的命令处理程序中出现,在我们对数据进行持久化操作的时候,作为一个后续活动事件来存在,比如我们今天要实现的两个处理工作:

1、通知信息的收集(之前我们是采用的缓存 Memory 来实现的);

2、领域通知处理程序(比如发邮件等);

 

这个时候,如果你对事件驱动有了一定的理解的话,你就会问,那我们在项目中具体的应该使用呢,请往下看。

 

二、创建事件总线

这个整体流程其实和命令总线分发很像,所以原理就不分析了,相信你如果看了之前的两篇文章的话,一定能看懂今天的内容的。

1、定义领域事件标识基类

就如上边我们说到的,我们可以定义一个接口,也可以定义一个抽象类,我比较习惯用抽象类,在核心领域层 Christ3D.Domain.Core 中的Events 文件夹中,新建Event.cs 事件基类:

namespace Christ3D.Domain.Core.Events
{
    /// <summary>
    /// 事件模型 抽象基类,继承 INotification
    /// 也就是说,拥有中介者模式中的 发布/订阅模式
    /// </summary>
    public abstract class Event : INotification
    {
        // 时间戳
        public DateTime Timestamp { get; private set; }
        
        // 每一个事件都是有状态的
        protected Event()
        {
            Timestamp = DateTime.Now;
        }
    }
}

 

2、定义事件总线接口

在中介处理接口IMediatorHandler中,定义引发事件接口,作为发布者,完整的 IMediatorHandler.cs 应该是这样的

namespace Christ3D.Domain.Core.Bus
{
    /// <summary>
    /// 中介处理程序接口
    /// 可以定义多个处理程序
    /// 是异步的
    /// </summary>
    public interface IMediatorHandler
    {
        /// <summary>
        /// 发送命令,将我们的命令模型发布到中介者模块
        /// </summary>
        /// <typeparam name="T"> 泛型 </typeparam>
        /// <param name="command"> 命令模型,比如RegisterStudentCommand </param>
        /// <returns></returns>
        Task SendCommand<T>(T command) where T : Command;

        /// <summary>
        /// 引发事件,通过总线,发布事件
        /// </summary>
        /// <typeparam name="T"> 泛型 继承 Event:INotification</typeparam>
        /// <param name="event"> 事件模型,比如StudentRegisteredEvent,</param>
        /// 请注意一个细节:这个命名方法和Command不一样,一个是RegisterStudentCommand注册学生命令之前,一个是StudentRegisteredEvent学生被注册事件之后
        /// <returns></returns>
        Task RaiseEvent<T>(T @event) where T : Event;

    }
}

 

 

3、实现总线分发接口

 在基层设施总线层的记忆总线 InMemoryBus.cs 中,实现我们上边的事件分发总线接口:

 /// <summary>
 /// 引发事件的实现方法
 /// </summary>
 /// <typeparam name="T">泛型 继承 Event:INotification</typeparam>
 /// <param name="event">事件模型,比如StudentRegisteredEvent</param>
 /// <returns></returns>
 public Task RaiseEvent<T>(T @event) where T : Event
 {
     // MediatR中介者模式中的第二种方法,发布/订阅模式
     return _mediator.Publish(@event);
 }

 

注意这里使用的是中介模式的第二种——发布/订阅模式,想必这个时候就不用给大家解释为什么要使用这个模式了吧(提示:不需要对请求进行必要的响应,与请求/响应模式做对比思考)。现在我们把事件总线定义(是一个发布者)好了,下一步就是如何定义事件模型和处理程序了也就是订阅者,如果上边的都看懂了,请继续往下走。

 

三、事件模型的处理与使用

 可能这句话不是很好理解,那说人话就是:我们之前每一个领域模型都会有不同的命令,那每一个命令执行完成,都会有对应的后续事件(比如注册和删除用户肯定是不一样的),当然这个是看具体的业务而定,就比如我们的订单领域模型,主要的有下单、取消订单、删除订单等。

我个人感觉,每一个命令模型都会有对应的事件模型,而且一个命令处理方法可能有多个事件方法。具体的请看:

1、定义添加Student 的事件模型

当然还会有删除和更新的事件模型,这里就用添加作为栗子,在领域层 Christ3D.Domain 中,新建  Events 文件夹,用来存放我们所有的事件模型,

因为是 Student 模型,所以我们在 Events 文件夹下,新建 Student 文件夹,并新建 StudentRegisteredEvent.cs 学生添加事件类:

namespace Christ3D.Domain.Events
{
    /// <summary>
    /// Student被添加后引发事件
    /// 继承事件基类标识
    /// </summary>
    public class StudentRegisteredEvent : Event
    {
        // 构造函数初始化,整体事件是一个值对象
        public StudentRegisteredEvent(Guid id, string name, string email, DateTime birthDate, string phone)
        {
            Id = id;
            Name = name;
            Email = email;
            BirthDate = birthDate;
            Phone = phone;
        }
        public Guid Id { get; set; }
        public string Name { get; private set; }
        public string Email { get; private set; }
        public DateTime BirthDate { get; private set; }
        public string Phone { get; private set; }
    }
}

 

2、定义领域事件的处理程序Handler

这个和我们的命令处理程序一样,只不过我们的命令处理程序是总线在应用服务层分发的,而事件处理程序是在领域层的命令处理程序中被总线引发的,可能有点儿拗口,看看下边代码就清楚了,就是一个引用场景的顺序问题。

在领域层Chirst3D.Domain 中,新建 EventHandlers 文件夹,用来存放我们的事件处理程序,然后新建 Student事件模型的处理程序 StudentEventHandler.cs:

 

namespace Christ3D.Domain.EventHandlers
{
    /// <summary>
    /// Student事件处理程序
    /// 继承INotificationHandler<T>,可以同时处理多个不同的事件模型
    /// </summary>
    public class StudentEventHandler :
        INotificationHandler<StudentRegisteredEvent>,
        INotificationHandler<StudentUpdatedEvent>,
        INotificationHandler<StudentRemovedEvent>
    {
        // 学习被注册成功后的事件处理方法
        public Task Handle(StudentRegisteredEvent message, CancellationToken cancellationToken)
        {
            // 恭喜您,注册成功,欢迎加入我们。

            return Task.CompletedTask;
        }

        // 学生被修改成功后的事件处理方法
        public Task Handle(StudentUpdatedEvent message, CancellationToken cancellationToken)
        {
            // 恭喜您,更新成功,请牢记修改后的信息。

            return Task.CompletedTask;
        }

        // 学习被删除后的事件处理方法
        public Task Handle(StudentRemovedEvent message, CancellationToken cancellationToken)
        {
            // 您已经删除成功啦,记得以后常来看看。

            return Task.CompletedTask;
        }
    }
}

相信大家应该都能看的明白,在上边的注释已经很清晰的表达了响应的作用,如果有看不懂,咱们可以一起交流。

好啦,现在第二步已经完成,剩下最后一步:如何通过事件总线分发我们的事件模型了。

 

3、在事件总线EventBus中引发事件

这个使用起来很简单,主要是我们在命令处理程序中,处理完了持久化以后,接下来调用我们的事件总线,对不同的事件模型进行分发,就比如我们的 添加Student 命令处理程序方法中,我们通过工作单元添加成功后,需要做下一步,比如发邮件,那我们就需要这么做。

在命令处理程序 StudentCommandHandler.cs 中,完善我们的提交成功的处理:

 // 持久化
 _studentRepository.Add(customer);

 // 统一提交
 if (Commit())
 {
     // 提交成功后,这里需要发布领域事件
     // 比如欢迎用户注册邮件呀,短信呀等
     Bus.RaiseEvent(new StudentRegisteredEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate,customer.Phone));
 }

这样就很简单的将我们的事件模型分发到了事件总线中去了,这个时候记得要在 IoC 原生注入类NativeInjectorBootStrapper中,进行注入。关于触发过程下边我简单说一下。

 

4、整体事件驱动执行过程

 说到了这里,你可能发现和命令总线很相似,也可能不是很懂,简单来说,整体流程是这样的:

1、首先我们在命令处理程序中调用事件总线来引发事件  Bus.RaiseEvent(........);

2、然后在Bus中,将我们的事件模型进行包装成固定的格式   _mediator.Publish(@event);

3、然后通过注入的方法,将包装后的事件模型与事件处理程序进行匹配,系统执行事件模型,就自动实例化事件处理程序 StudentEventHandler;

4、最后执行我们Handler 中各自的处理方法 Task Handle(StudentRegisteredEvent message)。

希望正好也温习下命令总线的执行过程。

 

5、依赖注入事件模型和处理程序

 // Domain - Events
 // 将事件模型和事件处理程序匹配注入
 services.AddScoped<INotificationHandler<StudentRegisteredEvent>, StudentEventHandler>();
 services.AddScoped<INotificationHandler<StudentUpdatedEvent>, StudentEventHandler>();
 services.AddScoped<INotificationHandler<StudentRemovedEvent>, StudentEventHandler>();

 

这个时候,我们DDD领域驱动设计核心篇的第一部分就是这样了,还剩下最后的,事件驱动的事件源事件存储/回溯,我们下一讲再说。

 

接下来咱们说说领域通知,为什么要说领域通知呢,大家应该还记得我们之前将错误信息放到了内存中,无论是操作还是业务上都很严重的问题,肯定是不可取的。那我们应该采用什么办法呢,欸?!没错,你会发现,通过上边的事件驱动设计,发现领域通知我们也可以采用这个方法,首先是多个模型之间相互通讯,但又不相互引用;而且也在命令处理程序中,对信息进行分发,和发邮件很类似,那具体如何操作呢,请往下看。

 

四、事件分发的另一个用途 —— 领域通知

1、领域通知模型 DomainNotification 

 这个通知模型,就像是一个消息队列一样,在我们的内存中,通过通知处理程序进行发布和使用,有自己的生命周期,当被访问并调用完成的时候,会手动对其进行回收,以保证数据的完整性和一致性,这个就很好的解决了咱们之前用Memory缓存通知信息的弊端。

在我们的核心领域层 Christ3D.Domain.Core 中,新建文件夹 Notifications ,然后添加领域通知模型 DomainNotification.cs:

namespace Christ3D.Domain.Core.Notifications
{
    /// <summary>
    /// 领域通知模型,用来获取当前总线中出现的通知信息
    /// 继承自领域事件和 INotification(也就意味着可以拥有中介的发布/订阅模式)
    /// </summary>
    public class DomainNotification : Event
    {
        // 标识
        public Guid DomainNotificationId { get; private set; }
        // 键(可以根据这个key,获取当前key下的全部通知信息)
        // 这个我们在事件源和事件回溯的时候会用到,伏笔
        public string Key { get; private set; }
        // 值(与key对应)
        public string Value { get; private set; }
        // 版本信息
        public int Version { get; private set; }

        public DomainNotification(string key, string value)
        {
            DomainNotificationId = Guid.NewGuid();
            Version = 1;
            Key = key;
            Value = value;
        }
    }
}

 

 

2、领域通知处理程序 DomainNotificationHandler

该处理程序,可以理解成,就像一个类的管理工具,在每次对象生命周期内 ,对领域通知进行实例化,获取值,手动回收,这样保证了每次访问的都是当前实例的数据。

 还是在文件夹 Notifications 下,新建处理程序 DomainNotificationHandler.cs:

namespace Christ3D.Domain.Core.Notifications
{
    /// <summary>
    /// 领域通知处理程序,把所有的通知信息放到事件总线中
    /// 继承 INotificationHandler<T>
    /// </summary>
    public class DomainNotificationHandler : INotificationHandler<DomainNotification>
    {
        // 通知信息列表
        private List<DomainNotification> _notifications;

        // 每次访问该处理程序的时候,实例化一个空集合
        public DomainNotificationHandler()
        {
            _notifications = new List<DomainNotification>();
        }

        // 处理方法,把全部的通知信息,添加到内存里
        public Task Handle(DomainNotification message, CancellationToken cancellationToken)
        {
            _notifications.Add(message);
            return Task.CompletedTask;
        }
        
        // 获取当前生命周期内的全部通知信息
        public virtual List<DomainNotification> GetNotifications()
        {
            return _notifications;
        }

        // 判断在当前总线对象周期中,是否存在通知信息
        public virtual bool HasNotifications()
        {
            return GetNotifications().Any();
        }

        // 手动回收(清空通知)
        public void Dispose()
        {
            _notifications = new List<DomainNotification>();
        }
    }
}

到了目前为止,我们的DDD领域驱动设计中的核心领域层部分,已经基本完成了(还剩下下一篇的事件源、事件回溯):

 

3、在命令处理程序中发布通知

 我们定义好了领域通知的处理程序,我们就可以像上边的发布事件一样,来发布我们的通知信息了。这里用一个栗子来试试:

在学习命令处理程序 StudentCommandHandler.cs 中的 RegisterStudentCommand 处理方法中,完善:

 // 判断邮箱是否存在
 // 这些业务逻辑,当然要在领域层中(领域命令处理程序中)进行处理
 if (_studentRepository.GetByEmail(customer.Email) != null)
 {
     ////这里对错误信息进行发布,目前采用缓存形式
     //List<string> errorInfo = new List<string>() { "该邮箱已经被使用!" };
     //Cache.Set("ErrorData", errorInfo);

     //引发错误事件
     Bus.RaiseEvent(new DomainNotification("", "该邮箱已经被使用!"));
     return Task.FromResult(new Unit());
 }

这个时候,我们把错误通知信息在事件总线中发布出去,剩下的就是需要在别的任何地方订阅即可,还记得哪里么,没错就是我们的自定义视图组件中,我们需要订阅通知信息,展示在页面里。

注意:我们还要修改一下之前我们的命令处理程序基类 CommandHandler.cs 的验证信息收集方法,因为之前是用缓存来实现的,我们这里也用发布事件来实现:

 //将领域命令中的验证错误信息收集
 //目前用的是缓存方法(以后通过领域通知替换)
 protected void NotifyValidationErrors(Command message)
 {
     List<string> errorInfo = new List<string>();
     foreach (var error in message.ValidationResult.Errors)
     {
         //errorInfo.Add(error.ErrorMessage);      
         //将错误信息提交到事件总线,派发出去
         _bus.RaiseEvent(new DomainNotification("", error.ErrorMessage));
     }
     //将错误信息收集一:缓存方法(错误示范)
     //_cache.Set("ErrorData", errorInfo);
 }

 

 

4、在视图组件中获取通知信息

这个很简单,之前我们用的是注入 IMemory 的方式,在缓存中获取,现在我们通过注入领域通知处理程序来实现,在视图组件 AlertsViewComponent.cs 中:

    public class AlertsViewComponent : ViewComponent
    {
        // 缓存注入,为了收录信息(错误方法,以后会用通知,通过领域事件来替换)
        // private IMemoryCache _cache;
        // 领域通知处理程序
        private readonly DomainNotificationHandler _notifications;

        // 构造函数注入
        public AlertsViewComponent(INotificationHandler<DomainNotification> notifications)
        {
            _notifications = (DomainNotificationHandler)notifications;
        }

        /// <summary>
        /// Alerts 视图组件
        /// 可以异步,也可以同步,注意方法名称,同步的时候是Invoke
        /// 我写异步是为了为以后做准备
        /// </summary>
        /// <returns></returns>
        public async Task<IViewComponentResult> InvokeAsync()
        {
            // 从通知处理程序中,获取全部通知信息,并返回给前台
            var notificacoes = await Task.FromResult((_notifications.GetNotifications()));
            notificacoes.ForEach(c => ViewData.ModelState.AddModelError(string.Empty, c.Value));

            return View();
        }
    }

 

5、StudentController 判断是否有通知信息

 通过注入的方式,把 INotificationHandler<DomainNotification> 注入控制器,然后因为这个接口可以实例化多个对象,那我们就强类型转换成 DomainNotificationHandler:

 

这里要说明下,记得要对事件处理程序注入,才能使用:

 // 将事件模型和事件处理程序匹配注入
 services.AddScoped<INotificationHandler<DomainNotification>, DomainNotificationHandler>();

 

 

五、结语

好啦,今天的讲解基本就到这里了,今天重点说明了,我们如何使用事件总线,已经事件驱动模型下如何定义事件模型和事件处理程序,如果你都看懂了呢,这里可以简单回想一下以下几个问题:

1、为什么要定义事件驱动呢?(提示词:业务分离)

2、我们是在哪里发布这些事件的呢?(提示词:.publish()方法) 

3、事件驱动中的生命周期是从哪里开始到哪里接受的?(提示:处理程序Handler)

 

如果你对以上的内容还是比较困惑呢,这里有两个文章可以参考,当然,多沟通才是关键!

https://www.cnblogs.com/lori/p/4080426.html

https://blog.csdn.net/sD7O95O/article/details/79609305

 

六、GitHub & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD 

 

 

--END

posted @ 2018-12-04 13:36  老张的哲学  阅读(7757)  评论(42编辑  收藏  举报
作者:老张的哲学
好好学习,天天向上
返回顶部小火箭
好友榜:
如果愿意,把你的博客地址放这里
jianshu.com/u/老张
SqlSugar codeisbug.com