模块概述

基于YAML配置文件实现的一个配置模块

  • 难点:Lexical_cast的泛化与偏特化实现
  • 设计点:
    • 将配置项ConfigVar抽象出虚基类ConfigVarBase,使得Config可以通过多态机制统一管理配置类
    • 通过Lexical_cast,将“支持更多的类型”,变为“支持更多类型的Lexcial_cast偏特化实现”

配置项概述

一项配置应该包括以下要素:

  1. 名称:对应一个字符串,必须唯一,不能与其他配置项产生冲突。支持名称的层级,如tcp.connect.timeout
  2. 类型:支持基本类型、复杂类型(STL)和自定义类型。
  3. 值:配置项的值
  4. 默认值:考虑到用户不一定总是会显式地给配置项赋值,所以配置项最好有一个默认值(约定优于配置)
  5. 配置变更通知:一旦用户更新了配置值,那么应该通知所有使用了这项配置的代码,以便于进行一些具体的操作,比如重新打开文件,重新起监听端口等。
  6. 校验方法:更新配置时会调用校验方法进行校验,以保证用户不会给配置项设置一个非法的值。

配置模块功能概述

本项目配置模块具备的基本功能:

  1. 支持定义/声明配置项:也就是在提供配置名称、类型以及可选的默认值的情况下生成一个可用的配置项。
  2. 支持更新配置项的值:用户可以使用新的值来覆盖掉原来的值。
  3. 支持从预置的途径中加载配置项:一般是配置文件。支持基本数据类型与复杂数据类型的加载,比如直接从配置文件中加载一个map类型的配置项,或是直接从一个预定格式的配置文件中加载一个自定义结构体。
  4. 支持给配置项注册配置变更通知:用户可以为配置项注册回调函数,让程序知道某项配置被修改了,以便于进行一些操作。比如对于网络服务器而言,如果服务器端口配置变化了,那程序应该重新起监听端口。由于一项配置可能在多个地方引用,所以配置变更回调函数是一个数组的形式。
  5. 支持导出当前配置:将当前配置导出为YAML字符串

待实现的功能:

  1. 支持给配置项设置校验方法:配置项在定义时也可以指定一个校验方法,以保证该项配置不会被设置成一个非法的值,比如对于文件路径类的配置,可以通过校验方法来确保该路径一定存在。
  2. 从命令行参数或是网络服务器中加载配置项

Yaml

基本语法

Yaml书写方式比较简单,主要注意缩进问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Person information
person:
name: John Doe
age: 30
address:
street: 123 Main St
city: Somewhere
country: USA
hobbies:
- reading
- hiking
- coding

# A list of items
items:
- apple
- banana
- orange

键值对(Mapping)

键值对使用 : 进行分隔,键和值之间用空格分开。

1
2
name: John Doe
age: 30

列表(Sequence)

列表元素前使用 - 符号进行标记。

1
2
3
4
fruits:
- apple
- banana
- cherry

嵌套结构

YAML 使用缩进来表示嵌套结构。通常使用2个空格作为缩进。

1
2
3
4
5
6
person:
name: Alice
age: 28
address:
street: 123 Main St
city: Wonderland

注释(Comments)

注释以 # 开头,注释内容会被忽略。

1
2
# This is a comment
name: John Doe # Inline comment

多行字符串

使用 |> 来表示多行字符串。

1
2
3
4
5
6
7
8
description: |
This is a multi-line
string that keeps line breaks.

note: >
This is a folded
multi-line string that
turns into a single line.

复杂数据类型

YAML 支持复杂数据类型,如时间、日期、布尔值等。

1
2
date: 2024-12-25
boolean: true

yaml-cpp

项目中主要使用yaml-cpp来进行yaml文件的解析

依赖安装

  • 方法1:apt安装依赖:
1
sudo apt install libyaml-cpp-dev
  • 方法2:git从源码安装最新版本
1
2
3
4
5
6
git clone https://github.com/jbeder/yaml-cpp.git
cd yaml-cpp
mkdir build
cd build
cmake ..
make && make install

用法

本项目中只使用部分Api,对于yaml-cpp掌握下面这些即可:

YAML::NodeType

yaml-cpp会自动解析Yaml字符串,并将解析结果变为以下几种YAML::NodeType:

  • Scalar:简单类型
  • Map:键值对
  • Sequence:列表

Yaml的加载

  • YAML::Load:从字符串中加载YAML::Node
  • YAML::LoadFile:从文件中加载YAML::Node

