Spring AOP统一功能处理示例代码

Java
290
0
0
2023-07-23
标签   Spring
目录
  • 1. 什么是Spring AOP?
  • 2. 为什要用 AOP?
  • 3. Spring AOP 应该怎么学习呢?
  • 3.1AOP组成
  • 3.1.1 切面(Aspect)
  • 3.1.2 连接点(Join Point)
  • 3.1.3 切点(Pointcut)
  • 3.1.4 通知(Advice)
  • 3.2 Spring AOP实现
  • 3.2.1 添加 AOP 框架支持
  • 3.2.2 定义切面和切点。
  • 3.2.3 定义相关通知
  • 3.3 Spring AOP 实现原理
  • 3.3.1 动态代理
  • 3.3.2 JDK和CGLIB实现的区别
  • 3.3.3 织入(Weaving):代理的生成时机
  • 3.3.4 总结
  • 4. SpringBoot 统一功能处理
  • 4.1 用户登录权限效验
  • 4.1.1 Spring拦截器
  • 4.1.2 自定义拦截器
  • 4.1.3 将自定义拦截器加入到系统配置
  • 4.1.4 拦截器实现原理
  • 4.1.5 拦截器小结
  • 4.1.6 扩展:统⼀访问前缀添加
  • 4.2 统一异常处理
  • 4.2 统一数据返回格式
  • 4.2.1 为什么需要统一数据返回格式?
  • 4.2.2 统一数据返回格式的实现
  • 总结

1. 什么是Spring AOP?

在介绍Spring AOP之前,首先要了解一下什么是AOP?

AOP (Aspect Oriented Programming)︰面向切面编程,它是一种思想,它是对某一类事情的集中处理。比如用户登录权限的效验,没学AOP之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法,然而有了AOP之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了。

而AOP是一种思想,而Spring AOP是一个框架,提供了一种对AOP思想的实现,它们的关系和loC与DI类似。

2. 为什要用 AOP?

我们之前的处理方式是每个Controller都要写一遍用户登录验证,然而当你的功能越来越多,那么你要写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会代码修改和维护的成本。那有没有简单的处理方案呢?答案是有的,对于这种功能统一,且使用的地方较多的功能,就可以考虑AOP来统一处理了。

除了统一的用户登录判断之外,AOP还可以实现:

  • 统一日志记录
  • 统一方法执行时间统计
  • 统一的返回格式设置
  • 统一的异常处理
  • 事务的开启和提交等

也就是说使用AOP可以扩充多个对象的某个能力,所以AOP可以说是OOP (Object OrientedProgramming,面向对象编程)的补充和完善。

3. Spring AOP 应该怎么学习呢?

Spring AOP学习主要分为以下3个部分:

1.学习AOP是如何组成的?也就是学习AOP组成的相关概念。

2.学习Spring AOP使用。

3.学习Spring AOP实现原理。下面我们分别来看。

3.1AOP组成

3.1.1 切面(Aspect)

切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。

切面是包含了:通知、切点和切面的类,相当于AOP实现的某个功能的集合。

3.1.2 连接点(Join Point)

应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

连接点相当于需要被增强的某个AOP功能的所有方法。

3.1.3 切点(Pointcut)

Pointcut是匹配Join Point的谓词。

Pointcut 的作用就是提供一组规则(使用AspectJ pointcut expression language来描述)来匹配Join Point,给满足规则的Join Point添加Advice。

切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)。

3.1.4 通知(Advice)

切面也是有目标的——它必须完成的工作。在AOP术语中,切面的工作被称之为通知。

通知︰定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。

Spring切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:

  • 前置通知使用@Before:通知方法会在目标方法调用之前执行。
  • 后置通知使用@After:通知方法会在目标方法返回或者抛出异常后调用。
  • 返回之后通知使用@AfterReturning:通知方法会在目标方法返回后调用。
  • 抛异常后通知使用@AfterThrowing:通知方法会在目标方法抛出异常后调用。
  • 环绕通知使用@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。

切点相当于要增强的方法。

AOP整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例:

在这里插入图片描述

3.2 Spring AOP实现

使用Spring AOP来实现一下AOP的功能,完成的目标是拦截所有UserController里面的方法,每次调用UserController中任意一个方法时,都执行相应的通知事件。

Spring AOP 的实现步骤是:

  • 添加 AOP 框架支持。
  • 定义切面和切点。
  • 定义通知。

3.2.1 添加 AOP 框架支持

3.2.2 定义切面和切点。

3.2.3 定义相关通知

