Saturday, April 15, 2006

Jetty setup for serving web apps

My servlet/JSP container of choice is Resin. For those that are unfamiliar with Resin, it is a fast and extremely easy to use servlet container. However, of late, I have been experimenting with using Jetty as an in-place servlet container that I can start with Ant and run my JWebUnit tests.

While there is some documentation in the Jetty site that explains how to set up Jetty to serve JSP pages from a web application, the process is not exactly straightforward. Moreover, there was not much information available on the web about my desired setup. The Jetty documentation actually recommends that one should precompile the JSPs using the Jasper compiler before deployment, implying that JSP support may be flaky. I am happy to report, however, that I was successful in serving an application that uses Spring and Hibernate for the Model and Controller layers, and JSTL in JSPs for the View layer. So I decided to write up my experience hoping that it would be helpful to someone else with similar requirements.

My desired setup was to be able to start Jetty as an Ant target in a standard Web Application project. The Jetty server would work directly on the application. No packaging into a WAR and deploying should be necessary, and neither would the server be required to explode a WAR file. To minimize problems, I made sure that my webapp worked perfectly with Resin.

Interestingly, there are at least 3 ways to start up Jetty for any specific purpose. The first approach is to use the provided start.jar with an application specific XML configuration file. The second approach is to call the org.mortbay.jetty.Server with the appropriate classpath settings from within Ant or from a shell script, and the application specific XML configuration file. The third approach is to write a class that will instantiate the Server and configure it using Java code.

The first approach is closely tied to the directory structure of the Jetty distribution, so if your application has a different directory structure, as mine was, you would need to override the start.config with your own settings. I did not want to package the Jetty JAR files along with this start.jar in my application, and I did not really want to mess with the non-standard start.config file (unless I could not do it any other way), so that eliminated the first approach.

I started off with the third approach, but had some early successes, but since I was still trying to figure out the configuration that will work for me, this approach started me on a path of too many compile-test cycles, so I ultimately gave it up in favor of the second approach.

I used the latest stable full release of Jetty at the time of writing this, which is 5.1.10. I downloaded jetty-5.1.10-all.zip which contained the demo and the sources. I started reading through the documentation and found that there is an example web application XML configuration etc/jetty.xml file which worked with a built in Ant target "run". This target uses the second approach outlined above.

I started out with a copy of jetty.xml as my starting point. To find the classes that need to be in the Java classpath for the Server class to run correctly, I ran the following command in the root directory of the Jetty download distribution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[sujit@cyclone jetty-5.1.10]$ ant -v run
Apache Ant version 1.6.5 compiled on June 2 2005
     [java] ... output snipped ...
     [java] Executing '/usr/java/jdk1.5.0_03/jre/bin/java' with arguments:
     [java] '-Djetty.home=/home/sujit/tmp/jetty-5.1.10'
     [java] '-classpath'
     [java] '/home/sujit/tmp/jetty-5.1.10/lib/org.mortbay.jetty.jar:             /home/sujit/tmp/jetty-5.1.10/lib/javax.servlet.jar:             /home/sujit/tmp/jetty-5.1.10/ext/jasper-runtime.jar:             /home/sujit/tmp/jetty-5.1.10/ext/jasper-compiler.jar:             /home/sujit/tmp/jetty-5.1.10/ext/ant.jar:             /home/sujit/tmp/jetty-5.1.10/ext/commons-el.jar:             /home/sujit/tmp/jetty-5.1.10/ext/commons-logging.jar:             /home/sujit/tmp/jetty-5.1.10/ext/mx4j-remote.jar:             /home/sujit/tmp/jetty-5.1.10/ext/mx4j-tools.jar:             /home/sujit/tmp/jetty-5.1.10/ext/mx4j.jar:             /home/sujit/tmp/jetty-5.1.10/ext/xercesImpl.jar:             /home/sujit/tmp/jetty-5.1.10/ext/xml-apis.jar:             /home/sujit/tmp/jetty-5.1.10/ext/xmlParserAPIs.jar'
     [java] 'org.mortbay.jetty.Server'
     [java] '/home/sujit/tmp/jetty-5.1.10/etc/admin.xml'
     [java] '/home/sujit/tmp/jetty-5.1.10/etc/jetty.xml'
     [java] ... more stuff snipped ...

