sql2java:一次外科手术式的bug修复过程

我接触的第一个也是目前唯一的ORM工具就是鲜为人知的sql2java,这个名字倒是很容易顾名思义,一看就知道是自动生成数据库访问代码(java)的code generator. 关于它的使用介绍,参见我的一篇博文《sql2java:一个古老但稳定的轻量级的ORM工具的使用说明》。 如果你看过上一篇文章,就知道sql2java这个开源项目早已经不再维护,是个老古董了,如果使用它的过程中遇到的问题,是不可能指望作者修复的,本文就是讲述我在使用这个古老工具过程遇到的问题以及修复它的过程。

我接触使用sql2java的历史大概有6~7年了。这期间使用它一直很稳定,稳定到我一直无视它的存在。 我的数据库(oracle)项目中有一张表中有一个存储二进制数据块的字段(名为code),最开始长度是840 bytes,所以这个字段我指定为RAW类型,sql2java生成的代码将这个数据类型映射为java的byte[],使用很方便 。 不久之后这个字段被要求扩充到2560字节,仍然没问题,因为RAW的最大长度限制是4000 bytes。

BLOB不支持

大约一年前,这个字段需要再次扩充到5120 bytes。因为超过了4000 bytes限制,这时已经不能使用一个RAW类型字段保存了,我想到的办法就是换成BLOB类型。

然而修改完表结构定义,当我开始用sql2java重新生成java 代码,当开始处理code字段时,异常发生了!

[sql2java] org.apache.velocity.exception.MethodInvocationException: Invocationof method ‘getJavaType’ in class net.sourceforge.sql2java.Column threw exception class java.lang.IllegalArgumentException : No primary type associated: GF_USER.CODE [sql2java] at org.apache.velocity.runtime.parser.node.ASTMethod.execute(ASTMethod.java:309) [sql2java] at org.apache.velocity.runtime.parser.node.ASTReference.execute(ASTReference.java:207) [sql2java] at org.apache.velocity.runtime.parser.node.ASTReference.render(ASTReference.java:250) 。。。

从上面的异常信息可以看出异常发生在net.sourceforge.sql2java.Column的getJavaType方法。但我却毫无办法,因为没有源码。 sql2java的官网上虽然有源码,但svn库中因为没有tag,已经无法溯源找到我所使用的2.6.7版本对应的源码。

也就是说sql2java不能为包含BLOB类型字段的table生成java代码。这在当时对我来说一道无法逾越的鸿沟,反反复复折腾了好几天,最终妥协: 还好我的项目中这是固定长度的字段,所以我重新设计了表结构用两个RAW字段分段存储5260 bytes,总算绕过了这个问题。

再次遇上它

最近一个新的数据库项目进入设计阶段,这次设计的表中有一个字段GRAY_IMAGE是用来存储图像数据(图像大小不固定),这就必须要用到BLOB类型来定义这个字段,因为图像数据不是固定长度,所以肯定不能用RAW代替。

要放弃吗?

一年遇到这个问题我绕过了它,这次真的绕不过了。sql2java确定对BlOB字段支持是有问题,要放弃它吗? 如果放弃sql2java,另选择一个当下流行的ORM工具(hibernate,JOOQ,Mybatis,OrmLite。。。),支持BLOB肯定是没有问题的, 但这些工具只是提供了ORM,对具体每张表的操作,大多还是要自己写数据库访问代码。习惯了sql2java大包大揽替我搞定一切的做法 ,想想要再手工写那么多重复代码就头疼。 另外我需要额外花时间来学习新的ORM工具的用法,学习时间本身也是有成本的,我是个比较慢热的人,学习新东西的效率比较低。 综合考虑,对于我来说,换一个新的ORM工具相当于之前几年在sql2java上积累的技术资产全部清零,还要再花时间精力学习使用新的ORM工具,这代价是挺高的。 一时间让我很难下决断啊!

一丝希望

在对sql2java瞎琢磨的过程中,我尝试将字段类型改为CLOB,再来用sql2java生成代码,居然没报错!能生成代码 。 这让我看到了一丝希望:

我(一脸嫌弃): 你快死了啦,不给你准备后事啦 sql2java: ==,==,我觉得我还能抢救一下…

