大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进。

abp框架在.net社区是spring一样的存在,用的人也非常多,毫无疑问,它确实是一个不错的框架,不然社区的star也不会那么多。我也是因为它的模块化,ddd,微服务兼容等特点做的选型。但是随着你使用的项目越多,你会发现它也有自己的不足,所谓没有十全十美的框架。

一、abp的痛点

通过使用商业版和社区版的经验,有几个痛点:

1.前端不支持VUE:

angular前端不好招聘,小公司可能个别会ng,但是架不住离职的风险。我曾经见过一个开发团队,后端用abp,前端用ng,前后端不分离,这个时候后端人员就硬着头皮去修改前人的angular,问他为什么改得这么慢,对方说:我比较熟悉vue,老板只有暗暗着急,却无可奈何。显然技术选型出问题,对团队和交付的影响是非常大的;

2.社区版界面比较丑陋:

是不是我的理解有偏差,感觉老外大部分都不怎么重视脸面问题。商业版的界面稍微就好看一些,但是味道感觉还是差了那么一点点;

3.售后服务非常慢:

这个可能是最坑的地方,对新人比较不友好,如果项目紧急,随时可能要面对的是漫长的等待,记得我刚入坑到时候,碰到一个https配置问题,问了邮件许久都没有回复;

4.功能不好用:

社区版的功能要么水土不服,要么功能太浅。看起来好些功能挺多的,但是用起来,还是要改得满头大汗;

5.价格比较昂贵:

带源码商业版要4万3左右,企业版更贵,大概要7.3万。

二、我们如何处理?

我不是说abp商业版不好,它挺不错的,只是不适合自己的业务需求,于是,我们用VUE3+ABP重做了一下(传送门),请大家指教,目前有几个增强点:

1.功能更深入。

比如:

  • 【增强】文件管理:支持多种SSO存储渠道配置和切换;支持图片、文档、视频、音频的存储;
  • 【新增】任务调度:支持单体和集群,支持可视化配置和日志记录;
  • 【重写】认证授权:支持用户池,用户同步,单点登录,目前正在开发中;
  • 【增强】用户和角色扩展;
  • 【复刻】关联账户;
  • 【复刻】委托登录;
  • 【新增】菜单管理;
  • 【新增】行政单位;
  • 【新增】计量单位;
  • 【新增】校验规则;

至于多语言,审计日志, opendidct,多租户等自带的功能,我们用vue3把他们优化和重写了,功能基本没有变化,没有什么可以分享的。

下面是认证授权的同步中心,用来解决多个系统用户集中管理和单点登录问题。

2.细节更深入。

功能层面

比如:菜单支持标星收藏和归类;前后端分离我们基于permission表扩展了菜单表,这样既能支持系统默认菜单,也能支持自定义菜单。

返回格式

又比如,abp的接口返回格式比较没有规则,考虑前端的体验,我们给它包了一个壳,做了格式统一:

  • abp的返回格式:

  • 我们的返回格式:

考虑前端的体验,我们给它包了一个壳

{
"code": null,
"data": {},
"isSuccess": true,
"msg": null,
"extras": null,
"timestamp": 1739241669
}

抛出异常状态码

这里面的状态码也很有讲究,我们观察发现abp也不是很规范,后台抛异常各自情况都有,我们统计了一下,abp的异常种类很多,多到感觉抓不到规律:


