In my previous posting, I gave a short overview on the very basic features of Google's dependency injection container Guice. Today, I want to delve a bit more into Guice's means of aspect oriented programming (AOP) by musing on its abilities for method interception.
AOP is a common way for implementing cross-cutting concerns of an application, such as logging/tracing, transaction handling or permission checking.
As opposed to fully-fledged AOP solutions such as AspectJ, Guice's AOP facilities are limited to simple method interceptors. But to be fair, this should do the trick for most scenarios, while at the same time keeping the mechanism very easy to understand and use.
Create a new project
As in the first part of this tutorial, Maven shall be used as build tool. Unfortunately, one required 3rd party library – the AOP alliance API – is not declared as dependency in Guice's Maven POM. Therefore, we have to declare this dependency in our own pom.xml, which should look as follows:
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 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.gm</groupId> <artifactId>guice-aop</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>guice-aop</name> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.5</version> <scope>test</scope> </dependency> <dependency> <groupId>com.google.code.guice</groupId> <artifactId>guice</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>aopalliance</groupId> <artifactId>aopalliance</artifactId> <version>1.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>RELEASE</version> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> </plugins> </build> </project> |
As exemplary business service for this tutorial let's now create a little video rental shop, where videos from the stock can be rented and new videos can be registered:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package org.gm.guicetutorial; public class VideoRental { public boolean rentMovie(long movieId) { System.out.println( String.format("Movie %s rented.", movieId)); return true; } public boolean registerNewMovie(String name) { System.out.println( String.format("New movie \"%s\" registered.", name)); return true; } } |
Create a simple interceptor
Now it's time to create our first method interceptor. Since Guice uses AOP alliance conform method interceptors, we have to implement the interface MethodInterceptor to do so. The following snippet shows the canonical example, a simple tracing interceptor:
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 |
package org.gm.guicetutorial.interceptors; import java.util.Arrays; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; public class TracingInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { long start = System.nanoTime(); try { return invocation.proceed(); } finally { System.out.println( String.format( "Invocation of method %s() with parameters %s took %.1f ms.", invocation.getMethod().getName(), Arrays.toString(invocation.getArguments()), (System.nanoTime() - start) / 1000000.0)); } } } |
The method invoke() will be called by the container for each intercepted method. The passed MethodInvocation object contains an instance of java.lang.reflect.Method to represent the called method, a reference to the object on which the method was called as well as the parameters of the invocation.
To execute the intercepted method call, the proceed() method of the passed invocation has to be invoked. Before and afterwards, one can do whatever one wants with the invocation, including a modification of the parameters or the return value or supressing the actual call at all. For the TracingInterceptor, we just log the called method together with the duration of the call.
Now we have to tell Guice, to which method invocations the interceptor shall be attached. As with the binding in the tutorial's first installment, we do this using a configuration module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package org.gm.guicetutorial; import static com.google.inject.matcher.Matchers.*; import org.gm.guicetutorial.interceptors.TracingInterceptor; import com.google.inject.AbstractModule; public class ExampleModule extends AbstractModule { public void configure() { bindInterceptor( subclassesOf(VideoRental.class), any(), new TracingInterceptor()); } } |
By calling bindInterceptor() we specify, which interceptor (TracingInterceptor) shall be attached to which methods (any()) of which classes (subclassesOf(VideoRental.class)). The latter two are so-called matchers, which allow a type-safe and refactoring-safe way of linking interceptors and the calls to which these shall be applied. More information on the matchers, that Guice provides, can be found in the JavaDoc of com.google.inject.matcher.Matchers.
Finally we create a test. Note the use of Injector.injectMembers() which allows for injection of dependencies into objects, which are not instantiated under the reign of Guice, as it is the case with JUnit test classes.
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 |
package org.gm.guicetutorial; import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; public class VideoRentalTest { @Inject private VideoRental videoRental; @Before public void setup() { Injector injector = Guice.createInjector(new ExampleModule()); injector.injectMembers(this); } @Test public void testRentMovie() throws Exception { assertTrue(videoRental.rentMovie(1)); } @Test public void testRegisterNewMovie() throws Exception { assertTrue(videoRental.registerNewMovie("The Fugitive")); } } |
Executing the test should yield in an output similar to the following:
1 2 3 4 |
Movie 1 rented. Invocation of method rentMovie([1]) took 10.5 ms. New movie "The Fugitive" registered. Invocation of method registerNewMovie([The Fugitive]) took 0.4 ms. |
A security checking interceptor
As a more advanced use of a method interceptor, let us now create an interceptor, that checks, if a given user is allowed to execute a certain action. The actions that a user is allowed to execute, shall be specified using a role based approach. Which role is required to perform a certain method call, shall be expressed using a custom annotation. The task of the interceptor will be to check, whether the current user has this role.
Note, that in a real-world scenario, one would probably have one level of indirection more by checking on a role's permissions instead of on the role itself, but let's keep things simple for now.
First of all, we need some sort of user manager, which holds the currently logged in user:
1 2 3 4 5 6 7 8 9 |
package org.gm.guicetutorial; public interface UserManager { void setCurrentUser(User user); User getCurrentUser(); } |
Our very simple implementation of the user manager looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package org.gm.guicetutorial; public class UserManagerImpl implements UserManager { private User currentUser; @Override public void setCurrentUser(User currentUser) { this.currentUser = currentUser; } @Override public User getCurrentUser() { return currentUser; } } |
Within a multi-user server application, the user manager typically would use some sort of session mechanism to manage multiple users logged in at the same time. On the contrary, within a client application, the user manager might be a singleton object with application scope. For the sake of simplicity, we stick with the latter scenario, which we will realize leveraging Guice's SINGLETON scope as will be shown.
The User entity is a simple model class, which contains a user's name and his/her role(s) in form of a set of roles:
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 |
package org.gm.guicetutorial; import java.util.Set; public class User { private String name; private Set<Role> roles; public User(String name, Set<Role> roles) { super(); this.name = name; this.roles = roles; } public String getName() { return name; } public Set<Role> getRoles() { return roles; } @Override public String toString() { return name; } } |
To allow for specifying roles in a simple and still safe manner, we model the Role entity as enum:
1 2 3 4 5 6 7 |
package org.gm.guicetutorial; public enum Role { CUSTOMER, EMPLOYEE; } |
As mentioned above, it would be nice, to have a custom annotation, that can be used to specify, which role is required for the execution of a certain method.
The next snippet shows a simple blueprint, how such an annotation could look like. For now, the annotation has just one attribute – the standard attribute "value", which holds the name of the required role. By the use of meta annotations we specify further details of the annotation: it shall be usable at methods (@Target(ElementType.METHOD)) and accessible at runtime (@Retention(RetentionPolicy.RUNTIME)).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package org.gm.guicetutorial; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresRole { Role value(); } |
Using our new annotation it's real fun to define, which roles are required to execute the methods of the VideoRental class:
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 |
package org.gm.guicetutorial; import com.google.inject.Inject; public class VideoRental { @Inject UserManager userManager; @RequiresRole(Role.CUSTOMER) public boolean rentMovie(long movieId) { System.out.println(String.format( "Movie %s rented by user %s.", movieId, userManager.getCurrentUser())); return true; } @RequiresRole(Role.EMPLOYEE) public boolean registerNewMovie(String name) { System.out.println(String.format( "New movie \"%s\" registered by user %s.", name, userManager.getCurrentUser())); return true; } } |
Now let's have a look at the interceptor, that checks, wether the current user has the role required to invoke a certain method.
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 |
package org.gm.guicetutorial.interceptors; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.gm.guicetutorial.RequiresRole; import org.gm.guicetutorial.Role; import org.gm.guicetutorial.UserManager; import com.google.inject.Inject; public class RoleValidationInterceptor implements MethodInterceptor { @Inject private UserManager userManager; @Override public Object invoke(MethodInvocation invocation) throws Throwable { Role requiredRole = invocation.getMethod().getAnnotation(RequiresRole.class).value(); if( userManager.getCurrentUser() == null || !userManager.getCurrentUser().getRoles().contains(requiredRole)) { throw new IllegalStateException("User requires role " + requiredRole); } return invocation.proceed(); } } |
We get the required role for a method invocation by evaluating the value attribute of the RequiresRole annotation. If this role is not contained within the current user's set of roles, an IllegalStateException is thrown. Otherwise, the intercepted method call is resumed.
As before, some plumbing has to be done now in the binding module:
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 |
package org.gm.guicetutorial; import static com.google.inject.matcher.Matchers.*; import java.util.ArrayList; import java.util.List; import org.gm.guicetutorial.interceptors.RoleValidationInterceptor; import com.google.inject.AbstractModule; import com.google.inject.Scopes; public class ExampleModule extends AbstractModule { private List<Object> injectees = new ArrayList<Object>(); public void configure() { bind(UserManager.class).to(UserManagerImpl.class).in(Scopes.SINGLETON); RoleValidationInterceptor roleValidationInterceptor = new RoleValidationInterceptor(); bindInterceptor( any(), annotatedWith(RequiresRole.class), roleValidationInterceptor); injectees.add(roleValidationInterceptor); } public List<Object> getInjectees() { return injectees; } } |
First, we bind UserManagerImpl to the interface UserManager. By using the SINGLETON scope we ensure, that the same UserManager instance will be returned by the injector each time, so that the UserManger injected into VideoRental as well as into the interceptor is the same.
Secondly, the RoleValidationInterceptor has to be configured. As method matcher we specify annotatedWith(RequiresRole.class), thereby attaching the interceptor only to those methods, which are tagged with our new shiny annotation.
Unfortunately, Guice currently can not inject dependencies into interceptors within a module's configure() method. To circumvent this issue, we let the module return a collection of injectees, that we will process manually, once the injector is created. A more sophisticated way to tackle that problem is shown by Tim Peierl in his blog.
In the upcoming Guice release 2.0 the new method requestInjection() in AbstractModule can be used to specify any injectees, that shall be provided with dependencies upon creation of the injector.
Finally let's build a test for the new role validation mechanism:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
package org.gm.guicetutorial; import static org.junit.Assert.*; import java.util.HashSet; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; public class VideoRentalTest { @Inject private VideoRental videoRental; @Inject private UserManager userManager; private static User customer; private static User employee; @BeforeClass public static void setupUsers() { customer = new User("Peter", EnumSet.of(Role.CUSTOMER)); employee = new User("Bob", EnumSet.of(Role.EMPLOYEE)); } @Before public void setup() { ExampleModule module = new ExampleModule(); Injector injector = Guice.createInjector(module); for (Object oneInjectee : module.getInjectees()) { injector.injectMembers(oneInjectee); } injector.injectMembers(this); } @Test public void testRentMovieSuccessfully() throws Exception { userManager.setCurrentUser(customer); assertTrue(videoRental.rentMovie(1)); } @Test(expected = IllegalStateException.class) public void testRentMovieFailing() throws Exception { userManager.setCurrentUser(employee); videoRental.rentMovie(1); } @Test public void testRegisterNewMovieSuccessfully() throws Exception { userManager.setCurrentUser(employee); assertTrue(videoRental.registerNewMovie("The Fugitive")); } @Test(expected = IllegalStateException.class) public void testRegisterNewMovieFailing() throws Exception { userManager.setCurrentUser(customer); assertTrue(videoRental.registerNewMovie("The Fugitive")); } } |
In the class setup, two users – one customer, one employee – are instantiated. In the test setup, the Guice injector is created, followed by the injection of dependencies into the injectees as returned by the binding module. Four test methods are present, one testing successful execution and one testing failure of execution due to the lack of the required role for each of VideoRental's methods.
This concludes the 2nd part of my tutorial series on Google Guice. As shown, Guice provides a simple but still powerful means of aspect oriented programming. Also advanced scenarios such as security checking using a custom annotation are not very hard to realize. The complete source code can be downloaded here.
In the next installment, I will show how to use Guice within a JEE environment, namely its use together with EJB.
12 comments:
Thanks for brilliant introduction to the Guice interceptor problematics!
Thx, George :-) I'm glad to hear, that the post was helpful.
It is certainly interesting for me to read the article. Thanks for it. I like such themes and anything that is connected to them. I definitely want to read more soon.
Alex
Phone blocker
since 2.0 no need for 'injectees' ... you can use binder.requestInjection( ... ) now
Thanks, this was helpful
Thanks for you feedback, great to hear that the post was useful for you.
Awesome tutorial! Keep up the good work.
It cant be more simplified than this.
excellent explaination ....Thanks a lot for this post..
Great blog post. I was looking for a way to provide some timing to a method to determine invocation times. This got me 90% there.
Thanks
Pretty cool explanation
ehello,
thx for the tuto is so helpful
please how i can intercept call of method using AOP alliance interceptor:
i have class Hello with method "sayHello"; and i would tracing call of this method, code of tracing is like this:
public class TracingInterceptor implements MethodInterceptor {
Object target;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
long start = System.nanoTime();
try {
return invocation.proceed();
}
finally {
System.out.println(
String.format(
"Invocation of method %s() with parameters %s took %.1f ms.",
invocation.getMethod().getName(),
Arrays.toString(invocation.getArguments()),
(System.nanoTime() - start) / 1000000.0));
}
}
after what i do in the main for the call of the method sayHello will be intercepted ?
please i need this information pleaase
It seems that the code is too long. Can you provide me the RoboGuice tutorials with lesser lines of code?
Post a Comment