Some of the JARs I already had in my WEB-INF/lib directory, the rest I copied from the Jetty distribution into my WEB-INF/lib directory of the webapp. I also made a copy of the supplied etc/jetty.xml file. The jetty.xml file is set up to start all applications under the context root, so I commented that portion and uncommented the next block which works with a single web application. I also changed the context root and the webapp name for my web application. This is the block that I changed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  <Call name="addWebApplication">
    <Arg>/prozac</Arg>
    <Arg>./webapps/prozac</Arg>
                                                                                
    <Set name="extractWAR">false</Set>
    <Set name="defaultsDescriptor">org/mortbay/jetty/servlet/webdefault.xml</Set>
    <Set name="classLoaderJava2Compliant">true</Set>
                                                                                
    <Set name="virtualHosts">
      <Array type="java.lang.String">
        <Item></Item>
        <Item>127.0.0.1</Item>
        <Item>localhost</Item>
      </Array>
    </Set>
  </Call>

I also uncommented the systemClasses and serverClasses section that prevents the webapp from reloading the classes listed under systemClasses and makes the serverClasses inaccessible from the web application. Since I was using the JSTL tag libraries, I also uncommented the TagLibConfiguration under WebApplicationConfigurationClassNames.

I also built a local "start-server" target that mimicked the "run" target of the Jetty distribution. When I ran this target, I got a log4j error, saying that log4j was not properly configured. A quick look at the log4j documentation pointed me to the answer, which was to add a system property "log4j.configuration" pointing to a file URL for the log4j.properties file.

One thing I want to mention here is that I created a specific log4j.properties file for the Jetty server, which was different from what I was using for the rest of the application. The reason for this is that I wanted to only log messages INFO and above for Jetty but DEBUG and above for the rest of the application. Setting the level to DEBUG for Jetty gives many messages which look like errors but is basically Jetty cycling through various alternatives. Also the DEBUG logging for Jetty is quite verbose and not very useful unless you are debugging Jetty.

The next roadblock I had was that Jetty complained that it could not find the class javax.servlet.jsp.jstl.fmt.LocalizationContext. I found this class in lib/jstl-11.jar of my Resin distribution, so I copied this to my local server classpath as well.

The next problem I had was that Jasper failed to compile my JSP because it could not find com.sun.javac.Main in my JDK. It gives a misleading message about possible bad setting of JAVA_HOME, but if you specifically include the tools.jar file of your Java distribution in your classpath, it is able to compile the JSP.

Another little side note. For those who are tempted to ignore the ant.jar in the original classpath, as I was, based on the understanding that ant.jar is already in the classpath since the target is being invoked by Ant, here is the reason why ant.jar is needed - Jasper uses Ant and the Java compiler javac to process and compile the JSPs in the application, and the ant.jar does need to be specifically included in the classpath.

The final problem before everything came together was the start-server target complaining that the log4j.properties file was not a zip file. This was because I had added the log4j.properties file to the classpath before I found out about the log4j.configuration system property. Removing the log4j.properties file allowed Jetty to start up without problems and serve my web application without any problems.

Here is my Ant target for starting the Jetty server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    <target name="start-server" depends="setup" description="Starts the built-in Jetty server">
        <java fork="yes" classname="org.mortbay.jetty.Server" dir="." failonerror="true">
            <classpath>
                <fileset dir="lib">
                    <include name="org.mortbay.jetty.jar" />
                    <include name="javax.servlet.jar" />
                    <include name="jasper-runtime*.jar" />
                    <include name="jasper-compiler*.jar" />
                    <include name="ant*.jar" />
                    <include name="commons-el*.jar" />
                    <include name="commons-logging*.jar" />
                    <include name="mx4j-remote*.jar" />
                    <include name="mx4j-tools*.jar" />
                    <include name="xercesImpl*.jar" />
                    <include name="xml-apis*.jar" />
                    <include name="xmlParserAPIs*.jar" />
                    <include name="log4j*.jar" />
                 </fileset>
                 <fileset dir="${env.JAVA_HOME}/lib">
                     <include name="tools.jar" />
                 </fileset>
            </classpath>
            <jvmarg line="-Djetty.home=${basedir}" />
            <arg value="WEB-INF/jetty.xml" />
            <sysproperty key="log4j.configuration" value="file://${basedir}/WEB-INF/classes/jetty-log4j.properties" />
        </java>
    </target>

And here is the contents of my jetty.xml file (commented out sections omitted for brevity), which is being passed as an argument to the org.mortbay.jetty.Server class in the "start-server" target:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.mortbay.org/configure.dtd">
 
