Springboot APO面向切面编程

Springboot APO面向切面编程

面向切面编程是对面向对象编程的补充

面向对象编程(OOP)的好处是显而易见的额,缺点也同样明显。当需要为多个不具有集成关系的对象天剑一个公共方法的时候,例如日志记录、性能监控等,如果采用面向对象编程的方式,会产生较大的重复工作和大量的重复代码,不利于维护。面向切面编程(AOP)是对面向对象编程的补充,简单来说就是统一处理某一"切面"的问题的编程思想,如果使用AOP的方式进行日志记录和处理,所有的日志代码都集中在一处,不需要再每个方法里面都去添加,极大减少了重复代码。利用AOP可以对我们边缘业务进行隔离,降低无关业务逻辑耦合性。提高程序的可重用性,同时提高了开发的效率。一般用于日志记录,性能统计,安全控制,权限管理,事务处理,异常处理,资源池管理

我们可以这样理解面向切面编程,在面向对象编程中,对象都是根据类来进行实例化的,类这个模具如果是生产面包,那么该类实例化的对象就是一个成型的面包,对类中成员变量进行赋值后,实例化的对象则无法改变,等着被使用,被回收等。面向切面编程,对于我们封装好的类,我们可以在编译期间或者运行期间,对其进行切割,在原有的方法里面织入一些新代码,对原有方法进行代码增强,即把面包切开,可以加入一个辅料,使得面包更美味。

基本概念

  • 目标对象(Target)

    指将要被增强的对象,即业务类对象,或者说是被切面所通知的对象。
    
  • 切面(Aspect)

    Aspect通常是一个类,里面定义了切入点和通知,它既包含了横切逻辑的定义,也包括了切入点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
    可以简单地认为, 使用 @Aspect 注解的类就是切面
    
    /**
     * 定义切面
     */
    @Aspect
    @Component
    public class LogAspect {    
    }
    
  • 连接点(JoinPoint)

    接点就是程序执行的某个特定的位置,如:类开始初始化前、类初始化后、类的某个方法调用前、类的某个方法调用后、方法抛出异常后等。因为Spring只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法。
    
    @Before("pointcut()")
    public void log(JoinPoint joinPoint) { //这个JoinPoint参数就是连接点
    }
    
  • 切入点(pointCut)

    定义了在"什么地方"进行切入,即要对哪些类中的哪些方法进行增强,进行切割,指的是被增强的方法。即要切哪些东西。
    Spring缺省使用AspectJ切入点语法。切入点就是提供一组规则(使用AspectJ pointcut expression language 来描述)来匹配连接点,给满足规则的连接点添加通知。
    
    /**
     * 使用Pointcut给这个方法定义切点,即UserService中全部方法均为切点。
     * 这里在这个log方法上面定义切点,然后就只需在下面的Before、After等等注解中填写这个切点方法"log()"即可设置好各个通知的切入位置。
     * 其中:
     *     execution:代表方法被执行时触发
     *     *:代表任意返回值的方法
     *     com.liang.service.impl.UserServiceImpl:这个类的全限定名
     *     (..):表示任意的参数
     */
    @Pointcut("execution(* com.liang.service.impl.UserServiceImpl.*(..))")
    public void log(){
    }
    
  • 通知(Advice)

    指拦截到连接点之后要执行的代码,包括"around"、“”、“”、“”、等不同类型的通知。
    
    /**
     * 异常通知 
     */
    @AfterThrowing("log()")
    public void doThrowing() {
        logger.error("方法抛出异常!");
    }
    
  • 织入(Weaving)

    就是把切面加入到核心业务逻辑的过程,织入可以在编译期织入,类装载期织入,动态代理织入。Spring采用的是动态代理织入,而AspectJ采用编译期织入和类装载期织入。
    

