I mentioned in my last post that I've been tinkering with a modern structure for OpenNTF's web site as a side project. In that, I talked about how I've been going with Jakarta MVC for the front end, but ran into an odd problem with the latest versions in Open Liberty, and that was the impetus to tinkering with ERB.
Well, I decided to go back and take a swing at trying to make JSP work in this case, since it's (still) a good engine for this purpose, and it could be a fun experiment. I was indeed able to do it, and I think the path I took is worth chronicling here.
Context
In previous projects, such as this blog, I've used older versions of the software stack involved - basically, Jakarta 8, which is before the "big bang" switch from the javax.*
to jakarta.*
package namespace. Since this is a clean new app, I really want to lean to the newest versions across the board, so I pegged my plans to that.
Though Jakarta EE 9 and 9.1 (the Java 11 official version) have been out for a bit, the switchover comes with the sort of turbulence one would expect. Until just this past week, Open Liberty supported JEE 9 only in beta releases - these have historically been plenty stable for me, but it's always asking for trouble. Even with that non-beta version out, I found myself still on the beta track: I'm addicted to using MicroProfile Config, and MicroProfile's move to JEE 9 support is still itself in the RC stage.
So, okay, betas it is.
The Problem
Once I set everything up on JEE 9 and MP 5, I hit this exception when trying to render a JSP via an MVC Controller object:
java.lang.RuntimeException: SRV.8.2: RequestWrapper objects must extend ServletRequestWrapper or HttpServletRequestWrapper
at com.ibm.wsspi.webcontainer.util.ServletUtil.unwrapRequest(ServletUtil.java:89)
at [internal classes]
at org.eclipse.krazo.engine.ServletViewEngine.forwardRequest(ServletViewEngine.java:135)
at org.eclipse.krazo.engine.JspViewEngine.processView(JspViewEngine.java:58)
at org.eclipse.krazo.core.ViewableWriter.writeTo(ViewableWriter.java:159)
at org.eclipse.krazo.core.ViewableWriter.writeTo(ViewableWriter.java:1)
at org.jboss.resteasy.core.interception.jaxrs.ServerWriterInterceptorContext.lambda$writeTo$1(ServerWriterInterceptorContext.java:79)
... 4 more
The short of it is that Krazo (the MVC implementation) passes JSP rendering along to the app container, rather than doing its own JSP work, which makes perfect sense. This, however, hits trouble within Liberty's ServletUtil
class, which attempts to "unwrap" the incoming HttpServletRequest
object to find the core Liberty-specific object to use extended methods on.
Normally, this sort of thing would work fine: every app server has its own variant of HttpServletRequest
for its own uses, and it's perfectly reasonable to do this kind of unwrapping. However, for some reason, this was going awry.
The specific code from Krazo that calls down into Liberty code does do new HttpServletRequestWrapper(request)
, but that's also legal: the unwrapRequest
method is intended specifically to unwrap spec-standard HttpServletRequestWrapper
objects like that. So that's not our culprit, and I had to dig deeper.
Investigation
To start my investigation, I knew I'd need to work with the Krazo layer. Fortunately, though many of the moving parts here are baked into the Liberty server, Krazo is not - I include it as a Maven dependency in my app. So I cloned the Krazo source, added it to my workspace, and set my dependency on the SNAPSHOT version, allowing me to do my work inside Krazo's classes.
Context
So what was going on? At first, I thought that maybe something had snuck in a javax.*
class somewhere - old code that wasn't fully migrated to JEE 9. That would certainly cause the trouble: javax.servlet.ServletRequestWrapper
and jakarta.servlet.ServletRequestWrapper
are, to the JVM's eyes, entirely-unrelated classes with no compatibility whatsoever. And, indeed, looking at the source of ServletUtil
could give one that impression right away, since the code uses javax.*
.
That's not the trouble, however. Though the class is written to javax.*
, I gather that it's run through Eclipse Transporter during packaging of the app server, and the actual class that's involved uses jakarta.*
. Okay, that's good to know and makes sense, but it also doesn't get us any closer to the root problem.
For my next step, I wanted to figure out what, specifically, it was looking for. The unwrapRequest
method takes a Class<?>
parameter to find the needed request type, but the stack trace above hid the path it took to get there. By attaching a debugger to the server, I gleaned that it was being called by the unwrapRequest
variant above it that looks specifically for a com.ibm.wsspi.webcontainer.servlet.IExtendedRequest
.
Okay, so I have the name of the interface it's looking for - I can work with this. My next step was to try to get a programmatic handle on it. The basic approach, when you don't have the class as part of your project, it to look it up via:
1 | Class<?> requestClass = Class.forName("com.ibm.wsspi.webcontainer.servlet.IExtendedRequest");
|
That doesn't work here, though: though the app server definitely has that class, it (properly) shields the running application from accessing the class directly, so that the app runtime isn't contaminated by the surrounding server code.
What I needed to do next was find a ClassLoader
that does know about it, and the best way to do that is to find a class provided from outside the running app and ask that. Fortunately, the incoming request
is exactly that. So:
1 | Class<?> requestClass = Class.forName("com.ibm.wsspi.webcontainer.servlet.IExtendedRequest", false, request.getClass().getClassLoader());
|
What that does is ask whatever ClassLoader
the request object comes from - that is to say, the container's loader - to find the class. And it worked! Now I could test to verify whether the core request matches the type needed:
1
2 | Class<?> requestClass = Class.forName("com.ibm.wsspi.webcontainer.servlet.IExtendedRequest", false, request.getClass().getClassLoader());
System.out.println("does it match? " + requestClass.isInstance(request));
|
As expected, that resolved to false
. In this case, that's good, since it'd have been a much-worse problem if it hadn't. But what is the request object, anyway? Well:
1
2 | System.out.println(request.getClass());
// output: jdk.proxy15.$Proxy68
|
Right, okay, that makes sense: all sorts of stuff uses proxy objects in Java, not the least of which being the CDI environment running the whole show.
Cracking Open Proxies
So I had some good information at this point: the HttpServletRequest
that Krazo is handed is a proxy object, but Liberty has a hard requirement that its dispatcher is given an instance of IExtendedRequest
, which this is not. That means that something in the stack is taking the original Liberty request object and making a proxy for it - fair enough, but inconvenient for me.
My next thought was that maybe I could track down the type of proxy object it is and, with that knowledge, get the underlying delegate request. That's a common-enough pattern: have an instance property in your proxy class that contains the delegate, and (if I'm lucky) have it accessible via a getter. Java's java.lang.reflect.Proxy
class has a static method for determining the object that actually handles called methods:
1
2 | System.out.println(Proxy.getInvocationHandler(request));
// output: org.jboss.resteasy.core.ContextParameterInjector$GenericDelegatingProxy@fc04422d
|
This was starting to come together all the more. Liberty recently switched from Apache CXF to RESTEasy for its JAX-RS implementation, and that could explain why this is trouble now when it wasn't before. More importantly for my immediate needs, that also gave me a lead to track down the proxy class.
However, though it was easy enough to find, my heart sank a bit at what I found: rather than having an easy instance property to get the real request, it uses an object from its container class and in turn asks that for the request. Maybe I'd be able to get to that via reflection, but the prospect of figuring out how to work with nested class contexts caused me to try to look around elsewhere instead.
CDI
Another potential answer came to me in a flash: CDI! Access to the CDI environment is standardized, and maybe I could fetch the original request from there. It'd be extremely likely that it'd just hand me back a similar proxy, but it'd be worth a shot. So here we go:
1
2
3 | HttpServletRequest cdiReq = CDI.current().select(HttpServletRequest.class).get();
System.out.println(cdiReq);
// output: com.ibm.ws.webcontainer40.srt.SRTServletRequest40@1a22712c
|
Oh! Good! That's one of Liberty's internal types! Is it the object I need, though? Well... no. Crap. requestClass.isInstance(cdiReq)
is false
, so this didn't really get me very far. That's a shame, too, since that solution could have involved no implementation-specific code at all.
Internal Liberty Classes
My next thought was that I should try to find another way to get around to finding the true request object. I looked back through the debug stack to find where it was originally calling unwrapRequest
to get a bit more context:
1
2
3 | WebContainerRequestState reqState = WebContainerRequestState.getInstance(false);
wasReq = (IExtendedRequest) ServletUtil.unwrapRequest(request);
|
Okay, so what's this with WebContainerRequestState
? That sure smells like an object that's meant to be a request-wide object to get to all sorts of state. If I were to write such a class, I'd use it to stash the incoming request as well as any other incidental data that I wouldn't want to ferret away in a way that might leak into the app. I was a little wary that WebAppRequestDispatcher
didn't use it to get the IExtendedRequest
, but maybe I'd luck out.
And boy, did I! Looking down the source of the file, I found my mark: public IExtendedRequest getCurrentThreadsIExtendedRequest()
.
The (Provisional) Solution
Now, I had all the tools I needed. Towards the bottom of Krazo's ServletViewEngine
class, I conjured up this reflective incantation:
1
2
3
4
5
6
7
8
9
10 | try {
Class<?> requestStateClass = Class.forName("com.ibm.wsspi.webcontainer.WebContainerRequestState", false, request.getClass().getClassLoader());
Method getInstance = requestStateClass.getDeclaredMethod("getInstance", boolean.class);
Object requestState = getInstance.invoke(null, false);
Method getCurrentThreadsIExtendedRequest = requestStateClass.getDeclaredMethod("getCurrentThreadsIExtendedRequest");
request = (HttpServletRequest)getCurrentThreadsIExtendedRequest.invoke(requestState);
} catch (Throwable e1) {
// Not on WAS
}
rd.forward(new HttpServletRequestWrapper(request), new HttpServletResponseWrapper(response));
|
So what I'm doing here is breaking into the container's ClassLoader
in order to get a handle on WebContainerRequestState
. From there, I'm able to call the method to get the current instance, and then in turn call the method to get the IExtendedRequest
. I overwrite the request
variable we're working with, and then pass that along to the dispatcher. If any of that were to fail, I just throw up my hands, assume I'm not on Liberty, and continue on as before.
And... it works! It actually works! The pages now render properly, with all the niceness of modern JSP at my fingertips! It was fun to toy with the idea of ERB, but I like this better for an otherwise-pure-Java app for sure.
Next Steps
So I have a solution that works for me, but it's so ugly and implementation-specific that I can't exactly be comfortable with it. Still, the trouble comes from an implementation-specific source, so that may be required. Maybe I'll have to leave it like this.
More responsibly, though, what I should do is narrow this down into a reproducible case without all the other moving parts in the app to make sure that it's actually a bug/incompatibility, and thus something that I can report. This is all open-source software, after all, and it'd do nobody any good for me to let a potential actual problem linger. I'll just have to properly identify where the true culprit is first. Is it because Krazo uses RequestDispatcher
in a somewhat-unusual way? Is it because RESTEasy is too aggressive about wrapping requests with no proper way to get to the delegate? Is it that Liberty should handle this case better internally? Or maybe it's just some side effect of the other things I have going on. Research is warranted.
In the mean time, that was a fun one. I don't know that I'll have a need for this specific solution again, but it was good to find, and it's always good to get some troubleshooting practice like this for sure.