主要用于定义/声明配置项,并且从配置文件中加载用户配置。
1 配置模块概述
主要用于定义/声明配置项,并且从配置文件中加载用户配置。一般而言,一项配置应该包括以下要素:
名称 :配置项的名称,对应一个字符串,必须唯一,不能与其他配置项产生冲突。
类型 :配置项的数据类型,可以是基本类型,但也应该支持复杂类型和自定义类型。
值 :配置项的值。
默认值 :配置项的默认值,考虑到用户不一定总是会显式地给配置项赋值,所以配置项最好有一个默认值。
配置变更通知 :一旦用户更新了配置值,那么应该通知所有使用了这项配置的代码,以便于进行一些具体的操作,比如重新打开文件,重新起监听端口等。
校验方法 :更新配置时会调用校验方法进行校验,以保证用户不会给配置项设置一个非法的值。
一个配置模块应具备的基本功能:
支持定义/声明配置项 :配置模块应该提供一种方法,允许用户声明和定义配置项。这包括提供配置的名称、数据类型以及可选的默认值。此外,应该支持在多个源文件中声明配置项,以确保在整个程序中都可以访问到这些配置。
支持更新配置项的值 :配置模块应该提供一种机制,允许用户更新配置项的值。这意味着用户可以通过代码动态地修改配置项的值,以满足程序运行时的需求。
支持从预置途径中加载配置项 :配置模块应该能够从预定义的途径(如配置文件、命令行参数、网络服务器等)加载配置项。这不仅包括基本数据类型的加载,还应该支持复杂数据类型的加载,如从配置文件中加载 map 类型的配置项或自定义结构体。
支持注册配置变更通知 :配置模块应该提供一种机制,允许用户注册配置变更通知。这意味着用户可以预先指定一个回调函数,当配置项发生变化时,配置模块会调用相应的回调函数通知用户,以便用户可以执行相应的操作。由于配置可能在多个地方引用,应该支持多个回调函数的注册。
支持给配置项设置校验方法 :配置模块应该允许用户为配置项设置校验方法,以确保配置项的值符合预期的规范。例如,用户可以为文件路径配置项设置一个校验方法,以确保指定的路径存在或符合特定的格式要求。
支持导出当前配置 :配置模块应该提供一种方法,允许用户导出当前的配置。这可以帮助用户将当前配置保存到文件中,或者用于程序的状态报告和调试。
配置模块的设计
采用约定优于配置 的思想,简单来说,约定优于配置的背景条件是,程序中的许多配置项通常具有公认的默认值,即约定。例如,对于一个HTTP
网络服务器,服务端口通常是80
端口;对于配置文件夹路径,一般是"conf"
文件夹;对于数据库目录,一般是"db"
或"data"
文件夹。由于这些配置具有公认的约定值,程序员无需在程序运行时逐项指定这些值,而是可以在程序初始化时将配置项设置为约定值。这样,程序员只需要修改那些超出约定范围的配置项,就能够以最小的代价使程序运行起来。
约定优于配置的方式能够减少程序员需要做的决定数量,获得简单的好处,同时也保持了一定的灵活性。因为程序员可以选择性地覆盖约定值,从而实现对程序行为的定制。
例如,通过以下方式设置协程栈大小,名称:fiber.stack_size
,默认值:128 * 1024
,描述:fiber stack size
:
1 2 static ConfigVar<uint32_t >::ptr g_fiber_stack_size = Config::Lookup <uint32_t >("fiber.stack_size" , 128 * 1024 , "fiber stack size" );
当对YAML
文件配置项做出改变时,也会改变相应的配置参数,此时,协程帧栈大小为256 * 1024
即256KB
:
1 2 fiber: stack_size: 256 * 1024
关于YAML
格式的介绍可参考YAML 入门教程 。
配置模块主要有以下几个类:
ConfigVarBase :配置项的基类,定义了配置项的公共成员和方法。它是一个虚基类,对每个配置项都包括名称和描述两项成员、以及toString
和fromString
纯虚函数方法。它并不包含配置项的类型和值,而是由继承类实现。
ConfigVar :具体的配置参数类,继承自ConfigVarBase
,是一个模板类,具有三个模板参数。第一个模板参数是配置项的类型T
,第二个和第三个模板参数FromStr
和ToStr
是仿函数,用于类型T
和YAML
字符串之间的相互转换。这两个模板参数有默认值,根据不同的类型T
有不同的偏特化实现。ConfigVar
类包含了一个T
类型的成员和一个变更回调函数数组。提供了setValue
和getValue
方法用于获取和更新配置值,并提供了addListener
和delListener
方法用于添加或删除配置变更的回调函数。
Config :ConfigVar
的管理类,负责管理所有的ConfigVar
对象,采用单例模式。提供了Lookup
方法,用于根据配置名称查询配置项。如果提供了默认值和描述信息,那么在未找到对应的配置项时,会自动创建一个对应的配置项。此外,Config
类还提供了LoadFromYaml
和LoadFromConfDir
方法,用于从YAML
对象或命令行选项指定的配置文件路径中加载配置。Config
类的所有成员变量和方法都是静态的,保证了全局只有一个实例。
这个配置模块的设计允许程序员定义并管理配置项,包括配置项的类型、默认值、描述信息等。同时提供了方便的方法来更新配置值、监听配置变更,并支持从不同来源加载配置。
2 ConfigVarBase
类
该类为抽象函数,提供三个纯虚函数供子类ConfigVar
实现:
1 2 3 4 5 6 7 8 virtual std::string toString () = 0 ;virtual bool fromString (const std::string& val) = 0 ;virtual std::string getTypeName () const = 0 ;
成员变量:
1 2 std::string m_name; std::string m_description;
2.1 成员函数
2.1.1 ConfigVarBase()
构造函数,初始化配置参数名称和描述,std::transform
被用于将字符串m_name
中的字母字符转换为小写形式并覆盖原来的字符串,所以不区分大小写。
1 2 3 4 5 ConfigVarBase (const std::string& name, const std::string& description = "" ) :m_name (name) ,m_description (description) { std::transform (m_name.begin (), m_name.end (), m_name.begin (), ::tolower); }
3 ConfigVar
类
成员变量:
1 2 3 RWMutexType m_mutex; T m_val; std::map<uint64_t , on_change_cb> m_cbs;
3.1 类型转换
1 2 3 4 5 6 7 8 template <class T , class FromStr = LexicalCast<std::string, T>, class ToStr = LexicalCast<T, std::string>>class ConfigVar : public ConfigVarBase { ... };
其中,FromStr
和ToStr
使用仿函数片特化的方式,实现不同类型T
与string
之间的相互转化,例如vector
与string
之间的转化,在转化的过程中,字符串格式都是以YAML
为标准。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 template <class F , class T >class LexicalCast {public : T operator () (const F& v) { return boost::lexical_cast <T>(v); } }; template <class T >class LexicalCast <std::string, std::vector<T>> {public : std::vector<T> operator () (const std::string& v) { YAML::Node node = YAML::Load (v); typename std::vector<T> vec; std::stringstream ss; for (size_t i = 0 ; i < node.size (); ++i) { ss.str ("" ); ss << node[i]; vec.push_back (LexicalCast <std::string, T>()(ss.str ())); } return vec; } }; template <class T >class LexicalCast <std::vector<T>, std::string> {public : std::string operator () (const std::vector<T>& v) { YAML::Node node (YAML::NodeType::Sequence) ; for (auto & i : v) { node.push_back (YAML::Load (LexicalCast <T, std::string>()(i))); } std::stringstream ss; ss << node; return ss.str (); } };
除了vector
类型外,还有list
、set
、unordered_set
、map
、unordered_map
等类型的转化,这里不再赘述。
3.2 成员函数
3.2.1 ConfigVar()
构造函数,给配置参数赋值,初始化配置参数名称、默认值和描述。
1 2 3 ConfigVar (const std::string& name ,const T& default_value ,const std::string& description = "" ) :ConfigVarBase (name, description) ,m_val (default_value) { }
3.2.2 toString()
若成功,返回转化后的string
,失败打出日志,异常以及值的类型。
try {} 里面的代码可能会抛出异常,如果抛出异常,会被catch捕获,然后执行catch里面的代码
1 2 3 4 5 6 7 8 9 10 11 12 std::string toString () override { try { RWMutexType::ReadLock lock (m_mutex) ; return ToStr ()(m_val); } catch (std::exception &e) { SYLAR_LOG_ERROR (SYLAR_LOG_ROOT ()) << "ConfigVar::toString exception " << e.what () << " convert: " << TypeToName <T>() << " to string" << " name=" << m_name; } return "" ; }
3.2.3 fromString()
从YAML String
初始化配置参数值,转换失败时抛出异常打印日志
1 2 3 4 5 6 7 8 9 10 11 bool fromString (const std::string& val) override { try { setValue (FromStr ()(val)); } catch (std::exception &e) { SYLAR_LOG_ERROR (SYLAR_LOG_ROOT ()) << "ConfigVar::fromString exception " << e.what () << " convert: string to " << TypeToName <T>() << " name=" << m_name << " - " << val; } return false ; }
3.2.4 getValue()
获取配置参数的值,加读锁保护
1 2 3 4 const T getValue () { RWMutexType::ReadLock lock (m_mutex) ; return m_val; }
3.2.5 setValue()
设置配置参数的值,如果参数值发生变化,调用回调函数
1 2 3 4 5 6 7 8 9 10 11 void setValue (const T& v) { { RWMutexType::ReadLock lock (m_mutex) ; if (v == m_val) return ; for (auto & i : m_cbs) { i.second (m_val, v); } } RWMutexType::WriteLock lock (m_mutex) ; m_val = v; }
3.2.6 getListener()
获取回调函数, key
为回调函数的唯一id
1 2 3 4 5 on_change_cb getListener (uint64_t key) { RWMutexType::ReadLock lock (m_mutex) ; auto it = m_cbs.find (key); return it == m_cbs.end () ? nullptr : it -> second; }
3.2.7 addListener()
添加变更回调函数,返回该回调函数的唯一id,用于删除
1 2 3 4 5 6 7 uint64_t addListener (on_change_cb cb) { static uint64_t s_fun_id = 0 ; RWMutexType::WriteLock lock (m_mutex) ; ++s_fun_id; m_cbs[s_fun_id] = cb; return s_fun_id; }
3.2.8 delListener()
删除回调函数
1 2 3 4 void delListener (uint64_t key) { RWMutexType::WriteLock lock (m_mutex) ; m_cbs.erase (key); }
3.2.9 clearListener()
清空所有回调函数
1 2 3 4 void clearListener () { RWMutexType::WriteLock lock (m_mutex) ; m_cbs.clear (); }
4 Config
类
成员变量:
1 2 3 4 5 6 7 8 9 10 11 static ConfigVarMap& GetDatas () { static ConfigVarMap s_datas; return s_datas; } static RWMutexType& GetMutex () { static RWMutexType s_mutex; return s_mutex; }
使用静态方法返回参数,保证初始化顺序
C++中,局部静态对象的生命周期是整个程序的,但是它们的初始化时机是在程序首次进入包含该静态对象定义的函数时进行的。
4.1 成员函数
4.1.1 Lookup(name, value, description)
用于获取或创建对应参数名的配置参数,如果参数名存在,返回对应的配置参数,否则创建一个新的配置参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 template <class T >static typename ConfigVar<T>::ptr Lookup (const std::string& name, const T& default_value, const std::string& description = "" ) { RWMutexType::WriteLock lock (GetMutex()) ; auto it = GetDatas ().find (name); if (it != GetDatas ().end ()) { auto tmp = std::dynamic_pointer_cast<ConfigVar<T>>(it -> second); if (tmp) { SYLAR_LOG_INFO (SYLAR_LOG_ROOT ()) << "Lookup name=" << name << " exists" ; return tmp; } else { SYLAR_LOG_ERROR (SYLAR_LOG_ROOT ()) << "Lookup name=" << name << " exists but type not " << TypeToName <T>() << " real_type=" << it->second->getTypeName () << " " << it->second->toString (); return nullptr ; } } if (name.find_first_not_of ("abcdefghijklmnopqrstuvwxyz._0123456789" ) != std::string::npos) { SYLAR_LOG_ERROR (SYLAR_LOG_ROOT ()) << "Lookup name invalid " << name; throw std::invalid_argument (name); } typename ConfigVar<T>::ptr v (new ConfigVar<T>(name, default_value, description)) ; GetDatas ()[name] = v; return v; }
4.1.2 Loadup(name)
查找配置参数, 若找到参数名为name
的配置参数,则返回对应的配置参数,否则返回nullptr
。
1 2 3 4 5 6 static typename ConfigVar<T>::ptr Lookup (const std::string& name) { RWMutexType::ReadLock lock (GetMutex()) ; auto it = GetDatas ().find (name); if (it == GetDatas ().end ()) return nullptr ; return std::dynamic_pointer_cast<ConfigVar<T>>(it -> second); }
4.1.3 LookupBase()
查找配置参数, 若找到参数名为name
的配置参数,则返回对应的配置参数的基类 (注意和上面函数的不同),否则返回nullptr
。
1 2 3 4 5 ConfigVarBase::ptr Config::LookupBase (const std::string &name) { RWMutexType::ReadLock lock (GetMutex()) ; auto it = GetDatas ().find (name); return it == GetDatas ().end () ? nullptr : it->second; }
4.1.4 LoadFromYaml()
使用YAML::Node初始化配置模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void Config::LoadFromYaml (const YAML::Node &root) { std::list<std::pair<std::string, const YAML::Node>> all_nodes; ListAllMember ("" , root, all_nodes); for (auto &i : all_nodes) { std::string key = i.first; if (key.empty ()) continue ; std::transform (key.begin (), key.end (), key.begin (), ::tolower); ConfigVarBase::ptr var = LookupBase (key); if (var) { if (i.second.IsScalar ()) var->fromString (i.second.Scalar ()); else { std::stringstream ss; ss << i.second; var->fromString (ss.str ()); } } } }
LoadFromYaml
函数首先调用ListAllMember
函数,将YAML
节点中的所有配置项提取出来,然后遍历所有的配置项,查找对应的配置参数,如果找到,则调用fromString
函数初始化配置参数的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static void ListAllMember (const std::string &prefix, const YAML::Node &node, std::list<std::pair<std::string, const YAML::Node>> &output) { if (prefix.find_first_not_of ("abcdefghijklmnopqrstuvwxyz._012345678" ) != std::string::npos) { SYLAR_LOG_ERROR (g_logger) << "Config invalid name: " << prefix << " : " << node; return ; } output.push_back (std::make_pair (prefix, node)); if (node.IsMap ()) { for (auto it = node.begin (); it != node.end (); ++it) { ListAllMember (prefix.empty () ? it->first.Scalar () : prefix + "." + it->first.Scalar (), it->second, output); } } }
4.1.5 Visit()
遍历所有的配置参数,调用回调函数。
1 2 3 4 5 6 7 void Config::Visit (std::function<void (ConfigVarBase::ptr)> cb) { RWMutexType::ReadLock lock (GetMutex()) ; ConfigVarMap &datas = GetDatas (); for (auto it = datas.begin (); it != datas.end (); ++it) { cb (it -> second); } }
5 配置与日志模块的结合
在项目中,配置模块和日志模块是紧密结合的,配置模块提供了一种机制,允许用户动态地修改日志模块的配置,以满足程序运行时的需求。例如,用户可以通过配置文件或命令行参数来修改日志的级别、输出格式、输出目的地等。
5.1 log.yaml
配置文件
1 2 3 4 5 6 7 8 9 10 11 12 logs: - name: root level: info appenders: - type: StdoutLogAppender pattern: "%d{%Y-%m-%d %H:%M:%S} %T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n" - name: system level: info appenders: - type: StdoutLogAppender - type: FileLogAppender file: ./system.txt
5.2 LogAppender
结构体重载==
定义日志输出器 结构体
1 2 3 4 5 6 7 8 9 struct LogAppenderDefine { int type = 0 ; std::string file; std::string pattern; bool operator ==(const LogAppenderDefine& oth) const { return type == oth.type && file == oth.file && pattern == oth.pattern; } };
5.3 LogDefine
结构体重载==
、<
定义日志器 结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct LogDefine { std::string name; LogLevel::Level level = LogLevel::NOTSET; std::vector<LogAppenderDefine> appenders; bool operator ==(const LogDefine& oth) const { return name == oth.name && level == oth.level && appenders == oth.appenders; } bool operator <(const LogDefine& oth) const { return name < oth.name; } bool isValid () const { return !name.empty (); } };
5.4 string
TO LogDefine
将YAML
格式的字符串反序列化为一个LogDefine
对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 template <>class LexicalCast <std::string, LogDefine> {public : LogDefine operator () (const std::string& v) { YAML::Node node = YAML::Load (v); LogDefine ld; if (!node["name" ].IsDefined ()) { std::cout << "log config error : name is null, " << node << std::endl; throw std::logic_error ("log config error : name is null" ); } ld.name = node["name" ].as <std::string>(); ld.level = LogLevel::FromString (node["level" ].IsDefined () ? node["level" ].as <std::string>() : "" ); if (node["appenders" ].IsDefined ()) { for (size_t i = 0 ; i < node["appenders" ].size (); i++) { auto a = node["appenders" ][i]; if (!a["type" ].IsDefined ()) { std::cout << "log appender config error : appender type is null, " << a << std::endl; continue ; } std::string type = a["type" ].as <std::string>(); LogAppenderDefine lad; if (type == "FileLogAppender" ) { lad.type = 1 ; if (!a["file" ].IsDefined ()) { std::cout << "log appender config error : file appender is null, " << a << std::endl; continue ; } lad.file = a["file" ].as <std::string>(); if (a["pattern" ].IsDefined ()) lad.pattern = a["pattern" ].as <std::string>(); } else if (type == "StdoutLogAppender" ) { lad.type = 2 ; if (a["pattern" ].IsDefined ()) lad.pattern = a["pattern" ].as <std::string>(); } else { std::cout << "log appender config error : appender type is invalid, " << a << std::endl; continue ; } ld.appenders.push_back (lad); } } return ld; } };
5.5 LogDefine
TO string
将日志定义对象转换成一个YAML格式的字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template <>class LexicalCast <LogDefine, std::string> {public : std::string operator () (const LogDefine &i) { YAML::Node n; n["name" ] = i.name; n["level" ] = LogLevel::ToString (i.level); for (auto &a : i.appenders) { YAML::Node na; if (a.type == 1 ) { na["type" ] = "FileLogAppender" ; na["file" ] = a.file; } else if (a.type == 2 ) { na["type" ] = "StdoutLogAppender" ; if (!a.pattern.empty ()) { na["pattern" ] = a.pattern; } n["appenders" ].push_back (na); } std::stringstream ss; ss << n; return ss.str (); } };
5.6 初始化Log
使用YAML
配置文件初始化日志系统,并确保当日志配置发生变化时,系统能够相应地更新日志设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 sylar::ConfigVar<std::set<LogDefine>>::ptr g_log_defines = sylar::Config::Lookup ("logs" , std::set <LogDefine>(), "logs config" ); struct LogIniter { LogIniter () { g_log_defines -> addListener ([](const std::set<LogDefine> &old_value, const std::set<LogDefine> &new_value){ SYLAR_LOG_INFO (SYLAR_LOG_ROOT ()) << "on log config changed" ; for (auto &i : new_value) { auto it = old_value.find (i); sylar::Logger::ptr logger; if (it == old_value.end ()) logger = SYLAR_LOG_NAME (i.name); else { if (!(i == *it))logger == SYLAR_LOG_NAME (i.name); else continue ; } logger -> setLevel (i.level); logger -> clearAppenders (); for (auto &a : i.appenders) { sylar::LogAppender::ptr ap; if (a.type == 1 ) ap.reset (new FileLogAppender (a.file)); else if (a.type == 2 ) { if (!sylar::EnvMgr::GetInstance () -> has ("d" )) ap.reset (new StdoutLogAppender); else continue ; } if (!a.pattern.empty ()) ap -> setFormatter (LogFormatter::ptr (new LogFormatter (a.pattern))); else ap -> setFormatter (LogFormatter::ptr (new LogFormatter)); logger -> addAppender (ap); } } for (auto &i : old_value) { auto it = new_value.find (i); if (it == new_value.end ()) { auto logger = SYLAR_LOG_NAME (i.name); logger -> setLevel (LogLevel::NOTSET); logger -> clearAppenders (); } } }); } };
6 总结
通过YAML
配置文件可以配置系统的参数,当设置新值时,可以通过回调函数更新系统配置。过程如下:
在main
之前就通过static LogIniter __log_init;
添加了变化回调函数。
使用YAML::Node root = YAML::LoadFile("sylar/bin/conf/log.yml");
加载文件。
使用sylar::Config::LoadFromYaml(root)
初始化配置模块,并会调用fromString()
将解析出的node
从string
转化为相应的类型,其中会调用setValue
设置参数值并且调用变化回调函数更新logger
的参数。