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
  1. #!/usr/bin/env jruby -w  
  2.   
  3. include Java  
  4. import java.sql.SQLRecoverableException  
  5. require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'  
  6. require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'  
  7. include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor  
  8.   
  9. # 1. "Happy Path" query to demonstrate JRuby/Java interaction works fine  
  10. puts "ALL'S WELL"  
  11. accessor = GenericOracleDatabaseAccessor.new  
  12. puts "#{accessor.perform_database_query \  
  13.    'jdbc:oracle:thin:@//localhost:1521/orcl',  
  14.    'select first_name, last_name from employees',  
  15.    ['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
  1. package dustin.examples.jruby;  
  2.   
  3. import java.sql.Connection;  
  4. import java.sql.ResultSet;  
  5. import java.sql.SQLException;  
  6. import java.sql.Statement;  
  7. import java.util.List;  
  8. import oracle.jdbc.pool.OracleDataSource;  
  9.   
  10. /** 
  11.  * Class used for demonstration purposes only and should not be used anywhere 
  12.  * in which security or maintainability matter.  The problems with this 
  13.  * particular Java class include its being in the unnamed package, the extreme 
  14.  * riskiness of supporting a generic query of any String passed into the method, 
  15.  * lack of use of PreparedStatement, lack of verification of correct and 
  16.  * non-null parameters before applying them, etc.  In fact, this class is used 
  17.  * here because its many problems make it easy to have it throw exceptions that 
  18.  * the JRuby example code can catch and handle. 
  19.  */  
  20. public class GenericOracleDatabaseAccessor  
  21. {  
  22.    final static String NEW_LINE = System.getProperty("line.separator");  
  23.   
  24.    /** 
  25.     * Perform a generic database query.  <strong>CAUTION</strong>: There are 
  26.     * numerous reasons why this particular code sample should not be used in 
  27.     * any real or production settings.  It is used here only to demonstrate 
  28.     * how JRuby supports handling of Java exceptions in a Ruby-like manner.  It 
  29.     * is not a good idea normally to pass in the JDBC URL.  The JDBC exception 
  30.     * handling is also intentionally poor in this example.  See class description 
  31.     * for other failings (mostly intentional) of this class and method.. 
  32.     * 
  33.     * @param jdbcUrl The JDBC URL for the database connection; a default is 
  34.     *    attempted if this is null. 
  35.     * @param queryString The query statement to be run against the database. 
  36.     * @param columnNames Column Names whose values are to be returned in 
  37.      *   resulting String. 
  38.     * @throws SQLException Checked exception related to use of JDBC. 
  39.     */  
  40.    public String performDatabaseQuery(  
  41.       final String jdbcUrl,  
  42.       final String queryString,  
  43.       final String[] columnNames) throws SQLException  
  44.    {  
  45.       final OracleDataSource datasource = new OracleDataSource();  
  46.       final String url =  jdbcUrl == null  
  47.                         ? "jdbc:oracle:thin:@//localhost:1521/orcl"  
  48.                         : jdbcUrl;  
  49.       datasource.setURL(url);  
  50.       datasource.setUser("hr");  
  51.       datasource.setPassword("hr");  
  52.       final Connection connection = datasource.getConnection();  
  53.       final Statement queryStatement = connection.createStatement();  
  54.       final ResultSet resultSet = queryStatement.executeQuery(queryString);  
  55.       final StringBuilder returnString = new StringBuilder();  
  56.       while (resultSet.next())  
  57.       {  
  58.          for (final String columnName : columnNames)  
  59.          {  
  60.             returnString.append(resultSet.getObject(columnName)).append(" ");  
  61.          }  
  62.          returnString.append(NEW_LINE);  
  63.       }  
  64.       resultSet.close();  
  65.       queryStatement.close();  
  66.       connection.close();  
  67.       return returnString.toString();  
  68.    }  
  69. }  


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
  1. #!/usr/bin/env jruby -w  
  2.   
  3. include Java  
  4. import java.sql.SQLRecoverableException  
  5. require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'  
  6. require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'  
  7. include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor  
  8.   
  9. puts "EXCEPTION WITHOUT HANDLING"  
  10. accessor = GenericOracleDatabaseAccessor.new  
  11. puts "#{accessor.perform_database_query \  
  12.    'jdbc:oracle:thin:@//unknownhost:1521/orcl',  
  13.    'select first_name, last_name from employees',  
  14.    ['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
  1. #!/usr/bin/env jruby -w  
  2.   
  3. include Java  
  4. import java.sql.SQLRecoverableException  
  5. require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'  
  6. require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'  
  7. include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor  
  8.   
  9. puts "RUBY CATCHING OF JAVA EXCEPTION"  
  10. accessor = GenericOracleDatabaseAccessor.new  
  11. begin  
  12.    puts "#{accessor.perform_database_query \  
  13.       'jdbc:oracle:thin:@//unknownhost:1521/orcl',  
  14.       'select first_name, last_name from employees',  
  15.       ['FIRST_NAME''LAST_NAME'].to_java(:string)}"  
  16. rescue SQLRecoverableException => sqlRecoverableException  
  17.    puts "#{sqlRecoverableException}"  
  18.    puts "Message: #{sqlRecoverableException.message}"  
  19.    puts "Backtrace: #{sqlRecoverableException.backtrace}"  
  20. 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
  1. #!/usr/bin/env jruby -w  
  2.   
  3. include Java  
  4. import java.sql.SQLRecoverableException  
  5. require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'  
  6. require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'  
  7. include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor  
  8.   
  9. puts "CATCHING INTENTIONAL EXCEPTION WITH RUBY'S RUNTIME ERROR"  
  10. accessor = GenericOracleDatabaseAccessor.new  
  11. begin  
  12.    puts "#{accessor.perform_database_query \  
  13.       'jdbc:oracle:thin:@//unknownhost:1521/orcl',  
  14.       'select first_name, last_name from employees',  
  15.       ['FIRST_NAME''LAST_NAME'].to_java(:string)}"  
  16. rescue RuntimeError => runtimeError  
  17.    puts "#{runtimeError}"  
  18.    puts "Message: #{runtimeError.message}"  
  19.    puts "Backtrace: #{runtimeError.backtrace}"  
  20. 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
  1. #!/usr/bin/env jruby -w  
  2.   
  3. include Java  
  4. import java.sql.SQLRecoverableException  
  5. require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'  
  6. require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'  
  7. include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor  
  8.   
  9. puts "CATCHING/RESCUING RUBY/JAVA EXCEPTIONS IN SAME RESCUE BLOCK"  
  10. accessor = GenericOracleDatabaseAccessor.new  
  11. begin  
  12.    puts "#{accessor.perform_database_query \  
  13.       'jdbc:oracle:thin:@//unknownhost:1521/orcl',  
  14.       'select first_name, last_name from employees',  
  15.       ['FIRST_NAME''LAST_NAME'].to_java(:string)}"  
  16. rescue RuntimeError, SQLRecoverableException => sqlError  
  17.    puts "#{sqlError}"  
  18.    puts "Message: #{sqlError.message}"  
  19.    puts "Backtrace: #{sqlError.backtrace}"  
  20. 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
  1. #!/usr/bin/env jruby -w  
  2.   
  3. include Java  
  4. import java.sql.SQLRecoverableException  
  5. require 'C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar'  
  6. require 'C:\java\examples\jrubyExamples\jrubyExceptionHandling\lib\Dustin.jar'  
  7. include_class Java::dustin.examples.jruby.GenericOracleDatabaseAccessor  
  8.   
  9. puts "USING RUBY'S ELSE AND ENSURE"  
  10. accessor = GenericOracleDatabaseAccessor.new  
  11. begin  
  12.    puts "#{accessor.perform_database_query \  
  13.       'jdbc:oracle:thin:@//localhost:1521/orcl',  
  14.       'select first_name, last_name from employees',  
  15.       ['FIRST_NAME''LAST_NAME'].to_java(:string)}"  
  16. rescue RuntimeError, SQLRecoverableException => sqlError  
  17.    puts "#{sqlError}"  
  18.    puts "Message: #{sqlError.message}"  
  19.    puts "Backtrace: #{sqlError.backtrace}"  
  20. else  
  21.    puts "ELSE: Data Access performed without exception!"  
  22. ensure  
  23.    puts "ENSURE: I am always called whether rescue or else is invoked."  
  24. 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: