热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:[AbpvNext源码分析]19.多租户

一、简介ABPvNext原生支持多租户体系,可以让开发人员快速地基于框架开发SaaS系统。ABPvNext实现多租户的思路也非常简单,通过一个

一、简介

ABP vNext 原生支持多租户体系,可以让开发人员快速地基于框架开发 SaaS 系统。ABP vNext 实现多租户的思路也非常简单,通过一个 TenantId 来分割各个租户的数据,并且在查询的时候使用统一的全局过滤器(类似于软删除)来筛选数据。

关于多租户体系的东西,基本定义与核心逻辑存放在 Volo.ABP.MultiTenancy 内部。针对 ASP.NET Core MVC 的集成则是由 Volo.ABP.AspNetCore.MultiTenancy 项目实现的,针对多租户的解析都在这个项目内部。租户数据的存储和管理都由 Volo.ABP.TenantManagement 模块提供,开发人员也可以直接使用该项目快速实现多租户功能。

二、源码分析


2.1 启动模块

AbpMultiTenancyModule 模块是启用整个多租户功能的核心模块,内部只进行了一个动作,就是从配置类当中读取多租户的基本信息,以 JSON Provider 为例,就需要在 appsettings.json 里面有 Tenants 节。

"Tenants": [
{
"Id": "446a5211-3d72-4339-9adc-845151f8ada0",
"Name": "tenant1"
},
{
"Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d",
"Name": "tenant2",
"ConnectionStrings": {
"Default": "...write tenant2's db connection string here..."
}
}
]

2.1.1 默认租户来源

这里的数据将会作为默认租户来源,也就是说在确认当前租户的时候,会从这里面的数据与要登录的租户进行比较,如果不存在则不允许进行操作。

public interface ITenantStore
{
Task FindAsync(string name);
Task FindAsync(Guid id);
TenantConfiguration Find(string name);
TenantConfiguration Find(Guid id);
}

默认的存储实现:

[Dependency(TryRegister = true)]
public class DefaultTenantStore : ITenantStore, ITransientDependency
{
// 直接从 Options 当中获取租户数据。
private readonly AbpDefaultTenantStoreOptions _options;
public DefaultTenantStore(IOptionsSnapshot options)
{
_optiOns= options.Value;
}
public Task FindAsync(string name)
{
return Task.FromResult(Find(name));
}
public Task FindAsync(Guid id)
{
return Task.FromResult(Find(id));
}
public TenantConfiguration Find(string name)
{
return _options.Tenants?.FirstOrDefault(t => t.Name == name);
}
public TenantConfiguration Find(Guid id)
{
return _options.Tenants?.FirstOrDefault(t => t.Id == id);
}
}

除了从配置文件当中读取租户信息以外,开发人员也可以自己实现 ITenantStore 接口,比如说像 TenantManagement 一样,将租户信息存储到数据库当中。

2.1.2 基于数据库的租户存储

话接上文,我们说过在 Volo.ABP.TenantManagement 模块内部有提供另一种 ITenantStore 接口的实现,这个类型叫做 TenantStore,内部逻辑也很简单,就是从仓储当中查找租户数据。