Application层: throw new AbpException($"No policy defined to get/set permissions for the provider '{providerName}'. Use {nameof(PermissionManagementOptions)} to map the policy.");
throw new ApplicationException($"The permission named '{permission.Name}' has not compatible with the provider named '{providerName}'");
throw new AbpValidationException();
throw new FileNotFoundException($"Signing Certificate couldn't found: {file}");
throw new BadHttpRequestException("");
throw new NotImplementedException($"{nameof(GetUserInfoAsync)} is not implemented default. It should be overriden and implemented by the deriving class!");
throw new EntityNotFoundException(typeof(IdentityRole), id);
throw new UserResponseAlreadyExistException();
throw new EmailAddressRequiredException(); Domain层: throw new BusinessException(code: IdentityErrorCodes.UserSelfDeletion);
throw new BusinessException(EventHubErrorCodes.CantChangeEventTiming)
.WithData("MaxTimingChangeLimit", EventConsts.MaxTimingChangeCountForUser);
throw new BusinessException(EventHubErrorCodes.TrackNameAlreadyExist)
.WithData("Name", name); throw new ArgumentException("identityResult.Errors should not be null.");
throw new ArgumentNullException(nameof(childCode), "childCode can not be null or empty."); throw new UserFriendlyException(L["CountryAndCityRequiredForUpdateInPersonEvent"]);
throw new AbpAuthorizationException(
L["EventHub:NotAuthorizedToUpdateEvent", @event.Title],
EventHubErrorCodes.NotAuthorizedToUpdateEvent
); public static class IdentityErrorCodes
{
public const string UserSelfDeletion = "Volo.Abp.Identity:010001";
public const string MaxAllowedOuMembership = "Volo.Abp.Identity:010002";
public const string ExternalUserPasswordChange = "Volo.Abp.Identity:010003";
public const string DuplicateOrganizationUnitDisplayName = "Volo.Abp.Identity:010004";
public const string StaticRoleRenaming = "Volo.Abp.Identity:010005";
public const string StaticRoleDeletion = "Volo.Abp.Identity:010006";
public const string UsersCanNotChangeTwoFactor = "Volo.Abp.Identity:010007";
public const string CanNotChangeTwoFactor = "Volo.Abp.Identity:010008";
public const string YouCannotDelegateYourself = "Volo.Abp.Identity:010009";
public const string ClaimNameExist = "Volo.Abp.Identity:010021";
}

因为不同的种类异常返回的状态码是有差异的,归类起来有如下业务状态:

  • 请求成功
  • 请求成功,返回空
  • 非法参数
  • 未授权,需提供身份验证
  • 已授权,但服务器拒绝请求
  • 服务器内部错误

到底我们应该如何返回才能减轻前端判断的复杂度,如何让abp的异常和这些码对应起来,如何拦截abp底层异常,进行重写返回码,这是一项挺复杂的任务,后续有机会再做深入分享。

DTO规范

除此之外,abp内部本身的规范也存在一定问题,比如dto的命名,除了以dto为后缀,也会出现以input为后缀的dto类。我们内部统一以input和output作为后缀,因为关于命名我们也有一套规范,这里不展开。dto重要吗,有没有必要拿出来讲,这是一个问题。我觉得dto有很多门道,值得写一篇长文,下面简单点一下

dto门道:

  • dto要不要包含业务逻辑?
  • 未使用的DTO要不要删掉
  • 输入DTO要不要共享
  • 输出DTO要不要共享
  • DTO命名规约(CURD/GET/SAVE/CHECK/LIST/TREE/SELECT/DETAIL等等)
  • DTO如何验证,如何做多语言

不再展开了,敬请期待《DTO应该如何规范比较好?》。

其他

其他的细节还有很多,后续会做一个系列文章《我用abp做企业数字化应用》,这里就不一一罗列,有兴趣的朋友可以通过(传送门)了解学习。

3.基于Ant Design的VUE3框架。

选vue主要考虑vue开发者比较充沛,人员离职交接比较方便;另外颜值这块目前vue生态发展得很给力,应该会更加符合国人口味,更换主体也是标配。好不好看,东哥说我有“眼盲”,不知道奶茶好不好看:

主题萝卜青菜各有所爱,通过比对,不敢也没有对abp不敬,更不是为了输赢,就是想追求品位多一点,不知道这个主题你满意吗?

4.重写abp源码。

框架本身扩展性很不错,但是架不住各自需求不断冲击。

