The explanation is so transparent, and aspect AOP elegantly implements permission verification! (VIP Collector’s Edition)

1 Understand AOP

1.1 What is AOP

AOP (Aspect Oriented Programming), aspect-oriented thinking, is one of the three core ideas of Spring (the other two are: IOC-Inversion of Control, DI-Dependency Injection).

So why is AOP so important? In our programs, there are often some systematic requirements, such as permission verification, logging, statistics, etc. These codes will be scattered and interspersed in various business logics, which is very redundant and not conducive to maintenance. For example, the following diagram:

How many business operations there are, how many repetitive verification and logging codes need to be written, which is obviously unacceptable. Of course, using object-oriented thinking, we can extract these repetitive codes and write them as public methods, as follows:

In this way, the problems of code redundancy and maintainability are solved, but each business method still has to manually call these public methods in sequence, which is also slightly cumbersome. Is there a better way? Yes, that is AOP. AOP completely extracts non-business code such as permission verification and logging, separates it from the business code, and looks for nodes to cut into the business code:

1.2 AOP system and concepts

To understand simply, AOP actually has to do three types of things:

  • Where to cut in, that is, in which business code non-business operations such as permission verification are performed.

  • When to cut in, before or after the business code is executed.

  • What to do after switching in, such as permission verification, logging, etc.

Therefore, the AOP system can be summarized as follows:

Detailed explanation of some concepts:

  • “Pointcut”: Pointcut, which determines where processing such as permission verification, logging, etc. is cut into the business code (i.e., woven into aspects). Pointcuts are divided into execution mode and annotation mode. The former can use path expressions to specify which classes are woven into aspects, and the latter can specify which annotation-modified code is woven into aspects.

  • “Advice”: processing, including processing time and processing content. Processing content means doing something, such as verifying permissions and recording logs. Processing timing refers to when the processing content is executed, which is divided into pre-processing (that is, before the business code is executed), post-processing (after the business code is executed), etc.

  • “Aspect”: Aspect, namely Pointcut and Advice.

  • “Joint point”: A connection point is a point of program execution. For example, the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.

  • “Weaving”: Weaving is the process of processing content in the target object method through a dynamic proxy.

There is a picture on the Internet that I think is very expressive, so I posted it here for everyone to take a closer look at:

2 AOP examples

Practice brings true knowledge. Next, we will write code to implement AOP. The complete project has been uploaded to: GitHub AOP demo project. This project is an integration project about springboot. Please pay attention to the [aop-demo] module for the AOP part.

To use AOP, you first need to introduce AOP dependencies.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.1 The first example

Next, let’s look at a minimalist example: before all get requests are called, the sentence “The advice of the get request is triggered” is output on the console.

The specific implementation is as follows:

1. Create an AOP aspect class and just add an @Aspect annotation to the class. The @Aspect annotation is used to describe an aspect class. This annotation needs to be added when defining the aspect class. The @Component annotation hands over the class to Spring for management. Implement advice in this class:

package com.mu.demo.advice;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAdvice {
    // Define a pointcut: all methods modified by GetMapping annotations will be woven into advice
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    private void logAdvicePointcut() {}

 // Before means that logAdvice will be executed before the target method is executed.
    @Before("logAdvicePointcut()")
    public void logAdvice(){
     // This is just an example, you can write any processing logic
        System.out.println("The advice requested by get is triggered");
    }
}

2. Create an interface class and create a get request internally:

package com.mu.demo.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/aop")
public class AopController {
    @GetMapping(value = "/getTest")
    public JSONObject aopTest() {
        return JSON.parseObject("{"message":"SUCCESS","code":200}");
    }
    
 @PostMapping(value = "/postTest")
    public JSONObject aopTest2(@RequestParam("id") String id) {
        return JSON.parseObject("{"message":"SUCCESS","code":200}");
    }
}

After the project is started, request the http://localhost:8085/aop/getTest interface:

When requesting the http://localhost:8085/aop/postTest interface, there is no output on the console, proving that the pointcut is indeed only for methods modified by GetMapping.

2.2 The second example

Let’s make the problem a little more complicated. The scenario in this example is:

  • Customize an annotation PermissionsAnnotation

  • Create an aspect class, set the pointcut to intercept all methods marked with PermissionsAnnotation, intercept the parameters of the interface, and perform simple permission verification.

  • Mark the PermissionsAnnotation on the test interface test of the test interface class

