C++编写配置文件解析模块(基于JSON格式)
1、环境准备
本文是基于bifang框架的配置模块的,可以看这里 bifang框架运行环境搭建入门指南 将代码环境搭建起来,拉下代码之后就可以看里面的配置模块了,主要是src/config.cpp和src/config.h这两个文件
2、JSON-CPP简单使用
由于配置文件的格式是json的,所以有必要了解一下json格式,网上教程很多,这里就不赘述了。代码中使用的是JSON-CPP来解析json格式的,使用的版本比较新,具体的版本忘记了,可能使用起来会和老版本的不太一样(因为代码里面有宏提示某部分代码已经不建议使用了),所以下面举两个简单的例子说明JSON-CPP使用方法
2.1、将json格式字符串转换为Json::Value
Json::Value root;
Json::String errs;
Json::CharReaderBuilder reader;
std::unique_ptr<Json::CharReader> const jsonReader(reader.newCharReader());
if (!jsonReader->parse(v.c_str(), v.c_str() + v.length(), &root, &errs))
{
std::stringstream ss;
ss << message << " strerror: " << errs;
throw std::logic_error(ss.str());
}
2.2、将Json::Value转换为json格式字符串
std::stringstream ss;
ss << root;
std::string v = ss.str()
3、序列化和反序列化的实现
3.1、通用序列化转换模板
可以看到下面代码使用了boost的lexical_cast来实现一些基本类型的转换(比如std::string转int、float),这也就是为什么之前搭建环境的时候要安装boost依赖的原因了,这里会使用到
template<class F, class T>
struct LexicalCast
{
/**
* brief: 类型转换
* param: v 源类型值
* return: 返回v转换后的目标类型
* exception: 当类型不可转换时抛出异常
*/
T operator()(const F& v)
{
return boost::lexical_cast<T>(v);
}
};
3.2、序列化反序列化通用代码块宏定义
如下所示,将每个序列化和反序列化会使用到的通用代码用宏封装了起来,实现的功能也就是 std::string->Json::Value 和 Json::Value->std::string
#define SEQUENCE(root, v, message) \
Json::Value root; \
Json::String errs; \
Json::CharReaderBuilder reader; \
std::unique_ptr<Json::CharReader> const jsonReader(reader.newCharReader()); \
if (!jsonReader->parse(v.c_str(), v.c_str() + v.length(), &root, &errs)) \
{ \
std::stringstream ss; \
ss << message << " strerror: " << errs; \
throw std::logic_error(ss.str()); \
}
#define RESEQUENCE(root) \
std::stringstream ss; \
ss << root; \
return ss.str()
3.3、实现std::vector和json格式字符串的转换
序列化,可以看到就是将std::vector的数据一个一个压进去Json::Value(数组类型)里面,然后将其转换为字符串输出
template<class T>
struct LexicalCast<std::vector<T>, std::string>
{
std::string operator()(const std::vector<T>& v)
{
Json::Value root(Json::arrayValue);
for (auto& i : v)
root.append(Json::Value(LexicalCast<T, std::string>()(i)));
RESEQUENCE(root);
}
};
反序列化,可以看到就是将json格式字符串转为Json::Value(数组类型),然后将其一个一个压进去std::vector里,最后输出
template<class T>
struct LexicalCast<std::string, std::vector<T> >
{
std::vector<T> operator()(const std::string& v)
{
SEQUENCE(root, v, "json string(vector) is illegal!");
typename std::vector<T> vec;
std::stringstream ss;
for (uint32_t i = 0; i < root.size(); i++)
{
ss.str("");
ss << root[i];
vec.push_back(LexicalCast<std::string, T>()(ss.str()));
}
return vec;
}
};
3.4、实现std::map和json格式字符串的转换
序列化,可以看到就是将std::map的数据一个一个压进去Json::Value(对象类型)里面,然后将其转换为字符串输出
template<class T>
struct LexicalCast<std::map<std::string, T>, std::string>
{
std::string operator()(const std::map<std::string, T>& v)
{
Json::Value root(Json::objectValue);
for (auto& i : v)
root[i.first] = Json::Value(LexicalCast<T, std::string>()(i.second));
RESEQUENCE(root);
}
};
反序列化,可以看到就是将json格式字符串转为Json::Value(对象类型),然后将其一个一个压进去std::map里,最后输出
template<class T>
struct LexicalCast<std::string, std::map<std::string, T> >
{
std::map<std::string, T> operator()(const std::string& v)
{
SEQUENCE(root, v, "json string(map) is illegal!");
typename std::map<std::string, T> _map;
std::stringstream ss;
auto names = root.getMemberNames();
for (auto& i : names)
{
ss.str("");
ss << root[i];
_map[i] = LexicalCast<std::string, T>()(ss.str());
}
return _map;
}
};
3.5、其他类型数据的转换
代码中还实现了其他stl容器与json字符串的转换,这里就不赘述了,与上面的实现差不多。从上面的例子可以看出,只要实现了自定义数据结构的序列化和反序列化的,就能支持自定义配置文件格式,具体大家可以去看一下代码中日志配置文件、tcp服务配置文件、数据库配置文件的序列化和反序列化、数据结构定义以及配置文件的格式。接下来以数据库配置文件为例简单分析一下
配置文件格式
"mysql" :
{
"sql1" :
{
"host" : "127.0.0.1",
"port" : 3306,
"user" : "root",
"passwd" : "123456",
"dbname" : "mysql",
"poolSize" : 15
}
}
自定义配置类
struct MySqlConf
{
typedef std::shared_ptr<MySqlConf> ptr;
std::string host;
int port;
std::string user;
std::string passwd;
std::string dbname;
uint32_t poolSize = 10;
bool operator==(const MySqlConf& oth) const
{
return host == oth.host
&& port == oth.port
&& user == oth.user
&& passwd == oth.passwd
&& dbname == oth.dbname
&& poolSize == oth.poolSize;
}
};
序列化代码,将MySqlConf的成员赋值到Json::Value(对象类型)中,与配置文件一致
template<>
struct LexicalCast<MySqlConf, std::string>
{
std::string operator()(const MySqlConf& conf)
{
Json::Value root;
root["host"] = conf.host;
root["port"] = conf.port;
root["user"] = conf.user;
root["passwd"] = conf.passwd;
root["dbname"] = conf.dbname;
root["poolSize"] = conf.poolSize;
RESEQUENCE(root);
}
};
反序列化代码,将json字符串转换为Json::Value(对象类型),然后将对象中各个成员赋值到MySqlConf中,与配置文件一致
template<>
struct LexicalCast<std::string, MySqlConf>
{
MySqlConf operator()(const std::string& v)
{
SEQUENCE(root, v, "json string(MySqlConf) is illegal!");
MySqlConf conf;
if (root.isMember("host"))
conf.host = root["host"].asString();
if (root.isMember("port"))
conf.port = root["port"].asInt();
if (root.isMember("user"))
conf.user = root["user"].asString();
if (root.isMember("passwd"))
conf.passwd = root["passwd"].asString();
if (root.isMember("dbname"))
conf.dbname = root["dbname"].asString();
if (root.isMember("poolSize"))
conf.poolSize = root["poolSize"].asInt();
return conf;
}
};
4、配置器类的设计
配置类要实现的功能由配置项的初始化,获取,以及变更通知功能
4.1、ConfigBase - 配置基类
如下所示,配置基类有两个成员,一个是参数名称,另一个是参数描述,还有三个虚函数由派生类实现(参数名一律转换为小写字母)
class ConfigBase
{
public:
typedef std::shared_ptr<ConfigBase> ptr;
/**
* brief: 构造函数
* param: name 参数名称(自动转换为小写, 故参数名称不区分大小写)
* description 参数描述
*/
ConfigBase(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);
}
virtual ~ConfigBase() {}
/**
* brief: 返回配置参数名称
*/
const std::string& getName() const { return m_name; }
/**
* brief: 返回配置参数的描述
*/
const std::string& getDescription() const { return m_description; }
/**
* brief: 返回配置参数值的类型名称
*/
virtual std::string getTypeName() const = 0;
/**
* brief: 将配置参数转成字符串
*/
virtual std::string toString() = 0;
/**
* brief: 从字符串初始化值
*/
virtual bool fromString(const std::string& val) = 0;
protected:
// 参数名称
std::string m_name;
// 参数描述
std::string m_description;
};
4.2、Config - 配置类
可以看到该类有三个成员,分别是读写锁,参数值,变更回调函数组。可以看到toString和fromString的实现是基于前面序列化反序列化模板的,使用setValue后逐个调用变更回调函数,也就实现了变更通知功能
template<class T, class FromStr = LexicalCast<std::string, T>,
class ToStr = LexicalCast<T, std::string> >
class Config : public ConfigBase
{
public:
typedef std::shared_ptr<Config> ptr;
typedef RWMutex RWMutexType;
typedef std::function<void(const T& old_value, const T& new_value)> OnChangeCb;
/**
* brief: 通过参数名, 参数值, 描述构造Config
* param: name 参数名称
* default_value 参数默认值
* description 参数描述
*/
Config(const std::string& name,
const T& default_value,
const std::string& description = "")
:ConfigBase(name, description)
,m_val(default_value)
{
}
/**
* brief: 将参数值转换成 JSON String
*/
std::string toString() override
{
try
{
RWMutexType::ReadLock lock(m_mutex);
return ToStr()(m_val);
}
catch (std::exception& e)
{
SystemLogger();
log_error << "Config::toString exception " << e.what()
<< ", convert: " << type_to_name<T>() << " to string"
<< ", name=" << m_name;
}
return "";
}
/**
* brief: 从 JSON String 转成参数的值
*/
bool fromString(const std::string& val) override
{
try
{
setValue(FromStr()(val));
}
catch (std::exception& e)
{
SystemLogger();
log_error << "Config::fromString exception " << e.what()
<< ", convert: string to " << type_to_name<T>()
<< ", name=" << m_name << ": " << val;
}
return false;
}
/**
* brief: 获取当前参数的值
*/
const T getValue()
{
RWMutexType::ReadLock lock(m_mutex);
return m_val;
}
/**
* brief: 设置当前参数的值
* details: 如果参数的值有发生变化, 则通知对应的注册回调函数
*/
void setValue(const T& v)
{
{
RWMutexType::ReadLock lock(m_mutex);
if (v == m_val)
return;
for (auto& i : m_cbs)
i(m_val, v);
}
RWMutexType::WriteLock lock(m_mutex);
m_val = v;
}
/**
* brief: 返回参数值的类型名称(typeinfo)
*/
std::string getTypeName() const override { return type_to_name<T>(); }
public:
/**
* brief: 添加变化回调函数
*/
void addCallback(OnChangeCb cb)
{
RWMutexType::WriteLock lock(m_mutex);
m_cbs.push_back(cb);
}
/**
* brief: 清理所有的回调函数
*/
void clearCallback()
{
RWMutexType::WriteLock lock(m_mutex);
m_cbs.clear();
}
private:
// RWmutex
RWMutexType m_mutex;
// 参数值
T m_val;
// 变更回调函数组
std::vector<OnChangeCb> m_cbs;
};
4.3、ConfigManager - 配置器管理类
从代码中可以看到,所有配置项都是放在类成员m_datas里面,get方法也是根据名称从其中取出对应的Config的。其他的都比较简单,下面重点讲一下载入配置项的两个load方法
class ConfigManager
{
public:
typedef RWMutex RWMutexType;
typedef std::unordered_map<std::string, ConfigBase::ptr> ConfigMap;
/**
* brief: 获取对应参数名的配置参数(若不存在则自动创建, 并用default_value赋值)
* param: name 配置参数名称
* default_value 参数默认值
* description 参数描述
* return: 返回对应的配置参数, 如果参数名存在但是类型不匹配则返回nullptr
*/
template<class T>
typename Config<T>::ptr get(const std::string& name,
const T& default_value,
const std::string& description = "")
{
RWMutexType::WriteLock lock(m_mutex);
auto it = m_datas.find(name);
if (it != m_datas.end())
{
SystemLogger();
auto tmp = std::dynamic_pointer_cast<Config<T> >(it->second);
if (tmp)
{
log_debug << "Lookup name=" << name << " exists";
return tmp;
}
else
{
log_debug << "Lookup name=" << name << " exists but type not "
<< type_to_name<T>() << ", real_type="
<< it->second->getTypeName() << " " << it->second->toString();
return nullptr;
}
}
typename Config<T>::ptr v(new Config<T>(name, default_value, description));
m_datas[v->getName()] = v;
return v;
}
/**
* brief: 查找配置参数
* param: name 配置参数名称
* return: 返回配置参数名为name的配置参数
*/
template<class T>
typename Config<T>::ptr get(const std::string& name)
{
auto it = m_datas.find(name);
return it == m_datas.end() ? nullptr : std::dynamic_pointer_cast<Config<T> >(it->second);
}
/**
* brief: 遍历配置模块里面所有配置项
* param: cb 配置项回调函数
*/
void visit(std::function<void(ConfigBase::ptr)> cb)
{
RWMutexType::ReadLock lock(m_mutex);
for (auto& it : m_datas)
cb(it.second);
}
/**
* brief: 从Json::Value中加载配置模块
*/
void load(Json::Value& root);
/**
* brief: 读取path文件夹里面的配置文件用以加载配置模块
*/
void load(const std::string& path);
private:
// RWmutex
RWMutexType m_mutex;
// Config列表
ConfigMap m_datas;
};
首先介绍一下我配置文件与配置项之间的转换规则,如下所示,还是以sql的配置文件举例,要获取sql1的数据,对应的配置项名称为mysql.sql1,要获取host则是mysql.sql1.host,类与类一层一层的关系用.来表示,则可以写出下面的解析代码listAllMember
"mysql" :
{
"sql1" :
{
"host" : "127.0.0.1",
"port" : 3306,
"user" : "root",
"passwd" : "123456",
"dbname" : "mysql",
"poolSize" : 15
}
}
listAllMember的实现比较简单,递归解析json格式,遇到json对象类型则继续递归解析,非对象类型则结束当前层次的解析。这样的解析存在一个问题,就是会会往配置管理器中压入一些根本用不到的数据,还是用上面sql配置文件的例子来说明,经过解析之后,势必会出现很多配置项,比如mysql、mysql.sql1、mysql.sql1.host、mysql.sql1.port等等,由于我们事先无法知道究竟哪一部分时代码要用的,所以只能全部将他们作为独立的配置项提取出来了,由于没用的配置项在接下来的load方法中会被剔除,所以这里多出来的那些配置项只会影响一开始读取配置文件的速度而已,对运行时的速度和内存占用没有影响,所以没必要优化这部分内容(事实上对于这个例子来说,除了mysql之外的配置项全部没有意义,因为sql的配置文件是实现了自己特使的序列化和反序列化的,所以该配置文件的其他子配置项在代码中根本用不到,也不会被载入配置器管理类)。
static void listAllMember(std::string prefix, const Json::Value& root,
std::vector<std::pair<std::string, const Json::Value> >& nodes)
{
std::transform(prefix.begin(), prefix.end(), prefix.begin(), ::tolower);
// 会多压入一些无用的数据, 考虑到map配置点和配置文件并非频繁使用, 暂不优化
nodes.push_back(std::make_pair(prefix, root));
if (root.isObject())
{
auto names = root.getMemberNames();
for (auto& i : names)
listAllMember(prefix.empty() ? i : prefix + "." + i, root[i], nodes);
}
}
接下来时load方法的实现,比较简单,用listAllMember将配置项取出来,然后将有效地配置信息更新到配置器管理类中,无效的剔除
void ConfigManager::load(Json::Value& root)
{
std::vector<std::pair<std::string, const Json::Value> > nodes;
listAllMember("", root, nodes);
for (auto& i : nodes)
{
if (i.first.empty())
continue;
auto it = m_datas.find(i.first);
ConfigBase::ptr var = it == m_datas.end() ? nullptr : it->second;
if (var)
{
if (i.second.isString()) // 防止字符串值打出双引号, 只能对其单独处理
{
var->fromString(i.second.asString());
// 打印配置点
//std::cout << i.first << "=" << i.second.asString() << std::endl;
}
else
{
std::stringstream ss;
ss << i.second;
var->fromString(ss.str());
// 打印配置点
//std::cout << i.first << "=" << ss.str() << std::endl;
}
}
}
}
这个是load方法的重载实现,实现了读取文件夹中全部配置文件,然后载入配置的功能,实现比较简单,大家有兴趣的可以看一看
void ConfigManager::load(const std::string& path)
{
std::string absoulte_path = FileUtil::PureDirname(path);
absoulte_path = EnvMgr::GetInstance()->getAbsolutePath(absoulte_path);
std::vector<std::string> files;
FileUtil::ListAllFile(files, absoulte_path, ".json");
for (auto& i : files)
{
try
{
Json::Value root;
Json::String errs;
Json::CharReaderBuilder reader;
std::ifstream ifs;
ifs.open(i);
if (!Json::parseFromStream(reader, ifs, &root, &errs))
{
log_error << "parse json err " << errs;
throw std::logic_error("read file:" + i + " error!");
}
load(root);
//log_debug << "LoadConfFile file=" << i << " ok";
}
catch (...)
{
log_error << "LoadConfFile file=" << i << " failed";
}
}
}
5、总结
大家如果想知道更多关于本文编写的配置器的使用方法,可以 点击这里 下载我的开源框架去看一下里面的源码,相信一定会有所收获的,也麻烦大家顺手点一下star,谢谢