先上github地址:https://github.com/shelldudu/never

 

引用其图片说明该构架所涉及到的工具

 

 

 

 

never           never

 

 

其中使用包含了一些开发设计模式,比如message的订阅与发布,熔断机制等。

 

【一】系统的整体设计

  以ApplicationStartup开始,加入启动服务,期间可根据注册不同组件。直接上代码

  netcore的部分代码

 1       /// <summary>
 2         /// 该方法被ConfigureServices里面的base.ConfigureServicese调用,由于ConfigureServices方法会使用netcore系统的不同组件,所以在其后面启动,是将这些组件方案所注册的ioc规则加入到自己的ioc规则里面去
 3         /// 同时替换了系统IServiceCollection自己生成的IServiceProvider对象
 4         /// </summary>
 5         /// <param name="sender"></param>
 6         /// <param name="e"></param>
 7         private void Startup_OnStarting(object sender, Never.StartupEventArgs e)
 8         {
 9             //ddd的command里面使用了恢复(即一些命令出错后被保存后过段时间再执行),当前使用sqlite本地数据库方式
10             var commandfile = new FileInfo(AppContext.BaseDirectory + "\\App_Data\\command_demo.db");
11             //ddd的event跟上面的一样
12             var eventfile = new FileInfo(AppContext.BaseDirectory + "\\App_Data\\event_demo.db");
13             //使用nlog组件
14             var logfile = new FileInfo(AppContext.BaseDirectory + "\\App_Config\\nlog.config");
15             //配置文件的读取
16             var configReader = new AppConfigReader(this.Configuration);
17 
18             /*json序列化配置*/
19             DefaultSetting.DefaultDeserializeSetting = new JsonDeserializeSetting() { DateTimeFormat = DateTimeFormat.ChineseStyle };
20             DefaultSetting.DefaultSerializeSetting = new JsonSerializeSetting() { DateTimeFormat = DateTimeFormat.ChineseStyle };
21 
22             //注册程序集过滤,因为整个启动过程会分析程序集里面的Type对象,很多dll我们不用分析,只焦点到我们现在注入的2个规则就行,"Never" + "B2C",正则只要匹配到该字符就加加载到待分析的dll集合中
23             e.Startup.RegisterAssemblyFilter("B2C".CreateAssemblyFilter()).RegisterAssemblyFilter("Never".CreateAssemblyFilter());
24 
25             //ioc分开2种启动方法:第一与最后,主要原因如下:(1)服务启动有先后顺序,不同的系统组件所注册的顺序不同的,但有些组件要求在所有环境下都只有第一或最后启动(2)由于使用环境自动注册这种设计下,一些组件要手动注册会带自己的规则就会被自动注册覆盖
26             e.Startup.UseEasyIoC(
27                 (x, y, z) =>
28                 {
29                     //先启动该服务注册组件,
30                 },
31                 (x, y, z) =>
32                 {
33                     //再按自己的个性化注册组件,比如Controller在下面UseApiDependency后会自动注入,但是我想HomeController注入的时候使用memecahed,这种情况就要手动注入了,说明注入了ICaching接口有多个实现
34                     //x.RegisterType<Controllers.HomeController, Controllers.HomeController>().WithParameter<Never.Caching.ICaching>("memcached");
35 
36                     //注入query与repository实例,为什么不用自动注入?哈哈,因为在framework或netcore等各种不同的环境下大家读取配置文件是不同的,一旦写死在B2C.Message.SqlData.Query里面读取配置文件,则使用不同的host技术就出现极大问题,
37                     //比如netcore没有connectionString这种配置(或者有人说可以手动引用System.Configuration,这不是嫌麻烦吗),并且sqlclient核心的dbproviderfactory有时候要手动构造
38                     x.RegisterInstance(new B2C.Message.SqlData.Query.QueryDaoBuilder(Infrastructure.SqldbType.sqlserver, () => configReader["message_conn"]));
39                     x.RegisterInstance(new B2C.Message.SqlData.Repository.RepositoryDaoBuilder(Infrastructure.SqldbType.sqlserver, () => configReader["message_conn"]));
40                 });
41 
42             //使用环境下自动注册组件,
43             e.Startup.UseAutoInjectingAttributeUsingIoC(new IAutoInjectingEnvironmentProvider[]
44             {
45                 //在message该环境下,所有单例注册组件只有匹配message的才注册,(1)有些组件是线程的,那么不会被描述和注入中,除非再加个线程provider;(2)即使是单例provider,但所运行不是message环境,所以也不会注入
46                 SingletonAutoInjectingEnvironmentProvider.UsingRuleContainerAutoInjectingEnvironmentProvider("message"),
47             })
48             //使用统一配置中心读取配置文件,实用性在后面有讲到
49             .UseConfigClient(new IPEndPoint(IPAddress.Parse(configReader["config_host"]), configReader.IntInAppConfig("config_port")), out var configFileClient);
50             configFileClient.Startup(TimeSpan.FromMinutes(10), new[] { new ConfigFileClientRequest { FileName = "message_api" } }, (c, t) =>
51             {
52                 var content = t;
53                 if (c != null && c.FileName == "message_api")
54                 {
55                     System.IO.File.WriteAllText(System.IO.Path.Combine(this.Environment.ContentRootPath, "appsettings.app.json"), content);
56                 }
57             }).Push("message_api").GetAwaiter().GetResult();
58 
59 
60             e.Startup
61                 .UseCounterCache() //使用countcache
62                 .UseConcurrentCache() //使用安全countcache
63                 .UseDataContractJson() //使用datacontract技术的序列化,实现了IJsonSerialize接口
64                 .UseEasyJson(string.Empty) //使用easyjson技术的序列化,实现了IJsonSerialize接口
65                 .UseNLog(logfile) //使用nlog
66                 .UseAppConfig(configReader) //将IConfigReader注入
67                 .UseForceCheckAggregateRootImplIHandle() //这几个Force都是为了检查ddd开发一些要求,比如是否继承某个类,某些接口
68                 .UseForceCheckCommandAppDomainAttribute() //检查所有的command是否带了特定attribute
69                 .UseForceCheckCommandEvenWithNoParamaterCtor() //检查所有的commandhandler所要的构造参数是否被注入中
70                 .UseForceCheckCommandHandlerCtor() //检查所有的eventhandler所要的构造参数是否被注入中
71                 .UseForceCheckEventAppDomainAttribute()//检查所有的event是否带了特定attribute
72                 .UseForceCheckEventHandlerCtor() //检查所有的eventhandler所要的构造参数是否被注入中
73                 .UseForceCheckMessageSubscriberCtor() //使用消息的订单与发布
74                 .UseInjectingCommandHandlerEventHandler(Never.IoC.ComponentLifeStyle.Singleton) //注入所有的commandhandler,在commandbus执行其对象行为
75                 .UseSqliteEventProviderCommandBus<DefaultCommandContext>(new SqliteFailRecoveryStorager(commandfile, eventfile)) //使用cqrs组件,指定sqlite作为恢复组件,
76                 .UseApiModelStateValidation() //mvc,webapi的模型参数验证,测试的时候在netcore 2.2下会返回404,2.1正常
77                 .UseApiActionCustomRoute(e.Collector as IServiceCollection) //自定义路由,相同于在controller可以使用httpget等route技术
78                 .UseApiDependency(e.Collector as IServiceCollection);//注入所有的controller
79 
80             //配置中心更新配置文件后,系统不一定马上能重新加载,所以我们睡眠1秒
81             e.Startup.Startup(TimeSpan.FromSeconds(1), (x) =>
82             {
83                 //我们在此启动看看所使用组件是否正常启动
84                 using (var sc = x.ServiceLocator.BeginLifetimeScope())
85                 {
86                     sc.Resolve<ICommandBus>();
87                     sc.Resolve<ILoggerBuilder>();
88                     sc.Resolve<IJsonSerializer>();
89                     var home = sc.Resolve<Controllers.MessageController>();
90 
91                     var logger = sc.Resolve<ILoggerBuilder>().Build(typeof(Startup));
92                     logger.Info("startup at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
93                 }
94             });
95         }

 

 Controller使用直接注入了

 

    [Route("api/")]
    [ApiController]
    public class VCodeController : BasicController, IVCodeService
    {
        #region field and ctor

        private readonly IEmailCodeQuery emailCodeQuery = null;
        private readonly IMobileCodeQuery mobileCodeQuery = null;
        private readonly ICommandBus commandBus = null;
        private readonly ILoggerBuilder loggerBuilder = null;
        private readonly IJsonSerializer jsonSerializer = null;

        public VCodeController(ICommandBus commandBus,
            ILoggerBuilder loggerBuilder,
            IJsonSerializer jsonSerializer,
            IEmailCodeQuery emailCodeQuery,
            IMobileCodeQuery mobileCodeQuery)
        {
            this.commandBus = commandBus;
            this.loggerBuilder = loggerBuilder;
            this.jsonSerializer = jsonSerializer;
            this.emailCodeQuery = emailCodeQuery;
            this.mobileCodeQuery = mobileCodeQuery;
        }

        #endregion field and ctor

        /// <summary>
        /// 校验邮箱验证码,组成了 api/a9a900aee8c6 这一个路由
        /// </summary>
        /// <param name="reqs"></param>
        /// <returns></returns>
        [ApiActionRemark("a9a900aee8c6", "HttpPost"), HttpPost]
        public ApiResult<string> CheckEmailValidateCode(CheckEmailValidateCodeReqs reqs)
        {
            if (!this.TryValidateModel(reqs))
            {
                return Anonymous.NewApiResult(ApiStatus.Fail, string.Empty, this.ModelErrorMessage);
            }

            try
            {
                var handler = this.commandBus.Send(new DestroyEmailCodeCommand(NewId.GenerateGuid())
                {
                    Email = reqs.Email,
                    UsageType = reqs.UsageType,
                    VCode = reqs.VCode,
                });

                if (handler == null)
                {
                    return Anonymous.NewApiResult(ApiStatus.Fail, string.Empty, "验证失败");
                }

                if (handler.Status != CommandHandlerStatus.Success)
                {
                    return Anonymous.NewApiResult(ApiStatus.Error, string.Empty, this.HandlerMerssage(handler));
                }

                return Anonymous.NewApiResult(ApiStatus.Success, string.Empty);
            }
            catch (Exception ex)
            {
                this.loggerBuilder.Build(typeof(VCodeController)).Error("check email code error", ex);
                return Anonymous.NewApiResult(ApiStatus.Error, string.Empty, ex.GetMessage());

            }
        }
    }

 

 

 

【二】构架的组成部分

  1. Emit                            避免使用反射带来的损耗,并且对OpCode的使用封装变成方法的调用,可容易理解与使用,是后面所有技术的支撑点。
  2.  ApplicationStartup     整个系统的初始化中心点,分Web环境与Console环境。
  3. IoC                             IoC实现简单的三种生命周期,单例 + 范围 + 短暂,注入指定参数,可以生成代理注入拦截器。
  4. Aop                            加入上下方日志跟踪(如LoggerAttribte自动写日志);Mock对象等。
  5. CQRS                        实现了一套commandbus + eventbus设计,commandbus执行命令后,若聚合对象有事件,则通过eventbus发布到订阅者;中间使用sqlite来保存订阅失败的队列,用于后期的恢复发布订阅。
  6. SqlClient                     配置极其简单,使用也很容易的一个sql执行方法,使用xml文件配置管理sql语句,可执行事务,对xml内容进行缩进使得好看;也可以直接写sql语句。使用typehander,用于处理阻抗失败的情况。
  7. Mapper                      直接映射对象,效率比emitmapper差一点。
  8. Message                    消息的发布与订阅,可以在内存,mq方式发布到不同的机器
  9. Socket                        使用SocketAsyncEventArgs实现的一套高性能方案,读取与发送分开队列,可以设置心跳
  10. Remoting                   在socket的基础上实现一套remoting通讯。
  11. Configuration             配置中心,对文件(夹)进行监控,修改文件会触发所有应用程序的配置更新;设置了共享级+应用级配置文件,不用的应用级配置文件可以直接link共享级的配置,共享级的配置可以读取文件,也可以到数据库查询
  12. Deployment                对WebApi里面的Service直接生成代理类,封装了web请求的参数,路由等信息,还可以使用熔断机制,在客户端发现服务不可用的时候自动返回友好结果
  13. Workflow                    实现了一套工作流内容,每一步骤都可以独立为插件或一个类,并且可组合不同步骤,包含等待,重试,中断等不同状态
  14. Memcached               小写了一个memcached客户端,文本协议+二进制协议,还有Gzip压缩,Binary序列化;定义的接口可以很方便使用protobuf等技术的自由扩展
  15. JsonSerializer            json序列化,可动态配置不用类型的输出结构,通过emit后缓存提高性能,还能支持用户自定义序列接口。

   ….etc

 

【三】该构架优势

 

通过上述代码可以知道,整个构架的初始化都在global或startup里面实现的,所有业务的开发者可以直接在业务上开发而不用注意组件如何注入。并且搭建环境也是非常简单。

对远程方法的调用,直接变成了本地调用方式,而一些参数等很多细节的功能做得也很极致,

 

        /// <summary>
        /// 修改密码
        /// </summary>
        [HttpPost, ApiActionRemark("ForgetPwd", "HttpPost")]
        public IActionResult ForgetPwd(RegisterViewModel model)
        {
            //这个验证规则实现的方式很灵活,可以直接查看model的实现
            /*
              /// <summary>
              /// 创建用户命令验证
              /// </summary>
              private class RequestValidator : Validator<RegisterViewModel>
              {
                   public override IEnumerable<KeyValuePair<Expression<Func<RegisterViewModel, object>>, string>> RuleFor(RegisterViewModel target)
                   {
                        if (target.Password.IsNullOrWhiteSpace())
                            yield return new KeyValuePair<Expression<Func<RegisterViewModel, object>>, string>(model => model.Password, "密码为空");

                        if (target.UserName.IsNullOrWhiteSpace())
                            yield return new KeyValuePair<Expression<Func<RegisterViewModel, object>>, string>(model => model.UserName, "手机号码为空");

                        if (target.VCode.IsNullOrWhiteSpace())
                            yield return new KeyValuePair<Expression<Func<RegisterViewModel, object>>, string>(model => model.VCode, "短信验证码为空");
                    }
                }
             */

            if (!this.TryValidateModel(model))
            {
                return this.Json("0002", this.ModelErrorMessage);
            }

            //实际上这里是webapi方法,使用代理生成类,带熔断,
            var api = this.validateCodeService.CheckMobileValidateCode(new Message.Contract.Request.CheckMobileValidateCodeReqs
            {
                UsageType = UsageType.找回登录密码,
                Mobile = model.UserName,
                Platform = this.GetAppPlatform(),
                VCode = model.VCode,
            });

            if (api.Status != ApiStatus.Success)
            {
                return this.Json("0002", api.Status == ApiStatus.Fail ? api.Message : "验证码不正确");
            }

            api = this.userService.ChangePassword(new ChangePwdReqs()
            {
                Mobile = model.UserName,
                Password = model.Password,
            });

            if (api.Status != ApiStatus.Success)
            {
                return this.Json("0002", api.Status == ApiStatus.Fail ? api.Message : "修改密码错误");
            }

            return this.Json("0000", "修改成功");
        }

 

在性能方面也不必担心,比如拿json的序列化与反序列化,在反序列化timespan下, 2700x + 32g内存1000万次测试,jsonnet 使用12.6秒(GC=3.7万),easyser使用2.6秒(GC=3.7K),jil使用0.8秒(GC=1.2k)

sqlient 与dapper这一类相似,但是所衍生出的eqsysql,对比ibatis后会发现极少的配置与灵活的使用(如没有了resultmap,alias),你可以将同一个sqlTab语句可使用不同的参数组合生成不同的sql语句,也不用指定查询的返回类型,如QueryForObject<T1>,

QueryForObject<T2>都可以使用相同的sqlTab,其中T1与T2可以是结构体或对象

 

文章导航:

  1. sqlcient 一套容易上手性能又不错的sqlhelper
  2. easySql使用xml管理带事务的orm
  3. ioc工具easyioc

版权声明:本文为shelldudu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/shelldudu/p/10944952.html