Spring Boot AOP开发

  • 添加依赖

    <!-- 在Spring Boot中,我们使用@AspectJ注解开发AOP,首先需要在pom.xml中引入依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  • 前期准备(业务逻辑代码编写)

    /**
     * User实体类
     */
    public class User {
    
        private Long id;
        private String username;
        private String nikeName;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getNikeName() {
            return nikeName;
        }
    
        public void setNikeName(String nikeName) {
            this.nikeName = nikeName;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", username='" + username + '\'' +
                    ", nikeName='" + nikeName + '\'' +
                    '}';
        }
    }
    
    public interface UserService {
    
        /**
         * 打印用户信息
         * @param user
         */
        public void printUser(User user);
    }
    
    @Service
    public class UserServiceImpl implements UserService {
    
        private Logger logger = LoggerFactory.getLogger(this.getClass());
        @Override
        public void printUser(User user) {
            logger.info("用户信息:" + user.toString());
        }
    }
    
    @RestController
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @PostMapping("/printUser")
        public String printUser(@RequestBody User user) {
            userService.printUser(user);
            return "用户信息打印完成!";
        }
        
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKpoIF0b-1683531407468)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20230508135918139.png)]

  • 定义切面

    /**
     * 定义切面
     */
    @Aspect
    @Component
    public class LogAspect {
    
        /**
         * 日志打印
         */
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        /**
         * 使用Pointcut定义切点
         * 其中:
         *     execution:代表方法被执行时触发
         *     *:代表任意返回值的方法
         *     com.liang.service.impl.UserServiceImpl:这个类的全限定名
         *     (..):表示任意的参数
         */
        @Pointcut("execution(* com.liang.service.impl.UserServiceImpl.*(..))")
        public void log(){
        }
    
        /**
         * 前置通知
         */
        @Before("log()")
        public void doBefore() {
            logger.warn("调用方法之前");
        }
    
        /**
         * 后置通知
         */
        @After("log()")
        public void doAfter() {
            logger.warn("调用方法之后");
        }
    
        /**
         * 返回通知
         */
        @AfterReturning("log()")
        public void doReturning() {
            logger.warn("方法正常返回之后");
        }
    
        /**
         * 异常通知
         */
        @AfterThrowing("log()")
        public void doThrowing() {
            logger.error("方法抛出异常!");
        }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6zIjcgq3-1683531407469)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20230508140102072.png)]

  • 环绕通知

    /**
     * 环绕通知是AOP中最强大的通知,可以同时实现前置和后置通知,
     * 不过它的可控性没那么强,如果不用大量改变业务逻辑,一般不需要用到它。
     * 通知方法中有一个ProceedingJoinPoint类型参数,
     * 通过其proceed方法来调用原方法。需要注意的是环绕通知是会覆盖原方法逻辑的,
     * 如果上面代码不执行joinPoint.proceed();这一句,就不会执行原被织入方法。
     * 因此环绕通知一定要调用参数的proceed方法,这是通过反射实现对被织入方法调用。
     * @param joinPoint
     */
    @Around("log()")
    public void around(ProceedingJoinPoint joinPoint){
        logger.warn("执行环绕通知之前:");
        try {
            joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        logger.warn("执行环绕通知之后");
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pmePoVht-1683531407469)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20230508140349283.png)]

  • 通知方法传参

    /**
     *  在注解后面加一个args选项,里面写参数名即可。
     *  需要注意的是,通知方法的参数必须和被织入方法参数一一对应
     */
    @Before("log() && args(user)")
    public void doBefore(User user) {
        logger.warn("调用方法之前 " + user);
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vP75Yc32-1683531407469)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20230508140745572.png)]

  • 连接点作为参数传入

    /**
     * 连接点作为参数传入
     * 获得类名,方法名,参数等信息
     */
    @Before("log()")
    public void doBefore(JoinPoint joinPoint) {
        System.out.println(joinPoint.getClass().getName());
        System.out.println(joinPoint.getSignature());
        System.out.println(joinPoint.getSignature().getName());
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            System.out.println(arg);
        }
        logger.warn("调用方法之前 "  );
    }
    

    SpringBoot实现对自定义注解的切面

    1. 自定义一个注解作为切点

      /**
       * 自定义注解 检查权限
       */
      @Target(ElementType.METHOD) //该注解作用在方法上
      @Retention(RetentionPolicy.RUNTIME) //该注解的作用范围,运行时能够识别该注解
      public @interface CheckOperateAuth {
          
          String value();
      }
      
    2. 在切面类中去编写鉴权的逻辑

      /**
       * 切面
       */
      @Aspect
      @Component
      public class AuthAspect {
      
          /**
           * @Before注解用于标注一个方法,在被这个注解的方法执行之前会被执行,它可以用来初始化所需要的资源,实例化对象或者准备测试数据。
           * 这里的value属性表示注解所需要过滤的目标注解。在这个例子中,这个方法将会在任何一个被@checkOperateAuth注解的方法执行之前执行。
           * @param joinPoint
           * @param checkOperateAuth
           */
          @Before(value = "@annotation(checkOperateAuth)" )
          public void before(JoinPoint joinPoint, CheckOperateAuth checkOperateAuth){
              if (!hasAnnotation(joinPoint)){
                  throw new RuntimeException("您没有该权限");
              }
          }
          /**
           * 先判断该方法或者类上面是否具有checkOperateAuth注解
           * 如果有则判断是否有权限
           * @param joinPoint
           * @return
           */
          public boolean hasAnnotation(JoinPoint joinPoint){
              MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
              //获取切点上的方法
              Method method = methodSignature.getMethod();
              // 获取方法上的该注解
              CheckOperateAuth annotation = method.getAnnotation(CheckOperateAuth.class);
              //方法上没有获取类上的
              if (annotation == null){
                  annotation = method.getDeclaringClass().getAnnotation(CheckOperateAuth.class);
              }
              //如果都没有就不进行鉴权
              if (annotation == null){
                  return true;
              }
              //有该注解并且有值,就进行鉴权操作
              if (StringUtils.hasLength(annotation.value())){
                  //模拟获取当前登录用户所拥有的权限
                  Set<String> permissionSet = new HashSet<>();
                  permissionSet.add("sys.user.list");
                  permissionSet.add("sys.user.add");
                  if (CollectionUtils.isEmpty(permissionSet)){
                      return false;
                  }
                  return permissionSet.contains(annotation.value());
              }
              return true;
          }
          
      }
      
    3. 在需要鉴权的地方加上自定义注解

      @GetMapping("list")
      @CheckOperateAuth("sys.user.list")
      public String list(){
          return "访问成功";
      }
      
      @PostMapping("delete")
      @CheckOperateAuth("sys.user.delete")
      public String delete(){
          return "访问成功";
      }
      
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值