比如:

  • 如何基于用户表,扩展原生的字段?
  • 国内外第三方有几十个,我应该如何集成?
  • 如何在授权中心托管一个VUE前端登录页面?
  • 想给CurrentUser增加一个IsAdmin字段要如何增加?
  • 如何隐藏API接口的显示?
  • 测试阶段,如何取消全部权限Authorize?
  • 如何使用纯净版的abp进行项目开发?
  • 为什么返回的CurrentUser为空?
  • 如何增加用户池Id,模仿IsDelete做数据过滤?
  • 有了控制器层,为什么还要有httpapi这一层?
  • 如何自定义Claim?
  • 为什么老是报实体对象没有注入的错?
  • abp太臃肿,如何给它瘦身?
  • 如何去除abp数据库的表前缀?
  • 微服务底下认证授权如何使用?
  • 如何抛弃abp的identity基础模块,重写opendidct?
  • 如何结合abp,jenkins,docker,k8s做devops?
  • abp的store和领域服务有什么区别?
  • ……

    以上可以继续罗列很多,只要你一直用abp开发,你遇到的坑会越多,你就会越想重写abp,但是当你越过了这些障碍,你就越了解它,知道它的优点和缺点,知道如何避免这些坑位。

下面是我们在做IDaaS这个项目过程中,重写的一个功能,我们给授权中心托管了一个登录组件,支持验证码(手机/邮箱)和密码(手机/邮箱/用户名)登录,这样不管未来你有多少个系统,你都不需要重写登录了。你可以在认证授权中心进行注册应用,选择许可模式,配置scope,你就可以愉快得进行单点登录了。oauth2.0和openid这两个协议想要用好其实挺不容易的:

  • 客户端有很多种类,比如app,web,spa,api等;
  • 许可模式有授权码,隐式,密码和客户端凭证或者自定义模式;
  • 什么类型的客户端要选择什么用的模式,如何自定义模式?

这些东西还是有点复杂的,如果您刚好也在做这类开发,欢迎您留言和我探讨。

5.追求性能极致。

abp在权限列表、权限保存和特征配置列表的性能非常拉胯,比如你定义的功能按钮在100以上,你会发现原生接口的权限渲染和保存会非常卡,主要原因就是在循环里面进行读写,我们花了大力气进行重构,下面是权限设置重构后的代码:

权限列表接口优化

下面是源码内容,里面有四个循环(晕):

 public virtual async Task<GetPermissionListResultDto> GetAsync(string providerName, string providerKey)
{
await CheckProviderPolicy(providerName); var result = new GetPermissionListResultDto
{
EntityDisplayName = providerKey,
Groups = new List<PermissionGroupDto>()
}; var multiTenancySide = CurrentTenant.GetMultiTenancySide(); foreach (var group in await PermissionDefinitionManager.GetGroupsAsync())
{
var groupDto = CreatePermissionGroupDto(group); var neededCheckPermissions = new List<PermissionDefinition>(); var permissions = group.GetPermissionsWithChildren()
.Where(x => x.IsEnabled)
.Where(x => !x.Providers.Any() || x.Providers.Contains(providerName))
.Where(x => x.MultiTenancySide.HasFlag(multiTenancySide)); foreach (var permission in permissions)
{
if(permission.Parent != null && !neededCheckPermissions.Contains(permission.Parent))
{
continue;
} if (await SimpleStateCheckerManager.IsEnabledAsync(permission))
{
neededCheckPermissions.Add(permission);
}
} if (!neededCheckPermissions.Any())
{
continue;
} var grantInfoDtos = neededCheckPermissions
.Select(CreatePermissionGrantInfoDto)
.ToList(); var multipleGrantInfo = await PermissionManager.GetAsync(neededCheckPermissions.Select(x => x.Name).ToArray(), providerName, providerKey); foreach (var grantInfo in multipleGrantInfo.Result)
{
var grantInfoDto = grantInfoDtos.First(x => x.Name == grantInfo.Name); grantInfoDto.IsGranted = grantInfo.IsGranted; foreach (var provider in grantInfo.Providers)
{
grantInfoDto.GrantedProviders.Add(new ProviderInfoDto
{
ProviderName = provider.Name,
ProviderKey = provider.Key,
});
} groupDto.Permissions.Add(grantInfoDto);
} if (groupDto.Permissions.Any())
{
result.Groups.Add(groupDto);
}
} return result;
}

