Creating Custom Annotations
Creating custom annotations in Java is easy. You just need to:- define an @interface
- declare your retention policy with @Retention annotation
- declare target elements of your annotation type.
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) public @interface Authorize { }
Retention Policy
Choosing the right retention policy is important. So, let's see the different retention policy options and understand their differences. There are 3 different retention policies:- SOURCE : The annotations will be ignored by the compiler and won't appear in decompiled .class file
- CLASS : The annotations will NOT be ignored by the compiler but the VM cannot inspect them at runtime. In other words getAnnotations() method result will not contain your annotation
- RUNTIME: The VM can inspect and your annotation will appear in the annotation list returned by the getAnnotations() method.
Target Element Type
Target on the other hand is about what do you want to annotate. Is it a class, method, a parameter of a method, field, constructor, or what?
You can provide one target type:
@Target(ElementType.METHOD)
or multiple targets in a list:
@Target({ ElementType.TYPE, ElementType.METHOD })
Using Annotations for Cross-Cutting Concerns
Before going deep, let's talk about Aspect Oriented Programming a little bit. AOP is used for cross cutting concerns.
So, what is a cross cutting concern?
It can be defined as a behavior to an existing code which is logically not central to the business logic.
The most common example for cross-cutting concerns is logging, but I think transaction and authorization are better examples for this topic. I am sure there are also other cool examples.
Terminology of AOP and what do they mean?
JoinPoint : A point in an execution flow of a program
Pointcut : An expression to match join points
Advice : The logic of what to do at joinpoints, the additional behavior itself.
There are different types of advice:
- Before : additional behavior will be executed before the join point
- After : additional behavior will be executed after the join point
- After Returning : additional behavior will be executed only after the join point is executed successfully.
- After Throwing : additional behavior will be executed only after the join point throws execption.
- Around : The most powerful kind of advice which encapsulates the join point. This means you can put additional behavior both before and after the join point.
The Best Part (Coding)
Using annotations in pointcut expressions is an easy way to detect join points in an execution flow. Annotations are commonly used for this purpose like Spring's @Transactional annotation. Spring detects methods annotated with @Transactional annotation and has an "Around Advice" for them which starts transaction before the method execution and ends it after the method execution is successfully completed.
My example is about "authorization". I have created a project with:
- Spring boot
- Spring MVC
- AspectJ
I created 2 new annotations:
- @Authorize : To detect the methods to be authorized
- @Resource : To differentiate the parameters to be used as resource from the others.
What "resource" means in my domain is irrelevant to us right now. The only thing important here is I defined an annotation type for method parameters to be able to differentiate some parameters from the others.
The Authorize Annotation
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) public @interface Authorize { }
The Resource Annotation
import java.lang.annotation.ElementType import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface Resource { }
Notice that the Target ElementType is PARAMETER.
Defining the Pointcuts
I have there pointcut expressions:
- One that matches the methods annotated with @Authorize annotation
- Another to match all public methods of a class that is annoated with @Authorize annotation
- One that joins them with OR
/** * All public methods that are annotated with Authorize. */ @Pointcut("execution(@com.fd.customannotation.annotations.Authorize * *(..))") public void authorizeAnnotatedMethod() {} /** * All public methods of a class annotated with Authorize. */ @Pointcut("execution(* (@com.fd.customannotation.annotations.Authorize *).*(..))") public void authorizeAnnotatedClassMethod() {} /** * Pointcut expression that joins the first two with OR */ @Pointcut("authorizeAnnotatedMethod() || authorizeAnnotatedClassMethod()") public void authorizeMethod() {}
The Additional Behavior - The Advice
The below implementation,"beforeAuthorizeMethods", will be executed before my methods annotated with @Authorize annotation or the methods of a class that is annotated with @Authorize. The below code simply finds out which parameters of a @Authorize annotated method are annotated with @Resource and prints the value of them.
@Before("authorizeMethod()") public void beforeAuthorizeMethods(final JoinPoint joinPoint) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); System.out.println(" Request URI: " + request.getRequestURI()); System.out.println(" Request URL: " + request.getRequestURL()); List<Object> resourceAnnotatedArguments = this.getResourceAnnotatedArguments(joinPoint); System.out.println(String.format("There are %d @Resource annotated parameters", resourceAnnotatedArguments.size())); for (Object value : resourceAnnotatedArguments) { System.out.println(" Parameter value : " + value); } } /** * Returns the list of arguments that are annotated with @Resource as a list of Objects */ private List<Object> getResourceAnnotatedArguments(final JoinPoint joinPoint) { List<Object> result = new ArrayList<>(); final MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); Method method = methodSignature.getMethod(); Object[] arguments = joinPoint.getArgs(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { Parameter parameter = parameters[i]; Annotation[] annotations = parameter.getAnnotations(); for (Annotation annotation : annotations) { if (annotation.annotationType() == Resource.class) { result.add(arguments[i]); break; } } } return result; }
Usage
I've written a simple RestController to test my pointcut expressions and advice.
@RestController @RequestMapping("/test") public class TestController { @Authorize @RequestMapping(value = "/index", method = RequestMethod.GET) public String index( @Resource @RequestParam(value="name", required=false, defaultValue="World") String name, String surname) { return "Hello " + name; } } }In this code the index method is annotated with @Authorize annotation. So, my pointcut expression should match this join point.
Also, my index method has two parameters: name and surname. Notice that name is annotated with @Resource. So, I should only see the value of the "name" parameter on the console when a request arrives to "http://base-address/test/index".