XML or database-backed ResourceBundle
Ever wanted to store your localized messages in an XML file or database? You have probably found that java.util.ResourceBundle was designed in such a way that it is impossible to provide a clean solution to this problem. This is tracked as bug 4403721 and it seems that some solution may be available in Mustang.
For those who need it now I managed to put together a couple of classes that address this issue. Not as clean as I'd like it to be, but it works fine with anything that uses ResourceBundle.getBundle(), such as JSTL. In other words it should work in any situation where .properties file would normally work.
The "not as clean as I'd like" part is that you'll need a new class for each supported locale. The good news is that this class is almost empty.
First you need to create some sort of MessageProvider. Its role is to read localized messages from the database or load them from XML file. This object would be a signleton instance shared by all bundles. It would also provide a method like getString(String localeName, String key) used by all the bundles to retrieve the messages.
All localized resource bundle classes would inherit from the same class. Let's call it AbstractResourceBundle. Its code could look like:
public abstract class AbstractResourceBundle extends ResourceBundle {
protected String mLocaleStr;
private MessageProvider mMessageProvider =
MessageProvider.getSharedInstance();
protected AbstractResourceBundle(String localeStr) {
mLocaleStr = localeStr;
}
protected AbstractResourceBundle() {
String className = getClass().getName();
int pos = className.indexOf('_');
if (pos == -1 || pos >= className.length()-1) {
throw new RuntimeException("Class name must contain locale suffix");
}
mLocaleStr = className.substring(pos+1);
}
public Enumeration getKeys() { return null; }
protected Object handleGetObject(String key) {
assert mLocaleStr != null : "mLocaleStr not set";
return mMessageProvider.getString(mLocaleStr, key);
}
}There are two important things here. First is the fact that
handleGetObject method delegates the call to the shared instance of
MessageProvider.
The second is that the constructor
AbstractMessageProvider() will figure out the locale from the inheriting class name. It's required that the class name contains locale suffix, otherwise
ResourceBundle.getBundle(...) will not be able to load it.
An implementation for a supported locale looks like this:
public class MyBundle_fr_FR extends AbstractResourceBundle {}Finally, you would use it like:
ResourceBundle rb = ResourceBundle.getBundle("MyBundle", new Locale("fr", "FR"));
String msg = rb.getString("label.hello");If anyone managed to solve it in a better way, please share it.
Hibernate lazy loading and HttpSession-scoped objects
In order for lazy loading to work (either collections or proxy) Hibernate requires the session in which these lazy objects were created to be open when when accessing them. Otherwise you'll get LazyInitializationException. To work around this issue the "Open session in view" pattern is used.
This works fine for request-scoped objects.
But how to deal with session- or application-scoped objects. Let's say you have an Account that has several Users. When a user logs into the application you put the reference to this user object into the session. The user's account (along with the list of its users) is rarely used and you don't want to fetch all account users for every logged-in user, so you declare it as lazy (proxy).
If you try to access the account outside the request in which the user was loaded you'll get LazyInitializationException.
Now Hibernate.initialize() doesn't really cut it. If I use it in my DAO layer then it means that I make assumption how my object will be used. Doing it in business layer is not good either as this would mean that the business layer knows what is the DAO implementation.
In addition Hibernate.initialize() would have to be called on any lazy property (proxy) or collection - one by one, which is not good either.
All this is a documented Hibernate behavior. I'm wondering why Hibernate couldn't open a session or pick one that is open and then do some magic to reassociate the object and have all these lazy loading always working...
What are the best practices to handle such longer-than-request-scoped objects? Is Hibernate.initialize() the only option?
javac's "-source" changes your class behavior ???
I came across this yesterday. Very wired!
I took quite time to understand what was going wrong. My JUnit tests worked fine in IDEA but they were failing when run from command line with ANT. Then I figured out that the only difference was the language compatibility level set in IDEA to "1.4" and ommited (set do default) in ANT.
Below is an example that you can try. I work on WinXP, Java HotSpot(TM) Client VM (build 1.4.2_06-b03, mixed mode).
public class ShowSourceImpact {
public static void main(String[] args) {
final Integer val = new Integer(1);
Sample s = new Sample() {
Integer createI() {
return val;
}
};
System.out.println("'i' is: " + s.getI());
System.out.println("'i' should be: " + val);
}
static class Sample {
private Integer i = null;
public Sample() {
i = createI();
}
Integer createI() {
return new Integer(-1);
}
public Integer getI() {
return i;
}
}
}Then, when you compile it like
javac ShowSourceImpact.java and run it you'll see:
'i' is: null
'i' should be: 1
When you compile it with
javac -source 1.4 ShowSourceImpact.java you'll get the following:
'i' is: 1
'i' should be: 1
So the problem is when you have an
inner class and you overwrite its methods in such a way that they rely on
final variables declared in the outer method. See
createI() method in the example above.
For "1.3" source code it seems that the value of these final variables are null inside the inner class. This works ok with "1.4" source.
I thought that the "-source" 1.3 vs 1.4 vs 1.5 was only about the new keywords introduced to the language. However with the example above it looks there is more to that.
Anyone has any good explanation for that?
MDB+Struts+Spring+Hibernate - the WebLogic way
Pretty heavy - the title. And the post is pretty long too. But I must say it was quite a challenge to put all these pieces together.
Struts+Spring+Hibernate is not exceptional if not the fact that I didn't find any good info on how to use Message-Driven Beans with Spring. It seems that MDBs remain the only thing for which Spring doesn't have an answer. If you need asynchronous, guaranteed delivery calls, you'll need MDB.
Spring contains abstract classes that should help you to develop your MDB. There is one problem though. Let's say you want your MDBs to rely on your application functionality that you assembled together in your main "applicationContext.xml". Let's say also that you want to have a pool 3 MDBs that can grow up to 20.
If you follow the Spring documentation you'll end up with, at the application startup, 3 duplicate contexts because each bean instance will load its own, separate context. For a small app this is not a problem. For a bigger application it may become one. But don't worry - there is a solution to that. It's called ContextSingletonBeanFactoryLocator.
But wait, it's not over yet.
Now imagine that your presentation layer relies on your core functionality captured in the same "applicationContext.xml". Ahh, I forgot to say - your requirement is to use Struts for MVC piece. And you want Hibernate to be lazy in all layers (load associated objects on demand only). And last, but not least, this all should run on WebLogic Server.
Below are all the ingredients that I used to make the whole thing work. Note that I'm a Spring newbie and I'm pretty sure there must be an easier way to do this. Looking forward for your comments.
If you want to continue be prepared for: classloaders' hell, Spring hacking, more than one Spring beans XML file ... But you'll be awarded - at the end it all seems to work fine together!!!
EAR layout is as follows:
- APP-INF/lib - Hibernate and Spring jars; a jar (myCore.jar) file with all my objects except the presentation piece and MDBs
- META-INF - usual .ear descriptors
- myMdbs.jar - EJB jar file containing my message-driven beans
- myWeb.war - my presentation layer classes plus Struts
"APP-INF/lib" is a special WebLogic directory where you can put all supporting libraries and all your classes that should be shared by your EJBs and WAR elements. From what I understood, the classloader for this directory is a parent classloader for the EJB classloader. And EJB classloader is parent for WAR classloader. This means that any class that is loaded by APP-INF/lib classloader cannot reference any other class that is present either in WAR/WEB-INF/lib or EJB. See where I'm heading? Ok, I'll give you a hint -
OpenSessionInViewFilter defined in spring-orm.jar. More details in a second.
The first implication of the classloader hierarchy is that you cannot use "spring.jar" and just drop it to "APP-INF/lib". Your "APP-INF/lib" will contain "spring-(core,aop,context,dao,orm).jar" and "spring-web.jar" must go to WAR's "WEB-INF/lib".
Message-driven beans inheriting from "AbstractJmsMessageDrivenBean" can specify, in "ejb-jar.xml" file, an "env-entry" to point to the "applicationContext.xml" file. But as I said, using this approach and having several MDBs, I ended up with several copies of the application context loaded.
ContextSingletonBeanFactoryLocator is the rescue here. I had to overwrite the
ejbCreate() as follows:
public void ejbCreate() {
setBeanFactoryLocatorKey("MyApp");
setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
super.ejbCreate();
}In this case Spring will look for XML context file using the following resource path "classpath:beanRefContext.xml". In my case this file is located in "myCore.jar" file and contains only a single bean called "MyApp" that defines the real context:
<beans>
<bean id="MyApp" class="org.springframework.context.support.ClassPathXmlApplicationContext">
<constructor-arg>
<list><value>applicationContext.xml</value></list>
</constructor-arg>
</bean>
</beans>
To sum up, now I have Spring application context shared by all my MDBs.
Hibernate laziness in the view requires to use
OpenSessionInViewFilter. In order to use this filter you have to also use
ContextLoaderListener, as the filter relies on a web application context set as ServletContext's attribute (webapp application scope). In the web tier I also wanted to use (share) the same Spring application context as the one used by my MDBs. To achieve that, in addition to the normal listener declaration, you need to define some "context-param" in your web.xml file:
<context-param>
<param-name>locatorFactorySelector</param-name>
<param-value>classpath:beanRefContext.xml</param-value>
</context-param>
<context-param>
<param-name>parentContextKey</param-name>
<param-value>MyApp</param-value>
</context-param>
You think "ouf, almost there..."? Well, not quite yet. You try to run this whole thing and you're getting a misterious "NoClassDefFoundError: org.springframework.web.filter.OncePerRequestFilter" (or maybe it was an ...Exception, don't remember). Then you look into your "web.xml" file - "But I don't use OncePerRequestFilter, so what the hell is going on?".
Well, the problem is that
OpenSessionInViewFilter is part of "spring-orm.jar" - located in "APP-INF/lib" directory and loaded with the parent classloader to rule them all. But this class extends
OncePerRequestFilter being part of "spring-web.jar" located in WAR's "/WEB-INF/lib" directory. As parent classloader cannot load classes available only to its child classloaders it seems like no way out. And I didn't find any, apart from hacking Spring jars by moving this filter class from "spring-orm.jar" to "spring-web.jar" - and I believe that's where it should be anyway.
There is one final problem. Even if you don't have any beans defined yet in the presentation layer,
ContextLoaderListener will not start if it doesn't have "/WEB-INF/applicationContext.xml" file. This file can be empty (i.e.
<beans></beans>) but it must be there.
Struts integration is actually pretty nice. You can find more info
here. As I have to use a custom request processor I opted for
DelegatingActionProxy approach. I hoped to put my action IoC config in the "/WEB-INF/applicationContext.xml" file so this file could become useful, but it seems that
DelegatingActionProxy relies on web application context bound as another ServletContext attribute (different name than the context bound by
ContextLoaderListener). This one is set by
ContextLoaderPlugIn defined in "struts-config.xml" file:
<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">
<set-property property="contextConfigLocation" value="/WEB-INF/strutsContext.xml" />
</plug-in>
The "stutsContext.xml" file contains my actions' IoC configuration.
... and that's it. Congratulations! You accomplished your mission successfully!!!
To sum up the main issues that I found were:
- Hibernate filter class is defined in "spring-orm.jar" but I believe it should be defined in some other jar belonging to the web tier.
- I probably still don't understand the Spring application contexts relationships and the use of
ContextSingletonBeanFactoryLoader but for me there were too many bean files (3 and 1 empty) while I only really needed 2 - one for my business and DAO layer, and one for presentation layer.
- From the Spring reference doc I understood that whether you use
ContextLoaderFilter or ContextLoaderPlugIn for your Struts integration shouldn't really matter. I didn't managed to get rid of the the plug-in though.
- I didn't find a good explanation of the use of
ContextSingletonBeanFactoryLocator - it was more like debug and reverse engineering type of work to get it working
The most important though is that at the end it all worked smoothly toghether.
As I said at the beginning - I'm a Spring rookie, so please advise on how to put together all the components mentioned here but in a simpler way.
let's get it started!
Yep. Here it is. My first post. I'm a Java developer and this blog will be about what I know about Java and, more important, what I don't. I learn a lot from others' blogs. Nothing starts better a day than a good read at JavaBlogs. "So many questions, so little time..." It takes time to get answers if you don't know where to look or if you have nobody to ask ;-) I hope this blog will help me to get those answers. And you never know, maybe others will find useful information on this blog too.