public class TenantStore : ITenantStore, ITransientDependency
{
private readonly ITenantRepository _tenantRepository;
private readonly IObjectMapper _objectMapper;
private readonly ICurrentTenant _currentTenant;
public TenantStore(
ITenantRepository tenantRepository,
IObjectMapper objectMapper,
ICurrentTenant currentTenant)
{
_tenantRepository = tenantRepository;
_objectMapper = objectMapper;
_currentTenant = currentTenant;
}
public async Task FindAsync(string name)
{
// 变更当前租户为租主。
using (_currentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities!
{
// 通过仓储查询租户是否存在。
var tenant = await _tenantRepository.FindByNameAsync(name);
if (tenant == null)
{
return null;
}
// 将查询到的信息转换为核心库定义的租户信息。
return _objectMapper.Map(tenant);
}
}
// ... 其他的代码已经省略。
}

可以看到,最后也是返回的一个 TenantConfiguration 类型。关于这个类型,是 ABP 在多租户核心库定义的一个基本类型之一,主要是用于规定持久化一个租户信息需要包含的属性。

[Serializable]
public class TenantConfiguration
{
// 租户的 Guid。
public Guid Id { get; set; }
// 租户的名称。
public string Name { get; set; }
// 租户对应的数据库连接字符串。
public ConnectionStrings ConnectionStrings { get; set; }
public TenantConfiguration()
{

}
public TenantConfiguration(Guid id, [NotNull] string name)
{
Check.NotNull(name, nameof(name));
Id = id;
Name = name;
COnnectionStrings= new ConnectionStrings();
}
}

2.2 租户的解析

ABP vNext 如果要判断当前的租户是谁,则是通过 AbpTenantResolveOptions 提供的一组 ITenantResolveContributor 进行处理的。

public class AbpTenantResolveOptions
{
// 会使用到的这组解析对象。
[NotNull]
public List TenantResolvers { get; }
public AbpTenantResolveOptions()
{
TenantResolvers = new List
{
// 默认的解析对象,会通过 Token 内字段解析当前租户。
new CurrentUserTenantResolveContributor()
};
}
}

这里的设计与权限一样,都是由一组 解析对象(解析器) 进行处理,在上层开放的入口只有一个 ITenantResolver ,内部通过 foreach 执行这组解析对象的 Resolve() 方法。

下面就是我们 ITenantResolver 的默认实现 TenantResolver,你可以在任何时候调用它。比如说你在想要获得当前租户 Id 的时候。不过一般不推荐这样做,因为 ABP 已经给我们提供了 MultiTenancyMiddleware 中间件。

技术图片

也就是说,在每次请求的时候,都会将这个 Id 通过 ICurrentTenant.Change() 进行变更,那么在这个请求执行完成之前,通过 ICurrentTenant 取得的 Id 都会是解析器解析出来的 Id。

public class TenantResolver : ITenantResolver, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
private readonly AbpTenantResolveOptions _options;
public TenantResolver(IOptions options, IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_optiOns= options.Value;
}
public TenantResolveResult ResolveTenantIdOrName()
{
var result = new TenantResolveResult();
using (var serviceScope = _serviceProvider.CreateScope())
{
// 创建一个解析上下文,用于存储解析器的租户 Id 解析结果。
var cOntext= new TenantResolveContext(serviceScope.ServiceProvider);
// 遍历执行解析器。
foreach (var tenantResolver in _options.TenantResolvers)
{
tenantResolver.Resolve(context);
result.AppliedResolvers.Add(tenantResolver.Name);
// 如果有某个解析器为上下文设置了值,则跳出。
if (context.HasResolvedTenantOrHost())
{
result.TenantIdOrName = context.TenantIdOrName;
break;
}
}
}
return result;
}
}

2.2.1 默认的解析对象

如果不使用 Volo.Abp.AspNetCore.MultiTenancy 模块,ABP vNext 会调用 CurrentUserTenantResolveContributor 解析当前操作的租户。

public class CurrentUserTenantResolveContributor : TenantResolveContributorBase
{
public const string COntributorName= "CurrentUser";
public override string Name => ContributorName;
public override void Resolve(ITenantResolveContext context)
{
// 从 Token 当中获取当前登录用户的信息。
var currentUser = context.ServiceProvider.GetRequiredService();
if (currentUser.IsAuthenticated != true)
{
return;
}
// 设置解析上下文,确认当前的租户 Id。
context.Handled = true;
context.TenantIdOrName = currentUser.TenantId?.ToString();
}
}

在这里可以看到,如果从 Token 当中解析到了租户 Id,会将这个 Id 传递给 解析上下文。这个上下文在最开始已经遇到过了,如果 ABP vNext 在解析的时候发现租户 Id 被确认了,就不会执行剩下的解析器。

2.2.2 ABP 提供的其他解析器

ABP 在 Volo.Abp.AspNetCore.MultiTenancy 模块当中还提供了其他几种解析器,他们的作用分别如下。




































