Java Servlet url-pattern - Exclude Me

Recently I was investigating one of the many Java web frameworks, and came across an old frustration. First though, I should give some background.

The Java Servlet specification provides for the ability for developers to map various URLs to specific Servlets (request handlers). You can map '*.do' to your Struts request dispatcher to enable the framework to handle all the requests for pages that should be served by the framework. Basic wild cards are supported, but only positive matching, no excludes.

One of the basic tenants I have adopted is that the URLs in an applications should be technology agnostic. If I can look at your website and tell what technology you are using, that is 'bad'. Of course, I didn't come up with this wisdom on my own, it is mostly guided by the W3C Cool URLs Article. So if your app has cgi-bin, .pl, .jsp, .asp, or .aspx files, etc, I consider that bad form. Of course, I can setup my Java application with all the pages named .aspx, but I find it much better to keep it generic to begin with.

One of the main issues I face when I use a web framework is how to integrate the main request dispatcher without manually mapping every valid URL. What I would like is the ability to map everything EXCEPT certain URLs.

For example, I'd like to map /* to my dispatcher, with the exception of *.html, *.gif, *.jpg, *.css, and *.js. This allows me to have all the dynamic requests served by the dispatcher, while the static content is served normally. In a production environment, I can accomplish this using Apache to only pass through the URLs I define to Tomcat (Apache does have the 'except' logic in it's mapping).

Another common example is applying Servlet Filters. Using Servlet Filters you can apply security or any type of logic to a large set of requests. Maybe I would like to secure all the pages in my site except for / and /login. Today I map the filter to all the URLs, and then in code define any URLs that should be excluded. However, in doing this I've mixed my configuration between my web.xml file and my source code.

The ability to define a Regular Expression, or more complex rules (match 'a' except 'b') would give much greater flexibility.

A few Google searches have yielded some mildly interesting work-arounds but, nothing that I really find satisfyingly. Let me know if you have any better solutions.

UPDATE: Thanks for the many work-arounds in the comments. There is another option as well, see my new post for details.

12 comments:

  1. I am currently trying to do the same thing (use /* for the dispatcher, but exclude resources and common files). What was your step-by-step solution for the following statement:

    "In a production environment, I can accomplish this using Apache to only pass through the URLs I define to Tomcat (Apache does have the 'except' logic in it's mapping)."

    Also, when you define the ServletFilter and catch the URLs you want to filter, do you just use RequestDispatcher.forward()? Won't this cause an infinite loop?

    Ideally, a <url-pattern-exclude> tag for the web.xml of Tomcat would solve this all for me.

    ReplyDelete
  2. So in Apache I use mod_jk to forward the request to Tomcat. When I configure mod_jk, I do the following:

    JkMount /* tunnelname
    JkUnMount /images/* tunnelname
    JkUnMount /css/* tunnelname

    Forwarding a request using RequestDispatcher does not trigger new calls to the filter, so no, you don't have an infinite loop.

    I agree, I'd love to have <url-pattern-exclude>.

    ReplyDelete
  3. +1 on "pretty urls" and hiding implementation details.

    Have a look at URLRewriteFilter... you can throw it in front of all other URL processing and customize using regex's.

    http://tuckey.org/urlrewrite/

    -T

    ReplyDelete
  4. I haven't had much success on this either. I hate the idea of programmatically performing the excludes in filters.

    I searched on Google and found your post.

    I would like to say "yes, that's what I would like as well."

    Sincerely,
    jonnie savell

    ReplyDelete
  5. Include me too.. What I did was setup a filter and exclude the URLs

    ReplyDelete
  6. just another "me too" comment...same situation, same old problem...i'm glad i found your blog post so i don't feel the need to keep on searching google.

    ReplyDelete
  7. I found a creative workaround that fits my situation. I have two filters that need to be executed on all requests except for AJAX calls.

    I simply created another servlet-mapping entry for *.ajax as well as the normal *.do, then set up the filter url-pattern to *.do.

    I can then make the AJAX requests to whatever.ajax without being run through the filter and regular requests to whatever.do and they are run through the filters.

    ReplyDelete
  8. Until the moment that the Servlet specification will allow exclude-url-pattern, we only have workarounds.

    For example, in the case that you have two or more filters for which you want to apply exclude-url-pattern, I devised this solution: you create another filter (SkipFilter) that will be called prior to the one that you want to skip (for example, AccessControlFilter).

    Inside SkipFilter you simply set some value to some request attribute, and the later in AccessControlFilter you check for the existence of that request attribute value. In case it exists, then you skip the processing of AccessControlFilter by simply calling filterChain.doFilter(servletRequest, servletResponse)

    To allow this filter to be used to skip other filters also, the best solution is to have the attribute name and value configured as filter init-parameters.

    ReplyDelete
  9. This is the implementation of SkipFilter:

    package my.company;

    import javax.servlet.*;
    import java.io.IOException;
    import org.apache.commons.lang.StringUtils;

    /**
    * Because the Servlet specification doesn't offer the option of exclude-url-pattern,
    * we implemented this filter to "skip" other specific filters that are called later in the filter chain.
    * <p>
    * As opposed to the solution of "forwarding" the request, and thus skip all subsequent filters,
    * this solution allows us to still call other filters, but skip some of them.
    * <p>
    * This filter simply sets some predefined value to some predefined request attribute.
    * The name of the request attribute that will be set is specified in the {@link #SET_ATTRIBUTE_NAME_PARAMETER} init-parameter of this filter,
    * and the value that will be set is specified in the {@link #SET_ATTRIBUTE_VALUE_PARAMETER} init-parameter of this filter.
    * <p>
    * For this to work, the filters that we want to skip must be written (or modified) by us,
    * to explicitly check for the existence of the given value in the given request attribute,
    * and skip the processing by simply calling filterChain.doFilter(request, response) instead.
    * <p>
    * So unfortunately this probably cannot be used for filters from external libraries,
    * unless they are open source and we modify their source to use them in our project.
    */
    public class SkipFilter implements Filter {
    public static final String SET_ATTRIBUTE_NAME_PARAMETER = "setAttributeName";
    public static final String SET_ATTRIBUTE_VALUE_PARAMETER = "setAttributeValue";

    private String attributeName;
    private String attributeValue;

    public void init(FilterConfig filterConfig) throws ServletException {
    attributeName = filterConfig.getInitParameter(SET_ATTRIBUTE_NAME_PARAMETER);
    // The attribute name must not be empty
    if (StringUtils.isBlank(attributeName)) {
    throw new ServletException("The init-parameter " + SET_ATTRIBUTE_NAME_PARAMETER + " must not be empty!");
    }
    attributeValue = filterConfig.getInitParameter(SET_ATTRIBUTE_VALUE_PARAMETER);
    // The attribute value is allowed to be empty
    // (maybe we will only check for the existence of the attribute, without caring for the attribute value)
    if (attributeValue == null) {
    throw new ServletException("The init-parameter " + SET_ATTRIBUTE_NAME_PARAMETER + " must not be empty!");
    }
    }

    public void destroy() {
    }

    public void doFilter(ServletRequest request,
    ServletResponse response, FilterChain filterChain)
    throws IOException, ServletException {
    request.setAttribute(attributeName, attributeValue);
    filterChain.doFilter(request, response);
    }
    }

    ReplyDelete
  10. And this is the example mapping in web.xml:

    <filter>
    <filter-name>SkipAccessControlFilter</filter-name>
    <filter-class>my.company.SkipFilter</filter-class>
    <init-param>
    <param-name>setAttributeName</param-name>
    <param-value>SkipAccessControlFilter</param-value>
    </init-param>
    <init-param>
    <param-name>setAttributeValue</param-name>
    <param-value>true</param-value>
    </init-param>
    </filter>

    <filter>
    <filter-name>AccessControlFilter</filter-name>
    <filter-class>my.company.AccessControlFilter</filter-class>
    <init-param>
    <param-name>checkAttributeName</param-name>
    <param-value>SkipAccessControlFilter</param-value>
    </init-param>
    <init-param>
    <param-name>checkAttributeValue</param-name>
    <param-value>true</param-value>
    </init-param>
    </filter>

    <filter-mapping>
    <filter-name>SkipAccessControlFilter</filter-name>
    <url-pattern>/someurl</url-pattern>
    </filter-mapping>

    <filter-mapping>
    <filter-name>AccessControlFilter</filter-name>
    <url-pattern>/*</url-pattern>
    </filter-mapping>

    ReplyDelete
  11. I found the solution to this problem of /* url-pattern at
    http://java-espresso.in/2011/09/webxml-problem-and-solution-for-url.html.

    They have given two approaches to solve this problem..

    ReplyDelete