下面是我们优化后的内容:

public override async Task<GetPermissionListResultDto> GetAsync(string providerName, string providerKey)
{
await CheckProviderPolicy(providerName); var result = new GetPermissionListResultDto
{
EntityDisplayName = providerKey,
Groups = new List<PermissionGroupDto>()
}; var multiTenancySide = CurrentTenant.GetMultiTenancySide(); // 预先获取所有权限组及其子权限
var groups = await _permissionDefinitionManager.GetGroupsAsync(); // 提前检查所有需要的特性
var featureNames = groups
.Select(group => group.Properties.FirstOrDefault(it => it.Key == "FeatureName").Value?.ToString())
.Where(featureName => !string.IsNullOrEmpty(featureName))
.Distinct()
.ToList(); var enabledFeatures = new HashSet<string>();
foreach (var featureName in featureNames)
{
if (await _featureChecker.IsEnabledAsync(featureName))
{
enabledFeatures.Add(featureName);
}
} // 批量查询权限状态
var allPermissions = groups.SelectMany(group => group.GetPermissionsWithChildren())
.Where(permission => permission.IsEnabled)
.Where(permission => !permission.Providers.Any() || permission.Providers.Contains(providerName))
.Where(permission => permission.MultiTenancySide.HasFlag(multiTenancySide))
.ToList(); var permissionNames = allPermissions.Select(permission => permission.Name).ToArray();
var multipleGrantInfo = await _permissionManager.GetAsync(permissionNames, providerName, providerKey);
var multipleGrantResult = multipleGrantInfo.Result.ToDictionary(x => x.Name); // 缓存状态检查结果
var simpleStateCheckerCache = new Dictionary<string, bool>();
foreach (var permission in allPermissions)
{
if (!simpleStateCheckerCache.ContainsKey(permission.Name))
{
simpleStateCheckerCache[permission.Name] = await _simpleStateCheckerManager.IsEnabledAsync(permission);
}
}
}

重构后,耗时不足1s,如下图所示:

权限保存接口优化

针对权限保存的性能问题,我们去除循环操作,一次性读取到内存,在缓冲器进行操作,重构代码如下:

/// <summary>
/// 保存权限
/// </summary>
/// <param name="providerName"></param>
/// <param name="providerKey"></param>
/// <param name="input"></param>
/// <returns></returns>
public async override Task UpdateAsync(string providerName, string providerKey, UpdatePermissionsDto input)
{
await CheckProviderPolicy(providerName); // 将 Permissions 根据 IsGranted 分为两个列表
var grantedPermissions = input.Permissions
.Where(p => p.IsGranted)
.Select(p => p.Name)
.ToList(); var deniedPermissions = input.Permissions
.Where(p => !p.IsGranted)
.Select(p => p.Name)
.ToList(); // 批量设置 Granted 为 true 的权限
if (grantedPermissions.Any())
{
await _permissionExtendManager.SetBatchAsync(grantedPermissions, providerName, providerKey, isGranted: true);
} // 批量设置 Granted 为 false 的权限
if (deniedPermissions.Any())
{
await _permissionExtendManager.SetBatchAsync(deniedPermissions, providerName, providerKey, isGranted: false);
}
}

6.死磕代码规范。

一般开源项目不怎么注重代码可读性,abp的模块化设计思想,如果不考虑隔离的代价,其实还是挺不错的,为了项目长期可持续发展,我们还是拟定了适合自己内部的代码规范。

开发规范手册

因为规范是一个团队的标准,老板一般都比较重视,为此我们写了一本开发规范手册,希望对你有所启发。

以上是大的方面,从细节上,我们从DDD的逻辑分层结构入手,每个层、每个类、方法、控制器、api、验证格式、dto、常量等都一一审视一遍。

