server.xml的构成和内存表现形式
server.xml中的内容,简要来说,主要有
prop_value
...
prop_value
...
...
...
...
...
prop_value
...
相应的,这三部分被分别加载成com.actiontech.dble.config.model
中的SystemConfig
、UserConfig
和FirewallConfig
这三个类。
配置文件元素 | 对应类 | 配置的内容 |
---|---|---|
SystemConfig | DBLE服务器配置,例如服务端口、管理端口等 | |
UserConfig | 访问DBLE的用户配置,例如用户名、密码、允许访问哪些库or表等 | |
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()
来提取出
、
和
的具体内容,填充到对应的对象里。
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这些比较复杂的技术,但它的工作思路其实非常朴素:
提取
中的配置项
的清单提取
SystemConfig
类中的可配置属性的清单把可配置属性清单过一遍,从
清单中找到同名配置项,把值赋给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 props = ConfigUtil.loadElements((Element) node);
ParameterMapping.mapping(system, props);
}
}
下面是相关的技术细节:
提取
中的配置项
的清单,需要两步:
通过
org.w3c.dom.Element.getElementsByTagName()
,定位到server.xml的DOM的
标签调用
com.actiontech.dble.config.util.ConfigUtil.loadElements()
,将每一个
标签创建成一个键值对(name, content),放到同一个Map
集合里
ParameterMapping.mapping()
则会搞定剩余的工作:
com.actiontech.dble.config.util.ParameterMapping.getDescriptors()
找出ServerConfig
中的可配置属性(符合Java Bean规范,即有对应的公开getter和setter方法),将它们放到一个数组中ParameterMapping.mapping()
现在手上有了两套清单:一个是server.xml里标签的所有 Map
,另一个是parameter ServerConfig
的所有属性PropertyDescriptor[] pds
。于是,剩下来的工作就是把parameter
中的值,根据pds
,赋值给ServerConfig
实例中的同名属性。
// 取一个SystemConfig属性
for (PropertyDescriptor pd : pds) {
// 找同名的标签
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)) {
// 把中的“${系统变量}”替换成系统变量
string = ConfigUtil.filter(string);
}
// 把标签的值转换成当前SystemConfig属性的数据类型
if (isPrimitiveType(cls)) {
value = convert(cls, string);
}
} else if (obj instanceof BeanConfig) {
// 中有配置是Java Bean类型时的处理方法,介绍从略
} else if (obj instanceof BeanConfig[] ) {
// 中有配置是Java Bean类型时的处理方法,介绍从略
}
// 如果上面的步骤没出错
if (cls != null && value != null) {
// 在外面实例化的SystemConfig中,
// 调用这个SystemConfig属性的setter,把的值赋进去
Method method = pd.getWriteMethod();
if (method != null) {
method.invoke(object, value);
}
}
}
UserConfig的加载过程——loadUsers()
这个方法的工作思路大致如下:
提取
中的name
属性和配置项
的清单直接把
UserConfig
的password、usingDecrypt、benchmark、readOnly、manager和schemas,在
清单中同名配置项的值赋上回到
中,提取它里面的
的
和,然后加载后面两种标签的
name
属性和dml
属性下面是相关的技术细节:
提取
的清单的方法和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
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()
则承包了加载
和它里面的
和的工作,工作流程如下:
读取
的check
属性为每一个
创建schemaPrivilege
实例,获取
的name
和dml
两个属性来给schemaPrivilege
赋值为同一个
里的每个创建
tablePrivilege
实例,获取的
name
和dml
两个属性来给tablePrivilege
赋值private void loadPrivileges(UserConfig userConfig, Element node) { // 实例化
对应的UserPrivilegesConfig类 UserPrivilegesConfig privilegesConfig = new UserPrivilegesConfig(); // 将 下的所有 标签找出来 NodeList privilegesNodes = node.getElementsByTagName("privileges"); // 取其中一个 标签 int privilegesNodesLength = privilegesNodes.getLength(); for (int i = 0; i < privilegesNodesLength; i++) { Element privilegesNode = (Element) privilegesNodes.item(i); // 读取check属性,并给UserPrivilegesConfig.check赋值 // tips: 虽然允许有多个 ,但由于UserPrivilegesConfig // 只实例过一个对象,所以UserPrivilegesConfig.check等于最后一个 // 的check String check = privilegesNode.getAttribute("check"); if (null != check) { privilegesConfig.setCheck(Boolean.valueOf(check)); } // 将 下的所有 标签找出来 NodeList schemaNodes = privilegesNode.getElementsByTagName("schema"); // 取其中一个 标签 int schemaNodeLength = schemaNodes.getLength(); for (int j = 0; j < schemaNodeLength; j++) { Element schemaNode = (Element) schemaNodes.item(j); // 读取name属性 // tips: 中设置的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)); } // 实例化 对应的SchemaPrivilege类,并把dml赋值 UserPrivilegesConfig.SchemaPrivilege schemaPrivilege = new UserPrivilegesConfig.SchemaPrivilege(); schemaPrivilege.setDml(dml1Array); // 取其中一个 标签 NodeList tableNodes = schemaNode.getElementsByTagName("table"); int tableNodeLength = tableNodes.getLength(); for (int z = 0; z < tableNodeLength; z++) { // 实例化
对应的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
的加载过程,尤其是反射部分(见前文),这部分的工作逻辑非常简单:先读取白名单——由于只有
含有
标签,找到每一个
标签,提取其中的host
和user
属性,形成{host, { user1, user2, ... , usern }}的项目,然后加入到白名单里再读取黑名单——黑名单功能本质上依赖
com.alibaba.druid
来实现,
标签对应的类正是com.alibaba.druid.wall.WallConfig
,所以和上面SystemConfig
加载的时候类似,使用了反射的方法,把
中,WallConfig
中的同名值赋值上去,来完成WallConfig
的初始化,最后再以此初始化druid的一个特别的语法解析器工厂
以下是技术细节:
private void loadFirewall(Element root) throws IllegalAccessException, InvocationTargetException { /* * 先加载白名单
*/ // 将 下的所有 标签找出来 NodeList list = root.getElementsByTagName("host"); Map > whitehost = new HashMap<>(); // 对每一个 标签 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地址)不允许和其他 的重复 if (this.firewall.existsHost(host)) { throw new ConfigException("host duplicated : " + host); } // 把”user1,user2,...usern"形式的user属性转变为数组 String[] arrayUsers = userStr.split(","); List userConfigs = new ArrayList<>(); // 对user属性中的每一个user, for (String user : arrayUsers) { // 这个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); } } // 完成白名单 的加载 firewall.setWhitehost(whitehost); /* * 然后加载黑名单 */ // 初始化com.alibaba.druid.wall.WallConfig WallConfig wallConfig = new WallConfig(); // 将 的所有 标签找出来 NodeList blacklist = root.getElementsByTagName("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)); } // 利用用反射的方法,把 加载给WallConfig.xxx,从而完成WallConfig的赋值 // loadElements()和mapping()细节同本文的loadSystem()部分,不再冗述 Map props = ConfigUtil.loadElements((Element) node); ParameterMapping.mapping(wallConfig, props); } } // 完成黑名单 的加载 firewall.setWallConfig(wallConfig); // 以加载后的 信息去初始化`com.alibaba.druid.wall.spi.MySqlWallProvider`(一个语法解析器工厂) firewall.init(); }