JAR Hell in Practice
Hibernate maps String
fields to varchar(255)
database columns by default. Sometimes it’s just OK, sometimes it can be tailored by changing the column length to a larger value. Unfortunately, sometimes the length of a user input shouldn’t be limited at all. That was the scope of the ticket I needed to resolve – to allow using the text of unlimited length in some specific fields.
The ticket seemed easy – “that’s what CLOBs are for” – was my first thought. Stackoverflow answers suggest many ways to map String
fields into CLOB columns, like:
@Type(type="clob")
public String getField()
@Type(type="text")
public String getField()
The one I liked the most was to simply add the @Lob
annotation:
@Lob
public String getField()
Normally I would just add those annotations wherever it was needed, run tests and ask a friend for a review. Not this time. After adding the @Lob
annotation and trying to persist the object I saw:
java.lang.AbstractMethodError: com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.setCharacterStream(ILjava/io/Reader;J)V
at org.hibernate.type.descriptor.sql.ClobTypeDescriptor$4$1.doBind(ClobTypeDescriptor.java:131)
at org.hibernate.type.descriptor.sql.BasicBinder.bind(BasicBinder.java:90)
at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:286)
at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:281)
at org.hibernate.param.NamedParameterSpecification.bind(NamedParameterSpecification.java:67)
at org.hibernate.loader.hql.QueryLoader.bindParameterValues(QueryLoader.java:613)
...
I quickly started googling for hints. What I found were mainly suggestions of updating c3p0 version or JDBC drivers. I checked the pom.xml file and confirmed that we used the newest version of the c3p0 dependency (which was 0.9.2.1 at the time). The exception occurred on every database we support including the newest H2 which ruled out the JDBC driver issue.
I spent a lot of time trying to find the cause of the error as well as searching workarounds. Finally, I decided to study the stack trace once again. In the java documentation of the AbstractMethodError
class we can read:
Thrown when an application tries to call an abstract method. Normally, this error is caught by the compiler; this error can only occur at run time if the definition of some class has incompatibly changed since the currently executing method was last compiled.
Well, that means that the com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.setCharacterStream(ILjava/io/Reader;J)V
method (which is a bytecode signature for setCharacterStream(int, java.io.Reader, long)
) cannot be found at runtime even though some class needs it. I wanted to check if the method existed so I pressed ctrl+shift+T to open Eclipse’s “Open Type” dialog and typed NewProxyPreparedStatement
. To my surprise Eclipse found that class in two different locations.
Then it dawned upon me – “what if we use two different c3p0 versions at the same time?”. I immediately opened pom.xml in Eclipse, opened the “Dependency hierarchy” tab and typed “c3p0” into the filter field. What I saw explained everything:
Two versions of c3p0 were present in the classpath at the same time and it almost always causes problems. The 0.9.1.1 version was a transitive dependency included by the Quartz Scheduler. Usually, maven is smart enough to download only one library version and prevent conflicts – why didn’t it work this time?
I found out that at one point c3p0 developers had decided to change maven group id of their artifacts. The group id was simply “c3p0” in the 0.9.1.1 version but it was “com.mchange” in 0.9.2.1 - that’s why maven didn’t know that those two jar files were the same artifact.
I excluded c3p0 dependency from quartz and then the @Lob
annotation started to work flawlessly.
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
<exclusions>
<exclusion>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
</exclusion>
</exclusions>
</dependency>