Introduction to jRuby

by Kangqing Yu (Simon)

School of Computer Science

Carleton University

16 May 2018

About Me

  • BSc degree in Computer Science from University of Surrey, UK  (2014)

  • BSc degree in Information Management and System from DUFE, China  (Same year)

  • Software Developer at The ai Corporation, UK (2014-2015)

  • Software Developer at Lifecycle Software, UK (2015-2017)

  • Full-stack Rails Developer at MicroMetrics, Ottawa, Canada (2017 Summer breaks)

  • Currently a MSc student at School of Computer Science, Carleton University

What is jRuby

  • Still Ruby, but run on JVM

  • Interpreter implemented in Java

  • Tightly Integrated with Java with two-way access

  • Open sourced project, started in 2002 by Charles Oliver Nutter (Headius)

  • Aim to be compatible with MRI Ruby (Currently support 2.3)

How I met jRuby

  • Corporates don't prefer Rails

  • Rails has no explicit build step (code not compiled)

  • Great for managed service and multi-tenancy

  • But not ideal for enterprise and single-tenancy

Today's Agenda

  • jRuby programming

  • Install and compiler options

  • Java integration

  • Multi-threading

  • Memory management

  • jRuby Applications

  • jRuby on Rails

  • Further applications

Install jRuby

rvm install jruby #latest version 9.1.13.0

rvm use jruby

jruby version MRI ruby version Latest Java version supported
9.1.X 2.3 9.0
9.0.X 2.2 8.0
1.7.X 1.9.3 8.0
  • Use RVM

jRuby Compiler

  • A Head Of Time Compilation (AOT)

 

#will output bytecode foo/bar/test.class
jrubyc foo/bar/test.rb 

java -cp .:/path/to/jruby.jar foo.bar.test
  • Just-In-Time Compilation (JIT)

#JIT compile source code
jruby -S foo/bar/test.rb
  • Ruby -> bytecode -> JVM

Some runtime options

  • jRuby runtime options:

  • invokedynamics

  • compile

  • jit

  • thread.pool

  •  

jruby --properties
  • JVM runtime options:

  • Heap space

  • Profiling

Configuring jRuby

  • Set JRUBY_OPTS environment variable

export JRUBY_OPTS="-X<property>=<value>"
  • Command Line Argument

jruby -J-Djruby.<property>=<value> -S foo.rb
  • In .jrubyrc file (after version 1.6.5)

<property>=<value>

Java Integration

  • Scripting Java from jRuby

  • Embedding Ruby in Java

  • Generating Java classes in jRuby

  • Java extension

Loading Java Classes

require 'java'
  • Load bootstrap classes:

  • Load additional classes:

require 'path/to/mycode.jar'

$CLASSPATH << "target/classes"

Referencing Java Classes

Java: org.foo.department.Widget
Ruby: Java::OrgFooDepartment::Widget
  • Full-qualified class name

  • Importing into a constant

 java_import java.util.ArrayList
 list = ArrayList.new
  • Importing into current namespace

module MyApp
 include_package 'org.apache.logging.log4j'
 Logger = LogManager.getLogger('MyApp')
end

Calling Java methods

  • Camel case <-> Snake case

  java.lang.System.currentTimeMillis
  java.lang.System.current_time_millis
  • Bean convensions

  x.getSomething            becomes   x.something
  x.setSomething(newValue)  becomes   x.something = new_value
  x.isSomething             becomes   x.something?
  • Additional Java methods

java_class
java_kind_of? #the same as instanceof operator
java_object
java_send #based on reflection
java_method
java_alias

Inheriting (subclassing) Java classes

require 'java'

class HelloThread < java.lang.Thread 
    def run
      puts 'hello world' 
    end
end

HelloThread.new.start

Note:  use field_accessor to access protected fields

Implementing Java interfaces

require 'java'

class HelloThread
    # This is optional as jruby supports duck-typing
    include java.lang.Runnable
    
    def run
        puts "hello world"
    end
end

java.lang.Thread.new(HelloThread.new).start
  • jRuby supports duck-typing

  • However, it is still necessary in some cases (see following)

Note:

public interface Balloon {
    void pop(); 
}
public interface Bubble {
    void pop(); 
}
public class Child{
    public void give(Bubble bubble){
        System.out.println("Thanks for the bubble.");
        bubble.pop(); 
    }
    public void give(Balloon balloon){ 
        System.out.println("Thanks for the balloon.");
        balloon.pop(); 
    }
}
require 'java'

class MyBalloon
    #required as jruby runtime will 
    #use this to infer the type information
    include Java::Ballon

    def pop
        puts 'Oh No!!!' 
    end
end

child = Java::Child.new 
child.give(MylarBalloon.new)

Java:

jRuby:

Implementing Functional-style interfaces

button = new javax.swing.JButton("Press me!");

button.addActionListener(new ActionListener(){ 
  public void actionPerformed(ActionEvent e){ 
    ((javax.swing.JButton) e.getSource()).setText("You did it!");
  } 
});
button = javax.swing.JButton.new "Press me!"