现摘录一部分:

HttpApi规约

接口数量最佳实践,我们约定:

  • 一个接口能完成的工作,尽量不要用多个接口。

    比如:租户有功能接口,租户版本有功能接口,可以考虑接口进行合并。
  • 多个接口分开做的事情,尽量不要一个接口。

    比如:列表和详情的接口,一般是分开编写的,确保接口的职责单一。
  • 接口分开合并的粒度要适中

    接口什么时候分开,什么时候合并,有个度的问题,比较难把握,如果自己不能确定,需要向上扩大讨论和取舍。坚持质量优先,进度可以适当放慢,不要为了赶进度,不兼顾代码质量,否正代码越多,错误越多,因为代码是需要长期维护的。

接口格式,我们在每个功能开发之前的设计范示:

Domain.Shared规约:

  • 实体文件夹以s结尾;
  • 错误代码以静态类+常量定义在该层,并以ErrorCodes结尾;
  • 全局设置定义在该层,并固定以Settings命名;
  • 多语言的json文件定义在该层;
  • 枚举类和数据库字段长度和默认名称定义在该层。

Domain规约:

  • 实体文件夹约定以s结尾;
  • 表结构字段约定以静态类+常量定义在该层,并以Consts结尾(如果有定义);
  • 全局设置约定定义在该层,并固定以Settings命名(如果有定义);
  • 种子数据约定定义在该层;

EF Core规约:

  • 实体文件夹采用s结尾;
  • 内部包含DbContext,DbContextFactory,ModelCreatingExtensions三个核心类

规范虽好,但是它也是一把双刃剑,开发过程要进行balance,否则会影响团队的效率。比如可以前期起好头,评审频繁一些,后期团队上道了,可以每周审查一次,最好要有审查的标准,而且审核后要督促修改。

我们是如何解决abp身上的几个痛点的更多相关文章

  1. 借助AMD来解决HTML5游戏开发中的痛点

    借助AMD来解决HTML5游戏开发中的痛点 游戏开发的痛点 现在,基于国内流行引擎(LayaAir和Egret)和TypeScript的HTML5游戏开发有诸多痛点: 未采用TypeScript编译器 ...

  2. 翻译《Mastering ABP Framework》

    前言 大家好,我是张飞洪,谢谢你阅读我的文章. 自从土牛Halil ibrahim Kalkan的<Mastering ABP Framework>出版之后,我就开始马不停蹄进行学习阅读和 ...

  3. 使用ABP EntityFramework连接MySQL数据库

    ASP.NET Boilerplate(简称ABP)是在.Net平台下一个很流行的DDD框架,该框架已经为我们提供了大量的函数,非常方便与搭建企业应用. 关于这个框架的介绍我就不多说,有兴趣的可以参见 ...

  4. ABP理论学习之发布说明

    返回总目录 查看更详细信息以及下载源代码请查看原文档 ABP v0.9.2.0 | [更新日期:2016/6/6 11:21:28 ] 解决方案转换成xproj/project.json格式. 添加了 ...

  5. 我是如何在实际项目中解决MySQL性能问题

    可能是本性不愿随众的原因,我对于程序员面试中动辄就是考察并发上千万级别的QPS向来嗤之以鼻,好像国内的应用都是那么多用户量一样,其实并发达到千万,百万以上的应用能有几个? 绝大多数的程序员面临的只是解 ...

  6. NGK——解决区块链用户之“难”

    自比特币诞生以来,区块链行业已发展十余年,而且在在金融.民生.司法存证.供应链协同.税务发票.版权保护等领域得到一定程度的应用,但大多属于边缘业务,以探索试点为主,应用深度和广度不足.为什么会这样?是 ...

  7. 解决pip下载速度慢问题

    解决pip下载速度慢的问题 痛点:当我们pip 安装第三方库的时候,由于是访问的国外地址,所以会出现下载很慢!干等..... 解决方案: # 1.在C盘目录-->Users-->用户--& ...

  8. 分布式开放消息系统(RocketMQ)的原理与实践

    分布式消息系统作为实现分布式系统可扩展.可伸缩性的关键组件,需要具有高吞吐量.高可用等特点.而谈到消息系统的设计,就回避不了两个问题: 消息的顺序问题 消息的重复问题 RocketMQ作为阿里开源的一 ...

  9. mycat分布式mysql中间件(数据库切分概述)[转]

    mysql数据库切分 前言 通 过MySQLReplication功能所实现的扩展总是会受到数据库大小的限制,一旦数据库过于庞大,尤其是当写入过于频繁,很难由一台主机支撑的时 候,我们还是会面临到扩展 ...

  10. MySQL 水平拆分(读书笔记整理)

    转:http://blog.csdn.net/mchdba/article/details/46278687 1,水平拆分的介绍 一般来说,简单的水平切分主要是将某个访问极其平凡的表再按照某个字段的某 ...

