从三层架构说起,谈谈对历史项目的小改造

news2024/11/24 3:30:31

在这里插入图片描述

项目背景说明

最近接手一个 “老” 项目的需求修改,项目整体基于 .net core 3.1 平台,以传统的三层架构为基础构建。了解需求后,逐步对原有项目框架进行大概的了解,主要是熟悉一些框架的开发规范,基本工具类库的使用,然后尝试修改业务需求以达到客户方的要求。

刚开始了解业务需求,先大概的了解了一些直观可视化的界面操作和基本菜单目录结构,此时还没来得及看项目框架代码,需求大概清楚后,开始对后端项目框架查看,随之逐步使用。这不使用不知道,一使用吓一跳,针对目前遇到的几个问题,先在这里列举部分:

  1. 通常情况下 BLL 层依赖引用 DAL 层,该项目中刚好与之相反;
  2. 单表的 CRUD 操作基本被封装到基类中(BaseController、BaseBll 和 BaseDal),对多条件查询的提供方法不太灵活(不好用),从基类可以看出,各层职责混淆,并且封装的操作 DB 的基本方法返回值都为 void 。比如在修改一个相对简单的需求,在业务整合的时候,使用框架封装提供的方法反而更麻烦;
  3. BLL 层的具体方法中,看到一个功能大概是批量列表操作的方法,方法内竟然多次循环操作 DB(此处不多说,自己体会)。听同事说,这个 项目刚上线就存在内存泄漏 的情况(感到惊讶!)。
  4. 还有同事在做需求业务修改时,发现 (单体项目)同库环境多表之间的关联操作没有走数据库事务处理,而是直接采用应用程序服务依赖处理(我还没接触到这类需求,暂时不太清楚怎么设计的)。

这个项目唯一的好处就是 模块名称映射,业务模块的命名在前端、服务端再到数据库名称和实体表之间都保持较好的映射规则,这是欣慰的地方。

目前所了解或者听闻的就大概这些,基于以上的这些问题,在不破坏现有的框架编码体系的前提下,又要满足自己修改业务需求时编写代码的顺手体验(专业描述叫 渐进式修改,说难听一点叫偷梁换柱),仔细看了下 DAL(数据访问层)的基本代码,幸好预留了一个 IDbContext 对象,于是基于该对象提供的信息(解析该对象)并引入顺手的 orm 操作(符合框架的扩展),决定不使用框架内置的封装方法。

产生了该想法,说干就干,本文将从以下几个方面来谈谈项目改造和相关的框架说明:

  1. 回顾经典的三层架构(单体项目中常用);
  2. 第三方 DI 框架 Autofac 的使用(项目中看到应用,顺便了解学习);
  3. 解析 DAL 层预留的 IDbContext 对象(在本项目中是 EF 提供的上下文对象),引入更轻量级的 ORM(此处使用 FreeSql);
  4. 最后基于三层架构,简单快速的搭建一个单体项目的框架雏形(实操);

说明:在原有的项目中,目的是跳出框架封装的基本 CRUD 操作方法(不太好用),主要改造点是解析 DAL 层的 IDbContext 对象并引入轻量化的 FreeSql 操作。

三层架构的理解

因为项目业务环境,基本上都是单体三层架构为主,三层架构想必大家都不陌生,在回顾之前我们先来了解生活中一个常见且形象的应用场景,以商场购买猪肉为例,先看下面的图:

商场购买猪肉

通过上图,我们可以很清晰直观的了解到,猪从猪圈里面逐步变成商品被流通市场 的大概过程,并且每一个过程中 职责分工比较明确,那么对应的在我们计算机程序设计中,也是可以依据此来抽象划分各个模块的,如下图所示:

三层架构

从上图中,我们按照【猪 => 工厂 => 商品】的模式抽象,形成了 职责明确的各个模块层,由于项目初期起步规模不大,各个模块组合的单体项目可以部署同一个服务器环境,也可以安装上图分离环境部署。

说明:此处不讨论架构设计,只是针对目前接手业务需求的项目回顾一下基本的三层架构,具体的服务资源部署可以依据公司的业务规模,考虑经济成本且满足当下使用需求即可。个人见解,架构是业务驱动的,而不是被过渡的设计。

上面的架构图中,分别标注了三种角色类型:

  • 客户端(Client),用于向【服务端应用程序】发起请求;
  • 应用服务层(App Server),用于接收 Client 的请求,经过一系列的数据处理,响应相应的反馈结果;
  • 数据服务层(Data Server),主要用于存储应用系统的基础数据和相关业务处理的数据。在该层通常为了减缓 DB 的 I/O 直接交互,通常会引入一个缓存组件(单体环境通常内存缓存,或者分布式部署环境的 Redis)提升应用系统的性能。

这里我们重点说下应用服务层(App Server),分别包含以下几个职责模块:

  • UI 层,接收 Client 的请求,承担展示页面直观的视觉效果和数据校验等相关工作。通常包括:winfrom/wpf/.aspx/.cshtml/.html 等。在前后端分离的项目中,相对前端应用程序来说,后端提供的 webapi/controller 层即代表该层。
  • BLL 层,接收 Client 的数据后,通常情况下使用 IBLL 定义接口规范,然后在 BLL 层实现相应的业务逻辑处理方法(依赖 IDAL 层提供的数据),比如方法或者服务的整合等。
  • DAL 层,提供对 DB 数据库的访问操作(数据源相关环节交涉),通常情况也在 IDAL 层定义接口规范,然后在 DAL 层实现对应的数据访问操作方法(比如单表的 CRUD 操作)。通常该层会借助一些 ORM 辅助类库,比如:ADO.NET、DbHelper/SqlHelper、FreeSql 、EF/EF Core、Dapper 等。
  • Model 层(数据模型的载体),为了更佳细化的分类规制,此处暂时考虑分三类模型,分别是 BaseEntity、BusinessEntity 和 ViewEntity
  • Common 层(通用类库和工具助手),该层有自定义封装整合的,也有依赖外部 Nuget 包封装处理的,依据项目业务情况按需获取组装。

