Spring AOP and unified processing

1. Spring AOP 

1.What is Spring AOP 

AOP (Aspect Oriented Programming): Aspect-oriented programming, it is an idea, it is a concentrated processing of a certain type of things .

2.The role of AOP

Imagine a scenario. When we are building a backend system, except for several functions such as login and registration, which do not require user login verification, the front-end controller (Controller) called by almost all other pages needs to verify the user login status first. Then How should we deal with this situation?
Our previous approach was to write user login verification for each Controller. However, as you have more and more functions, you will have to write more and more login verifications, and these methods are the same. There are so many This method will reduce the cost of code modification and maintenance. Is there any simple solution? The answer is yes. For functions that have unified functions and are used in many places, you can consider AOP for unified processing.

  • In addition to unified user login judgment, AOP can also achieve:
  • Unified logging
  • Unified method execution time statistics
  • Unified return format settings
  • Unified exception handling
  • Opening and submitting transactions, etc.
  • AOP is a complement to OOP 

 3.Related concepts of AOP

1.Aspect

Aspect consists of pointcut and advice. It includes both the definition of cross-cutting logic and

Includes the definition of connection points.

2.Join Point

A point in the application execution process can be inserted. This point can be when a method is called, when an exception is thrown, or even when a field is modified. Aspect code can use these points to insert into the normal flow of the application and add new behaviors

3. Pointcut

Pointcut is a predicate matching Join Point. The function of Pointcut is to provide a set of rules (described using AspectJ pointcut expression language) to match Join Points and add Advice to Join Points that meet the rules.

4.Advice

 An aspect also has a goal—a job it must accomplish. In AOP terminology, the aspect's work is called advice.

Notification: It defines what the aspect is and when to use it. It describes the work to be done by the aspect and also resolves when to perform this work.

question.

In the Spring aspect class, you can use the following annotations on the method. The method will be set as a notification method. When the conditions are met, the owner will be notified.

method to call:

  • Pre-notification uses @Before: the notification method will be executed before the target method is called.
  • Post notification uses @After: the notification method will be called after the target method returns or throws an exception.
  • Notification after return uses @AfterReturning: the notification method will be called after the target method returns.
  • Notification after throwing an exception uses @AfterThrowing: the notification method will be called after the target method throws an exception.
  • Around notifications use @Around: the notification wraps the notified method and is executed before the notified method is notified and after the call.
  • Perform custom behavior.

 4.Implementation of Spring AOP

1. Add AOP framework support

Add the following configuration in pom.xml:

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
<!--        Springboot test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--      Spring AOP 框架-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

2. Define tangent planes and tangent points

@Component
@Slf4j
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))")
    public void pointcut() {
    }

}

The @Aspect class annotation indicates that LoginAspect is an aspect class, and @Pointcut indicates that a cut point is defined, and the content thereof represents the rules of the connection point, that is, which classes or methods belong to this cut point and connection point.

The pointcut method is an empty method. It does not need a method body. This method name serves as an "identifier" to identify the following.

The notification method specifically refers to which cut point (because there may be many cut points)

AspectJ supports three wildcard characters

*: Matches any character, only matches one element (package, class, or method, method parameter)

..: matches any character and can match multiple elements. When representing a class, it must be used in conjunction with *.

+: Indicates that all classes matching the specified class according to type must be followed by the class name, such as com.cad.Car+, indicating that the class inherits

all subclasses including itself

Pointcut expressions consist of pointcut functions, among which execution() is the most commonly used pointcut function and is used to match methods. The syntax is:

execution(<modifier><return type><package.class.method(parameter)><exception>)

Modifiers and exceptions can be omitted, and their specific meanings are as follows:

The meaning we defined above is to match all methods under the com.javastudy.springaopdemo5.controller.LoginController class.

3. Define relevant notifications

First define the content of the controller layer

@RestController
@Slf4j
@RequestMapping("/user")
public class LoginController {

    @RequestMapping("/login")
    public String login() {
        log.info("login...");
        return "login...";
    }

    @RequestMapping("/register")
    public String register() {
        log.info("register...");
        return "register...";
    }

    @RequestMapping("/get")
    public String get() {
        log.info("get...");
        return "get...";
    }

}

1. Pre-notification @Before

The notification method will be executed before the target method is called.

Note: The content in @Before represents the pointcut, that is, in which interfaces these notifications are executed.

@Component
@Slf4j
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))")
    public void pointcut() {
    }

    //前置通知
    @Before("pointcut()")
    public void doBefore() {
        log.info("do before....");
    }


}

 When we access any page, the console prints the following log

You can see that the doBefore notification will be executed first.

2. Post notification @After

