动态代理在WEB与JDBC开发中的应用(JDBC篇)

背景描述

如果之前看过《动态代理在WEB与JDBC开发中的应用(WEB篇)》,这篇的内容可以全当是另一种应用的进阶举例,而在实现上确实没有太多进步的地方。我们先看一下项目所面临问题以及期望解决方案。在作者所接触的这个项目中,直接使用原始JDBC技术,java.sql.PreparedStatement和java.sql.ResultSet几乎占领了数据访问层,没有半点OR Mapping的迹象,看起来是不是很悲催?命啊~在项目开发至一半的时候,突然发现要对日文字符进行支持,而在之前一直使用英文进行测试。好了,问题来了,一个编码为8859_1的数据库怎么来接收Java中的UTF-8呢?并且很多CRUD功能已经完成,每个实现中涉及的字段数目相当之大,难道针对每个字段逐一调整不成?

问题分析

针对上面描述的情况,我们先做一个简单的分析。目前所面临的问题可以归为两种类型,第一类就是乱码的问题,这个可以通过重新编码得到解决;第二类则是面临大量的代码实现,如果对所有内容进行调整的话,很多功能就得重新测试,成本较高,但确实可行。

针对乱码问题,从实现的角度考虑,可以分别针对输入数据和输出数据进行编码转换。是在使用PreparedStatement的时候,对所有调用setString的地方进行编码转换;而对于数据读取的情况,可以对ResultSet.getString所返回的结果字符串进行编码转换。下面以UTF-8转8859_1为例子,实现编码转换(逆向转换只需交换编码名称的位置即可):

new String(new String("abc").getByte("UTF-8"), "8859_1")
如果以这种方式对每处PreparedStatement.setString和ResultSet.getString的调用都进行编码转换操作的话,工作量不仅繁重,而且不利于代码的维护与移植。从API上看,其实我们可以针对setString和getString进行“HOOK”,使用代理模式比较适合,动态代理最为适宜。

解决方案

编码问题解决了,利用动态代理统一地对setString和getString接口进行自制,让所有的调用都在我们的控制之下还何愁不能统一*!

public static ResultSet createResultSetJPWrapper(ResultSet rs) {
    return (ResultSet)(Proxy.newProxyInstance(ResultSet.class.getClassLoader(),
        new Class[] {ResultSet.class},
            new ResultSetJPWrapper(rs)));
}

private static class ResultSetJPWrapper implements InvocationHandler {

	private ResultSet wrappedResultSet;

    public ResultSetJPWrapper(ResultSet rs) {
        wrappedResultSet = rs;
    }

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Object result = method.invoke(wrappedResultSet, args);
        if ("getString".equals(method.getName()) && null != result) {
        	result = CommonUtils.convertCharset((String)result, ISAConstants.CHARSET_8859_1, ISAConstants.CHARSET_UTF_8);
        }
        return result;
    }
}

public static PreparedStatement createPreparedStatementJPWrapper(PreparedStatement pstmt) {
    return (PreparedStatement)(Proxy.newProxyInstance(PreparedStatement.class.getClassLoader(),
        new Class[] {PreparedStatement.class},
            new PreparedStatementJPWrapper(pstmt)));
}

private static class PreparedStatementJPWrapper implements InvocationHandler {

	private PreparedStatement wrappedStatement;

    public PreparedStatementJPWrapper(PreparedStatement pstmt) {
        wrappedStatement = pstmt;
    }

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        if ("setString".equals(method.getName()) && args.length == 2 && null != args[1] && String.class.equals(args[1].getClass())) {
        	args[1] = CommonUtils.convertCharset((String)args[1], ISAConstants.CHARSET_UTF_8, ISAConstants.CHARSET_8859_1);
        }
        return method.invoke(wrappedStatement, args);
    }
}
接下来就是在需要使用PreparedStatement和ResultSet的地方进行重构。
// 重构前
PreparedStatement stmt = conn.prepareStatement(sql);
// 重构后
PreparedStatement stmt = DBUtils.createPreparedStatementJPWrapper(conn.prepareStatement(sql));

// 重构前
ResultSet rs = stmt.executeQuery();
// 重构后
ResultSet rs = DBUtils.createResultSetJPWrapper(stmt.executeQuery());

重构后不论是stmt还是rs,setString和getString都会被我们定义的动态代理所劫持,在数据输入前或数据输出后重新编码,而这一切对于开发人员来说一切都是透明的,并且不会对现有逻辑造成污染。

后记

这里谈论的只是动态代理应用案例,并不意味着必须使用动态代理是该案例中解决问题的唯一方案。其实动态代理在这里不一定是最佳的实现方案,如果可以的话最好对数据库进行重新设置修改默认编码,或者是在应用层统一编码,但这些往往都不适用于一个经过十多年、几十人、且风格各异且“烂”的发臭的项目了。其实实际的数据库VARCHAR类型中存放的字符串编码五花八门,EUC-JP/SHIFTJIS/UTF-8/8859_1,并且这些编码可能同时存在于一张表,要了命了!

动态代理在WEB与JDBC开发中的应用(JDBC篇)

上一篇:Access Control List


下一篇:CentOS安装mysql*.rpm提示conflicts with file from package的解决办法