通知定义的是被拦截的方法具体要执行的业务,比如用户登录权限验证方法就是具体要执行的业务Spring AOP中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:

  • 前置通知使用@Before:通知方法会在目标方法调用之前执行。
  • 后置通知使用@After:通知方法会在目标方法返回或者抛出异常后调用。
  • 返回之后通知使用@AfterReturning:通知方法会在目标方法返回后调用。
  • 抛异常后通知使用@AfterThrowing:通知方法会在目标方法抛出异常后调用。
  • 环绕通知使用@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。

具体实现如下:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
    // 定义切点⽅法
    @Pointcut("execution(* com.example.demo.controller.UserController.*
(..))")
    public void pointcut(){ }
    // 前置通知
    @Before("pointcut()")
    public void doBefore(){
        System.out.println("执⾏ Before ⽅法");
    }
    // 后置通知
    @After("pointcut()")
    public void doAfter(){
        System.out.println("执⾏ After ⽅法");
    }
    // return 之前通知
    @AfterReturning("pointcut()")
    public void doAfterReturning(){
        System.out.println("执⾏ AfterReturning ⽅法");
    }
    // 抛出异常之前通知
    @AfterThrowing("pointcut()")
    public void doAfterThrowing(){
        System.out.println("执⾏ doAfterThrowing ⽅法");
    }
    
    // 添加环绕通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        Object obj = null;
        System.out.println("Around ⽅法开始执⾏");
        try {
            // 执⾏拦截⽅法
           obj = joinPoint.proceed();
           } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("Around ⽅法结束执⾏");
        return obj;
    }
}

3.3 Spring AOP 实现原理

3.3.1 动态代理

Spring AOP是构建在动态代理基础上,因此Spring对AOP的支持局限于方法级别的拦截。

Spring AOP支持JDK ProxyCGLIB方式实现动态代理。默认情况下,实现了接口的类,使用AOP会基于JDK生成代理类,没有实现接口的类,会基于CGLIB生成代理类。

在这里插入图片描述

这两种方式的代理目标都是被代理类中的方法,在运行期,动态的织入字节码生成代理类

3.3.2 JDK和CGLIB实现的区别

  • JDK实现,要求被代理类必须实现接口,之后是通过InvocationHandler 及 Proxy,在运行时动态的在内存中生成了代理类对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成。
  • CGLIB实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类

3.3.3 织入(Weaving):代理的生成时机

织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。

在目标对象的生命周期里有多个点可以进行织入∶

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载器:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器 (ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving. LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某一时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。SpringAOP就是以这种方式织入切面的。

此种实现在设计模式上称为动态代理模式,在实现的技术手段上,都是在class代码运行期,动态的织入字节码生成代理类。

3.3.4 总结

AOP是对某方面能力的统一实现,它是一种实现思想,Spring AOP是对AOP的具体实现,SpringAOP可通过AspectJ(注解) 的方式来实现AOP的功能,Spring AOP 的实现步骤是:

  • 添加AOP框架支持。
  • 定义切面和切点。
  • 定义通知。

Spring AOP是通过动态代理的方式,在运行期将AOP代码织入到程序中的,它的实现方式有两种JDK Proxy和CGLIB。

4. SpringBoot 统一功能处理

接下来是Spring Boot统一功能处理模块了,也是AOP的实战环节,要实现的课程目标有以下3个:

  • 统一用户登录权限验证;
  • 统—数据格式返回;
  • 统一异常处理。

接下我们一个一个来看。

4.1 用户登录权限效验

用户登录权限的发展从之前每个方法中自己验证用户登录权限,到现在统一的用户登录验证处理,它是—个逐渐完善和逐渐优化的过程。

4.1.1 Spring拦截器

Spring 中提供了具体的实现拦截器:HandlerInterceptor,

统一用户登录权限的效验使用WebMvcConfigurer + HandlerInterceptor来实现。

拦截器的实现分为以下两个步骤∶

  • 创建自定义拦截器,实现 HandlerInterceptor接口的preHandle (执行具体方法之前的预处理)方法。
  • 将自定义拦截器加入 WebMvcConfigurer的addInterceptors方法中。具体实现如下。

4.1.2 自定义拦截器

接下来使用代码来实现一个用户登录的权限效验,自定义拦截器是一个普通类,具体实现代码如下

import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
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("userinfo") != null) {
            return true;
        }
        response.setStatus();
        return false;
    }
}

4.1.3 将自定义拦截器加入到系统配置