基于上面的架构图,一个单体环境下基本的框架雏形就可以搭建了,但具体落地项目还需考虑以下几点细节和原则(包括但不限于):

  1. 系统开发的 命名规范,建议各个业务模块,在数据库表设计、前后端应用程序里面一一映射,这样可以很直观、方便的上手;
  2. 请求入口处 参数合法性的基础校验(必备常识),无论前端部分还是服务端部分,参数的校验还是很有必要性的。无效的参数,可能会导致程序的异常;
  3. 统一的入参格式,比如请求参数 JSON 格式化,遵循 HTTP/RESTful API 风格;
  4. 统一的数据响应载体,对比原生的数据格式返回,很多情况下的 null 结果无法确定接口在业务意义的成功或失败。
  5. 统一的异常处理机制和数据格式,通常采用 AOP 思想全局异常捕获(ExceptionFilterAttribute 异常过滤器),数据信息推荐 JSON 格式化记录;
  6. 系统日志的记录(数据建议 JSON 格式化),通常情况下会在框架层面采用 AOP 方式获取用户在系统中操作的全生命周期数据记录,也可提供日志写入方法,在关键业务逻辑处精细化的记录逻辑操作信息。从一定方面可以起到还原 “真相” 的保障;
  7. 整体遵循 单一职责原则SRP:Singleresponsibilityprinciple),该点也是最难做到的理想化指导原则,在编写业务方法的时候,一个方法尽量做到功能单一,比如复杂的业务处理,可以使用每个相关的单一方法整合;
  8. 依赖抽象,不应依赖具体实现,这也是 开-闭原则OCP:Open - Close Principle)的体现。比如:Controller => IBLL 接口规范 / BLL 具体实现 => IDAL 接口规范 / DAL 具体实现。还有在框架中无处不在的 DI 应用;

说明:此处只是列举部分比较常见或者基本必备的点,框架设计还有很多细节考虑,这里不再详细论述。

任何框架无论封装的如何优秀,关键还是在于局中 “玩游戏” 的开发者,框架只是提供了基本的开发规则,要大家共同的遵循,这其中最难的就是团队小伙伴达成一致的思维认知和共识。约定由于配置,任何框架不可能面面俱到,过渡设计框架务必会失去部分灵活性(物极必反的道理想必大家都知道),个人建议框架架构设计应该以 业务为驱动、技术为主导、约定和思想共识为辅助、开发规范为底线 这几个方面加强。

Autofac 基本概述

Autofac 官方地址 => https://autofac.org/

了解到项目的基本情况后,由于项目是 .net core 3.1 平台构建的,与老平台的 .netfx 相比,变化最大有以下几点(这里唠嗑一下):

  1. 框架平台的福利:开源、跨平台、高性能(看怎么使用,比如批量列表操作直接多次循环 DB 操作,这样的玩法神仙框架也无解);
  2. 无处不在的 DependencyInjection(DI,依赖注入),最直观的使用体验就是解放了了传统的 new实例化对象;
  3. 灵活的 Middleware(中间件)和透明的 HTTP Pipeline (http 管道)机制;

这里只说下 DI 依赖注入,在基本简单的注入方面 asp.net core 框架中默认提供的 DI 很方便,但在有些场景下就没有第三方的 DI 更佳灵活方便了。

默认 DI 框架

  • Microsoft.Extensions.DependencyInjection.Abstractions(容器的抽象)
  • Microsoft.Extensions.DependencyInjection(容器的具体实现)

当你要在自己的程序中使用时,使用nuget包管理工具安装 Microsoft.Extensions.DependencyInjection 包即可。

老牌第三方 DI 框架 Autofac

  • Autofac.Extensions.DependencyInjection

当然这里还有其他第三方 DI 框架,这里不再一一列举,感兴趣的小伙伴自行查看相关资料。

Autofac 基本知识点

这里先简单的回顾下 Autofac 的基本知识点 :

  1. Autofac 框架 DI 生命周期
  2. Autofac 框架 DI 注入方式
  3. Autofac 在 asp.net core 框架中的应用(简单提下,预留在下面的项目改造中描述)。

Autofac 框架 DI 的生命周期

  • InstancePerDependency(瞬时的),与 Microsoft DI 容器中的 Transient 类似,每次调用都会生成新的实例,这种模式也是 Autofac 的默认模式
  • SingleInstance(单例),和 Microsoft DI 容器中的 Singleton 类似,所有的调用均返回同一个实例;
  • InstancePerLifetimeScope(作用域),与 Microsoft DI 容器中的 Scoped 类似,它在一个作用域中获取的实例相同,在 asp.net core 中常用(大多数情况下推荐使用);
  • InstancePerMatchingLifetimeScope (匹配作用域),与 InstancePerLifetimeScope 类似,但是它支持对实例共享进行更精细的控制;
  • InstancePerRequest (每次请求一个实例),在老旧的 asp.net webformasp.net mvc 中使用,此处不再介绍;
  • InstancePerOwned (Owned 隐式关系类型创建了一个新的嵌套生命周期Scope)
  • ThreadScope (线程作用域),代表每个线程的一个实例,对于多线程场景,必须非常小心,不要在派生线程下释放父作用域。如果您生成了线程,然后释放了父作用域,那么可能会陷入一个糟糕的情况,即无法解析组件。
// 创建容器对象实例
var builder = new ContainerBuilder();

// 1、InstancePerDependency(瞬时的)
// 注册 Worker 类为 InstancePerDependency(瞬时的),每次获取都会生成新的实例
builder.RegisterType<Worker>().InstancePerDependency();
// 如果不指定与 InstancePerDependency(默认的模式)相同
builder.RegisterType<Worker>();

// 2、SingleInstance(单例)
// 注册 Worker 类为 SingleInstance(单例),每次获取均返回同一个实例
builder.RegisterType<Worker>().SingleInstance();

// 3、InstancePerLifetimeScope(作用域)
// 注册 Worker 类为 InstancePerLifetimeScope(作用域),
// 在同一个作用域中获得的是相同实例,在不同作用域获得的是不同实例
builder.RegisterType<Worker>().InstancePerLifetimeScope();
// InstancePerLifetimeScope(作用域)实例对象验证
using(var scope1 = container.BeginLifetimeScope())
{
  for(var i = 0; i < 100; i++)
  {
    // 在 scope1 中获取的 Worker 都是同一个实例
    var w1 = scope1.Resolve<Worker>();
  }
}

using(var scope2 = container.BeginLifetimeScope())
{
  for(var i = 0; i < 100; i++)
  {
    // 在 scope2 中获取的 Worker 都是同一个实例
    // 在 scope2 中获取的 Worker 实例和 scope1 中获取的 Worker 实例不相同,因为他们是两个不同的作用域
    var w2 = scope2.Resolve<Worker>();
  }
}

using(var scope3 = container.BeginLifetimeScope())
{
  var w3 = scope3.Resolve<Worker>();
  using(var scope4 = scope3.BeginLifetimeScope())
  {
    // w3 和 w4 是不同的实例,因为他们是在不同的作用域中请求的
    var w4 = scope4.Resolve<Worker>();
  }
}