配置模块设计

约定优于配置

约定优于配置的方式可以减少程序员做决定的数量,获得简单的好处,同时兼顾灵活性。

配置模块采用约定优于配置的思想,参考资料:

简单来说,约定优于配置的背景条件是,一般来说,程序所依赖的配置项都有一个公认的默认值,也就是所谓的约定。这点有可许多可以参考的例子,比如对于一个http网络服务器,服务端口通常都是80端口,对于配置文件夹路径,一般都是conf文件夹,对于数据库目录,一般都是db或data文件夹。对于这些具有公认约定的配置,就不需要麻烦程序员在程序跑起来后再一项一项地指定了,而是可以初始时就将配置项设置成对应的值。这样,程序员就可以**只修改那些约定之外的配置项,然后以最小的代价让程序跑起来**。

在代码上,约定优于配置的思路体现为所有的配置项在定义时都带一个的默认值,以下是一个sylar配置项的示例,这是一个int类型的配置项,名称为tcp.connect.timeout,初始值为5000。

1
2
static sylar::ConfigVar<int>::ptr g_tcp_connect_timeout = \
sylar::Config::Lookup("tcp.connect.timeout", 5000, "tcp connect timeout");

该配置项转换为YAML配置文件后如下:

1
2
3
tcp:
connect:
timeout: 10000

模块中的类

ConfigVarBase

配置项抽象类,定义了配置项公有的成员和方法:

  • 配置项的公有成员:
    • name:配置项的名称
    • description:配置项的描述
  • 配置项的公有方法,两个纯虚函数,由派生类负责实现:
    • toString:将配置项转换为YAML字符串
    • fromString:从YAML字符串中载入配置项

ConfigVar:

每个配置项对应一个ConfigVar对象

是一个模板类,有三个模板参数:

  • T:配置项的类型
  • FromStrToStr:两个仿函数,用于进行YAML字符串与配置项类型T之间的转换,是实现的重点,不同类型的配置项需要各自实现它们的偏特化版本

继承自ConfigVarBase(相当于定义了ConfigVarBase的一系列派生类),在其基础上实现了还需要实现以下功能:

  • 对配置项数值的更新/获取操作:setValue / getValue
  • 对配置项的监听回调:addListener / delListener

Config:

一个单例类(但单例的实现方式与普通的单例模式有所不同),使用map管理ConfigVar配置项

实现了以下方法:

  • Lookup:可以在容器中查找/新增配置项
  • LoadFromYaml:从YAML文件中载入配置项

模块实现

ConfigVarBase

抽象基类,简单定义了get/set方法,以及两个纯虚函数fromStringtoString

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
class ConfigVarBase{
public:
using ptr = std::shared_ptr<ConfigVarBase>;

ConfigVarBase(const std::string& name, const std::string& description = "")
:m_name(name)
,m_description(description){
}

virtual ~ConfigVarBase(){}

std::string getName() const { return m_name; }

std::string getDescription() const { return m_description; }

virtual std::string getTypename() const = 0;

// 将配置参数的值转换为字符串
virtual std::string toString() = 0;

// 从字符串中获取配置参数初始值
virtual bool fromString(const std::string& val) = 0;

private:
// 配置参数的名称
std::string m_name;
// 配置参数的描述
std::string m_description;
};

