Friday 16 October 2009

Struts 1 / Struts 2 and Guice gotchas

Hi,

In my current client we - with a chap- have been given , the task of migrating a huge monolithic legacy app which composed of core component library project and four small to midsize web application from Pico + Servlet api to Guice + Guice servlet module. On top of Pico, the legacy application currently was using Struts 1 heavily and Struts 2 in its REST api interface which was a seperate web application.

IOC side of migration has been pretty standart and well documented, and involved only binding objects in Guice by code or annotation instead of Pico. And so is Struts 2 and Guice migration, which involves including a plugin and moving all mapping in web.xml to a sevlet module. For us everything worked smoothly with those parts.

The most of the trouble shooting we have done was around Struts 1 and Guice servlet module interoperation. So this section will help mainly - in descending order - the ones trying

1. to move from Struts 1 + Servlet api application + Pico ----> to Struts 1 + Guice servlet module + Guice IO
2. to understand Guice and Guice servlet module better
3. to move from Struts 2 + Pico ----> to Struts 2 + Guice IOC

Note that point 1 is NOT OFFICIALLY supported by Guice but we have forced it by a small 'filter' fix defined below.

What Guice Servlet Module is not compatible with - by experience as of version 2


We have come up some issues we ran into and codewise expectations not officially stressed by Guice that a servlet filter or mvc/component framework must provide. They are stated below together with fixes we have done to get over.

Issue 1

Guice REQUIRES request dispatcher to be obtained through request NOT servlet context.The requirement is due to Guice overriding only request.getRequestDispathcer to delegate forwards/includes to only Servlet stack defined in the servlet module by client code instead of container first.

So one day, if you decide to write a framework, while getting dispatcher, you MUST call

[HttpServletRequest].getRequestDispatcher(String path)

Not

[ServletContext].getRequestDispatcher(String path)

Struts 2 is already doing that. Struts 1 does NOT. As solution we have overridden doForward/doInclude methods in a child request processor and made it obtain the dispatcher from request.

In conclusion, you must always use request.getRequestDispatcher with guice even in your servlet/filter code.

Issue 2

It is related to the first one. Remember we said Guice is overriding ONLY request.getRequestDispatcher to delegate forwards/includes to ONLY Servlet stack defined in the servlet module by client code instead of container first. It is not delegating forwards and includes to the filter stack defined in the servlet module by client.

Tucky's UrlReWriteFilter is the one where we discovered the problem. Inspired by Guice's own source code, we got around it by defining a filter coming before all others, wrapping the request and overriding getRequestDispathcer to delegate forwards/includes to Guice's dispatcher.

The fixer filter is below.

Issue 3

This a very specific issue.

During forwards, being confused about request uri, servlet path Guice is failing at info path calculation, at com.google.inject.servlet.ServletDefinition.java line 173.

The solution we found is to override, getRequestURI and getServletPath methods on the request dispatcher in the same filter class shown the bottom most- GuiceDispatchFixerFilter.java.

Issue 4

This is another specific issue. Guice is confused in the same place about context path if web application context is something like HOSTNAME/xxxx, around the same place above.

The solution is again in the same class - GuiceDispatchFixerFilter.java. We have overridden getContextPath of dispatcher.

Issue 5

We noticed that Guice does not match a url like '/xxxx', if you give '/xxxx/*' as url pattern to a filter in a servlet module, whereas Servlet Api mapping does.

The reason is, for this case, Guice's ServletStyleUriPatternMatcher just cuts '*' character off from the end of url pattern and checks if the given url starts with it, to be more specific It check if '/xxxx' starts with '/xxxx/' , which is false. We confirmed it by debugging Guice.

So obviously, a quick solution is to replace the mapping by these two mappings '/xxxx/*', '/xxxx'. Attention '/xxxx*' is not the right solution since it also mathces all uri's starting with /xxxx, i. e. /xxxxABC , /xxxxyyyyzzzz.

Another point

Guice servlet module does not support internal forwarding to filters yet. If you have a filter doing a forward and if it is instantiated by Guice, it forwards to the servlet stack and by passes any filter that you expect to go through.

In other words, Servlet Api 2.4 INTERNAL and FORWARD dispatcher types not supported by Guice Servlet module.


--------------------- GuiceDispatchFixerFilter.java ---------------------

public class Guic
Publish Post
eDispatchFixerFilter implements Filter {

@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;

// wrap the request
HttpServletRequestWrapper requestWrapped = new HttpServletRequestWrapper(request) {

@Override
public RequestDispatcher getRequestDispatcher(
final String path) {

// get the dispatcher overridden by GUICE
final RequestDispatcher guiceDispatcher = super.getRequestDispatcher(path);

// wraps GUICE's dispatcher, to overwrite request.getRequestURI
// and request.getServletPath
RequestDispatcher delegatorDispatcher = new RequestDispatcher() {

@Override
public void forward(final ServletRequest req,
ServletResponse resp)
throws ServletException, IOException {

guiceDispatcher.forward(new HttpServletRequestWrapper((HttpServletRequest) req) {

// Overridden to fix a bug in Guice while calculation path info.
@Override
public String getRequestURI() {
return path;
}

@Override
public String getServletPath() {
return path;
}

@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (value == null) {
if (getAttribute(name) != null) {
value = getAttribute(name).toString();
}
}
return value;
}

// for the bug : if web app context is like 'hostname/xxx'

@Override
public String getContextPath() {
String contextPath = "";
return contextPath;
}

}

, resp);

}

@Override
public void include(ServletRequest req,
ServletResponse resp)
throws ServletException, IOException {
guiceDispatcher.include(req, resp);
}
};

return delegatorDispatcher;
}

};

// keep chaining
chain.doFilter(requestWrapped, response);

}

}

1 comment:

  1. To use Google Guice (and also Guice's AOP) with struts 1 refer: https://code.google.com/p/google-guice-aop-struts1-integration/

    Try this.

    ReplyDelete

About Me

An independent java and agile consultant and developer in London. Reachable at cihan.uluisik@gmail.com.