var w5 = container.Resolve<Worker>();
using(var scope5 = container.BeginLifetimeScope())
{
  // w5 和 w6 不同
  // Scope 是一个生命周期范围,如果从 Scope 中解析一个 InstancePerLifetimeScope 服务,
  // 该实例将在 Scope 的持续时间内存在,并且实际上是一个单例
  // 它将在容器的生命周期内被保存,以防止其他对象试图从容器解析 Worker
    
  // 解释:注册为 InstancePerLifetimeScope 的服务,在每个 Scope 中请求类似于请求单例,
  // 在这个单例的生命周期于 Scope 的生命周期相同,在 Scope 中请求对应实例则返回对应的单例,
  // 这样就避免冲突,每个Scope请求的都是自己的实例
  var w6 = scope5.Resolve<Worker>();
}

// 4、InstancePerMatchingLifetimeScope (匹配作用域)
// 当您创建一个嵌套(多层级)的生命周期 Scope 时,您可以 “标记” 或 “命名” 该 Scope。
// 每个匹配标记的生命周期 Scope 作用域内最多只有一个与给定名称匹配的服务实例,包括嵌套的生命周期Scope。
// 这允许您创建一种“作用域单例”,在这种单例中,其他嵌套的生命周期作用域可以共享组件的实例,而无需声明全局共享实例。
builder.RegisterType<Worker>().InstancePerMatchingLifetimeScope("my-request");
// 创建标记的作用域Scope
using(var scope1 = container.BeginLifetimeScope("my-request"))
{
  for(var i = 0; i < 100; i++)
  {
    var w1 = scope1.Resolve<Worker>();
    using(var scope2 = scope1.BeginLifetimeScope())
    {
      var w2 = scope2.Resolve<Worker>();

      // w1 和 w2 的实例总是相同的,因为使用 InstancePerMatchingLifetimeScope 且为指定标记的Scope,
      // 嵌套的生命周期作用域可以共享实例,它实际上在标记作用域中是一个单例
    }
  }
}

// 创建另一个标记的作用域 Scope
using(var scope3 = container.BeginLifetimeScope("my-request"))
{
  for(var i = 0; i < 100; i++)
  {
    // w3和w1/w2是不同的实例,因为他们是两个不同的生命周期,虽然它们的标记相同
    // InstancePerMatchingLifetimeScope 依然是在不同的生命周期作用域创建新的实例
    var w3 = scope3.Resolve<Worker>();
    using(var scope4 = scope3.BeginLifetimeScope())
    {
      var w4 = scope4.Resolve<Worker>();
      // w3和w4是相同的实例,因为使用 InstancePerMatchingLifetimeScope 且为指定标记的 Scope
      // 嵌套的生命周期作用域可以共享实例
      // w3和w4是同样的实例,w1和w2是同样的实例,但是w1/w2和w3/w4是不同的实例,因为他们是两个不同的作用域 Scope
    }
  }
}

// 注意:不能在标记不匹配的生命周期 Scope 中获取 InstancePerMatchingLifetimeScope 中标记的实例会抛出异常。
using(var noTagScope = container.BeginLifetimeScope())
{
   // 在没有正确命名(标记)的生命周期Scope的情况下试图解析每个匹配生命周期Scope的组件,会抛出异常
  var fail = noTagScope.Resolve<Worker>();
}

// 5、InstancePerOwned
// Owned隐式关系类型创建了一个新的嵌套生命周期Scope。
// 可以使用每个拥有的实例注册将依赖关系限定到拥有的实例,简单讲就是将依赖注入限定到对应泛型实例
builder.RegisterType<MessageHandler>();
builder.RegisterType<ServiceForHandler>().InstancePerOwned<MessageHandler>();
using(var scope = container.BeginLifetimeScope())
{
  // MessageHandler 依赖 ServiceForHandler,它们的生命周期处于scope(当前Scope的名称)下的子生命周期范围内
  var h1 = scope.Resolve<Owned<MessageHandler>>();
  // 但是 InstancePerOwned 的实例需要自己处理,所以这里需要手动释放
  h1.Dispose();
}

// 6、ThreadScope (线程作用域)
builder.RegisterType<MyThreadScopedComponent>().InstancePerLifetimeScope();
var container = builder.Build();
void ThreadStart()
{
  using (var scope = container.BeginLifetimeScope())
  {
    // 从容器的一个子作用域解析服务
    var thisThreadsInstance = scope.Resolve<MyThreadScopedComponent>();
    // 还可以创建嵌套的作用域
	using(var unitOfWorkScope = scope.BeginLifetimeScope())
	{
	    var anotherService = unitOfWorkScope.Resolve();
	}
  }
}

Autofac 框架 DI 注入方式

  • RegisterType,类型注入
  • RegisterInstance,实例注入
  • Lambda 表达式注入
  • Property 属性注入
  • RegisterGeneric,泛型注入
  • 多种类型注入
  • 条件注入(Autofac 4.4+ 引入)
var builder = new ContainerBuilder();

// 1、RegisterType 类型注入
// 可以通过泛型的方式直接注册对应的类型
builder.RegisterType<ConsoleLogger>();
// 也可以通过 typeof 运算符得到对应的类型作为参数提供给 RegisterType 方法,这种方式在注册泛型类型时非常有用
builder.RegisterType(typeof(ConfigReader));
// 通过 UsingConstructor 指定【构造函数】中传递的类型,以确定使用与之对应参数的构造函数实例化对象
builder.RegisterType<MyComponent>()
       .UsingConstructor(typeof(ILogger), typeof(IConfigReader));

// 2、实例注入
// 预先得到一个实例
var output = new StringWriter();
// 通过 RegisterInstance 将实例注入到容器中,在通过容器获取 TextWriter 的实例时就会获得到 output 这个实例
builder.RegisterInstance(output).As<TextWriter>();
// Autofac会自己管理实例的生命周期,如果注册为瞬时的,那么这个实例在获取一次后就会被调用其对应的Dispose方法,
// 如果希望自己控制对象的生命周期,在注入时需要跟上 ExternallyOwned() 方法
builder.RegisterInstance(output)
       .As<TextWriter>()
       .ExternallyOwned(); // 使用 ExternallyOwned 方法告知 Autofac 这个被注入的实例对象的生命周期由自己掌控,不需要自动调用 Dispose 方法
// 将一个单例实例注入到容器中,其他被注入的对象就可以直接获取到这个单例,Autofac也不会释放这个单例
builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();

// 3、Lambda 表达式注入
// 反射是一种很好的创建依赖注入的方式,但是有时候需要注入的对象并不是使用简单的无参构造函数实例化一个对象,
// 它还需要一些其他的参数或者动作来得到一个对应的实例,这时候可以使用Lambda表达式注入。