ConfigVar

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// FromStr和ToStr:两个仿函数,用于将字符串转换为数值类型 / 将数值转换为字符串
// FromStr: T operator(const std::string&)
// ToStr: std::string operator(const T&)
template<typename T, typename FromStr = Lexical_cast<std::string, T>
, typename ToStr = Lexical_cast<T, std::string>>
class ConfigVar: public ConfigVarBase{
public:
using ptr = std::shared_ptr<ConfigVar>;
using on_change_cb = std::function<void(const T& oldVal, const T& newVal)>;
using RWMutexType = RWMutex;

ConfigVar(const std::string& name
, const T& val
, const std::string& description = "")
: ConfigVarBase(name, description)
, m_val(val){
}

// 调用ToStr()将配置值转换为Yaml字符串返回
std::string toString() override {
try{
RWMutexType::ReadLock lock(m_mutex);
// return boost::lexical_cast<std::string>(m_val);
return ToStr()(m_val);
} catch(std::exception& e) {
SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ConfigVar::toString exception: "
<< e.what() << " convert: " << typeid(m_val).name() << " to string";
}
return "";
}

// 调用FromStr()将Yaml字符串解析为配置值
bool fromString(const std::string& val) override {
try{
// setValue 中进行了加锁操作,这里不需要额外加锁
// m_val = boost::lexical_cast<T>(val);
setValue(FromStr()(val));
return true;
} catch(std::exception& e) {
SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ConfiVar::fromString exception: "
<< e.what() << " convert string to " << typeid(m_val).name();
}
return false;
}

// 获取配置项的值
const T getValue() const {
RWMutexType::ReadLock lock(m_mutex);
return m_val;
}
// 设置配置项的值
void setValue(const T& val){
// 先读后写
{
// 使用{}来控制RAII类的作用范围
RWMutexType::ReadLock lock(m_mutex);
if(val == m_val){
return;
}
// 如果配置项的值发生了变化,通知所有回调函数
for(auto cb : m_cbs){
cb.second(m_val, val);
}
}
RWMutexType::WriteLock lock(m_mutex);
m_val = val;
}

std::string getTypename() const override { return typeid(T).name(); }

// 添加回调函数,返回该回调函数的唯一id
uint64_t addListener(on_change_cb cb){
// 自动创建回调函数对应的key
static uint64_t s_cbfun_id = 0;

RWMutexType::WriteLock lock(m_mutex);
++s_cbfun_id;
m_cbs[s_cbfun_id] = cb;
return s_cbfun_id;
};

void delListener(uint64_t key){
RWMutexType::WriteLock lock(m_mutex);
m_cbs.erase(key);
};

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;
};

void clearListener(){
RWMutexType::WriteLock lock(m_mutex);
m_cbs.clear();
};


private:
// 配置项的值
T m_val;
// 管理回调函数的map
std::map<uint64_t, on_change_cb> m_cbs;
// 读写锁
mutable RWMutexType m_mutex;
};

多线程安全

由于配置项一般为读多写少,所以使用读写锁保证ConfigVar的线程安全

Lexical_cast

支持更多类型,在于支持更多类型的Lexical_cast特化版本

一个仿函数类,用于封装boost::lexical_cast,支持ToStrFromStr的实现

1
2
typename FromStr = Lexical_cast<std::string, T>;
typename ToStr = Lexical_cast<T, std::string>;

Lexical_cast泛化与特化两个版本

  • 泛化版本,可直接用作简单类型的转换,或被其他特化版本递归调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // F:转换源类型;T:转换目标类型
    // 泛化的版本,可直接用作简单类型的转换,或被其他特化版本递归调用
    template<typename F, typename T>
    class Lexical_cast{
    public:
    T operator()(const F& val){
    return boost::lexical_cast<T>(val);
    }
    };
  • 特化版本,用于扩展支持更多类型的配置项,如STL、用户自定义类型

toString / fromString

调用模板传入的仿函数ToStr/FromStr即可,注意读写锁的上锁

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
// 调用ToStr()将配置值转换为Yaml字符串返回
std::string toString() override {
try{
RWMutexType::ReadLock lock(m_mutex);
// return boost::lexical_cast<std::string>(m_val);
return ToStr()(m_val);
} catch(std::exception& e) {
SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ConfigVar::toString exception: "
<< e.what() << " convert: " << typeid(m_val).name() << " to string";
}
return "";
}

// 调用FromStr()将Yaml字符串解析为配置值
bool fromString(const std::string& val) override {
try{
// setValue 中进行了加锁操作,这里不需要额外加锁
// m_val = boost::lexical_cast<T>(val);
setValue(FromStr()(val));
return true;
} catch(std::exception& e) {
SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ConfiVar::fromString exception: "
<< e.what() << " convert string to " << typeid(m_val).name();
}
return false;
}

回调函数

ConfigVar支持添加回调函数,并在配置项数值更新时自动通知所有的回调函数。ConfigVar使用一个map管理所有的回调函数,在添加回调函数时会自动分配一个id

1
2
3
4
5
6
7
8
9
10
// 添加回调函数,返回该回调函数的唯一id
uint64_t addListener(on_change_cb cb){
// 自动创建回调函数对应的key
static uint64_t s_cbfun_id = 0;

RWMutexType::WriteLock lock(m_mutex);
++s_cbfun_id;
m_cbs[s_cbfun_id] = cb;
return s_cbfun_id;
};

