• 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.自定义拦截器
    在这里插入图片描述
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    AppVar代码

    //这个方法是定义session的值的
    public class AppVar
    {
        //Session的值
        public static final String SESSION_KEY="SESSION_KEY";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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就行了
            ;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    拦截器已经写好了,下面来一个测试类来测试我们自己写的拦截器
    在这里插入图片描述

    @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";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

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

    5.2统一异常处理

    比如后端给前端返回的响应出错了,前端的页面很懵逼,如下图
    在这里插入图片描述
    前端这时候不知道是哪里出现了问题
    这时候就要给异常做一个统一处理了,如果是空指针异常,就返回一种让前端看的懂的方式,如果是越界异常,就返回另一种方式
    这时候就要统一异常处理了
    统⼀异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
    下面代码是简单的处理了空指针异常
    在这里插入图片描述
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Result代码

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

    前端返回页面
    在这里插入图片描述
    但是上述只是空指针异常,而异常有很多,难道要我们一个一个写吗?
    当然不是,所有异常的父类都是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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    @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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述

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

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

  • 相关阅读:
    Explainable-ZSL
    不可变数据的制造艺术:Immutable的应用
    “熊猫视图”.Net图形控件功能介绍 [十二]:图像图元
    【MySQL】的存储过程
    Tmux 使用教程
    微服务实践之全链路追踪(sleuth,zipkin)详解-SpringCloud(2021.0.x)-4
    线程的概念及使用
    涂鸦云平台设备授权介绍
    一表看懂RPA与传统自动化的区别
    贴花、射线、动画通知——足迹01
  • 原文地址:https://blog.csdn.net/weixin_73000974/article/details/133588657