随机推荐

  1. [Java] Stream流使用最多的方式

    Java 中 Stream 流的用法全解析 在 Java 编程中,Stream 流提供了一种高效.便捷的方式来处理集合数据.它可以让我们以声明式的方式对数据进行各种操作,如过滤.映射.排序.聚合等,大 ...

  2. css var实现网页换肤

    前情 最近在做需求开发,要求根据后台传来的配置对网页换肤,按以往的换肤思路应该是写好几套样式做切换达到换肤效果,但是现在想做到能根据后台配置动态修改. 原理 通过css3新增变量特性,把颜色定义为变量 ...

  3. COS 数据工作流 + Serverless云函数自定义处理能力发布!

    01 背景 在工业4.0的浪潮下,智能和数据与物理世界结合越加紧密,多元化.灵活.高效的数据处理能力成为各行各业的热点需求. ​ 虽然COS已经预置电商.文创.教育.社交.安防等行业需要的基础数据处理 ...

  4. 源启容器平台KubeGien 打造云原生转型的破浪之舰

    ​ 云原生是应用上云的标准路径,也是未来发展大的趋势.如何将业务平滑过渡到云上?怎样应对上云期间的各项挑战呢?中电金信基于金融级数字底座"源启"打造了一款非常稳定可靠.多云异构.安 ...

  5. 【Spring】作业记录:spring项目从创建、配置到功能实现、测试

    提前声明: 1.这只是文档一次作业记录,也许会有不太恰当的地方,所以仅供参考. 2.适合不知道怎么创建配置的参考.仅仅是参考,而不是抄代码. 目录 项目创建 配置pom.xml 连接数据库 快速创建实 ...

  6. JGit的常用功能(提交、回滚,日志查询)

    最近项目中要做一个回滚功能,目的是如果这次发布出现了问题,立马回滚到上一次发布的版本,用jgit实现的,具体方法如下: public class GitUtil { private final sta ...

  7. 记一次单元测试问题com.sun.crypto.provider.HmacSHA1 cannot be cast to javax.crypto.MacSpi

    在用单元测试Junit测试部门的SDK时,有个md5鉴权步骤,出现了java.lang.ClassCastException: com.sun.crypto.provider.HmacSHA1 can ...

  8. Python 在Excel单元格中应用数据条

    在Excel中添加数据条是一种数据可视化技巧,它通过条形图的形式在单元格内直观展示数值的大小,尤其适合比较同一列或行中各个单元格的数值.这种表示方式可以让大量的数字信息一目了然.本文将介绍如何使用Py ...

  9. QtCreator中pro项目文件格式说明

    名称 说明 QT += core gui 添加本项目中需要的模块,影响后面代码文件include的时候自动弹出下拉选择,如果pro文件没有引入该模块则无法自动语法提示,一般打包发布的时候对应动态库文件 ...

  10. JDK 19 Virtual Threads 虚拟线程

    前言 Project Loom Loom 是什么? 为什么要引入 Loom? Virtual threads Platform thread 是什么? Virtual thread 是什么? Virt ...