BLOB,CLOB都是用来存储大型数据的类型,区别只是CLOB用于保存文本数据。既然CLOB没有报错,凭直觉我觉得作者在设计系统时已经加入了BLOB/CLOB的支持,而BLOB报错可能只是个bug。如果只是个bug,那么只要找到问题原因,修复起来应该并不会涉及太多的代码修改。

我: 如果我能自己在短期内修复这个bug,我就不需要更换ORM工具了,之前所有的困扰都不存在了。 $: 修改人家代码的bug?你行么? 我: 我…我想试试 sql2java: 源码你都没有,你怎么改? 我: 我…我想试试

下决心吧

要放弃?还是要自己修复? 这个问题在我心里缠绕了几天,最终我决定尝试一下修复bug,其实只是基于一个理由:

不做怎么知道不行呢?

获取源码

要查找BLOB抛出异常的原因,首先得有源码。 前面说过了,sql2java官网svn上倒是有源码,但没有tag,所以无法获取最后一个release版本sql2java-2-6-7.zip对应的源码。相当于没有源码。 这也是一年前没有解决这个问题的原因。 于是我想到了java 反编译工具。

java反编译

首先我想到了之前用过的一个反编译工具jd-gui 用它对net.sourceforge.sql2java.Column所在sql2java/lib/sql2java.jar进行反编译.很快得到了源码。 我急切的找到抛出异常的getJavaType方法

哈哈,果然如我所料,只是一个简单的bug。

如下,代码很简单就是一个switch语句针对不同的类型返回不同的类型字符串,case语句中有CLOB却缺少了针对BLOB类型的语句(执行到tiae();就抛出异常),这就可以解释为什么CLOB类型可以正常生成代码了。

    public String getJavaType() {
        switch (getMappedType()) {
            case 0 :
                return "Array";
            case 1 :
                return "java.math.BigDecimal";
            case 2 :
                return "Boolean";
            case 3 :
                return "byte[]";
            case 4 :
                return "Clob";
            case 5 :
                return "java.sql.Date";
            case 6 :
                return "java.util.Date";
            case 7 :
                return "Double";
            case 8 :
                return "Float";
            case 10 :
                return "Integer";
            case 11 :
                return "Long";
            case 12 :
                return "Ref";
            case 13 :
                return "String";
            case 14 :
                return "java.sql.Time";
            case 15 :
                return "java.sql.Timestamp";
            case 16 :
                return "java.net.URL";
            case 17 :
                return "Object";
            case 18 :
                return "java.util.Calendar";
            case 9 :
        }
        tiae();

        return null;
    }

于是我在switch语句中加上了BLOB的处理

        case M_BLOB : 
            return "java.sql.Blob";

然后将Eclipse自动编译好的Column.class更新到sql2java/lib/sql2java.jar中,再次运行ant all生成代码 。 生成代码立即通过。不再抛出异常!

我靠!原来解决问题这么简单?!

不靠谱的反编译器jd-gui