button.add_action_listener do |event|
 event.source.text = "You did it!"
end
  • Ruby Proc style:
  •  Java anonymous class style:

Handling Exceptions from Java

 begin
   java.lang.Integer.parse_int("asdf")
 rescue java.lang.NumberFormatException => e
   puts "Failed to parse integer: #{e.message}"
   puts e.print_stack_trace
 end
  • Catch exceptions

  • Raise exceptions

raise java.lang.IllegalArgumentException.new("Bad param")

Java Enumerations

  • Include Enumerable module in Ruby by default

ni = java.net.NetworkInterface.networkInterfaces
puts ni.class.ancestors

#=> [#<Class:01x7e666f>, Java::JavaUtil::Enumeration, Enumerable, 
#        Java::JavaLang::Object, ConcreteJavaProxy, JavaProxy,
#         JavaProxyMethods, Object, Java, Kernel]

java.net.NetworkInterface.networkInterfaces.each {|i| puts i }
  • Allocate new Java enumeration class

# Equivalent to Java's bytes = new byte[1024];  
bytes = Java::byte[1024].new 
list = java.util.ArrayList[1024].new
map = java.util.HashMap[1024].new

Type conversions

  • Manual
  [1,2,3].to_java
  ["a","b","c"].to_java(:string)
  [1, 2, 3.5].to_java Java::double

  java_arry.to_a
  java_map.to_hash
  java_set.to_set

  bytes = 'a string'.to_java_bytes
  string = String.from_java_bytes bytes

  # works for InputStreams, OutputStreams, and NIO Channels
  io = java_input_stream.to_io 
  # also to_outputstream and to_channel
  stream = io.to_inputstream

Type conversions

  • Automatic
Ruby Type Java Type Literal
String java.lang.String "foo"
Fixnum java.lang.Long, short, long, int, byte, char 1
Float java.lang.Double, java.lang.Float, float, double 1.0
TrueClass, FalseClass java.lang.Boolean true, false
Bignum java.math.BigInteger 1 << 128
NilClass null nil

Overloading

  • Constructor signature

 # Get the the three-integer constructor for this class
 construct = JavaClass.java_class.constructor(Java::int, Java::int, Java::int)
 object = construct.new_instance(0xa4, 0x00, 0x00)
  • Method signature

 java_import java.util.ArrayList

 list = ArrayList.new
 list.java_send :add, [Java::int, java.lang.Object], 0, 'foo'

 add = list.java_method :add, [Java::int, java.lang.Object]
 add.call(0, 'foo')

 class ArrayList
   java_alias :simple_add, :add, [Java::int, java.lang.Object]
 end

Multi-threading in jRuby

  • In 1.8.7, all threads map to a single native thread

  • In 2.0.0, each thread maps to a native OS thread (with GIL)

  • In jRuby, Ruby thread map to java.lang.Thread in Java (real-threading, with NO GIL) 😃

Parallelism

def bench_mark(n)
  start = Time.now
  1.upto(n).map do |n|
    Thread.new { sleep(0.5) }
  end.each(&:join)
  Time.now - start
end
  • IO operations

Parallelism

Number of threads

Running time

Parallelism

def bench_mark(n)
  start = Time.now
  1.upto(n).map do |n|
    Thread.new { fib(32) }
  end.each(&:join)
  Time.now - start
end

def fib(n)
  n < 2 ? n : fib(n-1) + fib(n-2)
end
  • Arithmetic operations

Parallelism

Number of threads

Running time

Concurrency

  • Don't do it, if possible

  • If do it, don's share data

  • If share data, don's share mutable data

  • If share mutable data, make sure it is thread-safty

Rule of thumb:

Thread-safty

  • Synchronized keyword

 obj.synchronized do
   obj.wait 1000
 end
  • Most operations in jruby are implemented thread-safely

  • Some gems might not be thread-safe e.g. dragonfly, json-schema

Memory management

  • Mark and Sweep GC

  • Generational GC in JVM

  • Young generation && old generation

  • Major GC run && minor GC run

  • GCs are triggered lazily 😥

Memory management

Some tips:

  • Get rid of unused objects early

  • Set heap space manually by setting:

jruby -J-Xmn512m -J-Xms2048m -J-Xmx2048m -J-server
  • Do not enable ObjectSpace

  • Running Rails app in thread-safe mode

jRuby on Rails

  • C extension alternatives

  • Debugging

  • Dependency management

  • Deployment options

  • Background jobs

C extension alternatives for DB Driver

  • Change DB driver adaptor
#Gemfile
gem 'activerecord-jdbcmysql-adapter'# for mysql
gem 'activerecord-jdbcsqlite3-adapter'# for sqlite
gem 'activerecord-jdbcpostgresql-adapter'# for postgres
  • Currently support Rails 5.1 (except for postgres)

  • activerecord-jdbc-adaptor only support ActiveRecord 5.1 at the moment
  • activerecord-jdbcpostgres-adaptor gem doesn't work with ActiveRecord 5.1

    Check out https://github.com/jruby/activerecord-jdbc-adapter/issues/780

  • Rails 4.2 is fully compatible

