Aspect Oriented Programming(AOP) with Spring Boot - 1
In this post, we are going to look at what is AOP and how your spring application matches with AOP concepts using Spring AOP module. After that we are …
This is the second post for a short series of aspect oriented programming with the spring boot.
In this post, I am going to implement a simple project example includes spring aop module. I will write aspect for both method execution(s) and annotation(s). At the end you will be able to reference this basic project for yours.
Before start, you may need to check the previous post which includes AOP definitions, advice types, aspectJ etc..
If you only need to use github repo, here is the link
Before diving into the project, let me point to the aspects I am going to use for that project.
value
will be added to the response cookie.In this example, I will generate random cookie name with the static cookie value, therefore you can verify that aspect is working by logging them on the controller
POST
method, log that “POST method called”Before adding the aspect, default project setup includes three controllers (CustomerController, LoginController, StatusController
) and two services (CustomerService, LoginService
) and one repository (CustomerRepository
)
There is no real database setup or other complex setup to start this application. These are just the dummy endpoints. Take a look at all the endpoints:
package com.mehmetozanguven.springaopexample;
@SpringBootApplication
public class SpringAopExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAopExampleApplication.class, args);
}
}
// ...
package com.mehmetozanguven.springaopexample.controller;
@RestController
@RequestMapping("/api")
public class StatusController {
@GetMapping("/status")
public String getApplicationStatus(){
return "application-is-working";
}
}
// ...
package com.mehmetozanguven.springaopexample.controller;
@RestController
@RequestMapping("/api")
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public String loginCustomer(LoginRequest loginRequest){
loginService.loginCustomer(loginRequest);
return "login-request";
}
}
// ...
package com.mehmetozanguven.springaopexample.controller;
@RestController
@RequestMapping("/api")
public class CustomerController {
@Autowired
private CustomerService customerService;
@GetMapping("/customer-by-id/{id}")
public String findCustomerById(HttpServletResponse response, @PathVariable String id){
customerService.findCustomerById(Long.parseLong(id));
return "customer-by-id-" + id;
}
}
Before directly adding annotation to other project,
First define some pointcut designators:
Second define some pre-define pointcut, such as pointcut for serviceClassMethod(s), repositoryClassMethod(s) etc..
@Aspect
public class SystemPointcut {
// define pointcut for any method execution inside package controller and its sub-package
// we can refer this pointcut via controllerLayer()
@Pointcut("within(com.mehmetozanguven.springaopexample.controller..*)")
public void controllerLayer() {}
// ...
}
Define the annotation:
package com.mehmetozanguven.springaopexample.annotation;
public @interface InjectResponseCookie {
String cookieValue() default "defaultValue";
}
Define the aspect: I decided to use @After
advice for that. I can also use this @Around
however this aspect will not modify the return value of the actual method execution.
@Aspect
@Component
public class InjectResponseCookieAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(InjectResponseCookieAspect.class);
@After(value = "com.mehmetozanguven.springaopexample.aspect.SystemPointcut.controllerLayer() && " +
"@annotation(injectResponseCookie)")
public void injectResponseCookie(InjectResponseCookie injectResponseCookie){
HttpServletResponse response = getResponse();
Cookie cookie = new Cookie(generateRandomCookieName(), injectResponseCookie.cookieValue());
response.addCookie(cookie);
LOGGER.info("Annotation value: {}", injectResponseCookie.cookieValue());
}
}
Add @InjectResponseCookie
annotation to any method: (I have added the findCustomerById
method)
@InjectResponseCookie(cookieValue = "injectResponseCookie")
@GetMapping("/customer-by-id/{id}")
public String findCustomerById(HttpServletRequest request, HttpServletResponse response, @PathVariable String id) {
customerService.findCustomerById(Long.parseLong(id));
logAllCookies(request);
return "customer-by-id-" + id;
}
// hit the http://localhost:8080/api/customer-by-id/22 and see the cookies
Define the annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
}
Define the aspect: **This should be @Around
advice, because i need to catch the exception and modify the calling producure, **
Note:
@Around
advice is the only advice that can prevent the original method from being called and only advice that can catch exceptions and it will not propagated to the caller. For more info about advice types check out my previous post
@Aspect
@Component
public class RetryAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(RetryAspect.class);
// Fully qualified name to avoid 'error Type referred to is not an annotation type:'
@Around("@annotation(com.mehmetozanguven.springaopexample.annotation.Retry)")
public Object retryTheExecution(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
try{
return proceedingJoinPoint.proceed();
}catch (Throwable throwable){
LOGGER.error("Error when calling method: {}, retrying again...", proceedingJoinPoint.getSignature().getName());
return proceedingJoinPoint.proceed();
}
}
}
Add @Rety
to any method execution, I have added to the StatusController
, logic is very basic:
@RestController
@RequestMapping("/api")
public class StatusController {
private static Logger LOGGER = LoggerFactory.getLogger(StatusController.class);
@Retry
@GetMapping("/status")
public String getApplicationStatus(){
int randomNumber = getRandomNumber();
LOGGER.info("Status controller with randomNumber: {}", randomNumber);
if (randomNumber % 2 != 0){
throw new RuntimeException("Dummy exception to test retry");
}
return "application-is-working";
}
private int getRandomNumber(){
return RandomUtils.nextInt(0, 10);
}
}
Depending on your execution, you may see the logs like this:
2020-11-17 00:08:52.802 INFO 15258 --- [nio-8080-exec-1] c.m.s.controller.StatusController : Status controller with randomNumber: 5
2020-11-17 00:08:52.804 ERROR 15258 --- [nio-8080-exec-1] c.m.springaopexample.aspect.RetryAspect : Error when calling method: getApplicationStatus, retrying again...
2020-11-17 00:08:52.804 INFO 15258 --- [nio-8080-exec-1] c.m.s.controller.StatusController : Status controller with randomNumber: 8
Define the aspect:
@Aspect
@Component
public class PostLoggingAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(PostLoggingAspect.class);
@Before("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void logBeforePostMethod(JoinPoint joinPoint){
LOGGER.info("POST method called: {}", joinPoint.getSignature());
}
}
You can hit the login endpoint via this payload
{
"email": "sample",
"password": "password"
}
Here is the aspect log:
2020-11-17 00:18:15.420 INFO 15979 --- [nio-8080-exec-1] c.m.s.aspect.PostLoggingAspect : POST method called: String com.mehmetozanguven.springaopexample.controller.LoginController.loginCustomer(LoginRequest)
As I have said previously, We are going to find out “aspect will work for indirect call or not?”
Define aspect:
@Aspect
@Component
public class TrickyAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(TrickyAspect.class);
@After("execution(* trickyMethod(..))")
public void trickyMethodAdvice(){
LOGGER.info("Tricky after advice called");
}
}
Update the CustomerController,Service and Repository
@RestController
@RequestMapping("/api")
public class CustomerController {
// ...
@GetMapping("/tricky")
public String callTrickyDirectly(){
customerService.callTrickyMethodDirectly();
return "direct call";
}
@GetMapping("/tricky-in")
public String callTrickyInDirectly(){
customerService.callingTrickyMethodInDirectly();
return "indirect call";
}
}
@Service
public class CustomerServiceImpl implements CustomerService{
// ...
@Override
public void callTrickyMethodDirectly() {
customerRepository.trickyMethod();
}
@Override
public void callingTrickyMethodInDirectly() {
customerRepository.indirectCallOfTrickyMethod();
}
}
@Repository
public class CustomerRepositoryImpl implements CustomerRepository {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomerRepositoryImpl.class);
@Override
public void trickyMethod() {
LOGGER.info("Tricky method called");
}
@Override
public void indirectCallOfTrickyMethod() {
LOGGER.info("-------");
LOGGER.info("indirectCallOfTrickyMethod will call the trickyMethod");
LOGGER.info("Aspect will not work in that case.");
LOGGER.info("-------");
trickyMethod();
}
}
Hit the: http://localhost:8080/api/tricky, here is the logs:
2020-11-17 01:14:15.911 INFO 20684 --- [nio-8080-exec-1] c.m.s.repository.CustomerRepositoryImpl : Tricky method called
2020-11-17 01:14:15.912 INFO 20684 --- [nio-8080-exec-1] c.m.s.aspect.TrickyAspect : Tricky after advice called
Hit the http://localhost:8080/api/tricky-in, here is the logs:
2020-11-17 01:16:28.204 INFO 20927 --- [nio-8080-exec-1] c.m.s.repository.CustomerRepositoryImpl : -------
2020-11-17 01:16:28.204 INFO 20927 --- [nio-8080-exec-1] c.m.s.repository.CustomerRepositoryImpl : indirectCallOfTrickyMethod will call the trickyMethod
2020-11-17 01:16:28.204 INFO 20927 --- [nio-8080-exec-1] c.m.s.repository.CustomerRepositoryImpl : Aspect will not work in that case.
2020-11-17 01:16:28.204 INFO 20927 --- [nio-8080-exec-1] c.m.s.repository.CustomerRepositoryImpl : -------
2020-11-17 01:16:28.204 INFO 20927 --- [nio-8080-exec-1] c.m.s.repository.CustomerRepositoryImpl : Tricky method called
As you can see TrickyAfterAdvice did not get call. If you look for an answer, answer is related to the Spring proxy mechanism.
When Spring knows your object (that’s means you annotated your object with @Component, @Service, @Conroller etc..
, what happens is that Spring wraps the original object via proxy. (Actually proxy pattern is applying here, for more information about proxy pattern, please look at the my previous post.)
All Spring beans communicate each other using the proxy. Because I am using Spring AOP, aspects is also known by Spring Container, therefore all advices method is being called with using the proxy object.
Here is your Original Object:
If Spring knows your object, Spring will wrap (will create a proxy) the original Object with proxy, this proxy looks like original object(s) and this is injected into other spring beans:
Other spring beans does call to this proxy, this call is forwarded to the Original Object and also the Advice is called. This is how Spring AOP works. Instead of the Original object proxy is used:
So what happens when you do a local method call (kind of a situation in the example “indirectCallOfTrickyMethod()
have called the trickyMethod()
”)?
We can see the local method call and proxy call adding debug point. Now just add a debug point in here:
@Repository
public class CustomerRepositoryImpl implements CustomerRepository {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomerRepositoryImpl.class);
// ...
@Override
public void trickyMethod() {
LOGGER.info("Tricky method called");
} // add debug point in here
// ...
}
After hit the http://localhost:8080/api/tricky, you can see that trickyMethod
will be called from class named: CustomerRepositoryImpl$$FastClassBySpringCGLIB...
which is nothing but a Proxy, and that means Advice will be called also.
After hit the http://localhost:8080/api/tricky-in, indirectCallOfTrickyMethod()
will be called by the same Proxy class from the above, but trickyMethod()
call inside the indirectCallOfTrickyMethod
will be done by the object itself. Therefore Proxy will never be executed and advice will not be called.
Another example could @Transactional
annotation:
@Repository
public class CustomerRepository{
@Transactional
public void transaction(){
}
public void callTransactional(){
/* Because @Transaction is implemented using Spring AOP,
callTransactional method has no configuration(even it is calling transaction method) for transaction such as:
the rollback rules, timeout, isolation level etc..
*/
transaction()
}
}
That’s it. I hope this post could be helpful for anyone that needs to use Spring-AOP.
Last but not least, wait it for the next one.
In this post, we are going to look at what is AOP and how your spring application matches with AOP concepts using Spring AOP module. After that we are …
In this post, we are going to learn Regex expression in one blog Let’s start with defining what is Regex or Regex expression. What is Regex? …