解析器类型作用优先级
QueryStringTenantResolveContributor通过 Query String 的 __tenant 参数确认租户。2
RouteTenantResolveContributor通过路由判断当前租户。3
HeaderTenantResolveContributor通过 Header 里面的 __tenant 确认租户。4
COOKIETenantResolveContributor通过携带的 COOKIE 确认租户。5
DomainTenantResolveContributor二级域名解析器,通过二级域名确定租户。第二

2.2.3 域名解析器

这里比较有意思的是 DomainTenantResolveContributor,开发人员可以通过 AbpTenantResolveOptions.AddDomainTenantResolver() 方法添加这个解析器。 域名解析器会通过解析二级域名来匹配对应的租户,例如我针对租户 A 分配了一个二级域名 http://a.system.com,那么这个 a 就会被作为租户名称解析出来,最后传递给 ITenantResolver 解析器作为结果。

技术图片

注意:

在使用 Header 作为租户信息提供者的时候,开发人员使用的是 NGINX 作为反向代理服务器 时,需要在对应的 config 文件内部配置 underscores_in_headers on; 选项。否则 ABP 所需要的 __tenantId 将会被过滤掉,或者你可以指定一个没有下划线的 Key。

域名解析器的详细代码解释:

public class DomainTenantResolveContributor : HttpTenantResolveContributorBase
{
public const string COntributorName= "Domain";
public override string Name => ContributorName;
private static readonly string[] ProtocolPrefixes = { "http://", "https://" };
private readonly string _domainFormat;
// 使用指定的格式来确定租户前缀,例如 “{0}.abp.io”。
public DomainTenantResolveContributor(string domainFormat)
{
_domainFormat = domainFormat.RemovePreFix(ProtocolPrefixes);
}
protected override string GetTenantIdOrNameFromHttpContextOrNull(
ITenantResolveContext context,
HttpContext httpContext)
{
// 如果 Host 值为空,则不进行任何操作。
if (httpContext.Request?.Host == null)
{
return null;
}
// 解析具体的域名信息,并进行匹配。
var hostName = httpContext.Request.Host.Host.RemovePreFix(ProtocolPrefixes);
// 这里的 FormattedStringValueExtracter 类型是 ABP 自己实现的一个格式化解析器。
var extractResult = FormattedStringValueExtracter.Extract(hostName, _domainFormat, ignoreCase: true);
context.Handled = true;
if (!extractResult.IsMatch)
{
return null;
}
return extractResult.Matches[0].Value;
}
}

从上述代码可以知道,域名解析器是基于 HttpTenantResolveContributorBase 基类进行处理的,这个抽象基类会取得当前请求的一个 HttpContext,将这个传递与解析上下文一起传递给子类实现,由子类实现负责具体的解析逻辑。

public abstract class HttpTenantResolveContributorBase : TenantResolveContributorBase
{
public override void Resolve(ITenantResolveContext context)
{
// 获取当前请求的上下文。
var httpCOntext= context.GetHttpContext();
if (httpCOntext== null)
{
return;
}
try
{
ResolveFromHttpContext(context, httpContext);
}
catch (Exception e)
{
context.ServiceProvider
.GetRequiredService>()
.LogWarning(e.ToString());
}
}
protected virtual void ResolveFromHttpContext(ITenantResolveContext context, HttpContext httpContext)
{
// 调用抽象方法,获取具体的租户 Id 或名称。
var tenantIdOrName = GetTenantIdOrNameFromHttpContextOrNull(context, httpContext);
if (!tenantIdOrName.IsNullOrEmpty())
{
// 获得到租户标识之后,填充到解析上下文。
context.TenantIdOrName = tenantIdOrName;
}
}
protected abstract string GetTenantIdOrNameFromHttpContextOrNull([NotNull] ITenantResolveContext context, [NotNull] HttpContext httpContext);
}

2.3 租户信息的传递

租户解析器通过一系列的解析对象,获取到了租户或租户 Id 之后,会将这些数据给哪些对象呢?或者说,ABP 在什么地方调用了 租户解析器,答案就是 中间件

Volo.ABP.AspNetCore.MultiTenancy 模块的内部,提供了一个 MultiTenancyMiddleware 中间件。