// 在容器中注入 A,但是 A 不是使用无参构造函数获得实例的,它使用从 Autofac 中取出的一个 B 对象实例作为参数,调用需要一个 B 对象实例的构造函数
builder.Register(c => new A(c.Resolve<B>()));
// 这里的c是一个IComponentContext对象,通过 IcomponentContext 对象可以从Autofac容器中解析出相应的对象,然后作为实参提供给A对象的构造函数

// Lambda表达式对复杂参数的注入非常有用
// 有时候构造函数并不是简单的一个固定参数,而可能是一个变化的情况,如果没有这种方式,可能就需要复杂的配置文件才能完成对应的功能
builder.Register(c => new UserSession(DateTime.Now.AddMinutes(30)));

// 4、Property 属性注入
// 相比上面直接在构造时给属性赋值,Autofac 有更优雅的属性注入。
// 给 A 实例的 MyB 属性赋一个从容器中解析出来的 B 实例
builder.Register(c => new A(){ MyB = c.ResolveOptional<B>() });

// 属性注入在以下情况特别有用
// 通过参数值选择实现能够提供一种运行时选择,它不仅仅是最开始时的参数决定的,这个参数在运行时也是可以改变以返回不同的实现
builder.Register<CreditCard>(
  (c, p) => {
      var accountId = p.Named<string>("accountId");
      if (accountId.StartsWith("9"))
      {
        return new GoldCard(accountId);
      }
      else
      {
        return new StandardCard(accountId);
      }
  });
// 此处推荐使用工厂模式,通过传入工厂委托的方式获取不同的实例。

// 5、RegisterGeneric 泛型注入
// 通常情况 IoC 容器都支持泛型注入,Autofac 支持以特别的语法强调特别的泛型,它的优先级比默认的泛型高,但是性能没有默认的泛型好,因为不能缓存。
// IoC 容器获取 IRepository<T> 类型的实例时容器会返回一个 NHibernateRepository<T> 实例
builder.RegisterGeneric(typeof(NHibernateRepository<>))
       .As(typeof(IRepository<>))
       .InstancePerLifetimeScope();

// 6、多种类型注入
// 有时候一个实例对应很多接口,通过不同的接口请求到的实例都是同一个实例,那么可以指定实例对应的类型
// 注意:要注册多个接口,那么具体类必须实现继承的这些接口。
// 通过 ILogger 和 ICallInterceptor 得到的都是 CallLogger 实例
builder.RegisterType<CallLogger>()
       .As<ILogger>()
       .As<ICallInterceptor>();

// 多种类型注入,涉及到多个服务注入的选择
// 如果一个类型进行了多个实例的注册,Autofac 默认以最后一次注入的为准
// 请求ILogger实例容器返回 ConsoleLogger 实例
builder.RegisterType<ConsoleLogger>().As<ILogger>();
// 请求 ILogger 实例容器返回 FileLogger 实例
builder.RegisterType<FileLogger>().As<ILogger>();
// 最终请求ILogger实例容器返回的是FileLogger实例,Autofac 默认以最后的为准

// 对于上面的情况,如果需要手动指定默认的,而不是使用最后一个,可以使用 PreserveExistingDefaults() 修饰
builder.RegisterType<ConsoleLogger>().As<ILogger>().PreserveExistingDefaults();
builder.RegisterType<FileLogger>().As<ILogger>(); // 此时该注入就无效了
// 最终请求 ILogger 实例容器返回的是 ConsoleLogger 实例,因为使用了PreserveExistingDefaults()修饰

// 7、条件注入
// 条件注入在 Autofac4.4 引入,使用 4.4 以后的版本可以使用
// 大多数情况下,如果一个类型注入了多个实现,使用PreserveExistingDefaults()手动指定就够了,但是有时候这还不够,这时候就可以使用条件注入

// 请求 IService 接口得到 ServiceA 
builder.RegisterType<ServiceA>()
       .As<IService>();
// 请求 IService 接口得到 ServiceB
builder.RegisterType<ServiceB>()
       .As<IService>()
       // 仅当 IService 没有注册过才会注册
       .IfNotRegistered(typeof(IService));
// 最后请求 IService 获得的实例是 ServiceA

builder.RegisterType<HandlerA>()
       .AsSelf()
       .As<IHandler>()
       // 注册 HandlerA 在 HandlerB 之前,所以检查会认为没有注册
       // 最后这条注册语句会成功执行
       .IfNotRegistered(typeof(HandlerB));
builder.RegisterType<HandlerB>()
       // 注册自己的类型,即 HandlerB
       .AsSelf()
       .As<IHandler>();
builder.RegisterType<HandlerC>()
       // 注册自己的类型,即 HandlerC
       .AsSelf()
       .As<IHandler>()
       // 不会执行,因为 HandlerB 已经注册了
       .IfNotRegistered(typeof(HandlerB));

// 注册 IManager
builder.RegisterType<Manager>()
       .As<IManager>()
      // 仅当IService和HandlerB都注册了对应服务时才会执行
       .OnlyIf(reg =>
         reg.IsRegistered(new TypedService(typeof(IService))) &&
         reg.IsRegistered(new TypedService(typeof(HandlerB))));

项目改造

说明:此处项目改造并非是在原来项目中改造,这里只是基于三层架构的理解,快速搭建一个基本的基础框架。这里尽量描述部分细节改造,注重思路循序渐进的理解。

项目准备工作或改造步骤:

  1. 三层架构基础框架搭建(展示项目结构);
  2. CommonHelper 层单例对象构造器创建;
  3. DbHelper 层使用单例构造器封装 IFreeSql 对象;
  4. WebAPI 层【Startup.cs + Program.cs】改造;
  5. WebAPI 层 Swagger 配置使用;
  6. WebAPI 层 Autofac 在 asp.net core 中的应用;
  7. WebAPI 层请求入参的校验(FluentValidation);
  8. 结构化日志记录 Serilog;
  9. AutoMapper 的使用(DTO 数据模型转换);

在实际项目应用中,使用到的 nuget 依赖包可能不只这些(还有其他未列举的),依据项目实际使用情况引入,合理封装到对应项目层中使用即可。

说明:其中第 5、7、8、9 点此处不做详述,这些类库的使用自行查阅相关资料。

  • FluentValidation 是一个非常流行的构建强类型验证规则的 .NET 库,这里不做讲述,可以参考该文章:https://www.cnblogs.com/lwqlun/p/10311945.html
  • Serilog 是 .NET 中著名的结构化日志类库。
  • Swagger 在 asp.net core 中默认集成 Swashbuckle.AspNetCore。

项目框架目录结构

依据架构图规划,搭建项目框架和依赖关系目录结构如下:

框架目录结构

CommonHelper 层

  • 单例对象构造器,SingletonConstructor
namespace Jeff.Mes.Common;

/// <summary>
/// 单例对象构造器
/// </summary>
/// <typeparam name="T"></typeparam>
public class SingletonConstructor<T> where T : class, new()
{
    private static T? _Instance;
    private readonly static object _lockObj = new();

    /// <summary>
    /// 获取单例对象的实例
    /// </summary>
    /// <returns></returns>
    public static T GetInstance()
    {
        if (_Instance != null) return _Instance;
        lock (_lockObj)
        {
            if (_Instance == null)
            {
                var item = System.Activator.CreateInstance<T>();
                System.Threading.Interlocked.Exchange(ref _Instance, item);
            }
        }
        return _Instance;
    }
}

DbHelper 层

说明:该层可以使用工厂模式,封装支持更多的 DB 类型,此处主要是描述下 FreeSql 对象的单例模式构建。这里推荐下 FreeSql 轻量级 ORM,支持多种关系型 DB,切换数据源方便,基本保持一致的使用体验,遵循 MIT 协议开源,上手简单方便,性能也不差,测试用例覆盖较全面。

该层可以添加 dbconfig.json 配置文件,考虑相对安全,该文件配置的字符串连接信息,可以使用密文编码(比如:base64编码,或者采用其他加密方式生产的密文)。

  • dbconfig.json 配置文件内容,此处依据自己的喜好定义,注意对敏感信息适当的安全考虑。
{
 "ConnectionStrings": {
    "Development": {
      "DbType": "SqlServer",
      "ConnectionString": ""
    },
    "Production": {
      "DbType": "SqlServer",
      "ConnectionString": ""
    }
  }
}
  • 使用 SingletonConstructor 构建 FreeSqlHelper
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using FreeSql;
using Jeff.Mes.Common;

namespace Jeff.Mes.DbHelper;

/// <summary>
/// 【Singleton 单例模式】构建 freesql 对象
/// </summary>
public sealed class FreeSqlHelper : SingletonConstructor<FreeSqlHelper>
{
    //连接字符串作为 key,存储构建的 IFreeSql 对象的字典集合
    private readonly static Dictionary<string, IFreeSql> _FreeDic = new();

    /// <summary>
    /// 构建 freesql 对象
    /// </summary>
    /// <param name="dbType">DB类型</param>
    /// <param name="connStr">连接字符串</param>
    /// <returns>IFreeSql</returns>
    public IFreeSql? FreeBuilder(string dbType, string connStr)
    {
        if (string.IsNullOrWhiteSpace(dbType) || string.IsNullOrWhiteSpace(connStr))
        {
            return default;
        }

        bool hasKey = _FreeDic.ContainsKey(connStr);
        if (hasKey)
        {
            return _FreeDic[connStr];
        }

        IFreeSql fsql;
        string myDbType = dbType.Contains('.') ? dbType.Substring(dbType.LastIndexOf('.') + 1) : dbType;
        switch (myDbType)
        {
            case "MySql":
                fsql = new FreeSqlBuilder()
                    .UseConnectionString(DataType.MySql, connStr)
                    .UseAutoSyncStructure(false) //自动同步实体结构到数据库
                    .Build(); //请务必定义成 Singleton 单例模式
                break;

            default:
                fsql = new FreeSqlBuilder()
                   .UseConnectionString(DataType.SqlServer, connStr)
                   .UseAutoSyncStructure(false) //自动同步实体结构到数据库
                   .Build(); //请务必定义成 Singleton 单例模式
                break;
        }

        bool isAdd = _FreeDic.TryAdd(connStr, fsql);
        if (isAdd)
        {
            return fsql;
        }
        else
        {
            fsql.Dispose();
            return _FreeDic[connStr];
        }
    }

    public IFreeSql? FreeBuilder(DataType dbType, string connStr) 
    {
        if (string.IsNullOrWhiteSpace(connStr))
        {
            return default;
        }

        /*
        bool hasKey = _FreeDic.ContainsKey(connStr);
        if (hasKey)
        {
            return _FreeDic[connStr];
        }*/

        bool isOk = _FreeDic.TryGetValue(connStr, out IFreeSql? fsql);
        if (isOk)
        {
            return fsql;
        }

        fsql = new FreeSqlBuilder()
                    .UseConnectionString(dbType, connStr)
                    .UseAutoSyncStructure(false) //自动同步实体结构到数据库
                    .Build(); //请务必定义成 Singleton 单例模式 

        bool isAdd = _FreeDic.TryAdd(connStr, fsql);
        if (isAdd)
        {
            return fsql;
        }
        else
        {
            fsql.Dispose();
            return _FreeDic[connStr];
        }
    }

    public (bool isOk, IFreeSql? fsql) GetFreeSql(DataType dbType, string connStr) 
    {
        bool isOk = _FreeDic.TryGetValue(connStr, out IFreeSql? fsql);
        if (!isOk) 
        {
           fsql = FreeBuilder(dbType, connStr);
           isOk = fsql != null;
        }
        return (isOk, fsql ?? default);
    }

    /// <summary>
    /// 反射获取【IDbContext】对象信息
    /// </summary>
    /// <typeparam name="T">IDbContext</typeparam>
    /// <param name="t">IDbContext</param>
    /// <returns>Dictionary(string,string)</returns>
    public static Dictionary<string, string> GetProperties<T>(T t) where T : class
    {
        Type type = t.GetType();
        var sb = new StringBuilder();
        foreach (PropertyInfo property in type.GetProperties().OrderBy(p => p.Name))
        {
            object? obj = property.GetValue(t, null);
            if (obj == null)
            {
                continue;
            }
            else
            {
                if (string.IsNullOrWhiteSpace(obj.ToString()))
                {
                    continue;
                }
            }
            sb.Append(property.Name + "=");

            if (property.PropertyType.IsGenericType)
            {
                var listVal = property.GetValue(t, null) as IEnumerable<object>;
                if (listVal == null) continue;
                foreach (var item in listVal)
                {
                    sb.Append(GetProperties(item));
                }
            }
            else if (property.PropertyType.IsArray)
            {
                var listVal = property.GetValue(t, null) as IEnumerable<object>;
                if (listVal == null) continue;
                foreach (var item in listVal)
                {
                    sb.Append(GetProperties(item));
                }
            }
            else
            {
                sb.Append(property.GetValue(t, null));
                sb.Append("&");
            }
        }

        var dic = new Dictionary<string, string>();
        var sbArray = sb.ToString().Trim('&').Split('&');
        foreach (var item in sbArray)
        {
            if (item.Contains('='))
            {
                int count = Regex.Matches(item, "=").Count;
                if (count <= 1)
                {
                    var itemArray = item.Split('=');
                    dic.Add(itemArray[0], itemArray[1]);
                }
                else
                {
                    int index = item.IndexOf('=');
                    string key = item.Substring(0, index);
                    string val = item.Substring(index + 1);
                    dic.Add(key, val);
                }
            }
        }
        return dic;
    }
}

