Archive for the 'Java' Category

Autowiring Jackson Deserializers in Spring

Wednesday, May 2nd, 2012

Recently I was working in a Spring 3.1 controller for a page with a multi-select of some other entity in the system. Let's say an edit user page that has a User object for which you're selecting Role objects (with Role being a persistent entity with an ID). And let's further say that I'm doing some fancy in place editing of a user within a user list so I want to use AJAX and JSON to submit the user to the server, for whatever reason (probably because it's rad \oo/).

Okay now that we have our contrived scenario I want to serialize the collection of roles on a user so that they're a JSON array of IDs of said roles. That part is pretty easy. Let's just make all of our persistent entities either extend some BaseDomainObject or implement some interface with getId and then write a generic JSON serializer for Jackson:

package com.runningasroot.webapp.spring.jackson;

import java.io.IOException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializerProvider;
import org.springframework.stereotype.Component;
import com.runningasroot.persistence.BaseDomainObject;

@Component
public class RunningAsRootDomainObjectSerializer extends JsonSerializer<BaseDomainObject> {

    @Override
    public void serialize(BaseDomainObject value, JsonGenerator jgen, SerializerProvider provider) 
            throws IOException, JsonProcessingException {
        jgen.writeNumber(value.getId());
    }
}

Awesome if that's what I want. We'll assume it is. Now if I submit this JSON back to the server I want to convert those IDs into real live boys, er, domain objects. To do this I need a deserializer that has access to some service that can find a domain object by ID. I'll leave figuring out ways to genericize this for multiple domain objects as an exercise for the reader because frankly that's not the part I'm interested in.

So how do I control how Jackson instantiates deserializers and make sure that I can inject Spring beans into them? You would think it would be very easy and it is. Figuring it out turned out to be unnecessarily hard. The latest version of Jackson has a class for this and even says that's what it's for. So let's make us an implementation of a HandlerInstantiator that is aware of Spring's ApplicationContext. Note that you could do this entirely differently with an interface from Spring but who cares? Here's what I did:

package com.runningasroot.webapp.spring;

import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.HandlerInstantiator;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.KeyDeserializer;
import org.codehaus.jackson.map.MapperConfig;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.introspect.Annotated;
import org.codehaus.jackson.map.jsontype.TypeIdResolver;
import org.codehaus.jackson.map.jsontype.TypeResolverBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class SpringBeanHandlerInstantiator extends HandlerInstantiator {

    private ApplicationContext applicationContext;

    @Autowired
    public SpringBeanHandlerInstantiator(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public JsonDeserializer<?> deserializerInstance(DeserializationConfig config,
            Annotated annotated,
            Class<? extends JsonDeserializer<?>> deserClass) {
        try {
            return (JsonDeserializer<?>) applicationContext.getBean(deserClass);
        } catch (Exception e) {
            // Return null and let the default behavior happen
        }
        return null;
    }

    @Override
    public KeyDeserializer keyDeserializerInstance(DeserializationConfig config,
            Annotated annotated,
            Class<? extends KeyDeserializer> keyDeserClass) {
        try {
            return (KeyDeserializer) applicationContext.getBean(keyDeserClass);
        } catch (Exception e) {
            // Return null and let the default behavior happen
        }
        return null;
    }

    // Two other methods omitted because if you don't get the idea yet then you don't 
    // deserve to see them.  phbbbbt.
}

Great now we just need to hook up a custom ObjectMapper to use this thing and we're home free (extra shit that would probably trip you up as well included at no extra charge):

package com.runningasroot.webapp.spring;

import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.HandlerInstantiator;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig.Feature;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.module.hibernate.HibernateModule;

@Component
public class RunningAsRootObjectMapper extends ObjectMapper {

    @Autowired
    ApplicationContext applicationContext;

    public RunningAsRootObjectMapper() {
        // Problems serializing Hibernate lazily initialized collections?  Fix here.
        HibernateModule hm = new HibernateModule();
        hm.configure(com.fasterxml.jackson.module.hibernate.HibernateModule.Feature.FORCE_LAZY_LOADING, true);
        this.registerModule(hm);

        // Jackson confused by what to set or by extra properties?  Fix it.
        this.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
        this.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        this.configure(Feature.FAIL_ON_EMPTY_BEANS, false);
    }

    @Override
    @Autowired
    public void setHandlerInstantiator(HandlerInstantiator hi) {
        super.setHandlerInstantiator(hi);
    }
}

Now you just have to tell everything to use your custom object mapper. This can be found elsewhere on the web but I'll include it here in case of link rot:

package com.runningasroot.webapp.spring;

import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;

@Component
public class JacksonConfigurer {
    private AnnotationMethodHandlerAdapter annotationMethodHandlerAdapter;
    private RunningAsRootObjectMapper objectMapper;

    @PostConstruct
    public void init() {
        HttpMessageConverter<?>[] messageConverters = annotationMethodHandlerAdapter.getMessageConverters();
        for (HttpMessageConverter<?> messageConverter : messageConverters) {
            if (messageConverter instanceof MappingJacksonHttpMessageConverter) {
                MappingJacksonHttpMessageConverter m = (MappingJacksonHttpMessageConverter) messageConverter;
                m.setObjectMapper(objectMapper);
            }
        }
    }

    @Autowired
    public void setAnnotationMethodHandlerAdapter(AnnotationMethodHandlerAdapter annotationMethodHandlerAdapter) {
        this.annotationMethodHandlerAdapter  = annotationMethodHandlerAdapter;
    }

    @Autowired
    public void setObjectMapper(RunningAsRootObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }
}

I think you can also perform this bit of trickery inside of an application-context.xml. But whatever works for you works. I think Yogi Berra said that.

Of course you still need to annotate your getters and setters with special Jackson annotations:

@JsonSerialize(contentUsing=RunningAsRootDomainObjectSerializer.class) 
public Collection<Role> getRoles() {
    ...
}

// Some deserializer with some hot Spring injection going on in the back end (if you know what I mean)
@JsonDeserialize(contentUsing=RoleListDeserializer.class)
public void setRoles(Collection<Role> roles) {
    ...
}

So there you have it: an example of a Spring Jackson JSON serializer that serializes the contents of collections of domain objects as an array of IDs and then deserializes JSON arrays of IDs into domain objects to be put into a collection. Say that three times fast.

Share

Job Postmortem #2

Tuesday, August 3rd, 2010

About the Company

Now that I'm done with my current job it's again time to reflect on what I learned and what went wrong. I've changed the names to protect the innocent. I spent about 2 years at "Company V." They make a retirement planning tool. It allows you to do some nice "what if" scenarios to determine whether or not you're on track to do all those things you dream of someday doing after you retire. It's much more sophisticated than the crappy one or two question forms on the website of most financial planning companies.

It's a great idea in my opinion. It has a lot of potential. For the record, I like the people at Company V and I love the product idea. I just think things could be better.

Now for the lessons. I won't bother talking about the many issues I had about software development methodology at Company V. Instead I'll just talk about the product side of things.

Analytics, Stupid

The first is a simple one: collect some fucking analytics. Any discussion about how important a feature is, why people aren't signing up, which type of sign up button is more attractive are all bullshit if you don't have some way of collecting data about your visitors. We collected almost zero data about our visitors. What was our conversion rate? Fuck if I know. How many people abandoned the sign up process once they saw all of the data we required? Fuck if I know. That's the answer to every one of those questions because there's no goddamn data.

I can't talk about analytics as well as these two videos: Startup Metrics for Pirates and Web App Marketing Metrics. They're pretty short and definitely worth a few minutes of your time.

Multiple Masters

Company V has two very different target customers. Home users and financial advisors. If you are serving two very disparate customer types you will wind up with some very serious conflicts. Each customer type is a reason not to do something for the other customer or a great way to more than double your effort in the rare case you actually get to work on a feature.

In the case of Company V it was that they have a feature called "offline mode." This allowed financial advisors to take their laptop to locations where they don't have an Internet connection and sit down with a customer, going over their retirement plan. This was accomplished via a desktop application written in Java.

Getting Java working on someone's computer is an unnecessary hurdle and places without Internet connections only exists in movies. Offline mode is not useful to the home user. I would argue that it's not sufficiently useful to the financial advisor either. However, it was a feature that kept us from doing a lot of cool stuff because we had to have it. Yes, this feature could be accomplished in a better way but the need to keep the feature presented unnecessary overhead and complexity in my opinion.

Too Many Hurdles

There's just too much shit for someone to do before they can use the product. They have to sign up for an account, install the Java plug-in, download the application (or launch the applet) which is over 100 megabytes, and figure out how to use your product.

The more of those steps you can eliminate the better. Each one of those steps throws away half of your potential users. They just go bye-bye. The observant reader will realize that I just pulled that number out of my ass since Company V doesn't collect that kind of data. Prove me wrong.

The Things I'd Do

Short and sweet. Here's a list of things I would have done that I firmly believe would make for a better product for Company V.

Web App

Easy. Ditch the desktop application and make it a web application. Use something like GWT so you can get some good use out of your current Java development staff and have a relatively rich UI for your user. No installation on your computer, no downloading. Nice. You could even use Gears to get some workable solution for offline mode.

Use It Before You Register

If you have that nice web application, let people start making their retirement plan without even signing up. Just start using the product. Of course it would be nice if your product guided people through unfamiliar territory, but that's a given.

Once you've proven your value to them then you can try and get them to create an account if that's really your sort of thing.

Don't Even Register

Even better is to let them sign in with their Facebook, Google, Yahoo!, or OpenID login. Create an imperfect, incomplete profile off of whatever data you've got and bug them later to fill in the blanks. So what if you don't have their email address? Why the hell do you want to email them anyway?

Stop Emailing People

We collected email as part of the registration so we could bother our customers. Why? If you have a product announcement or a change in your training schedule why not just Tweet it? Or post an update on your product's Facebook page? Fine, let them put in their email if they want to be updated that way or need a password reminder (assuming they aren't using a 3rd partly authentication mechanism), but don't demand it.

Be the Tool

With retirement planning there are a lot of financial advisors that blog about how cool they are and how huge their planning penises are. We should have helped them do that. Our web app should have allowed embedding of whole or partial plans into web pages. If you want to show the benefits of a 529 savings plan create a couple of portfolios and embed the relevant portions into your blog. Company V would have a teeny tiny link in there so they get a little free press and the financial planner gets a tool that makes displaying unwieldy information a little easier. It's one of those win-win things I hear so much about.

Be the Tool Part 2

If you go to a financial planner they need to ask you roughly 3500 questions (I made that up) to determine the current state of your financial clusterfuck. Company V helped them do this by creating a PDF that was 10 megabytes and 40 pages long. The advisor would email it to the potential customer, pray it doesn't bounce because it's fucking huge, the customer would print it out, fill out the relevant portions, take it to the financial advisor who then hands it off to some data entry monkey to type into our desktop application. Simple, no?

Yeah, to hell with that. Use the no registration web application to allow the financial advisor to email, host, whatever a guided process to determine the relevant data and collect it directly from the user and dump it straight into the Company V application. The advisor has access to it immediately and the end user doesn't see most of those irrelevant questions. Throw in some tracking codes so the advisor can see the ROI for different ad campaigns. Let the advisor create a special URL that they can include in every email signature or even print right on their business card that takes the potential customer right to where they need to go. You get the idea.

Nice Ideas, But…

In fairness Company V thought some of my ideas were good. They just weren't good enough to actually do. There was no shortage of excuses. We have to keep offline mode, there are more important features to work on, who's going to pay for the development, etc. I still think each of these is potentially a great idea in general and for Company V especially. My next task is to find a place to work that agrees with me.

Share

Shell Scripting Madness

Friday, October 16th, 2009

Every now and then I bask in the beauty of the simple things. I'm not talking about children smiling, flowers, or any of that other crap. Shell scripting, baby! Today I had to move some SQL statements in some XML document into a Java class. So I needed to change this (which I didn't write):

SELECT CASE
    WHEN primaryStartAge < 20  THEN ' 0 to 19'
	WHEN primaryStartAge BETWEEN 20 AND 29 THEN '20 to 29'
	WHEN primaryStartAge BETWEEN 30 AND 39 THEN '30 to 39'
	WHEN primaryStartAge BETWEEN 40 AND 49 THEN '40 to 49'
	WHEN primaryStartAge BETWEEN 50 AND 59 THEN '50 to 59'
	WHEN primaryStartAge BETWEEN 60 AND 69 THEN '60 to 69'
	WHEN primaryStartAge > 70 THEN '70 and up'
	END as "Primary Start Age Range",
	count(1) as "Count" FROM analyticsResults
	WHERE calculatorType like ?
	GROUP BY CASE
	WHEN primaryStartAge < 20  THEN ' 0 to 19'
	WHEN primaryStartAge BETWEEN 20 AND 29 THEN '20 to 29'
	WHEN primaryStartAge BETWEEN 30 AND 39 THEN '30 to 39'
	WHEN primaryStartAge BETWEEN 40 AND 49 THEN '40 to 49'
	WHEN primaryStartAge BETWEEN 50 AND 59 THEN '50 to 59'
	WHEN primaryStartAge BETWEEN 60 AND 69 THEN '60 to 69'
	WHEN primaryStartAge > 70 THEN '70 and up'
