平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“详细解析ABP框架中的日志管理和设置管理的基本配置”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。
日志管理
Server side(服务器端)
从实现思路看,ASP.NET Boilerplate采用Castle Windsor's logging facility日志记录工具,同时且能够采用不同的日志类库,比如:Log4Net, NLog, Serilog... 等等。对于所有的日志类库,Castle提供了一个通用的接口来实现,我们能够很便于的处理各种特殊的日志库,而且当业务需的时候,很容易替换日志组件。
译者注释:Castle是什么:Castle是针对.NET平台的一个开源项目,从数据访问框架ORM到IOC容器,再到WEB层的MVC框架、AOP,基本包括了整个开发过程中的所有东西。ASP.NET Boilerplate的ioc容器就是借助Castle实现的。
落到代码里,Log4Net 是asp.net下面最流行的一个日志库组件, ASP.NET Boilerplate 模板也采用了Log4Net日志库组件,但是呢,我们这里仅仅借助一行关键代码就实现Log4Net 的依赖注入(具体说明在下面的设置文件),所以,如果你想替换成自己的日志组件,也很容易。
拿到日志记录器logger
实际处理时,不管你选择哪一个日志库组件,借助代码来进行日志记录都是一样的。(这里吐槽, Castle's 通用 ILogger 接口实在太牛逼了)。
下面进入正题:(译者注:下面的代码是abp框架的Castle.Core源码分析以及实现)
1、首先呢,我们要先处理日志记录器对象logger, ASP.NET Boilerplate框架采用了dependency injection依赖注入技术,我们能够很便于的采用依赖注入生成日志记录器对象logger。
下面我们看一下 ASP.NET Boilerplate是怎么实现日志记录功能的吧:
using Castle.Core.Logging; //1: 导入日志的命名空间,Castle.Core.Logging
public class TaskAppService : ITaskAppService
{
//2:通过依赖注入获取日志记录器对象。
这里先定义了一个ILogger类型的public属性Logger,这个对象就是我们用来记录日志的对象。在创建了TaskAppService对象(就是我们应用中定义的任务)以后,通过属性注入的方式来实现。
public ILogger Logger { get; set; }
public TaskAppService()
{
//3: 如果没有日志记录器,将日志记录器返回一个空的实例,不写日志。这是依赖注入的最佳实现方式, // 如果你不定义这个空的日志记录器,当我们获取对象引用并且实例化的时候,就会产生异常。 // 这么做,保证了对象不为空。所以,换句话说,不设置日志记录器,就不记录日志,返回一个null的对象。 // NullLogger对象实际上什么都木有,空的。这么做,才能保证我们定义的类在实例化时正常运作。
Logger = NullLogger.Instance;
}
public void CreateTask(CreateTaskInput input)
{
//4: 写入日志
Logger.Info("Creating a new task with description: " + input.Description);
//TODO: save task to database... }
}
写入日志以后,我们能够查看日志文件,就像下面的格式:
public class HomeController : SimpleTaskSystemControllerBase
{
public ActionResult Index()
{
Logger.Debug("A sample log message...");
return View();
}
}
说明:SimpleTaskSystemControllerBase这个基类控制器是我们自己定义的基类控制器,他必须继承自 AbpController。
结合项目来看,这样实现,日志记录器才能正常工作。当然了,你也能够完成自己的基类,这样的话你也能够不采用依赖注入了。
设置
落到代码里,如果你在官网上借助ASP.NET Boilerplate templates 来生成了你的工程,Log4Net的所有设置都自动生成了。
默认的设置格式如下所示:
从实现思路看,•Log level: 日志记录等级,有DEBUG, INFO, WARN, ERROR or FATAL5个。
•Date and time: 日志记录时间。
•Thread number: 每行日志写时候的线程号。
实际处理时,•Logger name: 日志记录器的名字,通常情况就是类名称。
•Log text: 你写入的日志内容。
设置文件:log4net.config 一般都在项目的web目录下面。
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender" >
<file value="Logs/Logs.txt" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="10000KB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%-5level %date [%-5.5thread] %-40.40logger - %message%newline" />
</layout>
</appender>
<root>
<appender-ref ref="RollingFileAppender" />
<level value="DEBUG" />
</root>
<logger name="NHibernate">
<level value="WARN" />
</logger>
</log4net>
实际处理时,Log4Net是一个很强大和易用的日志库组件,你能够写各种日志,比如写到txt文件,写入到数据库等等。你能设置最小的日志等级,就像上面这个针对NHibernate的设置。不同的记录器写不同的日志,等等。
具体的用法大家能够参照:(链接已移除)
理解这一步时,最后,在工程的Global.asax 文件中,来定义Log4Net的设置文件:
public class MvcApplication : AbpWebApplication
{
protected override void Application_Start(object sender, EventArgs e)
{
IocManager.Instance.IocContainer.AddFacility<LoggingFacility>(f => f.UseLog4Net().WithConfig("log4net.config"));
base.Application_Start(sender, e);
}
}
结合项目来看,几行代码就调用了Log4Net这个日志记录组件,工程中的Log4Net库是在 nuget package包中的,你也能够换成其他日志组件库,但是代码不用做任何改变。因为,我们的框架是借助依赖注入实现日志记录器的!
Client side(客户端)
理解这一步时,最后,更厉害的是,你还能够在客户端调用日志记录器。在客户端,ASP.NET Boilerplate框架有对应的 javascript 日志API,这意味着你能够记录下来浏览器的日志,实现代码如下所示:
abp.log.warn('a sample log message...');
附上:客户端javascript的api,这里要说明的是,你能够采用console.log在客户端输出日志,但是这个API 不一定兼容所有的浏览器,还有可能导致你的脚本出现异常,你能够采用我们的api,我们的是安全的,你甚至能够重载或者扩展这些api。
abp.log.debug('...');
abp.log.info('...');
abp.log.warn('...');
abp.log.error('...');
abp.log.fatal('...');
设置管理
介绍
在这个场景下,每个应用程序需存储一些设置同时在应用程序的某个地方采用这些设置。ABP框架提供强大的基础架构,我们能够在服务端或者客户端设置,来存储/拿到应用程序、 租户和用户级别的设置。
在这个场景下,设置通常是存储在数据库(或另一个来源)中,用名称-值(name-value)字符串对应的结构来表示。我们能够把非字符串值转换成字符串值来存储。
注意:关于ISettingStore接口
从实现思路看,为了采用设置管理必须实现 ISettingStore 接口。你能够用自己的方式实现它,在module-zero项目中有完整的实现能够参考。
定义设置
落到代码里,采用设置之前必须要先定义。ABP框架是模块化设计,所以不同的模块能够有不同的设置。为了定义模块自己的设置,每个模块都应该新建继承自SettingProvider 的派生类。设置提供程序示比如下所示所示:
public class MySettingProvider : SettingProvider
{
public override IEnumerable<SettingDefinition> GetSettingDefinitions(SettingDefinitionProviderContext context)
{
return new[]
{
new SettingDefinition(
"SmtpServerAddress",
"127.0.0.1"
),
new SettingDefinition(
"PassiveUsersCanNotLogin",
"true",
scopes: SettingScopes.Application | SettingScopes.Tenant
),
new SettingDefinition(
"SiteColorPreference",
"red",
scopes: SettingScopes.User,
isVisibleToClients: true
)
};
}
}
理解这一步时,GetSettingDefinitions 方法得到 SettingDefinition 对象。SettingDefinition 类的构造函数中有如下所示参数:
在这个场景下,•Name (必填):必须具有全系统唯一的名称。比较好的办法是定义字符串常量来设置Name。
在这个场景下,•Default value: 设置一个默认值。此值能够是null 或空字符串。
•Scopes: 定义设置的范围 (见下文)。
在这个场景下,•Display name: 一个可本地化的字符串,用来以后在UI中显示设置的名称。
落到代码里,•Description: 一个可本地化的字符串,用来以后在UI中显示设置的描述。
从实现思路看,•Group: 可用来设置组。这仅仅是UI采用,不用来设置管理。
结合项目来看,•IsVisibleToClients: 设置为 true 将使设置在客户端可用。
结合项目来看,在新建设置提供程序(SettingProvider)之后,我们应该在预初始化(PreIntialize)方法中注册我们的模块:
落到代码里,Configuration.Settings.Providers.Add<MySettingProvider>();设置提供程序会自动注册依赖注入。所以,设置提供程序能够注入任何依赖项 (如存储库) 来生成设置定义的一些其它来源。
设置范围
有三个设置范围 (或级别) 在 SettingScopes 枚举中定义:
实际处理时,•Application:应用程序范围设置用来用户/租户独立的设置。比如,我们能够定义一个名为"SmtpServerAddress"的设置,当发送电子邮件时,拿到服务器的 IP 地址。如果此设置有一个单一的值 (不基于用户改变),那么我们能够定义它为应用程序范围。
落到代码里,•Tenant:如果应用程序是多租户的,我们能够定义特定于租户的设置。
•User:能够采用的用户范围的设置来为每个用户存储/拿到设置的值。
结合项目来看,SettingScopes 枚举具有Flags属性,所以我们能够定义一个具有多个作用域的设置。
落到代码里,设置范围是分层的。比如,如果我们定义设置范围为"Application | Tenant | User"同时尝试拿到当前设置的值;
•我们拿到特定用户的值,如果它定义 (重写) User。
实际处理时,•如果没有,我们拿到特定的租户值,如果它定义 (重写) Tenant。
理解这一步时,•如果没有,我们拿到应用的值,如果它定义Application。
•如果没有,我们得到的默认值。
落到代码里,默认值能够是 null 或空字符串。如果能够,建议为设置提供一个默认值。
拿到设置值
定义设置后,我们能够在服务器和客户端拿到到它的当前值。
(1)服务器端(Server side)
在这个场景下,ISettingManager 用来执行设置操作。我们能够在应用程序中任何地方注入和采用它。ISettingManager 定义了很多拿到设置值方法。
在这个场景下,最常用的方法是 GetSettingValue (或GetSettingValueAsync 为异步调用)。它将得到当前设置的基于默认值、 应用程序、 租户和用户设置范围的值(如设置范围之前的一段中所述)。例子:
//Getting a boolean value (async call)
var value1 = await SettingManager.GetSettingValueAsync<bool>("PassiveUsersCanNotLogin");
//Getting a string value (sync call)
var value2 = SettingManager.GetSettingValue("SmtpServerAddress");
结合项目来看,GetSettingValue 有泛型和异步版本,如上所示。也有方法来拿到特定的租户或用户的设置值或所有设置值的列表。
落到代码里,由于ISettingManager采用广泛,一些特定的基类 (如 ApplicationService、 DomainService 和 AbpController) 有一个名为 SettingManager的属性。如果我们从这些类继承,就无需显式地注入它。
(2)客户端
在这个场景下,如果定义设置时将 IsVisibleToClients 设置为 true,就能够在客户端采用 javascript得到它的当前值。abp.setting 命名空间定义所需的函数和对象。示例:
落到代码里,var currentColor = abp.setting.get("SiteColorPreference");也有 getInt 和 getBoolean 这样的方法。你能够采用 abp.setting.values 对象拿到所有值。请注意,如果你在服务器端更改设置,客户端不会知道这种变化,除非刷新页面或者以某种方式重新加载页面或者借助代码手动更新。
更改设置
理解这一步时,ISettingManager 定义了 ChangeSettingForApplicationAsync,ChangeSettingForTenantAsync 和 ChangeSettingForUserAsync 方法(以及同步版本)来更改应用程序,租户和用户分别的设置。
关于缓存
从实现思路看,缓存在服务器端设置管理,所以,我们不应直接采用存储库或数据库更新语句改变设置的值。