开发人员如果需要使用 ASP.NET Core 的多租户相关功能,也可以引入该模块。并且在模块的 OnApplicationInitialization() 方法当中,使用 IApplicationBuilder.UseMultiTenancy() 进行启用。

这里在启用的时候,需要注意中间件的顺序和位置,不要放到最末尾进行处理。

public class MultiTenancyMiddleware : IMiddleware, ITransientDependency
{
private readonly ITenantResolver _tenantResolver;
private readonly ITenantStore _tenantStore;
private readonly ICurrentTenant _currentTenant;
private readonly ITenantResolveResultAccessor _tenantResolveResultAccessor;
public MultiTenancyMiddleware(
ITenantResolver tenantResolver,
ITenantStore tenantStore,
ICurrentTenant currentTenant,
ITenantResolveResultAccessor tenantResolveResultAccessor)
{
_tenantResolver = tenantResolver;
_tenantStore = tenantStore;
_currentTenant = currentTenant;
_tenantResolveResultAccessor = tenantResolveResultAccessor;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 通过租户解析器,获取当前请求的租户信息。
var resolveResult = _tenantResolver.ResolveTenantIdOrName();
_tenantResolveResultAccessor.Result = resolveResult;
TenantConfiguration tenant = null;
// 如果当前请求是属于租户请求。
if (resolveResult.TenantIdOrName != null)
{
// 查询指定的租户 Id 或名称是否存在,不存在则抛出异常。
tenant = await FindTenantAsync(resolveResult.TenantIdOrName);
if (tenant == null)
{
//TODO: A better exception?
throw new AbpException(
"There is no tenant with given tenant id or name: " + resolveResult.TenantIdOrName
);
}
}
// 在接下来的请求当中,将会通过 ICurrentTenant.Change() 方法变更当前租户,直到
// 请求结束。
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
{
await next(context);
}
}
private async Task FindTenantAsync(string tenantIdOrName)
{
// 如果可以格式化为 Guid ,则说明是租户 Id。
if (Guid.TryParse(tenantIdOrName, out var parsedTenantId))
{
return await _tenantStore.FindAsync(parsedTenantId);
}
else
{
return await _tenantStore.FindAsync(tenantIdOrName);
}
}
}

在取得了租户的标识(Id 或名称)之后,将会通过 ICurrentTenant.Change() 方法变更当前租户的信息,变更了当租户信息以后,在程序的其他任何地方使用 ICurrentTenant.Id 取得的数据都是租户解析器解析出来的数据。

下面就是这个当前租户的具体实现,可以看到这里采用了一个 经典手法-嵌套。这个手法在工作单元和数据过滤器有见到过,结合 DisposeAction()using 语句块结束的时候把当前的租户 Id 值设置为父级 Id。即在同一个语句当中,可以通过嵌套 using 语句块来处理不同的租户。

using(_currentTenant.Change("A"))
{
Logger.LogInformation(_currentTenant.Id);
using(_currentTenant.Change("B"))
{
Logger.LogInformation(_currentTenant.Id);
}
}

具体的实现代码,这里的 ICurrentTenantAccessor 内部实现就是一个 AsyncLocal ,用于在一个异步请求内部进行数据传递。

public class CurrentTenant : ICurrentTenant, ITransientDependency
{
public virtual bool IsAvailable => Id.HasValue;
public virtual Guid? Id => _currentTenantAccessor.Current?.TenantId;
public string Name => _currentTenantAccessor.Current?.Name;
private readonly ICurrentTenantAccessor _currentTenantAccessor;
public CurrentTenant(ICurrentTenantAccessor currentTenantAccessor)
{
_currentTenantAccessor = currentTenantAccessor;
}
public IDisposable Change(Guid? id, string name = null)
{
return SetCurrent(id, name);
}
private IDisposable SetCurrent(Guid? tenantId, string name = null)
{
var parentScope = _currentTenantAccessor.Current;
_currentTenantAccessor.Current = new BasicTenantInfo(tenantId, name);
return new DisposeAction(() =>
{
_currentTenantAccessor.Current = parentScope;
});
}
}