<Configure class="org.mortbay.jetty.Server">
 
  <Call name="addListener">
    <Arg>
      <New class="org.mortbay.http.SocketListener">
        <Set name="Port"><SystemProperty name="jetty.port" default="8080"/></Set>
        <Set name="PoolName">P1</Set>
        <Set name="MinThreads">20</Set>
        <Set name="MaxThreads">200</Set>
        <Set name="lowResources">50</Set>
        <Set name="MaxIdleTimeMs">30000</Set>
        <Set name="LowResourcePersistTimeMs">2000</Set>
        <Set name="acceptQueueSize">0</Set>
        <Set name="ConfidentialPort">8443</Set>
        <Set name="IntegralPort">8443</Set>
      </New>
    </Arg>
  </Call>
 
  <Set name="WebApplicationConfigurationClassNames">
    <Array type="java.lang.String">
      <Item>org.mortbay.jetty.servlet.XMLConfiguration</Item>
      <Item>org.mortbay.jetty.servlet.JettyWebConfiguration</Item>
      <Item>org.mortbay.jetty.servlet.TagLibConfiguration</Item>
    </Array>
  </Set>
 
  <Call name="addWebApplication">
    <Arg>/prozac</Arg>
    <Arg>./webapps/prozac</Arg>
 
    <Set name="extractWAR">false</Set>
    <Set name="defaultsDescriptor">org/mortbay/jetty/servlet/webdefault.xml</Set>
    <Set name="classLoaderJava2Compliant">true</Set>
 
    <Set name="virtualHosts">
      <Array type="java.lang.String">
        <Item></Item>
        <Item>127.0.0.1</Item>
        <Item>localhost</Item>
      </Array>
    </Set>
  </Call>
 
  <Set name="RequestLog">
    <New class="org.mortbay.http.NCSARequestLog">
      <Arg><SystemProperty name="jetty.home" default="."/>/logs/yyyy_mm_dd.request.log</Arg>
      <Set name="retainDays">90</Set>
      <Set name="append">true</Set>
      <Set name="extended">false</Set>
      <Set name="LogTimeZone">GMT</Set>
    </New>
  </Set>
 
  <Set name="requestsPerGC">2000</Set>
  <Set name="statsOn">false</Set>
  <Set class="org.mortbay.util.FileResource" name="checkAliases" type="boolean">true</Set>
 
  <Set name="systemClasses">
    <Array type="java.lang.String">
      <Item>java.</Item>
      <Item>javax.servlet.</Item>
      <Item>javax.xml.</Item>
      <Item>org.mortbay.</Item>
      <Item>org.xml.</Item>
      <Item>org.w3c.</Item>
      <Item>org.apache.commons.logging.</Item>
    </Array>
  </Set>
 
  <Set name="serverClasses">
    <Array type="java.lang.String">
      <Item>-org.mortbay.http.PathMap</Item>
      <Item>org.mortbay.http.</Item>
      <Item>-org.mortbay.jetty.servlet.Default</Item>
      <Item>-org.mortbay.jetty.servlet.Invoker</Item>
      <Item>-org.mortbay.jetty.servlet.JSR154Filter</Item>
      <Item>org.mortbay.jetty.</Item>
      <Item>org.mortbay.start.</Item>
      <Item>org.mortbay.stop.</Item>
    </Array>
  </Set>
 
</Configure>

No other changes were required in the application. I have seen Jetty being used before as an embedded servlet container, and I know that JBoss used Jetty as its servlet container of choice at one point, so Jetty itself is not entirely new to me. However, this is the first time I have successfully used Jetty for anything. I found Jetty to be quite nimble and light on resources. I think that along with the many uses for Jetty as a lightweight, embeddable, high performance servlet container for moderate traffic (I am told that performance degrades with extremely high traffic volumes), it can also be generally useful for the use I am putting it to, that is, to serve as an in-place servlet container for JSP unit testing.

10 comments (moderated to prevent spam):

Anonymous said...

Very helpful information indeed!

Sujit Pal said...

Thanks, I am glad it was helpful.

kgignatyev said...

Jetty6rc4 by some reasons do not like
Call name="addWebApplication" and says NoSuchMethod

Hemant Patel said...

Hi Sujit,
I had solved a problem in my project, thanks.

Hemant Patel said...

Hi Sujit,
I had solved a problem in my project.
Thanks a lot.

Sujit Pal said...

You are welcome Hemant, and thanks for the feedback.

Anonymous said...

sujitpal jindabad sujitpal jindabad
sadaa neetya kasie ho sujitpal jasia ho
akali dal jindabad
congress jindabad

Sujit Pal said...

Thanks for the effusive comment, Anonymous. This was something I did before I discovered that maven2 did all these things for me, and more. If you liked this post, you should seriously consider looking at maven2.

Unknown said...

somebody help me to configure my jetty? because i'm new in jetty? and my task is to copy all the jar files to my jetty. how to do it please? advance thanks for helping me. please contact me to melvinbautista@yahoo.com

Sujit Pal said...

Hi Melvin, if you look at the addWebApplication tag in the jetty.xml file (in my post), the first argument is the path to the context relative to the directory where Jetty is running. JAR files go under ${contextDir}/WEB-INF/lib.