setValue中配置项值更新完成后通知所有回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 设置配置项的值
void setValue(const T& val){
// 先读后写
{
// 使用{}来控制RAII类的作用范围
RWMutexType::ReadLock lock(m_mutex);
if(val == m_val){
return;
}
// 如果配置项的值发生了变化,通知所有回调函数
for(auto cb : m_cbs){
cb.second(m_val, val);
}
}
RWMutexType::WriteLock lock(m_mutex);
m_val = val;
}

Config

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class Config{
public:
using ConfigVarMap = std::unordered_map<std::string, ConfigVarBase::ptr>;
using RWMutexType = RWMutex;

// 根据name在容器中查找配置项
template<typename T>
static typename ConfigVar<T>::ptr Lookup(const std::string& name){
// 这个Lookup版本只进行读操作,所以上读锁
RWMutexType::ReadLock lock(GetMutex());

auto it = GetDatas().find(name);
if(it != GetDatas().end()){
// dynamic_pointer_cast可以理解为作用于shared_ptr上的dynamic_cast
return std::dynamic_pointer_cast<ConfigVar<T>>(it->second);
}
return nullptr;
}

// 在容器中查找配置项,如果找不到就尝试新增配置项
template<typename T>
static typename ConfigVar<T>::ptr Lookup(const std::string& name,
const T& default_value, const std::string& description = ""){
auto& mp = GetDatas();
// 先上一个读锁,进行查询操作
{
RWMutexType::ReadLock lock(GetMutex());
auto it = mp.find(name);
if(it != GetDatas().end()){
return Config::pointer_cast<T>(name, it->second);
}
}

// 检查name中是否所有字符都合法
if(name.find_first_not_of("1234567890._abcdefghijklmnopqrstuvwxyz")
!= std::string::npos){
// name中存在非法字符,写入错误log并抛出invalid_argument异常
SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "Lookup name invalid: " << name;
throw std::invalid_argument(name);
}

// 如果查询失败,就尝试在容器中新增一个配置参数项
// 上写锁,进行插入操作
RWMutexType::WriteLock lock(GetMutex());
// 再次检查 name 是否存在,避免多线程重复插入的问题
auto it = mp.find(name);
if(it != mp.end()){
return Config::pointer_cast<T>(name, it->second);
}
typename ConfigVar<T>::ptr newConfigVar = std::make_shared<ConfigVar<T>>(name, default_value, description);
mp[name] = newConfigVar;
// SYLAR_LOG_DEBUG(SYLAR_LOG_ROOT()) << "Add config name: " << name << ", type: "<< typeid(T).name();
// SYLAR_LOG_DEBUG(SYLAR_LOG_ROOT()) << " -Add config: " << name;
return newConfigVar;
}

// 查找name,并返回其父类指针(利用多态性)
static ConfigVarBase::ptr LookupBase(const std::string& name);

// 从Yaml中加载配置
static void LoadFromYaml(const YAML::Node& root);

// Visit接收一个回调函数cb,对Config管理的所有配置项应用该回调函数
static void Visit(std::function<void(ConfigVarBase::ptr)> cb);

private:
// 封装 dynamic_pointer_cast 和类型检查逻辑操作
template<typename T>
static typename ConfigVar<T>::ptr pointer_cast(const std::string& name, ConfigVarBase::ptr val){
typename ConfigVar<T>::ptr tmp = std::dynamic_pointer_cast<ConfigVar<T>>(val);;
if(tmp){
SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << "Lookup name=" << name << "exists.";
return tmp;
} else {
SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "Lookup name=" << name << ", but type not <"
<< typeid(T).name() << ">, real type=<" << val->getTypename() << ">";
}
return nullptr;
}

// 使用静态成员函数获取数据,避免静态变量的初始化顺序问题(见Effective C++条款4)
static ConfigVarMap& GetDatas(){
static ConfigVarMap s_datas;
return s_datas;
}

// 因为读写锁应用的对象为静态变量,所以也需要使用静态成员函数获得读写锁
static RWMutexType& GetMutex(){
static RWMutexType s_mutex;
return s_mutex;
}
};

单例类实现

Config的单例实现方式有所不同。它不通过Sington来实现单例,而是将所有的成员函数都设置为静态

注意静态数据成员变量也转变为通过静态成员函数获取

  • 复习一下Effective C++条款4:

    Effective C++条款4——确定对象被使用前已被初始化

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;
}

Lookup

