Wednesday, May 20, 2009

JRuby's Ruby-Style Exception Handling

Both Java and Ruby have relatively sophisticated exception handling built-in to the respective languages. Their exception handling techniques have many similarities, but also have some differences in both keywords and in supported exception handling functionality. JRuby allows mixing of Java classes with Ruby code and so it is no surprise that JRuby supports handling of exceptions thrown by Java classes.

Ruby's equivalent of Java's try-catch-finally is begin-rescue-ensure, though Ruby throws in an additional clause with the else clause. The else clause is executed when no declared rescue block is exercised. In other words, if there was an equivalent in Java (there isn't), it would be a block of code that is executed only when none of the the declared catch blocks were executed. Note that the Ruby else is different from Ruby's ensure or Java's finally in that it is only executed if no rescue (equivalent of Java catch) statement is executed while Ruby's ensure and Java's finally are always executed no matter which exceptions were caught or not caught.

Speaking of catching exceptions, Ruby does not have an equivalent of Java's checked exceptions. JRuby (and Groovy by the way) emulate this as well. Java and Ruby throw exceptions in the first place in a very similar manner, though Java uses the keyword throw and Ruby uses the keyword raise.

With the idea that most of the concepts of Java exception handling are highly similar to Ruby exception handling (the notable exceptions being the keywords used, the use of checked exceptions only in Java, and the use of a block of code executed only when no declared exception condition is encountered only in Ruby), it is time to move on to an examples of catching exceptions thrown by a Java class in JRuby code.

Java exceptions, even checked exceptions, do not need to be rescued/caught in JRuby. This is demonstrated in the following code listing in which an SQLRecoverableException (a checked exception) might be thrown (and the called method is declared to throw its parent SQLException) but no explicit exception handling is implemented.

Listing 1 - JRuby Invocation of Java Method Throwing Exception

#!/usr/bin/env jruby -w

include Java
import java.sql.SQLRecoverableException
require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'
require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'
include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor

# 1. "Happy Path" query to demonstrate JRuby/Java interaction works fine
puts "ALL'S WELL"
accessor = GenericOracleDatabaseAccessor.new
puts "#{accessor.perform_database_query \
'jdbc:oracle:thin:@//localhost:1521/orcl',
'select first_name, last_name from employees',
['FIRST_NAME', 'LAST_NAME'].to_java(:string)}"


In the example above, there is a call to a custom Java class (GenericOracleDatabaseAccessor) and method (performDatabaseQuery). That class is shown in the next code listing.


Listing 2 - GenericOracleDatabaseAccessor.java

package dustin.examples.jruby;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import oracle.jdbc.pool.OracleDataSource;

/**
* Class used for demonstration purposes only and should not be used anywhere
* in which security or maintainability matter. The problems with this
* particular Java class include its being in the unnamed package, the extreme
* riskiness of supporting a generic query of any String passed into the method,
* lack of use of PreparedStatement, lack of verification of correct and
* non-null parameters before applying them, etc. In fact, this class is used
* here because its many problems make it easy to have it throw exceptions that
* the JRuby example code can catch and handle.
*/
public class GenericOracleDatabaseAccessor
{
final static String NEW_LINE = System.getProperty("line.separator");

/**
* Perform a generic database query. CAUTION: There are
* numerous reasons why this particular code sample should not be used in
* any real or production settings. It is used here only to demonstrate
* how JRuby supports handling of Java exceptions in a Ruby-like manner. It
* is not a good idea normally to pass in the JDBC URL. The JDBC exception
* handling is also intentionally poor in this example. See class description
* for other failings (mostly intentional) of this class and method..
*
* @param jdbcUrl The JDBC URL for the database connection; a default is
* attempted if this is null.
* @param queryString The query statement to be run against the database.
* @param columnNames Column Names whose values are to be returned in
* resulting String.
* @throws SQLException Checked exception related to use of JDBC.
*/
public String performDatabaseQuery(
final String jdbcUrl,
final String queryString,
final String[] columnNames) throws SQLException
{
final OracleDataSource datasource = new OracleDataSource();
final String url = jdbcUrl == null
? "jdbc:oracle:thin:@//localhost:1521/orcl"
: jdbcUrl;
datasource.setURL(url);
datasource.setUser("hr");
datasource.setPassword("hr");
final Connection connection = datasource.getConnection();
final Statement queryStatement = connection.createStatement();
final ResultSet resultSet = queryStatement.executeQuery(queryString);
final StringBuilder returnString = new StringBuilder();
while (resultSet.next())
{
for (final String columnName : columnNames)
{
returnString.append(resultSet.getObject(columnName)).append(" ");
}
returnString.append(NEW_LINE);
}
resultSet.close();
queryStatement.close();
connection.close();
return returnString.toString();
}
}


As the code listing for GenericOracleDatabaseAccessor above indicates, the main method that is called on that class is called performDatabaseQuery and its accepts three parameters. The JRuby code could have called this method in a traditional Java-style call, but I chose to use a more Ruby-stylistic method call with underscores separating the multiple words in the method name along with parameters passed to the method in Ruby style.

The Java method performDatabaseQuery is declared to throw SQLException (parent of SQLRecoverableException). However, the calling JRuby code did not need to explicitly handle the exception. The above code ran correctly, but how would the exception be handled if it did get thrown? The next JRuby sample code intentionally specifies a non-existent host to force an exception to be thrown.

Listing 3 - Java Exception Thrown But Not Explicitly Handled in JRuby

#!/usr/bin/env jruby -w

include Java
import java.sql.SQLRecoverableException
require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'
require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'
include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor

puts "EXCEPTION WITHOUT HANDLING"
accessor = GenericOracleDatabaseAccessor.new
puts "#{accessor.perform_database_query \
'jdbc:oracle:thin:@//unknownhost:1521/orcl',
'select first_name, last_name from employees',
['FIRST_NAME', 'LAST_NAME'].to_java(:string)}"


Running the above JRuby code leads to the following output.



Although the exception thrown from the Java class was never explicitly handled in the code, the JRuby output shown directly above indicates that the exception is ultimately "handled" by displaying stack trace information. Of particular interest is the NativeException, the JRuby exception that wraps Java exceptions.


Listing 4 - JRuby Code Explicitly Catching Java Exception

#!/usr/bin/env jruby -w

include Java
import java.sql.SQLRecoverableException
require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'
require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'
include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor

puts "RUBY CATCHING OF JAVA EXCEPTION"
accessor = GenericOracleDatabaseAccessor.new
begin
puts "#{accessor.perform_database_query \
'jdbc:oracle:thin:@//unknownhost:1521/orcl',
'select first_name, last_name from employees',
['FIRST_NAME', 'LAST_NAME'].to_java(:string)}"
rescue SQLRecoverableException => sqlRecoverableException
puts "#{sqlRecoverableException}"
puts "Message: #{sqlRecoverableException.message}"
puts "Backtrace: #{sqlRecoverableException.backtrace}"
end


The code in Listing 4 looks like Ruby code, but explicitly catches a Java exception (SQLRecoverableException). The output from running this is shown next.



The same exception can be caught with a Ruby-specific exception as well. This is shown with the next code listing (Listing 5).

Listing 5 - Catching Java Exception in JRuby as Ruby Exception

#!/usr/bin/env jruby -w

include Java
import java.sql.SQLRecoverableException
require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'
require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'
include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor

puts "CATCHING INTENTIONAL EXCEPTION WITH RUBY'S RUNTIME ERROR"
accessor = GenericOracleDatabaseAccessor.new
begin
puts "#{accessor.perform_database_query \
'jdbc:oracle:thin:@//unknownhost:1521/orcl',
'select first_name, last_name from employees',
['FIRST_NAME', 'LAST_NAME'].to_java(:string)}"
rescue RuntimeError => runtimeError
puts "#{runtimeError}"
puts "Message: #{runtimeError.message}"
puts "Backtrace: #{runtimeError.backtrace}"
end


The output for the above code listing is shown next.



Ruby provides the ability to catch multiple exceptions in the same rescue clause (a feature talked about for Java SE 7). With JRuby, we can leverage this functionality to use the same rescue clause to catch a Java-based exception and a Ruby-based exception. This is demonstrated with Listing 6.

Listing 6 - Catching Java and Ruby Exceptions in Same rescue Clause

#!/usr/bin/env jruby -w

include Java
import java.sql.SQLRecoverableException
require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'
require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'
include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor

puts "CATCHING/RESCUING RUBY/JAVA EXCEPTIONS IN SAME RESCUE BLOCK"
accessor = GenericOracleDatabaseAccessor.new
begin
puts "#{accessor.perform_database_query \
'jdbc:oracle:thin:@//unknownhost:1521/orcl',
'select first_name, last_name from employees',
['FIRST_NAME', 'LAST_NAME'].to_java(:string)}"
rescue RuntimeError, SQLRecoverableException => sqlError
puts "#{sqlError}"
puts "Message: #{sqlError.message}"
puts "Backtrace: #{sqlError.backtrace}"
end


The output for this code listing is now shown.



Ruby adds some extra possibilities to exception handling. Besides the rescue keyword, Ruby also provides the ability to execute a block of code if no rescue block is executed. This is indicated with the else keyword. This is different from ensure (Ruby's equivalent of Java's finally) because it only executes if none of the declared rescue blocks is executed.

The next code listing demonstrates this and the screen shot following the code listing shows what the output looks like upon execution.

Listing 7 - Demonstrating Ruby's else and ensure Keywords

#!/usr/bin/env jruby -w

include Java
import java.sql.SQLRecoverableException
require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'
require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'
include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor

puts "USING RUBY'S ELSE AND ENSURE"
accessor = GenericOracleDatabaseAccessor.new
begin
puts "#{accessor.perform_database_query \
'jdbc:oracle:thin:@//localhost:1521/orcl',
'select first_name, last_name from employees',
['FIRST_NAME', 'LAST_NAME'].to_java(:string)}"
rescue RuntimeError, SQLRecoverableException => sqlError
puts "#{sqlError}"
puts "Message: #{sqlError.message}"
puts "Backtrace: #{sqlError.backtrace}"
else
puts "ELSE: Data Access performed without exception!"
ensure
puts "ENSURE: I am always called whether rescue or else is invoked."
end




Conclusion

Exception handling is just one area in which JRuby makes it easy to mix Java and Ruby. This blog posting has covered different ways to handle exceptions thrown by Java classes in JRuby code. Additional resources with different and greater details are listed next.


Additional Resources

Handling Java Exceptions in JRuby (September 2006)

Dealing with Java Exceptions in JRuby (May 2007)

Java and Ruby Working Together (see "Exception handling" section)

Calling Java from JRuby (Wiki page that was last updated 21 March 2009 at time of this writing)

Using Java Classes in JRuby (September 2007)

Ruby Exceptions: Ruby Study Notes

Ruby's Exception Hierarchy (September 2006)

Programming Ruby: Exceptions, Catch, and Throw (2001)

No comments: