Extensionless URLs with Java Web Frameworks
Last week, I had a go of making a Spring MVC application use extensionless URLs. I did some googling, found some tips on the Spring Forums and believe I arrived at a solid solution. Using the UrlRewriteFilter (version 3), I was able to create a rule that looks for any URLs without an extension. If it finds one, it appends the extension and forwards to the controllers. This rule is as follows (where *.html is my servlet-mapping for DispatcherServlet in web.xml):
<rule> <from>^([^?]*)/([^?/\.]+)(\?.*)?$</from> <to last="true">$1/$2.html$3</to> </rule>
As long as I hand-write all my URLs without an extension (<a href="home"> vs. <a href="home.html">), this seems to work. To combat developers that use "home.html", one solution is to require all links to be wrapped with <c:url value="url"/> (or some other macro that call response.encodeURL()). If you can convince everyone to do this, you can write an outbound-rule that strips the .html extension from URLs.
<outbound-rule> <from>^(.*)\.html(\?.*)?$</from> <to last="false">$1$2</to> </outbound-rule>
In an ideal world, it'd be possible to modify the <a> tag at the very core of the view framework you're using to automatically encode the URL of any "href" attributes. I don't think this is possible with JSP, FreeMarker, Facelets or any other Java Web Framework templates (i.e. Tapestry or Wicket). If it is, please let me know.
Below is my final urlrewrite.xml with these rules, as well as my "welcome-file" rule at the top.
<?xml version="1.0" encoding="utf-8"?> <!DOCENGINE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.0//EN" "http://tuckey.org/res/dtds/urlrewrite3.0.dtd"> <urlrewrite> <rule> <from>/$</from> <to type="forward">home</to> </rule> <rule> <from>^([^?]*)/([^?/\.]+)(\?.*)?$</from> <to last="true">$1/$2.html$3</to> </rule> <outbound-rule> <from>^(.*)\.html(\?.*)?$</from> <to last="false">$1$2</to> </outbound-rule> </urlrewrite>
If you have other solutions for extensionless URLs with Java web frameworks, I'd love to hear about them. With any luck, 2008 will be the year we drop extensions (and path-mappings) from our URLs. The stat packages might not like it, but I do.
I created a Rails-like (and therefore Grails-like) extension to Spring MVC that I called Agile MVC. I haven't promoted it as an Open Source project, but I have posted the source to Google Code (since I have a client that is using it.)
(I mentioned this to you in a bar once...)
It uses a Spring MVC HandlerMapping to do use Rails-style "routes" to map from a path to a controller. It can even parse parameters out of URL components as part of the mapping process. Grails does many of the same things, but the idea here was to create something that could be used in regular Java.
Configuration is a little tedious as it uses simple Java beans wired up with basic Spring Bean XML. A Spring XML vocabulary or a Groovy Builder might be nicer, but what is there will work. There is a small amount of (un-rendered) DocBook documentation, but Google code doesn't let you post static HTML and I haven't learned how to use any of the Maven 2 DocBook plugins.
I'd be willing to contribute this to AppFuse if you were interested. I might even be able to be cajoled into working on it and/or documenting it a little more. I'd love any feedback from you.
Posted by Sean Gilligan on May 14, 2008 at 05:04 AM MDT #
Thanks Matt, I've been wanting to do something like this for a while. I also frequently struggle with the "best" way to manage URLs and the servlet context part of the path when I want my urls to hang off the root (e.g. http://www.foo.com/someform.html as opposed to http://www.foo.com/app/someform.html). Right now I'm cheating by making my webapp ROOT (using Context path=""). I don't think that's going to work if I want to host multiple apps in a single tomcat fronted (via mod_jk) by a single Apache, without some sort of rewrite hacking.
One thing that had bothered me about the UrlRewriteFilter approach was that for some odd reason I had initially thought that the outbound-rule stuff was processed by parsing the entire output (which would be somewhat expensive). But looking at the docs, it looks like that assumption was incorrect.
It's a shame that some of this stuff isn't easier. I'll have to look into Sean's approach (previous comment). It does seem to me that Spring MVC should have a better way of handling and "mounting" URLs (as I think they call it in Wicket).
Posted by Mark Helmstetter on May 14, 2008 at 05:39 AM MDT #
Posted by Jason on May 14, 2008 at 01:06 PM MDT #
For the life of me, I could never understand why Servlet mappings had to be done using only extensions.
Posted by Marcus Breese on May 14, 2008 at 03:34 PM MDT #
URL rewriting is actually a default feature even inside Tapestry 4. All links rendered by Tapestry are services links, e.g. page service renders a page, asset service renders an asset, etc.
You can simply overwrite the IEngineService responsible for the page service link creation and you have it, you can write your link the way you want. And the most interesting part that links will be modified as on rendering side also on processing, so rendered links will be consistently processed.
P.S. Check the new article about T5, I think it's time to start considering it for AppFuse - http://www.infoq.com/articles/tapestry5-intro
Posted by Renat Zubairov on May 14, 2008 at 06:29 PM MDT #
<code> "/$controller/$action?/$id?"() </code> In Grails url mappings ;-)
Posted by Graeme Rocher on May 14, 2008 at 07:23 PM MDT #
Posted by Matt Raible on May 14, 2008 at 07:24 PM MDT #
Posted by Sean Gilligan on May 14, 2008 at 07:43 PM MDT #
To Mark re: multiple root contexts... If you are willing to drop Apache, you can accomplish this with Tomcat virtual hosting (and the necessary DNS entries for your domain names). The easiest way is to make copies of your webapps directory ie: fooapps and barapps, And then in server.xml, replace this:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> </Host>
With something like:
<Host name="foo.com" appBase="fooapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> <Alias>www.foo.com</Alias> </Host>
<Host name="bar.com" appBase="barapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> <Alias>www.bar.com</Alias> </Host>
Also edit this line to reflect a valid default:
<Engine name="Catalina" defaultHost="foo.com">
Now, if someone would point me in the direction of subdomain support in Appfuse-Struts 2 where URLs bob.foo.com and tom.foo.com would resolve within the foo webapp to bobindex.jsp and tomindex.jsp respectively, I would much appreciate it.
I would also be interested in the Grails solution. That could swing me.
-BronPosted by Bron on May 15, 2008 at 03:38 PM MDT #
Posted by Howard Lewis Ship on May 15, 2008 at 05:00 PM MDT #
you could also map the Spring DispatcherServlet to "/*". Obviously this would would overwrite the default servlet but you could either use Apache to serve the static files, or (useful during development) use a Spring HandlerMapping that serves the static resources, for example http://www.riotfamily.org/api/latest/index.html?org/riotfamily/common/web/mapping/ResourceHandlerMapping.html
for Grails-like parameter extraction you can use the AdvancedBeanNameHandlerMapping provided by Riot: http://www.riotfamily.org/api/latest/index.html?org/riotfamily/common/web/mapping/AdvancedBeanNameHandlerMapping.html
Note that this HandlerMapping also allows you to perform reverse look-ups. You can for example write something like ${common.urlForHandler('someController')} in your FreeMarker views.
Posted by Felix Gnass on May 15, 2008 at 05:58 PM MDT #
Posted by Andreas Andreou on May 15, 2008 at 10:06 PM MDT #
http://theosophe74.blogspot.com/2008/05/spring-mvc-or-jsf.html
Thanks,
Mike
Posted by Michael S on May 16, 2008 at 01:00 AM MDT #
Posted by Dusty Pearce on May 16, 2008 at 03:30 PM MDT #
Posted by Scott Mark on May 30, 2008 at 02:02 AM MDT #
Posted by Gokhan Demir on July 09, 2008 at 11:07 AM MDT #
Thanks for this tip Matt! I was looking for some sort of solution to this problem and this works great.
There is one issue. If you are using DWR, the DWR Test interface is broken. I found adding the following condition statement to my rule and outbound-rule fixes DWR:
Posted by Mike Wille on July 14, 2008 at 06:33 PM MDT #
Extensionless URLs are easy using Wicket (just mount it, and optionally use an URL Strategy).
It's also possible to get Wicket to automatically encode parameters (either by letting Wicket handle to URL generation, or using a custom written Link).
Posted by John on February 14, 2011 at 08:34 AM MST #