
一个安静的程序猿~
原文 浅谈SQL Server内部运行机制 对于已经很熟悉T-SQL的读者,或者对于较专业的DBA来说,逻辑的增删改查,或者较复杂的SQL语句,都是非常简单的,不存在任何挑战,不值得一提,那么,SQL的哪些方面是他们的挑战 或者软肋呢? 那就是sql优化。然而,要向成为一个好的Sql优化高手,首先要做的一件事无疑就是了解sql语句在SQL Server中是如何执行的。在这一系列中,我们将开始sqlserver优化系列讲解,本 讲为优化系列的开篇文章, 在本篇文章中,我们将重点讲解SQL Server体系结构 在正式讲解之前,我们先来看看如下问题,你是否遇到过,若你遇到过且成功解决,那么这篇文章,你可以跳过。 为了测试需要,我们先模拟插入5亿3千多万条数据。 ? 1 SELECT COUNT(1) FROM BigDataTest (一)查询缓慢问题 *,临时表,表连接,子查询等造成的查询缓慢问题,你能解决吗? (二)内存泄漏 如下查询了8分2秒,然后内存溢出,你知道问题吗? ? 1 SELECT * FROM BigDataTest (三)经常听说如下概念,你都能解决吗? 事务与锁(请参考我另一篇文章:浅谈SQL Server事务与锁(上篇)),ACID,隔离级别,脏读,分表分库,水平拆分,垂直拆分,高并发等 一 SQL Server体系结构抽象 二 SQL Server体系结构概述 SQL Server核心体系结构,大致包括六大部分:客户端访问工具、SQL Server 网络接口(SQL Server Network Interface,SNI)、关系引擎、存储引擎、 磁盘和缓冲池。下图为SQL Server核心体系大致轮廓图。 (一)SQL Server客户端访问工具 SQL Server客户端访问工具,提供了远程访问技术,它与SQL Server服务端基于一定的协议,使其能够远程访问数据库,就像在本地操作数据库一样,如我们经常用的 Microsoft SQL Server Management Studio。 SQL Server客户端访问工具是比较多的,其中比较流行的要数Microsoft SQL Server Management Studio 和Navicat(Navicat在MySQL中也是比较常用的)了,至于其他工具, 本篇文章就不列举了,感兴趣的读者朋友,可以查询一下。 (二)SQL Server网络协议 SQL Server网络协议,又叫SQL Server网络接口(SNI),它是构成客户端和服务端通信的桥梁,它与SQL Server服务端基于一定协议,方可通信, 如我们在客户端输入一条查询语句SELECT * FROM BigDataTest,这条语句,只有客户端和服务端基于一定协议,方可被服务端解析,否则,被视为无 效语句。 SQL Server网络协议,由一组API构成,这些API供SQL Server数据库引擎和SQL Server本地客户端调用,如实现最基本的CRUD通信。 SQL Server 网络接口(SQL Server Network Interface,SNI)只需要在客户端和服务端配置网络协议即可,它支持一下协议: (1)共享内存 (2)TCP/IP (3)命名管道 (4)VIA (三)关系引擎 关系引擎,也叫查询引擎,其主要功能是负责处理SQL语句,其核心组件由三部分组成:命令分析器、查询优化器和查询执行器。 (1)命令分析器:负责解析客户端传递过来的T-SQL语句,如客户端传递一条SQL语句:SELECT * FROM BigDataTest,它会检查该语句的语法结构,若语法 错误,它会将错误返回给协议层,然后协议层将错误返回给客户端;如果语法结构正确,它会根据查询命令生成查询计划或寻找一个已存在的查询计划(先在缓冲池计划缓 存中查找,若找到,则直接给查询执行器执行,若未找到,则会生成基于T-SQL的查询树,然后交给查询优化器优化) (2)查询优化器:负责优化命令解析器生成的T-SQL查询树(基于资源的优化,而非基于时间的优化),然后将最终优化结果传递给查询执行器执行。查询优化器是基于 “资源开销”的优化器,这种算法评估多种可执行的查询方式,并从中选择开销最低的方案作为优化结果,然后将该结果生成查询计划输出给查询执行器。注意,查询优化器是 “基于资源开销最优”而非“基于方案最优”,也就是,查询优化器的最终优化结果未必是最好的方案,但一定是资源开销最低的方案。 (3)查询执行器:负责执行查询。假若查询执行器接收到命令解析器或查询优化器传递过来的SQL语句:SELECT * FROM BigDataTest,它通过OLE DB接口传递到存储 引擎,再传递到存储引擎的访问方法。 (四)存储引擎 存储引擎,本质就是管理资源存储的,它的核心组件包括三部分:访问方法、事务管理器和缓冲区管理器。 (1)访问方法:访问方法本质是一个接口,供查询执行器调用(该接口提供了所有检索数据的代码,接口的实际执行是由缓冲区管理器来执行的),假若查询执行器传递一条SQL语句: SELECT * FROM BigDataTest,访问方法接收到该请求命令后,就会调用缓冲区管理器,缓冲区管理器就会调用缓冲池的计划缓存,在计划缓存中寻找到相应的结果集,然后返回给关系 引擎。 (2)缓冲区管理器:供访问方法调用,管理缓冲池,在缓冲池中查询相应资源并返回结果集,供访问方法返回给关系引擎。 (3)事务管理器:主要负责事务的管理(ACID管理)和高并发管理(锁),它包括两个核心组件(日志管理器和锁管理器),锁管理器负责提供并发数据访问,设置隔离级别等;日志管理器负责 记录所有访问方法操作动作,如基本的CRUD。 (五)缓冲池 缓冲池驻于内存中,是磁盘和缓冲区管理器的桥梁SQL Server中,所有资源的查询都是在内存中进行的,即在缓冲池中进行的,假若缓冲池 接收到缓冲区管理器传递过来的的一条SQL语句:SELECT * FROM BigDataTest,缓冲区管理器数据缓存先从磁盘数据库中取满足条件的结果集, 然后放在缓冲池数据缓冲中,然后以结果集的形式返回给缓冲区管理器,供访问方法返回给关系引擎的查询执行器,然后返回给协议层,协议层再 返回给客户端。注意,这里操作的是缓冲池中数据,而不是磁盘DB中的数据,并且操作的缓冲池数据不会立即写入磁盘,因此就会造成查询到结果 与BD中的结果不一致,这就是所谓的脏读。 缓冲池主要包括两部分:计划缓存(生成执行计划是非常耗时耗资源的,计划缓存主要用来存储执行计划,以备后续使用)和数据缓存(通常是缓存池 中容量最大的,消耗内存最大,从磁盘中读取的数据页只要放在这里,方可调用) (六)磁盘 磁盘主要是用来存储持久化资源的,如日志资源,数据库资源和缓存池持久化支援等。 三 一个查询的完整流程 如下为一个比较完善的查询过程,即第二部分查询语句:SELECT * FROM BigDataTest 整个过程。 四 参考文献 【01】《SQL Server 2012 深入解析与性能优化 第3版》Christian Bolton,Justin Langford,Glenn Berry,Gavin Payne,Amit Banerjee,Rob Farley著 五 版权区 感谢您的阅读,若有不足之处,欢迎指教,共同学习、共同进步。 博主网址:http://www.cnblogs.com/wangjiming/。 极少部分文章利用读书、参考、引用、抄袭、复制和粘贴等多种方式整合而成的,大部分为原创。 如您喜欢,麻烦推荐一下;如您有新想法,欢迎提出,邮箱:2098469527@qq.com。 可以转载该博客,但必须著名博客来源。
原文 【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权 【.NET Core项目实战-统一认证平台】开篇及目录索引 上篇文章介绍了如何使用Dapper持久化IdentityServer4(以下简称ids4)的信息,并实现了sqlserver和mysql两种方式存储,本篇将介绍如何使用ids4进行客户端授权。 .netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。 一、如何添加客户端授权? 在了解如何进行客户端授权时,我们需要了解详细的授权流程,在【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析一篇中我大概介绍了客户端的授权方式,本篇再次回忆下客户端的授权方式,老规则,上源码。 首先查看获取token的方式,核心代码如下。 private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context) { _logger.LogDebug("Start token request."); // 1、验证客户端及授权信息结果 var clientResult = await _clientValidator.ValidateAsync(context); if (clientResult.Client == null) { return Error(OidcConstants.TokenErrors.InvalidClient); } // 2、验证请求结果 var form = (await context.Request.ReadFormAsync()).AsNameValueCollection(); _logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName); var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult); if (requestResult.IsError) { await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult)); return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse); } // 3、创建输出结果 _logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName); var response = await _responseGenerator.ProcessAsync(requestResult); await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult)); LogTokens(response, requestResult); // 4、返回结果 _logger.LogDebug("Token request success."); return new TokenResult(response); } 我们需要详细分析下第一步客户端授权信息是如何验证的?核心代码如下。 /// <summary> ///验证客户端授权结果 /// </summary> /// <param name="context">请求上下文</param> /// <returns></returns> public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context) { _logger.LogDebug("Start client validation"); var fail = new ClientSecretValidationResult { IsError = true }; //通过请求上下文和配置信息获取校验方式,从这里我们可以知道客户端请求的几种方式。 var parsedSecret = await _parser.ParseAsync(context); if (parsedSecret == null) { await RaiseFailureEventAsync("unknown", "No client id found"); _logger.LogError("No client identifier found"); return fail; } // 根据客户端ID获取客户端相关信息。(配合持久化篇查看) var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id); if (client == null) { await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client"); _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id); return fail; } SecretValidationResult secretValidationResult = null; if (!client.RequireClientSecret || client.IsImplicitOnly()) { _logger.LogDebug("Public Client - skipping secret validation success"); } else { //校验客户端授权和请求的是否一致 secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets); if (secretValidationResult.Success == false) { await RaiseFailureEventAsync(client.ClientId, "Invalid client secret"); _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId); return fail; } } _logger.LogDebug("Client validation success"); var success = new ClientSecretValidationResult { IsError = false, Client = client, Secret = parsedSecret, Confirmation = secretValidationResult?.Confirmation }; await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type); return success; } 这里几个方法可以从写的说明备注里就可以知道什么意思,但是 var parsedSecret = await _parser.ParseAsync(context);这句话可能不少人有疑问,这段是做什么的?如何实现不同的授权方式? 这块就需要继续理解Ids4的实现思路,详细代码如下。 /// <summary> /// 检查上下文获取授权信息 /// </summary> /// <param name="context">The HTTP context.</param> /// <returns></returns> public async Task<ParsedSecret> ParseAsync(HttpContext context) { // 遍历所有的客户端授权获取方式,提取当前哪一个满足需求 ParsedSecret bestSecret = null; foreach (var parser in _parsers) { var parsedSecret = await parser.ParseAsync(context); if (parsedSecret != null) { _logger.LogDebug("Parser found secret: {type}", parser.GetType().Name); bestSecret = parsedSecret; if (parsedSecret.Type != IdentityServerConstants.ParsedSecretTypes.NoSecret) { break; } } } if (bestSecret != null) { _logger.LogDebug("Secret id found: {id}", bestSecret.Id); return bestSecret; } _logger.LogDebug("Parser found no secret"); return null; } 就是从注入的默认实现里检测任何一个实现ISecretParser接口方法,通过转到实现,可以发现有PostBodySecretParser、JwtBearerClientAssertionSecretParser、BasicAuthenticationSecretParser三种方式,然后再查看下注入方法,看那些实现被默认注入了,这样就清楚我们使用Ids4时支持哪几种客户端授权方式。 /// <summary> /// 添加默认的授权分析 /// </summary> /// <param name="builder">The builder.</param> /// <returns></returns> public static IIdentityServerBuilder AddDefaultSecretParsers(this IIdentityServerBuilder builder) { builder.Services.AddTransient<ISecretParser, BasicAuthenticationSecretParser>(); builder.Services.AddTransient<ISecretParser, PostBodySecretParser>(); return builder; } 从上面代码可以发现,默认注入了两种分析器,我们就可以通过这两个方式来做客户端的授权,下面会分别演示两种授权方式的实现。 BasicAuthenticationSecretParser public Task<ParsedSecret> ParseAsync(HttpContext context) { _logger.LogDebug("Start parsing Basic Authentication secret"); var notfound = Task.FromResult<ParsedSecret>(null); var authorizationHeader = context.Request.Headers["Authorization"].FirstOrDefault(); if (authorizationHeader.IsMissing()) { return notfound; } if (!authorizationHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { return notfound; } var parameter = authorizationHeader.Substring("Basic ".Length); string pair; try { pair = Encoding.UTF8.GetString( Convert.FromBase64String(parameter)); } catch (FormatException) { _logger.LogWarning("Malformed Basic Authentication credential."); return notfound; } catch (ArgumentException) { _logger.LogWarning("Malformed Basic Authentication credential."); return notfound; } var ix = pair.IndexOf(':'); if (ix == -1) { _logger.LogWarning("Malformed Basic Authentication credential."); return notfound; } var clientId = pair.Substring(0, ix); var secret = pair.Substring(ix + 1); if (clientId.IsPresent()) { if (clientId.Length > _options.InputLengthRestrictions.ClientId || (secret.IsPresent() && secret.Length > _options.InputLengthRestrictions.ClientSecret)) { _logger.LogWarning("Client ID or secret exceeds allowed length."); return notfound; } var parsedSecret = new ParsedSecret { Id = Decode(clientId), Credential = secret.IsMissing() ? null : Decode(secret), Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret }; return Task.FromResult(parsedSecret); } _logger.LogDebug("No Basic Authentication secret found"); return notfound; } 由于代码比较简单,就不介绍了,这里直接模拟此种方式授权,打开PostMan,在Headers中增加Authorization的Key,并设置Value为Basic YXBwY2xpZW50JTNBc2VjcmV0,其中Basic后为client_id:client_secret值使用Base64加密。然后请求后显示如图所示结果,奈斯,得到我们授权的结果。 PostBodySecretParser public async Task<ParsedSecret> ParseAsync(HttpContext context) { _logger.LogDebug("Start parsing for secret in post body"); if (!context.Request.HasFormContentType) { _logger.LogDebug("Content type is not a form"); return null; } var body = await context.Request.ReadFormAsync(); if (body != null) { var id = body["client_id"].FirstOrDefault(); var secret = body["client_secret"].FirstOrDefault(); // client id must be present if (id.IsPresent()) { if (id.Length > _options.InputLengthRestrictions.ClientId) { _logger.LogError("Client ID exceeds maximum length."); return null; } if (secret.IsPresent()) { if (secret.Length > _options.InputLengthRestrictions.ClientSecret) { _logger.LogError("Client secret exceeds maximum length."); return null; } return new ParsedSecret { Id = id, Credential = secret, Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret }; } else { // client secret is optional _logger.LogDebug("client id without secret found"); return new ParsedSecret { Id = id, Type = IdentityServerConstants.ParsedSecretTypes.NoSecret }; } } } _logger.LogDebug("No secret in post body found"); return null; } 此种认证方式就是从form_data提取client_id和client_secret信息,我们使用PostMan继续模拟客户端授权,测试结果如下,也可以得到我们想要的结果。 有了前面的两个授权方式,我们清楚了首先验证客户端的授权信息是否一致,再继续观察后续的执行流程,这时会发现TokenRequestValidator中列出了客户端授权的其他信息验证,详细定义代码如下。 switch (grantType) { case OidcConstants.GrantTypes.AuthorizationCode: return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters); //客户端授权 case OidcConstants.GrantTypes.ClientCredentials: return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters); case OidcConstants.GrantTypes.Password: return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters); case OidcConstants.GrantTypes.RefreshToken: return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters); default: return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters); } 详细的授权验证代码如下,校验客户端授权的一般规则。 private async Task<TokenRequestValidationResult> ValidateClientCredentialsRequestAsync(NameValueCollection parameters) { _logger.LogDebug("Start client credentials token request validation"); ///////////////////////////////////////////// // 校验客户端Id是否开启了客户端授权 ///////////////////////////////////////////// if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.ClientCredentials)) { LogError("{clientId} not authorized for client credentials flow, check the AllowedGrantTypes of the client", _validatedRequest.Client.ClientId); return Invalid(OidcConstants.TokenErrors.UnauthorizedClient); } ///////////////////////////////////////////// // 校验客户端是否有请求的scopes权限 ///////////////////////////////////////////// if (!await ValidateRequestedScopesAsync(parameters, ignoreImplicitIdentityScopes: true, ignoreImplicitOfflineAccess: true)) { return Invalid(OidcConstants.TokenErrors.InvalidScope); } if (_validatedRequest.ValidatedScopes.ContainsOpenIdScopes) { LogError("{clientId} cannot request OpenID scopes in client credentials flow", _validatedRequest.Client.ClientId); return Invalid(OidcConstants.TokenErrors.InvalidScope); } if (_validatedRequest.ValidatedScopes.ContainsOfflineAccessScope) { LogError("{clientId} cannot request a refresh token in client credentials flow", _validatedRequest.Client.ClientId); return Invalid(OidcConstants.TokenErrors.InvalidScope); } _logger.LogDebug("{clientId} credentials token request validation success", _validatedRequest.Client.ClientId); return Valid(); } 最终输出详细的校验结果数据,现在整个客户端授权的完整逻辑已经介绍完毕,那如何添加我们的自定义客户端授权呢?比如我要给客户端A开放一个访问接口访问权限,下面就开通客户端A为案例讲解。 开通客户端授权 根据前面介绍的验证流程,我们清楚首先需要增加客户端信息,这里起名叫clienta,密码设置成secreta。上一篇我们介绍了Dapper持久化IdentityServer4的授权信息,所以这里我就直接以SQL语句的方式来演示添加配置信息。详细的语句如下: /* 添加客户端脚本 */ --1、添加客户端信息 INSERT INTO Clients(AccessTokenLifetime,ClientId,ClientName,Enabled) VALUES(3600,'clienta','测试客户端A',1); --2、添加客户端密钥,密码为(secreta) sha256 INSERT INTO ClientSecrets VALUES(21,'',null,'SharedSecret','2tytAAysa0zaDuNthsfLdjeEtZSyWw8WzbzM8pfTGNI='); --3、增加客户端授权权限 INSERT INTO ClientGrantTypes VALUES(21,'client_credentials'); --4、增加客户端能够访问scope INSERT INTO ClientScopes VALUES(21,'mpc_gateway'); 然后我们来测试下新开通的客户端授权,如下图所示,可以正常获取授权信息了,另外一种Basic授权方式可自行测试。 二、如何配合网关认证和授权? 前面使用的是项目自己进行验证的,正式项目运行时,我们会把请求放到网关中,统一由网关进行认证和授权等操作,内部api无需再次进行认证和授权,那如何实现网关认证和授权呢? 我们可以回忆下之前介绍网关篇时认证篇章,里面介绍的非常清楚。这里我们参照刚才添加的客户端A为案例增加网关授权,因为我们对外暴露的是网关地址,而不是内部具体认证项目地址。 1、添加网关授权路由 本项目的网关端口为7777,所以网关授权的地址为http://localhost:7777/connect/token,由于为添加网关路由,直接访问报401,我们首先增加网关的路由信息。 -- 1、插入认证路由(使用默认分类) insert into AhphReRoute values(1,'/connect/token','[ "POST" ]','','http','/connect/token','[{"Host": "localhost","Port": 6611 }]', '','','','','','','',0,1); --2、加入全局配置 INSERT INTO AhphConfigReRoutes VALUES(1,3) --3、增加认证配置地址路由 insert into AhphReRoute values(1,'/.well-known/openid-configuration','[ "GET" ]','','http','/.well-known/openid-configuration','[{"Host": "localhost","Port": 6611 }]', '','','','','','','',0,1); --4、加入全局配置 INSERT INTO AhphConfigReRoutes VALUES(1,4); --5、增加认证配置地址路由 insert into AhphReRoute values(1,'/.well-known/openid-configuration/jwks','[ "GET" ]','','http','/.well-known/openid-configuration/jwks','[{"Host": "localhost","Port": 6611 }]', '','','','','','','',0,1); --6、加入全局配置 INSERT INTO AhphConfigReRoutes VALUES(1,5); 通过PostMan测试,可以得到我们预期的授权信息结果。 然后继续访问我们之前配置的授权路由,提示401未授权,这块就涉及到前面网关篇的知识了,因为我们的网关增加了授权,所以需要增加客户端授权才能访问。 2、添加客户端授权访问 还记得是如何添加客户端授权的吗?详细介绍参考[【.NET Core项目实战-统一认证平台】第六章 网关篇-自定义客户端授权 ,我直接把授权的脚本编写如下: --7、插入把客户端加入测试路由组2 INSERT INTO AhphClientGroup VALUES(21,2) 使用我们刚授权的信息,再次访问之前配置的需要认证的路由,可以得到我们预期的结果,奈斯,和网关篇的内容完全一致。 注意:在配置完信息后需要清理缓存,因为我们之前做网关时,很多配置信息的读取使用了缓存。 三、如何统一输出结果? 作为一块准备应用到生产环境的产品,可能为各种第三方提供应用支持,那么统一的输出结果是必须要实现的,比如我们使用微信sdk或其他第三方sdk时,会发现它们都会列出出现错误的统一提示,由标识代码和说明组成,这里我们就需要解决如何标准化输出问题,自己业务系统输出标准结果很容易,因为都是自己控制的结果输出,那么我们网关集成Ocelot、认证集成IdentityServer4,这两块如何进行标准化输出呢? 那开始我们的改造之旅吧,首先我们要明确如果遇到错误如何进行输出,我们定义一个输出基类BaseResult,详细的定义如下: /// <summary> /// 金焰的世界 /// 2018-12-10 /// 信息输出基类 /// </summary> public class BaseResult { public BaseResult(int _errCode,string _errMsg) { errCode = _errCode; errMsg = _errMsg; } public BaseResult() { } /// <summary> /// 错误类型标识 /// </summary> public int errCode { get; set; } /// <summary> /// 错误类型说明 /// </summary> public string errMsg { get; set; } } /// <summary> /// 金焰的世界 /// 2018-12-10 /// 默认成功结果 /// </summary> public class SuccessResult : BaseResult { public SuccessResult() : base(0, "成功") { } } 1、网关默认输出改造 网关这段需要改造错误提示的代码和内容以及异常的输出结果,首先改造错误情况的输出结果,使用BaseResult统一输出,这里就需要重写输出中间件ResponderMiddleware,下面就开始重写之旅吧。 新增自定义输出中间件CzarResponderMiddleware,详细代码如下: using Czar.Gateway.Configuration; using Microsoft.AspNetCore.Http; using Ocelot.Errors; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responder; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; namespace Czar.Gateway.Responder.Middleware { /// <summary> /// 金焰的世界 /// 2018-12-10 /// 统一输出中间件 /// </summary> public class CzarResponderMiddleware: OcelotMiddleware { private readonly OcelotRequestDelegate _next; private readonly IHttpResponder _responder; private readonly IErrorsToHttpStatusCodeMapper _codeMapper; public CzarResponderMiddleware(OcelotRequestDelegate next, IHttpResponder responder, IOcelotLoggerFactory loggerFactory, IErrorsToHttpStatusCodeMapper codeMapper ) : base(loggerFactory.CreateLogger<CzarResponderMiddleware>()) { _next = next; _responder = responder; _codeMapper = codeMapper; } public async Task Invoke(DownstreamContext context) { await _next.Invoke(context); if (context.IsError) {//自定义输出结果 var errmsg = context.Errors[0].Message; int httpstatus = _codeMapper.Map(context.Errors); var errResult = new BaseResult() { errcode = httpstatus, errmsg = errmsg }; var message = errResult.ToJson(); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK; await context.HttpContext.Response.WriteAsync(message); return; } else if (context.HttpContext.Response.StatusCode != (int)HttpStatusCode.OK) {//标记失败,不做任何处理,自定义扩展时预留 } else if (context.DownstreamResponse == null) {//添加如果管道强制终止,不做任何处理,修复未将对象实例化错误 } else {//继续请求下游地址返回 Logger.LogDebug("no pipeline errors, setting and returning completed response"); await _responder.SetResponseOnHttpContext(context.HttpContext, context.DownstreamResponse); } } private void SetErrorResponse(HttpContext context, List<Error> errors) { var statusCode = _codeMapper.Map(errors); _responder.SetErrorResponseOnContext(context, statusCode); } } } 然后添加中间件扩展,代码如下。 using Ocelot.Middleware.Pipeline; namespace Czar.Gateway.Responder.Middleware { /// <summary> /// 金焰的世界 /// 2018-12-10 /// 应用输出中间件扩展 /// </summary> public static class CzarResponderMiddlewareExtensions { public static IOcelotPipelineBuilder UseCzarResponderMiddleware(this IOcelotPipelineBuilder builder) { return builder.UseMiddleware<CzarResponderMiddleware>(); } } } 最后使用此扩展来接管默认的输出中间件,详细代码如下。 //builder.UseResponderMiddleware(); builder.UseCzarResponderMiddleware(); 好了,网关统一输出中间件就完成了,是不是很简单呢?我们来测试下效果吧,PostMan闪亮登场, 奈斯,这才是我们需要的结果,那如何异常会输出什么呢??我们来模拟下结果,我直接在服务端抛出异常测试。 默认情况会支持输出异常的堆栈信息。那如何捕获服务端异常信息呢?我们需要了解在哪里发送了后端请求,通过源码分析,发现是由HttpRequesterMiddleware中间件做后端请求,这时我们只需要改造下此中间件即可完成统一异常捕获。改造核心代码如下: public async Task Invoke(DownstreamContext context) { var response = await _requester.GetResponse(context); if (response.IsError) { Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); SetPipelineError(context, response.Errors); return; } else if(response.Data.StatusCode != System.Net.HttpStatusCode.OK) {//如果后端未处理异常,设置异常信息,统一输出,防止暴露敏感信息 var error = new InternalServerError($"请求服务异常"); Logger.LogWarning($"路由地址 {context.HttpContext.Request.Path} 请求服务异常. {error}"); SetPipelineError(context, error); return; } Logger.LogDebug("setting http response message"); context.DownstreamResponse = new DownstreamResponse(response.Data); } 修改测试后端服务代码如下, // GET api/values/5 [HttpGet("{id}")] public ActionResult<string> Get(int id) { throw new Exception("测试异常"); } 然后通过网关访问路由地址http://localhost:7777/ctr/values/1,输出为{"errcode":500,"errmsg":"请求服务异常"},得到了预期的所有目标,网关统一输出全部改造完毕。 2、认证的统一输出改造 这里为了统一风格,我们先查看下Ids4的错误提示方式和输出结果,然后配合源码可以发现到输出都是继承IEndpointResult接口,并定义了各种方式的输出,且校验失败时,输出的状态码都不是200,那么我们可以从这里下手,在网关层增加独立的判断,来兼容自定义的输出。改造代码如下: using Czar.Gateway.Errors; using Newtonsoft.Json.Linq; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Requester; using System.Threading.Tasks; namespace Czar.Gateway.Requester.Middleware { /// <summary> /// 金焰的世界 /// 2018-12-10 /// 自定义请求中间件 /// </summary> public class CzarHttpRequesterMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; private readonly IHttpRequester _requester; public CzarHttpRequesterMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpRequester requester) : base(loggerFactory.CreateLogger<CzarHttpRequesterMiddleware>()) { _next = next; _requester = requester; } public async Task Invoke(DownstreamContext context) { var response = await _requester.GetResponse(context); if (response.IsError) { Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); SetPipelineError(context, response.Errors); return; } else if(response.Data.StatusCode != System.Net.HttpStatusCode.OK) {//如果后端未处理异常,设置异常信息,统一输出,防止暴露敏感信息 if (response.Data.StatusCode == System.Net.HttpStatusCode.BadRequest) {//提取Ids4相关的异常(400) var result = await response.Data.Content.ReadAsStringAsync(); JObject jobj = JObject.Parse(result); var errorMsg = jobj["error"]?.ToString(); var error = new IdentityServer4Error(errorMsg??"未知异常"); SetPipelineError(context, error); return; } else { var error = new InternalServerError($"请求服务异常"); Logger.LogWarning($"路由地址 {context.HttpContext.Request.Path} 请求服务异常. {error}"); SetPipelineError(context, error); return; } } Logger.LogDebug("setting http response message"); context.DownstreamResponse = new DownstreamResponse(response.Data); } } } 改造完成后,我们随时请求认证记录,最终显示效果如下。 奈斯,输出风格统一啦,这样就完美的解决了两个组件的输出问题,终于可以开心的使用啦。 四、内容总结 本篇我们详细的介绍了客户端授权的原理和支持的两个授权的方式,并各自演示了调用方式,然后知道了如何在数据库端新开通一个客户端的信息,然后介绍了配合网关实现客户端的授权和认证,并再次介绍了网关端的路由配置情况,最后介绍了如何把网关和认证统一输出格式,便于我们在正式环境的使用,涉及内容比较多,如果中间实现的有不对的地方,也欢迎大家批评指正。
原文 基于Token认证的多点登录和WebApi保护 在文章中有错误的地方,或是有建议或意见的地方,请大家多多指正,邮箱: linjie.rd@gmail.com 一天张三,李四,王五,赵六去动物园,张三没买票,李四制作了个假票,王五买了票,赵六要直接FQ进动物园 到了门口,验票的时候,张三没有买票被拒绝进入动物园,李四因为买假票而被补,赵六被执勤人员抓获,只有张三进去了动物园 后来大家才知道,当一个用户带着自己的信息去买票的时候,验证自己的信息是否正确,那真实的身份证(正确的用户名和密码),验证通过以后通过身份证信息和票据打印时间(用户登录时间)生成一个新的动物园参观票(Token令牌),给了用户一个,在动物园门口也保存了票据信息(相当与客户端和服务端都保存一份),在进动物园的时候两个票据信息对比,正确的就可以进动物园玩了 这就是我理解的Token认证.当然可能我的比喻不太正确,望大家多多谅解 下面是我们在服务端定义的授权过滤器 思路是根据切面编程的思想,相当于二战时期城楼门口设立的卡,当用户想api发起请求的时候,授权过滤器在api执行动作之前执行,获取到用户信息 如果发现用户没有登录,我们会判断用户要访问的页面是否允许匿名访问 用户没有登录但是允许匿名访问,放行客户端的请求 用户没有登录且不允许匿名访问,不允许通过,告诉客户端,状态码403或401,请求被拒绝了 如果发现用户登录,判断用户的良民证(Token令牌)是真的还是假的 用户登录,且良民证是真的,放行 发现良民证造价,抓起来,不允许访问 当然,这里可以加权限,验证是否有某个操作的权限 好了,服务端有验证了,客户端也不能拉下啊,客户端使用了动作过滤器,在用户操作之前或用户操作之后验证登录信息(这里可以加权限,验证是否有某个操作的权限) 客户端验证思路和服务端验证差不多 下面是客户端验证代码: 但是有良民证也不能也不能无限制的待在城里啊,我们做了一个时效性,在城市里什么时也不做到达一定的时长后得驱逐出城啊(类似与游戏中的挂机超过一定时间后T出本局游戏) 在这里使用的Redis记录良民证(Token),思路是用户登录之后生成的新的Token保存在Redis上,设定保存时间20分钟,当有用户有动作之后更新Redis保存有效期 下面是服务端验证token的,token有效,从新写入到Redis 以上就是Token认证 现在说说单点登录的思路 张三登录了qq:123456,生成了一个Token以键值对的方式保存在了数据库,键就是qq号,值就是qq信息和登录时间生成的一个Token 李四也登录了qq123456,qq信息是一致的,但是qq登录时间不同,生成了一个新的Token,在保存的时候发现Redis里已经存在这个qq的键了,说明这是已经有人登录了,在这里可以判断是否继续登录,登录后新的Token信息覆盖了张三登录QQ生成的Token,张三的Token失效了,当他再次请求的时候发现Token对应不上,被T下线了 多点登录也是,可以通过qq号加客户端类型作为键,这样手机qq登录的键是 123456_手机,电脑登录的键是123456_电脑,这样在保存到Redis的时候就不会发生冲突,可以保持手机和电脑同时在线 但是有一个人用手机登录qq 123456了,就会覆盖redis中键为123456_手机的Token信息,导致原先登录那个人的信息失效,被强制下线 来展示代码 判断是否可以登录 客户端类型实体 这是我们的客户端类型: 获取token需要的用户信息和登录时间的实体Model 这是我们的用户Model 生成Token用的实体 登录成功,通过JWT非对称加密生成Token 下面是JWT加密和解密的代码 将获取到的Token保存到Redis
原文:一文读懂阻塞、非阻塞、同步、异步IO 介绍 在谈及网络IO的时候总避不开阻塞、非阻塞、同步、异步、IO多路复用、select、poll、epoll等这几个词语。在面试的时候也会被经常问到这几个的区别。本文就来讲一下这几个词语的含义、区别以及使用方式。Unix网络编程一书中作者给出了五种IO模型:1、BlockingIO - 阻塞IO2、NoneBlockingIO - 非阻塞IO3、IO multiplexing - IO多路复用4、signal driven IO - 信号驱动IO5、asynchronous IO - 异步IO这五种IO模型中前四个都是同步的IO,只有最后一个是异步IO。信号驱动IO使用的比较少,重点介绍其他几种IO以及在Java中的应用。 阻塞、非阻塞、同步、异步以及IO多路复用 在进行网络IO的时候会涉及到用户态和内核态,并且在用户态和内核态之间会发生数据交换,从这个角度来说我们可以把IO抽象成两个阶段:1、用户态等待内核态数据准备好,2、将数据从内核态拷贝到用户态。之所以会有同步、异步、阻塞和非阻塞这几种说法就是根据程序在这两个阶段的处理方式不同而产生的。 同步阻塞 当在用户态调用read操作的时候,如果这时候kernel还没有准备好数据,那么用户态会一直阻塞等待,直到有数据返回。当kernel准备好数据之后,用户态继续等待kernel把数据从内核态拷贝到用户态之后才可以使用。这里会发生两种等待:一个是用户态等待kernel有数据可以读,另外一个是当有数据可读时用户态等待kernel把数据拷贝到用户态。 在Java中同步阻塞的实现对应的是传统的文件IO操作以及Socket的accept的过程。在Socket调用accept的时候,程序会一直等待知道有描述符就绪,并且把就绪的数据拷贝到用户态,然后程序中就可以拿到对应的数据。 同步非阻塞 对比第一张同步阻塞IO的图就会发现,在同步非阻塞模型下第一个阶段是不等待的,无论有没有数据准备好,都是立即返回。第二个阶段仍然是需要等待的,用户态需要等待内核态把数据拷贝过来才能使用。对于同步非阻塞模式的处理,需要每隔一段时间就去询问一下内核数据是不是可以读了,如果内核说可以,那么就开始第二阶段等待。 IO多路复用 IO多路复用也是同步的。 IO多路复用的方式看起来跟同步阻塞是一样的,两个阶段都是阻塞的,但是IO多路复用可以实现以较小的代价同时监听多个IO。通常情况下是通过一个线程来同时监听多个描述符,只要任何一个满足就绪条件,那么内核态就返回。IO多路复用使得传统的每请求每线程的处理方式得到解耦,一个线程可以同时处理多个IO请求,然后交到后面的线程池里处理,这也是netty等框架的处理方式,所谓的reactor模式。IO多路复用的实现依赖于操作系统的select、poll和epoll,后面会详细介绍这几个系统调用。 IO多路复用在Java中的实现方式是在Socket编程中使用非阻塞模式,然后配置感兴趣的事件,通过调用select函数来实现。select函数就是对应的第一个阶段。如果给select配置了超时参数,在指定时间内没有感兴趣事件发生的话,select调用也会返回,这也是为什么要做非阻塞模式下运行。 异步IO 异步模式下,前面提到的两个阶段都不会等待。使用异步模式,用户态调用read方法的时候,相当于告诉内核数据发送给我之后告诉我一声我先去干别的事情了。在这两个阶段都不会等待,只需要在内核态通知数据准备好之后使用即可。通常情况下使用异步模式都会使用callback,当数据可用之后执行callback函数。 IO多路复用 现在用Java开发的网络服务器通常采用IO多路复用的方式来加快网络IO操作,例如Netty、Tomcat等。IO多路复用的基础是select、poll和epoll。这三个函数是从操作系统的角度上支持的IO多路复用的操作,下面就分别来看一下这三个函数。 select 函数签名如下: int select(int maxfdp1, fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout) maxfdp1为指定的待监听的描述符的个数,因为描述符是从0开始的,所以需要加1readset为要监听的读描述符writeset为要监听的写描述符exceptset为要监听的异常描述符timeout监听没有准备好的描述符的话,多久可以返回,支持按照秒或者毫秒来配置时间 select操作的逻辑是首先将要监听的读、写以及异常描述符拷贝到内核空间,然后遍历所有的描述符,如果有感兴趣的事件发生,那么就返回。select在使用的过程中有三个问题:1、被监控的fds(描述符)集合限制为1024,1024太小了2、需要将描述符集合从用户空间拷贝到内核空间3、当有描述符可操作的时候都需要遍历一下整个描述符集合才能知道哪个是可操作的,效率很低。 poll 函数签名如下: int poll(struct pollfd[] fds, unsigned int nfds, int timeout); poll操作与select操作类似,仍旧避免不了描述符从用户空间拷贝到内核空间,但是poll不再有1024个描述符的限制。对于事件的触发通知还是使用遍历所有描述符的方式,因此在大量连接的情况下也存在遍历低效的问题。poll函数在传递参数的时候统一的将要监听的描述符和事件封装在了pollfd结构体数组中。 epoll epoll有三个方法:epoll_create、epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。 通过这三个方法epoll解决了select的三个问题。1、1024数量限制的问题通过epoll_create方法来创建一个epoll句柄,这个句柄监听的描述符的数量不再有限制。2、文件描述符频繁从用户空间拷贝到内核空间的问题通过观察select的操作会发现描述符从用户空间到内核空间拷贝发生在调用select方法的时候,只要没有注册新的事件或者取消注册事件,每次拷贝的描述符都是一样的。因此epoll引入了epoll_ctl调用,该方法用于注册新事件和取消注册事件。而在epoll_wait的时候并不会拷贝描述符,描述符始终存在于内核空间,当需要修改的时候只要调用epoll_ctl修改一下内核的描述符即可。如此一来便省去了描述符来回拷贝的开销。3、文件描述符可操作的时候遍历整个描述符集合的问题在调用epoll_ctl注册感兴趣的事件的时候,实际上会为设置的事件添加一个回调函数,当对应的感兴趣的事件发生的时候,回调函数就会触发,然后将自己加到一个链表中。epoll_wait函数的作用就是去查看这个链表中有没有已经准备就绪的事件,如果有的话就通知应用程序处理,如此操作epoll_wait只需要遍历就绪的事件描述符即可。 epoll在Java中的使用 目前针对Java服务器的非阻塞编程基本都是基于epoll的。在进行非阻塞编程的时候有两个步骤:1、注册感兴趣的事情;2、调用select方法,查找感兴趣的事件。 注册感兴趣的事件 我们在编写Socket的非阻塞代码的时候需要在Selector上注册感兴趣的事情,通常写法是serverSocketChannel.register(selector, SelectionKey.XXX)。来看一下这行代码背后的执行逻辑是什么样的。 注册的时候实际执行的是EPollSelectorImp。该方法主要有以下三步:1、implRegister方法。在fdToKey的Map中插入channel对应的文件描述法和SelectionKey的映射,当做注册Channel、关闭Channel、取消注册等操作是都是操作此Map。2、往pollWrapper[Epoll实例]中放入channel实例。3、往keys[HashSet]中放入SelectionKey select方法 通过Java的Selector.select方法来获取准备好的键的时候实际执行的代码如下: 首先调用EPollArrayWrapper的poll方法,该方法做两件事:1、调用epollCtl方法向epoll中注册感兴趣的事件;2、调用epollWait方法返回已就绪的文件描述符集合然后调用updateSelectedKeys方法调用把epoll中就绪的文件描述符加到ready队列中等待上层应用处理, updateSelectedKeys通过fdToKey查找文件描述符对应的SelectionKey,并在SelectionKey对应的channel中添加对应的事件到ready队列。 水平触发LT与边缘触发ET epoll支持两种触发模式,分别是水平触发和边缘触发。 LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。 ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核会通知你一次,并且除非你做了某些操作导致那个文件描述符不再为就绪状态了,否则不会再次发送通知。 可以看到,本来内核在被DMA中断,捕获到IO设备来数据后,只需要查找这个数据属于哪个文件描述符,进而通知线程里等待的函数即可,但是,LT要求内核在通知阶段还要继续再扫描一次刚才所建立的内核fd和io对应的那个数组,因为应用程序可能没有真正去读上次通知有数据后的那些fd,这种沟通方式效率是很低下的,只是方便编程而已; JDK并没有实现边缘触发,关于边缘触发和水平触发的差异简单列举如下,边缘触发的性能更高,但编程难度也更高,netty就重新实现了Epoll机制,采用边缘触发方式;另外像nginx等也采用的是边缘触发。 ---------------------------------------------------------------- 欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。
原文:如何构建高性能MySQL索引 介绍 上一篇文章中介绍了MySQL的索引基本原理以及常见的索引种类,这边文章的重点在于如何构建一个高性能的MySQL索引,从中你可以学到如何分析一个索引是不是好索引,以及如何构建一个好的索引。 索引误区 多列索引 一个索引的常见误区是为每一列创建一个索引,如下面创建的索引: CREATE TABLE `t` ( `c1` varchar(50) DEFAULT NULL, `c2` varchar(50) DEFAULT NULL, `c3` varchar(50) DEFAULT NULL, KEY `c1` (`c1`), KEY `c2` (`c2`), KEY `c3` (`c3`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; t表里有三列,并且为每列创建了一个索引。创建索引的人为了能够快速访问表中的任何一列,因此为每一列添加了一个单独的索引。在多个列上创建索引通常并不能很好的提高MySQL查询性能,虽然说MySQL 5.0之后引入了索引合并策略,可以将多个单列索引合并成一个索引,但这并不总是有效的。同时创建多个索引的时候还会增加数据插入的成本,在插入数据的时候需要同时维护多个索引的写入操作。 索引的计算 看下面这条sql语句: select name from student where id + 1 = 5 即使我们在student表的id列上建立索引,上面的这条SQL语句也无法使用索引。SQL语句中索引字段不能是表达式的一部分,也不能是函数的参数。 索引的长度以及选择性 尽量不要在一个很长的列上使用索引,否则会导致索引占用的空间很大,同时在进行数据的插入和更新的时候意味着更慢的速度。因此使用uuid列作为索引并不是一个好的选择。从上一篇文章中我们可以知道,为了加快数据的访问索引是需要常驻内存的,假如说我们把64位uuid作为索引,那么随着表中数据量的增加索引的大小也在急剧增加。同时因为uuid并没有顺序性,因此在数据插入的时候都需要从根节点找到当前索引的插入位置,如果同一个节点中的索引大小达到上限,还会导致节点分裂,更加降低了插入速度。 创建索引另外一个需要考虑的是索引的选择性,通常情况下我们会使用选择性高的列作为索引,但是也不一定一直是这样,下一节会介绍如何权衡索引的选择性。 创建高性能索引 选择正确的索引顺序 在选择索引的顺序的时候有一个原则:将索引选择性最高的列放在左侧,同时索引的顺序要与查询索引的顺序一致,并且要兼顾考虑排序和分组的需要。在一个多列B树多列中索引的顺序意味着索引首先按照最左侧的列进行排序,其次是第二列。所以无论是where语句还是order by语句都需要尽量满足这个顺序,这样才能更好的使用索引。 索引的选择性 列的选择性高的含义是通过这一列能够更多的过滤掉无用的数据,举个极端的例子,如果把自增id建成索引那么它的选择性是最高的,因为会把无用的数据都过滤掉,只会剩下一条有效数据。我们可以通过下面的方式来简单衡量某一个列的选择性: select count(distinct columnA)/count(*) as selectivity from table 当上面的数据越大的时候意味着columnA的选择性越高。这种方式提供了一个衡量平均选择性的办法,但是也不一定是有效的,需要具体情况具体分析。 前缀索引 当遇到特别长的列,但又必须要建立索引的时候可以考虑建立前缀索引。前缀索引的含义是把某一列的前N个字符作为索引,创建前缀索引的方式如下: alter table test add key(columnA(5)); 上面这个语句就是把columnA的前5个字符创建为前缀索引。前缀索引是一种使索引更小、更快的有效办法。但是前缀所有有一个缺点:MySQL无法使用前缀索引来做order by和group by,也无法使用前缀索引做覆盖扫描。 聚簇索引和非聚簇索引 聚簇索引 聚簇索引代表一种数据的存储方式,表示同一个结构中保存了B-Tree索引和数据行。也就是说当建立聚簇索引的时候实际的数据行存放在索引的叶子节点上。这也决定了每个表只能有一个聚簇索引。聚簇索引组织数据的方式如下图所示: 从图中可以看到索引的叶子节点和数据行是存放在一起的,这样的好处是可以直接读取到数据行。在创建表的时候如果我们不显式指定聚簇索引,那么MySQL将会按照下面的逻辑来选择聚簇索引:首先会通过主键列来聚集数据,如果没有主键列那么会选择唯一的非空索引来替代。如果还没有这样的索引那么会隐式的创建一个主键列来作为聚簇索引。 聚簇索引优点:1、相关数据存放在一起,检索的时候降低IO的次数2、数据访问更快3、使用覆盖索引扫描的查询可以直接使用节点中的主键值 在使用上面的优点的时候聚簇索引也有一定的缺点:1、聚簇索引将数据聚集在一起限制了插入速度,插入速度比较依赖于主键的顺序2、更新索引的时候代价会变高3、二级索引的访问的时候需要查找两次 非聚簇索引 非聚簇索引通常被称为二级索引,与聚簇索引的不同在于,非聚簇索引的叶子节点存放的是数据的行指针或者是一个主键值。这样在查找数据的时候首先定位到叶子节点上的主键值(或者行指针),然后通过主键值再到聚簇索引中查找到对应的数据。从中我们可以看到对于非聚簇索引的查询需要走两次索引。下图是一个非聚簇索引: 这个索引是InnoDB中的耳机索引,叶子节点中存储的是索引和主键。对于MyISAM叶子节点存储的是索引和行指针。 覆盖索引 如果一个索引包含或者说覆盖所有需要查询的字段的值,那么就称为覆盖索引。覆盖索引可以极大的提高查询的效率,如果我们的查询中只查询索引,而不用去回表那应该最好不过了。 通常我们使用explain关键字来查看一个查询语句的执行计划,通过执行计划我们可以了解到查询的细节。如果是覆盖索引,我们会看到执行计划的Extra列里有”Using Index”的信息。在查询语句中一般我们希望是where条件中的语句尽量能被覆盖,并且顺序要跟索引的保持一致。还有一个需要注意的点是MySQL不能在索引中使用like操作,这样会导致后面的索引失效。 后记 本文主要讲了几种索引的原理以及如何构建一个高性能的索引。索引的优先是一个渐进的过程,随着数据量和查询语句的不同而发生变化,重要的是了解索引的原理,这样做出正确的优化。下一篇文章中将会介绍explain关键字,教你如何来看执行计划,以及如何判断一个查询语句是否需要优化的。 ---------------------------------------------------------------- 欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。
原文:MySQL索引基础 介绍 索引用于加快数据访问的速度。把计算机的磁盘比作一本字典,索引就是字段的目录,当我们想快速查到某个词语的时候只需要通过查询目录找到词语所在的页数,然后直接打开某页就可以。MySQL最常用的索引是B+树索引,为什么使用B+作为MySQL的索引,这是许多面试官必问的问题。 为什么B+树 硬件相关知识 计算机的磁盘是一个圆盘的接口,圆盘上有一个个的圆圈,数据就是记录在这些圆圈的扇区上。如下图所示当计算机系统读取数据的时候要通过以下几个步骤:1、首先移动臂根据柱面号使磁头移动到所需要的柱面上,这一过程被称为寻道。所耗费的时间叫寻道时间(ts)。2、目标扇区旋转到磁头下,这个过程耗费的时间叫旋转时间。 因此访问磁盘的时间由三部分构成: 寻道时间+旋转时间+数据传输时间 第一部分寻道时间延迟最高,最大可达到100ms,旋转时间取决于磁盘的转速,转速在7200转/分钟的磁盘平均旋转时间在5ms左右。磁盘的读取是以block(盘块)为单位的,位于同一个盘块的数据可以一次性读取出来。在读写数据的时候尽量减少磁头来回移动的次数,避免过多的查找时间。如果每次从磁盘上读取数据的时候都要经历上面的几个过程那么效率上无疑是极低的。 为什么B+树 从上面可以看到,如果随机访问磁盘的速度是很慢的,因此需要设计一个合理的数据结构来减少随机访问磁盘的次数。B树就是这样一种数据结构。 B树、B+树介绍 B树 B树是为存储设备而设计的一种多叉平衡查找树。它与红黑树类似,但是在降低IO操作方面B树的表现要更好一些,B树与红黑树最大的区别在于B树可以有多个子节点,红黑树最多是有两个子节点,这就决定了大多数情况下B树的高度要比红黑树低很多,因此在查找的时候能够降低IO次数。下图是一棵B树:B 树又叫平衡多路查找树。一棵m阶的B树的特性如下: a.树中每个结点最多含有m个孩子(m>=2); b.除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数); c.若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点); d.所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息 e.每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中: a) Ki (i=1…n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。 b) Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。 c) 关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1。 B树中的每个节点都尽可能存储多的关键字信息和分支信息,但是不会超过磁盘块的大小。这样在有效降低了树的高度,在查找的时候可以快速定位在指定的磁盘块。假如要从上图中找到79这个数字,首先从根节点开始扫描,79大于35所以选择P3指针,指向磁盘块4,在磁盘块4中79在65和87之间,因此选择P2指针,选择磁盘块10,这时候就可以从磁盘块10中找到79。整个过程只需要3次IO,如果这棵树被缓存在内存中,那么只需要一次IO就可以读到79这个数字。 B+树 B+树是B的变种,一颗m阶B+树和m阶B树的异同点在于: 1、有n棵子树的节点中有n-1个关键字(与B树n棵子树有n-1个关键字,保持一致) 2、所有的叶子节点中包含了全部的关键字的信息,以及指向含有这些关键字记录的指针,且叶子节点本身依关键字的大小而自小而大顺序链接(而B树的叶子节点并没有包含全部需要查找的信息) 3、所有的非终端节点可以看成索引部分,节点中仅含有其子树根节点中最大或者最小的关键字(而B树的非终节点也要包含需要查找的有效信息) 由于B+树的叶子节点是连接在一起的,因此相对于使用B树作为索引,对于MySQL的范围查询更加优化。同时由于叶子节点包含所有关键字信息,因此有的查询语句就不需要回表,只需要查询索引就可以查到需要的数据。 索引类型 B树索引 虽然是叫B树索引,但是数据库实际上使用的是B+树来组织数据。B树索引意味着所有值都是按照顺序存储的,并且每个叶子节点到根节点的距离是相同的。假如有如下数据表: CREATE TABLE `people` ( `last_name` varchar(50) DEFAULT NULL, `first_name` varchar(50) DEFAULT NULL, `dob` date DEFAULT NULL, `gender` enum('m','f') DEFAULT NULL, KEY `last_name` (`last_name`,`first_name`,`dob`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 该表对last_name,first_name,dob三列建立了索引,索引的组织方式如下:当同时对多列进行索引的时候,索引的顺序是非常重要的,上面的索引首先是按照last_name进行索引,在last_name相同的情况下在对first_name进行排序,最后是dob字段。 B树索引适用于全键值、键值范围和最左前缀查找:全键值 查找名字为Allen Kim,出生日期为1930-07-12的人,这样的查找方式匹配了索引中的所有字段,依次扫描索引中的last_name、first_name和dob字段,找到对应的数据。键值范围 查找姓名在Allen和Barrymore之间的人,这种查找方式也会使用到索引。需要注意的是这里只能是索引中的第一列,也就是last_name的范围查找。前缀匹配 查找last_name是以Al开头的人,这种查询会以此扫描索引中的节点,然后选出来对应的复合条件的行。也是只能使用第一列的前缀查询。如果是说想查first_name的前缀匹配,那么是无法使用到索引的,意味着要进行全表扫描。精确匹配某一列,范围批量另外一列 精确匹配的列必须是所以中的第一列,范围匹配的列是第二列,这样才能使用到上面的索引。 B树索引的使用限制:1、不是按照最左列开始查询的,无法使用索引。2、不能跳过索引的列进行查询。3、如果使用到了范围匹配,那么范围匹配右边的列都无法使用索引查询。 哈希索引 哈希索引使用哈希表来实现,只有是精确匹配的时候才会生效。存储引擎会对索引列计算出一个哈希值,然后保存一个哈希值到行数据的指针。哈希索引由于其特殊的组织方式,限制了其使用场景。哈希索引只适合值比较少的情况,例如枚举类型。在以下几种方式中是不适合使用哈希索引的:1、哈希索引只包含哈希值和指针,不存储字段值,因此使用哈希索引避免不了要进行回表查询。2、哈希索引数据并不是按照值的顺序进行排序的,因此哈希索引无法用来排序3、哈希索引不支持部分索引列匹配。比如说在(A,B)两列上简历哈希索引,那么只有在同时使用A、B两列查询的时候才会使用哈希索引,只使用A列查询无法使用哈希索引。4、哈希索引只支持等值比较,不支持像between and这种范围查询。5、使用哈希索引的时候应该尽量避免哈希冲突。 后记 数据库的索引机制解决的问题是在访问内存数据与磁盘数据的速度差别很大的情况下,如何快速访问数据的问题。只有了解了索引的原理才可以更好的设计表的索引字段以及写出性能更优的查询语句。在我们写SQL语句的时候头脑中应该大体上能规划出查询数据以及如何使用索引的过程。下一篇会介绍一下高性能索引的策略,带你设计出更优的索引。 ---------------------------------------------------------------- 欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。
原文:Mysql 多主一从数据备份 Mysql 多主一从数据备份 概述 对任何一个数据库的操作都自动应用到另外一个数据库,始终保持两个数据库中的数据一致。 这样做有如下几点好处: 可以做灾备,其中一个坏了可以切换到另一个。 可以做负载均衡,可以将请求分摊到其中任何一台上,提高网站吞吐量。 对于异地热备,尤其适合灾备。 MySQL 主从复制的方式 1 Master 数据库操作的任何数据库的时候,都会将操作记录写入到biglog日志文件当中 2 Slave 数据库通过网络获取到主数据库的日志文件,写入本地日志系统 ,然后一条条的将数据库事件在数据库中完成 3 Slave 重做中继日志中的事件,将 Master 上的改变反映到它自己的数据库中,所以两端的数据是完全一样的。 环境 操作系统:CentOS MySQL版本:mysql-5.6.26 (主从两者数据库版本必须保持一致) Master1 配置 1 开启binlog日志功能 vim /etc/my.cnf server-id=6 log-bin=mysql-bin 2 重启mysql 登陆并授权 mysql -uroot -p123456 grant replication slave, replication client on *.* to 'repl'@'10.211.55.7' identified by '123456'; ip地址为slave服务器的ip地址 3 查看日志状态 show master status; Master2 配置 1 开启binlog日志功能 vim /etc/my.cnf server-id=8 log-bin=mysql-bin 2 重启mysql 登陆并授权 mysql -uroot -p123456 grant replication slave, replication client on *.* to 'repl'@'10.211.55.7' identified by '123456'; ip地址为slave服务器的ip地址 3 查看日志状态 show master status; Slave 配置 1 修改配置文件 (注意 slave的默认数据库启动的端口必须关闭 service mysql stop) vim /etc/my.cnf [mysqld] binlog-ignore-db=mysql binlog_format=mixed expire_logs_days=7 slave_skip_errors=1062 relay_log=mysql-relay-bin log_slave_updates=1 [mysqld_muliti] mysqld=/usr/bin/mysqld_safe mysqladmin=/usr/bin/mysqladmin user=root password=123456 [mysqld6] port=3306 datadir=/home/mysql/data6 pid-file=/home/mysql/data6/mysql.pid socket=/home/mysql/data6/mysql.sock user=mysql server-id=7 [mysqld8] port=3307 datadir=/home/mysql/data8 pid-file=/home/mysql/data8/mysql.pid socket=/home/mysql/data8/mysql.sock user=mysql server-id=7 2 初始化生成目录 /usr/local/mysql/scripts/mysql_install_db --user=mysql --basedir=/usr/local/mysql --datadir=/home/mysql/data6 & /usr/local/mysql/scripts/mysql_install_db --user=mysql --basedir=/usr/local/mysql --datadir=/home/mysql/data8 & 3 修改目录权限 chown -R mysql /home/mysql/data6 chown -R mysql /home/mysql/data8 4 启动服务 mysqld_multi --defaults-file=/etc/my.cnf start 6 mysqld_multi --defaults-file=/etc/my.cnf start 8 5 登录测试(并分别做授权) mysql -P 3306 -S /home/mysql/data6/mysql.sock mysql> change master to master_host='10.211.55.6', master_user='repl', master_password='123456', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos=120; mysql> start slave; mysql -P 3307 -S /home/mysql/data8/mysql.sock mysql> change master to master_host='10.211.55.8', master_user='repl', master_password='123456', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos=120; mysql> start slave; ok 就这样 完成了
原文:以Windows服务方式运行ASP.NET Core程序 我们对ASP.NET Core的使用已经进行了相当一段时间了,大多数时候,我们的Web程序都是发布到Linux主机上的,当然了,偶尔也有需求要发布到Windows主机上,这样问题就来了,难道直接以控制台形式运行这个Web程序吗? 直接以控制台形式运行程序当然是可以的,但有以下问题: 需要敲命令行(这个可以通过制作一个快捷方式解决) 用户也许会说有一个“黑黑的DOS窗口”,很奇怪 用户可能会随手把这个“黑黑的DOS窗口”关掉导致程序结束 不能够自动地随系统启动,得用户登录后再跑程序 如果我们把程序以Windows服务的方式来运行,那以上这些问题都没有了。 微软官方有篇文章,关于如何做这个事情的:https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/windows-service?view=aspnetcore-2.1 我也主要是参考了这篇文章,但其中还有些小小的坑,本文将会提到。OK,Let's get down to work. 包与生成目标 生成目标当然选择最新的.NET Core 2.1了,包则需要引入Microsoft.AspNetCore.Hosting.WindowsServices,最新版是2.1.1(2018/6/19更新,不就是今天嘛),如果你尝试使用最新的2.1.1版,那会出问题,报依赖冲突,你根据报错内容把ASP.NET 2.1.0升级到ASP.NET 2.1.1去,但依旧会出现问题: It was not possible to find any compatible framework versionThe specified framework 'Microsoft.AspNetCore.All', version '2.1.1' was not found. 看来需要安装新的SDK才行,但微软官网最新的正式版SDK就是2.1.300啊,这个还是等等吧,好,果断把版本降回2.1.0,这回没问题了。 修改Main入口 简单,非常简单,把Run改为RunAsService即可。 但这样会导致新的问题,你在开发过程中调试程序的时候会出现这样的错误:Cannot start service from the command line or a debugger. A Windows Service must first be installed and then started with the ServerExplorer, Windows Services Administrative tool or the NET START command. 程序跑不起来,意思显而易见,所以我们在调试中还是要以普通的Run方式。所以改成这样: IWebHost host = WebHost.CreateDefaultBuilder(args) .UseKestrel(options => { options.Listen(IPAddress.Any, 5201); }) //5201是我用的端口,你改成自己的 .UseStartup<Startup>() //使用Startup配置类 .Build(); bool isService = !(Debugger.IsAttached || args.Contains("--console")); //Debug状态或者程序参数中带有--console都表示普通运行方式而不是Windows服务运行方式 if (isService) { host.RunAsService(); } else { host.Run(); } 配置服务 ASP.NET Core publish生成的目标中并不包含exe文件,所以按照官网文档去弄的话可能会卡住,这是正确的姿势: sc create MyService binPath= "\"C:\program files\dotnet\dotnet.exe\" \"D:\Services\MyService.dll\"" DisplayName= "MyService" start= auto 注意1:必须以管理员身份运行上面命令注意2:binPath参数、DisplayName参数及start参数的等号后面必须带一个空格(官网文档也特别提起了这点) 启动服务: sc run MyService 停止服务: sc stop MyService 卸载服务: sc delete MyService 其它 默认情况下,Windows系统服务是以system用户身份运行的,如果你需要切换身份运行,可以在“控制面板 - 管理工具 - 服务”中找到MyService,打开属性面板,在“登录”Tab中指定特定身份。
原文:以Windows服务方式运行.NET Core程序 在之前一篇博客《以Windows服务方式运行ASP.NET Core程序》中我讲述了如何把ASP.NET Core程序作为Windows服务运行的方法,而今,我们又遇到了新的问题,那就是:我们的控制台程序,也就是普通的.NET Core程序(而不是ASP.NET Core程序)如何以服务的方式运行呢? 这个问题我们在.NET Core之前早就遇到过,那是是.NET Framework的时代(其实距今也没多远啦),我们是用一个第三方的组件——Topshelf,来解决这个问题的,Topshelf的官网是:http://topshelf-project.com/,它的使用很简单,官网上有具体的描述,对于一个普通的控制台程序而言(通常是一个不需要图形界面的服务),开发和调试的时候,把它当做一个普通的控制台程序来使用,十分方便;而实际部署的时候,通过传入不同的命令行参数,可以使它有了新的行为:安装Windows服务、运行Windows服务、停止/重启Windows服务或者卸载Windows服务。进入跨平台的.NET Core时代之后,Topshelf自然有了支持.NET Core的版本,使用方法与之前的类似,具体在此不表了,因为接下来我们根本不打算使用它! 现在我想要的是:不要引入任何组件,不要对现在控制台程序进行任何修改(ASP.NET Core程序也是控制台程序),开发调试时候不要进行任何复杂的参数配置,一切照旧,仅仅是在部署阶段,把程序当做Windows服务去运行。——你嘚讲吼不吼? 要达到这个目标,就要借助一个神器了,此神器为NSSM,Non-Sucking Service Manager,名字有点拗口,翻译成中文就是:不嗝屁服务管理器。 NSSM的官网是:https://nssm.cc/,十分简陋,但程序功能可是非常强大和全面的,下面我来一步步演示它如何使用。 1,先构建一个简单的服务程序 构建一个简单的服务程序,程序功能描述:程序没有图形界面,仅仅是定时记录一些日志(5秒钟写一下日志),在用户按下<Ctrl>+<C>的时候,程序退出。功能明确,Okay,let's get down to work. 1. 创建一个.NET Core Application,叫MyService 2. Nuget引入Quartz和NLog.Extensions.Logging,一个用来做定时任务,另一个用来log 3. 另外,程序使用了依赖注入,还需要用Nuget引入Microsoft.Extensions.DependencyInjection 4. 给项目增加NLog.Config配置文件,内容是 <?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" throwExceptions="false" internalLogLevel="Off"> <variable name="theLayout" value="${date:format=HH\:mm\:ss.fff} [${level}][${logger}] ${callsite:className=False:fileName=True:methodName=False} ${message} ${onexception:${newline}}${exception:format=Message,ShortType,StackTrace:innerFormat=Message,ShortType,StackTrace:separator=\r\n:innerExceptionSeparator=\r\n---Inner---\r\n:maxInnerExceptionLevel=5}"/> <targets> <target name="asyncFile" xsi:type="AsyncWrapper"> <target name="logfile" xsi:type="File" fileName="${basedir}/log/${shortdate}.log" layout="${theLayout}" encoding="UTF-8" /> </target> <target name="debugger" xsi:type="Debugger" layout="${theLayout}" /> <target name="console" xsi:type="Console" layout="${theLayout}" /> <target name="void" xsi:type="Null" formatMessage="false" /> </targets> <rules> <logger name="Quartz.*" minlevel="Trace" maxlevel="Info" writeTo="void" final="true" /> <logger name="*" minlevel="Debug" writeTo="asyncFile" /> <logger name="*" minlevel="Trace" writeTo="debugger"/> <logger name="*" minlevel="Trace" writeTo="console"/> </rules> </nlog> 还要注意的是这个文件必须复制到生成目录去以便程序运行时候能够加载到。 5. 增加MyServiceJobFactory.cs using Quartz; using Quartz.Spi; using System; namespace MyService { class MyServiceJobFactory : IJobFactory { protected readonly IServiceProvider _container; public MyServiceJobFactory(IServiceProvider container) { _container = container; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { return _container.GetService(bundle.JobDetail.JobType) as IJob; } public void ReturnJob(IJob job) { } } } 6. 增加PeriodLoggingJob.cs using Microsoft.Extensions.Logging; using Quartz; using System; using System.Threading.Tasks; namespace MyService { class PeriodLoggingJob : IJob { private readonly ILogger<PeriodLoggingJob> _logger; public PeriodLoggingJob(ILogger<PeriodLoggingJob> logger, IServiceProvider serviceProvider) { _logger = logger; } private void DoLoggingJob() { _logger.LogInformation("logging..."); } public Task Execute(IJobExecutionContext context) { try { DoLoggingJob(); } catch (Exception ex) { //必须妥善处理好定时任务中发生的异常 _logger.LogError(ex, "执行定时任务发生意外错误"); } returnTask.CompletedTask; } } } 7. Program.cs的完整内容如下 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using Quartz; using Quartz.Impl; using Quartz.Spi; using System; using System.Collections.Specialized; using System.IO; using System.Threading; namespace MyService { class Program { //注册各种服务 static void RegisterServices(IServiceCollection services) { //日志相关 services.AddSingleton<ILoggerFactory, LoggerFactory>(); services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)); //定时任务相关 services.AddSingleton<IJobFactory, MyServiceJobFactory>(); services.AddSingleton<PeriodLoggingJob>(); } static void Main(string[] args) { //注册退出事件处理(响应<Ctrl>+<C>) ManualResetEvent exitEvent = new ManualResetEvent(false); Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs e) { e.Cancel = true; exitEvent.Set(); }; //处理其它程序关闭事件(如kill),使得程序可以优雅地关闭 AppDomain.CurrentDomain.ProcessExit += (sender, e) => { exitEvent.Set(); }; //容器生成 ServiceCollection services = new ServiceCollection(); RegisterServices(services); using (ServiceProvider container = services.BuildServiceProvider()) { //日志初始化 var loggerFactory = container.GetRequiredService<ILoggerFactory>(); loggerFactory.AddNLog(new NLogProviderOptions { CaptureMessageTemplates = true, CaptureMessageProperties = true }); string nlogConfigFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NLog.config"); NLog.LogManager.LoadConfiguration(nlogConfigFile); //记录启动日志 ILogger<Program> logger = container.GetService<ILogger<Program>>(); logger.LogInformation("MyService启动."); //定时任务配置 NameValueCollection props = new NameValueCollection { { "quartz.serializer.type", "binary" } }; StdSchedulerFactory schedulerFactory = new StdSchedulerFactory(props); IScheduler scheduler = schedulerFactory.GetScheduler().Result; scheduler.JobFactory = container.GetService<IJobFactory>(); //每天1:00执行APP状态更新任务 ITrigger periodLoggingJobTrigger = TriggerBuilder.Create().WithIdentity("PeriodLoggingJobTrigger") .StartNow().WithSimpleSchedule(x=>x.WithIntervalInSeconds(5).RepeatForever()).Build(); IJobDetail checkPasswordOutOfDateJob = JobBuilder.Create<PeriodLoggingJob>().WithIdentity("PeriodLoggingJob").Build(); scheduler.ScheduleJob(checkPasswordOutOfDateJob, periodLoggingJobTrigger); //开启定时服务 scheduler.Start(); //----------------------------------------↑↑↑ 程序开始 ↑↑↑---------------------------------------- exitEvent.WaitOne(); //----------------------------------------↓↓↓ 程序结束 ↓↓↓---------------------------------------- //定时任务结束 scheduler.Shutdown(); //记录结束日志 logger.LogInformation("MyService停止."); } } } } 这就是整个服务程序的完整内容,本来我可以提供一个更简单的程序,这里啰里啰嗦写了这么一大堆,目的还是让初学者更加清楚.NET Core的程序结构和运行方式。其中内容包括:NLog的使用、Quartz的使用、容器及依赖注入的入门例子、如何处理程序关闭事件等,也许你想问“为什么要引入Quartz,搞这么复杂,弄个Timer不行吗?”当然行,但Quartz更强大,而且更适合给大家演示容器与依赖注入的使用。 8. 试运行程序 运行这个程序,输出几条日志信息后,以<Ctrl>+<C>来结束程序的运行,这样会在程序目录下产生log目录及日志文件,文件的内容大致如下: 19:03:37.117 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:55) MyService启动. 19:03:37.637 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:42.536 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:47.535 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:49.293 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:80) MyService停止. 9. 发布程序 选择publish,在publish的目标目录下产生一堆文件,将这些文件复制到D:\Service\MyService目录下,一会儿我们要用到这个目录。 2,NSSM配置 首先要获取NSSM程序,当然是要到官网下载,版本选择最新版,尽管它声称是pre-release版,但功能杠杠的,没有任何影响,而正式版(非pre-release)则是2014年的了,太旧了。下载下来后找到对应的exe文件,叫nssm.exe。(注意有32位版和64位版的分别) 它是个绿色软件,不需要安装,仅此一个exe文件,把这个文件复制到C:\Windows\System32目录下,之后经常要用。 在Windows命令行中直接敲nssm,会出现它的帮助提示。 1. 安装服务 >nssm install MyService 出现配置界面(注意,需要管理员权限) 配置选项比较多,这是我的配置,供参考: 点“Install service”即将服务安装好了。我们打开Windows服务来查看所安装的服务: 服务已经安装完毕,一切准备就绪。 2. 启动服务 >nssm start MyService 其它一些操作其实不用我说大家也应该知道了: nssm status MyService 查看服务状态 nssm stop MyService 停止服务 nssm restart MyService 重启服务 nssm edit MyService 重新配置服务的参数 nssm remove MyService 删除服务 其余的请自行参考nssm的使用手册。注意事项:需要用管理员身份来执行上面这些命令,否则会出现访问拒绝的错误。 3,分享一些想法 2018年快过去了,回顾这一年来,我觉得我在公司所做的最大且重要的一件事情就是推动了.NET Core的应用,将能迁移的.NET Framework的程序都迁移至.NET Core了,为什么要这么干?最最主要的原因当然是要跨平台,原先ASP.NET开发的网站,只能运行于Windows平台,它们得依赖于IIS!Windows(作为服务器)本身就是一个非常复杂的系统,有着各种令人眼花缭乱的配置,加上IIS,就更加令人感到困惑,我同意IIS是功能强大的服务器程序,但它真的过于复杂,设计不合理,很难用,让我等菜鸟频频掉到它的坑里爬不出来。IIS并不是一个能够自由选择版本的软件,它的版本通常认为与Windows操作系统绑定,微软官方并不建议安装与Windows操作系统原生版本不一致的IIS,所以现在甚至还有公司继续在用IIS6,而各个版本的IIS的行为却不尽相同,默认IIS并不带安装ASP.NET组件,所以在Windows系统和IIS刚部署好的时候,想直接运行ASP.NET网站居然还不行,要自己去安装ASP.NET的支持,完成后还需要使用一条额外的命令来注册ASP.NET组件,另外还可能遇到稀奇古怪的问题,大多数问题可以通过安装若干个补丁解决(如ASP.NET MVC的路由不起作用导致网站无法访问的问题),而有时则不会那么顺利,你得仔细看看这些补丁是否符合当前操作系统及IIS版本,甚至操作系统的语言版本也会影响你所要安装的补丁。IIS与ASP.NET程序之间的关系也是令人很懵逼,我想让我的ASP.NET程序自始至终运行着就是做不到,尽管应用程序池里似乎有这个选项,我在StackOverflow上针对相关问题进行过讨论,有不少人顶我,但也有人说不行(我猜跟IIS版本还有关系),ASP.NET程序空闲一段时间后便被IIS踢掉——即便你的主机不差内存,你无法肯定IIS一运行你的程序就跟着跑起来,也无法肯定你的程序什么时候在运行,什么时候被踢掉,这是个类似薛定谔的猫的问题,你的ASP.NET程序就通常处于这么一种“叠加态”,你得看一看才知道确切它是否在运行,这一看,才使得程序从“叠加态”坍缩为“生态”或“死态”,且从“死态”转入“生态”还需要耗费好些时间,表现为第一次打开页面时候的长时间卡顿,跟客户演示系统,有时候会很尴尬。我曾经为了让程序不被IIS踢掉,还手工写了一个KeepAlive的小程序,定时去get我的网站的首页,实在奇葩。微软对此的解释是:IIS并不是为long-term程序设计的,你想在IIS里做一个准时的定时服务,那是相当不妥,根本不是为这种事情设计的,所以不好用不能怪我。我承认这当然是一种设计,但ASP.NET网站除了提供网页之外,跑一些后台服务也应该是很正常的吧?没办法,于是我将服务和网站分开,中间用总线沟通,听起来很cool?——其实这是一段悲伤的往事,不过说来话长,以后有机会再提了。.NET Core出现了,ASP.NET Core也和它一起到来,2.0版开始就是一个很完善的版本,我想是时候上了,这是工作量很大的差事,但为了将来更好的发展,我们必须经历这个艰难的爬坡,所幸的是现在一切都已转入正轨,我预想的目的达到了。.NET Core的一大特点就是程序都可以独立运行,包括ASP.NET Core程序,不再依赖于IIS,我可以根据业务的需要,将系统划分为多个模块,方便开发分工和测试,这些模块甚至不需要部署在同一台主机上,极大提高了灵活性。一般来说,我还是推荐将程序部署至Linux环境,理由依旧是Linux作为服务器操作系统的使用体验远远好于Windows,Windows实在太过复杂了!但也有例外,如果遇到缺乏Linux支持技术的客户的情况,那就把程序部署到他们的Windows主机上吧,无所谓,反正.NET Core是跨平台的。不知这是不是我2018年的最后一篇博客,如果是,上面这段文字就算是我对今年自己的主要工作总结吧。
原文:.NET Core跨平台部署 .NET Core跨平台部署 1. Windows-IIS 大家对于在IIS上部署.NET站点已经驾轻就熟了,部署.NET Core也没有什么本质区别,但是这其中仍然有一些细节是不同的,下面记录了一些我在部署时遇到的问题 1.1 安装.NET Core Windows Server Hosting 要在IIS上运行ASP.NET Core,必须安装.NET Core Windows Server Hosting 安装完成后最好重启IIS 如果没有安装该组件就直接打开部署的网站会出现 500.19 相关的配置数据无效 1.2 配置应用程序池 Core的IIS站点应用程序池的.NET CLR版本要选择 无托管代码 1.3 使用发布文件 我最开始测试的时候,仍然使用Web根目录作为网站的物理路径,但是网站无法访问,报HTTP403错误——Web 服务器被配置为不列出此目录的内容,也是就是这个文件夹下没有可以访问的文件,在查阅网上的资料后发现其他人都是使用了发布文件夹作为物理路径,生成发布版本设置相应路径后.NET Core的示例站点即可正常访问 发布文件夹结构 成功访问 2 Linux 微软官方给出了不同系统的部署方法Tutorial Guide,由于Linux有不同的版本,所以这里选择CentOS作为示例,有以下几个步骤 2.1 添加.NET产品依赖 在安装.NET之前,你需要注册微软的Key,注册产品仓库,并且安装需要的依赖,在每台机器上只需要做一次。 直接执行以下命令: sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm 2.2 安装.NET SDK 更新可供安装的产品,然后安装.NET SDK 输入以下命令: sudo yum update sudo yum install dotnet-sdk-2.2 中间有两次手动确认,然后等待安装完成即可 2.3 创建你的应用 通过输入命令就可以创建一个官方的示例.NET Core程序 dotnet new console -o myApp cd myApp 第一条命令新建应用,第二条进入应用文件夹 通过 ls 命令我们可以看到该文件夹下只有两个文件,obj是文件夹 默认的主文件Program.cs的内容如下: using System; namespace myApp { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } } 2.4 运行应用 dotnet run 2.5 创建web应用 使用mkdir命令新建一个文件夹mvc,然后进入目录 创建网站 dotnet new mvc 然后发布这个网站程序 dotnet restore dotnet publish -c release 默认的发布目录是在/bin/release/netcoreapp2.x/publish/里,可以新建一个目录拷贝进去 scp -r /root/mvc/bin/release/netcoreapp2.2/publish/* /root/www/firstapp 2.6 从外网访问web应用 完成发布后,已经可以通过执行dotnet命令来启动网站了,但是只能在内网访问,显然这不是我们想要的,要想从外网访问,我们需要反向代理服务器,这里选择Nginx 使用yum命令远程安装 sudo yum install epel-release yum install nginx 启动 systemctl start nginx #启用Nginx systemctl enable nginx #设置开机启动 这时候已经可以直接通过服务器的IP地址的80端口访问Nginx的测试页了,需要注意的是如果使用阿里云服务器,需要在安全组配置中开放80端口才能够访问 接下来根据需要进行一些端口的配置,dotnet默认的访问端口为5000,但是我测试的时候好像是在linux上被占用了,所以对 Program.cs 进行修改,使其可以通过其他端口访问,这里使用8080 public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseUrls("http://*:8080") .UseStartup<Startup>(); } 改完之后需要重新生成发布,开始我使用的是微软官方的示例程序,这里为了对比端口,我在自己Windows系统下新建了一个2.1的示例程序,使用VS2017进行程序修改,发布后通过xftp再上传到Linux服务器上 然后去修改Nginx的配置,默认的路径应该是/etc/nginx/nginx.conf,在server节点下的location节点加一句 proxy_pass http://localhost:8080; 就可以 server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { proxy_pass http://localhost:8080; } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } } 修改完成后测试并重启Nginx服务 sudo nginx -t #测试配置 sudo nginx -s reload #重新加载配置 配置完成之后,启动网站访问服务器IP地址的8080端口即可 但是启动网站这里存在一个问题,如果像上面那样没有使用cd命令进入网站目录启动,样式和脚本等文件的路径就会出现错误,导致页面显示不正常所以要在网站目录启动 基本的网站部署就到这里,下一次讲讲用Docker如何进行.NET Core的部署与开发
原文:[UWP] 用 AudioGraph 来增强 UWP 的音频处理能力——AudioFrameInputNode 上一篇心得记录中提到了 AudioGraph, 描述了一下 什么是 AudioGraph 以及其中涉及到的各种类型的 节点(Node)。 这一篇就其中比较有意思的 AudioFrameInputNode 来详细展开一下。 借用 AudioFrameInputNode, 实现简单的音频左右声道互换 什么是 AudioFrameInputNode? 在微软的文档中这么介绍 An audio frame input node allows you to push audio data that you generate in your own code into the audio graph. This enables scenarios like creating a custom software synthesizer. 按照我个人的理解,AudioFrameInputNode 可以让我们自由的访问音频数据,音频数据是 PCM 格式,我们可以对音频数据做一些魔改,具体怎么魔改,就需要一些音频处理的算法知识了。 如何使用 AudioFrameInputNode? 1.创建 AudioFrameInputNode AudioEncodingProperties nodeEncodingProperties = audioGraph.EncodingProperties; nodeEncodingProperties.ChannelCount = 2; nodeEncodingProperties.Subtype = "float"; nodeEncodingProperties.SampleRate = 44100; nodeEncodingProperties.BitsPerSample = 32; AudioFrameInputNode frameInputNode = audioGraph.CreateFrameInputNode(nodeEncodingProperties); frameInputNode.QuantumStarted += FrameInputNode_QuantumStarted; 所有的音频输入节点,都必须通过 AudioGragh 的实例方法来创建,AudioFrameInputNode 也不例外,在创建时,需要传入一个 AudioEncodingProperties,来描述 AudioFrameInputNode 需要处理的音频的一些属性。 在创建完成一个 AudioFrameInputNode 的对象实例后,需要订阅其 QuantumStarted 事件,这个事件会在 AudioGraph 开始处理音频数据时调用,在该事件方法内部,可以完成对音频数据的添加和修改。 2.访问 AudioFrame AudioFrameInputNode 是基于 AudioFrame, 需要对其数据进行读取和写入。 所以在事件的订阅方法 FrameInputNode_QuantumStarted 内部,需要对 AudioFrame 填充 PCM 音频数据。 首先需要创建一个 AudioFrame 对象,在构造函数中,需要传入缓冲区的大小。 在这个示例中,每一个 采样点(Sample) 都是 Float 类型,采用立体声,也就是双通道,所以计算缓冲区大小的代码如下: var bufferSize = args.RequiredSamples * sizeof(float) * 2; AudioFrame audioFrame = new AudioFrame((uint)bufferSize); 在 AudioFrame 内部是一个 AudioBuffer,它代表存储 PCM 数据的缓冲区,所以接下来需要获取对该缓冲区的访问权,需要如下方法: AudioBuffer audioBuffer = audioFrame.LockBuffer(AudioBufferAccessMode.Write); IMemoryBufferReference bufferReference = audioBuffer.CreateReference(); 通过 AudioBuffer 的实例方法 CreateReference,得到 IMemoryBufferReference 的对象,它实际上是一个 COM 接口,通过如下方法强制转换,可以获取 native 的缓冲区指针和缓冲区长度: ((IMemoryBufferByteAccess)bufferReference).GetBuffer(out byte* dataInBytes, out uint capacityInBytes); 其中 IMemoryBufferByteAccess 接口定义如下: [ComImport] [Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] unsafe interface IMemoryBufferByteAccess { void GetBuffer(out byte* buffer, out uint capacity); } 注意,因为用到了指针,所以需要在工程配置文件中 允许unsafe code 选项打开, 并且在该方法签名中指明 unsafe 关键字。 至此,就得到了音频数据缓冲区的指针,但是此时整个缓冲区都是空的,需要填充 PCM 音频数据。 此处便是 AudioFrame 的便利之处,因为我们可以任意填充我们想要的音频数据,无论是处理过的还是没有处理过的。而获取 PCM 原始音频数据的途径很多,可以代码生成,也可以从文件读取,对于我这种对音频处理技术几乎白痴的人,我选择从一个 PCM 文件导入。 此处可以借用 Adobe Audition 等工具转换生成 PCM。 3.PCM 音频数据填充 打开一个 PCM 格式的文件流 fileStream, 其中 PCM 采样率是44100,32位浮点型,立体声。这些格式很重要,需要和初始化 AudioFrameInputNode 对象实例时设定的一样,才能保证数据填充过程正确。 在构造 AudioFrame 时传入了代表缓冲区长度的值 bufferSize,所以此处需要从文件流 fileStream 读取对应长度的数据到内存中, var managedBuffer = new byte[capacityInBytes]; var lastLength = fileStream.Length - fileStream.Position; int readLength = (int)(lastLength < capacityInBytes ? lastLength : capacityInBytes); if (readLength <= 0) { fileStream.Close(); fileStream = null; return; } fileStream.Read(managedBuffer, 0, readLength); 为了稍微体现一下 AudioFrameInputNode 的价值,这儿对要填充的数据做一项最简单的处理,即交换左右声道的内容。 在 PCM 中,每一个 Sample 是四个字节,具体排布是: 左声道,右声道,左声道,右声道,左声道,右声道,左声道,右声道........ 所以交换声道就很简单了,代码如下: for (int i = 0; i < readLength; i+=8) { dataInBytes[i+4] = managedBuffer[i+0]; dataInBytes[i+5] = managedBuffer[i+1]; dataInBytes[i+6] = managedBuffer[i+2]; dataInBytes[i+7] = managedBuffer[i+3]; dataInBytes[i+0] = managedBuffer[i+4]; dataInBytes[i+1] = managedBuffer[i+5]; dataInBytes[i+2] = managedBuffer[i+6]; dataInBytes[i+3] = managedBuffer[i+7]; } 因为 dataInBytes 是缓冲区的指针,所以对缓冲区赋值就是填充缓冲区的过程。在填充完后,需要释放 audioBuffer 和 bufferReference 对象,避免内存泄漏。 踩到的坑 大小端问题 借用百度百科内容: 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。 二进制内容在内存里面存储,是存在大小端问题的,对于PCM格式,也存在大小端问题,所以如果对数据想进一步处理,大小端的问题一定要注意。 在C#中调用 native 内容时,我的机器上实测时小端模式。 也可以通过如下 unsafe 代码来判断: int temp = 0x01; int* pTempInt = &temp; byte* pTempByte = (byte*)pTempInt; if(0x01== *pTempByte) { //小端 } else { //大端 } float 在内存中如何排布? 对于 int 类型,将其转换为二进制后,求补码,即是它在内存中的实际值,但是对于浮点型,就有一套自己的计算方法了,可以参考如下博客(大学计算机课本里的内容,忘得差不多了) float & double 内存布局 附件 Github AudioFrameInputNode Demo 附上我测试用的 PCM 数据,44100,32位 浮点型,小端模式 听说最近杭州下雪了,这歌现在很火! 许嵩-断桥残雪 片段 PCM 下图是该 PCM 的原始波形图, 所以听的时候听到的顺序应该是:先右声道,再立体声,最后左声道,和波形图里相反。 记得耳机别戴反!
参考:https://www.jianshu.com/p/50a2e13cd5cf 安装MySQL Utilities 下载地址:https://dev.mysql.com/downloads/utilities/ 下载完后直接安装即可 如果运行时需要python就下载 https://dev.mysql.com/downloads/connector/python/ 本机还需要安装与需要恢复数据的一样版本的Mysql 使用mysqlfrm命令读取frm的表结构 把需要进行数据恢复的frm文件放到一个目录里 mysqlfrm --diagnostic ./frm文件目录/ 这样就可以获得数据表的结构了。 创建新的数据库 把第二步获得的数据表结构执行,(利用旧的脚本)创建表。 对已创建的表进行表空间卸载 ALTER TABLE 表名 DISCARD TABLESPACE; 每个表都进行一次空间卸载 停掉MYSQL服务 把原始数据文件里的ibd文件拷到新的数据库文件夹里这里要注意把拷过来的ibd文件的所有者为mysql chwon mysql:mysql 数据库文件夹/*启动MYSQL服务 对数据表进行空间装载 ALTER TABLE 表名 IMPORT TABLESPACE; 每个表都进行一次空间装载
原文:C# 实现生成带二维码的专属微信公众号推广海报 很多微信公众号中需要生成推广海报的功能,粉丝获得专属海报后可以分享到朋友圈或发给朋友,为公众号代言邀请好友即可获取奖励的。海报自带渠道二维码,粉丝长按二维码即可关注微信公众号,从而达到吸粉的目的。 效果如下: 代码实现: 1.获取临时二维码ticket /// <summary> /// 获取临时二维码ticket /// </summary> /// <param name="scene_str">场景值ID openid做场景值ID</param> /// <returns></returns> public static string CreateTempQRCode(string scene_str,string access_token) { var result = HttpUtility.SendPostHttpRequest($"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={access_token}", "application/json", "{\"expire_seconds\": 2592000, \"action_name\": \"QR_STR_SCENE\", \"action_info\": {\"scene\": {\"scene_str\": \"" + scene_str + "\"}}}"); JObject jobect = (JObject)JsonConvert.DeserializeObject(result); string ticket = (string)jobect["ticket"]; if (string.IsNullOrEmpty(ticket)) { LogHelper.WriteLog(typeof(WeixinHelper), "获取临时二维码ticket失败" + result); return null; } return ticket; } 使用openid作为场景值的好处是通过扫A推广的二维码关注用户的场景值便是A的openid。 2. 生成带二维码的专属推广图片 /// <summary> /// 生成带二维码的专属推广图片 /// </summary> /// <param name="user"></param> /// <returns></returns> public string Draw(WxUser user) { //背景图片 string path = Server.MapPath("/Content/images/tg.jpg"); System.Drawing.Image imgSrc = System.Drawing.Image.FromFile(path); //处理二维码图片大小 240*240px System.Drawing.Image qrCodeImage = ReduceImage("https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+user.ticket, 240, 240); //处理头像图片大小 100*100px Image titleImage = ReduceImage(user.headimgurl, 100, 100); using (Graphics g = Graphics.FromImage(imgSrc)) { //画专属推广二维码 g.DrawImage(qrCodeImage, new Rectangle(imgSrc.Width - qrCodeImage.Width - 200, imgSrc.Height - qrCodeImage.Height - 200, qrCodeImage.Width, qrCodeImage.Height), 0, 0, qrCodeImage.Width, qrCodeImage.Height, GraphicsUnit.Pixel); //画头像 g.DrawImage(titleImage, 8, 8, titleImage.Width, titleImage.Height); Font font = new Font("宋体", 30, FontStyle.Bold); g.DrawString(user.nickname, font, new SolidBrush(Color.Red), 110, 10); } string newpath = Server.MapPath(@"/Content/images/newtg_" + Guid.NewGuid().ToString() + ".jpg"); imgSrc.Save(newpath, System.Drawing.Imaging.ImageFormat.Jpeg); return newpath; } /// <summary> /// 缩小/放大图片 /// </summary> /// <param name="url">图片网络地址</param> /// <param name="toWidth">缩小/放大宽度</param> /// <param name="toHeight">缩小/放大高度</param> /// <returns></returns> public Image ReduceImage(string url, int toWidth, int toHeight) { WebRequest request = WebRequest.Create(url); WebResponse response = request.GetResponse(); Stream responseStream = response.GetResponseStream(); Image originalImage = Image.FromStream(responseStream); if (toWidth <= 0 && toHeight <= 0) { return originalImage; } else if (toWidth > 0 && toHeight > 0) { if (originalImage.Width < toWidth && originalImage.Height < toHeight) return originalImage; } else if (toWidth <= 0 && toHeight > 0) { if (originalImage.Height < toHeight) return originalImage; toWidth = originalImage.Width * toHeight / originalImage.Height; } else if (toHeight <= 0 && toWidth > 0) { if (originalImage.Width < toWidth) return originalImage; toHeight = originalImage.Height * toWidth / originalImage.Width; } Image toBitmap = new Bitmap(toWidth, toHeight); using (Graphics g = Graphics.FromImage(toBitmap)) { g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; g.Clear(Color.Transparent); g.DrawImage(originalImage, new Rectangle(0, 0, toWidth, toHeight), new Rectangle(0, 0, originalImage.Width, originalImage.Height), GraphicsUnit.Pixel); originalImage.Dispose(); return toBitmap; } } 3.将图片上传微信服务器,并发送给用户 string imagePath = Draw(user); string result = HttpUtility.UploadFile($"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={access_token}&type=image", imagePath); JObject jObject = (JObject)JsonConvert.DeserializeObject(result); string media_id = (string)jObject["media_id"]; if (!string.IsNullOrEmpty(media_id)) { string resxml = "<xml><ToUserName><![CDATA[" + xmlMsg.FromUserName + "]]></ToUserName><FromUserName><![CDATA[" + xmlMsg.ToUserName + "]]></FromUserName><CreateTime>" + nowtime + "</CreateTime><MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[" + media_id + "]]></MediaId></Image></xml>"; return resxml; } LogHelper.WriteLog(typeof(WechatController), "上传专属推广图片素材失败" + result); 详细请查看 http://blog.yshizi.cn/50.html
原文:业务重点-实现一个简单的手机号码验证 前言 本文纯干货,直接拿走使用,不用付费。在业务开发中,手机号码验证是我们常常需要面对的问题,目前市场上各种各样的手机号码验证方式,比如正则表达式等等,本文结合实际业务场景,在业务级别对手机号码进行严格验证;同时增加可配置方式,方便业务扩展,代码非常简单,扩展非常灵活。 1. 目前手机号段有哪些 1.1 目前国内的手机号段主要集中在三大运营商手上,还有一些内部号段和虚拟号段 "中国电信": "133,153,189,180,181,177,173,199,174,141", "中国移动": "139,138,137,136,135,134,159,158,157,150,151,152,147,188,187,182,183,184,178,198", "中国联通": "130,131,132,146,156,155,166,186,185,145,175,176", "虛拟运营商": "170,171", "内部号码": "123" 2. 建立一个测试项目 Ron.PhoneTest 2.1 将上面的号段加入配置文件 appsettings.json 中 { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "phone-segment": { "中国电信": "133,153,189,180,181,177,173,199,174,141", "中国移动": "139,138,137,136,135,134,159,158,157,150,151,152,147,188,187,182,183,184,178,198", "中国联通": "130,131,132,146,156,155,166,186,185,145,175,176", "虛拟运营商": "170,171", "内部号码": "123" } } 3. 建立一个检查类,负责初始化号段库和校验的工作 public class PhoneValidator { private static readonly Regex checktor = new Regex(@"^1\d{10}$"); public IDictionary segment = null; public PhoneValidator(IDictionary segment) { this.segment = segment; } public bool IsPhone(ref string tel) { if (string.IsNullOrEmpty(tel)) { return false; } tel = tel.Replace("+86-", "").Replace("+86", "").Replace("86-", "").Replace("-", ""); if (!checktor.IsMatch(tel)) { return false; } string s = tel.Substring(0, 3); if (segment.Count > 0 && !segment.Contains(s)) { return false; } return true; } } 4. 通过 Startup.cs 实现读取配置和注入,以便系统使用 public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); CreatePhoneValidator(services); } private void CreatePhoneValidator(IServiceCollection services) { Hashtable segment = new Hashtable(); var coll = Configuration.GetSection("phone-segment").GetChildren(); foreach (var prefix in coll) { if (string.IsNullOrEmpty(prefix.Value)) continue; foreach (var s in prefix.Value.Split(',')) segment[s] = s; } var pv = new PhoneValidator(segment); services.AddSingleton<PhoneValidator>(pv); } 以上代码通过读取配置文件节点 phone-segment 并初始化 PhoneValidator 类,最后注入到 IServiceCollection 中,完成了初始化的工作 5. 在控制器中使用 PhoneValidator 进行验证 5.1 示例代码 [Route("api/home")] [ApiController] public class HomeController : ControllerBase { PhoneValidator validator = null; public HomeController(PhoneValidator pv) { validator = pv; } [HttpGet("login")] public IActionResult Login(string phone) { bool accept = validator.IsPhone(ref phone); return new JsonResult(new { phone, accept }); } } 5.2 运行项目,在浏览器中输入地址 http://localhost:33868/api/home/login?phone=86-13800138000 5.3 输出结果 结语 通过上面的示例,可以实现对各种各样手机号码的控制,由于号段写在配置文件中,我们可以在业务扩展到时候去动态的增加号段,还可以针对各个地区去扩展 PhoneValidator 类,以实现切合业务的验证需求,从此,手机号码验证不再需要一刀切。 示例代码下载 https://files.cnblogs.com/files/viter/Ron.PhoneTest.zip
原文:.NET redis cluster 一、下载Windows版本Redis 下载链接:https://github.com/MSOpenTech/redis/releases(根据系统选择对应版本) 二、修改默认的配置文件 如上图两个配置文件,redis.windows.conf(应用程序配置文件);redis.windows-service.conf(Redis windows 服务使用的配置文件)。 主要配置: 1. bind #IP 2.port #端口 3.loglevel #日志级别 4.logfile #日志保存位置 5.dir #数据保存地址 6.cluster-enabled yes #启用集群 7.cluster-config-file #nodes.conf ( redis记录文件,自动生成) 8.cluster-node-timeout #失效时间(毫秒) 注意:以上配置节点行头不要留有空格,否则会报错。 三、准备集群配置文件 将修改好的配置文件复制如下图 四、准备集群环境 安装rubay(由于 Redis 的集群使用 ruby脚本编写,所以系统需要有 Ruby 环境) 下载启动脚本:redis-trib.rb 五、编写批量启动Redis脚本(可选) @echo offcd 安装目录start redis-server.exe ./redis.windows.conf 如: 另存为bat文件,方便启动集群实例。 六、执行集群命令 1.先利用上面编写.bat文件启动Redis实例。 2.用cmd进入Redis安装目录 3.执行集群命令 ruby redis-trib.rb create --replicas 1 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 出现以上说明准备的环境是没有问题的。输入yes(表示同意上面的集群配置) 最后出现OK,说明集群成功。 七、集群测试 1.输入cluster info查看集群信息 2.ASP.NET MVC测试 至此集群配置成功。
原文:【.NET Core项目实战-统一认证平台】第九章 授权篇-使用Dapper持久化IdentityServer4 【.NET Core项目实战-统一认证平台】开篇及目录索引 上篇文章介绍了IdentityServer4的源码分析的内容,让我们知道了IdentityServer4的一些运行原理,这篇将介绍如何使用dapper来持久化Identityserver4,让我们对IdentityServer4理解更透彻,并优化下数据请求,减少不必要的开销。 .netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。 一、数据如何实现持久化 在进行数据持久化之前,我们要了解Ids4是如何实现持久化的呢?Ids4默认是使用内存实现的IClientStore、IResourceStore、IPersistedGrantStore三个接口,对应的分别是InMemoryClientStore、InMemoryResourcesStore、InMemoryPersistedGrantStore三个方法,这显然达不到我们持久化的需求,因为都是从内存里提取配置信息,所以我们要做到Ids4配置信息持久化,就需要实现这三个接口,作为优秀的身份认证框架,肯定已经帮我们想到了这点啦,有个EFCore的持久化实现,GitHub地址https://github.com/IdentityServer/IdentityServer4.EntityFramework,是不是万事大吉了呢?拿来直接使用吧,使用肯定是没有问题的,但是我们要分析下实现的方式和数据库结构,便于后续使用dapper来持久化和扩展成任意数据库存储。 下面以IClientStore接口接口为例,讲解下如何实现数据持久化的。他的方法就是通过clientId获取Client记录,乍一看很简单,不管是用内存或数据库都可以很简单实现。 Task<Client> FindClientByIdAsync(string clientId); 要看这个接口实际用途,就可以直接查看这个接口被注入到哪些方法中,最简单的方式就是Ctrl+F ,通过查找会发现,Client实体里有很多关联记录也会被用到,因此我们在提取Client信息时需要提取他对应的关联实体,那如果是数据库持久化,那应该怎么提取呢?这里可以参考IdentityServer4.EntityFramework项目,我们执行下客户端授权如下图所示,您会发现能够正确返回结果,但是这里执行了哪些SQL查询呢? 从EFCore实现中可以看出来,就一个简单的客户端查询语句,尽然执行了10次数据库查询操作(可以使用SQL Server Profiler查看详细的SQL语句),这也是为什么使用IdentityServer4获取授权信息时奇慢无比的原因。 public Task<Client> FindClientByIdAsync(string clientId) { var client = _context.Clients .Include(x => x.AllowedGrantTypes) .Include(x => x.RedirectUris) .Include(x => x.PostLogoutRedirectUris) .Include(x => x.AllowedScopes) .Include(x => x.ClientSecrets) .Include(x => x.Claims) .Include(x => x.IdentityProviderRestrictions) .Include(x => x.AllowedCorsOrigins) .Include(x => x.Properties) .FirstOrDefault(x => x.ClientId == clientId); var model = client?.ToModel(); _logger.LogDebug("{clientId} found in database: {clientIdFound}", clientId, model != null); return Task.FromResult(model); } 这肯定不是实际生产环境中想要的结果,我们希望是尽量一次连接查询到想要的结果。其他2个方法类似,就不一一介绍了,我们需要使用dapper来持久化存储,减少对服务器查询的开销。 特别需要注意的是,在使用refresh_token时,有个有效期的问题,所以需要通过可配置的方式设置定期清除过期的授权信息,实现方式可以通过数据库作业、定时器、后台任务等,使用dapper持久化时也需要实现此方法。 二、使用Dapper持久化 下面就开始搭建Dapper的持久化存储,首先建一个IdentityServer4.Dapper类库项目,来实现自定义的扩展功能,还记得前几篇开发中间件的思路吗?这里再按照设计思路回顾下,首先我们考虑需要注入什么来解决Dapper的使用,通过分析得知需要一个连接字符串和使用哪个数据库,以及配置定时删除过期授权的策略。 新建IdentityServerDapperBuilderExtensions类,实现我们注入的扩展,代码如下。 using IdentityServer4.Dapper.Options; using System; using IdentityServer4.Stores; namespace Microsoft.Extensions.DependencyInjection { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 使用Dapper扩展 /// </summary> public static class IdentityServerDapperBuilderExtensions { /// <summary> /// 配置Dapper接口和实现(默认使用SqlServer) /// </summary> /// <param name="builder">The builder.</param> /// <param name="storeOptionsAction">存储配置信息</param> /// <returns></returns> public static IIdentityServerBuilder AddDapperStore( this IIdentityServerBuilder builder, Action<DapperStoreOptions> storeOptionsAction = null) { var options = new DapperStoreOptions(); builder.Services.AddSingleton(options); storeOptionsAction?.Invoke(options); builder.Services.AddTransient<IClientStore, SqlServerClientStore>(); builder.Services.AddTransient<IResourceStore, SqlServerResourceStore>(); builder.Services.AddTransient<IPersistedGrantStore, SqlServerPersistedGrantStore>(); return builder; } /// <summary> /// 使用Mysql存储 /// </summary> /// <param name="builder"></param> /// <returns></returns> public static IIdentityServerBuilder UseMySql(this IIdentityServerBuilder builder) { builder.Services.AddTransient<IClientStore, MySqlClientStore>(); builder.Services.AddTransient<IResourceStore, MySqlResourceStore>(); builder.Services.AddTransient<IPersistedGrantStore, MySqlPersistedGrantStore>(); return builder; } } } 整体框架基本确认了,现在就需要解决这里用到的几个配置信息和实现。 DapperStoreOptions需要接收那些参数? 如何使用dapper实现存储的三个接口信息? 首先我们定义下配置文件,用来接收数据库的连接字符串和配置清理的参数并设置默认值。 namespace IdentityServer4.Dapper.Options { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 配置存储信息 /// </summary> public class DapperStoreOptions { /// <summary> /// 是否启用自定清理Token /// </summary> public bool EnableTokenCleanup { get; set; } = false; /// <summary> /// 清理token周期(单位秒),默认1小时 /// </summary> public int TokenCleanupInterval { get; set; } = 3600; /// <summary> /// 连接字符串 /// </summary> public string DbConnectionStrings { get; set; } } } 如上图所示,这里定义了最基本的配置信息,来满足我们的需求。 下面开始来实现客户端存储,SqlServerClientStore类代码如下。 using Dapper; using IdentityServer4.Dapper.Mappers; using IdentityServer4.Dapper.Options; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System.Data.SqlClient; using System.Threading.Tasks; namespace IdentityServer4.Dapper.Stores.SqlServer { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 实现提取客户端存储信息 /// </summary> public class SqlServerClientStore: IClientStore { private readonly ILogger<SqlServerClientStore> _logger; private readonly DapperStoreOptions _configurationStoreOptions; public SqlServerClientStore(ILogger<SqlServerClientStore> logger, DapperStoreOptions configurationStoreOptions) { _logger = logger; _configurationStoreOptions = configurationStoreOptions; } /// <summary> /// 根据客户端ID 获取客户端信息内容 /// </summary> /// <param name="clientId"></param> /// <returns></returns> public async Task<Client> FindClientByIdAsync(string clientId) { var cModel = new Client(); var _client = new Entities.Client(); using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { //由于后续未用到,暂不实现 ClientPostLogoutRedirectUris ClientClaims ClientIdPRestrictions ClientCorsOrigins ClientProperties,有需要的自行添加。 string sql = @"select * from Clients where ClientId=@client and Enabled=1; select t2.* from Clients t1 inner join ClientGrantTypes t2 on t1.Id=t2.ClientId where t1.ClientId=@client and Enabled=1; select t2.* from Clients t1 inner join ClientRedirectUris t2 on t1.Id=t2.ClientId where t1.ClientId=@client and Enabled=1; select t2.* from Clients t1 inner join ClientScopes t2 on t1.Id=t2.ClientId where t1.ClientId=@client and Enabled=1; select t2.* from Clients t1 inner join ClientSecrets t2 on t1.Id=t2.ClientId where t1.ClientId=@client and Enabled=1; "; var multi = await connection.QueryMultipleAsync(sql, new { client = clientId }); var client = multi.Read<Entities.Client>(); var ClientGrantTypes = multi.Read<Entities.ClientGrantType>(); var ClientRedirectUris = multi.Read<Entities.ClientRedirectUri>(); var ClientScopes = multi.Read<Entities.ClientScope>(); var ClientSecrets = multi.Read<Entities.ClientSecret>(); if (client != null && client.AsList().Count > 0) {//提取信息 _client = client.AsList()[0]; _client.AllowedGrantTypes = ClientGrantTypes.AsList(); _client.RedirectUris = ClientRedirectUris.AsList(); _client.AllowedScopes = ClientScopes.AsList(); _client.ClientSecrets = ClientSecrets.AsList(); cModel = _client.ToModel(); } } _logger.LogDebug("{clientId} found in database: {clientIdFound}", clientId, _client != null); return cModel; } } } 这里面涉及到几个知识点,第一dapper的高级使用,一次性提取多个数据集,然后逐一赋值,需要注意的是sql查询顺序和赋值顺序需要完全一致。第二是AutoMapper的实体映射,最后封装的一句代码就是_client.ToModel();即可完成,这与这块使用还不是很清楚,可学习相关知识后再看,详细的映射代码如下。 using System.Collections.Generic; using System.Security.Claims; using AutoMapper; namespace IdentityServer4.Dapper.Mappers { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 客户端实体映射 /// </summary> /// <seealso cref="AutoMapper.Profile" /> public class ClientMapperProfile : Profile { public ClientMapperProfile() { CreateMap<Entities.ClientProperty, KeyValuePair<string, string>>() .ReverseMap(); CreateMap<Entities.Client, IdentityServer4.Models.Client>() .ForMember(dest => dest.ProtocolType, opt => opt.Condition(srs => srs != null)) .ReverseMap(); CreateMap<Entities.ClientCorsOrigin, string>() .ConstructUsing(src => src.Origin) .ReverseMap() .ForMember(dest => dest.Origin, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientIdPRestriction, string>() .ConstructUsing(src => src.Provider) .ReverseMap() .ForMember(dest => dest.Provider, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientClaim, Claim>(MemberList.None) .ConstructUsing(src => new Claim(src.Type, src.Value)) .ReverseMap(); CreateMap<Entities.ClientScope, string>() .ConstructUsing(src => src.Scope) .ReverseMap() .ForMember(dest => dest.Scope, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientPostLogoutRedirectUri, string>() .ConstructUsing(src => src.PostLogoutRedirectUri) .ReverseMap() .ForMember(dest => dest.PostLogoutRedirectUri, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientRedirectUri, string>() .ConstructUsing(src => src.RedirectUri) .ReverseMap() .ForMember(dest => dest.RedirectUri, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientGrantType, string>() .ConstructUsing(src => src.GrantType) .ReverseMap() .ForMember(dest => dest.GrantType, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientSecret, IdentityServer4.Models.Secret>(MemberList.Destination) .ForMember(dest => dest.Type, opt => opt.Condition(srs => srs != null)) .ReverseMap(); } } } using AutoMapper; namespace IdentityServer4.Dapper.Mappers { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 客户端信息映射 /// </summary> public static class ClientMappers { static ClientMappers() { Mapper = new MapperConfiguration(cfg => cfg.AddProfile<ClientMapperProfile>()) .CreateMapper(); } internal static IMapper Mapper { get; } public static Models.Client ToModel(this Entities.Client entity) { return Mapper.Map<Models.Client>(entity); } public static Entities.Client ToEntity(this Models.Client model) { return Mapper.Map<Entities.Client>(model); } } } 这样就完成了从数据库里提取客户端信息及相关关联表记录,只需要一次连接即可完成,奈斯,达到我们的要求。接着继续实现其他2个接口,下面直接列出2个类的实现代码。 SqlServerResourceStore.cs using Dapper; using IdentityServer4.Dapper.Mappers; using IdentityServer4.Dapper.Options; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data.SqlClient; using System.Threading.Tasks; using System.Linq; namespace IdentityServer4.Dapper.Stores.SqlServer { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 重写资源存储方法 /// </summary> public class SqlServerResourceStore : IResourceStore { private readonly ILogger<SqlServerResourceStore> _logger; private readonly DapperStoreOptions _configurationStoreOptions; public SqlServerResourceStore(ILogger<SqlServerResourceStore> logger, DapperStoreOptions configurationStoreOptions) { _logger = logger; _configurationStoreOptions = configurationStoreOptions; } /// <summary> /// 根据api名称获取相关信息 /// </summary> /// <param name="name"></param> /// <returns></returns> public async Task<ApiResource> FindApiResourceAsync(string name) { var model = new ApiResource(); using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = @"select * from ApiResources where Name=@Name and Enabled=1; select * from ApiResources t1 inner join ApiScopes t2 on t1.Id=t2.ApiResourceId where t1.Name=@name and Enabled=1; "; var multi = await connection.QueryMultipleAsync(sql, new { name }); var ApiResources = multi.Read<Entities.ApiResource>(); var ApiScopes = multi.Read<Entities.ApiScope>(); if (ApiResources != null && ApiResources.AsList()?.Count > 0) { var apiresource = ApiResources.AsList()[0]; apiresource.Scopes = ApiScopes.AsList(); if (apiresource != null) { _logger.LogDebug("Found {api} API resource in database", name); } else { _logger.LogDebug("Did not find {api} API resource in database", name); } model = apiresource.ToModel(); } } return model; } /// <summary> /// 根据作用域信息获取接口资源 /// </summary> /// <param name="scopeNames"></param> /// <returns></returns> public async Task<IEnumerable<ApiResource>> FindApiResourcesByScopeAsync(IEnumerable<string> scopeNames) { var apiResourceData = new List<ApiResource>(); string _scopes = ""; foreach (var scope in scopeNames) { _scopes += "'" + scope + "',"; } if (_scopes == "") { return null; } else { _scopes = _scopes.Substring(0, _scopes.Length - 1); } string sql = "select distinct t1.* from ApiResources t1 inner join ApiScopes t2 on t1.Id=t2.ApiResourceId where t2.Name in(" + _scopes + ") and Enabled=1;"; using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { var apir = (await connection.QueryAsync<Entities.ApiResource>(sql))?.AsList(); if (apir != null && apir.Count > 0) { foreach (var apimodel in apir) { sql = "select * from ApiScopes where ApiResourceId=@id"; var scopedata = (await connection.QueryAsync<Entities.ApiScope>(sql, new { id = apimodel.Id }))?.AsList(); apimodel.Scopes = scopedata; apiResourceData.Add(apimodel.ToModel()); } _logger.LogDebug("Found {scopes} API scopes in database", apiResourceData.SelectMany(x => x.Scopes).Select(x => x.Name)); } } return apiResourceData; } /// <summary> /// 根据scope获取身份资源 /// </summary> /// <param name="scopeNames"></param> /// <returns></returns> public async Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeAsync(IEnumerable<string> scopeNames) { var apiResourceData = new List<IdentityResource>(); string _scopes = ""; foreach (var scope in scopeNames) { _scopes += "'" + scope + "',"; } if (_scopes == "") { return null; } else { _scopes = _scopes.Substring(0, _scopes.Length - 1); } using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { //暂不实现 IdentityClaims string sql = "select * from IdentityResources where Enabled=1 and Name in(" + _scopes + ")"; var data = (await connection.QueryAsync<Entities.IdentityResource>(sql))?.AsList(); if (data != null && data.Count > 0) { foreach (var model in data) { apiResourceData.Add(model.ToModel()); } } } return apiResourceData; } /// <summary> /// 获取所有资源实现 /// </summary> /// <returns></returns> public async Task<Resources> GetAllResourcesAsync() { var apiResourceData = new List<ApiResource>(); var identityResourceData = new List<IdentityResource>(); using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "select * from IdentityResources where Enabled=1"; var data = (await connection.QueryAsync<Entities.IdentityResource>(sql))?.AsList(); if (data != null && data.Count > 0) { foreach (var m in data) { identityResourceData.Add(m.ToModel()); } } //获取apiresource sql = "select * from ApiResources where Enabled=1"; var apidata = (await connection.QueryAsync<Entities.ApiResource>(sql))?.AsList(); if (apidata != null && apidata.Count > 0) { foreach (var m in apidata) { sql = "select * from ApiScopes where ApiResourceId=@id"; var scopedata = (await connection.QueryAsync<Entities.ApiScope>(sql, new { id = m.Id }))?.AsList(); m.Scopes = scopedata; apiResourceData.Add(m.ToModel()); } } } var model = new Resources(identityResourceData, apiResourceData); return model; } } } SqlServerPersistedGrantStore.cs using Dapper; using IdentityServer4.Dapper.Mappers; using IdentityServer4.Dapper.Options; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; using System.Threading.Tasks; namespace IdentityServer4.Dapper.Stores.SqlServer { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 重写授权信息存储 /// </summary> public class SqlServerPersistedGrantStore : IPersistedGrantStore { private readonly ILogger<SqlServerPersistedGrantStore> _logger; private readonly DapperStoreOptions _configurationStoreOptions; public SqlServerPersistedGrantStore(ILogger<SqlServerPersistedGrantStore> logger, DapperStoreOptions configurationStoreOptions) { _logger = logger; _configurationStoreOptions = configurationStoreOptions; } /// <summary> /// 根据用户标识获取所有的授权信息 /// </summary> /// <param name="subjectId">用户标识</param> /// <returns></returns> public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "select * from PersistedGrants where SubjectId=@subjectId"; var data = (await connection.QueryAsync<Entities.PersistedGrant>(sql, new { subjectId }))?.AsList(); var model = data.Select(x => x.ToModel()); _logger.LogDebug("{persistedGrantCount} persisted grants found for {subjectId}", data.Count, subjectId); return model; } } /// <summary> /// 根据key获取授权信息 /// </summary> /// <param name="key">认证信息</param> /// <returns></returns> public async Task<PersistedGrant> GetAsync(string key) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "select * from PersistedGrants where [Key]=@key"; var result = await connection.QueryFirstOrDefaultAsync<Entities.PersistedGrant>(sql, new { key }); var model = result.ToModel(); _logger.LogDebug("{persistedGrantKey} found in database: {persistedGrantKeyFound}", key, model != null); return model; } } /// <summary> /// 根据用户标识和客户端ID移除所有的授权信息 /// </summary> /// <param name="subjectId">用户标识</param> /// <param name="clientId">客户端ID</param> /// <returns></returns> public async Task RemoveAllAsync(string subjectId, string clientId) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "delete from PersistedGrants where ClientId=@clientId and SubjectId=@subjectId"; await connection.ExecuteAsync(sql, new { subjectId, clientId }); _logger.LogDebug("remove {subjectId} {clientId} from database success", subjectId, clientId); } } /// <summary> /// 移除指定的标识、客户端、类型等授权信息 /// </summary> /// <param name="subjectId">标识</param> /// <param name="clientId">客户端ID</param> /// <param name="type">授权类型</param> /// <returns></returns> public async Task RemoveAllAsync(string subjectId, string clientId, string type) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "delete from PersistedGrants where ClientId=@clientId and SubjectId=@subjectId and Type=@type"; await connection.ExecuteAsync(sql, new { subjectId, clientId }); _logger.LogDebug("remove {subjectId} {clientId} {type} from database success", subjectId, clientId, type); } } /// <summary> /// 移除指定KEY的授权信息 /// </summary> /// <param name="key"></param> /// <returns></returns> public async Task RemoveAsync(string key) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "delete from PersistedGrants where [Key]=@key"; await connection.ExecuteAsync(sql, new { key }); _logger.LogDebug("remove {key} from database success", key); } } /// <summary> /// 存储授权信息 /// </summary> /// <param name="grant">实体</param> /// <returns></returns> public async Task StoreAsync(PersistedGrant grant) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { //移除防止重复 await RemoveAsync(grant.Key); string sql = "insert into PersistedGrants([Key],ClientId,CreationTime,Data,Expiration,SubjectId,Type) values(@Key,@ClientId,@CreationTime,@Data,@Expiration,@SubjectId,@Type)"; await connection.ExecuteAsync(sql, grant); } } } } 使用dapper提取存储数据已经全部实现完,接下来我们需要实现定时清理过期的授权信息。 首先定义一个清理过期数据接口IPersistedGrants,定义如下所示。 using System; using System.Threading.Tasks; namespace IdentityServer4.Dapper.Interfaces { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 过期授权清理接口 /// </summary> public interface IPersistedGrants { /// <summary> /// 移除指定时间的过期信息 /// </summary> /// <param name="dt">过期时间</param> /// <returns></returns> Task RemoveExpireToken(DateTime dt); } } 现在我们来实现下此接口,详细代码如下。 using Dapper; using IdentityServer4.Dapper.Interfaces; using IdentityServer4.Dapper.Options; using Microsoft.Extensions.Logging; using System; using System.Data.SqlClient; using System.Threading.Tasks; namespace IdentityServer4.Dapper.Stores.SqlServer { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 实现授权信息自定义管理 /// </summary> public class SqlServerPersistedGrants : IPersistedGrants { private readonly ILogger<SqlServerPersistedGrants> _logger; private readonly DapperStoreOptions _configurationStoreOptions; public SqlServerPersistedGrants(ILogger<SqlServerPersistedGrants> logger, DapperStoreOptions configurationStoreOptions) { _logger = logger; _configurationStoreOptions = configurationStoreOptions; } /// <summary> /// 移除指定的时间过期授权信息 /// </summary> /// <param name="dt">Utc时间</param> /// <returns></returns> public async Task RemoveExpireToken(DateTime dt) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "delete from PersistedGrants where Expiration>@dt"; await connection.ExecuteAsync(sql, new { dt }); } } } } 有个清理的接口和实现,我们需要注入下实现builder.Services.AddTransient<IPersistedGrants, SqlServerPersistedGrants>();,接下来就是开启后端服务来清理过期记录。 using IdentityServer4.Dapper.Interfaces; using IdentityServer4.Dapper.Options; using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; namespace IdentityServer4.Dapper.HostedServices { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 清理过期Token方法 /// </summary> public class TokenCleanup { private readonly ILogger<TokenCleanup> _logger; private readonly DapperStoreOptions _options; private readonly IPersistedGrants _persistedGrants; private CancellationTokenSource _source; public TimeSpan CleanupInterval => TimeSpan.FromSeconds(_options.TokenCleanupInterval); public TokenCleanup(IPersistedGrants persistedGrants, ILogger<TokenCleanup> logger, DapperStoreOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); if (_options.TokenCleanupInterval < 1) throw new ArgumentException("Token cleanup interval must be at least 1 second"); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _persistedGrants = persistedGrants; } public void Start() { Start(CancellationToken.None); } public void Start(CancellationToken cancellationToken) { if (_source != null) throw new InvalidOperationException("Already started. Call Stop first."); _logger.LogDebug("Starting token cleanup"); _source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); Task.Factory.StartNew(() => StartInternal(_source.Token)); } public void Stop() { if (_source == null) throw new InvalidOperationException("Not started. Call Start first."); _logger.LogDebug("Stopping token cleanup"); _source.Cancel(); _source = null; } private async Task StartInternal(CancellationToken cancellationToken) { while (true) { if (cancellationToken.IsCancellationRequested) { _logger.LogDebug("CancellationRequested. Exiting."); break; } try { await Task.Delay(CleanupInterval, cancellationToken); } catch (TaskCanceledException) { _logger.LogDebug("TaskCanceledException. Exiting."); break; } catch (Exception ex) { _logger.LogError("Task.Delay exception: {0}. Exiting.", ex.Message); break; } if (cancellationToken.IsCancellationRequested) { _logger.LogDebug("CancellationRequested. Exiting."); break; } ClearTokens(); } } public void ClearTokens() { try { _logger.LogTrace("Querying for tokens to clear"); //提取满足条件的信息进行删除 _persistedGrants.RemoveExpireToken(DateTime.UtcNow); } catch (Exception ex) { _logger.LogError("Exception clearing tokens: {exception}", ex.Message); } } } } using IdentityServer4.Dapper.Options; using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Tasks; namespace IdentityServer4.Dapper.HostedServices { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 授权后端清理服务 /// </summary> public class TokenCleanupHost : IHostedService { private readonly TokenCleanup _tokenCleanup; private readonly DapperStoreOptions _options; public TokenCleanupHost(TokenCleanup tokenCleanup, DapperStoreOptions options) { _tokenCleanup = tokenCleanup; _options = options; } public Task StartAsync(CancellationToken cancellationToken) { if (_options.EnableTokenCleanup) { _tokenCleanup.Start(cancellationToken); } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { if (_options.EnableTokenCleanup) { _tokenCleanup.Stop(); } return Task.CompletedTask; } } } 是不是实现一个定时任务很简单呢?功能完成别忘了注入实现,现在我们使用dapper持久化的功能基本完成了。 builder.Services.AddSingleton<TokenCleanup>(); builder.Services.AddSingleton<IHostedService, TokenCleanupHost>(); 三、测试功能应用 在前面客户端授权中,我们增加dapper扩展的实现,来测试功能是否能正常使用,且使用SQL Server Profiler来监控下调用的过程。可以从之前文章中的源码TestIds4项目中,实现持久化的存储,改造注入代码如下。 services.AddIdentityServer() .AddDeveloperSigningCredential() //.AddInMemoryApiResources(Config.GetApiResources()) //.AddInMemoryClients(Config.GetClients()); .AddDapperStore(option=> { option.DbConnectionStrings = "Server=192.168.1.114;Database=mpc_identity;User ID=sa;Password=bl123456;"; }); 好了,现在可以配合网关来测试下客户端登录了,打开PostMan,启用本地的项目,然后访问之前配置的客户端授权地址,并开启SqlServer监控,查看运行代码。 访问能够得到我们预期的结果且查询全部是dapper写的Sql语句。且定期清理任务也启动成功,会根据配置的参数来执行清理过期授权信息。 四、使用Mysql存储并测试 这里Mysql重写就不一一列出来了,语句跟sqlserver几乎是完全一样,然后调用.UseMySql()即可完成mysql切换,我花了不到2分钟就完成了Mysql的所有语句和切换功能,是不是简单呢?接着测试Mysql应用,代码如下。 services.AddIdentityServer() .AddDeveloperSigningCredential() //.AddInMemoryApiResources(Config.GetApiResources()) //.AddInMemoryClients(Config.GetClients()); .AddDapperStore(option=> { option.DbConnectionStrings = "Server=*******;Database=mpc_identity;User ID=root;Password=*******;"; }).UseMySql(); 可以返回正确的结果数据,扩展Mysql实现已经完成,如果想用其他数据库实现,直接按照我写的方法扩展下即可。 五、总结及预告 本篇我介绍了如何使用Dapper来持久化Ids4信息,并介绍了实现过程,然后实现了SqlServer和Mysql两种方式,也介绍了使用过程中遇到的技术问题,其实在实现过程中我发现的一个缓存和如何让授权信息立即过期等问题,这块大家可以一起先思考下如何实现,后续文章中我会介绍具体的实现方式,然后把缓存迁移到Redis里。 下一篇开始就正式介绍Ids4的几种授权方式和具体的应用,以及如何在我们客户端进行集成,如果在学习过程中遇到不懂或未理解的问题,欢迎大家加入QQ群聊637326624与作者联系吧。
原文:SQLServer中查询的数字列前面补0返回指定长度的字符串 SQLServer中查询的数字列前面补0返回指定长度的字符串: 如: 角本如下: /****** Script for SelectTopNRows command from SSMS ******/ SELECT TOP 1000 [ID] ,[SN] ,[Name] FROM [EduDB].[dbo].[TestTab] select Right('0123456',SN) from TestTab; select RIGHT(REPLICATE('0',5)+CAST(SN AS varchar(10)),5) AS 'SN' from TestTab; select RIGHT('00000000'+CAST(SN as varchar(10)),5) as 'sn' from TestTab 效果如下:
原文:sql-----STR 函数 sql-----STR 函数 STR 函数由数字数据转换来的字符数据。 语法 STR ( float_expression [ , length [ , decimal ] ] ) 参数 float_expression是带小数点的近似数字 (float) 数据类型的表达式。不要在 STR 函数中将函数或子查询用作 float_expression。 length是总长度,包括小数点、符号、数字或空格。默认值为 10。 decimal是小数点右边的位数。 返回类型char 注释 如果为 STR 提供 length 和 decimal 参数值,则这些值应该是正数。在默认情况下或者小数参数为 0 时,数字四舍五入为整数。指定长度应该大于或等于小数点前面的数字加上数字符号(若有)的长度。短的 float_expression 在指定长度内右对齐,长的 float_expression 则截断为指定的小数位数。 例如,STR(12,10) 输出的结果是 12,在结果集内右对齐。而 STR(1223, 2) 则将结果集截断为 **。可以嵌套字符串函数。
原文:SQL常用函数之五 str() 使用str函数 :STR 函数由数字数据转换来的字符数据。 语法 STR ( float_expression [ , length [ , decimal ] ] ) 参数 float_expression 是带小数点的近似数字 (float) 数据类型的表达式。不要在 STR 函数中将函数或子查询用作 float_expression。 length 是总长度,包括小数点、符号、数字或空格。默认值为 10。 decimal 是小数点右边的位数。 返回类型 char 注释 如果为 STR 提供 length 和 decimal 参数值,则这些值应该是正数。在默认情况下或者小数参数为 0 时,数字四舍五入为整数。指定长度应该大于或等于小数点前面的数字加上数字符号(若有)的长度。短的 float_expression 在指定长度内右对齐,长的 float_expression 则截断为指定的小数位数。例如,STR(12,10) 输出的结果是 12,在结果集内右对齐。而 STR(1223, 2) 则将结果集截断为 **。可以嵌套字符串函数。 select str(123.46,8,1) 结果为 ###123.5 其中#代表空格 该结果总长度为8 ,取一位小数,小数为四舍五入,同时该SQL语句如果取3个小数的话补零操作。 说明 若要转换为 Unicode 数据,请在 CONVERT 或 CAST 转换函数内使用 STR。
原文:mysql下sql语句 update 字段=字段+字符串 mysql下sql语句令某字段值等于原值加上一个字符串 update 表明 SET 字段= 'feifei' || 字段; (postgreSQL 用 || 来连贯字符串) MySQL连贯字符串不能利用加号(+),而利用concat。 比方在aa表的name字段前加字符'x',利用: update aa set name=concat('x',name)
原文:【UWP】使用 Rx 改善 AutoSuggestBox 在 UWP 中,有一个控件叫 AutoSuggestBox,它的主要成分是一个 TextBox 和 ComboBox。使用它,我们可以做一些根据用户输入来显示相关建议输入的功能,例如百度首页搜索框那种效果: 在看这篇文章之前,我建议先看看老周写的这一篇:https://www.cnblogs.com/tcjiaan/p/4967031.html ,先对 AutoSuggestBox 有一个大体的印象,不然下面干什么都不知道了。 接下来开始我们的实验,先准备好百度的接口(这个可以用浏览器的开发者工具抓出来): public class BaiduService { static BaiduService() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } public async Task<IReadOnlyList<string>> GetSuggestionsAsync(string query) { using (var client = new HttpClient()) { var url = $"http://www.baidu.com/su?wd={HttpUtility.UrlEncode(query)}"; var str = await client.GetStringAsync(url); str = str.Substring(str.IndexOf('{')); str = str.Substring(0, str.LastIndexOf('}') + 1); var jObject = JObject.Parse(str); return jObject["s"].ToObject<string[]>(); } } } 需要引用一下 Newtonsoft.Json 这个包。 静态构造函数里我注册了一下本机的 Encoding,不然会报错(百度这厮用的是 gbk,而不是常见的 utf-8)。 然后开始编写 Demo 页面 XAML <Grid> <Grid Margin="20"> <StackPanel Orientation="Vertical"> <AutoSuggestBox x:Name="AutoSuggestBox" TextChanged="AutoSuggestBox_TextChanged" /> </StackPanel> </Grid> </Grid> 这里随便写了下,反正就是弄了个 AutoSuggestBox,订阅了一下它的 TextChanged 事件。 cs代码: private async void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { switch (args.Reason) { case AutoSuggestionBoxTextChangeReason.ProgrammaticChange: case AutoSuggestionBoxTextChangeReason.SuggestionChosen: sender.ItemsSource = null; return; } // User input var query = sender.Text; Debug.WriteLine("get suggestion: " + query); var suggestions = await _baiduService.GetSuggestionsAsync(query); sender.ItemsSource = suggestions; } 触发的事件参数中有个 Reason 属性,表面该次事件触发的原因。 在这里我如果是程序代码修改或者用户选择了建议项的话,那么就清除建议项列表。否则就去问百度要一下建议(顺便输出一下,说明触发了)。 然后就把我们的 Demo 程序跑起来吧。 看上去工作得还是蛮正常的嘛。 但是,在这里我要告诉你,这样写,是有一些坑的! 1、 全选,复制,再粘贴,我们的文字内容是没有变化才对的,然而也触发了一次请求。 2、 如果我的内容为空,那么就不应该请求才对的。 3、 在上面的图中,我 UWP 这三个字母的输入速度应该是比较快的,那么 U 那一次就不应该去请求才对。应该以停止输入一段时间后,才去进行请求。AutoSuggestBox 控件应该是做了(不然在 UW 时也应该会触发才对),但目测时间非常短(可能就 0.1 秒),而且也没有相关的属性能够控制这个时长。 4、 因为这个请求是一个异步的网络请求,所以说不好的话,后发起的请求有可能先返回。按上面的代码逻辑来说,这样输入和建议项就对不上了。 按传统思路,第 1 点我们可以在请求前加个判断,如果跟上一次相同就不请求。第 2 点加个空字符串判断即可。第 3 点就麻烦了,真要实现我们得加个计时之类的方法来做。第 4 点也是很麻烦,我目前想到的是发起请求时给个 token 之类,接收到的时候再对比是否是最新的 token。 但说实话,这么一整套下来,不麻烦么?而且代码量不是一点两点。 在这里,我要安利各位,只要你使用 Rx,解决这点小问题完全不在话下。 Rx 的全称是 Reactive Extensions,是一种针对异步编程的编程模型。Rx 不仅仅在 .Net 下有实现,在 JavaScript、Java 等等平台都有相关的实现。 概念说完了,继续实验。 引用 Rx 的 nuget 包,System.Reactive。 在页面的构造函数先编写如下的代码: var changed = Observable.FromEventPattern<TypedEventHandler<AutoSuggestBox, AutoSuggestBoxTextChangedEventArgs>, AutoSuggestBox, AutoSuggestBoxTextChangedEventArgs>( handler => AutoSuggestBox.TextChanged += handler, handler => AutoSuggestBox.TextChanged -= handler); 这段代码以 AutoSuggestBox 的 TextChanged 事件创建一个可监听的数据源 changed 对象。 接下来,我们处理第 1 点,需要忽略掉相同的文本内容。 var input = changed .DistinctUntilChanged(temp => temp.Sender.Text); DistinctUntilChanged 这个扩展方法是 Rx 提供的,如果数据源内容不变,则不会触发。 然后我们处理第 3 点,只有停止输入一段时间后,我们再去发起请求。 var input = changed .DistinctUntilChanged(temp => temp.Sender.Text) .Throttle(TimeSpan.FromSeconds(1)); 这个也很简单,Rx 提供了 Throttle 方法,传入需要的时间就可以了,这里我设定成停止输入 1 秒后才触发。 然后接下来我们要区分两种情况,一个是用户输入的,另一个是非用户输入的。 var notUserInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason != AutoSuggestionBoxTextChangeReason.UserInput); var userInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason == AutoSuggestionBoxTextChangeReason.UserInput) .Where(temp => !string.IsNullOrEmpty(temp.Sender.Text)); 在用户输入的时候,输入后文本框非空我们才触发(第 2 点)。 这里注意到还有 ObserveOnDispatcher 这个方法的调用,这个调用就是说,接下来我的操作需要在当前线程上进行。Rx 默认是会在另一个线程上的,在 Where 方法中我们引用到了 AutoSuggestBox 控件,所以需要调用到该方法。 接下来我们处理一下 userInput,有了输入,我们自然需要输出,输出就是建议项: var userInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason == AutoSuggestionBoxTextChangeReason.UserInput) .Where(temp => !string.IsNullOrEmpty(temp.Sender.Text)) .Select(temp => _baiduService.GetSuggestionsAsync(temp.Sender.Text)); 调用百度接口,返回 Task<IReadOnlyList<string>>。同时,我们对 notUserInput 也处理一下,返回 null,但类型也是 Task<IReadOnlyList<string>>。 var notUserInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason != AutoSuggestionBoxTextChangeReason.UserInput) .Select(temp => Task.FromResult<IReadOnlyList<string>>(null)); 现在,我们把这两个重新合成为一个,因为我们数据源触发的条件是 TextChanged,而不是因为上面这一大堆东西才进行触发。 var merge = Observable .Merge(notUserInput, userInput); 最后,我们可以监听这个数据源了,调用 Subscribe 方法(当然还要再 ObserveOnDispatcher 一次): merge .ObserveOnDispatcher() .Subscribe(suggestions => { AutoSuggestBox.ItemsSource = suggestions; }); 这样更新上去我们的 AutoSuggestBox 就行了。 慢着,我们的第 4 点还没处理呢。这个只需要稍微修改一下就可以了(Rx 真方便)。 var merge = Observable .Merge(notUserInput, userInput) .Switch(); Switch 方法会将输出的顺序按照输入的顺序来排序,这样之后,我们的第 4 点就能解决掉了。 最终下来,我们解决这么一系列问题只是写了这么点的代码,如果按传统的写法嘛,那不知道写到什么时候去了。Rx 万岁! 虽然 Rx 学习起来难度曲线非常大,但是在解决某些场景,Rx 是非常的有效的。(顺带一提,Angular 就集成了 RxJS,可见 Rx 存在其优势) 参考资料: DevCamp 2010 Keynote - Rx: Curing your asynchronous programming blues
原文:WPF TextBox 正则验证 大于等于0 小于等于1 的两位小数 正则:^(0\.\d+|[1-9][0-9]|1)$ TextBox绑定正则验证 <TextBox x:Name="txb" MaxLength="6" Margin="1 0 0 0" Width="40" > <TextBox.Text> <Binding Path="Opacity" ValidatesOnExceptions="True" ValidatesOnDataErrors="True" StringFormat="F2" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" > <Binding.ValidationRules> <shared1:InventoryValidationRule InventoryPattern="^(0\.\d+|[1-9][0-9]|1)$"/> </Binding.ValidationRules> </Binding> </TextBox.Text></TextBox> 用到的InventoryValidationRule类: public class InventoryValidationRule : ValidationRule { #region Properties public string InventoryPattern { get; set; } #endregion Properties #region Methods public override ValidationResult Validate( object value, CultureInfo cultureInfo) { if (InventoryPattern == null) return ValidationResult.ValidResult; if (!(value is string)) return new ValidationResult(false, "Inventory should be a comma separated list of model numbers as a string"); string[] pieces = value.ToString().Split(','); Regex m_RegEx = new Regex(InventoryPattern); foreach (string item in pieces) { Match match = m_RegEx.Match(item); if (match == null || match == Match.Empty) return new ValidationResult( false, "Invalid input format"); } return ValidationResult.ValidResult; } #endregion Methods }
原文:webapi的返回类型,webapi返回图片 1.0 首先是返回常用的系统类型,当然这些返回方式不常用到。如:int,string,list,array等。这些类型直接返回即可。 1 public List<string> Get() 2 { 3 List<string> list = new List<string>() { "11","22","33"}; 4 return list; 5 } 1.1 用不同的浏览器测试发现,返回的类型竟然是不一样的。如用ie,edge返回的是json,而用chrome,firefox返回的是xml类型。后来才知道原来WebApi的返回值类型是根据客户端的请求报文头的类型而确定的。IE在发生http请求时请求头accpet节点相比Firefox和Chrome缺少"application/xml"类型,由于WebAPI返回数据为xml或json格式,IE没有发送可接受xml和json类型,所以默认为json格式数据,而Firefox和chrome则发送了可接受xml类型。请参考:http://www.cnblogs.com/lzrabbit/archive/2013/03/19/2948522.html 2.0 返回json类型数据。这也是最常用的方式。 public HttpResponseMessage Get() { var jsonStr = "{\"code\":0,\"data\":\"abc\"}"; var result = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(jsonStr, Encoding.UTF8, "text/json") }; return result; } 3.0 返回流类型数据,如:图片类型。 public HttpResponseMessage Get() { var imgPath = System.Web.Hosting.HostingEnvironment.MapPath("~/111.jpg"); //从图片中读取byte var imgByte = File.ReadAllBytes(imgPath); //从图片中读取流 var imgStream = new MemoryStream(File.ReadAllBytes(imgPath)); var resp = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(imgStream) //或者 //Content = new ByteArrayContent(imgByte) }; resp.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpg"); return resp; }
原文:SQL SERVER-时间戳(timestamp)与时间格式(datetime)互相转换 SQL里面有个DATEADD的函数。时间戳就是一个从1970-01-01 08:00:00到时间的相隔的秒数。所以只要把这个时间戳加上1970-01-01 08:00:00这个时间就可以得到你想要的时间了select DATEADD(second,1268738429 + 8 * 60 * 60,'1970-01-01 00:00:00')注解:北京时间与GMT时间关系 1.GMT是中央时区,北京在东8区,相差8个小时 2.所以北京时间 = GMT时间 + 八小时例如: SELECT DATEADD(S,1160701488,'1970-01-01 08:00:00') --时间戳转换成普通时间 SELECT DATEDIFF(S,'1970-01-01 08:00:00', GETDATE()) --普通时间转换成时间戳
原文:【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析 【.NET Core项目实战-统一认证平台】开篇及目录索引 上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于IdentityServer4(后面简称Ids4)的认证相关知识,在具体介绍ids4实现我们统一认证的相关功能前,我们首先需要分析下Ids4源码,便于我们彻底掌握认证的原理以及后续的扩展需求。 .netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。 一、Ids4文档及源码 文档地址 http://docs.identityserver.io/en/latest/ Github源码地址 https://github.com/IdentityServer/IdentityServer4 二、源码整体分析 【工欲善其事,必先利其器,器欲尽其能,必先得其法】 在我们使用Ids4前我们需要了解它的运行原理和实现方式,这样实际生产环境中才能安心使用,即使遇到问题也可以很快解决,如需要对认证进行扩展,也可自行编码实现。 源码分析第一步就是要找到Ids4的中间件是如何运行的,所以需要定位到中间价应用位置app.UseIdentityServer();,查看到详细的代码如下。 /// <summary> /// Adds IdentityServer to the pipeline. /// </summary> /// <param name="app">The application.</param> /// <returns></returns> public static IApplicationBuilder UseIdentityServer(this IApplicationBuilder app) { //1、验证配置信息 app.Validate(); //2、应用BaseUrl中间件 app.UseMiddleware<BaseUrlMiddleware>(); //3、应用跨域访问配置 app.ConfigureCors(); //4、启用系统认证功能 app.UseAuthentication(); //5、应用ids4中间件 app.UseMiddleware<IdentityServerMiddleware>(); return app; } 通过上面的源码,我们知道整体流程分为这5步实现。接着我们分析下每一步都做了哪些操作呢? 1、app.Validate()为我们做了哪些工作? 校验IPersistedGrantStore、IClientStore、IResourceStore是否已经注入? 验证IdentityServerOptions配置信息是否都配置完整 输出调试相关信息提醒 internal static void Validate(this IApplicationBuilder app) { var loggerFactory = app.ApplicationServices.GetService(typeof(ILoggerFactory)) as ILoggerFactory; if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); var logger = loggerFactory.CreateLogger("IdentityServer4.Startup"); var scopeFactory = app.ApplicationServices.GetService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var serviceProvider = scope.ServiceProvider; TestService(serviceProvider, typeof(IPersistedGrantStore), logger, "No storage mechanism for grants specified. Use the 'AddInMemoryPersistedGrants' extension method to register a development version."); TestService(serviceProvider, typeof(IClientStore), logger, "No storage mechanism for clients specified. Use the 'AddInMemoryClients' extension method to register a development version."); TestService(serviceProvider, typeof(IResourceStore), logger, "No storage mechanism for resources specified. Use the 'AddInMemoryIdentityResources' or 'AddInMemoryApiResources' extension method to register a development version."); var persistedGrants = serviceProvider.GetService(typeof(IPersistedGrantStore)); if (persistedGrants.GetType().FullName == typeof(InMemoryPersistedGrantStore).FullName) { logger.LogInformation("You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch to a different store implementation."); } var options = serviceProvider.GetRequiredService<IdentityServerOptions>(); ValidateOptions(options, logger); ValidateAsync(serviceProvider, logger).GetAwaiter().GetResult(); } } private static async Task ValidateAsync(IServiceProvider services, ILogger logger) { var options = services.GetRequiredService<IdentityServerOptions>(); var schemes = services.GetRequiredService<IAuthenticationSchemeProvider>(); if (await schemes.GetDefaultAuthenticateSchemeAsync() == null && options.Authentication.CookieAuthenticationScheme == null) { logger.LogWarning("No authentication scheme has been set. Setting either a default authentication scheme or a CookieAuthenticationScheme on IdentityServerOptions is required."); } else { if (options.Authentication.CookieAuthenticationScheme != null) { logger.LogInformation("Using explicitly configured scheme {scheme} for IdentityServer", options.Authentication.CookieAuthenticationScheme); } logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for authentication", (await schemes.GetDefaultAuthenticateSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for sign-in", (await schemes.GetDefaultSignInSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for sign-out", (await schemes.GetDefaultSignOutSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for challenge", (await schemes.GetDefaultChallengeSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for forbid", (await schemes.GetDefaultForbidSchemeAsync())?.Name); } } private static void ValidateOptions(IdentityServerOptions options, ILogger logger) { if (options.IssuerUri.IsPresent()) logger.LogDebug("Custom IssuerUri set to {0}", options.IssuerUri); if (options.PublicOrigin.IsPresent()) { if (!Uri.TryCreate(options.PublicOrigin, UriKind.Absolute, out var uri)) { throw new InvalidOperationException($"PublicOrigin is not valid: {options.PublicOrigin}"); } logger.LogDebug("PublicOrigin explicitly set to {0}", options.PublicOrigin); } // todo: perhaps different logging messages? //if (options.UserInteraction.LoginUrl.IsMissing()) throw new InvalidOperationException("LoginUrl is not configured"); //if (options.UserInteraction.LoginReturnUrlParameter.IsMissing()) throw new InvalidOperationException("LoginReturnUrlParameter is not configured"); //if (options.UserInteraction.LogoutUrl.IsMissing()) throw new InvalidOperationException("LogoutUrl is not configured"); if (options.UserInteraction.LogoutIdParameter.IsMissing()) throw new InvalidOperationException("LogoutIdParameter is not configured"); if (options.UserInteraction.ErrorUrl.IsMissing()) throw new InvalidOperationException("ErrorUrl is not configured"); if (options.UserInteraction.ErrorIdParameter.IsMissing()) throw new InvalidOperationException("ErrorIdParameter is not configured"); if (options.UserInteraction.ConsentUrl.IsMissing()) throw new InvalidOperationException("ConsentUrl is not configured"); if (options.UserInteraction.ConsentReturnUrlParameter.IsMissing()) throw new InvalidOperationException("ConsentReturnUrlParameter is not configured"); if (options.UserInteraction.CustomRedirectReturnUrlParameter.IsMissing()) throw new InvalidOperationException("CustomRedirectReturnUrlParameter is not configured"); if (options.Authentication.CheckSessionCookieName.IsMissing()) throw new InvalidOperationException("CheckSessionCookieName is not configured"); if (options.Cors.CorsPolicyName.IsMissing()) throw new InvalidOperationException("CorsPolicyName is not configured"); } internal static object TestService(IServiceProvider serviceProvider, Type service, ILogger logger, string message = null, bool doThrow = true) { var appService = serviceProvider.GetService(service); if (appService == null) { var error = message ?? $"Required service {service.FullName} is not registered in the DI container. Aborting startup"; logger.LogCritical(error); if (doThrow) { throw new InvalidOperationException(error); } } return appService; } 详细的实现代码如上所以,非常清晰明了,这时候有人肯定会问这些相关的信息时从哪来的呢?这块我们会在后面讲解。 2、BaseUrlMiddleware中间件实现了什么功能? 源码如下,就是从配置信息里校验是否设置了PublicOrigin原始实例地址,如果设置了修改下请求的Scheme和Host,最后设置IdentityServerBasePath地址信息,然后把请求转到下一个路由。 namespace IdentityServer4.Hosting { public class BaseUrlMiddleware { private readonly RequestDelegate _next; private readonly IdentityServerOptions _options; public BaseUrlMiddleware(RequestDelegate next, IdentityServerOptions options) { _next = next; _options = options; } public async Task Invoke(HttpContext context) { var request = context.Request; if (_options.PublicOrigin.IsPresent()) { context.SetIdentityServerOrigin(_options.PublicOrigin); } context.SetIdentityServerBasePath(request.PathBase.Value.RemoveTrailingSlash()); await _next(context); } } } 这里源码非常简单,就是设置了后期要处理的一些关于请求地址信息。那这个中间件有什么作用呢? 就是设置认证的通用地址,当我们访问认证服务配置地址http://localhost:5000/.well-known/openid-configuration的时候您会发现,您设置的PublicOrigin会自定应用到所有的配置信息前缀,比如设置option.PublicOrigin = "http://www.baidu.com";,显示的json代码如下。 {"issuer":"http://www.baidu.com","jwks_uri":"http://www.baidu.com/.well-known/openid-configuration/jwks","authorization_endpoint":"http://www.baidu.com/connect/authorize","token_endpoint":"http://www.baidu.com/connect/token","userinfo_endpoint":"http://www.baidu.com/connect/userinfo","end_session_endpoint":"http://www.baidu.com/connect/endsession","check_session_iframe":"http://www.baidu.com/connect/checksession","revocation_endpoint":"http://www.baidu.com/connect/revocation","introspection_endpoint":"http://www.baidu.com/connect/introspect","frontchannel_logout_supported":true,"frontchannel_logout_session_supported":true,"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"scopes_supported":["api1","offline_access"],"claims_supported":[],"grant_types_supported":["authorization_code","client_credentials","refresh_token","implicit"],"response_types_supported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"response_modes_supported":["form_post","query","fragment"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"code_challenge_methods_supported":["plain","S256"]} 可能还有些朋友觉得奇怪,这有什么用啊?其实不然,试想下如果您部署的认证服务器是由多台组成,那么可以设置这个地址为负载均衡地址,这样访问每台认证服务器的配置信息,返回的负载均衡的地址,而负载均衡真正路由到的地址是内网地址,每一个实例内网地址都不一样,这样就可以负载生效,后续的文章会介绍配合Consul实现自动的服务发现和注册,达到动态扩展认证节点功能。 可能表述的不太清楚,可以先试着理解下,因为后续篇幅有介绍负载均衡案例会讲到实际应用。 3、app.ConfigureCors(); 做了什么操作? 其实这个从字面意思就可以看出来,是配置跨域访问的中间件,源码就是应用配置的跨域策略。 namespace IdentityServer4.Hosting { public static class CorsMiddlewareExtensions { public static void ConfigureCors(this IApplicationBuilder app) { var options = app.ApplicationServices.GetRequiredService<IdentityServerOptions>(); app.UseCors(options.Cors.CorsPolicyName); } } } 很简单吧,至于什么是跨域,可自行查阅相关文档,由于篇幅有效,这里不详细解释。 4、app.UseAuthentication();做了什么操作? 就是启用了默认的认证中间件,然后在相关的控制器增加[Authorize]属性标记即可完成认证操作,由于本篇是介绍的Ids4的源码,所以关于非Ids4部分后续有需求再详细介绍实现原理。 5、IdentityServerMiddleware中间件做了什么操作? 这也是Ids4的核心中间件,通过源码分析,哎呀!好简单啊,我要一口气写100个牛逼中间件。哈哈,我当时也是这么想的,难道真的这么简单吗?接着往下分析,让我们彻底明白Ids4是怎么运行的。 namespace IdentityServer4.Hosting { /// <summary> /// IdentityServer middleware /// </summary> public class IdentityServerMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="IdentityServerMiddleware"/> class. /// </summary> /// <param name="next">The next.</param> /// <param name="logger">The logger.</param> public IdentityServerMiddleware(RequestDelegate next, ILogger<IdentityServerMiddleware> logger) { _next = next; _logger = logger; } /// <summary> /// Invokes the middleware. /// </summary> /// <param name="context">The context.</param> /// <param name="router">The router.</param> /// <param name="session">The user session.</param> /// <param name="events">The event service.</param> /// <returns></returns> public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events) { // this will check the authentication session and from it emit the check session // cookie needed from JS-based signout clients. await session.EnsureSessionIdCookieAsync(); try { var endpoint = router.Find(context); if (endpoint != null) { _logger.LogInformation("Invoking IdentityServer endpoint: {endpointType} for {url}", endpoint.GetType().FullName, context.Request.Path.ToString()); var result = await endpoint.ProcessAsync(context); if (result != null) { _logger.LogTrace("Invoking result: {type}", result.GetType().FullName); await result.ExecuteAsync(context); } return; } } catch (Exception ex) { await events.RaiseAsync(new UnhandledExceptionEvent(ex)); _logger.LogCritical(ex, "Unhandled exception: {exception}", ex.Message); throw; } await _next(context); } } } 第一步从本地提取授权记录,就是如果之前授权过,直接提取授权到请求上下文。说起来是一句话,但是实现起来还是比较多步骤的,我简单描述下整个流程如下。 执行授权 如果发现本地未授权时,获取对应的授权处理器,然后执行授权,看是否授权成功,如果授权成功,赋值相关的信息,常见的应用就是自动登录的实现。 比如用户U访问A系统信息,自动跳转到S认证系统进行认证,认证后调回A系统正常访问,这时候如果用户U访问B系统(B系统也是S统一认证的),B系统会自动跳转到S认证系统进行认证,比如跳转到/login页面,这时候通过检测发现用户U已经经过认证,可以直接提取认证的所有信息,然后跳转到系统B,实现了自动登录过程。 private async Task AuthenticateAsync() { if (Principal == null || Properties == null) { var scheme = await GetCookieSchemeAsync(); //根据请求上下人和认证方案获取授权处理器 var handler = await Handlers.GetHandlerAsync(HttpContext, scheme); if (handler == null) { throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}"); } //执行对应的授权操作 var result = await handler.AuthenticateAsync(); if (result != null && result.Succeeded) { Principal = result.Principal; Properties = result.Properties; } } } 获取路由处理器 其实这个功能就是拦截请求,获取对应的请求的处理器,那它是如何实现的呢? IEndpointRouter是这个接口专门负责处理的,那这个方法的实现方式是什么呢?可以右键-转到实现,我们可以找到EndpointRouter方法,详细代码如下。 namespace IdentityServer4.Hosting { internal class EndpointRouter : IEndpointRouter { private readonly IEnumerable<Endpoint> _endpoints; private readonly IdentityServerOptions _options; private readonly ILogger _logger; public EndpointRouter(IEnumerable<Endpoint> endpoints, IdentityServerOptions options, ILogger<EndpointRouter> logger) { _endpoints = endpoints; _options = options; _logger = logger; } public IEndpointHandler Find(HttpContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); //遍历所有的路由和请求处理器,如果匹配上,返回对应的处理器,否则返回null foreach(var endpoint in _endpoints) { var path = endpoint.Path; if (context.Request.Path.Equals(path, StringComparison.OrdinalIgnoreCase)) { var endpointName = endpoint.Name; _logger.LogDebug("Request path {path} matched to endpoint type {endpoint}", context.Request.Path, endpointName); return GetEndpointHandler(endpoint, context); } } _logger.LogTrace("No endpoint entry found for request path: {path}", context.Request.Path); return null; } //根据判断配置文件是否开启了路由拦截功能,如果存在提取对应的处理器。 private IEndpointHandler GetEndpointHandler(Endpoint endpoint, HttpContext context) { if (_options.Endpoints.IsEndpointEnabled(endpoint)) { var handler = context.RequestServices.GetService(endpoint.Handler) as IEndpointHandler; if (handler != null) { _logger.LogDebug("Endpoint enabled: {endpoint}, successfully created handler: {endpointHandler}", endpoint.Name, endpoint.Handler.FullName); return handler; } else { _logger.LogDebug("Endpoint enabled: {endpoint}, failed to create handler: {endpointHandler}", endpoint.Name, endpoint.Handler.FullName); } } else { _logger.LogWarning("Endpoint disabled: {endpoint}", endpoint.Name); } return null; } } } 源码功能我做了简单的讲解,发现就是提取对应路由处理器,然后转换成IEndpointHandler接口,所有的处理器都会实现这个接口。但是IEnumerable<Endpoint>记录是从哪里来的呢?而且为什么可以获取到指定的处理器,可以查看如下代码,原来都注入到默认的路由处理方法里。 /// <summary> /// Adds the default endpoints. /// </summary> /// <param name="builder">The builder.</param> /// <returns></returns> public static IIdentityServerBuilder AddDefaultEndpoints(this IIdentityServerBuilder builder) { builder.Services.AddTransient<IEndpointRouter, EndpointRouter>(); builder.AddEndpoint<AuthorizeCallbackEndpoint>(EndpointNames.Authorize, ProtocolRoutePaths.AuthorizeCallback.EnsureLeadingSlash()); builder.AddEndpoint<AuthorizeEndpoint>(EndpointNames.Authorize, ProtocolRoutePaths.Authorize.EnsureLeadingSlash()); builder.AddEndpoint<CheckSessionEndpoint>(EndpointNames.CheckSession, ProtocolRoutePaths.CheckSession.EnsureLeadingSlash()); builder.AddEndpoint<DiscoveryKeyEndpoint>(EndpointNames.Discovery, ProtocolRoutePaths.DiscoveryWebKeys.EnsureLeadingSlash()); builder.AddEndpoint<DiscoveryEndpoint>(EndpointNames.Discovery, ProtocolRoutePaths.DiscoveryConfiguration.EnsureLeadingSlash()); builder.AddEndpoint<EndSessionCallbackEndpoint>(EndpointNames.EndSession, ProtocolRoutePaths.EndSessionCallback.EnsureLeadingSlash()); builder.AddEndpoint<EndSessionEndpoint>(EndpointNames.EndSession, ProtocolRoutePaths.EndSession.EnsureLeadingSlash()); builder.AddEndpoint<IntrospectionEndpoint>(EndpointNames.Introspection, ProtocolRoutePaths.Introspection.EnsureLeadingSlash()); builder.AddEndpoint<TokenRevocationEndpoint>(EndpointNames.Revocation, ProtocolRoutePaths.Revocation.EnsureLeadingSlash()); builder.AddEndpoint<TokenEndpoint>(EndpointNames.Token, ProtocolRoutePaths.Token.EnsureLeadingSlash()); builder.AddEndpoint<UserInfoEndpoint>(EndpointNames.UserInfo, ProtocolRoutePaths.UserInfo.EnsureLeadingSlash()); return builder; } /// <summary> /// Adds the endpoint. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="builder">The builder.</param> /// <param name="name">The name.</param> /// <param name="path">The path.</param> /// <returns></returns> public static IIdentityServerBuilder AddEndpoint<T>(this IIdentityServerBuilder builder, string name, PathString path) where T : class, IEndpointHandler { builder.Services.AddTransient<T>(); builder.Services.AddSingleton(new Endpoint(name, path, typeof(T))); return builder; } 通过现在分析,我们知道了路由查找方法的原理了,以后我们想增加自定义的拦截器也知道从哪里下手了。 执行路由过程并返回结果 有了这些基础知识后,就可以很好的理解var result = await endpoint.ProcessAsync(context);这句话了,其实业务逻辑还是在自己的处理器里,但是可以通过调用接口方法实现,是不是非常优雅呢? 为了更进一步理解,我们就上面列出的路由发现地址(http://localhost:5000/.well-known/openid-configuration)为例,讲解下运行过程。通过注入方法可以发现,路由发现的处理器如下所示。 builder.AddEndpoint<DiscoveryEndpoint>(EndpointNames.Discovery, ProtocolRoutePaths.DiscoveryConfiguration.EnsureLeadingSlash()); //协议默认路由地址 public static class ProtocolRoutePaths { public const string Authorize = "connect/authorize"; public const string AuthorizeCallback = Authorize + "/callback"; public const string DiscoveryConfiguration = ".well-known/openid-configuration"; public const string DiscoveryWebKeys = DiscoveryConfiguration + "/jwks"; public const string Token = "connect/token"; public const string Revocation = "connect/revocation"; public const string UserInfo = "connect/userinfo"; public const string Introspection = "connect/introspect"; public const string EndSession = "connect/endsession"; public const string EndSessionCallback = EndSession + "/callback"; public const string CheckSession = "connect/checksession"; public static readonly string[] CorsPaths = { DiscoveryConfiguration, DiscoveryWebKeys, Token, UserInfo, Revocation }; } 可以请求的地址会被拦截,然后进行处理。 它的详细代码如下,跟分析的一样是实现了IEndpointHandler接口。 using System.Net; using System.Threading.Tasks; using IdentityServer4.Configuration; using IdentityServer4.Endpoints.Results; using IdentityServer4.Extensions; using IdentityServer4.Hosting; using IdentityServer4.ResponseHandling; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace IdentityServer4.Endpoints { internal class DiscoveryEndpoint : IEndpointHandler { private readonly ILogger _logger; private readonly IdentityServerOptions _options; private readonly IDiscoveryResponseGenerator _responseGenerator; public DiscoveryEndpoint( IdentityServerOptions options, IDiscoveryResponseGenerator responseGenerator, ILogger<DiscoveryEndpoint> logger) { _logger = logger; _options = options; _responseGenerator = responseGenerator; } public async Task<IEndpointResult> ProcessAsync(HttpContext context) { _logger.LogTrace("Processing discovery request."); // 1、验证请求是否为Get方法 if (!HttpMethods.IsGet(context.Request.Method)) { _logger.LogWarning("Discovery endpoint only supports GET requests"); return new StatusCodeResult(HttpStatusCode.MethodNotAllowed); } _logger.LogDebug("Start discovery request"); //2、判断是否开启了路由发现功能 if (!_options.Endpoints.EnableDiscoveryEndpoint) { _logger.LogInformation("Discovery endpoint disabled. 404."); return new StatusCodeResult(HttpStatusCode.NotFound); } var baseUrl = context.GetIdentityServerBaseUrl().EnsureTrailingSlash(); var issuerUri = context.GetIdentityServerIssuerUri(); _logger.LogTrace("Calling into discovery response generator: {type}", _responseGenerator.GetType().FullName); // 3、生成路由相关的输出信息 var response = await _responseGenerator.CreateDiscoveryDocumentAsync(baseUrl, issuerUri); //5、返回路由发现的结果信息 return new DiscoveryDocumentResult(response, _options.Discovery.ResponseCacheInterval); } } } 通过上面代码说明,可以发现通过4步完成了整个解析过程,然后输出最终结果,终止管道继续往下进行。 if (result != null) { _logger.LogTrace("Invoking result: {type}", result.GetType().FullName); await result.ExecuteAsync(context); } return; 路由发现的具体实现代码如下,就是把结果转换成Json格式输出,然后就得到了我们想要的结果。 /// <summary> /// Executes the result. /// </summary> /// <param name="context">The HTTP context.</param> /// <returns></returns> public Task ExecuteAsync(HttpContext context) { if (MaxAge.HasValue && MaxAge.Value >= 0) { context.Response.SetCache(MaxAge.Value); } return context.Response.WriteJsonAsync(ObjectSerializer.ToJObject(Entries)); } 到此完整的路由发现功能及实现了,其实这个实现比较简单,因为没有涉及太多其他关联的东西,像获取Token和就相对复杂一点,然后分析方式一样。 6、继续运行下一个中间件 有了上面的分析,我们可以知道整个授权的流程,所有在我们使用Ids4时需要注意中间件的执行顺序,针对需要授权后才能继续操作的中间件需要放到Ids4中间件后面。 三、获取Token执行分析 为什么把这块单独列出来呢?因为后续很多扩展和应用都是基础Token获取的流程,所以有必要单独把这块拿出来进行讲解。有了前面整体的分析,现在应该直接这块源码是从哪里看了,没错就是下面这句。 builder.AddEndpoint<TokenEndpoint>(EndpointNames.Token, ProtocolRoutePaths.Token.EnsureLeadingSlash()); 他的执行过程是TokenEndpoint,所以我们重点来分析下这个是怎么实现这么复杂的获取Token过程的,首先放源码。 // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using IdentityModel; using IdentityServer4.Endpoints.Results; using IdentityServer4.Events; using IdentityServer4.Extensions; using IdentityServer4.Hosting; using IdentityServer4.ResponseHandling; using IdentityServer4.Services; using IdentityServer4.Validation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Threading.Tasks; namespace IdentityServer4.Endpoints { /// <summary> /// The token endpoint /// </summary> /// <seealso cref="IdentityServer4.Hosting.IEndpointHandler" /> internal class TokenEndpoint : IEndpointHandler { private readonly IClientSecretValidator _clientValidator; private readonly ITokenRequestValidator _requestValidator; private readonly ITokenResponseGenerator _responseGenerator; private readonly IEventService _events; private readonly ILogger _logger; /// <summary> /// 构造函数注入 <see cref="TokenEndpoint" /> class. /// </summary> /// <param name="clientValidator">客户端验证处理器</param> /// <param name="requestValidator">请求验证处理器</param> /// <param name="responseGenerator">输出生成处理器</param> /// <param name="events">事件处理器.</param> /// <param name="logger">日志</param> public TokenEndpoint( IClientSecretValidator clientValidator, ITokenRequestValidator requestValidator, ITokenResponseGenerator responseGenerator, IEventService events, ILogger<TokenEndpoint> logger) { _clientValidator = clientValidator; _requestValidator = requestValidator; _responseGenerator = responseGenerator; _events = events; _logger = logger; } /// <summary> /// Processes the request. /// </summary> /// <param name="context">The HTTP context.</param> /// <returns></returns> public async Task<IEndpointResult> ProcessAsync(HttpContext context) { _logger.LogTrace("Processing token request."); // 1、验证是否为Post请求且必须是form-data方式 if (!HttpMethods.IsPost(context.Request.Method) || !context.Request.HasFormContentType) { _logger.LogWarning("Invalid HTTP request for token endpoint"); return Error(OidcConstants.TokenErrors.InvalidRequest); } return await ProcessTokenRequestAsync(context); } private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context) { _logger.LogDebug("Start token request."); // 2、验证客户端授权是否正确 var clientResult = await _clientValidator.ValidateAsync(context); if (clientResult.Client == null) { return Error(OidcConstants.TokenErrors.InvalidClient); } /* 3、验证请求信息,详细代码(TokenRequestValidator.cs) 原理就是根据不同的Grant_Type,调用不同的验证方式 */ var form = (await context.Request.ReadFormAsync()).AsNameValueCollection(); _logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName); var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult); if (requestResult.IsError) { await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult)); return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse); } // 4、创建输出结果 TokenResponseGenerator.cs _logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName); var response = await _responseGenerator.ProcessAsync(requestResult); //发送token生成事件 await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult)); //5、写入日志,便于调试 LogTokens(response, requestResult); // 6、返回最终的结果 _logger.LogDebug("Token request success."); return new TokenResult(response); } private TokenErrorResult Error(string error, string errorDescription = null, Dictionary<string, object> custom = null) { var response = new TokenErrorResponse { Error = error, ErrorDescription = errorDescription, Custom = custom }; return new TokenErrorResult(response); } private void LogTokens(TokenResponse response, TokenRequestValidationResult requestResult) { var clientId = $"{requestResult.ValidatedRequest.Client.ClientId} ({requestResult.ValidatedRequest.Client?.ClientName ?? "no name set"})"; var subjectId = requestResult.ValidatedRequest.Subject?.GetSubjectId() ?? "no subject"; if (response.IdentityToken != null) { _logger.LogTrace("Identity token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.IdentityToken); } if (response.RefreshToken != null) { _logger.LogTrace("Refresh token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.RefreshToken); } if (response.AccessToken != null) { _logger.LogTrace("Access token issued for {clientId} / {subjectId}: {token}", clientId, subjectId, response.AccessToken); } } } } 执行步骤如下: 验证是否为Post请求且使用form-data方式传递参数(直接看代码即可) 验证客户端授权 详细的验证流程代码和说明如下。 ClientSecretValidator.cs public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context) { _logger.LogDebug("Start client validation"); var fail = new ClientSecretValidationResult { IsError = true }; // 从上下文中判断是否存在 client_id 和 client_secret信息(PostBodySecretParser.cs) var parsedSecret = await _parser.ParseAsync(context); if (parsedSecret == null) { await RaiseFailureEventAsync("unknown", "No client id found"); _logger.LogError("No client identifier found"); return fail; } // 通过client_id从客户端获取(IClientStore,客户端接口,下篇会介绍如何重写) var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id); if (client == null) {//不存在直接输出错误 await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client"); _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id); return fail; } SecretValidationResult secretValidationResult = null; if (!client.RequireClientSecret || client.IsImplicitOnly()) {//判断客户端是否启用验证或者匿名访问,不进行密钥验证 _logger.LogDebug("Public Client - skipping secret validation success"); } else { //验证密钥是否一致 secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets); if (secretValidationResult.Success == false) { await RaiseFailureEventAsync(client.ClientId, "Invalid client secret"); _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId); return fail; } } _logger.LogDebug("Client validation success"); var success = new ClientSecretValidationResult { IsError = false, Client = client, Secret = parsedSecret, Confirmation = secretValidationResult?.Confirmation }; //发送验证成功事件 await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type); return success; } PostBodySecretParser.cs /// <summary> /// Tries to find a secret on the context that can be used for authentication /// </summary> /// <param name="context">The HTTP context.</param> /// <returns> /// A parsed secret /// </returns> public async Task<ParsedSecret> ParseAsync(HttpContext context) { _logger.LogDebug("Start parsing for secret in post body"); if (!context.Request.HasFormContentType) { _logger.LogDebug("Content type is not a form"); return null; } var body = await context.Request.ReadFormAsync(); if (body != null) { var id = body["client_id"].FirstOrDefault(); var secret = body["client_secret"].FirstOrDefault(); // client id must be present if (id.IsPresent()) { if (id.Length > _options.InputLengthRestrictions.ClientId) { _logger.LogError("Client ID exceeds maximum length."); return null; } if (secret.IsPresent()) { if (secret.Length > _options.InputLengthRestrictions.ClientSecret) { _logger.LogError("Client secret exceeds maximum length."); return null; } return new ParsedSecret { Id = id, Credential = secret, Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret }; } else { // client secret is optional _logger.LogDebug("client id without secret found"); return new ParsedSecret { Id = id, Type = IdentityServerConstants.ParsedSecretTypes.NoSecret }; } } } _logger.LogDebug("No secret in post body found"); return null; } 验证请求的信息是否有误 由于代码太多,只列出TokenRequestValidator.cs部分核心代码如下, //是不是很熟悉,不同的授权方式 switch (grantType) { case OidcConstants.GrantTypes.AuthorizationCode: //授权码模式 return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters); case OidcConstants.GrantTypes.ClientCredentials: //客户端模式 return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters); case OidcConstants.GrantTypes.Password: //密码模式 return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters); case OidcConstants.GrantTypes.RefreshToken: //token更新 return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters); default: return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters); //扩展模式,后面的篇章会介绍扩展方式 } 创建生成的结果 TokenResponseGenerator.cs根据不同的认证方式执行不同的创建方法,由于篇幅有限,每一个是如何创建的可以自行查看源码。 /// <summary> /// Processes the response. /// </summary> /// <param name="request">The request.</param> /// <returns></returns> public virtual async Task<TokenResponse> ProcessAsync(TokenRequestValidationResult request) { switch (request.ValidatedRequest.GrantType) { case OidcConstants.GrantTypes.ClientCredentials: return await ProcessClientCredentialsRequestAsync(request); case OidcConstants.GrantTypes.Password: return await ProcessPasswordRequestAsync(request); case OidcConstants.GrantTypes.AuthorizationCode: return await ProcessAuthorizationCodeRequestAsync(request); case OidcConstants.GrantTypes.RefreshToken: return await ProcessRefreshTokenRequestAsync(request); default: return await ProcessExtensionGrantRequestAsync(request); } } 写入日志记录 为了调试方便,把生成的token相关结果写入到日志里。 输出最终结果 把整个执行后的结果进行输出,这样就完成了整个验证过程。 四、总结 通过前面的分析,我们基本掌握的Ids4整体的运行流程和具体一个认证请求的流程,由于源码太多,就未展开详细的分析每一步的实现,具体的实现细节我会在后续Ids4相关章节中针对每一项的实现进行讲解,本篇基本都是全局性的东西,也在讲解了了解到了客户端的认证方式,但是只是介绍了接口,至于接口如何实现没有讲解,下一篇我们将介绍Ids4实现自定义的存储并使用dapper替换EFCore实现与数据库的交互流程,减少不必要的请求开销。 对于本篇源码解析还有不理解的,可以进入QQ群:637326624进行讨论。
原文:Seaching TreeVIew WPF 项目中有一个树形结构的资源,需要支持搜索功能,搜索出来的结果还是需要按照树形结构展示,下面是简单实现的demo。 1.首先创建TreeViewItem的ViewModel,一般情况下,树形结构都包含DisplayName,Deepth,Parent,Children,Id, IndexCode,Visibility等属性,具体代码如下所示: 1 using System; 2 using System.Collections.Generic; 3 using System.Collections.ObjectModel; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using System.Windows; 8 9 namespace TreeViewDemo 10 { 11 public class TreeViewItemVM : NotifyPropertyChangedBase 12 { 13 public TreeViewItemVM () 14 { 15 Visible = Visibility.Visible; 16 } 17 18 private TreeViewItemVM parent; 19 public TreeViewItemVM Parent 20 { 21 get 22 { 23 return this.parent; 24 } 25 set 26 { 27 if (this.parent != value) 28 { 29 this.parent = value; 30 this.OnPropertyChanged(() => this.Parent); 31 } 32 } 33 } 34 35 private ObservableCollection<TreeViewItemVM> children; 36 public ObservableCollection<TreeViewItemVM> Children 37 { 38 get 39 { 40 return this.children; 41 } 42 set 43 { 44 if (this.children != value) 45 { 46 this.children = value; 47 this.OnPropertyChanged(() => this.Children); 48 } 49 } 50 } 51 52 private string id; 53 public string ID 54 { 55 get 56 { 57 return this.id; 58 } 59 set 60 { 61 if (this.id != value) 62 { 63 this.id = value; 64 this.OnPropertyChanged(() => this.ID); 65 } 66 } 67 } 68 69 private string indexCode; 70 public string IndexCode 71 { 72 get { return indexCode; } 73 set 74 { 75 if (indexCode != value) 76 { 77 indexCode = value; 78 this.OnPropertyChanged(() => IndexCode); 79 } 80 } 81 } 82 83 private string displayName; 84 public string DisplayName 85 { 86 get 87 { 88 return this.displayName; 89 } 90 set 91 { 92 if (this.displayName != value) 93 { 94 this.displayName = value; 95 this.OnPropertyChanged(() => this.DisplayName); 96 } 97 } 98 } 99 100 private int deepth; 101 public int Deepth 102 { 103 get 104 { 105 return this.deepth; 106 } 107 set 108 { 109 if (this.deepth != value) 110 { 111 this.deepth = value; 112 this.OnPropertyChanged(() => this.Deepth); 113 } 114 } 115 } 116 117 private bool hasChildren; 118 public bool HasChildren 119 { 120 get 121 { 122 return this.hasChildren; 123 } 124 set 125 { 126 if (this.hasChildren != value) 127 { 128 this.hasChildren = value; 129 this.OnPropertyChanged(() => this.HasChildren); 130 } 131 } 132 } 133 134 private NodeType type; 135 public NodeType Type 136 { 137 get { return type; } 138 set 139 { 140 if (type != value) 141 { 142 type = value; 143 OnPropertyChanged(() => this.Type); 144 } 145 } 146 } 147 148 private Visibility visible; 149 public Visibility Visible 150 { 151 get { return visible; } 152 set 153 { 154 if (visible != value) 155 { 156 visible = value; 157 OnPropertyChanged(() => this.Visible); 158 } 159 } 160 } 161 162 public bool NameContains(string filter) 163 { 164 if (string.IsNullOrWhiteSpace(filter)) 165 { 166 return true; 167 } 168 169 return DisplayName.ToLowerInvariant().Contains(filter.ToLowerInvariant()); 170 } 171 } 172 } 2.创建TreeViewViewModel,其中定义了用于过滤的属性Filter,以及过滤函数,并在构造函数中初始化一些测试数据,具体代码如下: 1 using System; 2 using System.Collections.Generic; 3 using System.Collections.ObjectModel; 4 using System.ComponentModel; 5 using System.Linq; 6 using System.Text; 7 using System.Threading.Tasks; 8 using System.Windows.Data; 9 10 namespace TreeViewDemo 11 { 12 public class TreeViewViewModel : NotifyPropertyChangedBase 13 { 14 public static TreeViewViewModel Instance = new TreeViewViewModel(); 15 16 private TreeViewViewModel() 17 { 18 Filter = string.Empty; 19 20 Root = new TreeViewItemVM() 21 { 22 Deepth = 0, 23 DisplayName = "五号线", 24 HasChildren = true, 25 Type = NodeType.Unit, 26 ID = "0", 27 Children = new ObservableCollection<TreeViewItemVM>() { 28 new TreeViewItemVM() { DisplayName = "站台", Deepth = 1, HasChildren = true, ID = "1", Type = NodeType.Region, 29 Children = new ObservableCollection<TreeViewItemVM>(){ 30 new TreeViewItemVM() { DisplayName = "Camera 01", Deepth = 2, HasChildren = false, ID = "3",Type = NodeType.Camera }, 31 new TreeViewItemVM() { DisplayName = "Camera 02", Deepth = 2, HasChildren = false, ID = "4",Type = NodeType.Camera }, 32 new TreeViewItemVM() { DisplayName = "Camera 03", Deepth = 2, HasChildren = false, ID = "5",Type = NodeType.Camera }, 33 new TreeViewItemVM() { DisplayName = "Camera 04", Deepth = 2, HasChildren = false, ID = "6",Type = NodeType.Camera }, 34 new TreeViewItemVM() { DisplayName = "Camera 05", Deepth = 2, HasChildren = false, ID = "7", Type = NodeType.Camera}, 35 }}, 36 new TreeViewItemVM() { DisplayName = "进出口", Deepth = 1, HasChildren = true, ID = "10", Type = NodeType.Region, 37 Children = new ObservableCollection<TreeViewItemVM>(){ 38 new TreeViewItemVM() { DisplayName = "Camera 11", Deepth = 2, HasChildren = false, ID = "13",Type = NodeType.Camera }, 39 new TreeViewItemVM() { DisplayName = "Camera 12", Deepth = 2, HasChildren = false, ID = "14",Type = NodeType.Camera }, 40 new TreeViewItemVM() { DisplayName = "Camera 13", Deepth = 2, HasChildren = false, ID = "15",Type = NodeType.Camera }, 41 new TreeViewItemVM() { DisplayName = "Camera 14", Deepth = 2, HasChildren = false, ID = "16", Type = NodeType.Camera}, 42 new TreeViewItemVM() { DisplayName = "Camera 15", Deepth = 2, HasChildren = false, ID = "17", Type = NodeType.Camera}, 43 }}, 44 } 45 }; 46 47 InitTreeView(); 48 } 49 50 private ObservableCollection<TreeViewItemVM> selectedCameras = new ObservableCollection<TreeViewItemVM>(); 51 52 private TreeViewItemVM root; 53 public TreeViewItemVM Root 54 { 55 get 56 { 57 return this.root; 58 } 59 set 60 { 61 if (this.root != value) 62 { 63 this.root = value; 64 this.OnPropertyChanged(() => this.Root); 65 } 66 } 67 } 68 69 /// <summary> 70 /// 过滤字段 71 /// </summary> 72 private string filter; 73 public string Filter 74 { 75 get 76 { 77 return this.filter; 78 } 79 set 80 { 81 if (this.filter != value) 82 { 83 84 this.filter = value; 85 this.OnPropertyChanged(() => this.Filter); 86 87 this.Refresh(); 88 } 89 } 90 } 91 92 /// <summary> 93 /// View 94 /// </summary> 95 protected ICollectionView view; 96 public ICollectionView View 97 { 98 get 99 { 100 return this.view; 101 } 102 set 103 { 104 if (this.view != value) 105 { 106 this.view = value; 107 this.OnPropertyChanged(() => this.View); 108 } 109 } 110 } 111 112 /// <summary> 113 /// 刷新View 114 /// </summary> 115 public void Refresh() 116 { 117 if (this.View != null) 118 { 119 this.View.Refresh(); 120 } 121 } 122 123 private bool DoFilter(Object obj) 124 { 125 TreeViewItemVM item = obj as TreeViewItemVM; 126 if (item == null) 127 { 128 return true; 129 } 130 131 bool result = false; 132 foreach (var node in item.Children) 133 { 134 result = TreeItemDoFilter(node) || result; 135 } 136 137 return result || item.NameContains(this.Filter); 138 } 139 140 private bool TreeItemDoFilter(TreeViewItemVM vm) 141 { 142 if (vm == null) 143 { 144 return true; 145 } 146 147 bool result = false; 148 if (vm.Type == NodeType.Region || vm.Type == NodeType.Unit) 149 { 150 foreach (var item in vm.Children) 151 { 152 result = TreeItemDoFilter(item) || result; 153 } 154 } 155 156 if (result || vm.NameContains(this.Filter)) 157 { 158 result = true; 159 vm.Visible = System.Windows.Visibility.Visible; 160 } 161 else 162 { 163 vm.Visible = System.Windows.Visibility.Collapsed; 164 } 165 166 return result; 167 } 168 169 public void InitTreeView() 170 { 171 this.View = CollectionViewSource.GetDefaultView(this.Root.Children); 172 this.View.Filter = this.DoFilter; 173 this.Refresh(); 174 } 175 } 176 } 3.在界面添加一个TreeView,并添加一个简单的Style,将ViewModel中必要数据进行绑定: 1 <Window x:Class="TreeViewDemo.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 Title="MainWindow" Height="450" Width="525"> 5 <Window.Resources> 6 <Style x:Key="style" TargetType="{x:Type TreeViewItem}"> 7 <Setter Property="Template"> 8 <Setter.Value> 9 <ControlTemplate TargetType="{x:Type TreeViewItem}"> 10 <Grid Visibility="{Binding Visible}" Background="{Binding Background}"> 11 <ContentPresenter ContentSource="Header"/> 12 </Grid> 13 14 <ControlTemplate.Triggers> 15 <Trigger Property="IsSelected" Value="true"> 16 <Setter Property="Background" Value="Green"/> 17 </Trigger> 18 </ControlTemplate.Triggers> 19 </ControlTemplate> 20 </Setter.Value> 21 </Setter> 22 </Style> 23 </Window.Resources> 24 <Grid> 25 <Grid.RowDefinitions> 26 <RowDefinition Height="Auto"/> 27 <RowDefinition Height="*"/> 28 </Grid.RowDefinitions> 29 30 <TextBox x:Name="searchTxt" Width="200" HorizontalAlignment="Center" Height="40" 31 Margin="20" Text="{Binding Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> 32 33 <TreeView 34 Grid.Row="1" 35 ItemsSource="{Binding View}"> 36 <TreeView.ItemTemplate> 37 <HierarchicalDataTemplate ItemContainerStyle ="{StaticResource style}" ItemsSource="{Binding Children}"> 38 <Grid Height="25" > 39 <TextBlock 40 x:Name="txt" 41 VerticalAlignment="Center" 42 Text="{Binding DisplayName}" 43 TextTrimming="CharacterEllipsis" 44 ToolTip="{Binding DisplayName}" /> 45 </Grid> 46 </HierarchicalDataTemplate> 47 </TreeView.ItemTemplate> 48 </TreeView> 49 </Grid> 50 </Window> 4.在给界面绑定具体的数据 1 using System.Windows; 2 3 namespace TreeViewDemo 4 { 5 /// <summary> 6 /// MainWindow.xaml 的交互逻辑 7 /// </summary> 8 public partial class MainWindow : Window 9 { 10 public MainWindow() 11 { 12 InitializeComponent(); 13 this.Loaded += MainWindow_Loaded; 14 } 15 16 void MainWindow_Loaded(object sender, RoutedEventArgs e) 17 { 18 this.DataContext = TreeViewViewModel.Instance; 19 } 20 } 21 } 5.运行结果:
参考 https://github.com/MicrosoftDocs/cordova-docs/blob/master/articles/tutorial-package-publish/tutorial-package-publish-readme.md Package Your Cordova App for Publishing to an App Store 为要定位的每个平台创建一个包。然后,您可以将每个包发布到商店。 打包Android版的应用 在这个部分: 修改应用的设置。 生成私钥。 请参阅配置文件中的该键。 创建包。 第1步:修改应用的设置 应用程序的常规设置显示在配置设计器的“ 常用”页面中。 在显示名称是出现在App Store名称。 该包名称是唯一标识您的应用程序的字符串。 选择一种命名方案,以减少名称冲突的可能性。 该域名访问集合列出了您的应用程序需要访问域。 例如,出现在上一图像中的WeatherApp从具有域的服务端点获取天气数据https://query.yahooapis.com。 大多数其他设置的目的从标题中清除,但您可以在此处找到有关它们的更多信息:config.xml文件。 特定于Android的设置显示在配置设计器的Android选项卡中。 您可以在config.xml参考主题的首选项部分中阅读有关每个选项的信息。 第2步:生成私钥 要为您的应用签名,请创建一个密钥库。密钥库是包含一组私钥的二进制文件。这是你如何创建一个。 在管理员模式下打开命令提示符。 在命令提示符中,将目录更改为该%JAVA_HOME%\bin文件夹。 (例如:) C:\Program Files (x86)\Java\jdk1.7.0_55\bin。 在命令提示符中,运行以下命令。 keytool -genkey -v -keystore c:\my-release-key.keystore -alias johnS -keyalg RSA -keysize 2048 -validity 10000 替换my-release-key.keystore并johnS使用对您有意义的名称。 系统会要求您为密钥提供密码和专有名称字段。 通过这一系列回复,您可以了解为每个提示提供的信息类型。与上一个命令一样,使用对您的应用有意义的信息响应每个提示。 Enter keystore password: pwd123 Re-enter new password: pwd123 What is your first and last name? [Unknown]= John Smith What is the name of your organizational unit? [Unknown]= ABC What is the name of your organization? [Unknown]= XYZ What is the name of your of your City or Locality? [Unknown]= Redmond What is the name of your State or Province? [Unknown]= WA What is the two-letter country code for this unit? [Unknown]= US Is CN=John Smith, OU=ABC, O=XYZ, L=Redmond, ST=WA, C=US correct?? [no]= y 提供此信息后,命令提示符中将显示如下输出。 Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days for: CN= John Smith, OU= ABC, O= XYZ, L=Redmond, ST=WA, C=US Enter key password for <johnS> (RETURN if same as keystore password): Android SDK将密钥库生成为名为my-release-key.keystore的文件,并将该文件放在C:\驱动器中。密钥库包含一个密钥,有效期为10000天。 如果您想了解有关此流程的更多详细信息,请参阅此处的Android开发人员文档:签署您的应用程序。 步骤3:参考配置文件中的私钥 首先,确定项目使用的Cordova CLI版本。这决定了您用来引用密钥的配置文件。 找到项目的CLI版本 CLI版本号显示在配置设计器的“ 平台”页面中。 您也可以在taco.json项目根目录的文件中找到它。 如果您的Cordova CLI版本低于5.0,请使用以下步骤 在Solution Explorer中,展开项目文件夹。然后展开res - > native - > android并选择ant.properties文件。 该ant.properties文件中的代码编辑器中打开。 在ant.properties文件中,添加描述密钥的信息。 key.store=c:\\my-release-key.keystore key.alias=johnS key.store.password= pwd123 key.alias.password= pwd123 重要说明:请勿使用引号括起这些值(例如:“pwd123”)。这可能会导致构建错误。 如果您的Cordova CLI版本大于5.0,请使用以下步骤 在解决方案资源管理器中,展开项目文件夹,然后选择build.json文件。如果项目中缺少该文件,则您的项目是使用早期版本的Cordova创建的,您应该手动创建该文件(并在步骤2中使用内容填充该文件)。 该build.json文件出现在代码编辑器。 在build.json文件中,添加描述密钥的信息。 { "android": { "release": { "keystore":"c:\\my-release-key.keystore", "storePassword":"pwd123", "alias":"johnS", "password":"pwd123", "keystoreType":"" } } } 第4步:创建包 在“标准”工具栏上,选择Android平台。 选择发布版本配置。 选择一个Android模拟器。 。 重要提示:不要选择任何Ripple模拟器。仅选择Android模拟器或设备。 在Build菜单上,选择Build Solution。 这将构建一个带.apk文件扩展名的文件。这是您要上传到商店的文件。 您可以在bin/Android/Release/项目的文件夹中找到该文件。 它是文件中不包含单词unaligned的文件。 将您的应用提交到商店 您可以将自己的应用发布到Google Play。 要为重要日子做准备,请查看Essentials以获取成功的应用程序。 然后,请参阅上传应用以使您的应用可以向全世界发布。 打包应用的iOS版本 在这个部分: 申请分发证书。 创建分发配置文件。 在Xcode中下载分发配置文件。 修改应用的设置。 创建包。 第1步:申请分发证书 分发证书可识别您的团队或组织。 如果您的团队已经拥有一个并且您想重复使用它,请参阅如何共享iOS分发证书。然后,直接跳到修改应用程序部分的设置。 如果您还没有分发证书,请继续本部分,我们将帮助您设置分发证书。 启动Xcode。 如果您尚未安装Xcode,请参阅第一步,在iOS设置指南的Mac部分安装一些内容。 在Xcode中,添加您的Apple ID(如果您还没有这样做)。 请参阅向您的帐户添加Apple ID。 在菜单栏中,选择Xcode - > Preferences。 在“ 帐户”页面中,选择“ 查看详细信息”按钮。 在帐户详细信息页面中,选择iOS分发签名标识旁边的“ 创建”按钮。 正在寻找有关签署身份的更多信息?请参阅创建签名身份(可选读数)。 选择完成按钮以关闭帐户详细信息页面。 第2步:创建分发配置文件 通过分发配置文件,您可以将应用程序提交到商店。 在“ 成员中心”页面上,选择“ 证书,标识符和配置文件”链接。 在“ 证书,标识符和配置文件”页面中,选择“ 配置配置文件”链接。 在“ 配置配置文件”页面中,选择+按钮。 在您需要什么类型的配置文件?单击页面,选择App Store选项,然后选择继续按钮。 在“ 选择应用程序ID”页面中,选择应用程序的应用程序ID,然后选择“ 继续”按钮。 在“ 选择证书”页面中,选择先前在Xcode中创建的分发证书,然后选择“ 继续”按钮。 在“ 命名此配置文件和生成”页面中,为您的配置文件命名,然后选择“ 生成”按钮。 在您的配置文件已就绪页面中,选择下载按钮。 需要更多细节?请参阅使用成员中心创建配置文件 第3步:下载分发配置文件 打开Xcode。 在菜单栏中,选择Xcode - > Preferences。 在“ 帐户”页面中,选择“ 查看详细信息”按钮。 在帐户详细信息页面中,选择配置文件的签名标识旁边的“ 下载”按钮。 选择完成按钮以关闭帐户详细信息页面。 第4步:修改应用的设置 应用程序的常规设置显示在配置设计器的“ 常用”页面中。 在显示名称是出现在App Store名称。 该包名称是唯一标识您的应用程序的字符串。 此标识符必须与您的分发配置文件的标识符相匹配。 您可以通过在Apple开发人员成员中心的“ 配置配置文件”页面中选择您的分发配置文件来查找配置文件的缩进程序。 。 该域名访问集合列出了您的应用程序需要访问域。 例如,出现在上一图像中的WeatherApp从具有域的服务端点获取天气数据https://query.yahooapis.com。 大多数其他设置的目的从标题中清除,但您可以在此处找到有关它们的更多信息:config.xml文件。 特定于iOS的设置显示在配置设计器的iOS选项卡中。 您可以在config.xml参考主题的首选项部分中阅读有关每个选项的信息。 第5步:创建包 构建您的应用程序以生成您将提交给商店的包。 在Mac上,确保远程代理正在运行。 请参阅在Mac上启动远程代理。 在Visual Studio中,打开应用程序的项目。 在“标准”工具栏上,选择iOS平台。 选择远程设备。 。 选择发布版本配置。 在Build菜单上,选择Build Solution。 这将启动remotebuild代理的构建,并使用分发证书和匹配的供应配置文件来构建已签名的iOS Application Archive(.ipa)文件。 您可以在bin/iOS/Release项目的文件夹中找到该文件。 将您的应用提交到商店 将.ipa文件复制到Mac上的文件夹中。 为您的应用创建iTunes Connect记录。 使用Application Loader将.ipa文件上传到iTunes。 Apple评论您的应用。如果他们不接受,您将收到一封电子邮件,说明原因以及您可以采取哪些措施来解决问题。这些文章描述了应用被拒绝的常见原因。 常见应用程序拒绝 App Store评论指南 打包应用程序的Windows版本 首先,确定您希望将应用程序提供给哪些平台和设备系列。您可以将应用程序用于Windows Phone,台式PC和平板电脑。 应用程序所针对的Windows版本无关紧要。Windows商店接受所有这些。也就是说,设备或PC的操作系统仅运行针对该操作系统或早期版本的相同版本的应用程序。 要了解有关Windows程序包和Windows设备兼容性的更多信息,请参阅OS版本和程序包分发。 在这个部分: 修改应用的设置。 使您的应用程序可用于Windows Phone。 使您的应用可用于Windows台式机或平板电脑。 在设备上安装您的应用程序或将其发布到商店。 修改您的应用设置 应用程序的常规设置显示在配置设计器的“ 常用”页面中。 在显示名称是出现在App Store名称。 该包名称是唯一标识您的应用程序的字符串。 选择一种命名方案,以减少名称冲突的可能性。 该域名访问集合列出了您的应用程序需要访问域。 例如,出现在上一图像中的WeatherApp从具有域的服务端点获取天气数据https://query.yahooapis.com。 大多数其他设置的目的从标题中清除,但您可以在此处找到有关它们的更多信息:config.xml文件。 特定于Windows的设置显示在配置设计器的Windows选项卡中。 您可能已经注意到,此页面共享三个与Common页面相同的字段名称(显示名称,包名称和版本)。 在“ 创建应用程序包向导”(稍后将使用)中,您可能必须选择不同的显示名称或程序包名称,因为Windows特定的命名要求,名称已由其他人保留,或者您要关联您的应用程序具有您之前保留的名称。 在任何这些情况下,一旦完成向导,Visual Studio将在Windows页面上更新显示名称和包名称。这样,您的其他平台目标不会被迫使用这些名称。 此页面具有“ 版本”字段的原因是Windows使用4位数版本号而不是3位数版本号。您可以直接修改此字段,也可以让Visual Studio根据您在“ 创建应用程序包向导”中选择的版本号设置此字段。 我们将在下一节中查看Windows目标版本字段。 使您的应用程序可用于Windows Phone 您的应用定位的是哪个版本的Windows?选择一个部分: 您的应用以Windows 10为目标。 您的应用面向Windows 8.1。 您的应用针对Windows 8。 您的应用以Windows 10为目标 在标准工具栏中,选择Windows-ARM。 在配置设计器的Windows页面中,从Windows目标版本下拉列表中选择Windows 10。 。 选择Project - > Store - > Create App Packages以启动打包向导。 完成向导。 有关分步指导,请参阅创建应用包 在AppPackages项目根目录中的文件夹中查找包装文件。 将Windows应用程序安装到设备上或将其发布到商店。 您的应用面向Windows 8.1 在标准工具栏中,选择Windows Phone(通用)。 在配置设计器的Windows页面中,从Windows目标版本下拉列表中选择Windows 10。 。 选择Project - > Store - > Create App Packages以启动打包向导。 完成向导。 有关分步指导,请参阅创建应用包 在AppPackages项目根目录中的文件夹中查找包装文件。 将Windows应用程序安装到设备上或将其发布到商店。 您的应用以Windows Phone 8为目标 在标准工具栏中,选择发布配置。 选择Windows Phone 8。 选择Build - > Build Solution来构建您的包。 在bin\WindowsPhone8\Release项目根目录中的文件夹中查找包装文件。 将Windows应用程序安装到设备上或将其发布到商店。 使您的应用可用于Windows台式机或平板电脑 在标准工具栏中,选择Windows-AnyCPU。 在配置设计器的Windows页面中,从Windows目标版本下拉列表中选择Windows 10或Windows 8.1。 。 选择Project - > Store - > Create App Packages以启动打包向导。 完成向导,然后在向导中选择要使应用程序可用的平台。 有关分步指导,请参阅创建应用包 在AppPackages项目根目录中的文件夹中查找包装文件。 将Windows应用程序安装到设备上或将其发布到商店 要将应用程序发布到商店,请参阅发布Windows应用程序。 将应用程序直接安装到设备上加载应用程序包。
原文:[UWP]在应用开发中安全使用文件资源 在WPF或者UWP应用开发中,有时候会不可避免的需要操作文件系统(创建文件/目录),这时候有几个坑是需要大家注意下的。 创建文件或目录时的非法字符检测 在Windows系统中,我们创建文件时会注意到,某些特殊字符是不可以用作文件名输入的。 那么,同样的,如果你的应用可以提供给用户创建文件/目录的功能,要特别注意的是:你必须对用户键入的文件或者目录名检测,避免用户键入非法字符。 否则,应用可能会遇到下面这个bug:System.IO.FileNotFoundException:“文件名、目录名或卷标语法不正确。” 避免手段其实也很简单,System.IO.Path类中可以获取到所有的非法字符,我们只需要检测文件或目录名,避免出现非法字符就可以了。 不可以在文件名中出现的字符 Path.GetInvalidFileNameChars(): char[41] { '"', '<', '>', '|', '\0', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\u000e', '\u000f', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017', '\u0018', '\u0019', '\u001a', '\u001b', '\u001c', '\u001d', '\u001e', '\u001f', ':', '*', '?', '\\', '/' } 不可以在路径字符串中出现的字符 Path.GetInvalidPathChars(): char[36] { '"', '<', '>', '|', '\0', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\u000e', '\u000f', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017', '\u0018', '\u0019', '\u001a', '\u001b', '\u001c', '\u001d', '\u001e', '\u001f' } 这里给大家提供一个小窍门,使用C#交互窗口(VS2015及更高版本都可以使用),可以快速查看代码片段执行结果。 在XAML中引用外部资源时的非法字符检测 此外,在开发WPF或者UWP应用时,如果我们需要在XAML中引入外部资源URI,那么情况会比较特殊一点。 有时候尽管你的文件名或者路径URI均没有包含Windows文件系统中的非法字符,应用仍有可能崩溃。这是因为,在XAML中定义了一些不允许出现的字符,这些字符与Windows文件系统中的非法字符不尽相同。 这些字符是: { ';' , '/' , '?' , ':' , '@' , '&' , '=' , '+' , '$' , ',','<' , '>' , '#' , '%' , '"' } 例如‘#’,它在文件系统中是合法字符,但是却不能出现在XAML中引入的外部资源URI字符串里。 这个问题在邵猛大佬的《WPF 图片显示中的保留字符问题》中也是有讲到的,但是文章中没有给到解决方法。 在某些情况下,如开发应用时,我们允许用户上传图片到应用文件夹下作为资源使用,我们可以在拷贝资源时通过排除/替换文件名里非法字符的方法来避免这个BUG。 public static class XamlUriHelper { private static readonly char[] Excluded = { ';' , '/' , '?' , ':' , '@' , '&' , '=' , '+' , '$' , ',','<' , '>' , '#' , '%' , '"' }; public static string GetValidName(string fileName) { foreach (var item in Excluded) { fileName = fileName.Replace(item, '_'); } return fileName; } } 结尾 上面说到的两种情况,第一种是比较好处理的,而第二种需要一些折中的处理手段。另外吐槽一点,XAML应用这么久了,第二种情况按理说是不应该出现的,不知道微软方面有没有注意到(或者说是否有官方解决方法,类似转义符什么的?)。如果有了解这个问题的大佬,欢迎在评论区指出! 这篇博文到此结束,谢谢大家!
原文:[WPF]为旧版本的应用添加触控支持 之前做WPF开发时曾经遇到这样一个需求:为一个基于 .NET Framework 3.5开发的老旧WPF程序添加触控支持,以便于大屏触控展示。 接手之后发现这是一个大坑。 项目最初的时候完全没考虑过软件架构设计,业务逻辑基本都写在后台代码中,经过两代程序员的开发维护(初代开发者已离职,文档这种东西不存在的),主界面cs代码已经有上万行,各种事件注册的非常杂乱。由于是做给政府部门用的,稳定性很重要,修修补补不断的打补丁,程序已经非常难维护了。 而且不像最新.net框架下的WPF以及UWP开发中,我们有Pointer开头的系列事件可以统一处理鼠标点击和触控。在基于.net框架 4.7以下版本构建的WPF应用里,鼠标点击和触控是独立的,需要分别处理。 这里有一点需要说明:在单点电阻式触控屏(除了ATM机之类的特殊用途,基本要被淘汰掉了)下,系统对单点触控的处理是模拟的鼠标操作,这种情况下即使不处理触控事件,程序也可以正常运行,需要处理触控事件特指的是支持多点触控的电容式触摸屏。 当时我接手的WPF应用之前是完全没有做过触控事件处理的,我粗略的查找统计了一下,需要处理的按钮点击事件大概有上千个,如果手动处理,将是非常难以接受的重复工作,另外修改后的应用程序也必须完整走一遍测试流程,以防带来灾难性BUG。 那么有没有一种简单的方法可以快速处理呢? 我们知道WPF开发中,所有的用户交互事件都是路由事件,其中带有Preview前缀的为隧道路由事件,不带前缀的为冒泡路由事件。其区别是:隧道路由事件由根元素传递到触发事件的元素,而冒泡路由事件传递方向正好相反。那么,尽管程序中需要处理触控事件的地方很多,但是我们都可以在应用顶层元素中通过冒泡路由事件拦截到。是不是可以利用这一点做文章呢? 我的想法是这样的:由于应用已经处理了鼠标交互事件,那我们完全可以将应用的触控事件转发给鼠标交互事件的Handler去处理,这样就避免了我们做机械的重复操作。 具体处理步骤如下: 在应用窗口的顶级元素(可视化树的根节点)上添加触控事件处理程序,捕获应用内部触控事件; this.AddHandler(TouchUpEvent, new RoutedEventHandler(GetTouchUp)); this.AddHandler(TouchDownEvent, new RoutedEventHandler(GetTouchDown)); 获取引发事件的源控件(原本想通过e.OriginalSource获取,但测试中发现获取的有错误,所以用UIElement类中的InputHitTest方法传入触控点坐标,获取到引发事件的源控件); TouchEventArgs te = (TouchEventArgs)e; Point p = te.GetTouchPoint(this).Position;//这里是获取触控点相对某个界面元素的坐标 UIElement uiControl = (UIElement)this.InputHitTest(p); 触发源控件的鼠标事件(在TouchUp中还同时触发了Button类的Click事件,用于处理按钮的点击事件); MouseButtonEventArgs args = new MouseButtonEventArgs(Mouse.PrimaryDevice,te.Timestamp,MouseButton.Left); args.RoutedEvent = MouseDownEvent; uiControl.RaiseEvent(args); 完整的事件处理代码如下: this.AddHandler(TouchUpEvent, new RoutedEventHandler(GetTouchUp)); this.AddHandler(TouchDownEvent, new RoutedEventHandler(GetTouchDown)); private void GetTouchDown(object sender, RoutedEventArgs e) { TouchEventArgs te = (TouchEventArgs)e; Point p = te.GetTouchPoint(this).Position; UIElement uiControl = (UIElement)this.InputHitTest(p); MouseButtonEventArgs args = new MouseButtonEventArgs(Mouse.PrimaryDevice, te.Timestamp, MouseButton.Left); args.RoutedEvent = MouseDownEvent; uiControl.RaiseEvent(args); } private void GetTouchUp(object sender, RoutedEventArgs e) { TouchEventArgs te = (TouchEventArgs)e; Point p = te.GetTouchPoint(this).Position; UIElement uiControl = (UIElement)this.InputHitTest(p); MouseButtonEventArgs args = new MouseButtonEventArgs(Mouse.PrimaryDevice, te.Timestamp, MouseButton.Left); args.RoutedEvent = MouseUpEvent; uiControl.RaiseEvent(args); uiControl.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); } 要说明的一点是,我这里的处理是不完善的,仅仅处理了常见的点击操作。譬如鼠标右键(合理的触控事件应该是长按界面元素一段时间后触发),鼠标移动,滚轮操作都没有做处理,这些也是可以通过类似的方法转换为合适的触控事件触发的。 结尾 今天文章里所述的内容其实已经是很久以前的东西了,我现在的主要工作方向远离WPF开发很久了,突然翻相关的旧文件想起来,所以才有了这篇文章。好记性不如烂笔头,知识不用总有忘的一天,不如写出来贡献给需要的人,谢谢大家!
原文:[UWP]如何实现UWP平台最佳图片裁剪控件 前几天我写了一个UWP图片裁剪控件ImageCropper(开源地址),自认为算是现阶段UWP社区里最好用的图片裁剪控件了,今天就来分享下我编码的过程。 为什么又要造轮子 因为开发需要,我们需要使用一个图片裁剪控件来编辑用户上传的图片。本着尽量不重复造轮子的原则,我找了下现在UWP生态圈里可用的图片裁剪控件,然后发现一个悲惨的事实:UWP生态圈甚至没有一个体验优秀的图片裁剪控件! 举例来说,就连现在商店里做的比较好的网易云音乐、IT之家以及爱奇艺等应用,他们使用的图片裁剪控件体验也糟糕的一塌糊涂(有认识他们开发人员的大佬,欢迎把我的这篇文章推荐给他们,不怕打脸)。 下图是爱奇艺与IT之家的头像裁剪控件: 那么好吧,我们只好又来造轮子了! 借鉴优秀的前辈 现阶段在Windows平台上,最让我称佩的裁剪图片的应用就是Windows照片了。 它有以下两个优点: 裁剪区域永远显示在视觉中心,突出重点; 操作体验顺畅,触屏操作也能有很好体验。 这次我们就来“抄袭”一下这个系统应用。 如何实现 有了实现目标,接下来就是思考如何编码实现了。 需要哪些属性来控制裁剪区域 分析一下这个控件的组成部分,其实就是由三部分组成的:最下层裁剪源图像,上层控制裁剪区域的四个按钮,以及遮盖在图像上的黑色半透明遮罩层。 所以我定义了下面几个依赖属性来控制界面: SourceImage:类型为WriteableBitmap,控制裁剪图像源; X1,Y1,X2,Y2:这四个double值,控制剪裁区域左上角与右下角两个点坐标; AspectRatio:类型为double值,控制裁剪图像纵横比; MaskArea:类型为GeometryGroup,控制黑色半透明遮罩层; ImageTransform:类型为CompositeTransform,控制裁剪过程中的源图像变换。 这样的话,更改裁剪区域只需要修改X1,Y1,X2,Y2这四个值就可以了。 另外,如果我们通过拖动图片来移动选择区域,同样是修改X1,Y1,X2,Y2的值(而不是对图片进行变换,动图中可能看不出来,源代码中可以看到)。 控制裁剪图像源Transform 在Windows照片应用裁剪图片控件中,其体验良好的一个主要原因就是剪裁区域永远处于视觉中心,这是通过控制裁剪图像源在界面上的Transform来完成的。 我们可以看到,裁剪图像源的变换规则如下: 裁剪区域永远位于界面中心(使用Uniform规则); 当裁剪区域缩小时,在停止拖动裁剪框控制按钮时,更新裁剪图像源的Transform; 当裁剪区域扩大时,实时更新裁剪图像源的Transform。 限制剪裁区域范围 另外要注意的是,我们必须保证X1,Y1,X2,Y2取值范围不超过图片区域。 这里有个关于Rect的坑要说明下。一开始我选用的判断方法是:通过Rect.Contains方法传入剪裁区域左上角与右下角两个点坐标,如果均为true,代表剪裁区域范围合法。但是我发现,在Rect长宽为有小数部分的double值时,如果我把右下角坐标设置为new Point(Rect.X + Rect.Width, Rect.Y + Rect.Height),这个方法会返回错误的false值,实在是坑爹! 因此,考虑到使用场景,我为Rect写了另外一个扩展方法: public static bool IsSafePoint(this Rect targetRect, Point point) { if (point.X - targetRect.X < 0.01) return false; if (point.X - (targetRect.X + targetRect.Width) > 0.01) return false; if (point.Y - targetRect.Y < 0.01) return false; if (point.Y - (targetRect.Y + targetRect.Height) > 0.01) return false; return true; } 核心逻辑代码 下图是这个图片剪裁控件的核心逻辑: 其中InitImageLayout方法会在图片源变化时被调用,它会初始化图片布局(通过调用UpdateImageLayout方法)。 private void InitImageLayout() { if (ImageTransform == null) ImageTransform = new CompositeTransform(); _maxClipRect = new Rect(0, 0, SourceImage.PixelWidth, SourceImage.PixelHeight); var maxSelectedRect = new Rect(1, 1, SourceImage.PixelWidth - 2, SourceImage.PixelHeight - 2); _currentClipRect = KeepAspectRatio ? maxSelectedRect.GetUniformRect(AspectRatio) : maxSelectedRect; UpdateImageLayout(); } UpdateImageLayout方法用于初始化控件或者控件SizeChanged时,调用此方法更新控件布局(通过调用UpdateImageLayoutWithViewport方法)。 private void UpdateImageLayout() { var canvasRect = new Rect(0, 0, CanvasWidth, CanvasHeight); var uniformSelectedRect = canvasRect.GetUniformRect(_currentClipRect.Width / _currentClipRect.Height); UpdateImageLayoutWithViewport(uniformSelectedRect, _currentClipRect); } UpdateImageLayoutWithViewport方法是更新控件布局的核心逻辑,它接受两个参数:viewport和viewportImgRect,其中viewport代表的是实际呈现在你视觉中心的区域,viewportImgRect表示viewport所对应的实际图片区域(以实际像素大小为单位),代码将通过这两个参数更新裁剪图像源的Transform。 private void UpdateImageLayoutWithViewport(Rect viewport, Rect viewportImgRect) { var imageScale = viewport.Width / viewportImgRect.Width; ImageTransform.ScaleX = ImageTransform.ScaleY = imageScale; ImageTransform.TranslateX = viewport.X - viewportImgRect.X * imageScale; ImageTransform.TranslateY = viewport.Y - viewportImgRect.Y * imageScale; var selectedRect = ImageTransform.TransformBounds(_currentClipRect); _limitedRect = ImageTransform.TransformBounds(_maxClipRect); var startPoint = _limitedRect.GetSafePoint(new Point(selectedRect.X, selectedRect.Y)); var endPoint = _limitedRect.GetSafePoint(new Point(selectedRect.X + selectedRect.Width, selectedRect.Y + selectedRect.Height)); _changeByCode = true; X1 = startPoint.X; Y1 = startPoint.Y; X2 = endPoint.X; Y2 = endPoint.Y; _changeByCode = false; } UpdateClipRectWithAspectRatio则在用户对剪裁区域改变时被调用,其中dragPoint代表用户操作的哪个按钮,diffPos代表该按钮的前后位置差值。 private void UpdateClipRectWithAspectRatio(DragPoint dragPoint, Point diffPos) { if (KeepAspectRatio) { if (Math.Abs(diffPos.X / diffPos.Y) > AspectRatio) { if (dragPoint == DragPoint.UpperLeft || dragPoint == DragPoint.LowerRight) diffPos.Y = diffPos.X / AspectRatio; else diffPos.Y = -diffPos.X / AspectRatio; } else { if (dragPoint == DragPoint.UpperLeft || dragPoint == DragPoint.LowerRight) diffPos.X = diffPos.Y * AspectRatio; else diffPos.X = -diffPos.Y * AspectRatio; } } var startPoint = new Point(X1, Y1); var endPoint = new Point(X2, Y2); switch (dragPoint) { case DragPoint.UpperLeft: startPoint.X += diffPos.X; startPoint.Y += diffPos.Y; break; case DragPoint.UpperRight: endPoint.X += diffPos.X; startPoint.Y += diffPos.Y; break; case DragPoint.LowerLeft: startPoint.X += diffPos.X; endPoint.Y += diffPos.Y; break; case DragPoint.LowerRight: endPoint.X += diffPos.X; endPoint.Y += diffPos.Y; break; } if (_limitedRect.IsSafePoint(startPoint) && _limitedRect.IsSafePoint(endPoint)) { var canvasRect = new Rect(0, 0, CanvasWidth, CanvasHeight); var newRect = new Rect(startPoint, endPoint); canvasRect.Union(newRect); if (canvasRect.X < 0 || canvasRect.Y < 0 || canvasRect.Width > CanvasWidth || canvasRect.Height > CanvasHeight) { var inverseImageTransform = ImageTransform.Inverse; if (inverseImageTransform != null) { var movedRect = inverseImageTransform.TransformBounds( new Rect(startPoint, endPoint)); movedRect.Intersect(_maxClipRect); _currentClipRect = movedRect; var oriCanvasRect = new Rect(0, 0, CanvasWidth, CanvasHeight); var viewportRect = oriCanvasRect.GetUniformRect(canvasRect.Width / canvasRect.Height); var viewportImgRect = inverseImageTransform.TransformBounds(canvasRect); UpdateImageLayoutWithViewport(viewportRect, viewportImgRect); } } else { X1 = startPoint.X; Y1 = startPoint.Y; X2 = endPoint.X; Y2 = endPoint.Y; } } } UpdateMaskArea方法用来更新遮盖在裁剪图像源上的黑色半透明遮罩层,其实就是图像上覆盖了一个Path元素,这里就不细讲了,直接贴代码。 private void UpdateMaskArea() { _maskArea.Children.Clear(); _maskArea.Children.Add(new RectangleGeometry { Rect = new Rect(-_layoutGrid.Padding.Left, -_layoutGrid.Padding.Top, _layoutGrid.ActualWidth, _layoutGrid.ActualHeight) }); _maskArea.Children.Add(new RectangleGeometry {Rect = new Rect(new Point(X1, Y1), new Point(X2, Y2))}); MaskArea = _maskArea; _layoutGrid.Clip = new RectangleGeometry { Rect = new Rect(0, 0, _layoutGrid.ActualWidth, _layoutGrid.ActualHeight) }; } 结尾 到这里,这个控件的所有东西就讲的差不多了,大家有没有觉得还缺了点什么? 对的,它还缺少了裁剪图像源Transform变化时的过渡动画,对于优秀的用户体验来说,这是不可或缺的! 之后我会抽时间补完这部分,并且跟大家讲一点Composition Api的东西,请大家敬请期待! 这篇文章到此结束,谢谢大家阅读!
原文:5.3Role和Claims授权「深入浅出ASP.NET Core系列」 希望给你3-5分钟的碎片化学习,可能是坐地铁、等公交,积少成多,水滴石穿,码字辛苦,如果你吃了蛋觉得味道不错,希望点个赞,谢谢关注。 Role授权 这是一种Asp.Net常用的传统的授权方法,当我们在生成Token的时候,配置的ClaimTypes.Role为Admin,而ValuesController.cs是一个普通user(如下图所示),我们看下是否能访问成功? 我们把生成的Token通过JWT官网验证一下,发现多了一个"role":"admin" Postman结果如下图所示,结果肯定是没有权限的!可以简单粗暴的说这就是Role授权,基于一组角色来进行授权: Claims授权 相比Role授权,更推荐大家使用Claims授权,这是.NET Core更推荐的授权方式,是传统没有的新东西。 首先,我们要在Starup.cs的ConfigureServices()方法中配置如下代码: 其次,我们要在生成token的时候配置Claims,同时在被访问的Controller上面配置Policy="Admin",如下所示: 我们把生成的Token通过JWT官网验证一下,发现多了一个"Admin":"true" 以上权限针对的是整个Controller,如果你想对该控制器内部某个方法开放匿名授权,也没有问题,只要配置如下代码即可。 至此Claims授权就完成了,简单粗暴,此文只是一个引子,如果想了解更深入的自定义授权方式,可以浏览下面的参考文献。 参考文献: Using Roles with the ASP.NET Core JWT middleware Claims-based authorization in ASP.NET Core
原文:MySQL5.7 添加用户、删除用户与授权 mysql -uroot -proot MySQL5.7 mysql.user表没有password字段改 authentication_string; 一. 创建用户: 命令:CREATE USER 'username'@'host' IDENTIFIED BY 'password'; 例子: CREATE USER 'dog'@'localhost' IDENTIFIED BY '123456'; CREATE USER 'dog2'@'localhost' IDENTIFIED BY ''; PS:username - 你将创建的用户名, host - 指定该用户在哪个主机上可以登陆,此处的"localhost",是指该用户只能在本地登录,不能在另外一台机器上远程登录,如果想远程登录的话,将"localhost"改为"%",表示在任何一台电脑上都可以登录;也可以指定某台机器可以远程登录; password - 该用户的登陆密码,密码可以为空,如果为空则该用户可以不需要密码登陆服务器。 二.授权: 命令:GRANT privileges ON databasename.tablename TO 'username'@'host' PS: privileges - 用户的操作权限,如SELECT , INSERT , UPDATE 等(详细列表见该文最后面).如果要授予所的权限则使用ALL.;databasename - 数据库名,tablename-表名,如果要授予该用户对所有数据库和表的相应操作权限则可用*表示, 如*.*. 例子: GRANT SELECT, INSERT ON mq.* TO 'dog'@'localhost'; 三.创建用户同时授权 mysql> grant all privileges on mq.* to test@localhost identified by '1234';Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> flush privileges;Query OK, 0 rows affected (0.01 sec) PS:必须执行flush privileges; 否则登录时提示:ERROR 1045 (28000): Access denied for user 'user'@'localhost' (using password: YES ) 四.设置与更改用户密码 命令:SET PASSWORD FOR 'username'@'host' = PASSWORD('newpassword'); 例子: SET PASSWORD FOR 'dog2'@'localhost' = PASSWORD("dog"); 五.撤销用户权限 命令: REVOKE privilege ON databasename.tablename FROM 'username'@'host'; 说明: privilege, databasename, tablename - 同授权部分. 例子: REVOKE SELECT ON mq.* FROM 'dog2'@'localhost'; PS: 假如你在给用户'dog'@'localhost''授权的时候是这样的(或类似的):GRANT SELECT ON test.user TO 'dog'@'localhost', 则在使用REVOKE SELECT ON *.* FROM 'dog'@'localhost';命令并不能撤销该用户对test数据库中user表的SELECT 操作.相反,如果授权使用的是GRANT SELECT ON *.* TO 'dog'@'localhost';则REVOKE SELECT ON test.user FROM 'dog'@'localhost';命令也不能撤销该用户对test数据库中user表的Select 权限. 具体信息可以用命令SHOW GRANTS FOR 'dog'@'localhost'; 查看. 六.删除用户 命令: DROP USER 'username'@'host'; 七.查看用户的授权 mysql> show grants for dog@localhost;+---------------------------------------------+| Grants for dog@localhost |+---------------------------------------------+| GRANT USAGE ON *.* TO 'dog'@'localhost' || GRANT INSERT ON `mq`.* TO 'dog'@'localhost' |+---------------------------------------------+2 rows in set (0.00 sec) PS:GRANT USAGE:mysql usage权限就是空权限,默认create user的权限,只能连库,啥也不能干
原文:如何自定义CSS滚动条的样式? 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由前端林子发表 本文会介绍CSS滚动条选择器,并在demo中展示如何在Webkit内核浏览器和IE浏览器中,自定义一个横向以及一个纵向的滚动条。 0.需求 有的时候我们不想使用浏览器默认的滚动条样式,因为不够定制化和美观。那么如何自定义滚动条的样式呢?下面一起来看看吧。 1 基础知识 1.1 Webkit内核的css滚动条选择器 ::-webkit-scrollbar CSS伪类选择器影响了一个元素的滚动条的样式 属性: ::-webkit-scrollbar — 整个滚动条 ::-webkit-scrollbar-track — 滚动条轨道 ::-webkit-scrollbar-thumb — 滚动条上的滚动滑块 ::-webkit-scrollbar-button — 滚动条上的按钮 (上下箭头) ::-webkit-scrollbar-track-piece — 滚动条没有滑块的轨道部分 ::-webkit-scrollbar-corner — 边角,即当同时有垂直滚动条和水平滚动条时交汇的部分 ::-webkit-resizer — 某些元素的corner部分的部分样式(例:textarea的可拖动按钮) 注意: (1)浏览器的支持情况: ::-webkit-scrollbar 仅仅在支持Webkit的浏览器 (Chrome, Safari)可以使用。 (2)可设置竖直/水平方向的滚动条 可以设置水平方向的滚动条(:horizontal),不加默认是竖直方向(:vertical)。 (3)滚动条上的按钮(:decrement、:increment) 可以设置图片,这点会在下面demo中展示。 1.2 IE自定义滚动条样式 可自定义的样式比较少,只能控制滚动条各个部分显示的颜色,定制性较低。这里我只列举了部分样式,诸如scrollbar-3dlight-color、scrollbar-highlight-color等样式试了下没有效果,这里不再列出: scrollbar-arrow-color — 滚动条三角箭头的颜色 scrollbar-face-color — 滚动条上滚动滑块颜色 scrollbar-track-color— 滚动条轨道、按钮背景的颜色 scrollbar-shadow-color— 滚动框上滑块边框的颜色 2.demo快速上手 2.1 Webkit内核的浏览器自定义滚动条样式 (chrome, safari) 如果觉得上述说明有些抽象,可以直接在浏览器中打开demo,结合demo中的注释来理解各个属性的意义。图中我对其中的一些属性做了标注,滚动条外层轨道属性并未在图中标注,可打开chrome浏览器控制台查看属性: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>scrollbar的demo--lynnshen</title> <style type="text/css"> * { margin: 0; padding: 0; } .scolltable { width: 500px; height: 300px; border: 1px solid black; /*实现水平垂直居中*/ position: absolute; left: 50%; top: 50%; margin-left: -250px; margin-top: -150px; overflow: scroll; } .content { /*要设的比.scolltable更宽一些*/ width: 600px; } /*整个滚动条*/ ::-webkit-scrollbar { width: 24px; background-color: transparent; } /*水平的整个滚动条*/ ::-webkit-scrollbar:horizontal { height: 24px; background-color: transparent; } /*滚动条轨道*/ ::-webkit-scrollbar-track { background-color: #f6f8fc; border-right: 1px solid #f1f5fa; border: 1px solid #f1f5fa; ; } /*竖直的滑块*/ ::-webkit-scrollbar-thumb { background-color: rgba(220, 228, 243, 1); border-radius: 0px; border-top: 1px solid #edf2f9; border-bottom: 1px solid #edf2f9; border-left: 1px solid #f1f5fa; } /*水平的滑块*/ ::-webkit-scrollbar-thumb:horizontal { /* background-color: rgba(220, 228, 243, 1); */ border-radius: 0px; border-top: 1px solid #edf2f9; /* border-right: 1px solid #f1f5fa; border-left: 1px solid #f1f5fa; */ } /*滚动条上的按钮--竖直滚动条向上*/ ::-webkit-scrollbar-button:decrement { border-bottom: 1px solid #edf2f9; height: 26px; background: url("./images/scroll_up.png") 7px 9px no-repeat; border-right: 1px solid #f1f5fa; border-left: 1px solid #f1f5fa; } /*滚动条上的按钮--竖直滚动条向下*/ ::-webkit-scrollbar-button:increment { border-top: 1px solid #edf2f9; height: 26px; background: url("./images/scroll_down.png") 7px 10px no-repeat; border-right: 1px solid #f1f5fa; border-left: 1px solid #f1f5fa; border-bottom: 1px solid #f1f5fa; } /*滚动条上的按钮--水平滚动条向左*/ ::-webkit-scrollbar-button:horizontal:decrement { border-top: 1px solid #edf2f9; width: 26px; background: url("./images/scroll_left.png") 9px 7px no-repeat; border-top: 1px solid #f1f5fa; border-bottom: 1px solid #f1f5fa; border-right:1px solid #f1f5fa; } /*滚动条上的按钮--水平滚动条向右*/ ::-webkit-scrollbar-button:horizontal:increment { border-top: 1px solid #edf2f9; width: 25px; background: url("./images/scroll_right.png") 10px 7px no-repeat; border-left:1px solid #f1f5fa; } /*边角*/ ::-webkit-scrollbar-corner{ border:1px solid #dce4f3; } </style> </head> <body> <div class="scolltable"> <div class="content"> 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 </div> </div> </body> </html> 实现效果: WebKit内核的浏览器 说明: (1)滚动条两端的按钮使用的图片,四个角分别使用了四张图片; (2).scolltable实现了水平垂直居中的效果,具体方法是:使用绝对对位,将元素的定点定位到body的中心。然后使用负margin(即元素宽高的一半)将其拉回到body的中心。 2.2 IE自定义滚动条样式 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>scrollbar for IE--lynnshen</title> <style type="text/css"> * { margin: 0; padding: 0; } .scolltable { width: 500px; height: 300px; border: 1px solid black; /*实现水平垂直居中*/ position: absolute; left: 50%; top: 50%; margin-left: -250px; margin-top: -150px; overflow: scroll; scrollbar-face-color:greenyellow; scrollbar-arrow-color:goldenrod; scrollbar-shadow-color:red; scrollbar-track-color:pink; } .content { /*要设的比.scolltable更宽一些*/ width: 600px; } </style> </head> <body> <div class="scolltable"> <div class="content"> 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容 </div> </div> </body> </html> 实现效果: IE 3.小结 本文主要是想记录下在Webkit内核的浏览器和IE中,如何自定义滚动条的样式,并分别提供了两个demo。如有问题,欢迎指正。 此文已由作者授权腾讯云+社区发布,更多原文请点击 搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!
原文:JavaScript:学习笔记(7)——VAR、LET、CONST三种变量声明的区别 JavaScript:学习笔记(7)——VAR、LET、CONST三种变量声明的区别 ES2015(ES6)带来了许多闪亮的新功能,自2017年以来,许多JavaScript开发人员已经熟悉并开始使用这些功能。虽然这种假设可能是正确的,但仍有可能其中一些功能对某些人来说仍然是一个谜。 ES6带来的一个新特性是新增了通过使用let、const来声明变量。在本文中,我们将讨论var,let和const的范围,使用和提升。在您阅读时,请注意它们之间的差异,我会指出。 VAR VAR的范围 范围本质是意味着这些变量可供使用的位置。var声明的范围是全局作用于或者本地函数作用域。当一个var变量声明在函数外面时它的作用域是全局的,这意味着在整个窗口中可以使用在函数块外部使用var声明的任何变量。var变量在函数内声明时是函数作用域。这意味着它可用,只能在该函数中访问。 我们举一个例子: <script> var greeter = "Hello"; function hello() { var hi = "Hi"; } </script> 在这里,greeter是全局范围的,因为它存在于函数外部,而hello是函数作用域。所以我们不能在函数外部访问变量hi。所以,如果我们这样做: var变量可以重新声明和更新 这个是比较好理解的 var变量提升 变量提升是什么意思呢?比如我们看下面这段代码 当f()执行后,输出的结果是什么?可能你会说是日期,因为函数f()中,虽然想对tmp进行字符串赋值,但是被if制止了,所以还是以前的Date类型,其实是错误的。这里涉及到一个问题,就是变量提升(Hoisting),它是一种JavaScript机制,它规定变量和函数声明在代码执行之前被移动到其作用域的顶部。所以上面代码等同于下面代码: 所以,结果就是undefined。 var变量带来的问题 var有一个弱点。我将使用下面的例子来解释这一点。 这段代码看起来是没有任何问题的,但是却是一个巨大的隐患,虽然如果您故意要求重新定义greeter,这不是问题,但如果您没有意识到之前已经定义过变量greeter,则会成为一个问题。如果您在代码的其他部分使用了greeter,那么您可能会对可能获得的输出感到惊讶。这可能会导致代码中出现很多错误。这就是let和const必要的原因。 也即是说,如果定义在全局作用域的var变量,极有可能对以前定义的同名变量进行覆盖,从而引发问题,而这一切我们都茫然不知。 LET 如果要定义变量,let现在是首选。毫不奇怪,因为它是对var声明的改进。它还解决了最后一个小标题中提出的这个问题。让我们考虑为什么会这样 Let是块作用域 块是由{}限定的代码块。一个块生活在花括号中。花括号内的任何东西都是块。因此,在带有let的块中声明的变量仅可在该块中使用。让我用一个例子解释一下。 这个是上面例子的改写,if语句构成了一个块,在外面我们无法访问hello,故报错。 Let可以更新但是不能重新声明 正如下面例子我们看到的,无法重新定义a变量。 在这里很明显两个a在同一个作用域下,如果我们放在不同的块中是可以的,但是切记这不是重新声明,他们隶属于不同的块,对每个块来说都是第一次定义: Let变量提升 就像var一样,让声明被提升到顶部。与初始化为undefined的var不同,let关键字未初始化。因此,如果您在声明之前尝试使用let变量,则会出现参考错误。 CONST 用const声明的变量保持常量值。 const声明与let声明共享一些相似之处。 const声明是块作用域 与let声明一样,const声明只能在声明的块中访问。 const无法更新或重新声明 因此,每个const变量必须在声明时初始化。 虽然无法更新const对象,但可以更新此对象的属性。因此,如果我们声明一个const对象 CONST变量提升 就像let一样,const声明被提升到顶部但未初始化。 总结 让我们来梳理一下三者的区别 var声明是全局作用域或函数作用域,而let和const是块作用域。 var变量可以在其范围内更新和重新声明;let变量可以更新但不能重新声明; const变量既不能更新也不能重新声明。 它们全部被提升到其范围的顶部,但是变量初始化为undefined时,let和const变量不会被初始化。 虽然可以声明var和let而不进行初始化,但必须在声明期间初始化const。
原文:.net core 控制台程序使用依赖注入(Autofac) 1、Autofac IOC 容器 ,便于在其他类获取注入的对象 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Autofac; using Autofac.Core; using Autofac.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; namespace BackToCOS.IoC { /// <summary> /// Autofac IOC 容器 /// </summary> public class AutofacContainer { private static ContainerBuilder _builder = new ContainerBuilder(); private static IContainer _container; private static string[] _otherAssembly; private static List<Type> _types = new List<Type>(); private static Dictionary<Type, Type> _dicTypes = new Dictionary<Type, Type>(); /// <summary> /// 注册程序集 /// </summary> /// <param name="assemblies">程序集名称的集合</param> public static void Register(params string[] assemblies) { _otherAssembly = assemblies; } /// <summary> /// 注册类型 /// </summary> /// <param name="types"></param> public static void Register(params Type[] types) { _types.AddRange(types.ToList()); } /// <summary> /// 注册程序集。 /// </summary> /// <param name="implementationAssemblyName"></param> /// <param name="interfaceAssemblyName"></param> public static void Register(string implementationAssemblyName, string interfaceAssemblyName) { var implementationAssembly = Assembly.Load(implementationAssemblyName); var interfaceAssembly = Assembly.Load(interfaceAssemblyName); var implementationTypes = implementationAssembly.DefinedTypes.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType && !t.IsNested); foreach (var type in implementationTypes) { var interfaceTypeName = interfaceAssemblyName + ".I" + type.Name; var interfaceType = interfaceAssembly.GetType(interfaceTypeName); if (interfaceType.IsAssignableFrom(type)) { _dicTypes.Add(interfaceType, type); } } } /// <summary> /// 注册 /// </summary> /// <typeparam name="TInterface"></typeparam> /// <typeparam name="TImplementation"></typeparam> public static void Register<TInterface, TImplementation>() where TImplementation : TInterface { _dicTypes.Add(typeof(TInterface), typeof(TImplementation)); } /// <summary> /// 注册一个单例实体 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="instance"></param> public static void Register<T>(T instance) where T:class { _builder.RegisterInstance(instance).SingleInstance(); } /// <summary> /// 构建IOC容器 /// </summary> public static IServiceProvider Build(IServiceCollection services) { if (_otherAssembly != null) { foreach (var item in _otherAssembly) { _builder.RegisterAssemblyTypes(Assembly.Load(item)); } } if (_types != null) { foreach (var type in _types) { _builder.RegisterType(type); } } if (_dicTypes != null) { foreach (var dicType in _dicTypes) { _builder.RegisterType(dicType.Value).As(dicType.Key); } } _builder.Populate(services); _container = _builder.Build(); return new AutofacServiceProvider(_container); } /// <summary> /// /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public static T Resolve<T>() { return _container.Resolve<T>(); } public static T Resolve<T>(params Parameter[] parameters) { return _container.Resolve<T>(parameters); } public static object Resolve(Type targetType) { return _container.Resolve(targetType); } public static object Resolve(Type targetType, params Parameter[] parameters) { return _container.Resolve(targetType, parameters); } } } 2、用nuget安装 using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; 3、Program类如下 1 using BackToCOS.IoC; 2 using log4net; 3 using Microsoft.Extensions.Configuration; 4 using Microsoft.Extensions.DependencyInjection; 5 using Microsoft.Extensions.Logging; 6 using Myvas.AspNetCore.TencentCos; 7 using System; 8 using System.IO; 9 using Topshelf; 10 11 namespace BackToCOS 12 { 13 class Program 14 { 15 static void Main(string[] args) 16 { 17 var configuration = new ConfigurationBuilder() 18 .SetBasePath(Directory.GetCurrentDirectory()) 19 .AddJsonFile("appsettings.json", true, true) 20 .AddJsonFile("appsettings.Development.json", true, true) 21 .Build(); 22 IServiceCollection services = new ServiceCollection(); 23 24 services.AddTencentCos(options => 25 { 26 options.SecretId = configuration["TencentCos:SecretId"]; 27 options.SecretKey = configuration["TencentCos:SecretKey"]; 28 }); 29 services.AddLogging(builder => builder 30 .AddConfiguration(configuration.GetSection("Logging")) 31 .AddConsole()); 32 //注入 33 services.AddSingleton<ITencentCosHandler, TencentCosHandler>(); 34 //用Autofac接管 35 AutofacContainer.Build(services); 36 log4net.Config.XmlConfigurator.ConfigureAndWatch(LogManager.CreateRepository("NETCoreRepository"), new FileInfo(AppDomain.CurrentDomain.BaseDirectory + "log4net.config")); 37 HostFactory.Run(x => 38 { 39 x.UseLog4Net(); 40 x.Service<BackupServiceRunner>(); 41 x.RunAsLocalSystem(); 42 x.SetDescription("备份到cos的服务"); 43 x.SetDisplayName("备份到cos的服务"); 44 x.SetServiceName("BackToCOS"); 45 x.EnablePauseAndContinue(); 46 }); 47 } 48 } 49 50 } 4、用容器获取事例(非构造函数) 1 using log4net; 2 using Microsoft.Extensions.Options; 3 using Myvas.AspNetCore.TencentCos; 4 using Quartz; 5 using System; 6 using System.Collections.Generic; 7 using System.Text; 8 using System.Threading.Tasks; 9 10 namespace BackToCOS.Jobs 11 { 12 //DisallowConcurrentExecution属性标记任务不可并行 13 [DisallowConcurrentExecution] 14 public class BackupJob : IJob 15 { 16 private readonly ILog _log = LogManager.GetLogger("NETCoreRepository", typeof(BackupJob)); 17 public Task Execute(IJobExecutionContext context) 18 { 19 try 20 { 21 _log.Info("测试任务,当前系统时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); 22 ITencentCosHandler tencentCosHandler = IoC.AutofacContainer.Resolve<ITencentCosHandler>(); 23 var ss = tencentCosHandler.AllBucketsAsync(); 24 return Task.CompletedTask; 25 } 26 catch (Exception ex) 27 { 28 JobExecutionException e2 = new JobExecutionException(ex); 29 _log.Error("测试任务异常", ex); 30 } 31 return Task.CompletedTask; 32 } 33 } 34 }
原文:四种mysql存储引擎 前言 数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以 获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySQL的核心就是存储引擎。 存储引擎查看 MySQL给开发者提供了查询存储引擎的功能,我这里使用的是MySQL5.1,可以使用: SHOW ENGINES 命令来查看MySQL使用的引擎,命令的输出为(我用的Navicat Premium): 看到MySQL给用户提供了这么多存储引擎,包括处理事务安全表的引擎和出来了非事物安全表的引擎。 如果要想查看数据库默认使用哪个引擎,可以通过使用命令: SHOW VARIABLES LIKE 'storage_engine'; 来查看,查询结果为: 在MySQL中,不需要在整个服务器中使用同一种存储引擎,针对具体的要求,可以对每一个表使用不同的存储引擎。Support列的值表示某种引擎是否能使用:YES表示可以使用、NO表示不能使用、DEFAULT表示该引擎为当前默认的存储引擎 。下面来看一下其中几种常用的引擎。 InnoDB存储引擎 InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,上图也看到了,InnoDB是默认的MySQL引擎。InnoDB主要特性有: 1、InnoDB给MySQL提供了具有提交、回滚和崩溃恢复能力的事物安全(ACID兼容)存储引擎。InnoDB锁定在行级并且也在SELECT语句中提供一个类似Oracle的非锁定读。这些功能增加了多用户部署和性能。在SQL查询中,可以自由地将InnoDB类型的表和其他MySQL的表类型混合起来,甚至在同一个查询中也可以混合 2、InnoDB是为处理巨大数据量的最大性能设计。它的CPU效率可能是任何其他基于磁盘的关系型数据库引擎锁不能匹敌的 3、InnoDB存储引擎完全与MySQL服务器整合,InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB将它的表和索引在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘文件)。这与MyISAM表不同,比如在MyISAM表中每个表被存放在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上 4、InnoDB支持外键完整性约束,存储表中的数据时,每张表的存储都按主键顺序存放,如果没有显示在表定义时指定主键,InnoDB会为每一行生成一个6字节的ROWID,并以此作为主键 5、InnoDB被用在众多需要高性能的大型数据库站点上 InnoDB不创建目录,使用InnoDB时,MySQL将在MySQL数据目录下创建一个名为ibdata1的10MB大小的自动扩展数据文件,以及两个名为ib_logfile0和ib_logfile1的5MB大小的日志文件 MyISAM存储引擎 MyISAM基于ISAM存储引擎,并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。MyISAM拥有较高的插入、查询速度,但不支持事物。MyISAM主要特性有: 1、大文件(达到63位文件长度)在支持大文件的文件系统和操作系统上被支持 2、当把删除和更新及插入操作混合使用的时候,动态尺寸的行产生更少碎片。这要通过合并相邻被删除的块,以及若下一个块被删除,就扩展到下一块自动完成 3、每个MyISAM表最大索引数是64,这可以通过重新编译来改变。每个索引最大的列数是16 4、最大的键长度是1000字节,这也可以通过编译来改变,对于键长度超过250字节的情况,一个超过1024字节的键将被用上 5、BLOB和TEXT列可以被索引 6、NULL被允许在索引的列中,这个值占每个键的0~1个字节 7、所有数字键值以高字节优先被存储以允许一个更高的索引压缩 8、每个MyISAM类型的表都有一个AUTO_INCREMENT的内部列,当INSERT和UPDATE操作的时候该列被更新,同时AUTO_INCREMENT列将被刷新。所以说,MyISAM类型表的AUTO_INCREMENT列更新比InnoDB类型的AUTO_INCREMENT更快 9、可以把数据文件和索引文件放在不同目录 10、每个字符列可以有不同的字符集 11、有VARCHAR的表可以固定或动态记录长度 12、VARCHAR和CHAR列可以多达64KB 使用MyISAM引擎创建数据库,将产生3个文件。文件的名字以表名字开始,扩展名之处文件类型:frm文件存储表定义、数据文件的扩展名为.MYD(MYData)、索引文件的扩展名时.MYI(MYIndex) MEMORY存储引擎 MEMORY存储引擎将表中的数据存储到内存中,未查询和引用其他表数据提供快速访问。MEMORY主要特性有: 1、MEMORY表的每个表可以有多达32个索引,每个索引16列,以及500字节的最大键长度 2、MEMORY存储引擎执行HASH和BTREE缩影 3、可以在一个MEMORY表中有非唯一键值 4、MEMORY表使用一个固定的记录长度格式 5、MEMORY不支持BLOB或TEXT列 6、MEMORY支持AUTO_INCREMENT列和对可包含NULL值的列的索引 7、MEMORY表在所由客户端之间共享(就像其他任何非TEMPORARY表) 8、MEMORY表内存被存储在内存中,内存是MEMORY表和服务器在查询处理时的空闲中,创建的内部表共享 9、当不再需要MEMORY表的内容时,要释放被MEMORY表使用的内存,应该执行DELETE FROM或TRUNCATE TABLE,或者删除整个表(使用DROP TABLE) 存储引擎的选择 不同的存储引擎都有各自的特点,以适应不同的需求,如下表所示: 功 能 MYISAM Memory InnoDB Archive 存储限制 256TB RAM 64TB None 支持事物 No No Yes No 支持全文索引 Yes No No No 支持数索引 Yes Yes Yes No 支持哈希索引 No Yes No No 支持数据缓存 No N/A Yes No 支持外键 No No Yes No 如果要提供提交、回滚、崩溃恢复能力的事物安全(ACID兼容)能力,并要求实现并发控制,InnoDB是一个好的选择 如果数据表主要用来插入和查询记录,则MyISAM引擎能提供较高的处理效率 如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存中的Memory引擎,MySQL中使用该引擎作为临时表,存放查询的中间结果 如果只有INSERT和SELECT操作,可以选择Archive,Archive支持高并发的插入操作,但是本身不是事务安全的。Archive非常适合存储归档数据,如记录日志信息可以使用Archive 使用哪一种引擎需要灵活选择,一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求,使用合适的存储引擎,将会提高整个数据库的性能
原文:MySQL - 常见的三种数据库存储引擎 数据库存储引擎:是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySQL的核心就是插件式存储引擎。 查看存储引擎: 我们可以用SHOW ENGINES; 来查询数据库的存储引擎。 MySQL给用户提供了许多不同的存储引擎。在MySQL中,不需要在整个服务器中使用同一种存储引擎,针对具体的要求,可以对每一个表使用不同的存储引擎。Support列的值表示某种引擎是否能使用:YES表示可以使用、NO表示不能使用、DEFAULT表示该引擎为当前默认的存储引擎。 我们也可以通过使用命令来查看数据库默认使用的引擎:SHOW VARIABLES LIKE 'storage_engine'; 下面来看一下其中几种常用的引擎。 l InnoDB存储引擎 InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID),其它存储引擎都是非事务安全表,支持行锁定和外键,MySQL5.5以后默认使用InnoDB存储引擎。 InnoDB主要特性 为MySQL提供了具有提交、回滚和崩溃恢复能力的事务安全(ACID兼容)存储引擎。InnoDB锁定在行级并且也在 SELECT语句中提供一个类似Oracle的非锁定读。这些功能增加了多用户部署和性能。在SQL查询中,可以自由地将InnoDB类型的表和其他MySQL的表类型混合起来,甚至在同一个查询中也可以混合。 InnoDB表的自动增长列可以手工插入,但是插入的如果是空或0,则实际插入到则是自动增长后到值。可以通过"ALTER TABLE...AUTO_INCREMENT=n;"语句强制设置自动增长值的起始值,默认为1,但是该强制到默认值是保存在内存中,数据库重启后该值将会丢失。可以使用LAST_INSERT_ID()查询当前线程最后插入记录使用的值。如果一次插入多条记录,那么返回的是第一条记录使用的自动增长值。 对于InnoDB表,自动增长列必须是索引。如果是组合索引,也必须是组合索引的第一列,但是对于MyISAM表,自动增长列可以是组合索引的其他列,这样插入记录后,自动增长列是按照组合索引到前面几列排序后递增的。 MySQL支持外键的存储引擎只有InnoDB,在创建外键的时候,父表必须有对应的索引,子表在创建外键的时候也会自动创建对应的索引。在创建索引的时候,可以指定在删除、更新父表时,对子表进行的相应操作,包括restrict、cascade、set null和no action。其中restrict和no action相同,是指限制在子表有关联的情况下,父表不能更新;casecade表示父表在更新或删除时,更新或者删除子表对应的记录;set null 则表示父表在更新或者删除的时候,子表对应的字段被set null。 当某个表被其它表创建了外键参照,那么该表对应的索引或主键被禁止删除。 可以使用set foreign_key_checks=0;临时关闭外键约束,setforeign_key_checks=1;打开约束。 InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB将它的表和索引在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘文件)。这与MyISAM表不同,比如在MyISAM表中每个表被存放在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上。 InnoDB支持外键完整性约束,存储表中的数据时,每张表的存储都按主键顺序存放,如果没有显示在表定义时指定主键,InnoDB会为每一行生成一个6字节的ROWID,并以此作为主键。 使用 InnoDB存储引擎 MySQL将在数据目录下创建一个名为 ibdata1的10MB大小的自动扩展数据文件,以及两个名为 ib_logfile0和 ib_logfile1的5MB大小的日志文件 l MyISAM存储引擎 MyISAM基于ISAM存储引擎,并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。MyISAM拥有较高的插入、查询速度,但不支持事务,不支持外键。 MyISAM主要特性: 被大文件系统和操作系统支持。 当把删除和更新及插入操作混合使用的时候,动态尺寸的行产生更少碎片。这要通过合并相邻被删除的块,若下一个块被删除,就扩展到下一块自动完成。 每个MyISAM表最大索引数是64,这可以通过重新编译来改变。每个索引最大的列数是16。 最大的键长度是1000字节,这也可以通过编译来改变,对于键长度超过250字节的情况,一个超过1024字节的键将被用上。 BLOB和TEXT列可以被索引。 NULL被允许在索引的列中,这个值占每个键的0~1个字节。 所有数字键值以高字节优先被存储以允许一个更高的索引压缩。 每个MyISAM类型的表都有一个AUTOINCREMENT的内部列,当INSERT和UPDATE操作的时候该列被更新,同时AUTOINCREMENT列将被刷新。所以说,MyISAM类型表的AUTOINCREMENT列更新比InnoDB类型的AUTOINCREMENT更快。 数据文件和索引文件可以放置在不同的目录,平均分配IO,获取更快的速度。要指定数据文件和索引文件的路径,需要在创建表的时候通过DATA DIRECTORY和INDEX DIRECTORY语句指定,文件路径需要使用绝对路径。 每个MyISAM表都有一个标志,服务器或myisamchk程序在检查MyISAM数据表时会对这个标志进行设置。MyISAM表还有一个标志用来表明该数据表在上次使用后是不是被正常的关闭了。如果服务器以为当机或崩溃,这个标志可以用来判断数据表是否需要检查和修复。如果想让这种检查自动进行,可以在启动服务器时使用--myisam-recover现象。这会让服务器在每次打开一个MyISAM数据表是自动检查数据表的标志并进行必要的修复处理。MyISAM类型的表可能会损坏,可以使用CHECK TABLE语句来检查MyISAM表的健康,并用REPAIR TABLE语句修复一个损坏到MyISAM表。 每个字符列可以有不同的字符集。 有VARCHAR的表可以固定或动态记录长度。 VARCHAR和CHAR列可以多达64KB。 使用MyISAM引擎创建数据库,将产生3个文件。文件的名字以表名字开始,扩展名之处文件类型:frm文件存储表定义、数据文件的扩展名为.MYD(MYData)、索引文件的扩展名时.MYI(MYIndex)。 MyISAM的表支持3种不同的存储格式:静态(固定长度)表,动态表,压缩表 静态表是默认的存储格式。静态表中的字段都是非变长字段,这样每个记录都是固定长度的,这种存储方式的优点是存储非常迅速,容易缓存,出现故障容易恢复;缺点是占用的空间通常比动态表多。静态表在数据存储时会根据列定义的宽度定义补足空格,但是在访问的时候并不会得到这些空格,这些空格在返回给应用之前已经去掉。同时需要注意:在某些情况下可能需要返回字段后的空格,而使用这种格式时后面到空格会被自动处理掉。 动态表包含变长字段,记录不是固定长度的,这样存储的优点是占用空间较少,但是频繁到更新删除记录会产生碎片,需要定期执行OPTIMIZE TABLE语句或myisamchk -r命令来改善性能,并且出现故障的时候恢复相对比较困难。 压缩表由myisamchk工具创建,占据非常小的空间,因为每条记录都是被单独压缩的,所以只有非常小的访问开支。 l MEMORY存储引擎 MEMORY存储引擎将表中的数据存储到内存中,为查询和引用其他表数据提供快速访问。 MEMORY主要特性: MEMORY表的每个表可以有多达32个索引,每个索引16列,以及500字节的最大键长度。 可以在一个MEMORY表中有非唯一键值。 MEMORY支持AUTO_INCREMENT列和对可包含NULL值的列的索引。 MEMORY表在所由客户端之间共享(就像其他任何非TEMPORARY表)。 MEMORY表内存被存储在内存中,内存是MEMORY表和服务器在查询处理时的空闲中,创建的内部表共享。 默认情况下,MEMORY数据表使用散列索引,利用这种索引进行“相等比较”非常快,但是对“范围比较”的速度就慢多了。因此,散列索引值适合使用在"="和"<=>"的操作符中,不适合使用在"<"或">"操作符中,也同样不适合用在order by字句里。如果确实要使用"<"或">"或betwen操作符,可以使用btree索引来加快速度。 存储在MEMORY数据表里的数据行使用的是固定长度的格式,因此加快处理速度,这意味着不能使用BLOB和TEXT这样的长度可变的数据类型。VARCHAR是一种长度可变的类型,但因为它在MySQL内部当作长度固定不变的CHAR类型,所以也可以使用。 create table tab_memoryengine=memory select id,name,age,addr from man order by id; 使用USING HASH/BTREE来指定特定到索引。 create index mem_hash using hashon tab_memory(city_id); 在启动MySQL服务的时候使用--init-file选项,把insert into...select或load data infile 这样的语句放入到这个文件中,就可以在服务启动时从持久稳固的数据源中装载表。 每个MEMORY表中放置到数据量的大小,受到max_heap_table_size系统变量的约束,这个系统变量的初始值是16M,同时在创建MEMORY表时可以使用MAX_ROWS子句来指定表中的最大行数。 每个MEMORY表实际对应一个磁盘文件,格式是.frm。MEMORY类型的表访问非常快,因为它到数据是放在内存中的,并且默认使用HASH索引,但是一旦服务器关闭,表中的数据就会丢失,但表还会继续存在。 服务器需要足够的内存来维持所在的在同一时间使用的MEMORY表,当不再需要MEMORY表的内容时,要释放被MEMORY表使用的内存,应该执行 DELETE FROM或 TRUNCATE TABLE,或者删除整个表(使用DROP TABLE)。 l 存储引擎的选择 在实际工作中,选择一个合适的存储引擎是一个比较复杂的问题。每种存储引擎都有自己的优缺点,不能笼统地说谁比谁好。 存储引擎的对比 特性 InnoDB MyISAM MEMORY 事务安全 支持 无 无 存储限制 64TB 有 有 空间使用 高 低 低 内存使用 高 低 高 插入数据的速度 低 高 高 对外键的支持 支持 无 无 InnoDB: 支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。 MyISAM: 插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以使用。 MEMORY: 所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。 同一个数据库也可以使用多种存储引擎的表。如果一个表要求比较高的事务处理,可以选择InnoDB。这个数据库中可以将查询要求比较高的表选择MyISAM存储。如果该数据库需要一个用于查询的临时表,可以选择MEMORY存储引擎。 若要修改默认引擎,可以修改配置文件中的default-storage-engine。可以通过:show variables like 'default_storage_engine';查看当前数据库到默认引擎。命令:show engines和show variables like 'have%'可以列出当前数据库所支持到引擎。其中Value显示为disabled的记录表示数据库支持此引擎,而在数据库启动时被禁用。在MySQL5.1以后,INFORMATION_SCHEMA数据库中存在一个ENGINES的表,它提供的信息与show engines;语句完全一样,可以使用下面语句来查询哪些存储引擎支持事物处理:select engine from information_chema.engines where transactions ='yes'; 可以通过engine关键字在创建或修改数据库时指定所使用到引擎。 在创建表的时候通过engine=...或type=...来指定所要使用的引擎。show table status from DBname来查看指定表的引擎。
原文:MySQL 在高并发下的 订单撮合 系统使用 共享锁 与 排他锁 保证数据一致性 作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:http://www.cnblogs.com/linguanh/ GitHub : https://github.com/af913337456/ 腾讯云专栏: https://cloud.tencent.com/developer/user/1148436/activities 虫洞区块链专栏:https://www.chongdongshequ.com/article/1536563643883.html 前序 距离上次择文发表,两月余久。2018年也即将要结束了,目前的工作依然是与区块链应用相关的,也很荣幸在9月初受邀签约出版暂名为《区块链以太坊DApp实战开发》一书,预计在明年年初出版。 这次让我有感记录这篇文章的原因是最近在使用Go语言重写一个原来由PHP语言编写的交易所订单撮合模块的时候,发现订单撮合的部分代码在撮合的时候,为保证各表数据在并发情况下不出现读写脏乱而采用了全局锁表的操作。后面我采用了共享锁的形式进行了修改,于刚刚重写完,并进行了并发单元测试,表现正常。 目录 场景描述 解决问题 订单撮合实例 共享锁 与 排他锁 前置知识 行锁与表锁 两种行锁的特点 两种行锁的加锁方式 锁的释放 操作例子 改造代码片段 场景描述 高并发的业务常见是有很多种类的,最常见的例如秒杀抢购。它们都有一个共同的特点就是数据更新都比较频繁,通常涉及到多张业务表的增改操作,且表格越多的,要考虑的问题也越多。 订单撮合可以理解为订单买卖,拿这个为例子进行列举一个可能会导致数据错乱的情形。假设现在买卖手机,A用户是要买手机的,B用户是卖手机的。A的买入单订单1,和B的卖出单订单2,订单2卖出手机,一台手机卖1000元。此时A的网上的钱包余额是1001元,刚好比手机价格高,是可以成交的。 此时记录用户钱包钱数数量的是一张数据表。每次花费了钱或者增加了钱,都要更新这个表。 当这两笔订单进入到系统里面进行撮合。假设系统的订单撮合运行流程如下图所示: 当判断条件进行A用户的钱包余额判断的时候,发现 1001 > 1000,结果是通过,此时准备进入“进行记录更细”步骤。但是,就在这个过程之中的时间差中,A用户使用了系统的网上提现功能,并成功转出了10元,剩余的是1001 - 10 = 991元。但是由于撮合系统的余额判断过程以及通过了,导致下面的交易流程依然能进行,最终A用991元买了B的1000元售价的手机。 解决问题 上述的常见问题是一个很简单的模型,现实的系统中往往是更复杂的。但是它所体现出的问题却是真实存在的,对于这类问题,有很多解决方案。其中,就可以考虑使用数据库的锁。 本文要介绍的是MySQL数据库的共享锁 与 排他锁,其它的不作说明或引申。 订单撮合实例 下面的截图就是我所重写好的撮合系统原始的PHP代码,所使用了表锁的方式来解决前面的并发读写导致数据脏乱的问题。这种方式虽然是解决了问题,但是导致了性能低下的问题。 共享锁 与 排他锁 前置知识: MySQL 是数据库,不是数据库引擎 MySQL有两种常用存储引擎: MyISAM和InnoDB MyISAM不支持事务操作,InnoDB支持事务操作 MySQL 的锁分有 行锁 和 表锁 MyISAM 只有表锁 Innodb 行锁,表锁都有 行锁中有共享锁和排他锁 共享锁 简称 S锁,排他锁简称 X锁 行锁与表锁 简述: 行锁,锁的是表中对应的行,只限制当前行的读写。 表锁,锁的是整张表,限制的是整张表的数据读写。 比较: 行锁,计算机资源开销大,加锁慢;会出现死锁;锁定粒度最小,锁冲突的概率最低,并发度最高,性能高。 表锁,计算机资源开销小,加锁快;不会出现死锁;锁定粒度大,锁冲突的概率最高,并发度最低,性能低。 两种行锁的特点 共享锁 A 对数据 B 加了 共享锁,A能读取和修改数据B,C 等其它只能读取数据B,但是不能修改。直至A释放了B的锁。 排他锁 A 对数据 B 加了 排他锁,A能读取和修改数据B,C 等其它不能再对数据B加其它的锁。直观体验是不能修改,不能使用含有加锁动作的select读取。 两种行锁的加锁方式 要注意的是: 行锁的实现SQL语句中必须要有索引的限制条件,例如含有 where id=xxx 这类语句。 行锁的实现SQL语句没有索引限制条件会变成表锁 InnoDB引擎 默认的修改数据类SQL语句,update,delete,insert等,都会自动给涉及到的数据加上排他锁。 共享锁 select 的添加可以使用满足格式:select ... where 索引限制 lock in share mode 的语句。例如“select name from lgh_user where id = 1 lock in share model” 此时 id 是索引。 排他锁 满足格式:select ... where 索引限制 for update 的语句 锁的释放 非事务(Transaction) 中,语句执行完毕,便释放锁。 行锁在事务 (Transaction) 中,只有等到当前的事务Transaction 进行了 commit 或 roll back,锁才能释放。 操作例子 演示事务 tx 中的例子,文字解析见图。 改造代码片段 撮合中的所有表锁替换成了共享锁,运行其它业务读取所锁的行数据,在当前事务的批量操作还没结束之前,不允许修改。 完
原文:【ABP框架系列学习】模块系统(4) 0.引言 ABP提供了构建模块和通过组合模块以创建应用程序的基础设施。一个模块可以依赖于另外一个模块。通常,程序集可以认为是模块。如果创建多个程序集的应用程序,建议为每个程序集创建模块定义。 当前,模块系统主要集中在服务器,而不是客户端。 1.模块定义 模块是从ABP包中的AbpModule派生的类定义的。比如说开发一个可以用于不同应用程序的博客模块(Blog Module)。最简单的模块定义如下 : public class MyBlogApplicationModule : AbpModule { public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } } 模块定义类负责通过依赖注入注册类,如有必要(可以像上述事例按惯例完成)。它还可以配置应用程序和其它模块,给应用程序增加新的功能等等。 2.生命周期方法 ABP在程序启动和关闭时调用模块一些特定的方法。你可以重写这些方法以执行某些特定的任务。 ABP按照依赖顺序调用这些方法。如果模块A依赖模块B,那么模块B在模块A之前初始化。 启动方法执行准确的顺序:PreInitialize-B, PreInitialize-A, Initialize-B, Initialize-A, PostInitialize-B, PostInitialize-A。对于所有依赖关系图都是如此。关闭方法也是类似的,但顺序相反。 相关源码:模块启动时依次执行PreInitialize()、Initialize()、PostInitialize(),模块关闭时首先Reverse()、然后在逐个模块Shutdown()。 public virtual void StartModules() { var sortedModules = _modules.GetSortedModuleListByDependency(); sortedModules.ForEach(module => module.Instance.PreInitialize()); sortedModules.ForEach(module => module.Instance.Initialize()); sortedModules.ForEach(module => module.Instance.PostInitialize()); } public virtual void ShutdownModules() { Logger.Debug("Shutting down has been started"); var sortedModules = _modules.GetSortedModuleListByDependency(); sortedModules.Reverse(); sortedModules.ForEach(sm => sm.Instance.Shutdown()); Logger.Debug("Shutting down completed."); } PreInitialize 当应用程序启动时,首先调用该方法。它是框架和其它模块初始化之前配置它们的首选方法。 你还可以在该方法中编写特定的代码,以便在依赖注入注册之前运行。例如,如果你创建一个传统的注册类,那么你应在该方法中使用IOCManager.AddConventionalRegisterer方法注册它们。 Initialize 该方法是依赖注入注册的地方,通过使用IocManager.RegisterAssemblyByConvention方法完成注册。如果想定义自定义的依赖注册,请见后续依赖注入章节。 PostInitialize 该方法在程序启动的最后调用。在这里解析依赖是安全的。 Shutdown 该方法在程序关闭时调用。 3.模块依赖(Module Dependencies) 一个模块可以依赖于另外的模块。你可以通过DependsOn特性显示声明依赖项,如下代码: [DependsOn(typeof(MyBlogCoreModule))] public class MyBlogApplicationModule : AbpModule { public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } } 上述事例代码中,声明了MyBlogApplicationModule模块依赖于MyBlogCoreModule模块,那么MyBlogCoreModule模块应该在MyBlogApplicationModule模块之前完成初始化。 ABP可以从启动模块(start module)开始就递归的解析依赖关系,并相应地初始化它们。启动模块(start module)是最后进行初始化的模块。 4.插件模块 虽然模块从启动模块开始查找并遍历依赖关系,ABP还可以动态加载模块。AbpBootstrapper类中定义了PlugInSources属性,该属性可用于向动态加载的插件模块添加源。插件源可以是实现IPlugInSource接口的任何类。通过实现FolderPlugInSource类以从指定文件夹中的程序集获取插件模块。 ASP.NET CORE ABP中ASP.NET CORE模块在AddAbp扩展方法中定义选项,用于在启动类中添加插件源: services.AddAbp<MyStartupModule>(options => { options.PlugInSources.Add(new FolderPlugInSource(@"C:\MyPlugIns")); }); 也可以使用更简单的语法AddFolder扩展方法: services.AddAbp<MyStartupModule>(options => { options.PlugInSources.AddFolder(@"C:\MyPlugIns"); }); ASP.NET MVC,Web API 对于传统的ASP.NET MVC应用程序,可以通过重写global.asax文件中Application_Start方法添加插件文件夹,如下代码: public class MvcApplication : AbpWebApplication<MyStartupModule> { protected override void Application_Start(object sender, EventArgs e) { AbpBootstrapper.PlugInSources.AddFolder(@"C:\MyPlugIns"); //... base.Application_Start(sender, e); } } Controllers in PlugIns 如果你的模块包括MVC或Web API Controolers,ASP.NET不能查找你的控制器。为了克服这个问题,你可以修改global.asax文件,如下代码: using System.Web; using Abp.PlugIns; using Abp.Web; using MyDemoApp.Web; [assembly: PreApplicationStartMethod(typeof(PreStarter), "Start")] namespace MyDemoApp.Web { public class MvcApplication : AbpWebApplication<MyStartupModule> { } public static class PreStarter { public static void Start() { //... MvcApplication.AbpBootstrapper.PlugInSources.AddFolder(@"C:\MyPlugIns\"); MvcApplication.AbpBootstrapper.PlugInSources.AddToBuildManager(); } } } 附加程序集(Additional Assemblies) 默认实现IAssemblyFinder和ITypeFinder接口只能在这些程序集中查找模块程序集和类型。也可以在模块中重写GetAdditionalAssembliesy方法来包括其它程序集。 自定义模块方法(Custom Module Methods) 你的模块还可以拥有自定义的方法,并能在依赖于这个模块的其它模块中调用这个方法。假设MyModule2依赖于MyModule1,并想在PreInitialize方法中调用MyModule1模块中的方法。 public class MyModule1 : AbpModule { public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } public void MyModuleMethod1() { //this is a custom method of this module } } [DependsOn(typeof(MyModule1))] public class MyModule2 : AbpModule { private readonly MyModule1 _myModule1; public MyModule2(MyModule1 myModule1) { _myModule1 = myModule1; } public override void PreInitialize() { _myModule1.MyModuleMethod1(); //Call MyModule1's method } public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } } 在上述代码中,通过构造函数把MyModule1注入到MyModule2,所以MyModule2可以调用MyModule1中的自定义方法,前提是MyModule2依赖于MyModule1。 模块配置(Module Configuration) 然而自定义方法可以用来配置模块,建议使用启动配置(startup configuration)系统来定义和设置模块的配置。 模块生命周期(Module Lifetime) 模块类自动注册为单实例对象(singleton)。
1.配置管理器内启动TCP/IP协议(端口改为1433)以及加入防火墙允许 2.进入本地实例: cmd Microsoft Windows [版本 6.3.9600] (c) 2013 Microsoft Corporation。保留所有权利。 C:\Users\Administrator>sqlcmd -S .\SQLEXPRESS 1> select @@version 2> GO -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ------------------------------------------------------------ Microsoft SQL Server 2017 (RTM) - 14.0.1000.169 (X64) Aug 22 2017 17:04:49 Copyright (C) 2017 Microsoft Corporation Express Edition (64-bit) on Windows Server 2012 R2 Datacenter 6.3 <X64> (Build 9600: ) (Hypervisor) (1 行受影响) 1> 2.使用DAC允许远程连接 1> sp_configure 'remote admin connections',1 2> GO 配置选项 'remote admin connections' 已从 0 更改为 1。请运行 RECONFIGURE 语句进行 安装。 1> RECONFIGURE; 2> select @@version 3> GO
原文:实现网站页面的QQ临时会话,分享到空间微博等按钮. 一 qq临时会话 要实现qq临时会话首先要到qq在线状态官网开通qq在线状态,其中临时对话也分为加密和未加密。 1.1:加密模式 <a target="_blank" href="http://sighttp.qq.com/authd?IDKEY=9a2ea740a2af0f88c15eb511395e2460bd84bf549dd66365"><img border="0" src="http://wpa.qq.com/imgd?IDKEY=9a2ea740a2af0f88c15eb511395e2460bd84bf549dd66365&pic=41 &r=0.7254801596980349" alt="点击这里给我发消息" title="点击这里给我发消息"></a> 1.2:未加密模式 非加密代码 <a target="_blank" href="http://wpa.qq.com/msgrd?v=3&uin=1195593066&site=qq&menu=yes"><img border="0" src="http://wpa.qq.com/pa?p=2:1195593066:41" alt="点击这里给我发消息" title="点击这里给我发消息"></a> 效果:
原文:【快速入门ORM框架之Dapper】大牛勿进系列 前言:dapper是什么?Dapper是.NET下一个micro的ORM,它和Entity Framework或Nhibnate不同,属于轻量级的,并且是半自动的。也就是说实体类都要自己写。它没有复杂的配置文件,一个单文件就可以了。 使用:在项目中nuget下载即可,dapper如何使用呢?它和EF不同,我们什么的配置都需要手写,连接配置,实体,上下文,这和ADO.NET有一点相似点。 实战: 配置Users表 create database TextInfo USE TEXTINFO create table Users ( UserID INT IDENTITY(1,1) NOT NULL, UserName varchar(50) Null, Email [varchar](100) Null, [Address] [varchar](100) Null ) 创建连接字符串 <connectionStrings> <add name="CnnhoRechargePlatformConnectionString" connectionString="Data Source=DESKTOP-OEJGKOO;Initial Catalog=TextInfo;Integrated Security=True"/> </connectionStrings> IDbConnection说明 //读取config中的字符串 string connstr = ConfigurationManager.ConnectionStrings["CnnhoRechargePlatformConnectionString"].ToString(); //IDbConnection IDbConnection connection = new SqlConnection(connstr); 我们都知道Sqlconnection继承了DbConnection,那我们再去DbConnection中一探究竟,发现DbConnection继承于IDbConnection,所以呢dapper就是通过这种方式进行了多数据的封装,dapper就是实现了这种接口支持多数据库的特性也就产生了! Insert插入与InsertBulk insert: var result = connection.Execute("insert into Users values(@UserName,@Email,@Address)",new { UserName = "zara",Email="zaranet@163.com",Address="上海浦东"}); 在这里支持匿名对象传入进行插入,非常的人性化哈!你可以发现dapper我们又开始写sql了,没错,这个博主上面没有提到,我们从微软的高度封装中逃了出来?可以这么比喻!对吧。。 insertBulk: 既然是Bulk操作,那一定是批量操作了,我们就可以把这个匿名对象编程匿名对象集合,这样就OK了 var userList = Enumerable.Range(0, 10).Select(i => new { Email = "zaranet@qq.com", Address = "安徽", UserName = "jack" }); var result = connection.Execute("insert into Users values(@UserName,@Email,@Address)",userList); 这样我们就插入了10个,这种操作为了demo,其道理也是非常得离谱。 Query(select) 为了查询我们定义一个UserModel public class User { public int UserID { get; set; } public string UserName { get; set; } public string Email { get; set; } public string Address { get; set; } } var query = connection.Query<User>("select * from Users where UserName = @userName",new { UserName = "zara" }); Update(Edit) 这个操作和上面的几乎没什么差别就是改了下sql var query = connection.Query<User>("update Users set UserName='zzh' where UserName = @UserName",new { UserName = "zara" }); Delete 这个操作和上面也差不多,非常简单的哈。 var query = connection.Query<User>("delete * from Users where UserName = @UserName",new { UserName = "zara" }); 好了,希望你看完本片文章会让你对dapper有一个基本的认识,如果觉得写的可以话,点个赞吧!
原文:8分钟学会使用AutoMapper 一.什么是AutoMapper与为什么用它。 它是一种对象与对象之间的映射器,让AutoMapper有意思的就是在于它提供了一些将类型A映射到类型B这种无聊的实例,只要B遵循AutoMapper已经建立的惯例,那么大多数情况下就可以进行相互映射了。 二.如何使用? 直接nuget install-package automapper 简单到不能再简单了。 三.入门 定义了连个简单的Model: public class Destination { public string name { get; set; } public string InfoUrl { get; set; } } public class Source { public string name { get; set; } public string InfoUrl { get; set; } public string aa { get; set; } } static void Main(string[] args) { Destination des = new Destination() { InfoUrl = "www.cnblogs.com/zaranet", name ="张子浩" }; Mapper.Initialize(x => x.CreateMap<Destination, Source>()); Source source = AutoMapper.Mapper.Map<Source>(des); Console.WriteLine(source.InfoUrl); } Initialize方法是Mapper的初始化,里面可以写上CreateMap表达式,具体是谁和谁进行匹配。在之后就可以直接进行一个获取值的过程了,非常的简单。 四.映射前后操作 偶尔有的时候你可能需要在映射的过程中,你需要执行一些逻辑,这是非常常见的事情,所以AutoMapper给我们提供了BeforeMap和AfterMap两个函数。 Mapper.Initialize(x => x.CreateMap<Destination, Source>().BeforeMap( (src,dest)=>src.InfoUrl ="https://"+src.InfoUrl).AfterMap( (src,dest)=>src.name="真棒"+src.name)); 其中呢,src是Destination对象,dest是Source,你呢就可以用这两个对象去获取里面的值,说白了这就是循环去找里面的值了。 五.条件映射 Mapper.Initialize(x => x.CreateMap<Destination, Source>().ForMember(dest => dest.InfoUrl,opt => opt.Condition(dest => dest.InfoUrl == "www.cnblogs.com/zaranet1")).ForMember(...(.ForMember(...)))); 在条件映射中,通过ForMember函数,参数是一个委托类型Fun<>,其里面呢也是可以进行嵌套的,但一般来说一个就够用了。 六.AutoMapper配置 初始化配置是非常受欢迎的,每个领域都应该配置一次。 //初始化配置文件 Mapper.Initialize(cfg => { cfg.CreateMap<Aliens, Person>(); }); 但是像这种情况呢,如果是多个映射,那么我们只能用Profile来配置,组织你的映射配置,并将配置放置到构造函数中(这种情况是5.x以上的版本),一个是以下的版本,已经被淘汰了。 5.0及以上版本: public class AliensPersonProfile : Profile { public AliensPersonProfile() { CreateMap<Destination, Source>(); } } 5.0以下版本(在早期版本中,使用配置方法而不是构造函数。 从版本5开始,Configure()已经过时。 它将在6.0中被删除。) public class OrganizationProfile : Profile { protected override void Configure() { CreateMap<Foo, FooDto>(); } } 然后在程序启动的时候初始化即可。 Mapper.Initialize(x=>x.AddProfile<AliensPersonProfile>()); 七.AutoMapper的最佳实践 上文中已经说到了AutoMapper的简单映射,但是在实际项目中,我们应该有很多类进行映射,这么多的映射应该怎么组织,这是一个活生生的问题,这成为主映射器。 在主映射器中,组织了多个小映射器,Configuration为我们的静态配置入口类;Profiles文件夹为我们所有Profile类的文件夹。如果是MVC,我们需要在Global中调用,那我的这个是控制台的。 public static void Configure() { Mapper.Initialize(cfg => { cfg.AddProfile<DestinationSourceProfile>(); cfg.AddProfile(new StudentSourceProfile()); }); } 其中添加子映射,可以用以上两种方式。 public void Configuration(IAppBuilder app) { AutoMapperConfiguration.Configure(); } 八.指定映射字段 在实际业务环境中,你不可能说两个类的字段是一 一 对应的,这个时候我们就要对应它们的映射关系。 public class CalendarEvent { public DateTime Date { get; set; } public string Title { get; set; } } public class CalendarEventForm { public DateTime EventDate { get; set; } public int EventHour { get; set; } public int EventMinute { get; set; } public string DisplayTitle { get; set; } } 在这两个类中,CalendarEvent的Date将被拆分为CalendarEventForm的日期、时、分三个字段,Title也将对应DisplayTitle字段,那么相应的Profile定义如下: public class CalendarEventProfile : Profile { public CalendarEventProfile() { CreateMap<CalendarEvent, CalendarEventForm>() .ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.Date.Date)) .ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.Date.Hour)) .ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Date.Minute)) .ForMember(dest => dest.DisplayTitle, opt => opt.MapFrom(src => src.Title)); } } main方法通过依赖注入,开始映射过程,下图是代码和图。 static void Main(string[] args) { CalendarEvent calendar = new CalendarEvent() { Date = DateTime.Now, Title = "Demo Event" }; AutoMapperConfiguration.Configure(); CalendarEventForm calendarEventForm = Mapper.Map<CalendarEventForm>(calendar); Console.WriteLine(calendarEventForm); } 那么最后呢,如果是反向的映射,一定回缺少属性,那么就你就可以obj.属性进行赋值。 附AutoHelper封装类 /// <summary> /// AutoMapper扩展帮助类 /// </summary> public static class AutoMapperHelper { /// <summary> /// 类型映射 /// </summary> public static T MapTo<T>(this object obj) { if (obj == null) return default(T); Mapper.CreateMap(obj.GetType(), typeof(T)); return Mapper.Map<T>(obj); } /// <summary> /// 集合列表类型映射 /// </summary> public static List<TDestination> MapToList<TDestination>(this IEnumerable source) { foreach (var first in source) { var type = first.GetType(); Mapper.CreateMap(type, typeof(TDestination)); break; } return Mapper.Map<List<TDestination>>(source); } /// <summary> /// 集合列表类型映射 /// </summary> public static List<TDestination> MapToList<TSource, TDestination>(this IEnumerable<TSource> source) { //IEnumerable<T> 类型需要创建元素的映射 Mapper.CreateMap<TSource, TDestination>(); return Mapper.Map<List<TDestination>>(source); } /// <summary> /// 类型映射 /// </summary> public static TDestination MapTo<TSource, TDestination>(this TSource source, TDestination destination) where TSource : class where TDestination : class { if (source == null) return destination; Mapper.CreateMap<TSource, TDestination>(); return Mapper.Map(source, destination); } /// <summary> /// DataReader映射 /// </summary> public static IEnumerable<T> DataReaderMapTo<T>(this IDataReader reader) { Mapper.Reset(); Mapper.CreateMap<IDataReader, IEnumerable<T>>(); return Mapper.Map<IDataReader, IEnumerable<T>>(reader); } } }
原文:ABP框架 - 介绍 文档目录 本节内容: 简介 一个快速示例 其它特性 启动模板 如何使用 简介 我们总是对不同的需求开发不同的应用。但至少在某些层面上,一次又一次地重复实现通用的和类似的功能。如:授权,验证,异常处理,日志,本地化,数据库连接管理,设置管理,审核日志等功能。所以我们创建架构和最佳实践,如分层和模块架构,DDD,依赖注入等,并尝试开发应用时基于一些约定。 由于所有这些是非常耗时而且很难单独创建并可适用于每个项目,许多公司创建自己的框架,他们用自己的框架能快速开发新应用而且不出错。但不是所有的公司都是幸运的,大部分公司没有时间,预算和团队来开发好的框架。他们甚至都没有可能创建一个框架,因为编写文档,培训开发人员和维护框架都是非常困难的。 ASP.NET Boilerplate (ABP) 是一个开源并且有丰富文档的应用框架,开发宗旨是:“为所有公司,所有开发人员,开发出一个通用框架!”,而且不只是一个框架,同时提供一个强大的基于DDD的构架模型和最佳实践。 一个快速示例 让我们从一个简单的类来体会一下ABP带来的便利: public class TaskAppService:ApplicationService,ITaskAppService { private readonly IRepository<Task> _taskRepository; public TaskAppService(IRepository<Task> taskRepository) { _taskRepository = taskRepository; } [AbpAuthorize(MyPermissions.UpdatingTasks)] public async Task UpdateTask(UpdateTaskInput input) { Logger.Info("Updating a task for input: " + input); var task = await _taskRepository.FirstOrDefaultAsync(input.TaskId); if (task == null) { throw new UserFriendlyException(L("CouldNotFoundTheTaskMessage")); } Input.MapTo(task); } } 示例里我们看到一个应用服务方法,在DDD中,应用服务方法是在表示层执行应用的用户用例的。我们可以想成UpdateTask方法是被Ajax调用。让我们看看ABP带来的便利: 依赖注入:ABP使用并提供一个强大而且符合约定的DI框架。上述的应用服务,按照约定临时的(每个请求Request创建一个)注册到DI容器,它能简单地注入所有依赖项(如示例中的Irepository<Task>)。 仓储:ABP能为每个实体创建一个默认的仓储(如示例中的Irepository<Task>)。默认仓储包含许多有用的方法,如示例中的FirstOrDefault方法。我们可以根据需要,很容易地扩展默认仓储。仓储抽象了DBMS和ORM以及简化了数据访问逻辑。 授权:ABP可以检查许可。如果当前用户没有“updating task”权限或是未登录,ABP就会阻止他们访问UpdateTask方法。用陈述性的特性来简化授权,当然还有另外的授权方式。 验证:ABP自动检查input是否为null。根据标准的数据注释特性和自定义验证规则,检查一个input的所有属性。如果请求没有通过验证,会抛出一个对应的验证异常。 审计日志:根据约定和配置,每个请求的用户、浏览器、Ip地址、调用服务、方法、参数、调用时间、执行耗时和其它的一些信息会被自动地保存下来。 工作单元:在ABP里,每个应用服务方法都默认地被认定为一个工作单元。在方法开始前,它自动创建一个连接并开启一个事务。如果方法成功完成,接着事务会被提交并释放连接。即使是使用不同的仓储或是方法,它们都可以是原子性(事务性)的,并且当事务提交时实体中所有的修改都自动地被保存。因此,如同示例所示,我们甚至不需要去调用_repository.Update(task)方法。 异常处理:我们几乎不用在一个使用ABP的Web应用中写异常处理。所有的异常都自动地被默认处理。当一个异常发生,ABP自动记录它并返回一个对应的结果给客户端。例如,一个AJAX请求,它会返回一个Json对象给客户端,告知发生了一个错误。如示例所示,UserFriendlyException可以向客户端隐藏具体的异常,显示友好信息。它同样可以在客户端理解并处理客户端错误,并向用户显示对应的信息。 日志:如你所见,我们可以用定义在基类中的Logger对象写日志。默认使用Log4Net,不过这是可修改和可配置的。 本地化:请注意我们在抛出异常时,使用了L方法。因此,它可自动依据用户区域,使用相应的本地化信息。当然,我们需要在某处定义CouldNotFoundTheTaskMessage(更多信息参见“本地化”文档)。 自动映射:最后一行代码,我们使用ABP的MapTo扩展方法来映射input属性到实体属性。它使用AutoMapper库来执行映射。因此,我们可以简单地基于命名约定,从一个对象映射到另一个。 动态Web API 层:实际上TaskAppService是一个简单的类(甚至是不需要从ApplicationService继承)。我们通常包装一个Web API 控制器为Javascript客户端公开方法,ABP会在运行时自动地完成这件事。因此,我们可以直接在客户端使用应用服务。 动态Javascript AJAX 代理:ABP创建Javascript代理方法,以便就本地调用一样,来调用应用服务。 通过这么一个简单类我们能看到ABP的便利。完成这些任务一般来说都是很费时的,但是所有的一切,ABP都自动处理了。 其它特性 除了示例所示,ABP提供了一个强大的基础架构和应用模型,下列为ABP的其它特性: 模块化:提供一个强大的基础架构来创建可重用的模块。 数据过滤:提供自动地数据过滤来实现一些模式,像软删除和多租户。 多租户:完全地支持多租户,包含单数据库或每租户一个单独数据库。 设置管理:提供一个基础架构来读取/修改应用、租户和用户级别的设置。 单元和集成测试:以可测试为宗旨,当然提供基础类来简化单元测试和集成测试。点击查看更多相关信息。 查看文档了解所有功能。 启动模板 开始一个新的解决方案、创建层、安装nuget包、创建一个简单的布局和菜单...所有这些都是耗时的工作。 ABP提供预创建的启动模板,使开始一个新的解决方案更简单。模板支持SPA(单页面应用)和MPA(多页面MVC应用)结构。同时允许我们使用不同的ORM工具。 如何使用 ABP开源项目在Github上,分发在Nuget上。“启动模板”是使用ABP的最简单方式(按文档所述操作)。 kid1412附:英文原文:http://www.aspnetboilerplate.com/Pages/Documents/Introduction
原文:【ABP框架系列学习】介绍篇(1) 0.引言 该系列博文主要在【官方文档】及【tkbSimplest】ABP框架理论研究系列博文的基础上进行总结的,或许大家会质问,别人都已经翻译过了,这不是多此一举吗?原因如下: 1.【tkbSimplest】的相关博文由于撰写得比较早的,在参照官方文档学习的过程中,发现部分知识未能及时同步(当前V4.0.2版本),如【EntityHistory】、【Multi-Lingual Engities】章节未涉及、【Caching】章节没有Entity Caching等内容。 2.进一步深入学习ABP的理论知识。 3.借此机会提高英文文档的阅读能力,故根据官方当前最新的版本,并在前人的基础上,自己也感受一下英文帮助文档的魅力。 好了,下面开始进入正题。 1.APB是什么? ABP是ASP.NET Boilerplate的简称,从英文字面上理解它是一个关于ASP.NET的模板,在github上已经有5.7k的star(截止2018年11月21日)。官方的解释:ABP是一个开源且文档友好的应用程序框架。ABP不仅仅是一个框架,它还提供了一个最徍实践的基于领域驱动设计(DDD)的体系结构模型。 ABP与最新的ASP.NET CORE和EF CORE版本保持同步,同样也支持ASP.NET MVC 5.x和EF6.x。 2.一个快速事例 让我们研究一个简单的类,看看ABP具有哪些优点: public class TaskAppService : ApplicationService, ITaskAppService { private readonly IRepository<Task> _taskRepository; public TaskAppService(IRepository<Task> taskRepository) { _taskRepository = taskRepository; } [AbpAuthorize(MyPermissions.UpdateTasks)] public async Task UpdateTask(UpdateTaskInput input) { Logger.Info("Updating a task for input: " + input); var task = await _taskRepository.FirstOrDefaultAsync(input.TaskId); if (task == null) { throw new UserFriendlyException(L("CouldNotFindTheTaskMessage")); } input.MapTo(task); } } 这里我们看到一个Application Service(应用服务)方法。在DDD中,应用服务直接用于表现层(UI)执行应用程序的用例。那么在UI层中就可以通过javascript ajax的方式调用UpdateTask方法。 var _taskService = abp.services.app.task; _taskService.updateTask(...); 3.ABP的优点 通过上述事例,让我们来看看ABP的一些优点: 依赖注入(Dependency Injection):ABP使用并提供了传统的DI基础设施。上述TaskAppService类是一个应用服务(继承自ApplicationService),所以它按照惯例以短暂(每次请求创建一次)的形式自动注册到DI容器中。同样的,也可以简单地注入其他依赖(如事例中的IRepository<Task>)。 部分源码分析:TaskAppService类继承自ApplicationService,IApplicaitonServcie又继承自ITransientDependency接口,在ABP框架中已经将ITransientDependency接口注入到DI容器中,所有继承自ITransientDependency接口的类或接口都会默认注入。 //空接口 public interface ITransientDependency { } //应用服务接口 public interface IApplicationService : ITransientDependency { } //仓储接口 public interface IRepository : ITransientDependency { } View Code public class BasicConventionalRegistrar : IConventionalDependencyRegistrar { public void RegisterAssembly(IConventionalRegistrationContext context) { //注入到IOC,所有继承自ITransientDependency的类、接口等都会默认注入 context.IocManager.IocContainer.Register( Classes.FromAssembly(context.Assembly) .IncludeNonPublicTypes() .BasedOn<ITransientDependency>() .If(type => !type.GetTypeInfo().IsGenericTypeDefinition) .WithService.Self() .WithService.DefaultInterfaces() .LifestyleTransient() ); //Singleton context.IocManager.IocContainer.Register( Classes.FromAssembly(context.Assembly) .IncludeNonPublicTypes() .BasedOn<ISingletonDependency>() .If(type => !type.GetTypeInfo().IsGenericTypeDefinition) .WithService.Self() .WithService.DefaultInterfaces() .LifestyleSingleton() ); //Windsor Interceptors context.IocManager.IocContainer.Register( Classes.FromAssembly(context.Assembly) .IncludeNonPublicTypes() .BasedOn<IInterceptor>() .If(type => !type.GetTypeInfo().IsGenericTypeDefinition) .WithService.Self() .LifestyleTransient() ); } View Code 仓储(Repository):ABP可以为每一个实体创建一个默认的仓储(如事例中的IRepository<Task>)。默认的仓储提供了很多有用的方法,如事例中的FirstOrDefault方法。当然,也可以根据需求扩展默认的仓储。仓储抽象了DBMS和ORMs,并简化了数据访问逻辑。 授权(Authorization):ABP可以通过声明的方式检查权限。如果当前用户没有【update task】的权限或没有登录,则会阻止访问UpdateTask方法。ABP不仅提供了声明属性的方式授权,而且还可以通过其它的方式。 部分源码分析:AbpAuthorizeAttribute类实现了Attribute,可在类或方法上通过【AbpAuthorize】声明。 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class AbpAuthorizeAttribute : Attribute, IAbpAuthorizeAttribute { /// <summary> /// A list of permissions to authorize. /// </summary> public string[] Permissions { get; } /// <summary> /// If this property is set to true, all of the <see cref="Permissions"/> must be granted. /// If it's false, at least one of the <see cref="Permissions"/> must be granted. /// Default: false. /// </summary> public bool RequireAllPermissions { get; set; } /// <summary> /// Creates a new instance of <see cref="AbpAuthorizeAttribute"/> class. /// </summary> /// <param name="permissions">A list of permissions to authorize</param> public AbpAuthorizeAttribute(params string[] permissions) { Permissions = permissions; } } View Code 通过AuthorizationProvider类中的SetPermissions方法进行自定义授权。 public abstract class AuthorizationProvider : ITransientDependency { /// <summary> /// This method is called once on application startup to allow to define permissions. /// </summary> /// <param name="context">Permission definition context</param> public abstract void SetPermissions(IPermissionDefinitionContext context); } View Code 验证(Validation):ABP自动检查输入是否为null。它也基于标准数据注释特性和自定义验证规则验证所有的输入属性。如果请求无效,它会在客户端抛出适合的验证异常。 部分源码分析:ABP框架中主要通过拦截器ValidationInterceptor(AOP实现方式之一,)实现验证,该拦截器在ValidationInterceptorRegistrar的Initialize方法中调用。 internal static class ValidationInterceptorRegistrar { public static void Initialize(IIocManager iocManager) { iocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered; } private static void Kernel_ComponentRegistered(string key, IHandler handler) { if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(handler.ComponentModel.Implementation)) { handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(ValidationInterceptor))); } } } View Code public class ValidationInterceptor : IInterceptor { private readonly IIocResolver _iocResolver; public ValidationInterceptor(IIocResolver iocResolver) { _iocResolver = iocResolver; } public void Intercept(IInvocation invocation) { if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation)) { invocation.Proceed(); return; } using (var validator = _iocResolver.ResolveAsDisposable<MethodInvocationValidator>()) { validator.Object.Initialize(invocation.MethodInvocationTarget, invocation.Arguments); validator.Object.Validate(); } invocation.Proceed(); } } View Code 自定义Customvalidator类 public class CustomValidator : IMethodParameterValidator { private readonly IIocResolver _iocResolver; public CustomValidator(IIocResolver iocResolver) { _iocResolver = iocResolver; } public IReadOnlyList<ValidationResult> Validate(object validatingObject) { var validationErrors = new List<ValidationResult>(); if (validatingObject is ICustomValidate customValidateObject) { var context = new CustomValidationContext(validationErrors, _iocResolver); customValidateObject.AddValidationErrors(context); } return validationErrors; } } View Code 审计日志(Audit Logging):基于约定和配置,用户、浏览器、IP地址、调用服务、方法、参数、调用时间、执行时长以及其它信息会为每一个请求自动保存。 部分源码分析:ABP框架中主要通过拦截器AuditingInterceptor(AOP实现方式之一,)实现审计日志,该拦截器在AuditingInterceptorRegistrar的Initialize方法中调用。 internal static class AuditingInterceptorRegistrar { public static void Initialize(IIocManager iocManager) { iocManager.IocContainer.Kernel.ComponentRegistered += (key, handler) => { if (!iocManager.IsRegistered<IAuditingConfiguration>()) { return; } var auditingConfiguration = iocManager.Resolve<IAuditingConfiguration>(); if (ShouldIntercept(auditingConfiguration, handler.ComponentModel.Implementation)) { handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(AuditingInterceptor))); } }; } View Code private static bool ShouldIntercept(IAuditingConfiguration auditingConfiguration, Type type) { if (auditingConfiguration.Selectors.Any(selector => selector.Predicate(type))) { return true; } if (type.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true)) { return true; } if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true))) { return true; } return false; } } View Code internal class AuditingInterceptor : IInterceptor { private readonly IAuditingHelper _auditingHelper; public AuditingInterceptor(IAuditingHelper auditingHelper) { _auditingHelper = auditingHelper; } public void Intercept(IInvocation invocation) { if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing)) { invocation.Proceed(); return; } if (!_auditingHelper.ShouldSaveAudit(invocation.MethodInvocationTarget)) { invocation.Proceed(); return; } var auditInfo = _auditingHelper.CreateAuditInfo(invocation.TargetType, invocation.MethodInvocationTarget, invocation.Arguments); if (invocation.Method.IsAsync()) { PerformAsyncAuditing(invocation, auditInfo); } else { PerformSyncAuditing(invocation, auditInfo); } } private void PerformSyncAuditing(IInvocation invocation, AuditInfo auditInfo) { var stopwatch = Stopwatch.StartNew(); try { invocation.Proceed(); } catch (Exception ex) { auditInfo.Exception = ex; throw; } finally { stopwatch.Stop(); auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); _auditingHelper.Save(auditInfo); } } private void PerformAsyncAuditing(IInvocation invocation, AuditInfo auditInfo) { var stopwatch = Stopwatch.StartNew(); invocation.Proceed(); if (invocation.Method.ReturnType == typeof(Task)) { invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithFinally( (Task) invocation.ReturnValue, exception => SaveAuditInfo(auditInfo, stopwatch, exception) ); } else //Task<TResult> { invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithFinallyAndGetResult( invocation.Method.ReturnType.GenericTypeArguments[0], invocation.ReturnValue, exception => SaveAuditInfo(auditInfo, stopwatch, exception) ); } } private void SaveAuditInfo(AuditInfo auditInfo, Stopwatch stopwatch, Exception exception) { stopwatch.Stop(); auditInfo.Exception = exception; auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); _auditingHelper.Save(auditInfo); } } View Code 工作单元(Unit Of Work):在ABP中,应用服务方法默认视为一个工作单元。它会自动创建一个连接并在方法的开始位置开启事务。如果方法成功完成并没有异常,事务会提交并释放连接。即使这个方法使用不同的仓储或方法,它们都是原子的(事务的)。当事务提交时,实体的所有改变都会自动保存。如上述事例所示,甚至不需要调用_repository.Update(task)方法。 部分源码分析:ABP框架中主要通过拦截器UnitOfWorkInterceptor(AOP实现方式之一,)实现工作单元,该拦截器在UnitOfWorkRegistrar的Initialize方法中调用。 internal class UnitOfWorkInterceptor : IInterceptor { private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IUnitOfWorkDefaultOptions _unitOfWorkOptions; public UnitOfWorkInterceptor(IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkDefaultOptions unitOfWorkOptions) { _unitOfWorkManager = unitOfWorkManager; _unitOfWorkOptions = unitOfWorkOptions; } /// <summary> /// Intercepts a method. /// </summary> /// <param name="invocation">Method invocation arguments</param> public void Intercept(IInvocation invocation) { MethodInfo method; try { method = invocation.MethodInvocationTarget; } catch { method = invocation.GetConcreteMethod(); } var unitOfWorkAttr = _unitOfWorkOptions.GetUnitOfWorkAttributeOrNull(method); if (unitOfWorkAttr == null || unitOfWorkAttr.IsDisabled) { //No need to a uow invocation.Proceed(); return; } //No current uow, run a new one PerformUow(invocation, unitOfWorkAttr.CreateOptions()); } private void PerformUow(IInvocation invocation, UnitOfWorkOptions options) { if (invocation.Method.IsAsync()) { PerformAsyncUow(invocation, options); } else { PerformSyncUow(invocation, options); } } private void PerformSyncUow(IInvocation invocation, UnitOfWorkOptions options) { using (var uow = _unitOfWorkManager.Begin(options)) { invocation.Proceed(); uow.Complete(); } } private void PerformAsyncUow(IInvocation invocation, UnitOfWorkOptions options) { var uow = _unitOfWorkManager.Begin(options); try { invocation.Proceed(); } catch { uow.Dispose(); throw; } if (invocation.Method.ReturnType == typeof(Task)) { invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally( (Task) invocation.ReturnValue, async () => await uow.CompleteAsync(), exception => uow.Dispose() ); } else //Task<TResult> { invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult( invocation.Method.ReturnType.GenericTypeArguments[0], invocation.ReturnValue, async () => await uow.CompleteAsync(), exception => uow.Dispose() ); } } } View Code 异常处理(Exception):在使用了ABP框架的Web应用程序中,我们几乎不用手动处理异常。默认情况下,所有的异常都会自动处理。如果发生异常,ABP会自动记录并给客户端返回合适的结果。例如:对于一个ajax请求,返回一个json对象给客户端,表明发生了错误。但会对客户端隐藏实际的异常,除非像上述事例那样使用UserFriendlyException方法抛出。它也理解和处理客户端的错误,并向客户端显示合适的信息。 部分源码分析:UserFriendlyException抛出异常方法。 [Serializable] public class UserFriendlyException : AbpException, IHasLogSeverity, IHasErrorCode { /// <summary> /// Additional information about the exception. /// </summary> public string Details { get; private set; } /// <summary> /// An arbitrary error code. /// </summary> public int Code { get; set; } /// <summary> /// Severity of the exception. /// Default: Warn. /// </summary> public LogSeverity Severity { get; set; } /// <summary> /// Constructor. /// </summary> public UserFriendlyException() { Severity = LogSeverity.Warn; } /// <summary> /// Constructor for serializing. /// </summary> public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> public UserFriendlyException(string message) : base(message) { Severity = LogSeverity.Warn; } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> /// <param name="severity">Exception severity</param> public UserFriendlyException(string message, LogSeverity severity) : base(message) { Severity = severity; } /// <summary> /// Constructor. /// </summary> /// <param name="code">Error code</param> /// <param name="message">Exception message</param> public UserFriendlyException(int code, string message) : this(message) { Code = code; } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> /// <param name="details">Additional information about the exception</param> public UserFriendlyException(string message, string details) : this(message) { Details = details; } /// <summary> /// Constructor. /// </summary> /// <param name="code">Error code</param> /// <param name="message">Exception message</param> /// <param name="details">Additional information about the exception</param> public UserFriendlyException(int code, string message, string details) : this(message, details) { Code = code; } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> /// <param name="innerException">Inner exception</param> public UserFriendlyException(string message, Exception innerException) : base(message, innerException) { Severity = LogSeverity.Warn; } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> /// <param name="details">Additional information about the exception</param> /// <param name="innerException">Inner exception</param> public UserFriendlyException(string message, string details, Exception innerException) : this(message, innerException) { Details = details; } } View Code 日志(Logging):由上述事例可见,可以通过在基类定义的Logger对象来写日志。ABP默认使用了Log4Net,但它是可更改和可配置的。 部分源码分析:Log4NetLoggerFactory类。 public class Log4NetLoggerFactory : AbstractLoggerFactory { internal const string DefaultConfigFileName = "log4net.config"; private readonly ILoggerRepository _loggerRepository; public Log4NetLoggerFactory() : this(DefaultConfigFileName) { } public Log4NetLoggerFactory(string configFileName) { _loggerRepository = LogManager.CreateRepository( typeof(Log4NetLoggerFactory).GetAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy) ); var log4NetConfig = new XmlDocument(); log4NetConfig.Load(File.OpenRead(configFileName)); XmlConfigurator.Configure(_loggerRepository, log4NetConfig["log4net"]); } public override ILogger Create(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } return new Log4NetLogger(LogManager.GetLogger(_loggerRepository.Name, name), this); } public override ILogger Create(string name, LoggerLevel level) { throw new NotSupportedException("Logger levels cannot be set at runtime. Please review your configuration file."); } } View Code 本地化(Localization):注意,在上述事例中使用了L("XXX")方法处理抛出的异常。因此,它会基于当前用户的文化自动实现本地化。详细见后续本地化章节。 部分源码分析:...... 自动映射(Auto Mapping):在上述事例最后一行代码,使用了ABP的MapTo扩展方法将输入对象的属性映射到实体属性。ABP使用AutoMapper第三方库执行映射。根据命名惯例可以很容易的将属性从一个对象映射到另一个对象。 部分源码分析:AutoMapExtensions类中的MapTo()方法。 public static class AutoMapExtensions { public static TDestination MapTo<TDestination>(this object source) { return Mapper.Map<TDestination>(source); } public static TDestination MapTo<TSource, TDestination>(this TSource source, TDestination destination) { return Mapper.Map(source, destination); } ...... } View Code 动态API层(Dynamic API Layer):在上述事例中,TaskAppService实际上是一个简单的类。通常必须编写一个Web API Controller包装器给js客户端暴露方法,而ABP会在运行时自动完成。通过这种方式,可以在客户端直接使用应用服务方法。 部分源码分析:...... 动态javascript ajax代理(Dynamic JavaScript AJAX Proxy):ABP创建动态代理方法,从而使得调用应用服务方法就像调用客户端的js方法一样简单。 部分源码分析:...... 4.本章小节 通过上述简单的类可以看到ABP的优点。完成所有这些任务通常需要花费大量的时间,但是ABP框架会自动处理。 除了这个上述简单的事例外,ABP还提供了一个健壮的基础设施和开发模型,如模块化、多租户、缓存、后台工作、数据过滤、设置管理、领域事件、单元&集成测试等等,那么你可以专注于业务代码,而不需要重复做这些工作(DRY)。
原文:【ABP框架系列学习】N层架构(3) 目录 0.引言 1.DDD分层 2.ABP应用构架模型 客户端应用程序(Client Applications) 表现层(Presentation Layer) 分布式服务层(Distributed Service Layer) 应用层(Application Layer) 领域层 基础设施层 3.使用ABP项目模版快速生成应用程序 0.引言 应用程序的分层是一种广泛接受的技术, 可以降低复杂度和提高代码的可重用性。为了实现分层架构,ABP遵循领域驱动设计(DDD)原则。 1.DDD分层 领域驱动设计有四个基本的层: 表现层(Presentaiton Layer):为用户提供接口。使用应用层实现与用户交互。 应用层(Application Layer):表现层和领域层的中间者。协调业务对象以执行特定的应用程序任务。 领域层(Domain Layer):包含业务对象和规则,是整个应用程序的核心。 基础设施层:提供支持上层通用的技术能力,大部分是借助于第三方库 2.ABP应用构架模型 除了DDD,现代的应用程序架构还包括逻辑和物理层等。如下图是ABP建议并实施的模型,它不仅通过提供基类和服务来快速实现这个模型,而且还提供了启动模板直接开始这个模型。 客户端应用程序(Client Applications) 远程客户端通过HTTP APIs(API Controllers,OData Controllers,GraphQL终端)等将应用程序作为服务。远程客户端可以是SPA、移动APP、或第三方消费者等。该应用程序主要包括本地化和导航功能。 表现层(Presentation Layer) ASP.NET [Core] MVC可以认为是表现层。它可以是物理层(通过HTTP APIs使用应用程序)或是逻辑层(直接注入和使用应用服务)。无论是哪一种情况,一般包括本地化(Location)、导航(Navigation)、对象映射(Object Mapping)、缓存(Caching)、配置管理(Configuration Manager)、审计日志(Audit Logging)等等。还包括授权(Authorization)、会话(Session)、功能(Features,对于多租户应用程序)以及异常处理(Exception Handling)。 分布式服务层(Distributed Service Layer) 该层主要通过远程APIs(如REST、OData、GraphQL等)服务于应用服务/领域功能。该层只是将HTTP请求转换为领域交互,或可使用应用服务来委托操作,而并不包含业务逻辑。通常包括授权(Authorization)、缓存(Caching)、审计日志(Audit Logging)、对象映射(Object Mapping)、异常处理(Exception Handling)、会话(Session)等。 应用层(Application Layer) 应用层主要包括使用领域层和领域对象(领域服务、实体...)来执行应用程序功能请求的应用服务。应用层使用DTO(数据传输对象)从表现层或分布式服务层获取或返回数据。包括授权(Authorization)、缓存(Caching)、审计日志(Audit Logging)、对象映射(Object Mapping)、会话(Session)等。 领域层 这是实现领域逻辑主要的层,包括执行业务/领域逻辑的实体(Entities)、值对象(Value Objects)、和领域服务(Domain Services)。它还包括规约(Specifications)和触发领域事件(trigger Domain Events),并定义了用于从数据源(通常是DBMS)读取和持久化实体的仓储接口(Repository Interfaces)。 基础设施层 基础设施层用于辅助其它层:包括实现仓储接口(Repository Interfaces,如EF Core)实际操作数据库。还可能包括与供应商(vendor)的集成,以便发送电子邮件等。基础设施层是最下、也是不严格的一层,实际上是通过实现它们的抽象概念来支持其他层。 3.使用ABP项目模版快速生成应用程序 1.打开网址【Startup Templates】 2.依次选择ASP.NET Core 2.x >> .NET CORE(Cross Plateform) >> Multi Page Web Application,输入项目名称:XXX.FirstABP、验证码,点击创建项目按钮。 3.跳转自动下载源代码。 4.解压并用Visual Studio 2017打开,启动程序后VS会自动还原项目所需要的包,wait for a minute......使用ABP项目模版生成的解决方案如下: 5.设置【XXX.FirstABP.Web.MVC】为启动项目,打开appsetting.json文件修改连接字符串,如下: 6.打开【程序包管理控制台】,默认项目选择【XXX.FirstABP.EntityFrameworkCore】,在命令行依次输入:Add-Migration "FirstABP"、Update-Database,wait for a minute... 7.执行步骤6的操作后,默认生成了ABP的数据。 8.回到Visual Studio 2017,按F5运行程序...有惊喜哟,登录界面出现了,输入admin/123qwe,点击登录。 9.如图,进入主界面。至此,利用ABP项目模板快速生成了应用程序,主要有租户管理、用户管理、角色管理等功能模块。
原文:【ABP杂烩】面向切面编程(AOP)知识总结 目录 1.存在问题 2.AOP的概念 3.AOP应用范围 3.AOP实现方式 4.应用举例 5.结束语 本文在学习【老张的哲学】系列文章AOP相关章节后,自己归纳总结的笔记。 1.存在问题 最近,其它项目小组在开发的过程中遇到了一个问题,在日志记录时,用户明明点击的是更新操作,可翻看记录时却发现是查询操作,起初是一头雾水,后面跟踪该更新操作的代码后才发现,在日志记录时确实是写着查询,说到这里,大家可能已经知道问题的所在了,这是由于在开发的过程中,开发员直接从查询的方法里把日志记录的代码直接copy过来,而后面又忘记修改,相信其它的方法里也同样是存在这样的问题。 在平常的开发过程中,日志往往采取如下图的编程方式,在小项目中这种方法简单快捷,没有太大的问题,但对于比较大的项目,这样重复的代码会造成后期的维护成本比较高的,万一哪天说日志记录要换换格式或采用第三方库,估计就蛋疼了。 问题是发现了,有没有改进的方法呢?答案是肯定的。这个时候AOP该闪亮登场了,第一次学习【老张的哲学】系统文章时,也没怎么注意到AOP这个知识点,当时就懵懵懂懂,可后面发现在很多的开源框架里都有涉及到AOP的概念,所以借此机会自己也学习一下,时机成熟时也可以引入到项目里。 2.AOP的概念 AOP是Aspect Oriented Programing的缩写,中文翻译为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从面使得业务逻辑各部分之间的耦合度低,提高程序的可重用性,同时提高开发的效率。 通俗的讲:面向切面编程是在不影响原有功能的前提下,可为软件横向扩展,即可插拔,拔掉软件正常运行,插上扩展功能起效果。 3.AOP应用范围 既然把AOP说得那么厉害,那它到底可以用到哪些地方呢? 日记功能 审计功能 验证功能 安全控制 事务处理 异常处理 ...... 3.AOP实现方式 在.net core中,AOP的实现可以通过以下三种试: 过滤器(Filter):适用于身份验证、参数验证、处理耗时的Web服务 动态代理(DynamicProxy):适用功能模块间的解耦和重用 中间件(Middleware):适用底层服务的通信 主要框架有: 编译时:PostSharp、LinFu、SheepAspect、Fody、CIL 运行时:Castle Windsor、StructureMap、Unity、Spring.NET 4.应用举例 在本文中主要利用PostSharp实现AOP例子,PostSharp是一个在.net平台上实现的AOP框架,是一个收费的框架。好拉,下面将利用PostSharp解决开篇提到的问题,通过AOP为系统添加日志记录功能。 步骤一:新建项目FirstAOP,并添加PostShap 步骤二:定义LogOperator日志记录类,并继承OnMethodBoundaryAspect [Serializable] public class LogOperator : OnMethodBoundaryAspect { public override void OnEntry(MethodExecutionArgs args) { //MehodExcutionArgs提供了绑定方法的信息和上下文 Console.WriteLine($"【{args.Method.Name}】方法开始记录日志..."); } public override void OnExit(MethodExecutionArgs args) { Console.WriteLine($"【{args.Method.Name}】方法结束记录日志..."); } } 步骤三:将LogOperator切面以特性的形式添加到Main函数中 class Program { [LogOperator] static void Main(string[] args) { Console.WriteLine("Hello AOP!"); } } 步骤四:运行,可以看到日志已经成功开启 5.结束语 本文首先抛出了在项目开发过程中遇到的问题,接着介绍了AOP的相关概念、应用范围和实现方式,最后通过PostSharp的一个简单的例子实现了面向切面编程,当然无论你选择了哪一种方式,AOP都会提高项目的开发效率及后期的维护成本,从而避免了相同的代码复制-黏贴或bug修改数十、数百次,希望在平常的开发中能帮到大家。
原文:【ABP杂烩】Extensions后缀扩展方法 1.Extensions介绍 扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。 对于用 C#、F# 和 Visual Basic 编写的客户端代码,调用扩展方法与调用在类型中实际定义的方法没有明显区别。 详细见官方文档:扩展方法(C# 编程指南) 2.语法介绍 * 需要写在一个静态类中 * 必须是一个静态方法 * 通过第一个参数和this关键字指定扩展的目标类型 * 不同类型的扩展方法不一定要写在同一个类中 3.事例介绍 在ABP框架原中会发现在很多地方带有Extensions的类,其主要作用是在不改变原有接口或类的基础上扩展自定义的方法,从而方便使用,如QueryableExtensions类是对IQueryable进行扩展,里面添加了分页PageBy、条件判断WhereIf方法,代码如下: using System; using System.Linq; using System.Linq.Expressions; using Abp.Application.Services.Dto; namespace Abp.Linq.Extensions { /// <summary> /// Some useful extension methods for <see cref="IQueryable{T}"/>. /// </summary> public static class QueryableExtensions { /// <summary> /// Used for paging. Can be used as an alternative to Skip(...).Take(...) chaining. /// </summary> public static IQueryable<T> PageBy<T>(this IQueryable<T> query, int skipCount, int maxResultCount) { if (query == null) { throw new ArgumentNullException("query"); } return query.Skip(skipCount).Take(maxResultCount); } /// <summary> /// Used for paging with an <see cref="IPagedResultRequest"/> object. /// </summary> /// <param name="query">Queryable to apply paging</param> /// <param name="pagedResultRequest">An object implements <see cref="IPagedResultRequest"/> interface</param> public static IQueryable<T> PageBy<T>(this IQueryable<T> query, IPagedResultRequest pagedResultRequest) { return query.PageBy(pagedResultRequest.SkipCount, pagedResultRequest.MaxResultCount); } /// <summary> /// Filters a <see cref="IQueryable{T}"/> by given predicate if given condition is true. /// </summary> /// <param name="query">Queryable to apply filtering</param> /// <param name="condition">A boolean value</param> /// <param name="predicate">Predicate to filter the query</param> /// <returns>Filtered or not filtered query based on <paramref name="condition"/></returns> public static IQueryable<T> WhereIf<T>(this IQueryable<T> query, bool condition, Expression<Func<T, bool>> predicate) { return condition ? query.Where(predicate) : query; } /// <summary> /// Filters a <see cref="IQueryable{T}"/> by given predicate if given condition is true. /// </summary> /// <param name="query">Queryable to apply filtering</param> /// <param name="condition">A boolean value</param> /// <param name="predicate">Predicate to filter the query</param> /// <returns>Filtered or not filtered query based on <paramref name="condition"/></returns> public static IQueryable<T> WhereIf<T>(this IQueryable<T> query, bool condition, Expression<Func<T, int, bool>> predicate) { return condition ? query.Where(predicate) : query; } } } 如上所述,对IQueryable扩展后,可以像调用原生方法一样,使上层的调用感受不到区别和不用做过多的操作,方便对第三方的库进行扩展,从而增加自定义需求,有效提高项目的开发效率。调用代码如下: public async Task<PagedResultDto<ProjectListDto>> GetProjects(GetProjectsInput input) { var query = _projectRepository.GetAll(); query = query .WhereIf(!input.Name.IsNullOrWhiteSpace(), item => item.Name.Contains(input.Name)) .WhereIf(!input.Address.IsNullOrWhiteSpace(), item => item.Address.Contains(input.Address)) .WhereIf(!input.ResponbleDepart.IsNullOrWhiteSpace(), item => item.ResponbleDepart.Contains(input.ResponbleDepart)) .WhereIf(!input.Type.IsNullOrWhiteSpace(), item => item.Type.Contains(input.Type)) .WhereIf(input.ProjectDateStart.HasValue, item => item.StartTime >= input.ProjectDateStart) .WhereIf(input.ProjectDateEnd.HasValue, item => item.EndTime <= input.ProjectDateEnd) .WhereIf(input.ReportDateStart.HasValue, item => item.ReportTime >= input.ReportDateStart) .WhereIf(input.ReportDateEnd.HasValue, item => item.ReportTime <= input.ReportDateEnd); var projectCount = await query.CountAsync(); var projects = await query.OrderBy(input.Sorting).PageBy(input).ToListAsync(); var projectListDtos = ObjectMapper.Map<List<ProjectListDto>>(projects); return new PagedResultDto<ProjectListDto>( projectCount, projectListDtos ); }
原文:为控件动态添加Style 此文可解决: 重写控件时,给控件加入子控件或父控件的样式切换问题。 很灵活的可以根据不同内容显示不同样式 子控件作用在: <DataTemplate x:Key="ColmunHeader1"> <DockPanel Background="Transparent"> <!-- The control to host the filter UI for this column --> <controls:dgDataGrid DockPanel.Dock="Right" /> <ContentPresenter x:Name="PART_Content" Content="{Binding}" SnapsToDevicePixels="{Binding SnapsToDevicePixels, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGridColumnHeader}}" HorizontalAlignment="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGridColumnHeader}}" VerticalAlignment="{Binding VerticalContentAlignment, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGridColumnHeader}}" /> </DockPanel> </DataTemplate> 如下样式: <ControlTemplate x:Key="DataGridCheckBoxColumn1"> <Grid> <CheckBox /> </Grid> </ControlTemplate> <ControlTemplate x:Key="DataGridTextBoxColumn1"> <Grid> <TextBox/> </Grid> </ControlTemplate> 然后在子控件类的loaded事件中加入: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; namespace WpfApp1.Controls { public class dgDataGrid : Control { public DataGridColumnHeader ColumnHeader { get; set; } public dgDataGrid() { this.Loaded += (s, e) => { ColumnHeader = this.FindAncestorOrSelf<DataGridColumnHeader>(); var value = ColumnHeader.Column.GetType(); if (value == typeof(DataGridTextColumn)) { this.Template = this.FindResource("DataGridTextColumn1") as ControlTemplate; } else { this.Template = this.FindResource("DataGridCheckBoxColumn1") as ControlTemplate; } }; } } }
原文:TextBox 加阴影 <Border.Effect> <DropShadowEffect x:Name="dse" BlurRadius="8" ShadowDepth="0" Color="#9966afe9" Opacity="0" ></DropShadowEffect> </Border.Effect> <Trigger Property="IsFocused" Value="True"> <Trigger.EnterActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="dse" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15"> <DoubleAnimation.EasingFunction> <BackEase EasingMode="EaseInOut" /> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> <Setter Property="BorderBrush" Value="#66afe9"/> </Trigger> View Code 讲代码复制到 TextBox 样式中Border中即可