将上一步中的自定义拦截器加入到系统配置信息中,具体实现代码如下:

@Configuration
public class AppConfig implements WebMvcConfigurer {
    // 添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 拦截所有接⼝
                .excludePathPatterns("/art/param"); // 排除接⼝
    }
}

其中:

  • addPathPatterns:表示需要拦截的URL,“**”表示拦截任意方法(也就是所有方法)。
  • excludePathPatterns:表示需要排除的URL。

说明:以上拦截规则可以拦截此项目中的使用URL,包括静态文件(图片文件、JS和CSS等文件

排除所有的静态资源

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

4.1.4 拦截器实现原理

正常情况下的调用顺序:

在这里插入图片描述

然而有了拦截器之后,会在调用Controller 之前进行相应的业务处理,执行的流程如下图所示:

在这里插入图片描述

4.1.5 拦截器小结

通过上面的源码分析,我们可以看出,Spring 中的拦截器也是通过动态代理环绕通知的思想实现的大体的调用流程如下:

在这里插入图片描述

4.1.6 扩展:统⼀访问前缀添加

所有请求地址添加 api 前缀:

@Configuration
public class AppConfig implements WebMvcConfigurer {
    // 所有的接⼝添加 api 前缀
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api", c -> true);
    }
}

@Configuration
public class AppConfig implements WebMvcConfigurer {
    // 所有的接⼝添加 api 前缀
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api", c -> true);
    }
}

其中第二个参数是⼀个表达式,设置为 true 表示启动前缀。

4.2 统一异常处理

统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,

@ControllerAdvice表示控制器通知类,@ExceptionHandler是异常处理器,两个结合表示当出现异常的时候执行某个通知就是执行某个方法事件,具体实现代码如下:

import java.util.HashMap;

@ControllerAdvice
public class ErrorAdive {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handler(Exception e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("success",);
        map.put("status",);
        map.put("msg", e.getMessage());
        return map;
    }
}
PS:方法名和返回值可以自定义,其中最重要的是@ExceptionHandler(Exception.class)注解.

以上方法表示,如果出现了异常就返回给前端一个HashMap的对象,其中包含的字段如代码中定义的那样。

我们可以针对不同的异常,返回不同的结果,比以下代码所示:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;

@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public Object exceptionAdvice(Exception e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", -);
        result.put("message", "总的异常信息:" + e.getMessage());
        result.put("data", null);
        return result;
    }
    @ExceptionHandler(NullPointerException.class)
    public Object nullPointerexceptionAdvice(NullPointerException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", -);
        result.put("message", "空指针异常:" + e.getMessage());
        result.put("data", null);
        return result;
    }
}

当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配,案例演示。在UserController中设置一个空指针异常,实现代码如下:

@RestController
@RequestMapping("/u")
public class UserController {
    @RequestMapping("/index")
    public String index() {
        Object obj = null;
        int i = obj.hashCode();
        return "Hello,User Index.";
    }
}

以上程序的执行结果如下:

在这里插入图片描述

4.2 统一数据返回格式

4.2.1 为什么需要统一数据返回格式?

统一数据返回格式的优点有很多,比如以下几个:

  • 方便前端程序员更好的接收和解析后端数据接口返回的数据。
  • 降低前端程序员和后端程序员的沟通成本,这按照某个格式实现就行了,因为所有接口都是这样返回的。
  • 有利于项目统—数据的维护和修改。
  • 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容。

4.2.2 统一数据返回格式的实现

统一的数据返回格式可以使用

@ControllerAdvice + ResponseBodyAdvice 的方式实现,具体实现代码如下:

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    /**
     * 内容是否需要重写(通过此⽅法可以选择性部分控制器和⽅法进⾏重写)
     * 返回 true 表示重写
     */
    @Override
    public boolean supports(MethodParameter returnType, Class 
converterType) {
        return true;
    }
    /**
     * ⽅法返回之前调⽤此⽅法
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter 
		returnType, MediaType selectedContentType, Class selectedConverterType, 
		ServerHttpRequest request, ServerHttpResponse response) {
        // 构造统⼀返回对象
        HashMap<String, Object> result = new HashMap<>();
        result.put("success",);
        result.put("message", "");
        result.put("data", body);
        return result;
    }
}

总结

  • 统一用户登录权限的效验使用WebMvcConfigurer + HandlerInterceptor来实现,
  • 统一异常处理使用 @ControllerAdvice + @ExceptionHandler 来实现,
  • 统一返回值处理使用 @ControllerAdvice + ResponseBodyAdvice 来处理。