在原始项目中解析 IDbContext 对象的方法就是 GetProperties() ,该方法借助【反射 + 递归】获取上下文对象,对于 FreeSql 对象实例而言,只需获取 db 的连接字符串信息即可快速使用,而 DB 字符串连接信息就存储于 IDbContext 对象中。

原始项目 Dal 层中预留的代码:

 public class OrderDal : BaseDal<OrderInfo>, IOrderDal
 {
     public OrderDal(IDbContext dbContext, ILoggerFactory loggerFactory) : base(dbContext, loggerFactory) 
     {}
 }

在项目改造之前,该 Dal 层中需要引入相应 DBFreeSql 包,如下格式:

  • FreeSql.Provider.Xxx(Xxx 是数据库类型名称)

添加 nuget 依赖包后,原始项目 Dal 层改造后的代码如下:

public class OrderDal : BaseDal<OrderInfo>, IOrderDal
{
    private readonly IDbContext _IDbContext;
    
    public OrderDal(IDbContext dbContext, ILoggerFactory loggerFactory) : base(dbContext, loggerFactory) 
    {
        _IDbContext = dbContext;
    }

    public (IDbContext dbContext, IFreeSql fsql) GetDbContext()
    {
        /*
        ((Xxx.Framework.Dal.EFDbContext)_IDbContext).ConnStr
        ((Microsoft.EntityFrameworkCore.DbContext)ss).Database.ProviderName
        */

        //-	Database {Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade}	
        DatabaseFacade dbFacade = _IDbContext.GetProperty<Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade>("Database");
        var dic = FreeSqlHelper.GetProperties(_IDbContext);
        dic.Add("DbType", dbFacade.ProviderName);
        IFreeSql fsql = FreeSqlHelper.GetInstance().FreeBuilder(dic["DbType"], dic["ConnStr"]);

        return (_IDbContext, fsql);
    }
}

IOrderDal.cs 文件中添加如下代码:

public interface IOrderDal : IBaseDal<OrderInfo>,IDependency
{
    /// <summary>
    /// 获取 DB 上下文对象,并构建 IFreeSql 实例
    /// </summary>
    /// <returns></returns>
    public (IDbContext dbContext, IFreeSql fsql) GetDbContext();
}

在 BLL 层中方法中的调用,代码如下:

public class OrderBll : BaseBll<OrderInfo>, IOrderBll
{
    private readonly IOrderDal _orderDal;
    public OrderBll(IOrderDal orderDal)
    {
       _orderDal = orderDal; //构造函数注入 IOrderDal 
    }

    public (bool state, List<OrderInfo> orderInfos) Test()
    {
        var (dbContext, fsql) =  _ppsOrderDal.GetDbContext();
        // 此处获取到构建的 fsql 对象,即可使用。
    }
}  

有了 FreeSql 的引入,从此在 BLL层就可以方便的编写操作业务方法了,不再局限于框架内提供的方法。此处利于 Dal 层预留的 IDbContext 进行扩展,不修改上层项目的玩法,该怎么用还是怎么用。

WebApi 层

WebApi 中添加 Nuget 包:

  • Autofac.Extensions.DependencyInjection v8.0.0
  • Swashbuckle.AspNetCore v6.4.0

接下来逐步改造以下两点:

  • MiniAPI 改造【Startup.cs + Program.cs】模式;
  • asp.net core 中使用 Autofac

首先新建 Startup.cs 文件,添加如下代码:

using Autofac;
using Autofac.Extensions.DependencyInjection;
using Jeff.Mes.WebApi.Modules;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Jeff.Mes.WebApi;

public class Startup
{
    public IConfiguration Configuration;
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    // Add services to the container. 注册服务到 Ioc 容器
    public void RegisterServices(IServiceCollection services, IHostBuilder host)
    {
        #region 在 host 中注册 Autofac
        host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
        host.ConfigureContainer<ContainerBuilder>(builder =>
        {
            builder.RegisterModule<AutofacModule>(); //此处编写相关服务&属性的注入 
        });
        services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
        #endregion

        host.ConfigureAppConfiguration((hostContext, config) => {
            var env = hostContext.HostingEnvironment;
            string path = Path.Combine(env.ContentRootPath, "Configuration");
            config.SetBasePath(path)
                  .AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true)
                  .AddJsonFile(path: $"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
        });

        services.AddControllers();
        // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen();
    }

    // Configure the HTTP request pipeline. 配置 HTTP 请求管道(中间件管道即中间件委托链)
    public void SetupMiddlewares(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }
        
        app.UseRouting();
        app.UseAuthorization();

        app.UseEndpoints(endpoints => {
            endpoints.MapControllers();
            endpoints.MapGet("/env", async context =>
            {
                // 获取环境变量信息
                await context.Response.WriteAsync(
                    $"EnvironmentName:{env.EnvironmentName},IsDevelopment:{env.IsDevelopment()}"
                );
            });
        });
    }
}

其次修改 Program.cs 文件中的代码,修改如下:

namespace Jeff.Mes.WebApi;

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        var startup = new Startup(builder.Configuration);

        var services = builder.Services;
        var host =  builder.Host;
        startup.RegisterServices(services, host);

        var app = builder.Build();
        var env = builder.Environment;
        startup.SetupMiddlewares(app, env);

        await app.RunAsync();
    }
}

接下来在项目中新建 Modules 文件夹,分别存放如下文件:

  • AutowiredAttribute.cs,该文件用于标记属性注入的特性,和 AutowiredPropertySelector.cs 文件搭配使用;
  • AutowiredPropertySelector.cs,该文件主要用于自定义属性的选择器;
  • AutofacModule.cs,该文件主要定义 Autofac 中业务类的注入方式;

此处主要描述 Autofacasp.net core 项目中的使用,同时还自定义了特性标记的属性注入模式。

首先新建 Modules 文件夹,用于存放上面的 .cs 文件,各文件的完整代码分别如下:

1、AutowiredAttribute.cs 文件代码:

namespace Jeff.Mes.WebApi.Modules;

[AttributeUsage(AttributeTargets.Property)]
public class AutowiredAttribute : Attribute
{ }

2、AutowiredPropertySelector.cs 文件代码:

using Autofac.Core;
using System.Reflection;

namespace Jeff.Mes.WebApi.Modules;

