Using JRuby for Web Applications

1. What is JRuby?

JRuby is an implementation of the Ruby programming language that runs on the Java Virtual Machine (JVM). It allows developers to write Ruby code that can seamlessly interact with Java classes and libraries. This creates a powerful bridge between Ruby’s elegance and Java’s extensive ecosystem. You can host JRuby apps using any hosting package from JVMHost.com. They all include a private JVM and the entry package can ordered here.

2. Pros and Cons of JRuby

Pros

Cons

3. Example Web Applications: Email Sending Form, Database app, Hello World app and API endpoint

Below is an example (SendMailServlet.rb) using JRuby with a simple servlet. The servlet serves a form and processes form submissions to send an email.

SendMailServlet.rb

require 'java'

java_import javax.servlet.http.HttpServlet
java_import javax.servlet.http.HttpServletRequest
java_import javax.servlet.http.HttpServletResponse
java_import javax.mail.Message
java_import javax.mail.Session
java_import javax.mail.Transport
java_import javax.mail.internet.InternetAddress
java_import javax.mail.internet.MimeMessage
java_import java.util.Properties
java_import java.io.FileInputStream

class SendMailServlet < HttpServlet

  def doGet(request, response)
    response.setContentType("text/html;charset=UTF-8")
    out = response.getWriter
    out.println(<<-HTML
      <html><body>
      <h2>Send Email</h2>
      <form method='POST'>
        To: <input type='text' name='to'><br><br>
        Subject: <input type='text' name='subject'><br><br>
        Body:<br>
        <textarea name='body' rows='6' cols='40'></textarea><br><br>
        <input type='submit' value='Send'>
      </form>
      </body></html>
    HTML
    )
  end

  def doPost(request, response)
    to = request.getParameter("to")
    subject = request.getParameter("subject")
    body = request.getParameter("body")

    props = Properties.new
    props_path = getServletContext.getRealPath("/WEB-INF/mail.properties")
    props.load(FileInputStream.new(props_path))

    session = Session.getInstance(props, nil)
    msg = MimeMessage.new(session)
    msg.setFrom(InternetAddress.new(props.getProperty("mail.smtp.user")))
    msg.setRecipient(Message::RecipientType::TO, InternetAddress.new(to))
    msg.setSubject(subject)
    msg.setText(body)

    result = ""
    begin
      transport = session.getTransport("smtp")
      transport.connect(
        props.getProperty("mail.smtp.host"),
        props.getProperty("mail.smtp.user"),
        props.getProperty("mail.smtp.pass")
      )
      transport.sendMessage(msg, msg.getAllRecipients)
      transport.close
      result = "OK"
    rescue => e
      result = "FAILURE: #{e}"
    end

    response.setContentType("text/html;charset=UTF-8")
    out = response.getWriter
    out.println("<html><body>")
    out.println("<h2>Result: #{result}</h2>")
    out.println("<a href='SendMailServlet.rb'>Back to form</a>")
    out.println("</body></html>")
  end
end

Below is a JRuby servlet (DBTest.rb) handling database insert + read. We assume a JDBC database such as MySQL or MariaDB is pre-created.