Lookup用于向map中查找/添加配置项,整体逻辑比较简单。注意一下几点即可:

  • 有两个重载版本的Lookup:

    • 版本1:只用于读操作

      1
      static typename ConfigVar<T>::ptr Lookup(const std::string& name)
    • 版本2:接受数值与描述,如果没有查询到对应的配置项则使用该数值与描述新增一个配置项

      1
      2
      static typename ConfigVar<T>::ptr Lookup(const std::string& name, 
      const T& default_value, const std::string& description = "")
  • 版本2在新增配置项前需要对名称与数值类型进行检查:

    • 名称检查:仅支持名称由 数字、字母(大小写不敏感)、‘.’和’_’构成

    • 类型检查:通过封装pointer_cast进行类型检查

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 封装 dynamic_pointer_cast 和类型检查逻辑操作
      template<typename T>
      static typename ConfigVar<T>::ptr pointer_cast(const std::string& name, ConfigVarBase::ptr val){
      typename ConfigVar<T>::ptr tmp = std::dynamic_pointer_cast<ConfigVar<T>>(val);;
      if(tmp){
      SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << "Lookup name=" << name << "exists.";
      return tmp;
      } else {
      SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "Lookup name=" << name << ", but type not <"
      << typeid(T).name() << ">, real type=<" << val->getTypename() << ">";
      }
      return nullptr;
      }

Visit

Visit接收一个回调函数cb,对Config管理的所有配置项应用该回调函数

1
2
3
4
5
6
7
void Config::Visit(std::function<void(ConfigVarBase::ptr)> cb){
RWMutexType::ReadLock lock(GetMutex());
auto& mp = GetDatas();
for(auto& i : mp){
cb(i.second);
}
}

LoadFromYaml

LoadFromYaml实现从YAML::Node中载入配置

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
// 从Yaml中导入配置
void Config::LoadFromYaml(const YAML::Node& root){
// 将Yaml的树状结构拍平为list(方便遍历)
// <name, yaml_node>
std::list<std::pair<std::string, const YAML::Node>> all_nodes;
ListAllMember("", root, all_nodes);

// 遍历Yaml,逐个解析配置项
for(auto& item : all_nodes){
std::string key = item.first;
if(key.empty()){
continue;
}

// 将键转换为全小写
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
// std::cout << key << std::endl;

// 在Config容器中查找一项配置项var(只查找注册过的配置项,如果没有找到不会自动添加配置项)
ConfigVarBase::ptr var = LookupBase(item.first);
// 从字符串获取配置项var的值
if(var){
if(item.second.IsScalar()){
var->fromString(item.second.Scalar());
} else {
std::stringstream ss;
ss << item.second;
// ss.str()为Yaml格式的字符串,交给fromString来解析
var->fromString(ss.str());
}
}
}
}

步骤:

  1. 将Yaml树结构拍平为list,便于后面遍历

    text
    1
    2
    3
    tcp:
    connect: ----拍平----> tcp.connect.timeout
    timeout: 10000
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 将Yaml的树结果拍平为list
    // ·prefix:配置项名称的前缀
    // ·output:传出参数
    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("1234567890._abcdefghijklmnopqrstuvwxyz")
    != std::string::npos){
    SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "Lookup name invalid: " << prefix;
    throw std::invalid_argument(prefix);
    }

    output.push_back(std::make_pair(prefix, node));

    if(node.IsMap()){
    // 递归地处理map
    for(auto& it : node){
    ListAllMember(prefix.empty() ? it.first.Scalar() : prefix + "." + it.first.Scalar(),
    it.second, output);
    }
    }
    }
  2. 遍历YAML::Node,逐个解析配置项,有两个注意点

    • 将配置项的键(名称)转换为全小写(大小写不敏感)
    • 只有已经存在于Config中的配置项才会被载入

STL支持

支持更多类型,在于支持更多类型的Lexical_cast特化版本

  • 实现方法:定义一个仿函数类Lexical_cast,以及它的一系列偏特化
  • 由于Lexical_cast的递归调用,实现天生支持STL的嵌套

STL的实现方式分为以下两大类

  • 将容器转化为YAML::NodeType::Sequence
    • 包括vector、list、set、unordered_set
  • 将容器转化为YAML::NodeType::Map
    • map、unordered_map(目前的实现只支持key为std::string)

vector、list、set、unordered_set