END
ORDER BY 1 ASC

to something like this (which I still didn't write):

"SELECT CASE "
            + "    WHEN primaryStartAge < 20  THEN ' 0 to 19' "
            + "    WHEN primaryStartAge BETWEEN 20 AND 29 THEN '20 to 29' "
            + "    WHEN primaryStartAge BETWEEN 30 AND 39 THEN '30 to 39' "
            + "    WHEN primaryStartAge BETWEEN 40 AND 49 THEN '40 to 49' "
            + "    WHEN primaryStartAge BETWEEN 50 AND 59 THEN '50 to 59' "
            + "    WHEN primaryStartAge BETWEEN 60 AND 69 THEN '60 to 69' "
            + "    WHEN primaryStartAge > 70 THEN '70 and up' "
            + "    END as \"Primary Start Age Range\", "
            + "    count(1) as \"Count\" FROM analyticsResults "
            + "    WHERE calculatorType like ? "
            + "    GROUP BY CASE "
            + "    WHEN primaryStartAge < 20  THEN ' 0 to 19' "
            + "    WHEN primaryStartAge BETWEEN 20 AND 29 THEN '20 to 29' "
            + "    WHEN primaryStartAge BETWEEN 30 AND 39 THEN '30 to 39' "
            + "    WHEN primaryStartAge BETWEEN 40 AND 49 THEN '40 to 49' "
            + "    WHEN primaryStartAge BETWEEN 50 AND 59 THEN '50 to 59' "
            + "    WHEN primaryStartAge BETWEEN 60 AND 69 THEN '60 to 69' "
            + "    WHEN primaryStartAge > 70 THEN '70 and up' "
            + "END "
            + "ORDER BY 1 ASC";

I could copy and paste and fix it manually, use a text editor with regex search and replace, or something equally bland. Since it was Friday though i decided to treat myself and do it from a Cygwin shell. This got me close enough and made me giddy with satisfaction:

getclip |sed -e 's/"/\\"/g' -e 's/^/"/g' -e 's/$/ " +/g' |putclip

This grabs the contents of the clipboard, replaces all quotes with escaped quotes, replaces the beginning of each line with a double quote, and replaces the end of each line with a space / double quote / space / plus combo. It then sticks it back into the clipboard. It's not fancy, it could be better, but it was a minor bright point. And thanks to Cygwin it happened in Windows. Sort of.

Share

Interminably Long Timeouts for META-INF Under IIS

Wednesday, August 27th, 2008

How's that for a catchy title? To recap recent events: I'm working on speeding up a Java applet, sloppy code in applet libraries try to load resources from the server which then 404, you can avoid this by setting the codebase_lookup property to false in the applet tag, and finally eliminating 23 megs of invisible data can help speed up downloading. Now that we're all caught up, let's turn to today's adventure: "deployment nightmares" OR "why the hell doesn't my test environment match production?"

Applet Won't Load

I finally got the applet to the "good enough for government work" level of load-time performance. Understand that I don't even work on any of the code in the applet, I'm just trying to optimize what's there and how it's delivered from the server. Today was the day we decided to quietly deploy to production.

The first sign of a problem was when the Apple guys came into the office. The applet wouldn't load for them in either Safari, Firefox 2, or Firefox 3. However, it worked fine on every server they tried except the production server. While trying to figure out what was going on it turned out that the issue had to do with all machines using JRE 1.5 regardless of OS or browser. They all worked against every server except production.

Differences Between Production and Test Environments

In production we have some sort of load balancer, Tomcat is behind IIS, and it's an external network. Nothing in our test environment has a load balancer in front of it, only one machine has IIS but works fine, and my external EC2 deployment is obviously off our network. I'm not sure why we don't mirror as much of this as possible in our test environment, but we don't.

Now back to the bug. Turning off the load balancer had no effect. Eventually, someone let their browser sit long enough to see that the applet did in fact load. It just took around 10 minutes. I finally noticed that the Java console would hang on different non-existent resources it tried to load from the server. I used cURL to retrieve the URL and had to wait 2 minutes until it returned an empty reply. Most non-existent resources timed out immediately. Only URLs that contained META-INF or WEB-INF would hang.

Various 3rd partly libraries were trying to load odd things from the server as I mentioned previously. A few of these load attempts point at the META-INF directory. This only happens under 1.5 because I used the codebase_lookup parameter in the tag. Tomcat, Apache in front of Tomcat, and our internal IIS server all return immediately. The first two serve a custom 404 page while the IIS server sends an immediate empty reply.

WEB-INF and META-INF Protection

Both WEB-INF and META-INF are directories that you probably shouldn't be exposing. In fact, in most versions of the Tomcat Connector the connector will automatically 403 or 404 when any resource from those directories is requested. In our case, we were running an older version of the connector that just happened to have a bug that caused requests to either directory to take 2 minutes to timeout. A quick upgrade and an IIS service bounce fixed everything.

So the debugging lessons for the day are: use something like ngrep to watch your traffic, your test environment should mirror your production environment, applets under 1.5 sucks, and check your version numbers on third party libraries (and consider upgrading).

Share

Optimizing PNG File Sizes

Thursday, August 21st, 2008

For the past few weeks I've been working on improving the load time of an applet at work. Someone here noticed a while back that we seemed to have a ridiculous amount of image files in the applet. There are roughly 23 megabytes of images in the final jar which is around 14 megabytes in total size.

I opened a couple of the files in GIMP and resaved them to find that the final file was much smaller than the original. It turns out that the images were created using Fireworks and by default it puts some extra information in the PNG file, things like layers or palette information I believe. A few minutes of searching around on the internet and I found a wonderful tool named PNGOUT that could be used to losslessly optimize the size of PNG files. I used Cygwin (I'm on Windows at work) to run all of the PNG files through the utility via this command: find . -type f -name '*.png' -exec pngout.exe {} \; and waited a few minutes. The end result was as follows:

Before PNGOUT
Total size of images: 24,147,770 bytes
Total size of app jar: 14,086,540 bytes

After PNGOUT
Total size of images: 1,026,186 bytes
Total size of app jar: 4,335,560 bytes

Yikes. Not bad for a few minutes worth of work.

Share

More on applets and codebase_lookup

Friday, August 8th, 2008

As I mentioned in the last post I'm farting around with applets for work. You may remember that the applet was hammering the server whenever it couldn't find a resource in the jars. Of course, everything the applet needs is already in the jars so if it's not in them then it's not on the server either. It's all because the AppletClassLoader tries to load stuff from the codebase on the server if it fails to load it from the jars.

As part of the fix of setting codebase_lookup to false I did some very quick, unofficial benchmarking. The test environment was an EC2 deployment I've been messing around with–the smallest instance. I timed the startup time of the applet and counted the number of server hits during startup. To further minimize variablility, I did this after the jars had already been cached. This roughly corresponds to the startup time of a visitor to the site that has already successfully launched the applet. The results were as follows:

codebase_lookup – true (default setting)
Startup time: 35 seconds
Server hits: 442

codebase_lookup – false
Startup time: 7 seconds
Server hits: 34

When the the applet does hit the server to look stuff up in the code base, it doesn't just do it at startup. Some libraries that don't cache failed attempts to load a resource keep on hitting the server. In our app it's XFire and a lot of non-existent "aegis.xml" and "doc.xml" files as well as a whole bunch of "BeanInfo.class" attempts thanks to java.beans.Introspector. Each of these attempts takes a tiny bit of time and puts an annoying 404 in the access logs. It's hard to say what the distributed sluggishness of the app will be because of all these little attempts, but it's definitely non zero. It also has an affect on server side scalability when each client is potentially 13 times more chatty with the server than it needs to be.

An additional concern I have is that some of this cavalier attitude toward resource loading in these libraries also happens on the server side. How many failed getResourceAsStream attempts am I not seeing and what impact are they having on overall server performance? At the current traffic levels it's probably insignificant but the idea of that much inefficiency spread out throughout the app kind of bothers me.

Share

XFire, Applets, and 404s

Wednesday, August 6th, 2008

XFire Mapping Files

The application I work on uses XFire as a Java SOAP framework (this project has been replaced by CXF which is why this isn't a bug report with a patch) that is launched via Web Start. At some point we decided to allow it to be run as an applet as well. One of the weird things I ran into when we did this was a ton of 404 errors in the access logs on the server. The applet was repeatedly trying to load these "*.aegis.xml" files. These are mapping files used to configure how XFire maps your types to XML. However, you don't necessarily need these files if you elect to configure the mappings another way. XFire apparently still looks for the mapping file to allow you a mechanism for overriding other mapping methods. Everybody still here? Good.

Repeatedly Loading Non-Existent Resources

Since we don't use the mapping files, they're not in any of the jars that would be loaded by the applet. So what happens is XFire calls XMLClassMetaInfoManager.getDocument whenever it needs to figure out how to map one of your objects to XML. The manager then executes a getResourceAsStream to see if you have a mapping file (either as a primary or backup configuration). This winds up in the classloader which in the case of an applet is the AppletClassLoader. The class loader first tries to find the requested resource locally and if it's not there it tries to load it from the codebase. In our case it's not on the server so you get a 404 in the logs. The fact that the resource isn't available anywhere isn't a problem but the fact that this information isn't cached anywhere means that the next time something looks for the mapping file the whole thing is going to happen all over again.

The code in XMLClassMetaInfoManager.getDocument could easily add and check for a key with a null value in the event that it had previously tried to find the mapping and couldn't. This is what it does instead:

	public Document getDocument(Class clazz) {
		if (clazz == null)
			return null;
		Document doc = (Document) documents.get(clazz.getName());
		if (doc != null) {
			return doc;
		}
		String path = '/' + clazz.getName().replace('.', '/') + ".aegis.xml";
		InputStream is = clazz.getResourceAsStream(path);
		if (is == null) {
			log.debug("Mapping file : " + path + " not found.");
			return null;
		}
		log.debug("Found mapping file : " + path);
		try {
			doc = new StaxBuilder().build(is);
			documents.put(clazz.getName(), doc);
			return doc;
		} catch (XMLStreamException e) {
			log.error("Error loading file " + path, e);
		}
		return null;
	}

(Incidentally, I'm really liking the syntaxhighlighter WordPress plugin.) The AppletClassLoader is off the hook because you can easily say that its behavior is a feature, not a bug.

The Quick Fix

Like I said, XFire is in maintenance mode. The prospect of getting the source code, creating a vendor branch in the SCM, fixing / testing, and finally internally hosting the patched artifact didn't appeal to me on the limited time scale on which I was working. The quick fix (that apparently only works in JRE 1.6) is to set the codebase_lookup parameter to false in the applet tag.

Sadly, the 1.6 restriction means I probably won't get to live with this fix for long and will likely wind up either patching XFire or upgrading to CXF. CXF may or may not still have the problem, but at least I'd be writing a patch for an actively developed library.

To further complicate things, there are other incidents of 404s with resource bundles and XML parsers. The nice thing about the codebase_lookup solution is that it "fixes" those issues as well. If I can't rely on JRE 1.6 as a requirement I'll have a lot more work to do than just patching XFire.

Applets. Yay.

Update

It's probably actually XMLTypeCreator that is causing my problem, but the code is pretty much identical. There are also issues with XMLDocumentationBuilder.loadDocument. I don't blame XFire in particular. There are a lot of projects out there with code that uses getResourceAsStream knowing that it could fail and then not caching the fact that it failed. They then repeat the operation multiple times. This assumes that getResourceAsStream and other such operations are very cheap performance-wise or that it's not worth the effort or memory to cache failed attempts. I'm not sure I buy it in the first place but it certainly goes out the window when you're in an applet and every such call that fails locally then gets executed across the network back to your codebase.

Share

Java, Private Members, and Dynamic Proxies

Friday, July 25th, 2008

I found this problem and solution and then backtracked to some Google search results to make sure I wasn't being completely stupid. The issue was that the equals method on one of my objects was returning false when I knew the two objects were equal. A quick trip to the debugger showed me that the method was using some of the private member variables of the provided object to determine equality. Something along the lines of if(this.name.equals(other.name)) (no that's not the whole method, yes I know how to write an equals method, etc). The problem is that if the object provided to the method is proxied by something like a CGLIBLazyInitializer, those private fields may not have values. So, you're best off using the getter method. Also, as another blog and its comments point out, you need to be careful when calling certain methods on the provided object. One example is that you can't rely on using the getClass() method and should use instanceof instead.

As the other post also points out, this is one of the possible odd side effects from using Hibernate, even though it's really an issue with the CGLIB dynamic proxy class(es).

Share