We wanted to implement a simple Java (JAX-WS) webservice on Google App Engine, but unfortunately this is not fully supported. All the javax.xml.* classes are available on Google App Engine, but not the com.sun.xml.ws.* classes that are normally used to implement a JAX-WS service. But with a little bit of custom code we can get a JAX-WS service to run on Google App Engine as can be seen in the SoapUI screenshot. You can download the public WSDL at https://redheap-jaxws.appspot.com/HelloWorldService.wsdl
SoapUI test invoking HelloWorld service on Google App Engine |
You can use the normal JAX-WS annotations in your service class and use JAXB to marshal and unmarshal the request and responses. This is all very similar to a JAX-WS service on a JEE container. The thing you need extra is a custom HttpServlet that handles the HTTP POST, unmarshals the request payload to a java object using JAXB, invoke the actual web service class, marshal the web service class response back to XML and send it to the client.
This post describes all the steps to create a project using maven and complete it in JDeveloper 12c as well as deploying it to a local and remote server. If you prefer other build tools, like Apache ANT, or other Java IDE's you can still use similar steps but you might need to adjust them for your environment. The key part is the custom servlet which is the same for each setup.
Navigate to the directory where you want to create a subdirectory for a project. No need to create the project directory yourself, just navigate to its parent. Then run
mvn archetype:generate
. Search for com.google.appengine.archetypes:skeleton-archetype
and select the entry for com.google.appengine.archetypes:appengine-skeleton-archetype
. Finish the wizard by specifying a unique groupId and artifactId. The rest can be left at their defaults:C:\work\redheap\jdev12120>mvn archetype:generate [INFO] Scanning for projects... [INFO] [INFO] Using the builder org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder with a thread count of 1 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building Maven Stub Project (No POM) 1 [INFO] ------------------------------------------------------------------------ [INFO] [INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom >>> [INFO] [INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom <<< [INFO] [INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom --- [INFO] Generating project in Interactive mode [INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0) Choose archetype: ..... [[snip long list]] ..... Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 361: com.google.appengine.archetypes:skeleton-archetype Choose archetype: 1: remote -> com.google.appengine.archetypes:appengine-skeleton-archetype (-) 2: remote -> com.google.appengine.archetypes:skeleton-archetype (-) Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 1 Define value for property 'groupId': : com.redheap.samples Define value for property 'artifactId': : AppEngineJaxWs Define value for property 'version': 1.0-SNAPSHOT: : Define value for property 'package': com.redheap.samples: : Confirm properties configuration: groupId: com.redheap.samples artifactId: AppEngineJaxWs version: 1.0-SNAPSHOT package: com.redheap.samples Y: : Y
In JDeveloper 12c go to Tools > Preferences and change the location of maven to use a recent version you downloaded yourself. The maven version included with JDeveloper 12.1.2.0.0 is too old for Google App Engine as that requires maven to be at least version 3.1:
Use at least version 3.1 of maven in JDeveloper 12c |
Next steps are for Oracle JDeveloper, but you can take similar steps from the command line or your favorite Java IDE. Create a new JDeveloper workspace from the maven pom files. JDeveloper will warn that the project already exists and it will be overwritten. Just click Yes even though this is a new project.
Start maven import wizard from New Gallery |
Import all pom's |
Point to parent directory of project as subdir is implied |
appengine.target.version
property to the latest App Engine version. Currently this is 1.9.2 but you might want to check the latest Java SDK version at https://developers.google.com/appengine/downloads.Open the application level maven pom |
Update appengine.target.version |
AppEngineJaxWs
in this example. The project ID has to be globally unique across all App Engine customers and will become the first part of your URL. For example, typing redheap-jaxws
will create http://redheap-jaxws.appspot.com
Open the project properties for the AppEngineJaxWs-ear project and add the
src/main/application
directory to the Java Source Paths.Add src/main/application as source directory |
Then open the
application/META-INF/appengine-application.xml
file and update the application name with the globally unique project ID (not the name) that was created in the Google developer console:Update application name with App Engine Project ID |
Next, create a Java class Person in the war project that we will be using as an argument to our webservice, just to demonstrate how to handle complex payloads:
package com.redheap.appengine; public class Person { private String firstName; private String lastName; public void setFirstName(String firstName) { this.firstName = firstName; } public String getFirstName() { return firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getLastName() { return lastName; } }
Next create the HelloWorld class that we will be exposing as a JAX-WS service:
package com.redheap.appengine; public class HelloWorld { public String sayHello(Person person) { return "Hello " + person.getFirstName() + " " + person.getLastName(); } }
Right click the HelloWorld class in the application navigator and create a web service out of it. When running this for the first time it will ask if you want a WebLogic specific web service or JAX-WS Reference Implementation version. Be sure to check the RI one as we want to deploy to a non-weblogic environment. Other than that, accept all default values. If you are not using JDeveloper, you can also use the command line wsgen tool that ships with the JDK.
This will have added a number of annotations to the HelloWorld class. Change the @WebParam annotation for more meaningful argument name than
arg0
as this name will end up in the XSD for the web service.@WebService public class HelloWorld { @WebMethod public String sayHello(@WebParam(name = "person") Person person) { return "Hello " + person.getFirstName() + " " + person.getLastName(); } }
Now right click HelloWorld again and generate a WSDL file.
Right click the generated wsdl file and use the Move option from the Refactor menu to relocate it to webapp directory itself (two levels up) so the file is no longer in the WEB-INF folder and accessible through the web. Repeat the same steps for the xsd file so it will also be available on the web.
Open the generated WSDL file and update the
soap:address
at the end of the file to the URL at your appengine instance. For example, mine is https://redheap-jaxws.appspot.com/HelloWorldService
Open the generated XSD file and add
elementFormDefault="qualified"
to the root element to ensure we will be using namespaces in the web service requests and responses.Use namespace qualified elements |
Google App Engine doesn't include the com.sun.xml.ws.* classes that are normally used by a JAX-WS service. So we need to create our own servlet that handles the requests and invokes the HelloWorld class. This is where the magic happens. Most of the code in this class is generic for any webservice. We implemented everything in a single class for this example, but in a real application you want to refactor the generic code to a super class that can be reused for each web service.
public class HelloWorldServlet extends HttpServlet { public static final String NS = "http://appengine.redheap.com/"; public static final QName QNAME_SAY_HELLO = new QName(NS, "sayHello"); public static final QName QNAME_OTHER = new QName(NS, "otherOperation"); private final HelloWorld serviceImpl = new HelloWorld(); private static final MessageFactory messageFactory; @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { MimeHeaders headers = getHeaders(request); InputStream in = request.getInputStream(); SOAPMessage soapReq = messageFactory.createMessage(headers, in); SOAPMessage soapResp = handleSOAPRequest(soapReq); response.setStatus(HttpServletResponse.SC_OK); response.setContentType("text/xml;charset=\"UTF-8\""); OutputStream out = response.getOutputStream(); soapResp.writeTo(out); out.flush(); } catch (SOAPException e) { throw new IOException("exception while creating SOAP message", e); } } public MimeHeaders getHeaders(HttpServletRequest request) { MimeHeaders retval = new MimeHeaders(); EnumerationheaderNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = request.getHeader(name); StringTokenizer values = new StringTokenizer(value, ","); while (values.hasMoreTokens()) { retval.addHeader(name, values.nextToken().trim()); } } return retval; } protected SOAPMessage handleSOAPRequest(SOAPMessage request) throws SOAPException { Iterator iter = request.getSOAPBody().getChildElements(); Object respPojo = null; while (iter.hasNext()) { // find first Element child Object child = iter.next(); if (child instanceof SOAPElement) { respPojo = handleSOAPRequestElement((SOAPElement) child); break; } } SOAPMessage soapResp = messageFactory.createMessage(); SOAPBody respBody = soapResp.getSOAPBody(); if (respPojo != null) { JAXB.marshal(respPojo, new SAAJResult(respBody)); } else { SOAPFault fault = respBody.addFault(); fault.setFaultString("Unknown SOAP request"); } return soapResp; } protected Object handleSOAPRequestElement(SOAPElement reqElem) { QName reqName = reqElem.getElementQName(); if (QNAME_SAY_HELLO.equals(reqName)) { return handleSayHello(JAXB.unmarshal(new DOMSource(reqElem), SayHello.class)); } else if (QNAME_OTHER.equals(reqName)) { // } return null; } protected SayHelloResponse handleSayHello(SayHello request) { SayHelloResponse response = new SayHelloResponse(); response.setReturn(serviceImpl.sayHello(request.getPerson())); return response; } static { try { messageFactory = MessageFactory.newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } }
Remove all entries from web.xml that were created by the web service wizard in JDeveloper and register our own servlet. The
url-pattern
has to match the endpoint you configured in the soap:address
in the WSDL file.<web-app version="2.5" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <servlet> <servlet-name>HelloWorldServlet</servlet-name> <servlet-class>com.redheap.appengine.HelloWorldServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/HelloWorldService</url-pattern> </servlet-mapping> </web-app>
Finally all we need to do is add some maven goals to JDeveloper so we can select them from the build menu. To do this, go to the Tools menu and select Preferences. Then go to the Maven > Phases/Goals and add a goal to the list of Selected Phases/Goals:
Add a goal to Maven preferences |
Add two goals:
com.google.appengine:appengine-maven-plugin:devserver
and com.google.appengine:appengine-maven-plugin:update
. The first one is to start a local AppEngine server for testing and the second one is to deploy your application to the real Google App Engine.Add Maven goals |
If everything is well, you can now run your application:
- Right-click the
pom.xml
in the Application Resources > Build Files and run maven phaseinstall
from theRun Maven Goal Profile "Default"
popup menu. This should compile and build your project - Right-click the
pom.xml
in the AppEngineJaxWs-ear project and run theappengine:devserver
phase from the same popup menu. This should launch the local AppEngine server and you should be able to download the WSDL at http://localhost:8080/HelloWorldService.wsdl and invoke the local webservice at http://localhost:8080/HelloWorldService - Once this is fine, you can run the
appengine:update
phase from the AppEngineJaxWs-ear project. This installs the project at your Google AppEngine server. Be sure to run this from the command line the first time and not from JDeveloper. The reason is it will launch a web browser to authenticate yourself and you have to enter a code from the web browser into the maven build. This cannot be done when building from JDeveloper as it doesn't allow you to enter information.
As always you can download the full sample application or browse the subversion repository to look at the source code. If you want to have a quick look at the solution start with the com.redheap.appengine.HelloWorldServlet servlet that does most of the important work.
Excelent post Wilfred! thanks for sharing your knowledge!
ReplyDelete