转化为YAML::NodeType::Sequence,以vector为例:

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
// vector -> Yaml格式字符串
template<typename T>
class Lexical_cast<std::vector<T>, std::string>{
public:
std::string operator()(const std::vector<T> vec){
YAML::Node node(YAML::NodeType::Sequence);
for(auto& i : vec){
// 递归地将vec的元素转换为Yaml字符串格式
node.push_back(YAML::Load(Lexical_cast<T, std::string>()(i)));
}
// 使用stringstream来接收Yaml格式字符串
std::stringstream ss;
ss << node;
return ss.str();
}
};

// Yaml格式字符串 -> vector
template<typename T>
class Lexical_cast<std::string, std::vector<T>>{
public:
std::vector<T> operator()(const std::string str){
// 使用YAML::Load解析Yaml格式字符串
YAML::Node nodes = YAML::Load(str);
std::vector<T> vec;
std::stringstream ss;
for(const auto& node : nodes){
// 使用使用stringstream来接收Yaml格式字符串
ss.str("");
ss << node;
// 递归地将Yaml格式字符串转换为vec的元素
vec.push_back(Lexical_cast<std::string, T>()(ss.str()));
}
return vec;
}
};

map、unordered_map

转化为YAML::NodeType::Map,以map为例:

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
// map,只支持key类型为std::string
// map -> Yaml格式字符串
template<typename T>
class Lexical_cast<std::map<std::string, T>, std::string>{
public:
std::string operator()(const std::map<std::string, T> mp){
YAML::Node node(YAML::NodeType::Map);
for(auto& i : mp){
node[i.first] = YAML::Load(Lexical_cast<T, std::string>()(i.second));
}
std::stringstream ss;
ss << node;
return ss.str();
}
};

// Yaml格式字符串 -> map
template<typename T>
class Lexical_cast<std::string, std::map<std::string, T>>{
public:
std::map<std::string, T> operator()(const std::string str){
YAML::Node nodes = YAML::Load(str);
std::map<std::string, T> mp;
std::stringstream ss;
for(const auto& node : nodes){
ss.str("");
ss << node.second;
mp.insert(std::make_pair(node.first.Scalar(),
Lexical_cast<std::string, T>()(ss.str())));
}
return mp;
}
};

自定义类型支持

同STL,只要提供自定义类型的fromString和toString,就可以让配置系统支持该自定义类型

例子:

  • 类定义

    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
    // 一个自定义类
    class Person{
    public:
    Person(const std::string& name = "", int age = 0, bool sex = false)
    : m_name(name), m_age(age), m_sex(sex){
    }

    std::string toString() const {
    std::stringstream ss;
    ss << "[Person: name=" << m_name << ", age=" << m_age << ", sex=" << (m_sex ? "female" : "male") << "]";
    return ss.str();
    }

    std::string getName() const { return m_name; }
    int getAge() const { return m_age; }
    bool getSex() const { return m_sex; }

    bool operator==(const Person& oth) const {
    return m_name == oth.m_name
    && m_age == oth.m_age
    && m_sex == oth.m_sex;
    }

    private:
    std::string m_name;
    int m_age;
    bool m_sex;
    };
  • fromString和toString

    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
    // 定义Person类的Lexical_cast偏特化版本,使得配置系统支持该自定义类型
    namespace sylar{

    // Yaml字符串 -> Person类
    template<>
    class Lexical_cast<std::string, Person>{
    public:
    Person operator()(const std::string& val){
    YAML::Node node = YAML::Load(val);
    Person p(node["name"].as<std::string>(),
    node["age"].as<int>(),
    node["sex"].as<bool>());
    return p;
    }
    };

    // Person类 -> Yaml字符串
    template<>
    class Lexical_cast<Person, std::string>{
    public:
    std::string operator()(const Person& val){
    YAML::Node node(YAML::NodeType::Map);
    node["name"] = val.getName();
    node["age"] = val.getAge();
    node["sex"] = val.getSex();
    std::stringstream ss;
    ss << node;
    return ss.str();
    }
    };

    }
  • 使用

    1
    2
    3
    4
    5
    6
    7
    // 自定义类
    sylar::ConfigVar<Person>::ptr g_person_config =
    sylar::Config::Lookup("class.person", Person("y", 0, false), "Person class");

    // 自定义类与STL的嵌套
    sylar::ConfigVar<std::map<std::string, Person>>::ptr g_person_map_config =
    sylar::Config::Lookup("class.persons", std::map<std::string, Person>({{"nobody", Person()}}), "Person class map");

模块使用

载入配置项

