OpenMRS AOP

Aspect Oriented Programming is meant to aid programmers with cross-cutting programming concerns. Typically, you'll find AOP being used for logging, authorization, and authentication. See Wikipedia's AOP entry Aspect-oriented programming

AOP Example: Logging

Imagine we want to log the start and end of every method and optionally record the time it took to execute every method.

The bold lines below are extraneous and can be pulled out into an AOP method.

public void someMethod() {
log.debug("Starting method 'someMethod'");
int startTime = new Date().getTime();

...(actual method code here)...

log.debug("Ending method 'someMethod');
int endTime = new Date().getTime();
log.trace("someMethod execution time: " + endTime - startTime + " ms");
}
Alternatively you can pull out that logging code to keep the actual method clean:public void someMethod() {
...(actual method code here)...
}

...(setup of AOP in external xml file)...

public void aroundAdvice(Method m) {
log.debug("Starting method " + m.getName());
int startTime = new Date().getTime();

invocation.proceed();

log.debug("Ending method " + m.getName());
int endTime = new Date().getTime();
log.trace(m.getName() + " execution time: " + endTime - startTime + " ms");
}

AOP in a Nutshell

There are two requirements for AOP to work: 1) Your code must be operating off of an Interface instead of an actual concrete implementation class 2) You must be retrieving your objects from a central place ("no calls to 'new', aka new PersonServiceImpl());

AOP in OpenMRS

OpenMRS uses AOP mainly for the module system to allow the core API (Services) to be extended in an infinite number of ways. Instead of only providing hooks at certain points in the code for specific uses, every service method within the API can be wrapped by a method in a module. Module services can be extended via AOP as well.

There are three ways to wrap an API method:

  • before
    • The module's method will be called before the api method is called.
    • The module's method will receive all parameters that the api method is receiving.
  • after
    • The module's method will be called after the api method is called.
    • The module's method will receive all parameters that the api method received
    • The module's method will receive the return value of the api method
    • The module has the option of returning a different value to whomever called the api.
  • around
    • The module's method will be called before the api method is called.
    • The module's method will receive all parameters that the api method is set to receive
    • The module's method has the option of invoking the api method or not.
    • The module's method can receive the return value of the api method (if invoked).
    • The module's method can return either the value of the api method or another arbitrary value.

Example

AOP advice is added on a class basis. Lets say we want our method to be called after the PatientService.getPatient(Integer) method. We need to first create an advice class.

package org.openmrs.module.helloWorld.advice;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.AfterReturningAdvice;

public class CountingAfterAdvice implements AfterReturningAdvice {

private Log log = LogFactory.getLog(this.getClass());

private int count = 0;

public void afterReturning(Object returnValue, Method method, Object\[\] args, Object target) throws Throwable {
		if (method.getName().equals("getPatient"))
			log.debug("Method: " + method.getName() + ". After advice called " + (++count) + " time(s) now.");
	}

}

This class will be wrapped around the entire PatientService class. Every API call in PatientService will now pass through our class after returning. The afterReturning method distinguishes between different method calls using the java.lang.reflect.Method class.

In order to wrap this advice class around the PatientService, we add an entry in the module's /metadata/config.xml file:

<advice>
<point>org.openmrs.api.PatientService</point>
<class>org.openmrs.module.helloWorld.advice.CountingAfterAdvice</class>
</advice>
Before advice works in a very similar way. The only difference is that the org.springframework.aop.MethodBeforeAdvice interface is implemented instead of org.springframework.aop.AfterReturningAdvice.

Around Advice

Around advice is the most powerful type of AOP that you can do. The around advice classes need to implement org.springframework.aop.Advisor. An example of the most basic type:

package org.openmrs.module.helloWorld.advice;

import java.lang.reflect.Method;

import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;

public class PrintingAroundAdvisor extends StaticMethodMatcherPointcutAdvisor implements Advisor {

private static final long serialVersionUID = 3333L;

private Log log = LogFactory.getLog(this.getClass());

public boolean matches(Method method, Class targetClass) {
		// only 'run' this advice on the getter methods
		if (method.getName().startsWith("get"))
			return true;

		return false;
	}

@Override
public Advice getAdvice() {
		log.debug("Getting new around advice");
		return new PrintingAroundAdvice();
	}

private class PrintingAroundAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {

			log.debug("Before " + invocation.getMethod().getName() + ".");

			// the proceed() method does not have to be called
			Object o = invocation.proceed();

			log.debug("After " + invocation.getMethod().getName() + ".");

			return o;
		}
}

}

Note that in this example, invocation.proceed() passes all arguments to the original service class. If you don't want the original service class methods to be called (methods starting with "get" in this example), simply omit invocation.proceed(), and return a dummy object, or you can call invocation.proceed() conditionally.

Around advice uses the same metadata in config.xml as after and before advice -- the point tag specifies which service api to wrap around, and the class tag allows you to specify your advice class (PrintingAroundAdvisor, in this case).

Also, since the spring 2.5 upgrade, make sure that your moduleApplicationContext file's basicUrlMapping has the "order" property set to a value lower than 99 (if this property ever disappears from openMRS's core basicUrlMapping, disregard this). If this is not set, advice may not be loaded properly.

Programatically Using AOP in Core OpenMRS Code

Core OpenMRS code can add AOP methods around the services as well. There are methods on `Context` that add either Advice or an Advisor. (An advisor is used for 'around' AOP, and an advice object is for 'before'/'after' AOP.)

Context.addAdvisor(Class cls, Advisor advisor)
Context.removeAdvisor(Class cls, Advisor advisor)
Context.addAdvice(Class cls, Advice advice)
Context.removeAdvice(Class cls, Advice advice)

A word of caution if using AOP as an Event Mechanism between modules (by Chica Team)

There is no existing API in the OpenMRS framework for an event bus. Therefore, to be able to publish and subscribe to events among our modules we used AOP as an event mechanism for Chica implementation. Chica implementation relies on "real time" events such as a file created in a folder or a new patient being created from a HL7 checkin.

In our use case we found soon that methods invoked using AOP are:

1. Not thread safe automatically by virtue of being called from Spring. There is no queuing mechanism at Spring level and therefore one needs to be cautious and protect with synchronized blocks to prevent data corruption from successive AOP calls.

2.If you start new worker thread(s) in the invoked AOP method, do not return from the method call after starting the threads. You need to wait for the thread(s) to finish, otherwise your spawned threads will not live.