Last week, I wrote about the creative use one can make of Filters by grading assignments of students. About, there’s another recurring issue that warrants a blog post: servlet mappings. While servlet mappings seem easy on the surface, they sometimes can be the cause of huge headaches.
The issue
In the assignment mentioned above, students have to create a mock e-commerce shop application. The home page shows a title, an introduction phrase, as well as a catchy image. The issue is that the image is not displayed.
At this point, students get creative - this is an assignment after all. Either they discard the requirement to display the image, or they use an image hosted online - hot-linking. The latter amazingly works!
Likewise, JavaScript and CSS files are not found. To make them work, they get inlined.
Let’s see what’s the reason for that.
Analysis
JavaEE webapps offer two path mapping methods:
- The servlet mapping one, configured either in the
web.xml
web deployment descriptor or via@WebServlet
annotations on the class.@WebServlet("/foo") public class FooServlet extends HttpServlet { ... }
The servlet is now accessible under the
/foo
path. - The resource one:
whatever is not in the
WEB-INF
folder can be accessed directly.sample.war |__ image | |__ a.jpg | |__ b.jpg |__ script | |__ a.js |__ style | |__ a.css |___ WEB-INF |__ web.xml
Given the above structure, the following paths are exposed:
/image/a.jpg
/image/a.jpg
/script/a.js
/style/a.css
The end user doesn’t know whether a path is served by a servlet or a static resource.
A servlet could be mapped to /foo.html
.
Likewise, one could implement a servlet serving dynamic images and map it to *.jpg
.
Now the important bit. When a request is made for a path, the server first looks for matching servlet mappings. If none is found, then it tries to find the relevant static resource by path. Hence, a servlet mapping shadows a static resource if both match the same path.
In the above issue, the culprit is indeed a servlet, and more precisely the one mapped to the /
mapping.
@WebServlet("/")
public class HomePageServlet extends HttpServlet { ... }
This is easily demonstrated by running the JVM in debug mode, and setting a breakpoint inside the service()
method of the said servlet.
It will readily be apparent that it shadows every static resource.
How it works
The Servlet Specifications has a section dedicated to mappings and path matching. Here’s an excerpt:
- The container will try to find an exact match of the path of the request to the path of the servlet. A successful match selects the servlet.
- The container will recursively try to match the longest path-prefix. This is done by stepping down the path tree a directory at a time, using the
/
character as a path separator. The longest match determines the servlet selected.- If the last segment in the URL path contains an extension (e.g.
.jsp
), the servlet container will try to match a servlet that handles requests for the extension. An extension is defined as the part of the last segment after the last.
character.- If neither of the previous three rules result in a servlet match, the container will attempt to serve content appropriate for the resource requested. If a "default" servlet is defined for the application, it will be used. Many containers provide an implicit default servlet for serving content.
Notice the mention of the default servet.
This default servlet is the one mapped to /
, and will indeed match every path that is not matched by other servlets, because the longest path matching rules.
Solution
The solution is also found in the specification:
The empty string ("") is a special URL pattern that exactly maps to the application’s context root, i.e., requests of the form
http://host:port/<context-root>/
. In this case the path info is/
and the servlet path and context path is empty string ("").
The fix is to just remove the /
in the servlet mapped to the home page:
@WebServlet("")
public class HomePageServlet extends HttpServlet { ... }
Conclusion
The devil is in the details. One extra character can wreck one’s application, and send developers into a frenzy of bad workarounds. Most of times, reading the documentation (or specifications) allows to find the correct solution.