1
2
3
4
// YAML文件载入为YAML::Node
YAML::Node root = YAML::LoadFile("/conf/test.yml");
// 从YAML::Node中载入配置项
sylar::Config::LoadFromYaml(root);

添加/获取配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sylar::ConfigVar<int>::ptr g_int_value_config = 
sylar::Config::Lookup<int>("system.port", 8080, "system port");

sylar::ConfigVar<std::vector<int> >::ptr g_int_vec_value_config =
sylar::Config::Lookup("system.int_vec", std::vector<int>{1, 2}, "system int vector");

sylar::ConfigVar<std::list<int> >::ptr g_int_list_value_config =
sylar::Config::Lookup("system.int_list", std::list<int>{1, 2}, "system int list");

sylar::ConfigVar<std::set<int> >::ptr g_int_set_value_config =
sylar::Config::Lookup("system.int_set", std::set<int>{1, 2}, "system int set");

sylar::ConfigVar<std::unordered_set<int> >::ptr g_int_uset_value_config =
sylar::Config::Lookup("system.int_uset", std::unordered_set<int>{1, 2}, "system int unordered_set");

sylar::ConfigVar<std::map<std::string, int> >::ptr g_str_int_map_value_config =
sylar::Config::Lookup("system.str_int_map", std::map<std::string, int>{{"k",2}}, "system str int map");

sylar::ConfigVar<std::unordered_map<std::string, int> >::ptr g_str_int_umap_value_config =
sylar::Config::Lookup("system.str_int_umap", std::unordered_map<std::string, int>{{"k",2}}, "system str int map");

与日志系统的整合

LogAppenderDefine

LogAppenderDefine为定义Appender信息的结构体,结构体需要包含以下内容:

  1. type:表示Appender的类型,目前日志系统支持输出到终端/文件两种appender,type取值如下:
    • 0:未初始化
    • 1:输出到文件
    • 2:输出到终端
  2. pattern:表示输出格式的模板
  3. file:在type == 1时有效,记录输出文件的路径
  4. operator==:为配置系统提供结构体的比较方法,上面三个数据成员全相等才返回true
1
2
3
4
5
6
7
8
9
10
11
struct LogAppenderDefine{
int type = 0; // 1: File, 2: Stdout
std::string pattern;
std::string file;

bool operator==(const LogAppenderDefine& oth) const {
return type == oth.type
&& pattern == oth.pattern
&& file == oth.file;
}
};

LoggerDefine

LoggerDefine为记录Logger信息的结构体,结构体需要包含以下内容:

  1. name:Logger的名称
  2. level:Logger的级别,类型为LogLevel::Level
  3. appenders:管理LogAppenderDefine结构体的数组
  4. 重载关系运算符,包括operator==、operator!=、operator<
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 记录Logger相关信息的结构体
struct LoggerDefine{
std::string name;
LogLevel::Level level = LogLevel::UNKNOW;
std::vector<LogAppenderDefine> appenders;

bool operator==(const LoggerDefine& oth) const {
return name == oth.name
&& level == oth.level
&& appenders == oth.appenders;
}

bool operator!=(const LoggerDefine& oth) const {
return !(*this == oth);
}

bool operator<(const LoggerDefine& oth) const {
return name < oth.name;
}
};

Lexcial_cast偏特化

实现Lexical_cast偏特化,使用YAML::NodeType::Map管理。整体逻辑不难理解,注意一下YAML::Node的操作方法即可

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 偏特化Lexical_cast
// Yaml字符串 -> LoggerDefine
template<>
class Lexical_cast<std::string, LoggerDefine>{
public:
LoggerDefine operator()(const std::string& val){
YAML::Node node = YAML::Load(val);
LoggerDefine ld;
if(!node["name"].IsDefined()){
std::cerr << "log config error: name is null" << node << std::endl;
throw std::logic_error("log config name is null");
}
// 处理name和level
ld.name = node["name"].as<std::string>();
ld.level = LogLevel::FromString(node["level"].IsDefined() ? node["level"].as<std::string>() : "");
// 处理appenders
if(node["appenders"].IsDefined()){
for(auto a : node["appenders"]){
if(!a["type"].IsDefined()){
std::cerr << "log config error: appender type is null" << a << std::endl;
continue;
}
LogAppenderDefine lad;

// 不同的appender类型设置不同的type,FileLogAppender还需要获取输出的文件路径
std::string type = a["type"].as<std::string>();
if(type == "FileLogAppender"){
lad.type = 1;
if(!a["file"].IsDefined()){
std::cerr << "log config error: FileLogAppender filename is null" << a << std::endl;
continue;
}
lad.file = a["file"].as<std::string>();
} else if(type == "StdoutLogAppender"){
lad.type = 2;
} else {
std::cerr << "log config error: appender type is invalid" << a << std::endl;
continue;
}
// 以上appender都需要检测是否配置了formatter的pattern
if(a["pattern"].IsDefined()){
lad.pattern = a["pattern"].as<std::string>();
}
// 将处理完成的LogAppenderDefine加入appenders中
ld.appenders.push_back(lad);
}
}
return ld;
}
};

