server.xml的构成和内存表现形式
server.xml中的内容,简要来说,主要有<system>、<user>和<firewall>这三部分。
<dble:server xmlns:dble="http://dble.cloud/">
<system>
<property name="prop_name">prop_value</property>
...
</system>
<user name="login_user_name">
<property name="prop_name">prop_value</property>
...
<privileges check="true_or_false">
<schema name="this_user_rights_on_this_schema" dml="">
<table name="this_user_rights_on_this_table" dml="" />
...
</schema>
...
</privileges>
</user>
...
<firewall>
<whitehost>
<host host="user_host" user="user_names" />
...
</whitehost>
<blacklist check="">
<property name="prop_name">prop_value</property>
...
</blacklist>
</firewall>
</server>
相应的,这三部分被分别加载成com.actiontech.dble.config.model
中的SystemConfig
、UserConfig
和FirewallConfig
这三个类。
配置文件元素 | 对应类 | 配置的内容 |
---|---|---|
<system> | SystemConfig | DBLE服务器配置,例如服务端口、管理端口等 |
<user> | UserConfig | 访问DBLE的用户配置,例如用户名、密码、允许访问哪些库or表等 |
<firewall> | FirewallConfig | 访问权限配置,例如基于用户的机器IP来允许/禁止访问 |
加载操作的代码入口
无论是server.xml的哪部分,都是在XMLConfigure
类的构造函数实例化XMLServerLoader
的过程中完成加载的。
com.actiontech.dble.config.loader.xml.XMLConfigure
com.actiontech.dble.config.loader.xml.XMLServerLoader
而XMLServerLoader
的构造函数的工作思路是,先分配空的内存对象(SystemConfig
、UserConfig
和FirewallConfig
),然后再用这个类的load()
函数来“填充”它们。
public XMLServerLoader() {
this.system = new SystemConfig();
this.users = new HashMap<>();
this.firewall = new FirewallConfig();
this.load();
}
而load()
的工作思路就也很单纯:先用ConfigUtil.getDocument()
把server.xml读入到内存里,然后再用loadSystem()
、loadUsers()
和loadFirewall()
来提取出<system>
、<user>
和<firewall>
的具体内容,填充到对应的对象里。
private void load() {
// ...
dtd = ResourceUtil.getResourceAsStream("/server.dtd");
xml = ResourceUtil.getResourceAsStream("/server.xml");
Element root = ConfigUtil.getDocument(dtd, xml).getDocumentElement();
loadSystem(root);
loadUsers(root);
loadFirewall(root);
// ...
}
把server.xml文件载入内存——ConfigUtil.getDocument()
该方法利用JAR包内(源代码中project_loc/src/main/resources文件夹)的server.dtd,进行带DTD校验的XML读取。
最终,将server.xml文件整个读取成一个DOM(Document Object Model)对象,缓存在内存中。
SystemConfig的加载过程——loadSystem()
这个方法虽然使用了DOM操作、反射、Java Bean这些比较复杂的技术,但它的工作思路其实非常朴素:
提取
<system>
中的配置项<property>
的清单提取
SystemConfig
类中的可配置属性的清单把可配置属性清单过一遍,从
<property>
清单中找到同名配置项,把值赋给SystemConfig
NodeList list = root.getElementsByTagName("system");
for (int i = 0, n = list.getLength(); i < n; i++) {
Node node = list.item(i);
if (node instanceof Element) {
Map<String, Object> props = ConfigUtil.loadElements((Element) node);
ParameterMapping.mapping(system, props);
}
}
下面是相关的技术细节:
提取<system>
中的配置项<property>
的清单,需要两步:
通过
org.w3c.dom.Element.getElementsByTagName()
,定位到server.xml的DOM的<system>
标签调用
com.actiontech.dble.config.util.ConfigUtil.loadElements()
,将每一个<property>
标签创建成一个键值对(name, content),放到同一个Map<String, Object>
集合里
ParameterMapping.mapping()
则会搞定剩余的工作:
com.actiontech.dble.config.util.ParameterMapping.getDescriptors()
找出ServerConfig
中的可配置属性(符合Java Bean规范,即有对应的公开getter和setter方法),将它们放到一个数组中ParameterMapping.mapping()
现在手上有了两套清单:一个是server.xml里<server>标签的所有<property>Map<String, Object> parameter
,另一个是ServerConfig
的所有属性PropertyDescriptor[] pds
。于是,剩下来的工作就是把parameter
中的值,根据pds
,赋值给ServerConfig
实例中的同名属性。
// 取一个SystemConfig属性
for (PropertyDescriptor pd : pds) {
// 找同名的<property>标签
Object obj = parameter.get(pd.getName());
Object value = obj;
// 确定当前SystemConfig属性的数据类型
Class<?> cls = pd.getPropertyType();
if if (obj instanceof String) {
String string = (String) obj;
if (!StringUtil.isEmpty(string)) {
// 把<property>中的“${系统变量}”替换成系统变量
string = ConfigUtil.filter(string);
}
// 把<property>标签的值转换成当前SystemConfig属性的数据类型
if (isPrimitiveType(cls)) {
value = convert(cls, string);
}
} else if (obj instanceof BeanConfig) {
// <server>中有配置是Java Bean类型时的处理方法,介绍从略
} else if (obj instanceof BeanConfig[] ) {
// <server>中有配置是Java Bean类型时的处理方法,介绍从略
}
// 如果上面的步骤没出错
if (cls != null && value != null) {
// 在外面实例化的SystemConfig中,
// 调用这个SystemConfig属性的setter,把<property>的值赋进去
Method method = pd.getWriteMethod();
if (method != null) {
method.invoke(object, value);
}
}
}
UserConfig的加载过程——loadUsers()
这个方法的工作思路大致如下:
提取
<user>
中的name
属性和配置项<property>
的清单直接把
UserConfig
的password、usingDecrypt、benchmark、readOnly、manager和schemas,在<property>
清单中同名配置项的值赋上回到
<user>
中,提取它里面的<privileges>
的<schema>
和<table>
,然后加载后面两种标签的name
属性和dml
属性
下面是相关的技术细节:
提取<property>
的清单的方法和loadSystem()
一样,都是org.w3c.dom.Element.getElementsByTagName()
和com.actiontech.dble.config.util.ConfigUtil.loadElements()
这两个函数干活。相对“新颖”的是,使用了org.w3c.dom.Element.getAttribute()
来提取name
属性。
NodeList list = root.getElementsByTagName("user");
// ...
String name = e.getAttribute("name");
Map<String, Object> props = ConfigUtil.loadElements(e);
在UserConfig
的name、password、usingDecrypt、benchmark、readOnly、manager和schemas属性赋值时,没有通过反射来定位这些属性,而使用了硬编码的方法:先用props.get("XXX")
把XXX属性的值取出来,需要的话对这个值进行一下处理,最后调用UserConfig.setXXX()
完成XXX属性的赋值。这样做的好处在于代码逻辑清晰简单,但是牺牲了增删这些属性时而无需改动这个方法的灵活性。
// ...
String password = (String) props.get("password");
String usingDecrypt = (String) props.get("usingDecrypt");
String passwordDecrypt = DecryptUtil.decrypt(usingDecrypt, name, password);
user.setName(name);
user.setPassword(passwordDecrypt);
user.setEncryptPassword(password);
String benchmark = (String) props.get("benchmark");
if (null != benchmark) {
user.setBenchmark(Integer.parseInt(benchmark));
}
String readOnly = (String) props.get("readOnly");
if (null != readOnly) {
user.setReadOnly(Boolean.parseBoolean(readOnly));
}
String manager = (String) props.get("manager");
if (null != manager) {
user.setManager(Boolean.parseBoolean(manager));
}
String schemas = (String) props.get("schemas");
if (user.isManager() && schemas != null) {
throw new ConfigException("manager user can't set any schema!");
} else if (!user.isManager()) {
if (schemas != null) {
if (system.isLowerCaseTableNames()) {
schemas = schemas.toLowerCase();
}
String[] strArray = SplitUtil.split(schemas, ',', true);
user.setSchemas(new HashSet<>(Arrays.asList(strArray)));
}
// ...
}
com.actiontech.dble.config.loader.xml.XMLServerLoader.loadPrivileges()
则承包了加载<privileges>
和它里面的<schema>
和<table>
的工作,工作流程如下:
读取
<privileges>
的check
属性为每一个
<schema>
创建schemaPrivilege
实例,获取<schema>
的name
和dml
两个属性来给schemaPrivilege
赋值为同一个
<schema>
里的每个<table>
创建tablePrivilege
实例,获取<table>
的name
和dml
两个属性来给tablePrivilege
赋值
private void loadPrivileges(UserConfig userConfig, Element node) {
// 实例化<privileges>对应的UserPrivilegesConfig类
UserPrivilegesConfig privilegesConfig = new UserPrivilegesConfig();
// 将<user>下的所有<privileges>标签找出来
NodeList privilegesNodes = node.getElementsByTagName("privileges");
// 取其中一个<privileges>标签
int privilegesNodesLength = privilegesNodes.getLength();
for (int i = 0; i < privilegesNodesLength; i++) {
Element privilegesNode = (Element) privilegesNodes.item(i);
// 读取check属性,并给UserPrivilegesConfig.check赋值
// tips: 虽然允许有多个<privileges>,但由于UserPrivilegesConfig
// 只实例过一个对象,所以UserPrivilegesConfig.check等于最后一个
// <privileges>的check
String check = privilegesNode.getAttribute("check");
if (null != check) {
privilegesConfig.setCheck(Boolean.valueOf(check));
}
// 将<privileges>下的所有<schema>标签找出来
NodeList schemaNodes = privilegesNode.getElementsByTagName("schema");
// 取其中一个<schema>标签
int schemaNodeLength = schemaNodes.getLength();
for (int j = 0; j < schemaNodeLength; j++) {
Element schemaNode = (Element) schemaNodes.item(j);
// 读取name属性
// tips:<system>中设置的isLowerCaseTableNames会导致这里对name取小写
String name1 = schemaNode.getAttribute("name");
if (system.isLowerCaseTableNames()) {
name1 = name1.toLowerCase();
}
// 读取dml属性
String dml1 = schemaNode.getAttribute("dml");
int[] dml1Array = new int[dml1.length()];
for (int offset1 = 0; offset1 < dml1.length(); offset1++) {
dml1Array[offset1] = Character.getNumericValue(dml1.charAt(offset1));
}
// 实例化<schema>对应的SchemaPrivilege类,并把dml赋值
UserPrivilegesConfig.SchemaPrivilege schemaPrivilege = new UserPrivilegesConfig.SchemaPrivilege();
schemaPrivilege.setDml(dml1Array);
// 取其中一个<table>标签
NodeList tableNodes = schemaNode.getElementsByTagName("table");
int tableNodeLength = tableNodes.getLength();
for (int z = 0; z < tableNodeLength; z++) {
// 实例化<table>对应的TablePrivilege类
UserPrivilegesConfig.TablePrivilege tablePrivilege = new UserPrivilegesConfig.TablePrivilege();
Element tableNode = (Element) tableNodes.item(z);
// 读取name属性
String name2 = tableNode.getAttribute("name");
if (system.isLowerCaseTableNames()) {
name2 = name2.toLowerCase();
}
// 读取dml属性
String dml2 = tableNode.getAttribute("dml");
int[] dml2Array = new int[dml2.length()];
for (int offset2 = 0; offset2 < dml2.length(); offset2++) {
dml2Array[offset2] = Character.getNumericValue(dml2.charAt(offset2));
}
// 对dml赋值
tablePrivilege.setDml(dml2Array);
// 把TablePrivilege追加到所属的SchemaPrivilege内
schemaPrivilege.addTablePrivilege(name2, tablePrivilege);
}
// 把SchemaPrivilege追加到所属的UserPrivilegesConfig内
privilegesConfig.addSchemaPrivilege(name1, schemaPrivilege);
}
}
// 把UserPrivilegesConfig赋值给UserConfig
userConfig.setPrivilegesConfig(privilegesConfig);
}
FirewallConfig的加载过程——loadFirewall()
只要了解SystemConfig
的加载过程,尤其是反射部分(见前文),这部分的工作逻辑非常简单:
先读取白名单——由于只有
<whitehost>
含有<host>
标签,找到每一个<host>
标签,提取其中的host
和user
属性,形成{host, { user1, user2, ... , usern }}的项目,然后加入到白名单里再读取黑名单——黑名单功能本质上依赖
com.alibaba.druid
来实现,<blacklist>
标签对应的类正是com.alibaba.druid.wall.WallConfig
,所以和上面SystemConfig
加载的时候类似,使用了反射的方法,把<property>
中,WallConfig
中的同名值赋值上去,来完成WallConfig
的初始化,最后再以此初始化druid的一个特别的语法解析器工厂
以下是技术细节:
private void loadFirewall(Element root) throws IllegalAccessException, InvocationTargetException {
/*
* 先加载白名单<whitehost>
*/
// 将<whitehost>下的所有<host>标签找出来
NodeList list = root.getElementsByTagName("host");
Map<String, List<UserConfig>> whitehost = new HashMap<>();
// 对每一个<host>标签
for (int i = 0, n = list.getLength(); i < n; i++) {
Node node = list.item(i);
if (node instanceof Element) {
Element e = (Element) node;
// 取host和user属性
String host = e.getAttribute("host").trim();
String userStr = e.getAttribute("user").trim();
// host属性(IP地址)不允许和其他<host>的重复
if (this.firewall.existsHost(host)) {
throw new ConfigException("host duplicated : " + host);
}
// 把”user1,user2,...usern"形式的user属性转变为数组
String[] arrayUsers = userStr.split(",");
List<UserConfig> userConfigs = new ArrayList<>();
// 对user属性中的每一个user,
for (String user : arrayUsers) {
// 这个user一定要在之前加载的用户权限(<user>标签)中配置过
UserConfig uc = this.users.get(user);
if (null == uc) {
throw new ConfigException("[user: " + user + "] doesn't exist in [host: " + host + "]");
}
// 这个user不是管理用户的话,它必须能访问起码一个库
if (!uc.isManager() && (uc.getSchemas() == null || uc.getSchemas().size() == 0)) {
throw new ConfigException("[host: " + host + "] contains one root privileges user: " + user);
}
// 这个user符合上述条件的话,则将它在用户权限中的实体索引到这里来
userConfigs.add(uc);
}
// 将{host, { user1, user2, ... , usern }}加入到白名单中
whitehost.put(host, userConfigs);
}
}
// 完成白名单<whitehost>的加载
firewall.setWhitehost(whitehost);
/*
* 然后加载黑名单<blacklist>
*/
// 初始化com.alibaba.druid.wall.WallConfig
WallConfig wallConfig = new WallConfig();
// 将<firewall>的所有<blacklist>标签找出来
NodeList blacklist = root.getElementsByTagName("blacklist");
// 对于每一个<blacklist>标签
for (int i = 0, n = blacklist.getLength(); i < n; i++) {
Node node = blacklist.item(i);
if (node instanceof Element) {
Element e = (Element) node;
// 加载check属性
String check = e.getAttribute("check");
if (null != check) {
firewall.setBlackListCheck(Boolean.parseBoolean(check));
}
// 利用用反射的方法,把<property name="xxx">加载给WallConfig.xxx,从而完成WallConfig的赋值
// loadElements()和mapping()细节同本文的loadSystem()部分,不再冗述
Map<String, Object> props = ConfigUtil.loadElements((Element) node);
ParameterMapping.mapping(wallConfig, props);
}
}
// 完成黑名单<blacklist>的加载
firewall.setWallConfig(wallConfig);
// 以加载后的<blacklist>信息去初始化`com.alibaba.druid.wall.spi.MySqlWallProvider`(一个语法解析器工厂)
firewall.init();
}