这里的 BasicTenantInfoTenantConfiguraton 不同,前者仅用于在程序当中传递用户的基本信息,而后者是用于定于持久化的标准模型。

2.4 租户的使用


2.4.1 数据库过滤

租户的核心作用就是隔离不同客户的数据,关于过滤的基本逻辑则是存放在 AbpDbContext 的。从下面的代码可以看到,在使用的时候会从注入一个 ICurrentTenant 接口,这个接口可以获得从租户解析器里面取得的租户 Id 信息。并且还有一个 IsMultiTenantFilterEnabled() 方法来判定当前 是否应用租户过滤器

public abstract class AbpDbContext : DbContext, IEfCoreDbContext, ITransientDependency
where TDbContext : DbContext
{
protected virtual Guid? CurrentTenantId => CurrentTenant?.Id;
protected virtual bool IsMultiTenantFilterEnabled => DataFilter?.IsEnabled() ?? false;

// ... 其他的代码。

public ICurrentTenant CurrentTenant { get; set; }
// ... 其他的代码。
protected virtual Expression> CreateFilterExpression() where TEntity : class
{
// 定义一个 Lambda 表达式。
Expression> expression = null;
// 如果聚合根/实体实现了软删除接口,则构建一个软删除过滤器。
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
expression = e => !IsSoftDeleteFilterEnabled || !EF.Property(e, "IsDeleted");
}
// 如果聚合根/实体实现了多租户接口,则构建一个多租户过滤器。
if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
{
// 筛选 TenantId 为 CurrentTenantId 的数据。
Expression> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property(e, "TenantId") == CurrentTenantId;
expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter);
}
return expression;
}
// ... 其他的代码。
}

2.4.2 种子数据构建

Volo.ABP.TenantManagement 模块当中,如果用户创建了一个租户,ABP 不只是在租户表插入一条新数据而已。它还会设置种子数据的 构造上下文,并且执行所有的 种子数据构建者(IDataSeedContributor)。

[Authorize(TenantManagementPermissions.Tenants.Create)]
public virtual async Task CreateAsync(TenantCreateDto input)
{
var tenant = await TenantManager.CreateAsync(input.Name);
await TenantRepository.InsertAsync(tenant);
using (CurrentTenant.Change(tenant.Id, tenant.Name))
{
//TODO: Handle database creation?
//TODO: Set admin email & password..?
await DataSeeder.SeedAsync(tenant.Id);
} return ObjectMapper.Map(tenant);
}

这些构建者当中,就包括租户的超级管理员(admin)和角色构建,以及针对超级管理员角色进行权限赋值操作。

这里需要注意第二点,如果开发人员没有指定超级管理员用户和密码,那么还是会使用默认密码为租户生成超级管理员,具体原因看如下代码。

public class IdentityDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IIdentityDataSeeder _identityDataSeeder;
public IdentityDataSeedContributor(IIdentityDataSeeder identityDataSeeder)
{
_identityDataSeeder = identityDataSeeder;
}
public Task SeedAsync(DataSeedContext context)
{
return _identityDataSeeder.SeedAsync(
context["AdminEmail"] as string ?? "admin@abp.io",
context["AdminPassword"] as string ?? "1q2w3E*",
context.TenantId
);
}
}

所以开发人员要实现为不同租户 生成随机密码,那么就不能够使用 TenantManagement 提供的创建方法,而是需要自己编写一个应用服务进行处理。

2.4.3 权限的控制

如果开发人员使用了 ABP 提供的 Volo.Abp.PermissionManagement 模块,就会看到在它的种子数据构造者当中会对权限进行判定。因为有一些 超级权限 是租主才能够授予的,例如租户的增加、删除、修改等,这些超级权限在定义的时候就需要说明是否是数据租主独有的。

关于这点,可以参考租户管理模块在权限定义时,传递的 MultiTenancySides.Host 参数。

