AOP

Spring AOP

1.什么是AOP

AOP(Aspect Oriented Programming):面向切面编程,它是⼀种思想,它是对某⼀类事情的集中处理。 AOP 是⼀种思想,而 Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和IoC 与 DI 类似。
比如
没学AOP之前,有一个项目,这个项目有8个页面,这个项目的每一个页面进去之前都需要判断用户是否登录
验证是否登录就可以归类为一类事情
学了AOP之后,就可以在AOP这个框架设置一下,把这个‘一类事情’通过AOP框架设置一下,就不需要每个页面进去之后都需要验证用户是否登录了,这样就是判断直接在AOP框架实现了,如果没登陆直接就进不了这几个页面,降低了耦合性,因为判断是一件事,进入页面也是一件事,为啥又要进页面又要判断呢,这不是复杂化了嘛
或者还是不懂的话,换一种方式理解
一个火车站有10个站台,假设安装一个小安保设备是10万,大安保设备是50万
不集中处理就是这10个站台每个站台都安装一个安保设备,花了100万,10个设备都需要保安检查,就需要花10队保安的钱,然后这时候有一队保安需要调走,于是就有一个车站空了,这时候就需要从另外9队保安调人过来,这就牵一发动全身了
集中处理就是在火车站进站口花50万安装一个大的安保设备,只需要花一队保安的钱,这时候这队保安需要走,可以再调一队过来,就可以把保安留还是调走这件事集中处理了

2.为什么要用AOP

想象⼀个场景,我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面调用的前端控制器( Controller)都需要先验证用户登录的状态,那这个时候我们要怎么处理呢?
我们之前的处理方式是每个 Controller 都要写⼀遍用户登录验证,然而当你的功能越来越多,那么你要写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会代码修改和维护的成本。那有没有简单的处理方案呢?答案是有的,对于这种功能统⼀,且使用的地方较多的功能,就可以考虑 AOP来统⼀处理了
除了统⼀的用户登录判断之外,AOP 还可以实现:
统⼀日志记录
统⼀方法执行时间统计
统⼀的返回格式设置
统⼀的异常处理
事务的开启和提交等
也就是说使用AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。

3.AOP组成

3.1切面(Aspect)

定义的是事件,也就是AOP针对哪一方面的(是用来检测登录的还是用来输出日志的)

切面相当于老板:定义公司的方向

3.2切点(Pointcut)

定义具体规则的。
比如用户登录,哪些接口需要进行用户登录,哪些不需要进行,这个就是个规则,需要让AOP按照这个规则进行

切点相当于公司中层:指定具体的方案

3.3通知(Advice)

AOP执行的具体方法
比如获取用户登录信息(session),获取到就是登录,接着往下进行,没获取到返回false

通知相当于公司的底层:具体业务执行者

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

3.4连接点(JoinPoint)

有可能触发切点的点
比如用户登录这个例子,连接点就是每个接口,因为每个接口都可能触发用户验证登录这个条件

4.Spring AOP实现原理

Spring AOP 是构建在动态代理基础上
在这里插入图片描述
这俩的区别:
1.JDK 动态代理基于接口,要求目标对象实现接口;CGLIB 动态代理基于类,可以代理没有实现接口的目标对象。
2.JDK 动态代理使用 java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 来生成代理对象;CGLIB 动态代理使用 CGLIB 库来生成代理对象。(出身不同,JDK 动态代理来源java,CGLIB来源于第三方)
3.JDK 动态代理生成的代理对象是目标对象的接口实现;CGLIB 动态代理生成的代理对象是目标对象的子类。
4.JDK 动态代理性能相对较高,生成代理对象速度较快;CGLIB 动态代理性能相对较低,生成代理对象速度较慢。JDK7之前CGLIB 性能高于JDK 动态代理,7之后就反过来了
5.CGLIB 动态代理无法代理 final 类和 final 方法;JDK 动态代理可以代理任意类。

5.AOP实战

5.1用户登录权限校验

5.1拦截器

之前的AOP的步骤太繁琐了,并且获取不到session对象,为此Spring提供了拦截器HandlerInterceptor,拦截器的实现分为以下两个步骤
1.自定义拦截器
AOP_第1张图片
UserInterceptor代码

//自定义拦截器
//HandlerInterceptor可以看成用来管理所有拦截器的管理器
//同时用注解将UserInterceptor注入到ioc容器中
@Component
public class UserInterceptor implements HandlerInterceptor
{
    //返回true,表明拦截器验证成功,继续执行后面的方法
    //返回false,表明拦截器验证失败,不会执行后面的方法
    //preHandle可以看成是执行目标方法之前的方法,因为拦截器就是在执行目标方法之前判断是否登录的
    //以下方法在idea自动生成步骤:右键->generate->override method
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        //先获取session对象,没获取到就不创建新的session对象了
        HttpSession session= request.getSession(false);
        if(session!=null&&session.getAttribute(AppVar.SESSION_KEY)!=null)
            return true;
        return false;
    }
}

AppVar代码

//这个方法是定义session的值的
public class AppVar
{
    //Session的值
    public static final String SESSION_KEY="SESSION_KEY";
}

2.将自定义拦截器配置到系统设置中,并自定义规则

AOP_第2张图片

//这个类继承了WebMvcConfigurer之后就是一个系统的配置文件
//且必须加上@Configuration注解
@Configuration
public class AppConfig implements WebMvcConfigurer
{
    @Autowired
    private UserInterceptor userInterceptor;
    //addInterceptors是添加多个拦截器,在一个项目中,拦截器可以有多个
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        //获取到拦截器
        registry.addInterceptor(userInterceptor)
                //设置拦截规则
                .addPathPatterns("/**")//拦截所有请求
                .excludePathPatterns("/user/reg")//不拦截(放行)注册请求
                .excludePathPatterns("/user/login");//不拦截(放行)登录请求
                //如果根据需求还可以放行更多的,就还继续.excludePathPatterns就行了
        ;
    }
}