public class AutowiredPropertySelector : IPropertySelector
{ 
    // 属性注入
    public bool InjectProperty(PropertyInfo propertyInfo, object instance)
    {
        // 自定义属性特性标记 [Autowired] 的才生效
        return propertyInfo.CustomAttributes.Any(it => it.AttributeType == typeof(AutowiredAttribute));
    }
}

3、AutofacModule.cs 文件代码:

using Autofac;
using Jeff.Mes.Bll.BLL;
using Jeff.Mes.Bll.IBLL;
using Microsoft.AspNetCore.Mvc;

namespace Jeff.Mes.WebApi.Modules;

public class AutofacModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        // The generic ILogger<TCategoryName> service was added to the ServiceCollection by ASP.NET Core.
        // It was then registered with Autofac using the Populate method. All of this starts
        // with the `UseServiceProviderFactory(new AutofacServiceProviderFactory())` that happens in Program and registers Autofac
        // as the service provider.

        #region 此处编写服务(BaseServiceRegister)的注册规则
        // ValuesService 构造函数有参
        builder.Register(c => new ValuesService(c.Resolve<ILogger<ValuesService>>(), c.Resolve<IConfiguration>()))
            .As<IValuesService>()
            .InstancePerLifetimeScope();

        /*
        // ValuesService 构造函数无参
        builder.Register<ValuesService>().As<IValuesService>(); //不声明生命周期默认是瞬态。
        */

        builder.Register(c => new ValuesService(c.Resolve<ILogger<ValuesService>>(), c.Resolve<IConfiguration>()))
          .As<IValuesService>()
          .InstancePerLifetimeScope();

        builder.RegisterType<UserService>()
            .As<IUserService>()
            .InstancePerLifetimeScope();
        builder.RegisterType<UserService>()
            .As<IUserService>()
            .PropertiesAutowired()
            .InstancePerLifetimeScope();
        builder.Register(c => new UserService())
            .As<IUserService>()
            .PropertiesAutowired()
            .InstancePerLifetimeScope();

        //builder.RegisterType<OrdersService>().As<IOrdersService>().InstancePerLifetimeScope();
        builder.Register(c => new OrdersService(c.Resolve<ILogger<OrdersService>>(), c.Resolve<IConfiguration>()))
          .As<IOrdersService>()
          .InstancePerLifetimeScope();

        #endregion

        #region 此处编写属性(PropertiesAutowired)的注册规则
        var controllerBaseType = typeof(ControllerBase);

        // 说明:以下 1、2 两种方式等效。

        // 1、获取所有控制器类型并使用属性注入
        var controllersTypesInAssemblyAll = typeof(Startup).Assembly.GetExportedTypes()
            .Where(type => typeof(ControllerBase).IsAssignableFrom(type)).ToArray();
        builder.RegisterTypes(controllersTypesInAssemblyAll).PropertiesAutowired();

        // 2、自定义特性并使用属性注入
        /*
        builder.RegisterAssemblyTypes(typeof(Program).Assembly)
            .Where(type => controllerBaseType.IsAssignableFrom(type) && type != controllerBaseType)
            .PropertiesAutowired(new AutowiredPropertySelector());
        */

        // 2.1 从 Startup 程序集中筛选出 ControllerBase 派生类程序集(ControllerBase 类中的程序集,并且排除本身 ControllerBase)
        var controllersTypesInAssembly = typeof(Startup).Assembly.GetExportedTypes()
            .Where(type => controllerBaseType.IsAssignableFrom(type) && type != controllerBaseType).ToArray();
        // 2.2 注册筛选出的 ControllerBase 派生类的程序集,使用自定义特性标注的属性注入
        builder.RegisterTypes(controllersTypesInAssembly)
             .PropertiesAutowired(new AutowiredPropertySelector());
        #endregion
    }
}

接下来(习惯性)新增一个 Configuration 文件夹,用于存放 appsettings.json 相关配置文件。因为上面的项目中我们在 host.ConfigureAppConfiguration 方法中调整了文件路径。

其他项目层没啥好讲的,从架构图中即可看出对应的功能职责,到这里遵循架构图规则搭建的基础项目结构就基本完成了,该文章主要目的是相对于原始项目做一个对照,重新梳理了一遍经典的三层架构,方便初学人员有一个基础的模型参照。

总结

在项目实战开发中,理解三层架构并遵循该架构合理化的搭建项目基础框架是必要保障,而不是 DAL 层项目去依赖 BLL 层项目(ಥ_ಥ ),每一层尽量做到职责分明,各司其职,各个项目层之间协调配合,项目层之间的调用应该依赖抽象而非具体实现,该项目框架的基本雏形就搭建完毕了,有了良好的地基,里面很多细节的装修就相对方便了,比如:很多 nuget 依赖包的使用,自行查看相关文档。该文章的目的是提供一个基本的模型参照,理解思路然后逐步按需完善部分框架细节,并应用到实践中才会有更深的体会和记忆。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/25710.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

寒亭5.8万亩盐碱稻 国稻种芯·中国水稻节:山东潍坊插秧期

寒亭5.8万亩盐碱稻 国稻种芯中国水稻节&#xff1a;山东潍坊插秧期 新京报讯&#xff08;记者赵利新&#xff09;新闻中国采编网 中国新闻采编网 谋定研究中国智库网 中国农民丰收节国际贸易促进会 国稻种芯中国水稻节 中国三农智库网-功能性农业农业大健康大会报道&#xff…

MMRotate 全面升级,新增 BoxType 设计

引言&#xff1a;大大降低水平框检测器改旋转框检测器的难度 MMRotate 是一个基于 PyTorch 和 MMDetection 的开源旋转框目标检测工具箱。它将目标检测从水平框扩展到旋转框&#xff0c;为场景文字、遥感影像、自动驾驶等领域的应用打下了基础&#xff0c;为学术界和产业界提供…

瞄准镜-第12届蓝桥杯Scratch选拔赛真题精选

[导读]&#xff1a;超平老师计划推出Scratch蓝桥杯真题解析100讲&#xff0c;这是超平老师解读Scratch蓝桥真题系列的第82讲。 蓝桥杯选拔赛每一届都要举行4~5次&#xff0c;和省赛、国赛相比&#xff0c;题目要简单不少&#xff0c;再加上篇幅有限&#xff0c;因此我精挑细选…

数据结构——单链表

一.简介 上一篇文章&#xff0c;我们介绍了线性表中的顺序表。 而顺序表拥有一些缺陷 1.空间不够时需要增容&#xff0c;增容需要付出代价 2.为避免重复扩容&#xff0c;我们进行指数扩容&#xff0c;可能会造成空间浪费 3.顺序表从开始位置连续存储&#xff0c;插入删除数…

卡特尔世界杯来了,只喝精酿啤酒不玩望京扑克,其实也是一种缺失

