For hardcore server-side Java developer the only way to "speak" out to the world is by using APIs. Today's post is all about JAX-RS: writing and exposing RESTful services using Java.
But we won't do that using a traditional, heavyweight approach involving application server, WAR packaging and whatnot. Instead, we will use awesome Apache CXF framework and as always rely on Spring to wire all pieces together. And for sure we won't stop on that either as we need a web server to run our services on. Using fat or one jar concept we will embed Jetty server into our application and make our final JAR redistributable (all dependencies included) and runnable.
It's a lot of work so let's get started. As we stated above, we will use Apache CXF, Spring and Jetty as a building blocks so let's have them described in a POM file. The one additional dependency worth mentioning is excellent Jackson library for JSON processing.
4.0.0 com.example spring-one-jar 0.0.1-SNAPSHOT jar UTF-8 2.7.2 3.2.0.RELEASE 8.1.8.v20121106 org.apache.cxf cxf-rt-frontend-jaxrs ${org.apache.cxf.version} javax.inject javax.inject 1 org.codehaus.jackson jackson-jaxrs 1.9.11 org.codehaus.jackson jackson-mapper-asl 1.9.11 cglib cglib-nodep 2.2 org.springframework spring-core ${org.springframework.version} org.springframework spring-context ${org.springframework.version} org.springframework spring-web ${org.springframework.version} org.eclipse.jetty jetty-server ${org.eclipse.jetty.version} org.eclipse.jetty jetty-webapp ${org.eclipse.jetty.version org.apache.maven.plugins maven-compiler-plugin 3.0 org.apache.maven.plugins maven-jar-plugin com.example.Starter org.dstovall onejar-maven-plugin 1.4.4 0.97 onejar one-jar onejar-maven-plugin.googlecode.com http://onejar-maven-plugin.googlecode.com/svn/mavenrepo maven2-repository.dev.java.net http://download.java.net/maven/2/
It's a lot of stuff but should be pretty clear. Now, we are ready to develop our first JAX-RS services by starting with simple JAX-RS application.
package com.example.rs; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath( "api" ) public class JaxRsApiApplication extends Application { }
As simple as it looks like, our application defines an /api to be the entry path for the JAX-RS services. The sample service will manage people represented by Person class.
package com.example.model; public class Person { private String email; private String firstName; private String lastName; public Person() { } public Person( final String email ) { this.email = email; } public String getEmail() { return email; } public void setEmail( final String email ) { this.email = email; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public void setFirstName( final String firstName ) { this.firstName = firstName; } public void setLastName( final String lastName ) { this.lastName = lastName; } }
And following bare bones business service (for simplicity, no database or any other storage are included).
package com.example.services; import java.util.ArrayList; import java.util.Collection; import org.springframework.stereotype.Service; import com.example.model.Person; @Service public class PeopleService { public Collection< Person > getPeople( int page, int pageSize ) { Collection< Person > persons = new ArrayList< Person >( pageSize ); for( int index = 0; index < pageSize; ++index ) { persons.add( new Person( String.format( "person+%d@at.com", ( pageSize * ( page - 1 ) + index + 1 ) ) ) ); } return persons; } public Person addPerson( String email ) { return new Person( email ); } }
As you can see, we will generate a list of persons on the fly depending on the page requested. Standard Spring annotation @Service marks this class as a service bean. Our JAX-RS service PeopleRestService will use it for retrieving persons as the following code demonstrates.
package com.example.rs; import java.util.Collection; import javax.inject.Inject; import javax.ws.rs.DefaultValue; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import com.example.model.Person; import com.example.services.PeopleService; @Path( "/people" ) public class PeopleRestService { @Inject private PeopleService peopleService; @Produces( { "application/json" } ) @GET public Collection< Person > getPeople( @QueryParam( "page") @DefaultValue( "1" ) final int page ) { return peopleService.getPeople( page, 5 ); } @Produces( { "application/json" } ) @PUT public Person addPerson( @FormParam( "email" ) final String email ) { return peopleService.addPerson( email ); } }
Though simple, this class needs more explanations. First of all, we want to expose our RESTful service to /people endpoint. Combining it with /api (where our JAX-RS application resides), it gives as the /api/people as qualified path.
Now, whenever someone issues HTTP GET to this path, the method getPeople should be invoked. This method accepts optional parameter page (with default value 1) and returns list of persons as JSON. In turn, if someone issues HTTP PUT to the same path, the method addPerson should be invoked (with required parameter email) and return new person as a JSON.
Now let's take a look on Spring configuration, the core of our application.
package com.example.config; import java.util.Arrays; import javax.ws.rs.ext.RuntimeDelegate; import org.apache.cxf.bus.spring.SpringBus; import org.apache.cxf.endpoint.Server; import org.apache.cxf.jaxrs.JAXRSServerFactoryBean; import org.codehaus.jackson.jaxrs.JacksonJsonProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.example.rs.JaxRsApiApplication; import com.example.rs.PeopleRestService; import com.example.services.PeopleService; @Configuration public class AppConfig { @Bean( destroyMethod = "shutdown" ) public SpringBus cxf() { return new SpringBus(); } @Bean public Server jaxRsServer() { JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint( jaxRsApiApplication(), JAXRSServerFactoryBean.class ); factory.setServiceBeans( Arrays.< Object >asList( peopleRestService() ) ); factory.setAddress( "/" + factory.getAddress() ); factory.setProviders( Arrays.< Object >asList( jsonProvider() ) ); return factory.create(); } @Bean public JaxRsApiApplication jaxRsApiApplication() { return new JaxRsApiApplication(); } @Bean public PeopleRestService peopleRestService() { return new PeopleRestService(); } @Bean public PeopleService peopleService() { return new PeopleService(); } @Bean public JacksonJsonProvider jsonProvider() { return new JacksonJsonProvider(); } }
It doesn't look complicated but a lot happens under the hood. Let's dissect it into the peices. The two key component here are the factory JAXRSServerFactoryBean which does all heavy lifting for configuring our instance of JAX-RS server, and SpringBus instance which seamlessly glues Spring and Apache CXF together. All other components represent regular Spring beans.
What's not on a picture yet is embedding Jetty web server instance. Our main application class Starter does exactly that.
package com.example; import org.apache.cxf.transport.servlet.CXFServlet; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import com.example.config.AppConfig; public class Starter { public static void main( final String[] args ) throws Exception { Server server = new Server( 8080 ); // Register and map the dispatcher servlet final ServletHolder servletHolder = new ServletHolder( new CXFServlet() ); final ServletContextHandler context = new ServletContextHandler(); context.setContextPath( "/" ); context.addServlet( servletHolder, "/rest/*" ); context.addEventListener( new ContextLoaderListener() ); context.setInitParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() ); context.setInitParameter( "contextConfigLocation", AppConfig.class.getName() ); server.setHandler( context ); server.start(); server.join(); } }
Looking through this code uncovers that we are running Jetty server instance on port 8080, we are configuring Apache CXF servlet to handle all request at /rest/* path (which together with our JAX-RS application and service gives us the /rest/api/people), we are adding Spring context listener parametrized with the configuration we have defined above and finally we are starting server up. What we have at this point is full-blown web server hosting our JAX-RS services. Let's see it in action. Firstly, let's package it as single, runnable and redistributable fat or one jar:
mvn clean package
Let's pick up the bits from the target folder and run them:
java -jar target/spring-one-jar-0.0.1-SNAPSHOT.one-jar.jar
And we should see the output like that:
2013-01-19 11:43:08.636:INFO:oejs.Server:jetty-8.1.8.v20121106 2013-01-19 11:43:08.698:INFO:/:Initializing Spring root WebApplicationContext Jan 19, 2013 11:43:08 AM org.springframework.web.context.ContextLoader initWebApplicationContext INFO: Root WebApplicationContext: initialization started Jan 19, 2013 11:43:08 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh INFO: Refreshing Root WebApplicationContext: startup date [Sat Jan 19 11:43:08 EST 2013]; root of context hierarchy Jan 19, 2013 11:43:08 AM org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider registerDefaultFilters INFO: JSR-330 'javax.inject.Named' annotation found and supported for component scanning Jan 19, 2013 11:43:08 AM org.springframework.web.context.support.AnnotationConfigWebApplicationContext loadBeanDefinitions INFO: Successfully resolved class for [com.example.config.AppConfig] Jan 19, 2013 11:43:09 AM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessorINFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring Jan 19, 2013 11:43:09 AM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1f8166e5: defining beans [org.springframework.context.annotation.internal ConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProces sor,org.springframework.context.annotation.internalCommonAnnotationProcessor,appConfig,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,c xf,jaxRsServer,jaxRsApiApplication,peopleRestService,peopleService,jsonProvider]; root of factory hierarchy Jan 19, 2013 11:43:10 AM org.apache.cxf.endpoint.ServerImpl initDestination INFO: Setting the server's publish address to be /api Jan 19, 2013 11:43:10 AM org.springframework.web.context.ContextLoader initWebApplicationContext INFO: Root WebApplicationContext: initialization completed in 2227 ms 2013-01-19 11:43:10.957:INFO:oejsh.ContextHandler:started o.e.j.s.ServletContextHandler{/,null} 2013-01-19 11:43:11.019:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080
Having our server up and running, let's issue some HTTP requests to it so to be sure everything works just as we expected:
> curl http://localhost:8080/rest/api/people?page=2 [ {"email":"person+6@at.com","firstName":null,"lastName":null}, {"email":"person+7@at.com","firstName":null,"lastName":null}, {"email":"person+8@at.com","firstName":null,"lastName":null}, {"email":"person+9@at.com","firstName":null,"lastName":null}, {"email":"person+10@at.com","firstName":null,"lastName":null} ] > curl http://localhost:8080/rest/api/people -X PUT -d "email=a@b.com" {"email":"a@b.com","firstName":null,"lastName":null}
Awesome! And please notice, we are completely XML-free! Source code: https://github.com/reta/spring-one-jar/tree/jetty-embedded
Before ending the post, I would like to mention one great project, Dropwizard, which uses quite similar concepts but pushes it to the level of excellent, well-designed framework, thanks to Yammer guys for that.
30 comments:
Very nice article
Congratulations
Thank you! I am very glad to read such a comments :)
to be completely xml free you need to get rid of maven ;-)
good article, thanks
Thanks a lot, but ... Maven never dies! :-D
Great stuff -
With NIO being in the limelight I've seen a lot of cool implementations in java ... Java's orthogonal architecture really shines at 'snapping-in' new features ...
One rant --- Not sure if you need the explicit call to cglib in your pom anymore ...
" As of Spring Framework 3.2, we now repackage and inline the newly-released CGLIB 3.0."
And I too love (and hate) Maven ;-)
Thanks a lot, Edward! It's good to know that Spring 3.2 doesn't require explicit cglib dependency anymore.
Thank you!
Best Regards,
Andriy Redko
Hello, I tried to run your example code, I the output is "No service was found". In the warning logs, we have outputted WARNING: Can't find the the request for http://localhost:8080/rest/api/people's Observer. Any suggestions?
Hi Amit!
Could you please post the application startup log from the console/file? I did clean build/run just to be sure and I do not see the problem happening. Anyway, I would be happy to help you troubleshooting the issue.
Thank you!
Best Regards,
Andriy Redko
Why is the pom.xml lowercased? Such values as groupId are written groupid. This is making it hard to follow along.
@Amit, you need to use cxf-rt-frontend-jaxrs version 2.7.4.
4@Amit, you need to use cxf-rt-frontend-jaxrs version 2.7.4.
@Amit, try using cxf-rt-frontend-jaxrs version 2.7.4
@Amit, try using cxf-rt-frontend-jaxrs version 2.7.4
@Amit, try using cxf-rt-frontend-jaxrs version 2.7.4
Hi #mobiusinversion,
I apologize for lowercased POM elements (I missed that), the checkstyle JavaScript library is one to blame for that. Please pull all the sources from GitHub, everything is well-formatted there.
Thank you.
Best Regards,
Andriy Redko
How would you go about switching the AppConfig's PeopleService bean to be injected by Spring, instead of using "new PeopleService();"?
Hi Jim,
Thank you for the comment. That's a very valid approach as well. Somethings like this should work:
@Named
public class PeopleRestService {
// ...
}
@ComponentScan( basePackageClasses = PeopleService.class )
class AppConfig {
@Inject private PeopleService peopleRestService;
// ...
}
Thank you.
Best Regards,
Andriy Redko
Thanks so much, I'm not as familiar with non-xml config setup. I enjoyed the article!
This is by far the best post on this subject. I have been reading up on trends in Spring and clever ways of packaging, but this works, and is no nonsense 0 xml. Wonderful.
Hi Andriy, Having a small issue getting the example running. Can you please take a look at this SO ticket?
http://stackoverflow.com/questions/18341072/embedded-jetty-with-spring-example-failing
Hi David,
I've looked at the stackoverflow and was not able to reproduce the issue just by duplicating AppConfig changes and adding HelloWorldService. Dependencies could be the cause as well. Could you please post the github link so I can clone the project and reproduce the issue?
Thank you.
Best Regards,
Andriy Redko
Hi Andriy,
Do you think its possible to have jsp routes here as well? so we can have and apache cxf rest service a spring web mvc app living in the same uberjar??? :)))
Hi,
With respect to JSPs, I am not able to say for sure, but I think it won't be as trivial as JAX-RS. JSPs involve parsing, compilation and whatnot. Personally, I haven't done it but it may be possible. My apologies for such a vague answer.
Thank you.
Best Regards,
Andriy Redko
@Amit -- I had the same issue when trying with CXF 2.7.7 (and Jackson 1.9.13). I resolve the issue by using:
factory.setAddress("" + factory.getAddress());
instead of:
factory.setAddress("/" + factory.getAddress());
The clue was the following log message:
"org.apache.cxf.endpoint.ServerImpl - Setting the server's publish address to be //api"
Notice the two slashes... when going back to CXF 2.7.2 (or even 2.7.4), there is only one slash (i.e /api).
You can also add in more Maven dependencies to get rid of messages like:
"o.a.c.j.provider.ProviderFactory - Problem with creating the default provider org.apache.cxf.jaxrs.provider.json.JSONProvider: org.apache.cxf.jaxrs.provider.json.JSONProvider"
and:
"o.a.c.j.provider.ProviderFactory - Problem with creating the default provider org.apache.cxf.jaxrs.provider.json.JSONProvider: org/codehaus/jettison/mapped/TypeConverter "
Hey Frederic,
Thank you very much for your discovery. Yes, for latest Apache CXF the
factory.setAddress(factory.getAddress());
is the way to go.
Thanks!
Best Regards,
Andriy Redko
You made my day, thanks a lot :)
BTW, having
factory.setAddress(factory.getAddress());
it's the same as removing the line completely...
If you have time for a question, for what I tested it's not possible to have more than one Application running at the same time? I'm migrating a Jersey POC to CXF and in Jersey I did have 2 running Applications.
Hi Antonio,
Thanks a lot for your comments. To your question, it's quite easy to create as many JAX-RS 2.0 applications as you want: just clone JaxRsApiApplication, change @ApplicationPath() and clone jaxRsServer() and jaxRsApiApplication() beans from AppConfig. Example:
@ApplicationPath( "api2" )
public class JaxRsApiApplication2 extends Application {
}
@Bean @DependsOn( "cxf" )
public Server jaxRsServer2() {
JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint( jaxRsApiApplication2(), JAXRSServerFactoryBean.class );
// your service beans, etc.
return factory.create();
}
@Bean
public JaxRsApiApplication2 jaxRsApiApplication2() {
return new JaxRsApiApplication2();
}
Your new application's JAX-RS services will be accessible under: http://[host]:[port]/rest/api2/ base URL.
Please let me know if you have more questions.
Thanks.
Best Regards,
Andriy Redko
Post a Comment