This page explains how to create web applications using modern Java technologies (as of late 2004). Before we start:
If you like OO development, you should be able to create a web app without polluting it with cheesy, fragile scripts, which is what a JSP is. Secondly, the web app we put together uses a single database. It isn't a financial banking application. It doesn't need distributed transactions. And since nobody in their right minds needs declarative security, we can rule out the use of EJB as being a timesaver in this app (not that it ever is).
Moving right along, the first thing you need to do is pick a web framework. Echo, Tapestry, and Wicket are the best. They are the best because they don't use cheesy scripts. They use Objects. And you don't have to waste one second of your time generating HTML or rolling loops to create tables. They are "modern" (meaning they use technology like MVC that has been around for 20 years, unlike JSP). And don't even tell me a Struts framework uses MVC.
OK, so we'll use Wicket because we like to be on the bleeding edge. It's in Alpha, its buggy. Yeah yeah yeah - I know. I love it because I can write an insanely good webapp in like 20 lines of code. If you don't have the cajones for wicket use Echo or Tapestry. Echo is good for creating an "application" that runs within the web browser, meaning it's not good for creating a "website" based on web pages. It's closer to Swing in a browser. You can't use the back button. Wicket it closer to Tapestry; a good fit for making web pages.
Now that we've picked a web framework, we need to pick a persistence framework. Wait, why do we need a persistence framework? Just use JDBC, right? Yep, if your webapp has a handful of tables, you could just use JDBC. If you've written a moderate to large webapp, you know that most of your bugs are in all that JDBC code that's hacked into all your JSP pages. But wait you say, we know better than that, so we created our own DAO layer. Hey, this is 2004. There is no reason to roll your own persistence framework. Admit it, your persistence framework sucks and that's where the other half of your bugs are.
OK, moving right along. Already ruled out Entity Beans. There are about two dozen persistence frameworks out there with varying scopes. Some help you write JDBC and don't try to do transparent object persistence. IBatis is well known. Hibernate and Cayane are well know and closer to transparent object persistence. Then there are others that implement the Java Data Objects standard for transparent object persistence.
One thing I have learned in the past 5 years of Java is that standards are great, but the stuff coming out of OpenSource is often just better and WAY more usable. One big exception to that rule has been JDO. Java Data Objects (JDO) was JSR 12. We're now at JSR 300 or something like that. Being one of the first JSR's (12) the Java community realized, out of the gates, that transparent object persistent was widely used and needed to be standardized. Let's think on that for a minute. Let's contrast that with EJB, a spec that really, really sucks. Frameworks like Toplink and Cocobase had been allowing object/relational mapping for YEARS at the time that JSR 12 was conceived. ODMG had been around for YEARS. So JDO was not inventing new stuff.
JDO took years to get out. And in the mean time EJB was thrashing through 3 major rewrites and making an ass of itself. BTW, EJB 3.0 is a total rewrite of EJB...again. Anyway, the JDO folks bided their time and got it right, for the most part in JDO 1.0. There were complaints that JDO's "bytecode enhancement" was unnecessary, so this requirement has been dropped in JDO 2.0. This seems to have made everyone happy.
So for this app we'll use JDO, because it has great API's and free drivers. No lock-in. Also, those implementations that use the bytecode enhancement feature of JDO have a theoretical advantage over those that use reflection. As with all software, a theoretical advantage doesn't always translate into a runtime advantage, but my experience is that JDO drivers are, in fact faster than Hibernate and other reflective drivers. Hmmm, maybe I'll do a performance comparison... Anyway, what is certain is that they are all fast enough for this web app.
On to the webapp! You can download this particular webapp from http://www.jdomax.com.
This webapp implements an order processing system. This webapp let's you retrieve a Customer or create a Customer, and attach Orders with Line Items to the Customer, and edit the Customer contact info. Very simple example.
We start with a domain model. That's right, tough guy, we're NOT designing tables first. Why, go to line 1 and recall that this is a "modern" web app. We are actually going to use Objects. You've heard of those right. The first objects we design are the "domain model" objects. In other words, the objects that represent the persistent "things" of our domain. It should already be obvious.
Let's say all the domain model classes for this order processing system go into package "examples.order".Next major shock for you procedural rockers is that Doken, Styks, and Rush have not been cool for 15 years.(actually Rush is still pretty cool). Another shock is that we are NOT going to define the "N-ary" relationships between objects. This is where I slap you across the face (hard). We are going to let the Objects themselves define the relationships.
Customer has a Collection of Order Objects. Order has a Collection of Line Item Objects. Customer has an Address. Customer has a ContactInfo?. It's cake.
Now that we have a domain model, we need to think about how we query the datastore to retrieve domain model objects and how we persist domain model objects to the database. BTW, from here on out, let's call domain model objects "PersistenceCapable?" which is the JDO term. We most definitely do NOT want to put a single scrap of persistence code into our domain model, so let's create another package for the web code.
examples.order.wicket is the package that has the classes that control the web pages, and query and persist PersistenceCapable? objects. Coolest thing about Wicket: if you are lazy, rejoice. Follow a simple file naming pattern and you don't have to write a single config file. If you are used to JSP/Struts you probably don't believe me. No, seriously, zero configuration. Just name the classfile that controls the webpage the same name as the web page. Home.html corresponds to Home.class. SO let's look in Home.java and see what's there.
Used to writing shitloads of persistence code? smoke this! It's absurdly simple, and especially so when compared to the farcical inadequacy of Entity Beans. Here's a snippet.
props.put("javax.jdo.option.ConnectionURL", "jdbc:hsqldb:" + getHsqldbURL()); props.put("javax.jdo.option.ConnectionDriverName?", "org.hsqldb.jdbcDriver"); props.put("javax.jdo.option.RetainValues?", "true"); props.put("javax.jdo.option.NontransactionalRead?", "true"); props.put("Max.ORMapping", "examples-mapping.xml"); PersistenceManager? pm = JDOHelper.getPersistenceManagerFactory(props).getPersistenceManager(); System.out.println("PersistenceManager? initialized in (ms):" + (System.currentTimeMillis()-start)); session.setProperty("pm", pm);//put the pm in the user's sessionAll we did was set some properties, create the PersistenceManager? (pm) and toss the pm into the user's session. Now the code for any page can grab the pm from the session and use the pm to access the datastore. Next we need to create a Query. Here we are creating a Query to search for Customer based on lastName. So we create a reusable parameterized query, and again, store it in the user's session.
Query query = pm.newQuery(Customer.class,"lastName.startsWith(lname)"); // <-- this is JDOQL. Looks just like java query.declareImports("import examples.order.*"); //import domain model classes into query namespace //notice that 'lname' is referenced in the JDOQL above. //this means you can pass in the 'lname' parameter at runtime for dynamic query query.declareParameters("String lname"); //create a parameter for the compiled query. start = System.currentTimeMillis(); query.compile(); System.out.println("Query compiled in (ms):" + (System.currentTimeMillis()-start)); session.setProperty("query", query); //put the query in the user's sessionOK, now let's look at the code on the Search.java (corresponds to Search.html) to use the query to find a Customer. Wicket is Object Oriented so all you have to do to handle the user hitting the "search" button on the HTML page is extend Form, which Search.java does with an inner class:
public final class SearchForm extends Form { public SearchForm(final String componentName) { // Construct form with no validation listener super(componentName, null); // Add text entry widget add(new TextField("firstNameParameter", customerQuery)); } public final void handleSubmit(final RequestCycle? cycle) { customerQuery.execute((javax.jdo.Query)getPage().getSession().getProperty("query")); table.invalidateModel(); } }There is a thin wrapper (customerQuery) that we use to execute the query that we stored in the session. Wicket wants to call setFirstNameParameter on a model object that backs the TextField where the user enters the Customer name to search for. Actually I just realized it should be called setLastNameParameter since the query searches on last name, but whatever. Just picked a bad name for the text field. Anyhow, the customerQuery wrapper has a setFirstName method, which automatically get's called by wicket when the user hits the "search" button. Then note in the handleSubmit we just execute the query wrapper. Then we invalidate the table backing the results so that it will refresh. Like Ice-cube says, "nothin' to it but to do it". The customerQuery is wired up as the model to the Table, so that as soon as you call invalidateModel, the Table will ask customerQuery for the results of the query. Standard MVC stuff.
Now let's look at a page for creating and modifying the contact info.
public EditForm?(final String componentName, Customer customer) { super(componentName, null); this.customer = customer; PersistenceManager? pm = (PersistenceManager?) EditContactInfo?.this.getPage().getSession().getProperty("pm"); Transaction tx = pm.currentTransaction(); if (!tx.isActive()){ //for edit operations, must make sure the optimistic TX begins before user can enter edits tx.setOptimistic(true); tx.begin(); } //if contactInfo not yet created, create new and make persistent //reference from customer to contactInfo contactInfo = customer.getContactInfo(); if (null == contactInfo){ contactInfo = new ContactInfo?(); pm.makePersistent(contactInfo); pm.makeTransactional(customer); customer.setContactInfo(contactInfo); } pm.makeTransactional(contactInfo); // Add text entry widget add(new TextField("phone",contactInfo )); add(new TextField("email",contactInfo )); } public final void handleSubmit(final RequestCycle? cycle) { PersistenceManager? pm = (PersistenceManager?) getPage().getSession().getProperty("pm"); Transaction tx = pm.currentTransaction(); try{ tx.commit(); }finally{ tx.setOptimistic(true); tx.begin(); //an optimistic transaction is always open so any changes entered by user are recorded pm.makeTransactional(contactInfo); //want contactInfo to continue to record changes entered by user } }JDO offers "optimistic" transactions and "datastore" transactions. "datastore" tx means that during the entire transaction in your code, the datastore is keeping a transaction open. This can be expensive. In a web app, you need to detect changes made to the model backing the form, but the user might keep the page open all day without entering squat. You don't want each page to keep a datastore tx open the whole time. Therefore optimistic tx's are the best choice.
Optimistic TX just open and close datastore tx's to mark changes to the object. This can be accomplished by writing a timestamp or version number. So unless the user actually enters something, the database doesn't get hit. In the code above, you see that pm.makePersistent is used to "insert" the object into the database. But how do we point the foreign key from the Customer to his new ContactInfo? that we created. This where I slap you again (hard).
In JDO you don't worry about that bullshit, the driver does it for you. All you have to do is make the Java assignment of contactInfo. Since we are using an optimistic transaction you do have to call makeTransactional on Customer, which basically pulls the customer into the transaction so the driver tracks the assignment of contactInfo.
pm.makeTransactional(customer); customer.setContactInfo(contactInfo);When you call pm.commit the driver detects that an assignment has been made and takes care of whatever foreign key stuff is needed. In the finally block we reopen the optimistic transaction and makeTransactional(contactInfo) so that if the user subsequently edits the ContactInfo? the edits will get picked up and applied.
Well, I think that's good enough for now. Really you should download the complete example from http://www.jdomax.com. You can build it with a single Ant command and deploy it Tomcat or Jetty. It is bundled with HSQLDB so there is really no reason to not have this webapp up and running in any more than 5 minutes.
-- geoff
Is this an ad? A tutorial? A rant? I can't tell. Is it useful? It's an OrphanPage ...