北京时间2022年11月20日&#xff0c;卡特尔世界杯正式拉开了序幕&#xff0c;全球都进入了世界杯时间。世界杯的开幕&#xff0c;最高兴的还是球迷朋友&#xff0c;大家可以欢聚一堂&#xff0c;喝着精酿啤酒看着足球&#xff0c;那滋味别提多舒服了。 世界杯对于广大球迷来说&…

表的增删查改

目录 插入数据 基本查询 更新数据 清空数据 聚合函数 group by子句 内置函数 基本查询练习 多表查询 子查询 合并查询 表的内外连接 插入数据 单行—全列插入 如下图&#xff0c;全列插入可以省略要在哪些列插入&#xff01; 多行—指定列插入 如下图&#xff0…

安装 Red Hat Enterprise Linux 9.1 虚拟机

目录1. 官方下载链接与新闻2. 安装提示3. 系统安装步骤&#xff08;1&#xff09;进入系统引导界面&#xff08;2&#xff09;进入【系统语言选择】界面&#xff08;3&#xff09;进入【安装信息摘要】界面① 设置【root密码】② 设置【安装目的地】&#xff08;4&#xff09;进…

【python】使用python将多个视频合并、延长视频的时间

今天做知识分享的时候&#xff0c;最后保存的视频时长是58分钟&#xff0c;那么如何改变视频的时长&#xff0c;将视频时长改为一个小时呢&#xff1f; 下面提供3个方案&#xff1a; 方案1&#xff0c;重新录&#xff0c;很显然&#xff0c;不合理&#xff1b; 方案2&#xf…

蓝屏page_fault_in_nonpaged_area的解决办法

用户在操作电脑的过程中&#xff0c;难免会遇到蓝屏问题&#xff0c;最近就有用户遇到电脑蓝屏重启无限循环&#xff0c;提示代码page_fault_in_nonpaged_area&#xff0c;这要如何解决呢&#xff1f;下面就来看看详细的解决办法。 page_fault_in_nonpaged_area蓝屏代码解决方法…

【MySQL篇】第一篇——数据库基础

目录 什么是数据库 主流数据库 基本使用 MySQL安装 连接服务器 服务器管理 服务器&#xff0c;数据库&#xff0c;表关系 使用案例 创建数据库 使用数据库 创建数据库表 表中插入数据 查询表中的数据 数据逻辑存储 MySQL架构原理 MySQL整体逻辑架构 MySQL逻辑…

Eureka架构篇 - 服务发现

前言 从客户端与服务端两个角度概述一下Eureka服务发现的原理&#xff0c;如下&#xff1a; 客户端 依赖自动装配机制&#xff0c;客户端启动时就会从Eureka服务端全量获取服务实例的注册信息并缓存到本地。之后每隔30秒向Eureka服务端发起增量获取的请求&#xff0c;如果增…

云原生周刊 | 波音公司允许员工给开源项目做贡献

如果你要问谁对开源项目的贡献最小&#xff0c;那一定是保密等级很高的国防工业机构&#xff0c;但这个魔咒最近被波音公司给打破了。在最近的一次 Linux 基金会成员峰会 keynote 演讲中&#xff0c;波音公司提到他们会在 2022 年成立一个开源办公室&#xff0c;并且从即日起&a…

m基于MATLAB-GUI的GPS数据经纬度高度解析与kalman分析软件设计

目录 1.算法概述 2.仿真效果预览 3.MATLAB部分代码预览 4.完整MATLAB程序 1.算法概述 经度纬度和高度来自GPS信号的中的GPGGA的数据。所以提取这三个信息主要是对GPGGA中的数据进行整理。GPGGA的数据格式如下所示&#xff1a; GPGGA是GPS数据输出格式语句&#xff0c;意思是…

9问502

一、502意味着什么 502 Bad Gateway是指错误网关&#xff0c;无效网关&#xff1b;在互联网中表示一种网络错误。表现在WEB浏览器中给出的页面反馈。它通常并不意味着上游服务器已关闭&#xff08;无响应网关/代理&#xff09; &#xff0c;而是上游服务器和网关/代理使用不一…

latex 模板使用技巧——参考文献篇

参考文献说明&#xff1a; 一、 常用参考文献类型 1、会议 &#xff08;INPROCEEDINGS&#xff09; 示例&#xff1a; INPROCEEDINGS{rcnn,title{Rich feature hierarchies for accurate object detection and semantic segmentation},author{Girshick, Ross and Donahue, J…

骨传导耳机是利用什么原理听歌?什么骨传导耳机好用

这几年来骨传导耳机的火热程度不言而喻&#xff0c;很多运动人士手上必有一款骨传导耳机&#xff0c;也随着骨传导耳机的兴起&#xff0c;越来越多小伙伴都加入了运动当中。当然&#xff0c;也有很多小伙伴是不知道骨传导耳机的&#xff0c;更不知道骨传导耳机有什么作用&#…

KESION(.NET版)安装方法

若是windows2008系统,访问xxxxxx.com - xxx sex videos free hd porn 资源和信息。 安装界面没有样式加载,请先编辑web.config 去掉 <defaultDocument> <files> <clear /> <add value"index.aspx" /> </files> </defaultDocume…

Android App开发手机阅读中PDF文件渲染器的讲解及使用(附源码 简单易懂)

需要源码和图片集请点赞关注收藏后评论区留言~~~ 一、PDF文件渲染器 Android集成了PDF的渲染操作&#xff0c;从很大程度上方便了开发者&#xff0c;这个PDF文件渲染器便是PdfRenderer。渲染器允许从存储卡读取PDF文件 打开PDF文件只是第一步&#xff0c;接下来使用PdfRender…

Java FreeMarker模板引擎注入深入分析

0x01 前言 最近和 F1or 大师傅一起挖洞的时候发现一处某 CMS SSTI 的 0day&#xff0c;之前自己在复现 jpress 的一些漏洞的时候也发现了 SSTI 这个洞杀伤力之大。今天来好好系统学习一手。 有三个最重要的模板&#xff0c;其实模板引擎本质上的原理差不多&#xff0c;因为在…

CPT-MNPS/Fe3O4 NPs/Au NPs顺铂偶联磁性纳米粒子/四氧化三铁纳米粒子/金纳米粒子

小编下面整理了CPT-MNPS/Fe3O4 NPs/Au NPs顺铂偶联磁性纳米粒子/四氧化三铁纳米粒子/金纳米粒子&#xff0c;来看&#xff01; CPT-偶联纳米粒子 采用新工艺制备了包载盐酸阿霉素的明胶-泊洛沙姆纳米脂质体&#xff0c;并进行相关性能的表征。采用WW型明胶-泊洛沙姆乳液体系结合…