I was interviewed for a project two weeks ago and got accepted soon after. I will be working in Rotterdam to build a new website. The new site will replace the old site, which is not even live yet. This will be my first (or actually second, but first big) project outside the company so although there are many bad smells, I want to see it happen with my own eyes. Bad smells are triggers which tell you the project is crap and will stay crap. What were the smells I smelled:
- The site which will be replaced is not in operation at this moment
- The project will probably use old technologies with some buzzwords sprinkled in:
- old tech:
- Struts as web framework instead of JSF or even Stripes, or Struts 2.0
- iBatis as O/R mapper, instead of Hibernate or JPA
- new tech (= buzzwords):
- Web Services, but not only to communicate to the outside world, but for use between the front and the back-end inside the same VM!
- EJB 3, what is the use of EJB 3 if you are using iBatis instead of JPA or Hibernate?
Anyway, EJB 3 might be the only good thing about the whole project. I know EJB 3 quite well at the moment. We will be developing in Oracle JDeveloper, for the Oracle application server and using the Oracle Portal framework. I never heard of Portal before, but they told me that is the reason they will be using Struts. I still have to get a good answer why JPA or Hibernate would be worse than iBatis. And the Web Service idea is completely back wards.
Web Services are intended to expose a certain API to the outside world in a language independent manner so others can use your service. I underlined several words in that last sentence because they highlight the problem I have with using a Web Service inside your own application stack, where you export and consume your own web service at the same time.
Certain API. You expose a certain high level, coarse (not fine grained) API, which is designed to be as easy to use as possible and have the minimum amount of operations possible. This API is hard to change when others start using the web service you are exporting, because they are dependent on it.
Outside world. Your web service is to be used by the outside world. Not by yourself. See the previous argument.
Language independent. Web services can be used and exported in any programming language, programming system and operating system. This is possible because they use XML to define all communications. This means that every time you call a web service, or your webservice is called, you have to generate and decode XML messages. These messages will be send over http connections. All these things cause overhead, which will not be a problem when external parties want to communicate with you, because there is no other easier way, but why would you want to incur all this overhead and other problems inside your own system?
For my Java Business Components certification I learned about web services, but I never wrote one. Some projects I worked on used web services, but I only altered some small things, I never wrote one from scratch. So for this new project I wanted to test myself, can I write a web service?
The Library Web Service
In the last blog post I wrote about a small project setup based on three layers. The back end, the front end, and the interface for the back end, used by the front end. For this web service, I would keep the back end and the interface the same, but I would change the front end. It would not be a website, but web service, which will expose the back end interface.
At first I failed. I tried the standard JBoss 4.2.2, but I could not get it to work. I generated all the proxy and skeleton classes but the web service explorer could not connect to it. And clients I created would fail to connect as well. I tried using Glassfish, the open source application server from Sun. But I had to rewrite all my build scripts. And I could not get my ear file through the verifier, which verifiers the correctness of my ear file. At last I installed another webservice stack for JBoss, the Sun Metro implementation and got a lot further, but I had to generate the proxy classes myself for this stack. I can do that, but the native stack from JBoss can do that during deploy time, so I installed that stack and now my web service finally works correctly.
How did I do it?
I started a new Dynamic Web Project inside Eclipse. In that project I created a new class, called com.enigmatry.library.ws.Library which has a method for each of my library service methods. I only have to annotate this class with the @WebService annotation to make it a web service. I changed the web.xml file so it maps everything to my webservice. Now it looks like this:
1: <?xml version="1.0" encoding="UTF-8"?>
2: <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3: xmlns="http://java.sun.com/xml/ns/javaee"
4: xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
5: xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
6: id="WebApp_ID" version="2.5">
7: <display-name>EnigLibWS</display-name>
8: <servlet>
9: <servlet-name>LibraryWebService</servlet-name>
10: <servlet-class>com.enigmatry.library.ws.Library</servlet-class>
11: <load-on-startup>1</load-on-startup>
12: </servlet>
13: <servlet-mapping>
14: <servlet-name>LibraryWebService</servlet-name>
15: <url-pattern>/*</url-pattern>
16: </servlet-mapping>
17: <session-config>
18: <session-timeout>30</session-timeout>
19: </session-config>
20: </web-app>
The biggest problem I have (or had) is to reference EJB3 Session Beans from the web layer. I have not found a way to let me locate the EJB3 bean from the InitailContext without specifying an explicit JNDI name with the @org.jboss.annotation.ejb.LocalBinding annotation, which is JBoss specific, so prohibits me from writing a portable ear. In the end I used a JBoss specific deployment descriptor, so I do not have to use the annotation.
Inside the META-INF dir inside the ejb3 jar I added jboss.xml:
1: <jboss>
2: <enterprise-beans>
3: <session>
4: <ejb-name>library</ejb-name>
5: <jndi-name>library</jndi-name>
6: </session>
7: </enterprise-beans>
8: </jboss>
Now I can get a reference to the library bean by writing:
1: Library library = (Library) new InitialContext().lookup("library");
JBoss 5.0 will support the @EJB annotation in the web layer to inject EJB3 Session beans. Other application servers already support this, but JBoss is so much easier to work with. Deploying is just a copy operation instead of calling complex ant tasks with difficult class path definitions.
When I build this war file and use it in my ear file instead (or next to my website war) I can access the webservice definition (the wsdl) by going to this url: http://localhost/war-name/?wsdl. This wsdl is generated at deploy time by the JBoss native web service stack. With other stacks you have to use tools to generate this file. This file can now be used to build client applications which can talk with the created web service.
The Client
I wrote a simple proof of concept client, just to see it working outside the web service explorer. I created the required proxy files from the wsdl by using the wsimport tool from JBoss inside my build script, this is my ant target:
1: <target name="create-proxy" description="Create local port proxy">
2: <taskdef name="wsimport" classname="com.sun.tools.ws.ant.WsImport2">
3: <classpath>
4: <path refid="cp"/>
5: </classpath>
6: </taskdef>
7:
8: <mkdir dir="${dist.dir}" />
9: <mkdir dir="${build.dir}" />
10: <wsimport
11: wsdl="${conf.dir}/library.wsdl"
12: destdir="${build.dir}"
13: sourcedestdir="${src.dir}"
14: keep="true"
15: extension="false"
16: verbose="true"
17: wsdlLocation="http://localhost:8080/EnigLibWS/library?wsdl"
18: package="com.enigmatry.library.ws">
19: </wsimport>
20: </target>
This creates several files which can then be used to talk with the web service. The only code I had to write is this:
1: public static void main(String[] args) {
2: try {
3: LibraryService service = new LibraryService();
4: System.out.println("Retrieving the port from the following service: " + service);
5: Library library = service.getLibraryPort();
6: System.out.println("Invoking the getVersion operation on the port.");
7: String response = library.getVersion();
8: System.out.println(response);
9: BookArray books = library.searchBook(new SearchBookVO());
10: for(Book book : books.getItem()){
11: System.out.println(book);
12: }
13: } catch(Exception e) {
14: e.printStackTrace();
15: }
16: }
As you can see, I create a new LibraryService object (line 3) and get the port from this service to get a proxy object (line 5) which I can use to call the web service. All these objects are generated by the wsimport tool. Then I can just call the web service as it is my normal interface (line 7 and 9).
There are some things different though. I could not use a Set<Book> as a return type in my web service, so I used a Book[], but as you can see in the client, the method will return a BookArray type (line 9). Which returns a List<Book> if you call getItem() on it (line 10).
It is probably possible to change the generated files so it returns a Book[] directly, but that will involve a lot of hand coding.