上一阶段生成java代码的确是不报错了,但是新的问题来了。 在编译生成的java代码时报错了,报错内容就不贴了,一看代码就知道是生成的java文件有语法错误,然后排查原因,折腾很久终于发现问题出在下面这段由jd-gui反编译出来的代码上。参见代码代码中的中文注释:

    public String getPreparedStatementMethod(String var, String pos) {
        StringBuffer sb = new StringBuffer();
        StringBuffer end = new StringBuffer();
        end.append(pos).append(", ").append(var).append(");");
        if ('"' != var.charAt(0)) {
            sb.append("if (").append(var).append(" == null) { ps.setNull(").append(pos).append(", ")
                    .append(getJavaTypeAsTypeName()).append("); } else { ");

            end.append(" }");
        }
        switch (getMappedType()) {
            case 0 :
                return "ps.setArray(" + end;
                // 正确的逻辑应该是这样的,下同
                // return sb.append("ps.setArray(").append(end).toString();
            case 11 :
                return CodeWriter.MGR_CLASS + ".setLong(ps, " + end;
            case 3 :
                return "ps.setBytes(" + end;
            case 9 :
                return "ps.setBlob(" + end;
            case 2 :
                return CodeWriter.MGR_CLASS + ".setBoolean(ps, " + end;
            case 13 :
                return "ps.setString(" + end;
            case 4 :
                return "ps.setClob(" + end;
            case 16 :
                return "ps.setURL(" + end;
            case 1 :
                return "ps.setBigDecimal(" + end;
            case 7 :
                return CodeWriter.MGR_CLASS + ".setDouble(ps, " + end;
            case 10 :
                return CodeWriter.MGR_CLASS + ".setInteger(ps, " + end;
            case 17 :
                return "ps.setObject(" + end;
            case 8 :
                return CodeWriter.MGR_CLASS + ".setFloat(ps, " + end;
            case 5 :
                return "ps.setDate(" + end;
            case 14 :
                return "ps.setTime(" + end;
            case 15 :
                return "ps.setTimestamp(" + end;
            case 6 :
                switch (getType()) {
                    case 93 :
                        return "ps.setTimestamp(" + pos + ", new java.sql.Timestamp(" + var + ".getTime())); }";
                    case 91 :
                        return "ps.setDate(" + pos + ", new java.sql.Date(" + var + ".getTime())); }";
                    case 92 :
                        return "ps.setTime(" + pos + ", new java.sql.Time(" + var + ".getTime())); }";
                }
                return null;
            case 18 :
                return CodeWriter.MGR_CLASS + ".setCalendar(ps, " + end;
            case 12 :
                sb.setLength(0);
                sb.append("ps.setRef(").append(end);
                sb.setLength(sb.length() - 2);
                return sb.toString();
        }
        sb.setLength(0);
        sb.append("ps.setObject(").append(end);
        sb.setLength(sb.length() - 2);
        return sb.toString();
    }

天呐! 也就是说jd-gui反编译出来的代码逻辑不对!造成生成的代码存在语法错误,反编译器还有这么不靠谱的?!

论反编译器的重要性

事实证明jd-gui反编译器得到的源码是有问题,怎么办呢?于是继续百度,得知开源的反编译器不止一种,

由此我也想到,可能没有一款java反编译器对所有的java class都能反编译出正确的结果,但是这不是我首先要考虑的问题,现在知道问题出在net/sourceforge/sql2java/Column.class这个类,只要找到一款反编译器能正确把这个类反编译出来就好.

看来选择一款合适的反编译器是很重要的,根据这篇博客的介绍–《7款开源Java反编译工具》,我阴错阳差安装了Eclipse Class Decompiler

这是个Eclipse插件,它的优点就是无缝集成了JD, Jad, FernFlower, CFR, Procyon五款反编译工具,你可以在首选项/Java/反编译器中自由选择这5款反编译器的任何一款来使用。

也可以在’反编译器’菜单中直接选择

因为在Eclipse中集成,所以使用起来非常方便,如下点击任何一个class,就会自动执行反编译显示源码

有了这个神器,我逐个尝试用不同的反编译器对net/sourceforge/sql2java/Column.class这个类进行反编译,查看结果,发现jd-core和jad反编译的结果都不正常,用CFR获得了正确的反编译代码.

修复说明

在CFR反编译的代码基础上代码做了简单修改,终于修复了sql2java对BLOB/CLOB两种数据类型的支持。

