/* * Copyright (c) 2013, OpenCloudDB/MyCAT and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software;Designed and Developed mainly by many Chinese * opensource volunteers. you can redistribute it and/or modify it under the * terms of the GNU General Public License version 2 only, as published by the * Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Any questions about this component can be directed to it's project Web address * https://code.google.com/p/opencloudb/. * */ package io.mycat.server.packet.util; import io.mycat.MycatServer; import io.mycat.backend.PhysicalDBPool; import io.mycat.backend.PhysicalDatasource; import io.mycat.server.config.node.DBHostConfig; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.concurrent.Callable; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.alibaba.fastjson.JSON; import com.google.common.util.concurrent.ListenableFuture; /** * 该类被彻底重构,fix 掉了原来的 collationIndex 和 charset 之间对应关系的兼容性问题, * 比如 utf8mb4 对应的 collationIndex 有 45, 46 两个值,如果我们只配置一个 45或者46的话, * 那么当mysqld(my.cnf配置文件)中的配置了:collation_server=utf8mb4_bin时,而我们却仅仅 * 值配置45的话,那么就会报错:'java.lang.RuntimeException: Unknown charsetIndex:46' * 如果没有配置 collation_server=utf8mb4_bin,那么collation_server就是使用的默认值,而我们却仅仅 * 仅仅配置46,那么也会报错。所以应该要同时配置45,46两个值才是正确的。 * 重构方法是,在 MycatServer.startup()方法在中,在 config.initDatasource(); 之前,加入 * CharsetUtil.initCharsetAndCollation(config.getDataHosts()); * 该方法,直接从mysqld的information_schema.collations表中获取 collationIndex 和 charset 之间对应关系, * 因为是从mysqld服务器获取的,所以肯定不会出现以前的兼容性问题(不同版本的mysqld,collationIndex 和 charset 对应关系不一样)。 * @author mycat */ public class CharsetUtil { public static final Logger logger = LoggerFactory.getLogger(CharsetUtil.class); /** collationIndex 和 charsetName 的映射 */ private static final Map<Integer,String> INDEX_TO_CHARSET = new HashMap<>(); /** charsetName 到 默认collationIndex 的映射 */ private static final Map<String, Integer> CHARSET_TO_INDEX = new HashMap<>(); /** collationName 到 CharsetCollation 对象的映射 */ private static final Map<String, CharsetCollation> COLLATION_TO_CHARSETCOLLATION = new HashMap<>(); /** * 异步 初始化 charset 和 collation(根据 mycat.xml文件中的 dataHosts 去mysqld读取 charset 和 collation 的映射关系) * 使用异步时,应该改用 ConcurrentHashMap * @param charsetConfigMap mycat.xml文件中 charset-config 元素指定的 collationIndex --> charsetName */ public static void asynLoad(Map<String, PhysicalDBPool> dataHosts, Map<String, Object> charsetConfigMap){ MycatServer.getInstance().getListeningExecutorService().execute(new Runnable() { public void run() { CharsetUtil.load(dataHosts, charsetConfigMap); } }); } /** * 同步 初始化 charset 和 collation(根据 mycat.xml文件中的 dataHosts 去mysqld读取 charset 和 collation 的映射关系) * @param charsetConfigMap mycat.xml文件中 charset-config 元素指定的 collationIndex 和 charsetName 映射 */ public static void load(Map<String, PhysicalDBPool> dataHosts, Map<String, Object> charsetConfigMap){ try { if(dataHosts != null && dataHosts.size() > 0) CharsetUtil.initCharsetAndCollation(dataHosts); // 去mysqld读取 charset 和 collation 的映射关系 else logger.debug("param dataHosts is null"); // 加载配置文件中的 指定的 collationIndex --> charsetName for (String index : charsetConfigMap.keySet()){ int collationIndex = Integer.parseInt(index); String charsetName = INDEX_TO_CHARSET.get(collationIndex); if(StringUtils.isNotBlank(charsetName)){ INDEX_TO_CHARSET.put(collationIndex, charsetName); CHARSET_TO_INDEX.put(charsetName, collationIndex); } logger.debug("load charset and collation from mycat.xml."); } } catch (Exception e) { logger.error(e.getMessage()); } } /** * <pre> * 根据 dataHosts 去mysqld读取 charset 和 collation 的映射关系: * mysql> SELECT ID,CHARACTER_SET_NAME,COLLATION_NAME,IS_DEFAULT FROM INFORMATION_SCHEMA.COLLATIONS; * +-----+--------------------+--------------------------+------------+ * | ID | CHARACTER_SET_NAME | COLLATION_NAME | IS_DEFAULT | * +-----+--------------------+--------------------------+------------+ * | 1 | big5 | big5_chinese_ci | Yes | * | 84 | big5 | big5_bin | | * | 3 | dec8 | dec8_swedish_ci | Yes | * | 69 | dec8 | dec8_bin | | *</pre> */ private static void initCharsetAndCollation(Map<String, PhysicalDBPool> dataHosts){ if(COLLATION_TO_CHARSETCOLLATION.size() > 0){ // 已经初始化 logger.debug(" charset and collation has already init ..."); return; } // 先利用mycat.xml配置文件 中的 heartbeat(该配置一般是存在的)的连接信息来获得CharsetCollation,避免后面的遍历; // 如果没有成功,则遍历mycat.xml配置文件 中的所有dataHost元素,来获得CharsetCollation; DBHostConfig dBHostconfig = getConfigByDataHostName(dataHosts, "jdbchost"); if(dBHostconfig != null){ if(getCharsetCollationFromMysql(dBHostconfig)){ logger.debug(" init charset and collation success..."); return; } } // 遍历 配置文件 mycat.xml 中的 dataHost 元素,直到可以成功连上mysqld,并且获取 charset 和 collation 信息 for(String key : dataHosts.keySet()){ PhysicalDBPool pool = dataHosts.get(key); if(pool != null && pool.getSource() != null){ PhysicalDatasource ds = pool.getSource(); if(ds != null && ds.getConfig() != null && "mysql".equalsIgnoreCase(ds.getConfig().getDbType())){ DBHostConfig config = ds.getConfig(); if(getCharsetCollationFromMysql(config)){ logger.debug(" init charset and collation success..."); return; // 结束外层 for 循环 } } } } logger.error(" init charset and collation from mysqld failed, please check datahost in mycat.xml."+ SystemUtils.LINE_SEPARATOR + " if your backend database is not mysqld, please ignore this message."); // 使用Mycat-server的环境中,其配置文件mycat.xml一台mysqld也没有配置,也就是后台数据库都是sqlserver或者oracle等 // 所以无法从mysqld中读取字符映射信息,所以只有在此种情况下使用配置文件代替, // 注意配置文件因为存在时效性,可能存在兼容问题,这也是为什么从mysqld中读取,而不使用配置文件的原因; getCharsetInfoFromFile(); logger.info(" backend database is not mysqld, read charset info from file."); } public static DBHostConfig getConfigByDataHostName(Map<String, PhysicalDBPool> dataHosts, String hostName){ PhysicalDBPool pool = dataHosts.get(hostName); if(pool != null && pool.getSource() != null){ PhysicalDatasource ds = pool.getSource(); return ds.getConfig(); } return null; } public static final String getCharset(int index) { return INDEX_TO_CHARSET.get(index); } /** * 因为 每一个 charset 对应多个 collationIndex, 所以这里返回的是默认的那个 collationIndex; * 如果想获得确定的值 index,而非默认的index, 那么需要使用 getIndexByCollationName * 或者 getIndexByCharsetNameAndCollationName * @param charset * @return */ public static final int getIndex(String charset) { if (StringUtils.isBlank(charset)) { return 0; } else { Integer i = CHARSET_TO_INDEX.get(charset.toLowerCase()); if(i == null && "Cp1252".equalsIgnoreCase(charset) ) charset = "latin1"; // 参见:http://www.cp1252.com/ The windows 1252 codepage, also called Latin 1 i = CHARSET_TO_INDEX.get(charset.toLowerCase()); return (i == null) ? 0 : i; } } /** * 根据 collationName 和 charset 返回 collationIndex * @param charset * @param collationName * @return */ public static final int getIndexByCharsetNameAndCollationName(String charset, String collationName) { if (StringUtils.isBlank(collationName)) { return 0; } else { CharsetCollation cc = COLLATION_TO_CHARSETCOLLATION.get(collationName.toLowerCase()); if(cc != null && charset != null && charset.equalsIgnoreCase(cc.getCharsetName())) return cc.getCollationIndex(); else return 0; } } /** * 根据 collationName 返回 collationIndex, 二者是一一对应的关系 * @param collationName * @return */ public static final int getIndexByCollationName(String collationName) { if (StringUtils.isBlank(collationName)) { return 0; } else { CharsetCollation cc = COLLATION_TO_CHARSETCOLLATION.get(collationName.toLowerCase()); if(cc != null) return cc.getCollationIndex(); else return 0; } } private static boolean getCharsetCollationFromMysql(DBHostConfig config){ String sql = "SELECT ID,CHARACTER_SET_NAME,COLLATION_NAME,IS_DEFAULT FROM INFORMATION_SCHEMA.COLLATIONS"; try(Connection conn = getConnection(config)){ if(conn == null) return false; try(Statement statement = conn.createStatement()){ ResultSet rs = statement.executeQuery(sql); while(rs != null && rs.next()){ int collationIndex = new Long(rs.getLong(1)).intValue(); String charsetName = rs.getString(2); String collationName = rs.getString(3); boolean isDefaultCollation = (rs.getString(4) != null && "Yes".equalsIgnoreCase(rs.getString(4))) ? true : false; INDEX_TO_CHARSET.put(collationIndex, charsetName); if(isDefaultCollation){ // 每一个 charsetName 对应多个collationIndex,此处选择默认的collationIndex CHARSET_TO_INDEX.put(charsetName, collationIndex); } CharsetCollation cc = new CharsetCollation(charsetName, collationIndex, collationName, isDefaultCollation); COLLATION_TO_CHARSETCOLLATION.put(collationName, cc); } if(COLLATION_TO_CHARSETCOLLATION.size() > 0) return true; return false; } catch (SQLException e) { logger.warn(e.getMessage()); } } catch (SQLException e) { logger.warn(e.getMessage()); } return false; } /** * 利用参数 DBHostConfig cfg 获得物理数据库的连接(java.sql.Connection) * 在mysqld刚启动马上启动mycat-server,该函数执行很慢。 * 但是又不能又不能使用mysql协议来获得所要的数据,因为mysql协议中mysqld在第一次发来的handshake * 就指定了 "serverCharsetIndex":46,在登录之前,我们无法修改connection的字符,必须使用 serverCharsetIndex * 指定的字符编码完成 handshake 和登录: * {"packetId":0,"packetLength":78,"protocolVersion":10,"restOfScrambleBuff":"OihYY2tvakVadV5Y", * "seed":"YiJ+eWVsb2c=","serverCapabilities":63487, * "serverCharsetIndex":46,"serverStatus":2,"serverVersion":"NS42LjI3LWxvZw==","threadId":65} * 所以我们无法使用 mysql协议从mysqld获得字符信息,而JDBC协议中可以在url中指定字符集。所以只能用JDBC来获得字符信息。 * @param cfg * @return * @throws SQLException */ public static Connection getConnection(DBHostConfig cfg){ if(cfg == null) return null; String url = new StringBuffer("jdbc:mysql://").append(cfg.getUrl()) .append("/mysql").append("?characterEncoding=UTF-8").toString(); Connection connection = null; long millisecondsEnd2 = System.currentTimeMillis(); try { Class.forName("com.mysql.jdbc.Driver"); connection = DriverManager.getConnection(url, cfg.getUser(), cfg.getPassword()); } catch (ClassNotFoundException | SQLException e) { if(e instanceof ClassNotFoundException) logger.error(e.getMessage()); else logger.warn(e.getMessage() + " " + JSON.toJSONString(cfg)); } long millisecondsEnd = System.currentTimeMillis(); logger.debug(" function getConnection cost milliseconds: " + (millisecondsEnd - millisecondsEnd2)); return connection; } /** * 从配置文件 index_to_charset.properties 读取 collationIndex 到 charsetName的映射关系, * 文件中的内容来源于:SELECT ID,CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.COLLATIONS order by id; * * 从配置文件charset_to_default_index.properties中读取charsetName到默认的collationIndex的映射关系, * 文件内容来源于:SELECT CHARACTER_SET_NAME,ID FROM INFORMATION_SCHEMA.COLLATIONS where Default='Yes'; * * 如果存在兼容性问题,请按照上面给出的方式更新那两个文件即可。 * * 只有在所有数据库都是 非 mysql数据库时,才需要使用到该函数。 */ public static void getCharsetInfoFromFile(){ Properties pros = new Properties(); try { pros.load(CharsetUtil.class.getClassLoader().getResourceAsStream("index_to_charset.properties")); Iterator<Entry<Object, Object>> it = pros.entrySet().iterator(); while (it.hasNext()) { Entry<Object, Object> entry = it.next(); Object key = entry.getKey(); Object value = entry.getValue(); INDEX_TO_CHARSET.put(Integer.parseInt(key.toString()), value.toString()); } // System.out.println(JSON.toJSONString(INDEX_TO_CHARSET)); pros.clear(); pros.load(CharsetUtil.class.getClassLoader().getResourceAsStream("charset_to_default_index.properties")); it = pros.entrySet().iterator(); while (it.hasNext()) { Entry<Object, Object> entry = it.next(); Object key = entry.getKey(); Object value = entry.getValue(); CHARSET_TO_INDEX.put(key.toString(), Integer.parseInt(value.toString())); } // System.out.println(JSON.toJSONString(CHARSET_TO_INDEX)); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args){ getCharsetInfoFromFile(); } } /** * 该类用来表示 mysqld 数据库中 字符集、字符集支持的collation、字符集的collation的index、 字符集的默认collation 的对应关系: * 一个字符集一般对应(支持)多个collation,其中一个是默认的 collation,每一个 collation对应一个唯一的index, * collationName 和 collationIndex 一一对应, 每一个collationIndex对应到一个字符集,不同的collationIndex可以对应到相同的字符集, * 所以字符集 到 collationIndex 的对应不是唯一的,一个字符集对应多个 index(有一个默认的 collation的index), * 而 collationIndex 到 字符集 的对应是确定的,唯一的; * mysqld 用 collation 的 index 来描述排序规则。 * @author Administrator * */ class CharsetCollation { // mysqld支持的字符编码名称,注意这里不是java中的unicode编码的名字, // 二者之间的区别和联系可以参考驱动jar包中的com.mysql.jdbc.CharsetMapping源码 private String charsetName; private int collationIndex; // collation的索引顺序 private String collationName; // collation 名称 private boolean isDefaultCollation = false; // 该collation是否是字符集的默认collation public CharsetCollation(String charsetName, int collationIndex, String collationName, boolean isDefaultCollation){ this.charsetName = charsetName; this.collationIndex = collationIndex; this.collationName = collationName; this.isDefaultCollation = isDefaultCollation; } public String getCharsetName() { return charsetName; } public void setCharsetName(String charsetName) { this.charsetName = charsetName; } public int getCollationIndex() { return collationIndex; } public void setCollationIndex(int collationIndex) { this.collationIndex = collationIndex; } public String getCollationName() { return collationName; } public void setCollationName(String collationName) { this.collationName = collationName; } public boolean isDefaultCollation() { return isDefaultCollation; } public void setDefaultCollation(boolean isDefaultCollation) { this.isDefaultCollation = isDefaultCollation; } }