Specific implementation steps:

1. Use @Target, @Retention, and @Documented to customize an annotation (for details about these three annotations, please see: Meta-annotation details):

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionAnnotation{
}

2. Create the first AOP aspect class, just add an @Aspect annotation to the class. The @Aspect annotation is used to describe an aspect class. This annotation needs to be added when defining the aspect class. The @Component annotation hands over the class to Spring for management. Implement the first step of permission verification logic in this class:

package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class PermissionFirstAdvice {

 //Define an aspect and write the path of the custom annotation in step 1 in parentheses
    @Pointcut("@annotation(com.mu.demo.annotation.PermissionAnnotation)")
    private void permissionCheck() {
    }

    @Around("permissionCheck()")
    public Object permissionCheckFirst(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("===================The first aspect===================: " + System.currentTimeMillis());

        //Get request parameters, see interface class for details
        Object[] objects = joinPoint.getArgs();
        Long id = ((JSONObject) objects[0]).getLong("id");
        String name = ((JSONObject) objects[0]).getString("name");
        System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id);
        System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);

        // If the id is less than 0, an illegal id exception will be thrown.
        if (id < 0) {
            return JSON.parseObject("{"message":"illegal id","code":403}");
        }
        return joinPoint.proceed();
    }
}

3. Create an interface class and mark the custom annotation PermissionsAnnotation on the target method:

package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/permission")
public class TestController {
    @RequestMapping(value = "/check", method = RequestMethod.POST)
    //Add this annotation
    @PermissionsAnnotation()
    public JSONObject getGroupList(@RequestBody JSONObject request) {
        return JSON.parseObject("{"message":"SUCCESS","code":200}");
    }
}

Here, we first conduct a test. First, fill in the request address and header:

Second, construct the normal parameters:

You can get normal response results:

Then, construct an exception parameter and request again:

The response result shows that the aspect class has made a judgment and returned the corresponding result:

Some people may ask, what should I do if I want to set up multiple aspect classes for verification on an interface? How to manage the execution order of these aspects?

It’s very simple. A custom AOP annotation can correspond to multiple aspect classes. The execution order of these aspect classes is managed by the @Order annotation. The smaller the number after the annotation, the earlier the aspect class is executed.

The following is demonstrated in an example:

Create a second AOP aspect class and implement the second step of permission verification in this class:

package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(0)
public class PermissionSecondAdvice {

   @Pointcut("@annotation(com.mu.demo.annotation.PermissionAnnotation)")
   private void permissionCheck() {
   }

   @Around("permissionCheck()")
   public Object permissionCheckSecond(ProceedingJoinPoint joinPoint) throws Throwable {
       System.out.println("====================The second aspect===================: " + System.currentTimeMillis());

       //Get request parameters, see interface class for details
       Object[] objects = joinPoint.getArgs();
       Long id = ((JSONObject) objects[0]).getLong("id");
       String name = ((JSONObject) objects[0]).getString("name");
       System.out.println("id->>>>>>>>>>>>>>>>>>>>>>" + id);
       System.out.println("name->>>>>>>>>>>>>>>>>>>>>>" + name);

       // If name is not an administrator, an exception will be thrown.
       if (!name.equals("admin")) {
           return JSON.parseObject("{"message":"not admin","code":403}");
       }
       return joinPoint.proceed();
   }
}

Restart the project, continue testing, and construct a situation where both parameters are abnormal:

The response results indicate that the execution order of the second aspect class is higher:

3 AOP related notes

In the above case, many annotations are used. These annotations are explained in detail below.

3.1 @Pointcut

The @Pointcut annotation is used to define a pointcut, that is, the entry point to something of concern above. The pointcut point defines the event triggering time.

@Aspect
@Component
public class LogAspectHandler {

    /**
     * Define an aspect to intercept all methods under the com.mutest.controller package and sub-packages
     */
    @Pointcut("execution(* com.mutest.controller..*.*(..))")
    public void pointCut() {}
}

The @Pointcut annotation specifies a pointcut and defines what needs to be intercepted. Here are two commonly used expressions: one is to use execution(), and the other is to use annotation().

execution expression:

Take the execution(* com.mutest.controller…(..))) expression as an example:

  • The position of the first *: indicates the return value type, and * indicates all types.

  • Package name: Indicates the name of the package that needs to be intercepted. The following two periods indicate the current package and all sub-packages of the current package. In this example, it refers to the com.mutest.controller package and all classes under the sub-package. method.

  • The position of the second * sign: represents the class name, and * represents all classes.

  • *(..): This asterisk represents the method name, * represents all methods, the following brackets represent the parameters of the method, and two periods represent any parameters.

annotation() expression:

The annotation() method is to define a pointcut for a certain annotation. For example, if we make an aspect for a method with the @PostMapping annotation, we can define the aspect as follows:

@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {}

Then using this aspect will cut into all methods annotated with @PostMapping. This method is very suitable for handling scenarios where different annotations such as @GetMapping, @PostMapping, and @DeleteMapping have various specific processing logics.

There is also the option of defining aspects for custom annotations, as shown in the above case.

@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
private void permissionCheck() {}

3.2 @Around

The @Around annotation is used to modify Around enhancement processing. Around enhancement processing is very powerful, as shown in:

  • @Around can freely choose the execution order of the enhancement action and the target method, which means that the target method can be executed before, after, or even during the enhancement action. The implementation of this feature is that the target method will be executed only when the procedd() method of the ProceedingJoinPoint parameter is called.

  • @Around can change the parameter values of the target method, and can also change the return value after executing the target method.

Around enhancement processing has the following characteristics:

  • When defining an Around enhancement processing method, the first parameter of the method must be of type ProceedingJoinPoint (at least one parameter). In the enhanced processing method body, the target method will be executed only by calling the proceed method of ProceedingJoinPoint: This is the key to @Around enhanced processing being able to fully control the timing and execution of the target method; if the program does not call the proceed method of ProceedingJoinPoint, the target method will not implement.

  • When calling the proceed method of ProceedingJoinPoint, you can also pass in an Object[] object, and the value in the array will be passed into the target method as an actual parameter – this is the key to the Around enhanced processing method that can change the parameter value of the target method. This means that if the length of the incoming Object[ ] array is not equal to the number of parameters required by the target method, or the elements of the Object[ ] array do not match the type of parameters required by the target method, an exception will occur in the program.

Although the @Around function is powerful, it usually needs to be used in a thread-safe environment. Therefore, if the problem can be solved by using ordinary Before and AfterReturning, there is no need to use Around. If you need to share some state data before and after the target method is executed, you should consider using Around. Especially when you need to use enhancement processing to prevent the execution of the target, or when you need to change the return value of the target method, you can only use Around enhancement processing.

Next, make some modifications to the previous example to observe the characteristics of @Around.

Custom annotation classes remain unchanged. First, define the interface class:

package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/permission")
public class TestController {
    @RequestMapping(value = "/check", method = RequestMethod.POST)
    @PermissionsAnnotation()
    public JSONObject getGroupList(@RequestBody JSONObject request) {
        return JSON.parseObject("{"message":"SUCCESS","code":200,"data":" + request + "}");
    }
}

The only aspect class (there are two aspect classes in the previous case, only one needs to be kept here):

package com.example.demo;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


@Aspect
@Component
@Order(1)
public class PermissionAdvice {

    @Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
    private void permissionCheck() {
    }


    @Around("permissionCheck()")
    public Object permissionCheck(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("====================Start enhancement processing===================") ;

        //Get request parameters, see interface class for details
        Object[] objects = joinPoint.getArgs();
        Long id = ((JSONObject) objects[0]).getLong("id");
        String name = ((JSONObject) objects[0]).getString("name");
        System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id);
        System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);

  //Modify input parameters
        JSONObject object = new JSONObject();
        object.put("id", 8);
        object.put("name", "lisi");
        objects[0] = object;
  
  // Pass in the modified parameters
        return joinPoint.proceed(objects);
    }
}

Also use JMeter to call the interface and pass in the parameters: {“id”:-5,”name”:”admin”}. The response result shows that @Around intercepts the input parameters of the interface and causes the interface to return the results in the aspect class. .

3.3 @Before

The method specified by the @Before annotation is executed before the aspect cuts into the target method. You can do some Log processing, and you can also do some information statistics, such as obtaining the user’s request URL and the user’s IP address, etc. This is useful when building a personal site. All methods are commonly used. For example, the following code:

@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * Execute this method before the aspect method defined above
     * @param joinPoint jointPoint
     */
    @Pointcut("execution(* com.mutest.controller..*.*(..))")
    public void pointCut() {}
    
    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("====doBefore method entered ====");

        // Get signature
        Signature signature = joinPoint.getSignature();
        // Get the package name of the cut-in
        String declaringTypeName = signature.getDeclaringTypeName();
        // Get the name of the method to be executed
        String funcName = signature.getName();
        log.info("The method to be executed is: {}, belonging to the {} package", funcName, declaringTypeName);

        // It can also be used to record some information, such as getting the URL and IP of the request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // Get the request URL
        String url = request.getRequestURL().toString();
        // Get the request IP
        String ip = request.getRemoteAddr();
        log.info("The url requested by the user is: {}, the ip address is: {}", url, ip);
    }
}

The JointPoint object is useful and can be used to obtain a signature. The signature can be used to obtain the requested package name, method name, including parameters (obtained through joinPoint.getArgs()), etc.

3.4 @After

The @After annotation corresponds to the @Before annotation. The specified method is executed after the aspect cuts into the target method. You can also do some Log processing after completing a certain method.

@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * Define an aspect to intercept all methods under the com.mutest.controller package
     */
    @Pointcut("execution(* com.mutest.controller..*.*(..))")
    public void pointCut() {}

    /**
     * Execute this method after the aspect method defined above
     * @param joinPoint jointPoint
     */
    @After("pointCut()")
    public void doAfter(JoinPoint joinPoint) {

        log.info("==== doAfter method entered ====");
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        log.info("Method {} has been executed", method);
    }
}

At this point, let’s write a Controller to test the execution results and create a new AopController as follows:

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/{name}")
    public String testAop(@PathVariable String name) {
        return "Hello " + name;
    }
}

Start the project, enter: localhost:8080/aop/csdn in the browser, and observe the output information of the console:

====doBefore method entered====
The method to be executed is: testAop, which belongs to the com.itcodai.mutest.AopController package
The url requested by the user is: http://localhost:8080/aop/name, and the ip address is: 0:0:0:0:0:0:0:1
==== doAfter method entered ====
Method testAop has been executed

From the printed Log, you can see the logic and sequence of program execution, and you can intuitively grasp the actual functions of the @Before and @After annotations.

3.5 @AfterReturning

The @AfterReturning annotation is somewhat similar to @After. The difference is that the @AfterReturning annotation can be used to capture the return value after the cut-in method is executed, and enhance the business logic of the return value, for example:

@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * Execute this method after the aspect method defined above returns, you can capture the returned object or enhance the returned object
     * @param joinPoint joinPoint
     * @param result result
     */
    @AfterReturning(pointcut = "pointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {

        Signature signature = joinPoint.getSignature();
        String classMethod = signature.getName();
        log.info("Method {} has been executed, and the return parameters are: {}", classMethod, result);
        //In actual projects, specific return value enhancements can be made based on the business.
        log.info("Business enhancement of return parameters: {}", result + "enhanced version");
    }
}

It should be noted that in the @AfterReturning annotation, the value of the returning attribute must be consistent with the parameter, otherwise it will not be detected. The second input parameter in this method is the return value of the cut method. In the doAfterReturning method, the return value can be enhanced and encapsulated accordingly according to business needs. Let’s restart the service and test again:

The method testAop is executed and the return parameter is: Hello CSDN
Business enhancements to return parameters: Hello CSDN enhanced version

3.6 @AfterThrowing

When an exception is thrown during the execution of the cut method, it will be executed in the method annotated with @AfterThrowing. In this method, some exception handling logic can be done. It should be noted that the value of the throwing attribute must be consistent with the parameter, otherwise an error will be reported. The second parameter in this method is the exception thrown.

@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * This method is executed when the aspect method defined above throws an exception.
     * @param joinPoint jointPoint
     * @param ex ex
     */
    @AfterThrowing(pointcut = "pointCut()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        //Exception handling logic
        log.info("Error executing method {}, exception: {}", method, ex);
    }
}

The complete project has been uploaded to Gtihub:

  • https://github.com/ThinkMugz/aopDemo