public class AbpTenantManagementPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var tenantManagementGroup = context.AddGroup(TenantManagementPermissions.GroupName, L("Permission:TenantManagement"));
var tenantsPermission = tenantManagementGroup.AddPermission(TenantManagementPermissions.Tenants.Default, L("Permission:TenantManagement"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Create, L("Permission:Create"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Update, L("Permission:Edit"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Delete, L("Permission:Delete"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageFeatures, L("Permission:ManageFeatures"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageConnectionStrings, L("Permission:ManageConnectionStrings"), multiTenancySide: MultiTenancySides.Host);
}
private static LocalizableString L(string name)
{
return LocalizableString.Create(name);
}
}

下面是权限种子数据构造者的代码:

public class PermissionDataSeedContributor : IDataSeedContributor, ITransientDependency
{
protected ICurrentTenant CurrentTenant { get; }
protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
protected IPermissionDataSeeder PermissionDataSeeder { get; }
public PermissionDataSeedContributor(
IPermissionDefinitionManager permissionDefinitionManager,
IPermissionDataSeeder permissionDataSeeder,
ICurrentTenant currentTenant)
{
PermissiOnDefinitionManager= permissionDefinitionManager;
PermissiOnDataSeeder= permissionDataSeeder;
CurrentTenant = currentTenant;
}
public virtual Task SeedAsync(DataSeedContext context)
{
// 通过 GetMultiTenancySide() 方法判断当前执行
// 种子构造者的租户情况,是租主还是租户。
var multiTenancySide = CurrentTenant.GetMultiTenancySide();
// 根据条件筛选权限。
var permissiOnNames= PermissionDefinitionManager
.GetPermissions()
.Where(p => p.MultiTenancySide.HasFlag(multiTenancySide))
.Select(p => p.Name)
.ToArray();
// 将权限授予具体租户的角色。
return PermissionDataSeeder.SeedAsync(
RolePermissionValueProvider.ProviderName,
"admin",
permissionNames,
context.TenantId
);
}
}

而 ABP 在判断当前是租主还是租户的方法也很简单,如果当前租户 Id 为 NULL 则说明是租主,如果不为空则说明是具体租户。

public static MultiTenancySides GetMultiTenancySide(this ICurrentTenant currentTenant)
{
return currentTenant.Id.HasValue
? MultiTenancySides.Tenant
: MultiTenancySides.Host;
}

2.4.4 租户的独立设置

关于这块的内容,可以参考之前的 这篇文章 ,ABP 也为我们提供了各个租户独立的自定义参数在,这块功能是由 TenantSettingManagementProvider 实现的,只需要在设置参数值的时候提供租户的 ProviderName 即可。

例如:

settingManager.SetAsync("WeChatIsOpen", "true", TenantSettingValueProvider.ProviderName, tenantId.ToString(), false);

三、总结

其他相关文章,请参阅 文章目录


推荐阅读
  • 单链表的高效遍历及性能优化策略
    本文探讨了单链表的高效遍历方法及其性能优化策略。在单链表的数据结构中,插入操作的时间复杂度为O(n),而遍历操作的时间复杂度为O(n^2)。通过在 `LinkList.h` 和 `main.cpp` 文件中对单链表进行封装,我们实现了创建和销毁功能的优化,提高了单链表的使用效率。此外,文章还介绍了几种常见的优化技术,如缓存节点指针和批量处理,以进一步提升遍历性能。 ... [详细]
  • Squaretest:自动生成功能测试代码的高效插件
    本文将介绍一款名为Squaretest的高效插件,该工具能够自动生成功能测试代码。使用这款插件的主要原因是公司近期加强了代码质量的管控,对各项目进行了严格的单元测试评估。Squaretest不仅提高了测试代码的生成效率,还显著提升了代码的质量和可靠性。 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • 本文详细介绍了使用 Python 进行 MySQL 和 Redis 数据库操作的实战技巧。首先,针对 MySQL 数据库,通过 `pymysql` 模块展示了如何连接和操作数据库,包括建立连接、执行查询和更新等常见操作。接着,文章深入探讨了 Redis 的基本命令和高级功能,如键值存储、列表操作和事务处理。此外,还提供了多个实际案例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • 本文详细探讨了在ASP.NET环境中通过加密数据库连接字符串来提升数据安全性的方法。加密技术不仅能够有效防止敏感信息泄露,还能增强应用程序的整体安全性。文中介绍了多种加密手段及其实施步骤,帮助开发者在日常开发过程中更好地保护数据库连接信息,确保数据传输的安全可靠。 ... [详细]
  • iOS 设备唯一标识获取的高效解决方案与实践
    在iOS 7中,苹果公司再次禁止了对MAC地址的访问,使得开发者无法直接获取设备的物理地址。为了在开发过程中实现设备的唯一标识,苹果推荐使用Keychain服务来存储和管理唯一的标识符。此外,还可以结合其他技术手段,如UUID和广告标识符(IDFA),以确保设备的唯一性和安全性。这些方法不仅能够满足应用的需求,还能保护用户的隐私。 ... [详细]
  • 本文详细介绍了如何安全地手动卸载Exchange Server 2003,以确保系统的稳定性和数据的完整性。根据微软官方支持文档(https://support.microsoft.com/kb833396/zh-cn),在进行卸载操作前,需要特别注意备份重要数据,并遵循一系列严格的步骤,以避免对现有网络环境造成不利影响。此外,文章还提供了详细的故障排除指南,帮助管理员在遇到问题时能够迅速解决,确保整个卸载过程顺利进行。 ... [详细]
  • Objective-C 中的委托模式详解与应用 ... [详细]
  • 提升Android开发效率:Clean Code的最佳实践与应用
    在Android开发中,提高代码质量和开发效率是至关重要的。本文介绍了如何通过Clean Code的最佳实践来优化Android应用的开发流程。以SQLite数据库操作为例,详细探讨了如何编写高效、可维护的SQL查询语句,并将其结果封装为Java对象。通过遵循这些最佳实践,开发者可以显著提升代码的可读性和可维护性,从而加快开发速度并减少错误。 ... [详细]
  • 第六章:枚举类型与switch结构的应用分析
    第六章深入探讨了枚举类型与 `switch` 结构在编程中的应用。枚举类型(`enum`)是一种将一组相关常量组织在一起的数据类型,广泛存在于多种编程语言中。例如,在 Cocoa 框架中,处理文本对齐时常用 `NSTextAlignment` 枚举来表示不同的对齐方式。通过结合 `switch` 结构,可以更清晰、高效地实现基于枚举值的逻辑分支,提高代码的可读性和维护性。 ... [详细]
  • 在使用SSH框架进行项目开发时,经常会遇到一些常见的问题。例如,在Spring配置文件中配置AOP事务声明后,进行单元测试时可能会出现“No Hibernate Session bound to thread”的错误。本文将详细探讨这一问题的原因,并提供有效的解决方案,帮助开发者顺利解决此类问题。 ... [详细]
  • 本文探讨了 Kafka 集群的高效部署与优化策略。首先介绍了 Kafka 的下载与安装步骤,包括从官方网站获取最新版本的压缩包并进行解压。随后详细讨论了集群配置的最佳实践,涵盖节点选择、网络优化和性能调优等方面,旨在提升系统的稳定性和处理能力。此外,还提供了常见的故障排查方法和监控方案,帮助运维人员更好地管理和维护 Kafka 集群。 ... [详细]
  • 本文总结了JavaScript的核心知识点和实用技巧,涵盖了变量声明、DOM操作、事件处理等重要方面。例如,通过`event.srcElement`获取触发事件的元素,并使用`alert`显示其HTML结构;利用`innerText`和`innerHTML`属性分别设置和获取文本内容及HTML内容。此外,还介绍了如何在表单中动态生成和操作``元素,以便更好地处理用户输入。这些技巧对于提升前端开发效率和代码质量具有重要意义。 ... [详细]
  • Spring框架的核心组件与架构解析 ... [详细]
  • 本文详细探讨了MySQL数据库实例化参数的优化方法及其在实例查询中的应用。通过具体的源代码示例,介绍了如何高效地配置和查询MySQL实例,为开发者提供了有价值的参考和实践指导。 ... [详细]
author-avatar
lodng
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有