Monday, March 29, 2010

ColdSpring Lite

Most of the projects I work on, including my recently released framework ColdMVC, use ColdSpring to manage their components. I honestly can't imagine starting a new project without ColdSpring -- it's that important. However, the development process can be a drag at times having to constantly reload the bean factory in order to see your changes.

While the typical response to the slow start-up time is "your application only loads once so it's not a big deal", that's really only applicable in a production environment. As a developer, my application is loaded constantly throughout the day, so it is a big deal to me. Rather than just complain about it, I decided to see if I could speed things up. Which is why I wrote my own ColdSpring Lite bean factory.

I took an approach similar to what Facebook did when they wrote HipHop: sacrifice some rarely used features in exchange for improved performance. With that in mind, I had to decide which features were necessary and which features I could live without.

Here's what it supports:

  • property injection

  • value, list, and map definitions

  • dynamic properties (i.e. ${property})

  • factory and bean post processors

  • autowiring



Here's what it doesn't support:

  • constructor arguments

  • complex nested bean injection (arrays of bean ref's)

  • bean aliases

  • parent beans

  • non-singleton beans

  • aop proxies

  • remote proxies

  • factory beans

  • xml imports



Will this limited feature set work for every project? Of course not. Will it be sufficient enough for most projects? Maybe.

Compared to ColdSpring's DefaultXmlBeanFactory, I cut the bean factory load time almost in half, which is pretty good if you ask me. Granted the load time is pretty dependent on the size of your application, so the performance improvements could vary between projects.

Here's the code if anyone is interested. I've also included a slightly modified version inside ColdMVC as well.


component {

public any function init(required string filePath, struct config) {

beanDefinitions = {};
beanInstances = {};
factoryPostProcessors = [];
beanPostProcessors = [];

var xml = fileRead(filePath);

if (structKeyExists(arguments, "config")) {
var setting = "";
for (setting in config) {
xml = replaceNoCase(xml, "${#setting#}", config[setting], "all");
}
}

loadBeans(xmlParse(xml));

return this;

}

private void function loadBeans(required xml xml) {

var i = "";

for (i=1; i <= arrayLen(xml.beans.xmlChildren); i++) {

var xmlBean = xml.beans.xmlChildren[i];

var bean = {
id = xmlBean.xmlAttributes.id,
class = xmlBean.xmlAttributes.class,
constructed = false,
autowired = false,
properties = {}
};

for (j=1; j <= arrayLen(xmlBean.xmlChildren); j++) {
bean.properties[xmlBean.xmlChildren[j].xmlAttributes.name] = xmlBean.xmlChildren[j].xmlChildren[1];
}

if (getXMLAttribute(xmlBean, "factory-post-processor", false)) {
arrayAppend(factoryPostProcessors, bean.id);
}

if (getXMLAttribute(xmlBean, "bean-post-processor", false)) {
arrayAppend(beanPostProcessors, bean.id);
}

beanDefinitions[bean.id] = bean;

}

processFactoryPostProcessors();

}

private string function getXMLAttribute(required xml xml, required string key, string def="") {

if (structKeyExists(xml.xmlAttributes, key)) {
return xml.xmlAttributes[key];
}
else {
return def;
}

}

private void function processBeanPostProcessors(required any bean, required string beanName) {

var i = "";

for (i=1; i <= arrayLen(beanPostProcessors); i++) {
var postProcessor = getBean(beanPostProcessors[i]);
postProcessor.postProcessAfterInitialization(bean, beanName);
}

}

private void function processFactoryPostProcessors() {

var i = "";

for (i=1; i <= arrayLen(factoryPostProcessors); i++) {
var postProcessor = getBean(factoryPostProcessors[i]);
postProcessor.postProcessBeanFactory(this);
}

}

public boolean function containsBean(required string beanName) {
return structKeyExists(beanDefinitions, beanName);
}

public any function getBean(required string beanName) {

var beanDef = beanDefinitions[beanName];

if (!beanDef.constructed) {
constructBean(beanName);
}

return beanInstances[beanName];

}

private void function constructBean(required string beanName) {

var property = "";
var i = "";

var dependencies = findDependencies(beanName, beanName);

for (i=1; i <= listLen(dependencies); i++) {

var beanDef = beanDefinitions[listGetAt(dependencies, i)];

lock name="BeanFactory.constructBean.#beanDef.id#" type="exclusive" timeout="5" throwontimeout="true" {

if (!beanDef.constructed) {

var beanInstance = getBeanInstance(beanDef.id);
var functions = findFunctions(beanDef.id);

if (structKeyExists(functions, "setBeanFactory")) {
beanInstance.setBeanFactory(this);
}

if (structKeyExists(functions, "setBeanName")) {
beanInstance.setBeanName(beanDef.id);
}

for (property in beanDef.properties) {
var value = parseProperty(beanDef.properties[property]);
evaluate("beanInstance.set#property#(value)");
}

beanDef.constructed = true;

processBeanPostProcessors(beanInstance, beanDef.id);

}

}

}

}

private void function addAutowiredProperties(required string beanName) {

var beanDef = beanDefinitions[beanName];

if (!beanDef.autowired) {

var functions = findFunctions(beanName);
var func = "";

for (func in functions) {

if (left(func, 3) == "set") {

var property = replaceNoCase(func, "set", "");

if (!structKeyExists(beanDef.properties, property) && containsBean(property)) {

var xml = xmlNew();
xml.xmlRoot = xmlElemNew(xml, "ref");
xml.xmlRoot.xmlAttributes["bean"] = property;

beanDef.properties[property] = xml.xmlRoot;

}

}

}

beanDef.autowired = true;

}

}

private any function getBeanInstance(required string beanName) {

lock name="BeanFactory.getBeanInstance.#beanName#" type="exclusive" timeout="5" throwontimeout="true" {

if (!structKeyExists(beanInstances, beanName)) {

beanInstances[beanName] = createObject("component", beanDefinitions[beanName].class);

if (structKeyExists(beanInstances[beanName], "init")) {
beanInstances[beanName].init();
}

}

}

return beanInstances[beanName];

}

private struct function findFunctions(required string beanName) {

var beanDef = beanDefinitions[beanName];

if (!structKeyExists(beanDef, "functions")) {

var metaData = getComponentMetaData(beanDef.class);
var functions = {};
var access = "";
var i = "";

while (structKeyExists(metaData, "extends")) {

if (structKeyExists(metaData, "functions")) {

for (i=1; i <= arrayLen(metaData.functions); i++) {

if (structKeyExists(metaData.functions[i], "access")) {
access = metaData.functions[i].access;
}
else {
access = "public";
}

if (!structKeyExists(functions, metaData.functions[i].name)) {
if (access != "private") {
functions[metaData.functions[i].name] = access;
}
}

}

}

metaData = metaData.extends;

}

beanDef.functions = functions;

}

return beanDef.functions;

}

private string function findDependencies(required string beanName, required string dependencies) {

addAutowiredProperties(beanName);

var beanDef = beanDefinitions[beanName];
var property = "";

for (property in beanDef.properties) {

var xml = beanDef.properties[property];

if (xml.xmlName == "ref") {

var dependency = xml.xmlAttributes.bean;

if (!listFindNoCase(dependencies, dependency)) {
dependencies = listAppend(dependencies, dependency);
dependencies = findDependencies(dependency, dependencies);
}

}

}

return dependencies;

}

private any function parseProperty(required xml xml, struct result) {

var i = "";

if (!structKeyExists(arguments, "result")) {
arguments.result = {};
}

switch(xml.xmlName) {

case "property": {
result[xml.xmlAttributes.name] = parseProperty(xml.xmlChildren[1], result);
break;
}

case "value": {
return xml.xmlText;
}

case "list": {

var array = [];

for (i=1; i <= arrayLen(xml.xmlChildren); i++) {
var value = parseProperty(xml.xmlChildren[i], result);
arrayAppend(array, value);
}

return array;

}

case "map": {

var struct = {};

for (i=1; i <= arrayLen(xml.xmlChildren); i++) {
var value = parseProperty(xml.xmlChildren[i].xmlChildren[1], result);
struct[xml.xmlChildren[i].xmlAttributes.key] = value;
}

return struct;

}

case "ref": {
return getBeanInstance(xml.xmlAttributes.bean);
}

default: {

for (i=1; i <= arrayLen(xml.xmlRoot.xmlChildren); i++) {
parseProperty(xml.xmlRoot.xmlChildren[i], result);
}

}

}

return result;

}

}

Sunday, March 28, 2010

Implementing findWhere() and findAllWhere() in ColdMVC

I posted this on the ColdMVC Google Group, but since the group isn't very active yet, I figured I'd post it here too for more visibility. Basically I'm looking for some feedback on implementing the findWhere() and findAllWhere() methods inside ColdMVC. For convenience sake, here's the post from the Google Group:

I haven't fully implemented the findWhere() and findAllWhere() methods for models because I haven't yet decided how it should be done in all circumstances. In Grails, it looks like this:

def book = Book.findWhere(title:"The Shining", author:"Stephen King")

This is easy enough to convert to ColdFusion, but I see 2 possible ways of achieving the same thing.

Option 1: Pass in multiple arguments, with each argument being a property on the model.

params.book = _Book.findWhere(title="The Shining", author="Stephen King");

Option 2: Pass in a single struct argument, with each key in the struct being a property on the model.

params.book = _Book.findWhere({title="The Shining", author="Stephen King"});

I think the first option more closely resembles Grails, which is nice. Plus it's a little cleaner. However, I think I prefer option 2 more when you start adding paging parameters (offset, max, sort, order) to findAllWhere(). For example:

params.books = _Book.findAllWhere({author="Stephen King"}, {max="10", sort="datePublished", order="desc"});

Having the constraint of only accepting 1 or 2 arguments would greatly simplify the code that generates the HQL, plus you no longer have to worry about naming conflicts between paging paramaters and model properties.

Also, I'd like some opinions on how to handle various operators (like, startsWith, endsWith, etc...). In Grails, you can use closures to generate the HQL like such:

def books = Book.createCriteria().list(max: 5, offset: 10) {
like("title","foo%")
}

Since closures aren't available in ColdFusion (yet), we need to figure out our own syntax. I think the most natural way of handling operators would be to make them available to the findWhere() and findAllWhere() methods. Here are a couple ways I see this working:

Option 1: Use an array where the first item is the operator and the second item is the value.

params.books = _Book.findAllWhere({
title = [ "like", "foo" ]
}, {
max="5",
offset="10"
});

Option 2: Use a struct with operator and value keys.

params.books = _Book.findAllWhere({
title = { operator="like", value="foo" }
}, {
max="5",
offset="10"
});

Option 3: Use a struct where the key is the operator and the value is... the value.

params.books = _Book.findAllWhere({
title = { like="foo" }
}, {
max="5",
offset="10"
});

I don't think any of the options would be that hard to implement, so it's really a matter of preference. I think right now I'm leaning towards option 1, although I wouldn't be against the other options either.

Any thoughts or comments are appreciated.

Also, here's a Grails reference if you're interested:
http://www.grails.org/DomainClass+Dynamic+Methods

Friday, March 12, 2010

ColdMVC Available on GitHub

I recently wrote that I've been working on a new convention-based MVC framework for ColdFusion 9 that features Hibernate ORM. I'm happy to announce that it's now available on GitHub for those who want to check it out.

I'll admit, documentation for how to use the framework is pretty limited at the moment. If you're feeling adventurous and want to try things out, I've included two small sample applications to look at. Nothing too spectacular, but hopefully they can shed some light on how things work.

Other than that, I'm always happy to answer questions via my blog or email. Also, I decided to create a Twitter account to make myself more available (even though I hate Twitter).

As a final disclaimer, please be aware that ColdMVC shouldn't be considered stable yet, so things might change as time goes on.

Update: I've also created a Google group for anyone that has questions.

Monday, March 1, 2010

ColdFusion 9 40% Faster? I doubt it...

Adobe recently released a performance brief claiming ColdFusion 9 is 40% faster than ColdFusion 8. While that number looks really good at first glance, Marc Ackermann was kind enough to point out that they were running ColdFusion 8 using the 1.6.0_04 version of Java, which has a known class loader bug.

While I don't doubt that ColdFusion 9 is faster than ColdFusion 8, I don't trust any of the numbers from the performance brief, especially a 700% improvement in CFC object creation.

Going from IIS to Apache and Other Things...

Even though it seems that everybody and their grandma uses Apache, I've always used IIS. So after reading a recent post by Matt Woodward called Moving From IIS To Apache: It's Easier Than You Think, I figured I'd give Apache a shot and see how easy it really is to get set up.

And you know what? It was actually much easier than I thought it would be. After about 15 minutes, I had my local workspace up and running on Apache. To top it off, I also added a little URL re-writing to get rid of that pesky index.cfm reference in my ColdMVC sample blogging application. I won't take full credit for figuring out how to do it (I found the solution in a comment on a forum after a little Googling), but here's what worked for me:


<VirtualHost *:80>
ServerName blog.local
DocumentRoot "c:/workspace/coldmvc/samples/blog/public"
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !^.*?\.[^/]{2,5}$
RewriteRule (.*) /index.cfm$1 [PT]
<Directory "c:/workspace/coldmvc/samples/blog/public">
Order allow,deny
Allow from all
</Directory>
</VirtualHost>


And in the spirit of learning new things, I also just finished reading Programming Ruby 1.9: The Pragmatic Programmers' Guide. I played around with Ruby on Rails a couple years ago, but at the time I didn't know anything about Ruby, so I wasn't able to fully grasp what was going on. After reading up on the language, I have to admit there are a lot of really nice features that ColdFusion could borrow from Ruby - blocks, closures, and variable naming rules to name a few. I'm also really jealous of how includes are implemented in Ruby, where they're treated almost like superclasses, so you don't have to worry about conflicts between method names.

Now I'm not saying I'm going to give up on ColdFusion, but there's definitely room for improvement in the language. Is there a technical limitation as to why ColdFusion variables can't start with @ or :? Is there a reason why method names can't end in ? or =? Do we really need to have parenthesis and semi-colons all over the place? I know much of ColdFusion is based on Java conventions and rules, but there's a reason why I'd rather code in CF than Java - it's easier. Why not embrace that, stray away from the Java roots, and make ColdFusion that much more enjoyable to work with.

Ok that's enough ranting for now. Next on my to-do list is to figure out Git and GitHub so I can get my framework out there for people to try out.