// LoggerDefine -> Yaml字符串
template<>
class Lexical_cast<LoggerDefine, std::string>{
public:
std::string operator()(const LoggerDefine& ld){
YAML::Node node(YAML::NodeType::Map);
node["name"] = ld.name;
node["level"] = LogLevel::ToString(ld.level);
std::stringstream ss_appenders;
for(const auto& lad : ld.appenders){
YAML::Node nlad(YAML::NodeType::Map);
if(lad.type == 1){
nlad["type"] = "FileLogAppender";
nlad["file"] = lad.file;
} else if (lad.type == 2){
nlad["type"] = "StdoutLogAppender";
}
if(!lad.pattern.empty()){
nlad["pattern"] = lad.pattern;
}
node["appenders"].push_back(nlad);
}
std::stringstream ss;
ss << node;
return ss.str();
}
};

日志模块配置项g_log_config

在log.h中注册日志模块的配置项:

1
2
3
// 注册 logs 配置项
ConfigVar<std::set<LoggerDefine>>::ptr g_log_config =
Config::Lookup("logs", std::set<LoggerDefine>(), "logs config");

回调函数

**全局对象在main函数前构造(其构造函数在main函数之前触发)**,我们可以利用这一特性为 g_log_config 配置项添加回调函数

回调函数接收两个set,一个为配置项旧值oldVal,一个为新值newVal。对比配置项的新旧值,完成logger的创建 / 修改 / 删除工作:

  • 创建 / 修改:创建/获取newVal中对应的日志器,根据LoggerDefine的新值设置日志器属性
  • 删除:如果日志器在oldVal中但不在newVal中,则说明该日志器需要被删除。进行的操作为逻辑删除(将Level设置为无法访问到的级别,并情况其appender)
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
struct LogIniter{
LogIniter(){
// SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << "g_log_config add callback";
g_log_config->addListener([](const std::set<LoggerDefine>& oldVal,
const std::set<LoggerDefine>& newVal){
// 监测以下三种情况:新增 / 修改 / 删除
for(auto& i : newVal){
auto it = oldVal.find(i);
Logger::ptr logger;
if(it == oldVal.end()){
// <新增>
logger = SYLAR_LOG_NAME(i.name);
} else {
if(i != *it){
// <修改>
logger = SYLAR_LOG_NAME(i.name);
} else {
continue;
}
}
// 根据newVal中的数据成员修改logger配置
// 设置level
logger->setLevel(i.level);
// 设置appenders
logger->clearAppenders();
for(auto &a : i.appenders){
LogAppender::ptr ap;
if(a.type == 1){
ap.reset(new FileLogAppender(a.file));
} else if(a.type == 2){
ap.reset(new StdoutLogAppender());
}
// pattern非空时设置自定义formatter,否则使用默认formatter
if(!a.pattern.empty()){
ap->setFormatter(LogFormatter::ptr(new LogFormatter(a.pattern)));
}
logger->addAppender(ap);
}
}

// <删除>
for(auto& i : oldVal){
auto it = newVal.find(i);
if(it == newVal.end()){
// 对配置文件中被删除的logger执行逻辑删除
Logger::ptr logger = SYLAR_LOG_NAME(i.name);
// 逻辑删除:将Level设置为访问不到的级别,并清空所有appender
logger->setLevel(LogLevel::NOTSET);
logger->clearAppenders();
}
}
});
}
};
// 全局对象在main函数前构造
static LogIniter __log_init;

配置文件

日志系统的配置系统可以如下形式:

1
2
3
4
5
6
7
8
logs:
- name: root
level: info
appenders:
- type: FileLogAppender
file: /home/mySylar/log.txt
- type: StdoutLogAppender
pattern: "%d%T%m%n"