CREATE TABLE messages (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(120),
    subject VARCHAR(255),
    body TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

And the JRuby code is:

require 'java'

java_import javax.servlet.http.HttpServlet
java_import java.sql.DriverManager
java_import java.lang.Class
java_import java.util.Properties
java_import java.io.FileInputStream

class DBTest < HttpServlet

  def init(config)
    @props = Properties.new
    props_path = config.getServletContext.getRealPath("/WEB-INF/db.properties")
    @props.load(FileInputStream.new(props_path))
  end

  def doPost(request, response)
    email = request.getParameter("email")
    subject = request.getParameter("subject")
    body = request.getParameter("body")

    begin
      Class.forName("com.mysql.cj.jdbc.Driver")
      conn = DriverManager.getConnection(
        @props.getProperty("url"),
        @props.getProperty("user"),
        @props.getProperty("pass")
      )

      stmt = conn.prepareStatement(
        "INSERT INTO messages (email, subject, body) VALUES (?, ?, ?)"
      )
      stmt.setString(1, email)
      stmt.setString(2, subject)
      stmt.setString(3, body)
      stmt.executeUpdate

      conn.close
      result = "Message saved successfully."

    rescue => e
      result = "DB error: #{e}"
    end

    out = response.getWriter
    out.println(result)
  end

  def doGet(request, response)
    begin
      Class.forName("com.mysql.cj.jdbc.Driver")
      conn = DriverManager.getConnection(
        @props.getProperty("url"),
        @props.getProperty("user"),
        @props.getProperty("pass")
      )

      stmt = conn.createStatement
      rs = stmt.executeQuery(
        "SELECT id, email, subject, created_at FROM messages ORDER BY id DESC LIMIT 20"
      )

      out = response.getWriter
      out.println("<h1>Last 20 messages</h1>")
      out.println("<ul>")

      while rs.next
        out.println(
          "<li>#{rs.getInt('id')} - #{rs.getString('email')} - #{rs.getTimestamp('created_at')}</li>"
        )
      end

      out.println("</ul>")

      conn.close

    rescue => e
      out = response.getWriter
      out.println("DB error: #{e}")
    end
  end
end

Here goes a simple Hello World (index.rb) example:

require 'java'

java_import javax.servlet.http.HttpServlet

class Index < HttpServlet
  def doGet(req, res)
    res.setContentType("text/html")
    out = res.getOutputStream
    
    out.print("<html>")
    out.print("<head><title>Hello World, How are we?</title></head>")
    out.print("<body>Hello World, how are we?")
    out.print("</body>")
    out.print("</html>")
    out.close
  end
end

And finally, an API endpoint (api/status.rb) example:

require 'java'
require 'json'

java_import javax.servlet.http.HttpServlet
java_import javax.servlet.http.HttpServletRequest
java_import javax.servlet.http.HttpServletResponse
java_import java.lang.System

class Status < HttpServlet
  def doGet(req, res)
    data = {
      status: "ok",
      ruby_version: RUBY_VERSION,
      jruby_version: JRUBY_VERSION,
      timestamp: System.currentTimeMillis.to_s
    }
    
    res.setContentType("application/json")
    res.getWriter.write(JSON.generate(data))
  end
end

4. Manual Deployment Steps on a Hosting Account with JDK 22 and Tomcat 9

Step 1 — Prepare directory structure

jruby-examples
├── api
│   └── status.rb
├── DBTest.rb
├── index.rb
├── SendMailServlet.rb
└── WEB-INF
    ├── classes
    ├── db.properties
    ├── lib
    │   ├── javax.activation.jar
    │   ├── javax.mail.jar
    │   ├── jruby-complete-9.4.5.0.jar
    │   └── mysql-connector-j-8.0.33.jar
    ├── mail.properties
    └── web.xml

Step 2 — Add JARs to WEB-INF/lib/

Download:

jruby-complete.jar

Place in:

WEB-INF/lib/

Step 3 — Create WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <servlet>
        <servlet-name>JRubyServlet</servlet-name>
        <servlet-class>org.jruby.rack.RackServlet</servlet-class>
        <init-param>
            <param-name>jruby.home</param-name>
            <param-value>WEB-INF/lib</param-value>
        </init-param>
        <init-param>
            <param-name>jruby.compile.mode</param-name>
            <param-value>jit</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>JRubyServlet</servlet-name>
        <url-pattern>*.rb</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.rb</welcome-file>
    </welcome-file-list>
</web-app>

mail.properties (for SendMailServlet.rb) should contain (adjust the variables):

[email protected]
mail.smtp.pass=mail_pass
mail.smtp.host=localhost
mail.smtp.port=587
mail.smtp.auth=true
mail.smtp.starttls.enable=true
mail.smtp.ssl.protocols=TLSv1.1 TLSv1.2

db.properties (for DBTest.rb) should contain (adjust the variables):

url=jdbc:mysql://localhost:3306/db_name
user=db_user
pass=db_pass

Step 4 — Build WAR File

cd jruby-examples
jar -cvf ../ROOT.war .

As you can see, compilation is not needed like in case of regular .java files.

Step 5 — Deploy to Tomcat

Copy the WAR into:

cp ROOT.war $CATALINA_HOME/webapps/

Tomcat will unpack it automatically.

Step 6 — Test the application

Visit:

https://jruby.domain.com/
https://jruby.domain.com/SendMailServlet
https://jruby.domain.com/DBTest
https://jruby.domain.com/api/status.rb

to test the 4 apps.

Add data to the database using the DBTest.rb app:

curl -X POST \
  -d "[email protected]" \
  -d "subject=Hello" \
  -d "body=Testing JDBC" \
  https://jruby.domain.com/DBTest.rb

5. Building the WAR with Maven

Instead of manually zipping files, you can use Maven to automate WAR creation and dependency management.

Create the following pom.xml in the parent directory of jruby-examples directory

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>ROOT</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jruby.version>9.4.5.0</jruby.version>
    </properties>

    <dependencies>
        <!-- JRuby Complete -->
        <dependency>
            <groupId>org.jruby</groupId>
            <artifactId>jruby-complete</artifactId>
            <version>${jruby.version}</version>
        </dependency>

        <!-- JRuby Rack for servlet integration -->
        <dependency>
            <groupId>org.jruby.rack</groupId>
            <artifactId>jruby-rack</artifactId>
            <version>1.1.22</version>
        </dependency>

        <!-- Optional: MySQL JDBC -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
        </dependency>

        <dependency>
            <groupId>com.sun.activation</groupId>
            <artifactId>javax.activation</artifactId>
            <version>1.2.0</version>
        </dependency>
        
        <dependency>
            <groupId>com.sun.mail</groupId>
            <artifactId>javax.mail</artifactId>
            <version>1.5.0</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
        <plugins>
            <!-- WAR plugin -->
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
                <configuration>
                    <webResources>
                        <resource>
                            <directory>jruby-examples</directory>
                            <targetPath>/</targetPath>
                            <includes>
                                <include>**/*</include>
                            </includes>
                        </resource>
                    </webResources>
                </configuration>
            </plugin>

            <!-- Copy WAR to Tomcat webapps -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <phase>install</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <copy file="${project.build.directory}/${project.build.finalName}.war"
                                      todir="${env.CATALINA_HOME}/webapps"/>
                            </target>
                            <target>
                                <delete dir="${project.build.directory}"/>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

From the parent directory run:

mvn clean install

This will produce the ROOT.war and copy it to $CATALINA_HOME/webapps for automatic deployment.

Advantages of Maven

6. Building the WAR with Ant

If you prefer Ant, here’s a simple build.xml to create and deploy the WAR. Place it in the parent directory of jruby-examples.

<project name="JRuby examples" default="deploy" basedir=".">

    <property environment="env"/>
    <property name="build.dir" value="build"/>
    <property name="web.dir" value="jruby-examples"/>
    <property name="war.name" value="ROOT.war"/>
    <property name="tomcat.webapps" value="${env.CATALINA_HOME}/webapps"/>

    <target name="clean">
        <delete dir="${build.dir}"/>
        <delete file="${war.name}"/>
    </target>

    <target name="compile">
        <mkdir dir="${build.dir}"/>
    </target>

    <target name="war" depends="compile">
        <delete file="${war.name}"/>  <!-- remove old WAR -->
        <war destfile="${war.name}" webxml="${web.dir}/WEB-INF/web.xml">
            <fileset dir="${web.dir}" includes="**/*"/>
        </war>
    </target>

    <target name="deploy" depends="war">
        <copy file="${war.name}" todir="${tomcat.webapps}"/>
        <delete dir="${build.dir}"/><!-- remove build directory -->
    </target>
</project>

Usage:

ant deploy

This will:

  1. Build the WAR from src/main/webapp
  2. Copy it into Tomcat’s webapps folder
  3. Tomcat automatically deploys it

Advantages of Ant vs Maven:

Feature Maven Ant
Dependency Management Automatic Manual
WAR Packaging Automatic Manual via <war> task
Copy to Tomcat Easy via plugin Easy via <copy> task
Build Scripts XML + conventions XML only, flexible

Above setups lets you automatically package and deploy the jruby-examples web app using either Maven or Ant.

7. Final Notes

See also Why JRuby Hosting on Tomcat Is Different from MRI Ruby