C Extension Alternatives for other gems

  • Find an alternative gems that is written based on Java extension
  • pg -> activerecord-jdbcpostgresql-adaptor

  • memcached -> jruby-memcached

  • RMagick -> RMagick4J

  • therubyracer -> therubyrhino

  • ...etc.

  • If nothing is found, just use the pure Java versions! e.g. Nokogiri

Dependency Management

  • Use Bundler gem for managing gem dependencies

#Jarfile
# Custom repos which are not Maven Central
repository :aicorporation, "LOCAL_REPO_URL"

jar 'com.fasterxml.jackson.module:jackson-module-mrbean', '2.4.3'

jar 'com.fasterxml.jackson.core:jackson-core', '2.4.3'

jar 'com.fasterxml.jackson.core:jackson-databind', '2.4.3'
#in Gemfile
platform :jruby do
  gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3.7'
end

platform :ruby do
  gem 'pg', '~> 0.17.1'
end
  • Use jbundler gem for managing jar depedencies

Debugging

  • Gems byebug and pry-byebug are MRI only

  • These gems can be used for jRuby

# add the --debug flag to your ruby/jruby command
  def index
    binding.pry
    @posts = Post.all
  end

Deployment options

  • Traditional Rails Servers (Rack-based)

  • War file deployment (J2EE-based)

  • Tomcat

  • Jetty

  • JBoss

  • GlassFish

  • WebLogic

  • WebSphere

Further Deployment options

  • Asynchronous web servers (non-blocking)

package cz.wildweb.example;

import static cz.wildweb.api.Wildweb.*;

public class Main {

    public static void main(String[] args) {
        get("/", (req, res) -> { res.close("Welcome"); });
        startServer(8080);
    }

}

War file deployment

jruby -S gem install warbler
  • Generate config file and configure warble (optional)

# generate config file in config/warble.rb
warble config
  • Compile assets input public folder

jruby -S rake assets:precompile
  • Generate war file

# at root of rails project
warble 

How it works

  • Rails is based on Rack interface (as middleware)

  • J2EE app servers are compliant with Java Servlet API

  • Warbler depends on jruby-rack gem that 'translate' the Rack-interface used in Rails into Java Servlet API

Rails          Rack          jruby-rack          Java Servlet          J2EE App Server

J2EE App

Rack App

// in web.xml file
<listener>
  <listener-class>org.jruby.rack.rails.RailsServletContextListener</listener-class>
</listener>

Configuring warbler

  • config/warble.rb

  • config/web.xml.erb

  # only these four features supported atm  
  config.features = %w(gemjar executable runnable compiled)

  # Application directories to be included in the webapp.
  config.dirs = %w(app config db lib log script vendor tmp)

  # Public ROOT mapping
  config.public.root = ''

  # Value of RAILS_ENV for the webapp -- default as shown below
  config.webxml.rails.env = ENV['RAILS_ENV'] || 'production'

  # Control the pool of Rails runtimes.
  config.webxml.jruby.min.runtimes = 2
  config.webxml.jruby.max.runtimes = 4
  • Two places to configure:
  • Some commonly used options (config/warble.rb):

Background Jobs

  • Thread-based workers

  • On top of jruby-rack (runs in J2EE container)

  • Support Resque, Deplayed::Job,Navvy natively

  • Custom worker can be provided

  • mach_hare gem

  • jruby client for RabbitMQ

  • Similar to the bunny gem in MRI

Example from jruby-rack-worker

  //in web.xml file
  <context-param>
    <param-name>jruby.worker</param-name>
    <param-value>resque</param-value>
  </context-param>
  <context-param>
    <param-name>QUEUES</param-name>
    <param-value>mails,posts</param-value>
  </context-param>
  <context-param>
    <param-name>INTERVAL</param-name>
    <param-value>2.5</param-value>
  </context-param>

  <listener>
    <listener-class>org.kares.jruby.rack.WorkerContextListener</listener-class>
  </listener>

Things to take note

  • For Rails 2.x, you need to install jruby-openssl gem

jruby -S gem install jruby-openssl
  • Don't forget JCE Policy Files

  • Some jars in jBundler may not be packaged correctly in war file

  #in config/warble.rb
  config.java_libs += FileList["lib/java/*.jar"]
  • You might need this jar manny in some cases

#in Jarfile
jar 'org.bouncycastle:bcprov-jdk15on', '1.47'

Further applications in jRuby

  • Java Swing GUI

  • Spring MVC

  • JNDI

  • JMS

  • LDAP

  • .etc

Where to find more information

  • jRuby Wiki page 

  • jRuby Cookbook by Justin Edelson

  • Deploying with jRuby by Joe Kutner

Thank you

My contact details:

Questions?

Introduction to jRuby

By Simon Yu

Introduction to jRuby

Presentation slides for Ruby Meetup in Ottawa, 16 May 2018

  • 321