Intercepting Methods
To understand when to use interceptors, we'll look at some modified code of our TravelAgent EJB's bookPassage( ) method. The application developer of this code has added some profiling logic to time how long the bookPassage( ) method takes to be invoked. It is as simple as polling the current time at the start of the method and printing out the time it took at the end of it within a finally block:
@Remove
public TicketDO bookPassage(CreditCardDO card, double price)
throws IncompleteConversationalState {
long startTime = System.currentTimeMillis( );
try {
if (customer == null || cruise == null || cabin == null) {
throw new IncompleteConversationalState( );
}
try {
Reservation reservation = new Reservation(
customer, cruise, cabin, price, new Date( ));
entityManager.persist(reservation);
process.byCredit(customer, card, price);
TicketDO ticket = new TicketDO(customer, cruise, cabin, price);
return ticket;
} catch(Exception e) {
throw new EJBException(e);
}
} finally {
long endTime = System.currentTimeMillis( ) - startTime;
System.out.println("bookPassage( ) took: " + endTime + " (ms)");
}
}
Although this code will compile and run fine, it has a lot of design flaws:
The bookPassage( ) method is polluted with code that has nothing to do with the business logic of the method. Not only has the developer added six lines of code to the method, but he has also made the code a bit harder to read. It is difficult to turn profiling off and on since you have to comment out the code and recompile the bean class. This profiling logic is obviously a template that could be reused across many methods of your application. If this profiling code is littered throughout your EJB methods, you would need to potentially modify a lot of different classes to expand the functionality of your profiling logic.
Interceptors provide a mechanism to encapsulate this profiling logic and an easy way to apply it to your methods without making the code harder to read. Interceptors provide a structure for this type of behavior so that it can easily be extended and expanded in one class. Finally, they provide a simple, configurable mechanism for applying their behavior wherever you like.
Interceptor Class
Encapsulating this profile logic into an interceptor is as simple as creating a plain Java class that has a method with the @javax.interceptor.AroundInvoke
annotation and the following signature:
@AroundInvoke
Object <any-method-name>(javax.interceptor.InvocationContext
invocation)
throws Exception;
The @AroundInvoke method in an interceptor class
does just what it implies. It wraps around the call to your business method and is actually invoked in the same Java call stack and in the same transaction and security context as the bean method it is intercepting. The javax.interceptor.InvocationContext parameter is a generic representation of the business method the client is invoking. You can obtain information such as the target bean instance on which you are invoking, access to its parameters expressed as an array of objects, and a reference to a java.lang.reflect.Method
object that is the generic representation of the actual invoked method. InvocationContext is also used to drive the invocation. Let's convert our profiling logic into an @AroundInvoke method:
1 import javax.ejb.*;
2
3 public class Profiler {
4 @AroundInvoke
5 public Object profile(InvocationContext
invocation) throws
Exception {
6 long startTime = System.currentTimeMillis( );
7 try {
8 return invocation.proceed( );
9 } finally {
10 long endTime = System.currentTimeMillis( ) - startTime;
11 System.out.println("Method " + invocation.getMethod( )
12 + " took " + endTime + " (ms)");
13 }
14 }
15 }
The @AroundInvoke method of our interceptor class is the profile( ) method. It looks pretty much the same as the code in our bookPassage( ) method, except that the business logic is gone and all that is left is our generic profiling logic. In Line 8, the InvocationContext.proceed( ) method is called. If another interceptor must be invoked as part of the method call, then proceed( ) calls the @AroundInvoke method of that other interceptor. If no other interceptors need executing, then the EJB container calls the bean method on which the client is invoking. Because the profile( ) method is invoked in the same Java call stack as the business method on which you are invoking, proceed( ) must be called by the interceptor code or the actual EJB method is not called at all.
In Lines 10 and 11, the profile( ) method calculates the execution time and prints out the time the method took to execute. The InvocationContext.getMethod( ) operation gives the profile( ) code access to the java.lang.reflect.Method
object that represents the actual bean method being invoked. It is used in Line 11 to print out the name of the method being called. Besides getMethod( ), the InvocationContext interface has some other interesting methods:
package javax.interceptor;
public interface InvocationContext {
public Object getTarget( );
public Method getMethod( );
public Object[] getParameters( );
public void setParameters(Object[] newArgs);
public java.util.Map<String, Object> getContextData( );
public Object proceed( ) throws Exception;
}
The getTarget( ) method returns a reference to the target bean instance. We could change our profile( ) method to also print out the parameters of the invoked bean method by using the getParameters( ) method. The setParameters( ) method allows you to actually modify the parameters of the method that is being invoked. Use this with care. The getContextData( ) method returns a Map object that is active for the
entire method invocation. Interceptors can use this map to pass contextual data between each other within the same method invocation.
Applying Interceptors
Now that the interceptor class has been written, it is time to apply it to an EJB. One or more interceptors can be applied to all EJBs within a deployment (default interceptors), to all methods of one EJB, or to a single method of an EJB. Interceptors can be applied via an annotation or by using an XML deployment descriptor. We'll discuss all options in this section.
Annotated methods and classes
The @javax.interceptor.Interceptors
annotation can be used to apply interceptors to individual methods or to every method in the EJB's bean class:
package javax.interceptor;
import java.lang.annotation.*;
@Retention(RetentionType.RUNTIME)
@Target({ElementType.CLASS, ElementType.METHOD})
public @interface Interceptors {
Class[] value( );
}
Profiling the bookPassage( ) method of our TravelAgent EJB is as easy as applying
the @Interceptors
annotation:
@Remove
@Interceptors(Profiler.class)
public TicketDO bookPassage(CreditCardDO card, double price)
throws IncompleteConversationalState {
if (customer == null || cruise == null || cabin == null) {
throw new IncompleteConversationalState( );
}
try {
Reservation reservation = new Reservation(
customer, cruise, cabin, price, new Date( ));
entityManager.persist(reservation);
process.byCredit(customer, card, price);
TicketDO ticket = new TicketDO(customer, cruise, cabin, price);
return ticket;
} catch(Exception e) {
throw new EJBException(e);
}
}
When the @Interceptors annotation is applied to an individual method, that interceptor is executed only when that particular method is invoked. If you use the
@Interceptors
annotation on the bean class, all interceptor classes listed will interpose on every method invocation of every business method of the EJB:
@Stateful
@Interceptors(Profiler.class)
public class TravelAgentBean implement TravelAgentRemote {
@Remove
public TicketDO bookPassage(CreditCardDO card, double price)
throws IncompleteConversationalState {
...
}
...
}
Whether you want to apply the @Interceptors annotation on the class or the method is really determined by the interceptor you are using.
Applying interceptors through XML
Although the @Interceptors annotation allows you to apply the profiling interceptor easily, it does force you to modify and recompile your class every time you want to remove profiling from or add it to a particular method or EJB. Unless an interceptor is part of your business logic, it may not be the best idea to annotate your code; using XML bindings instead might be a better approach. Because the EJB 3.0 specification supports partial XML deployment descriptors, it is quite painless and easy to apply an interceptor through XML:
<ejb-jar
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
version="3.0">
<assembly-descriptor>
<interceptor-binding>
<ejb-name>TravelAgentBean</ejb-name>
<interceptor-class>com.titan.Profiler</interceptor-class>
<method-name>bookPassage</method-name>
<method-params>
<method-param>com.titan.CreditCardDO</method-param>
<method-param>double</method-param>
</method-params>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
The preceding XML is a complete deployment descriptor. The <interceptor-binding> element specifies that we want the Profiler interceptor to be executed whenever the bookPassage( ) method of the TravelAgent EJB is invoked. Because the bookPassage( ) method is not overloaded in the bean class, the <method-params> element isn't really required.
If you want to apply an interceptor to every business method of a particular EJB, then leave out the <method-name> and <method-params> elements:
<ejb-jar
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
version="3.0">
<assembly-descriptor>
<interceptor-binding>
<ejb-name>TravelAgentBean</ejb-name>
<interceptor-class>com.titan.Profiler</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
As you can see, you can use an XML deployment descriptor only when you need it and leave the rest of the EJB metadata expressed as annotations. In this particular interceptor use case, it makes more sense to use XML than an annotation because profiling is probably something you'd want to do in development, not in a production application.
Default interceptors
XML has some other advantages as well. For instance, the <ejb-name> element in an <interceptor-binding> can take a wildcard. In this case, you are applying one or more interceptors that you declare in the interceptor-binding to every EJB in that particular JAR file deployment:
<ejb-jar
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
version="3.0">
<assembly-descriptor>
<interceptor-binding>
<ejb-name>*</ejb-name>
<interceptor-class>com.titan.Profiler</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
Disabling interceptors
If you are using default
interceptors or class-level interceptors, there may be times when you want to disable them for a particular EJB or for a particular method of an EJB. You can do this via an annotation or through XML. Let's look at disabling
default interceptors first:
<ejb-jar
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
version="3.0">
<assembly-descriptor>
<interceptor-binding>
<ejb-name>*</ejb-name>
<interceptor-class>com.titan.Profiler
</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
In the preceding XML, we have enabled the Profiler interceptor for every EJB deployed in the particular JAR file in which the XML is placed. Let's say we do not want the Profiler to be executed for our TravelAgent EJB. We can turn off all default interceptors by using the @javax.interceptor.ExcludeDefaultInterceptors
annotation:
@Stateful
@ExcludeDefaultInterceptors
@Interceptors
(com.titan.SomeOtherInterceptor.class)
public class TravelAgentBean implements TravelAgentRemote {
...
}
In the preceding example, the Profiler will not be executed. Just because the @ExcludeDefaultInterceptors annotation has been used, it does not mean we cannot specify an @Interceptors annotation that triggers other interceptor classes. This exclusion can be done within XML as well:
<ejb-jar
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
version="3.0">
>
<interceptor-binding>
<ejb-name>*</ejb-name>
<interceptor-class>com.titan.Profiler</interceptor-class>
</interceptor-binding>
<interceptor-binding>
<ejb-name>TravelAgentBean</ejb-name>
<exclude-default-interceptors/>
<interceptor-class>com.titan.SomeOtherInterceptor</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
What we're really doing in the preceding examples is giving the TravelAgent EJB a brand-new interceptor stack that overrides and supercedes any default interceptors.
The same overriding and disabling of interceptors can be done at the method level as well. You can turn off interceptors entirely for a particular method by using the @javax.interceptor.ExcludeDefaultInterceptors
and @javax.interceptor.ExcludeClassInterceptors
annotations:
@Stateful
@Interceptors
(com.titan.SomeOtherInterceptor.class)
public class TravelAgentBean implements TravelAgentRemote {
...
@Remove
@ExcludeClassInterceptors
@ExcludeDefaultInterceptors
public TicketDO bookPassage(CreditCardDO cc, double amount) {
...
}
...
}
The @ExcludeClassInterceptors annotation turns off any applied class-level interceptors, and the @ExcludeDefaultInterceptors annotation turns off any default interceptors defined in XML. You could also specify an @Interceptors annotation on the bookPassage( ) method to define a different interceptor stack compared to the rest of the methods in the bean class. This is also available in XML format:
<ejb-jar
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
version="3.0">
<assembly-descriptor>
<interceptor-binding>
<ejb-name>*</ejb-name>
<interceptor-class>com.titan.Profiler</interceptor-class>
</interceptor-binding>
<interceptor-binding>
<ejb-name>TravelAgentBean</ejb-name>
<interceptor-class>com.titan.SomeOtherInterceptor</interceptor-class>
</interceptor-binding>
<interceptor-binding>
<ejb-name>TravelAgentBean</ejb-name>
<exclude-default-interceptors/>
<exclude-class-interceptors/>
<interceptor-class>com.titan.MyMethodInterceptor</interceptor-class>
<method-name>bookPassage</method-name>
<method-params>
<method-param>com.titan.CreditCardDO</method-param>
<method-param>double</method-param>
</method-params>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
Usually, you will not be concerned with disabling interceptors, but it is good to know you have the tools to do so if you need to.
|