If you have cross-cutting concerns for several parts of your application you usually don't want to copy and paste the code. For Java EE applications the CDI (Context and Dependency Injection) spec contains the concept of interceptors which are defined in the Java Interceptor specification. With these CDI interceptors, you can intercept business method, timeouts for EJB timers, and lifecycle events.
With this blog post, I'll demonstrate where and how to use the interceptors for a Java EE 8 application, using Java 8 and running on Payara 5.192.
Injection points for interceptors
Even though interceptors are part of the CDI spec they can intercept: EJBs, session beans, message-driven beans, and CDI managed beans. The Java Interceptors 1.2 release (latest) is part of the maintenance release JSR-318 and the CDI spec builds upon its basic functionality.
The specification defines five types of injection points for interceptors:
@AroundInvoke
intercept a basic method call@AroundTimeout
used to intercept timeout methods of EJB Timers@AroundConstruct
interceptor method that receives a callback after the target class is constructed@PostConstruct
intercept post-construct lifecycle events@PreDestroy
interceptor method for pre-destroy lifecycle events
For most of these injection points, I'll provide an example in the following sections.
Writing CDI interceptors
Writing a CDI interceptor is as simple as the following:
1 2 3 4 5 6 7 8 | @Interceptor public class SecurePaymentInterceptor { @AroundInvoke public Object securePayment(InvocationContext invocationContext) throws Exception { return invocationContext.proceed(); } } |
You just annotate a class with @Interceptor
and add methods for intercepting your desired injection points. Within an interceptor method, you have access to the InvocationContext
. With this object, you can retrieve the name of the method, its parameters and you can also manipulate them. Make sure to call the .proceed()
method if you want to continue with the execution of the original method.
As an example I'm going to intercept the following EJB:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Startup @Singleton @ManipulatedPayment public class PaymentProvider { @PostConstruct public void setUpPaymentProvider() { System.out.println("Setting up payment provider ..."); } public void withdrawMoneyFromCustomer(String customer, BigDecimal amount) { System.out.println("Withdrawing money from " + customer + " - amount: " + amount); } } |
To demonstrate how to manipulate the method parameters, I'm going to change the amount of the .withdrawMoneyFromCustomer(String customer, BigDecimal amount)
if the customer name is duke. In addition. I'm logging a single line to console once lifecycle event interceptors are triggered:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | @Interceptor public class PaymentManipulationInterceptor { @Inject private PaymentManipulator paymentManipulator; @AroundInvoke public Object manipulatePayment(InvocationContext invocationContext) throws Exception { if (invocationContext.getParameters()[0] instanceof String) { if (((String) invocationContext.getParameters()[0]).equalsIgnoreCase("duke")) { paymentManipulator.manipulatePayment(); invocationContext.setParameters(new Object[]{ "Duke", new BigDecimal(999.99).setScale(2, RoundingMode.HALF_UP) }); } } return invocationContext.proceed(); } @AroundConstruct public void aroundConstructInterception(InvocationContext invocationContext) throws Exception { System.out.println( invocationContext.getConstructor().getDeclaringClass() + " will be manipulated"); invocationContext.proceed(); } @PostConstruct public void postConstructInterception(InvocationContext invocationContext) throws Exception { System.out.println( invocationContext.getMethod().getDeclaringClass() + " is ready for manipulation"); invocationContext.proceed(); } @PreDestroy public void preDestroyInterception(InvocationContext invocationContext) throws Exception { System.out.println( "Stopped manipulating of class " + invocationContext.getMethod().getDeclaringClass()); invocationContext.proceed(); } } |
For a more realistic example, I'm creating an interceptor to intercept JAX-RS methods and check if a required HTTP header is set. If the header is not present, the server will return with an HTTP status 400:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Interceptor public class SecurePaymentInterceptor { @Context private HttpHeaders headers; @AroundInvoke public Object securePayment(InvocationContext invocationContext) throws Exception { String requiredHttpHeader = invocationContext .getMethod() .getAnnotation(SecurePayment.class) .requiredHttpHeader(); if (headers.getRequestHeaders().containsKey(requiredHttpHeader)) { return invocationContext.proceed(); } else { throw new WebApplicationException( "Missing HTTP header: " + requiredHttpHeader, Response.Status.BAD_REQUEST); } } } |
The required HTTP header is stored in the annotation @SecurePayment(requiredHttpHeader="X-Duke")
, which is used to bind an interceptor to a method/class, as you'll see in the next chapter.
Binding interceptors to methods and classes
Up until now we just created CDI interceptors but did not bind them to a specific method or class. For this, we'll use a custom annotation with @InterceptorBinding
. The simplest annotation for this looks like the following:
1 2 3 4 5 | @InterceptorBinding @Target({TYPE, METHOD}) @Retention(RUNTIME) public @interface ManipulatedPayment { } |
We can also add custom attributes to the annotation, as we need it for your @SecurePayment
binding to specify the required HTTP header:
1 2 3 4 5 6 7 8 | @InterceptorBinding @Target({METHOD, TYPE}) @Retention(RUNTIME) public @interface SecurePayment { @Nonbinding String requiredHttpHeader() default "X-Duke"; } |
Once this annotation is in place, we have to add it to the interceptor class and to the method or class (to include all methods of this class) we want to intercept:
1 2 3 4 5 6 7 | @Interceptor @SecurePayment public class SecurePaymentInterceptor { // ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Path("payments") public class PaymentResource { @Inject private PaymentProvider paymentProvider; @GET @Path("/{customerName}") @SecurePayment(requiredHttpHeader = "X-Secure-Payment") public Response getPaymentForCustomer(@PathParam("customerName") String customerName) { paymentProvider .withdrawMoneyFromCustomer(customerName, new BigDecimal(42.00).setScale(2, RoundingMode.HALF_UP)); return Response.ok("Payment was withdrawn from customer " + customerName).build(); } } |
Activating CDI interceptors
The CDI interceptors are not active by default and we have to activate them first. Currently, there are two possible ways to activate them:
- Add the fully qualified class name of the interceptor to the
beans.xml
file - Add the
@Priority(int priority)
annotation to the interceptor
To show you how both work, I'm using the first approach for the first interceptor:
1 2 3 4 5 6 7 8 9 | <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="all"> <interceptors> <class>de.rieckpil.blog.PaymentManipulationInterceptor</class> </interceptors> </beans> |
The second interceptor is activated using the annotation. With the priority number we can specify the execution order of interceptors if multiple apply for the same method:
1 2 3 4 5 6 7 8 | @Priority(42) @Interceptor @SecurePayment public class SecurePaymentInterceptor { // ... } |
Putting it all together and hitting /resources/payments/mike
and /resources/payments/duke
with the X-Secure-Payment header, results in the following log output:
1 2 3 4 5 6 7 8 9 10 11 | [Payara 5.192] [INFO] [[ Clustered CDI Event bus initialized]] [Payara 5.192] [INFO] [[ class de.rieckpil.blog.PaymentProvider will be manipulated]] [Payara 5.192] [INFO] [[ class de.rieckpil.blog.PaymentProvider is ready for manipulation]] [Payara 5.192] [INFO] [[ Setting up payment provider ...]] [Payara 5.192] [INFO] [[ Initializing Soteria 1.1-b01 for context '']] [Payara 5.192] [INFO] [[ Loading application [intercept-methods-with-cdi-interceptors] at [/]]] [Payara 5.192] [INFO] [[intercept-methods-with-cdi-interceptors was successfully deployed in 1,678 milliseconds.]] [Payara 5.192] [INFO] [[ Context path from ServletContext: differs from path from bundle: /]] [Payara 5.192] [INFO] [[ Withdrawing money from mike - amount: 42.00]] [Payara 5.192] [INFO] [[ Manipulating payment...]] [Payara 5.192] [INFO] [[ Withdrawing money from Duke - amount: 999.99]] |
For more information visit the Weld documentation (CDI reference implementation) or the website of the CDI spec.
You can find the source code for this example on GitHub.
Have fun using CDI interceptors,
Phil