拦截器已经写好了,下面来一个测试类来测试我们自己写的拦截器
AOP_第3张图片

@RestController
@RequestMapping("/user")
public class UserController
{
    //这个方法没说放行,所以访问的时候会被拦截的
    @RequestMapping("/getuser")
    public String getUser()
    {
        System.out.println("执行了getUser方法");
        return "user";
    }
    //这个没被拦截,可以执行
    @RequestMapping("/reg")
    public String reg()
    {
        System.out.println("执行了reg方法");
        return "reg";
    }
    //这个没被拦截,可以执行
    @RequestMapping("/login")
    public String login()
    {
        System.out.println("执行了login方法");
        return "login";
    }
}

AOP_第4张图片
AOP_第5张图片
怎么排除所有的静态资源,就是比如图片,就不拦截,但是图片就有好几十种格式,难道我们要一个个excludePathPatterns吗?当然不是
我们可以在static下加一个image的目录,所有的图片放这个image里,然后再excludePathPatterns
AOP_第6张图片
AOP_第7张图片
拦截器实现流程
AOP_第8张图片

5.2统一异常处理

比如后端给前端返回的响应出错了,前端的页面很懵逼,如下图
AOP_第9张图片
前端这时候不知道是哪里出现了问题
这时候就要给异常做一个统一处理了,如果是空指针异常,就返回一种让前端看的懂的方式,如果是越界异常,就返回另一种方式
这时候就要统一异常处理了
统⼀异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
下面代码是简单的处理了空指针异常
AOP_第10张图片
ExceptionAdvice代码

@RestControllerAdvice
public class ExceptionAdvice
{
    @ExceptionHandler(NullPointerException.class)
    public Result doNullPointerException(NullPointerException e)
    {
        Result result=new Result();
        result.setCode(-1);
        result.setMeg("空指针异常"+e.getMessage());
        result.setData(null);
        return result;
    }
}

Result代码

@Data
public class Result
{
    //无论什么时候,后端都返回给前端以下三个信息
    private int code;//状态码
    private String meg;//状态码的描述信息
    private Object data;//返回数据
}

前端返回页面
AOP_第11张图片
但是上述只是空指针异常,而异常有很多,难道要我们一个一个写吗?
当然不是,所有异常的父类都是Expection
于是我们可以这么写

    @ExceptionHandler(NullPointerException.class)
    public Result doException(Exception e)
    {
        Result result=new Result();
        result.setCode(-1);
        result.setMeg("异常"+e.getMessage());
        result.setData(null);
        return result;
    }

5.3统一数据返回格式

@Data
public class Result
{
    //无论什么时候,后端都返回给前端以下三个信息
    private int code;//状态码
    private String meg;//状态码的描述信息
    private Object data;//返回数据

    //返回成功对象
    public static Result success(Object data)
    {
        Result result=new Result();
        result.setCode(200);
        result.setMeg("");
        result.setData(data);
        return  result;
    }
    public static Result fail(int code,String message)
    {
        Result result=new Result();
        result.setCode(code);
        result.setMeg(message);
        result.setData(null);
        return  result;
    }
}
package com.example.demo.config;

import com.example.demo.common.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * 统一返回值的保底实现类
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper objectMapper;

    /**
     * true -> 才会调用 beforeBodyWrite 方法,
     * 反之则永远不会调用 beforeBodyWrite 方法
     *
     * @param returnType
     * @param converterType
     * @return
     */
    @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) {
        // 已经包装好的对象
        if (body instanceof Result) {
            return body;
        }
        // 对字符串进行判断和处理
        if (body instanceof String) {
            Result resultAjax = Result.success(body);
            try {
                return objectMapper.writeValueAsString(resultAjax);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return Result.success(body);
    }
}
@RestControllerAdvice
public class ExceptionAdvice
{
    @ExceptionHandler(NullPointerException.class)
    public Result doNullPointerException(NullPointerException e)
    {
        Result result=new Result();
        result.setCode(-1);
        result.setMeg("空指针异常"+e.getMessage());
        result.setData(null);
        return result;
    }
    @ExceptionHandler(NullPointerException.class)
    public Result doException(Exception e)
    {
        Result result=new Result();
        result.setCode(-1);
        result.setMeg("异常"+e.getMessage());
        result.setData(null);
        return result;
    }
}

AOP_第12张图片

6. @ControllerAdvice 源码分析(了解)

通过对 @ControllerAdvice 源码的分析我们可以知道上⾯统⼀异常和统⼀数据返回的执行流程,我们先从 @ControllerAdvice 的源码看起,点击 @ControllerAdvice 实现源码如下
AOP_第13张图片
从上述源码可以看出 @ControllerAdvice 派生于 @Component 组件,而所有组件初始化都会调用InitializingBean 接口。所以接下来我们来看 InitializingBean 有哪些实现类?在查询的过程中我们发现了,其中 Spring MVC中的实现子类是RequestMappingHandlerAdapter,它里面有⼀个方法 afterPropertiesSet() 方法,表示所有的参数设置完成之后执行的方法,如下图所示:
AOP_第14张图片
而这个方法中有⼀个 initControllerAdviceCache 方法,查询此方法的源码如下
AOP_第15张图片
我们发现这个方法在执行是会查找使用所有的@ControllerAdvice 类,这些类会被容器中,但发生某个事件时,调用相应的 Advice 方法,比如返回数据前调⽤统⼀数据封装,比如发生异常是调用异常的Advice 方法实现。

你可能感兴趣的:(状态模式,spring,java,后端,spring,boot,mvc)