具体的修复细节,参见我的github上的源码(https://github.com/10km/sql2java-2-6-7) 说明: dev 分支包含反编译出来的sql2java.jar包的代码。

对于CLOB/BLOB类型,在应用层如果让调用者直接处理java.sql.Blob,java.sql.Clob对象是很不方便的,所以我对net/sourceforge/sql2java/Column.class中的数据类型映射做了进一步改进:

BLOB类型映射到byte[]而不是java.sql.Blob CLOB类型映射到java.lang.String而不是java.sql.Clob

细节参见comit–《将BLOB外部类型改为byte[],CLOB外部类型改为String》

生成的java bean中CLOB和BLOB类型的字段getter/setter方法如下:

    /**
     * Getter method for colorImage.
     * <br>
     * Meta Data Information (in progress):
     * <ul>
     * <li>full name: TEST_USER.COLOR_IMAGE</li>
     * <li>column size: 4000</li>
     * <li>jdbc type returned by the driver: Types.CLOB</li>
     * </ul>
     *
     * @return the value of colorImage
     */
    public String getColorImage()
    {
        return colorImage;
    }

    /**
     * Setter method for colorImage.
     * <br>
     * The new value is set only if compareTo() says it is different,
     * or if one of either the new value or the current value is null.
     * In case the new value is different, it is set and the field is marked as 'modified'.
     *
     * @param newVal the new value to be assigned to colorImage
     */
    public void setColorImage(String newVal)
    {
        if ((newVal != null && colorImage != null && (newVal.compareTo(colorImage) == 0)) ||
            (newVal == null && colorImage == null && colorImageIsInitialized)) {
            return;
        }
        colorImage = newVal;
        colorImageIsModified = true;
        colorImageIsInitialized = true;
    }

    /**
     * Getter method for grayImage.
     * <br>
     * Meta Data Information (in progress):
     * <ul>
     * <li>full name: TEST_USER.GRAY_IMAGE</li>
     * <li>column size: 4000</li>
     * <li>jdbc type returned by the driver: Types.BLOB</li>
     * </ul>
     *
     * @return the value of grayImage
     */
    public byte[] getGrayImage()
    {
        return grayImage;
    }

    /**
     * Setter method for grayImage.
     * <br>
     * Attention, there will be no comparison with current value which
     * means calling this method will mark the field as 'modified' in all cases.
     *
     * @param newVal the new value to be assigned to grayImage
     */
    public void setGrayImage(byte[] newVal)
    {
        if ((newVal != null && grayImage != null && newVal.equals(grayImage)) ||
            (newVal == null && grayImage == null && grayImageIsInitialized)) {
            return;
        }
        grayImage = newVal;
        grayImageIsModified = true;
        grayImageIsInitialized = true;
    }

对于调用者来说,根本不用关心BLOB/CLOB类型怎么处理,sql2java生成的代码自动完成byte[] <–>java.sql.Blob,java.lang.String<–> java.sql.Clob的转换。

总结

如果总结一下这次bug修复过程的话,我想说: 人总是懒惰的,在外力压迫下,才会激发创造力。 如果你不做,永远不知道自己行不行,何不试一下?

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏HBStream流媒体与音视频技术

采集音频和摄像头视频并实时H264编码及AAC编码

3957
来自专栏Golang语言社区

Golang控制goroutine的启动与关闭

最近在用golang做项目的时候,使用到了goroutine。在golang中启动协程非常方便,只需要加一个go关键字:  go myfunc(){     ...

3077
来自专栏Android群英传

看ASM在代码中的强势插入

1133
来自专栏Seebug漏洞平台

D-Link DIR-605L 拒绝服务错误报告 (CVE-2017-9675)

原文:http://hypercrux.com/bug-report/2017/06/19/DIR605L-DoS-BugReport/ 译者:Serene ...

3276
来自专栏landv

实现用VB.Net/(C#)开发K/3 BOS 插件的真正可行方法

651
来自专栏Crossin的编程教室

【Python 第62课】 调试程序

写代码,不可避免地会出现bug。很多人在初学编程的时候,当写完程序运行时,发现结果与自己预料中的不同,或者程序意外中止了,就一时没了想法,不知道该从何下手,只能...

2629
来自专栏大内老A

[WCF安全系列]谈谈WCF的客户端认证[Windows认证]

结束了服务认证的介绍之后,我们接着介绍WCF双向认证的另一个方面,即服务对客户端的认证,简称客户端认证。客户端认证采用的方式决定于客户端凭证的类型,内容只要涉及...

2006
来自专栏ml

MFC学习之窗口基础

                          WinMain函数  1、句柄(HANDLE):{ 1. 定义:资源的标识 2. 句柄的作用: 操作系统通过...

2856
来自专栏我杨某人的青春满是悔恨

Kingfisher源码阅读(三)

上一篇地址:Kingfisher源码阅读(二) 第一篇地址:Kingfisher源码阅读(一)

794
来自专栏JackieZheng

Java豆瓣电影爬虫——小爬虫成长记(附源码)

  以前也用过爬虫,比如使用nutch爬取指定种子,基于爬到的数据做搜索,还大致看过一些源码。当然,nutch对于爬虫考虑的是十分全面和细致的。每当看到屏幕上唰...

19411

扫码关注云+社区