The notification method will be called after the target method returns or throws an exception .

    //后置通知
    @After("pointcut()")
    public void doAfter() {
        log.info("do after...");
    }

 To simulate abnormal situations, you can add 10/0 to a method in LoginController and observe

    @RequestMapping("/login")
    public String login() {
        log.info("login...");
        int i=10/0;
        return "login...";
    }

It can be observed that after the method exception, the @After notification will still be executed.

3. Notify @AfterReturning after return

The notification method will be called after the target method returns .

    // return 之前通知
    @AfterReturning("pointcut()")
    public void doAfterReturning() {
        log.info("do after returning...");
    }

When we access the login interface (10/0, there is an exception), observe whether there is output

There is no output at this time.

Access other interfaces, no abnormal interfaces

 At this time, the @AfterReturning notification is executed normally.

4. Notify @AfterThrowing after throwing an exception

The notification method is called after the target method throws an exception.

    //抛出异常之前通知
    @AfterThrowing("pointcut()")
    public void doAfterThrowing() {
        log.info("do after throwing");
    }

Execute the interface with exception and print the log

Execute the interface without exception and print no logs 

5. Surround notification @Around

The notification wraps the notified method and performs custom behavior before and after the notified method is notified and called.

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object oj = null;
        log.info("环绕通知执行之前...");
        log.info(joinPoint.getSignature().toLongString());
        try {
            oj = joinPoint.proceed();//调用目标方法
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        log.info("环绕通知执行之后....");
        return oj;
    }

Execute the interface without exception:

Interface with exception in execution: 

Next let’s look at the common methods of ProceedingJoinPoint

Information about the location of the connection point
toShortString Brief information about the location of the connection point
toLongString All information about the location of the connection point
getThis Returns the AOP proxy object, which is com.sun.proxy.$Proxy18
getTarget Returns the target object (the interface or class in which the method is defined)
getArgs() Returns the notified method parameter list
getSignature Returns the current connection point signature, and its getName() method returns the FQN of the method

Execute all notifications and observe the order of surrounding notifications, pre-notifications and post-notifications

 It can be observed that the surrounding notification precedes before and follows after.

If a pointcut contains only one notification, then we can put the pointcut rule on the notification

@Component
@Slf4j
@Aspect
public class LoginAspect {
    //前置通知
    @Before("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))")
    public void doBefore() {
        log.info("do before....");
    }


}

4. Spring AOP implementation principle

Spring AOP is built on the basis of dynamic proxy , so Spring's support for AOP is limited to method-level interception.

Spring AOP supports JDK Proxy and CGLIB methods to implement dynamic proxy. By default, classes that implement the interface make

Using AOP will generate proxy classes based on JDK. For classes that do not implement interfaces, proxy classes will be generated based on CGLIB.

Agent mode:

1. Static proxy

1. Define the interface
public interface PayService {
    void pay();
}
2. Implement the interface
public class AliPayService implements PayService{
    @Override
    public void pay() {
        System.out.println("ali pay...");
    }
}
3. Create a proxy class and implement the payment interface as well
public class StaticProxy implements PayService {
    private final PayService payService;

    public StaticProxy(PayService payService) {
        this.payService = payService;
    }

    @Override
    public void pay() {
        System.out.println("before...");
        payService.pay();
        System.out.println("after...");
    }
}
4.Actual use
    public static void main(String[] args) {
        PayService service = new AliPayService();
        PayService proxy = new StaticProxy(service);
        proxy.pay();
    }

 Static proxy has a big disadvantage, that is, when there are many different interfaces, we need to define many proxy classes to implement different interfaces. When the functions implemented by our proxy are the same, but there are multiple interfaces, it is completed at this time So many proxy classes are very troublesome, so our dynamic proxy is needed at this time.

2. Dynamic proxy

1.JDK dynamic proxy

From the JVM perspective, a dynamic proxy dynamically generates class bytecode at runtime and loads it into the JVM.

As far as Java is concerned, there are many ways to implement dynamic proxy, such as JDK dynamic proxy, CGLIB dynamic proxy and so on.

Define JDK dynamic proxy class

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @author Chooker
 * @create 2023-07-27 22:36
 */

public class JDKInvocationHandler implements InvocationHandler {
    //⽬标对象即就是被代理对象
    private Object target;

    public JDKInvocationHandler(Object target) {
        this.target = target;
    }
    //proxy代理对象

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过反射调⽤被代理类的⽅法
        Object retVal = method.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
}

Create a proxy object and use 

    public static void main(String[] args) {
        PayService target = new AliPayService();
        //创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{PayService.class},
                new JDKInvocationHandler(target)
        );
        proxy.pay();
    }

Disadvantages: JDK's dynamic proxy must have an interface

2.CGLIB dynamic proxy

CGLIB dynamic proxy class usage steps

1. Define a class;

2. Customize MethodInterceptor and rewrite the intercept method. Intercept is used for interception enhancement.

The method of the proxy class is similar to the invoke method in JDK dynamic proxy;

3. Create a proxy class through create() of the Enhancer class

Add dependencies (if you are creating a Spring project, there is no need to introduce it, because the bottom layer of Spring has already introduced the cglib framework)

<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib</artifactId>
 <version>3.3.0</version>
</dependency>

Custom MethodInterceptor (method interceptor)

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @author Chooker
 * @create 2023-07-27 22:48
 */
public class CGLIBInterceptor implements MethodInterceptor {
    //被代理对象
    private Object target;

    public CGLIBInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过cglib的代理⽅法调⽤
        Object retVal = methodProxy.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }


}

1. obj: the proxy object (the object that needs to be enhanced)

2. method: intercepted method (method that needs to be enhanced)

3. args: method input parameters

4. proxy: used to call the original method

 Create a proxy class and use 

    public static void main(String[] args) {
        PayService target = new AliPayService();
        PayService proxy = (PayService) Enhancer.create(target.getClass(), new CGLIBInterceptor(target));
        proxy.pay();
    }

Comparison between JDK dynamic proxy and CGLIB dynamic proxy

1. JDK dynamic proxy can only proxy classes that implement interfaces or directly proxy interfaces, while CGLIB can proxy classes that do not implement any interfaces.

2. CGLIB dynamic proxy intercepts method calls of the proxied class by generating a subclass of the proxied class, so the proxy cannot be declared final. 

Performance: In most cases, JDK dynamic proxy is better. With the upgrade of JDK version, this advantage becomes more obvious.

Spring proxy selection

1. proxyTargetClass is false, the target implements the interface, and uses jdk proxy

2. proxyTargetClass is false, the target does not implement the interface, use cglib proxy

3. proxyTargetClass is true, use cglib proxy

Weaving: The timing of proxy generation.
Weaving is the process of applying aspects to the target object and creating a new proxy object. The aspects are weaved into the target object at the specified connection point
.
There are multiple points in the target object's life cycle where weaving can occur:

  • Compile time: aspects are woven into the target class when it is compiled. This approach requires a special compiler. AspectJ's weaving compiler weaves aspects in this way.
  • Class loading period: aspects are woven in when the target class is loaded into the JVM. This approach requires a special class loader (ClassLoader), which can enhance the bytecode of the target class before it is introduced into the application. AspectJ5's load-time weaving (LTW) supports weaving aspects in this way.
  • Runtime: Aspects are woven in at a certain moment when the application is running. Generally, when weaving aspects, the AOP container will dynamically create a proxy object for the target object. SpringAOP weaves aspects into this way.

What we learned above is the principle of Spring AOP, which is the bottom layer of what we will learn next. Some common functions of SpringBoot are encapsulated, and the bottom layer is implemented using AOP.

2. SpringBoot unified function processing

It is necessary to implement the verification function of the user's login permission. In the Servlet stage, we can save the user's information in the Session. Then each page first determines whether the user's information exists in the session. If it exists, it means that the user has logged in. If not, it will jump to the login page.

1. Spring AOP user unified login verification problem

Our first thought is to solve this problem through surround notifications. We can use surround notifications for pages other than login and registration to determine whether the user has logged in. However, the following two problems will arise.

1.. There is no way to obtain the HttpSession object.

2. We need to intercept some methods, but not other methods. For example, registration methods and login methods are not intercepted. In this case, the rules for excluding methods are difficult to define, or even impossible to define.

So how to solve it?

2.Spring interceptor

For the above problems, Spring provides a specific implementation interceptor: HandlerInterceptor. The implementation of the interceptor is divided into the following two steps:

1. Create a custom interceptor and implement the preHandle (preprocessing before executing specific methods) method of the HandlerInterceptor interface.

2. Add the custom interceptor to the addInterceptors method of WebMvcConfigurer

1. Custom interceptor

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);

        if (session != null && session.getAttribute("username") != null) {
            //通过
            return true;
        }
        //没有权限访问
        response.setStatus(401);
        return false;
    }
}

2. Add the custom interceptor to the system configuration

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).
                //表示拦截所有的路径
                addPathPatterns("/**").
                //不拦截login接口
                excludePathPatterns("/login").
                //不拦截register接口
                excludePathPatterns("/register");
    }
}

in:

addPathPatterns: indicates the URL that needs to be intercepted, "**" indicates intercepting any method (that is, all methods).

excludePathPatterns: Indicates URLs that need to be excluded.

Note: The above interception rules can intercept the URLs used in this project, including static files (image files, JS, CSS, etc.).

Exclude all static resources

    // 拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 拦截所有接⼝
                .excludePathPatterns("/**/*.js")
                .excludePathPatterns("/**/*.css")
                .excludePathPatterns("/**/*.jpg")
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/**/login"); // 排除接⼝
    }

Expand the following, you can add the addition of unified prefix in it

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")// 拦截所有url
                .excludePathPatterns("/api/user/login")
                .excludePathPatterns("/api/user/reg");
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api", c -> true);
    }
}

3.Controller interface imitates login

@RestController
@Slf4j
@RequestMapping("/user")
public class LoginController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request, String username, String password) {
        log.info("login...");
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return false;
        }
        //此时表示账号密码正确
        if ("admin".equals(username) && "123456".equals(password)) {
            HttpSession session = request.getSession(true);
            session.setAttribute("username", username);
            return true;
        }
        return false;

    }

    @RequestMapping("/register")
    public String register() {
        log.info("register...");
        return "register...";
    }

    @RequestMapping("/get")
    public String get() {
        log.info("get...");
        return "get...";
    }

}

When we access the get interface directly: 401 is displayed, indicating no permission.

Normal access to the login and register interfaces can be achieved

 

 At this time we use the correct account and password to log in: you can see that you have logged in correctly at this time

 At this time we access the get interface again: you can see that the access is correct at this time

 3. Implementation principle of interceptor

The calling sequence under normal circumstances:

 However, with the interceptor, corresponding business processing will be performed before calling the Controller. The execution process is shown in the figure below:

The interceptor is based on AOP and Spring is based on Servlet 

3. Unified exception handling

Unified exception handling is implemented using @ControllerAdvice + @ExceptionHandler. @ControllerAdvice represents the controller notification class, and @ExceptionHandler is the exception handler. The combination of the two means executing a certain notification when an exception occurs, that is, executing a certain method. event, the specific implementation code is as follows
@ControllerAdvice
@ResponseBody
public class ErrorHandler {

    @ExceptionHandler(Exception.class)
    public Object error(Exception e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("success", 0);
        map.put("status", -1);
        map.put("msg", e.getMessage());
        return map;

    }

    @ExceptionHandler(NullPointerException.class)
    public Object error2(NullPointerException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", 0);
        result.put("status", -2);

        result.put("message", "空指针异常:" + e.getMessage());
        return result;
    }

    @ExceptionHandler(ArithmeticException.class)
    public Object error2(ArithmeticException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", 0);
        result.put("status", -3);

        result.put("message", "算数异常:" + e.getMessage());
        return result;
    }
}

controller

@RestController
@Slf4j
@RequestMapping("/error")
public class ErrorController {

    @RequestMapping("/test1")
    public boolean test1() {
        int i = 10 / 0;
        return true;
    }

    @RequestMapping("/test2")
    public boolean test2() {
        String a = null;
        a.length();
        return true;
    }

    @RequestMapping("/test3")
    public String test3() {
        throw new RuntimeException("test3手动创建异常");
    }
}
When there are multiple exception notifications, the matching order is the current class and its subclasses in upward order.

Visit test1

Visit test2

Visit test3

It can be observed that when the error exception is a subclass, the matching order is the current class and its subclasses in upward order.

3. Unified data return format

1. Why is it necessary to unify the return format ?

There are many advantages to a unified data return format, such as the following:

  1. It is convenient for front-end programmers to better receive and parse the data returned by the back-end data interface.
  2. To reduce the communication cost between front-end programmers and back-end programmers, just implement it in a certain format, because all interfaces are returned in this way.
  3. Conducive to the maintenance and modification of unified project data.
  4. It is conducive to the formulation of unified and standardized standards for the back-end technology department, and there will be no weird return content.

2. Implementation of unified data return format

A unified data return format can be implemented using @ControllerAdvice + ResponseBodyAdvice. The specific implementation code is as follows:
@ControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice {

    /**
     * 内容是否需要重写(通过此⽅法可以选择性部分控制器和⽅法进⾏重写)
     * 返回 true 表示重写
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
    /**
     * ⽅法返回之前调⽤此⽅法
     */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 构造统⼀返回对象
        HashMap<String, Object> result = new HashMap<>();
        result.put("state", 1);
        result.put("msg", "");
        result.put("data", body);
        if(body instanceof String){
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(result);
        }
        return result;
    }
}

If there is no if((body instanceof String)) code, the following error will occur

 controller:

@RestController
@Slf4j
@RequestMapping("/user")
public class LoginController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request, String username, String password) {
        log.info("login...");
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return false;
        }
        //此时表示账号密码正确
        if ("admin".equals(username) && "123456".equals(password)) {
            HttpSession session = request.getSession(true);
            session.setAttribute("username", username);
            return true;
        }
        return false;

    }

    @RequestMapping("/register")
    public String register() {
        log.info("register...");
        return "register...";
    }

    @RequestMapping("/get")
    public String get() {
        log.info("get...");
        return "get...";
    }

}

Guess you like

Origin blog.csdn.net/qq_64580912/article/details/131987339