Using dependency injection and aspect-oriented mechanisms like interceptors allow us to separate cross-cutting-concerns in our Java enterprise application, to control global aspects of our application and to avoid boilerplate code.
In the following short tutorial we’re going to create an aspect-oriented logger to protocol the initiating user, class and method called and the parameters passed to the method and finally we’re adding this interceptor to a sample RESTful web-service by adding a simple annotation.
Project Setup / Maven Archetype
Basically we need only one dependency here to be added to our pom.xml:
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
Otherwise there is a nice Maven archetype, org.codehaus.mojo.archetypes.webapp-javaee7 that allows us to create our project structure using mvn archetype:generate
mvn archetype:generate -Dfilter=webapp-javaee7
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[..]
Choose archetype:
1: remote -> org.codehaus.mojo.archetypes:webapp-javaee7 (Archetype for a web application using Java EE 7.)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 1
Java EE 6? The following tutorial works in Java EE 6 without any problem. The only difference is the RESTful webservice where we might need to add some more configuration.
Log Producer
The following producer allows us to use dependency injection to create a logger instance in every location of our application like this: @Inject Logger log;
Using a producer allows us to obtain information about the injection point and use this information to extract the package/class name and create a new logger instance with it (as I’m using GlassFish in this tutorial, we’re using the JULI/JDK logger) .
package com.hascode.tutorial;
import java.util.logging.Logger;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
public class LogProvider {
@Produces
public Logger createLogger(InjectionPoint ip) {
return Logger.getLogger(ip.getMember().getDeclaringClass().getName());
}
}
Around Invoke Interceptor
The following interceptor gets the session context and the logger from the producer above injected and uses the information gathered from the session context and the invocation context to protocol the following information:
-
The username of the initiating user .. e.g. fred
-
The object whose method is called .. e.g. com.hascode.tutorial.UserWebservice@67784ed2
-
The name of the method called .. e.g. getUser
-
The parameters passed to the method
One warning regarding the code below: For production we definitely should add some null-checks for the parameters (or simply use ObjectUtils.toString(obj, “null”) of Apache Commons Lang or something similar) and a fallback if no principal can be obtained from the session context e.g. by writing something like “anonymoususer” to the logs.
Another thing to consider is the logging of all our parameters – it’s fine for the demonstration purpose in this tutorial but on a production system we definitely don’t want to write things like passwords, hashes or other fragments of sensitive information to our logs as this is one common vulnerability listed on the OWASP list.
Detailed information about this vulnerability can be found at the OWASP Wiki: Logging and Auditing Vulnerability.
When we’re finished writing our logs, we’re calling the invocation context’ proceed method to pass the control further the chain of interceptors in our application.
package com.hascode.tutorial;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
public class ActionProtocolInterceptor {
@Resource
private SessionContext sessionCtx;
@Inject
private Logger log;
@AroundInvoke
protected Object protocolInvocation(final InvocationContext ic)
throws Exception {
StringBuilder sb = new StringBuilder("[");
for (Object obj : ic.getParameters()) {
sb.append(obj.toString());
sb.append(", ");
}
sb.append("]");
log.log(Level.INFO,
"user {0} invoced {1} with method {2} and parameters: {3}",
new Object[] { sessionCtx.getCallerPrincipal().getName(),
ic.getTarget().toString(), ic.getMethod().getName(),
sb.toString() });
return ic.proceed();
}
}
Applying the Interceptor to a Sample REST-Service
Finally the only thing we need to do is to add our interceptor to some executable code.
There are different possible ways to configure the interaction of our interceptor with our application:
-
@Interceptors: Specified on class or method level allows us to add one or more interceptors executed in the order of the definition e.g. @Interceptors(\{Interceptor1.class, Interceptor2.class..})
-
@ExcludeDefaultInterceptors: Disables declared default constructors for this location.
-
@ExcludeClassInterceptors: Disables class specified interceptors for this location.
-
Global Configuration via ejb-jar.xml: We’re able to add interceptors to our application on a global level and using wildcards .. e.g. for every EJB in the service package. The ejb-jar.xml should be put into WEB-INF or META-INF depending on the packaging format.
-
For more detailed information, please feel free to have a look at Oracle’s Java Documentation here.
The following RESTful webservice is easy summarized – it offers two callable methods, restricts its usage to users assigned to the group named user and our interceptor is applied using the @Interceptors annotation.
package com.hascode.tutorial;
import javax.annotation.security.RolesAllowed;
import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Stateless
@Path("/user")
@Interceptors(ActionProtocolInterceptor.class)
@RolesAllowed("user")
public class UserWebservice {
@DELETE
@Path("/{userId}")
public Response removeUser(@PathParam("userId") String userId) {
// do stuff..
return Response.ok().build();
}
@GET
@Path("/{userId}")
@Produces(MediaType.APPLICATION_JSON)
public Response getUser(@PathParam("userId") String userId) {
// do stuff..
return Response.ok().build();
}
}
The Logger in Action
We’re now ready to initiate a request to the RESTful webservice to see our logger in action.
When calling the url for the GET annotated method, we should see a login prompt like this. How to configure a realm and user accounts is describe in the Appendix A below.
Afterwards we should be able to view some similar excerpt in the domain logs:
[2014-05-25T15:47:08.582+0200] [glassfish 4.0] [INFO] [] [com.hascode.tutorial.ActionProtocolInterceptor] [tid: _ThreadID=127 _ThreadName=http-listener-1(4)] [timeMillis: 1401025628582] [levelValue: 800] [[
user fred invoked com.hascode.tutorial.UserWebservice@67784ed2 with method getUser and parameters: [25C46B7E-5A22-4470-B048-593DCA9600ED, ]]]
This is what the log excerpt looks like in the GlassFish log viewer:
Tutorial Sources
Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/javaee7-aop-logging.git
Resources
Appendix A: Setup Basic Auth on a File-based Realm on GlassFish 4
Neither basic authentication nor using a file-based realm is something we’d like to use on a production system, but for a tutorial like this one, it should be sufficient ;)
Specifying Security Constraints
The following constraint, added to our web.xml (must be put in WEB-INF) enforces that an initiator of a request must be authenticated and assigned to the user role named user.
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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/web-app_3_1.xsd"
version="3.1">
<security-constraint>
<web-resource-collection>
<web-resource-name>everything</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>
Role-to-Group Mapping
Now we need only one more descriptor to map between roles assigned from the container to groups in our application.
Simply add the following sun-web.xml to src/main/webapp/WEB-INF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-web-app PUBLIC "-//Sun Microsystems, Inc.//DTD GlassFish Application Server 3.0 Servlet 3.0//EN" "http://www.sun.com/software/appserver/dtds/sun-web-app_3_0-0.dtd">
<sun-web-app>
<security-role-mapping>
<role-name>user</role-name>
<group-name>user</group-name>
</security-role-mapping>
<class-loader delegate="true" />
</sun-web-app>
Add User Account to File Realm
We’re able to configure the user accounts for our file based realm in the GlassFish administration web console in the section Configurations > server-config > Security > Realms > file > Manage users.
There we may add a user, set the password and assign the group named user to him.
Appendix B: Complete REST Service Configuration
In Java EE 7 we don’t need to add some REST servlet to the web.xml but I wanted to modify the application path for the REST services that’s why I added the following Java configuration to the project that does nothing more than to register the prefix/path “rs” for the services.
package com.hascode.tutorial;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/rs")
public class RESTConfiguration extends Application {
}