【java】Spring Boot -- Spring AOP原理及简单实现

news2024/11/26 3:11:29

文章目录

  • 一、AOP基本概念
    • 1.1、Filter、Interceptor、AOP
    • 1.2、AOP中的一些概念
      • 1).通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理;通知类型,主要有以下几种:
      • 2).连接点(Join Point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用,可以说目标对象中的方法就是一个连接点;
      • 3).切点(Pointcut): 就是连接点的集合;对应Spring中的@Pointcut注解;
      • 4).切面(Aspect): 切面是通知和切点的结合;对应Spring中的注解@Aspect修饰的一个类;
      • 5).目标对象(Target object):即被代理的对象;
      • 6).代理对象(AOP proxy):包含了目标对象的代码和增强后的代码的那个对象;
    • 1.3、切点匹配表达式
  • 二、AOP简单实现
    • 2.1、后置处理器
    • 2.2、AOP实现思路
    • 2.3、AOP实现代码
      • 1、首先创建AOP中使用到的注解,这些注解类名称和Spring自带的一致:
      • 2、问题2
      • 3、问题3
    • 2.4、测试

一、AOP基本概念

什么是AOP,AOP英语全名就是Aspect oriented programming,字面意思就是面向切面编程。面向切面的编程是对面向对象编程的补充,面向对象的编程核心模块是类,然而在AOP中核心模块是切面。切面实现了多种类型和对象的模块化管理,比如事物的管理。

上面的解释可以你还是看不懂,那么我们举个例子来说明AOP是来解决什么样的问题。我们都知道传统的OOP是自上而下的逻辑开发:

在这里插入图片描述
上面这张图形象生动了描述了我们通过浏览器访问一个接口的函数调用过程,我们发送的http请求首先会根据url匹配到对应的controller,然后controller会去调用对应的的service,service再去调用dao、然后将处理的结果返回给浏览器。

1.1、Filter、Interceptor、AOP

那么现在我们有一个需求,想记录发送http请求的客户端IP以及请求接口信息,最简单的方法就是我们在每一个controller方法中调用一个打印相关信息的函数,这样会存在一个问题,有多少接口,我们就会调用多少次日志打印函数。那么有没有一种简单的方法可以实现日志的记录呢,有当然有,我们可以通过Filter或者Interceptor实现请求拦截功能,记录日志信息。那么该有人问什么是Filter、Interceptor,那它们和我们将要说的AOP有什么区别?

AOP使用的主要是动态代理 , 过滤器使用的主要是函数回调;拦截器使用是反射机制 。一个请求过来,先进行过滤器处理,看程序是否受理该请求 。 过滤器放过后 , 程序中的拦截器进行处理 ,处理完后进入 被 AOP动态代理重新编译过的主要业务类进行处理 。

  • Filter:和框架无关,过滤器拦截的是URL,可以控制最初的http请求,但是更细一点的类和方法控制不了。
  • Interceptor:拦截器拦截的也是URL,拦截器有三个方法,相对于过滤器更加细致,有被拦截逻辑执行前、后等。
  • AOP: 面向切面拦截的是类的元数据(包、类、方法名、参数等) 相对于拦截器更加细致,而且非常灵活,拦截器只能针对URL做拦截,而AOP针对具体的代码,能够实现更加复杂的业务逻辑。

三者功能类似,但各有优势,从过滤器 >拦截器 >切面,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是AOP。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;针对日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。

在这里插入图片描述

1.2、AOP中的一些概念

上面说了那么多题外话,你应该对AOP有了一个初步的理解,下面我们将更深入的介绍AOP,AOP是一种面向切面的编程思想。这些横切性问题,把它们抽象为一个切面,关注点在切面的编程。

AOP主要应用在日志记录权限验证事务管理中。我们首先来看一下Spring官方提供的一些有关AOP的基本概念:

1).通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理;通知类型,主要有以下几种:

  • Before :前置通知,在连接点方法前调用;对应Spring中@Before注解;
  • After :后置通知,在连接点方法后调用;对应Spring中的@After注解;
  • AfterReturning:返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常;对应- Spring中的@AfterReturning注解;
  • AfterThrowing:异常通知,当连接点方法异常时调用;对应Spring中的@AfterThrowing注解;
  • Around:环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法;对应Spring中的@Around注解;

2).连接点(Join Point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用,可以说目标对象中的方法就是一个连接点;

3).切点(Pointcut): 就是连接点的集合;对应Spring中的@Pointcut注解;

4).切面(Aspect): 切面是通知和切点的结合;对应Spring中的注解@Aspect修饰的一个类;

5).目标对象(Target object):即被代理的对象;

6).代理对象(AOP proxy):包含了目标对象的代码和增强后的代码的那个对象;

在这里插入图片描述

我们利用上面这一张图来说一下目标对象和代理对象的关系,代理对象可以看作是目标对象的加强版,它是对目标对象中方法功能的一个扩充。代理对象的实现主要有两种,一种是基于jdk动态代理的,这要求目标对象必须是接口的实现;而另一种实现是基于cglib,即代理对象是继承自目标对象。

1.3、切点匹配表达式

切面是如何拦截到指定的类的元数据(包、类、方法名、参数等) 的呢,这是通过切点匹配表达式实现的。目前Spring支持的切点匹配表达式主要有以下几种:

  • execution:可以定义到的最小粒度是方法,修饰符,包名,类名,方法名,Spring AOP主要也是使用这个匹配表达式;
  • within:只能定义到类;例如@Pointcut(within(com.jnu.example.*))
  • this:当前生成的代理对象的类型匹配;
  • target:目标对象类型匹配;
  • args:只针对参数;
  • annotation:针对注解;

例如: execution (* com.sample.service…*. *(…))

整个表达式可以分为五个部分:

  • 1、execution()::表达式主体;
  • 2、第一个*号:表示返回类型, *号表示所有的类型;
  • 3、包名:表示需要拦截的包名,包名后面的…,表明com.sample.service包、及其子包;
  • 4、第二个*号:表示类名,*号表示所有的类;
  • 5、*(…):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个点表示任何参数;

关于Spring AOP注解的使用,我这里就不介绍了,网上有大量的博客介绍如何使用。我们接下来就带大家来实现一个类似within切点匹配表达式的效果。

二、AOP简单实现

在开始之前,我们先引入一个概念,Spring扩展点和后置处理器,我们知道Spring IOC可以对应用程序中的java bean做一个集中化的管理,从而使我们从繁琐的new Object()中解锁出来。

其核心就是先创建一个bean工厂,也就是我们常说的beanFactory,通过beanFactory来生产出我们应用程序中所需要的java ben,这些java bean大多都是被Spring代理之后的对象。

2.1、后置处理器

今天呢,我跟大家介绍的后置处理器呢,有三个,它们在Spring启动过程中均会被执行,具体详情可以阅读Spring启动过程源码分析:

BeanFactoryPostProcessor:可以插手beanFactory的生命周期,是针对整个工厂生产出来的BeanDefinition作出修改或者注册,作用于BeanDefinition时期;

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;

@FunctionalInterface
public interface BeanFactoryPostProcessor {
    void postProcessBeanFactory(ConfigurableListableBeanFactory var1) throws BeansException;
}

BeanPostProcessor:可以插手bean的生命周期,该接口定义了两个方法,分别在bean初始化前后执行(注意这里的初始化是指对象创建之后,属性赋值之前),方法的返回值为一个object,这个object呢就是我们存在于容器的对象了(所以这个位置我们是不是可以对我们的bean做一个动态的修改,替换等等操作,所以这也是我们Spring的扩展点之一,后面结合我么自己手写AOP来详细讲解这个扩展点的应用);

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;
import org.springframework.lang.Nullable;

public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

ImportSelector:借助@Import注解,可以动态实现将一个类是否交由Spring管理,常用作开关操作;
在Spring处理我们的java类的时候,会分成四种情况去处理 :

  • 普通类:就是@Component,@Service,@Repository等注解的类
  • Import进来的类:这里呢,又分为三种情况:
    a)Import一个普通类:@Import(A.class)
    b)Import一个Registrar:比如我们的aop @Import(AspectJAutoProxyRegistrar.class)
    c)Import一个ImportSelector:比如:@import(ImportSelector.class) ImportSelector 接口有一个实现方法,返回一个字符串类型的数组,里面可以放类名,在@import(ImportSelector.class)的时候,Spring会把我们返回方法里面的类全部注册到BeanDefinitionMap中,继而将对象注册到Spring容器中;

至于Spring在什么时候处理的呢,我大致叙述一下,有兴趣的可以自己去研究下spring源码: 对于普通类,Spring在扫描的时候,就将扫描出来的java类转换成我们的BeanDefinition,然后放入一个BeanDefinitionMap中去;

对于@import的三种情况,处理就在ConfigurationClassPostProcessor(该类是BeanDefinitionRegistryPostProcessor后置处理器的一个实现,同时这也是我们spring内部自己维护的唯一实现类)类中,具体处理Import的核心代码如下,if-else 很容易可以看出spring对于我们Import三种类型的处理:

/**
     * 处理我们的@Import注解,注意我们的@Import注解传入的参数,可能有三种类型
     * 1,传入一个class类,直接解析
     * 2,传入一个registrar,需要解析这个registrar
     * 3,传入一个ImporterSelector,这时候会去解析ImporterSelector的实现方法中返回的数组的class
     *         
     */
    private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
            Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

        if (importCandidates.isEmpty()) {
            return;
        }

        if (checkForCircularImports && isChainedImportOnStack(configClass)) {
            this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
        }
        else {
            this.importStack.push(configClass);
            try {
                for (SourceClass candidate : importCandidates) {
                    //处理我们的ImportSelector
                    if (candidate.isAssignable(ImportSelector.class)) {
                        // Candidate class is an ImportSelector -> delegate to it to determine imports
                        Class<?> candidateClass = candidate.loadClass();
                        ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                selector, this.environment, this.resourceLoader, this.registry);
                        if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
                            this.deferredImportSelectors.add(
                                    new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
                        }
                        else {
                            String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                            Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
                            //注意可能我们ImportSelector传入的类上还有可能会Import,所以这里,spring采用了
                            //一个递归调用,解析所有的import
                            processImports(configClass, currentSourceClass, importSourceClasses, false);
                        }
                    }
                    //处理我们的Registrar
                    else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                        // Candidate class is an ImportBeanDefinitionRegistrar ->
                        // delegate to it to register additional bean definitions
                        Class<?> candidateClass = candidate.loadClass();
                        ImportBeanDefinitionRegistrar registrar =
                                BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                registrar, this.environment, this.resourceLoader, this.registry);
                        //添加的一个和Importselector方式不同的map中,sprig对两种方式传入的类注册方式不同
                        configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
                    }
                    else {
                        // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
                        // process it as an @Configuration class
                        
                        //最后如果是普通类,传入importStack后交由processConfigurationClass进行注册处理
                        this.importStack.registerImport(
                                currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                        processConfigurationClass(candidate.asConfigClass(configClass));
                    }
                }
            }
            catch (BeanDefinitionStoreException ex) {
                throw ex;
            }
            catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to process import candidates for configuration class [" +
                        configClass.getMetadata().getClassName() + "]", ex);
            }
            finally {
                this.importStack.pop();
            }
        }
    }

2.2、AOP实现思路

同样根据如上原理,下面我们便可以来模拟我们的AOP,如果有点基础的可能应该会知道,Spring是基于我们的动态代理实现的(先不考虑是cglib还是jdk动态代理),结合我们AOP使用,那么我们就需要解决如下几个问题:

  • 我们知道开启和关闭aop需要注解@EnableAspectJAutoProxy,如何实现,结合上文,我们可以使用@import(ImportSelector.class)来实现该功能;
  • 如何确定代理关系,即哪些是我们需要代理的目标对象和其中的目标方法,以及哪些方法是要增强到目标对象的目标方法上去的?
  • 如何实现目标对象的替换,就是我们在getBean的时候,如何根据目标对象来获取到我们增强后的代理对象?

2.3、AOP实现代码

我们首先需要构建一个项目,创建如下包:

annotation:存放我们所有自定义的注解;

holder:存放代理类信息;

processor:存放后置处理器的实现类;

selector:存放ImportSelector的实现类;

1、首先创建AOP中使用到的注解,这些注解类名称和Spring自带的一致:

After.java:

package com.zy.blog.common.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;


/**
 *  后置通知注解类
 *
 *  @author  zy
 *  @since 2020/6/20 17:32
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface After {
    String value() default "";
}

Around.java:

package com.zy.blog.common.annotation;


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 *  环绕通知注解类
 *  
 *  @author zy
 *  @since 2020/6/20 17:33
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface Around {
    String value() default "";
}

Before.java:

package com.zy.blog.common.annotation;


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 *  前置通知注解类
 *  
 *  @author zy
 *  @since 2020/6/20 17:31
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface Before {
    String value() default "";
}

Aspect.java:

package com.zy.blog.common.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Aspect {
}

EnableAspectAutoProxy.java:

package com.zy.blog.common.annotation;


import com.zy.blog.common.selector.CustomizedImportSelector;
import org.springframework.context.annotation.Import;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 *  aop开关注解
 *  如果在App启动类上加入该注解,将会将我们的后置处理器的实现交给spring管理,spring才能去扫描得到这个类,才能去执行我们的自定义的后置处理器里面的方法,才能实现我们的aop的代理
 *
 *  @author zy
 *  @since 2020/6/20 17:22
 */
@Retention(RetentionPolicy.RUNTIME)
@Import(CustomizedImportSelector.class)
public @interface EnableAspectAutoProxy {
}

上面我们已经说过,在@import(ImportSelector.class)的时候,Spring会把我们返回方法里面的类全部注册到BeanDefinitionMap中,继而将对象注册到Spring容器中,因此我们定义ImportSelector的实现类CustomizedImportSelector:

package com.zy.blog.common.selector;

import com.zy.blog.common.processor.CustomizedBeanFactoryPostProcessor;
import com.zy.blog.common.processor.CustomizedBeanPostProcessor;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;


/**
 *  自定义aop实现,提交给spring容器
 *  ImportSelector 接口有一个实现方法,返回一个字符串类型的数组,里面可以放类名,在@import(ImportSelector.class)的时候,spring会把我们返回方法里面的类全部注册到
 *  BeanDefinitionMap中,继而将对象注册到Spring容器中\
 *
 *  @author zy
 *  @since 2020/6/20 17:16
 */
public class CustomizedImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{CustomizedBeanFactoryPostProcessor.class.getName(), CustomizedBeanPostProcessor.class.getName()};
    }
}

这样就可以将CustomizedBeanFactoryPostProcessor和CustomizedBeanPostProcessor类注册到BeanDefinitionMap中,继而将对象注册到Spring容器中。

Pointcut.java:

package com.zy.blog.common.annotation;


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 *  切点
 *  
 *  @author zy
 *  @since 2020/6/21 17:54
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface Pointcut {
    String value() default "";
}

同时定义一个工具类AspectUtil:

package com.zy.blog.common.util;

import com.zy.blog.common.annotation.*;
import com.zy.blog.common.holder.ProxyBeanHolder;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 *  aop切面注解工具类
 *  AOP 领域中的特性术语:
 *  通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理;对应注解@After、@Before、@Around ...
 *  连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用,可以说目标对应中的方法就是一个连接点
 *  切点(PointCut): 就是连接点的集合;对应注解@PonitCut
 *  切面(Aspect): 切面是通知和切点的结合;对应注解@Aspect修饰的一个类
 *
 *  @author zy
 *  @since 2020/6/20 15:27
 */
public class AspectUtil {
    /*
     * 指定切面注解类
     */
    public static final String ASPECT = Aspect.class.getName();

    /*
     * 指定切点注解类
     */
    public static final String POINTCUT = Pointcut.class.getName();;

    /*
     * 指定前置通知注解类
     */
    public static final String BEFORE = Before.class.getName();;

    /*
     * 指定后置通知注解类
     */
    public static final String AFTER = After.class.getName();;

    /*
     * 指定环绕通知注解类
     */
    public static final String AROUND = Around.class.getName();;

    /*
     * 存放AOP代理的全部目标类  目标类 ->(切面类,代理方法,通知注解)  如:com.jnu.example.blog.service.IArticleService -> [(com.jnu.example.blog.AspectTest, testBefore, com.zy.blog.common.annotation.Before)]
     */
    public static volatile Map<String, List<ProxyBeanHolder>> classzzProxyBeanHolder = new ConcurrentHashMap<>();
}

2、问题2

针对问题2,我们可以定义一个BeanFactoryPostProcessor的实现类,完成对所有BeanDefinition的扫描,找出我们定义的所有的切面类,然后循环里面的方法,找到切点、以及所有的通知方法,然后根据注解判断通知类型(也就是前置,后置还是环绕),最后解析切点的内容,扫描出所有的目标类,放入我们定义好的容器中。

创建ProxyBeanHolder类,用于存放代理信息:

package com.zy.blog.common.holder;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 *  自定义数据结构 用于存放代理信息
 *
 *  @author zy
 *  @since 2020/6/20 15:03
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProxyBeanHolder {
    /**
     * 切面类名称
     */
    private String className;

    /**
     * 代理方法 如:testBefore
     */
    private String methodName;

    /**
     * 通知注解类名称 如:{@link com.zy.blog.common.annotation.Before}
     */
    private String annotationName;
}

定义我们的注册类CustomizedBeanFactoryPostProcessor,用于注册我们的目标对象和切面对象之间的关系:

package com.zy.blog.common.processor;


import cn.hutool.core.util.StrUtil;
import com.zy.blog.common.holder.ProxyBeanHolder;
import com.zy.blog.common.util.AspectUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.core.type.AnnotationMetadata;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.*;

/**
 *  BeanFactory初始化后调用
 *  定义注册类  用于注册目标对象和切面对象之间的关系
 *  1. 切面对象指的就是被@Aspect注解修饰的配置类对象
 *  2. 目标对象就是被@Pointcut("execution(public * com.zy.example.bigdataplat.admin.controller.*.*(..))")拦截的bean
 *  完成所有BeanDefinition的扫描,找出我们所有的切面类,然后循环里面的方法,找到切点、以及所有的通知方法,然后根据注解判断通知类型(也就是前置,后置还是环绕),
 *  最后解析切点的内容,扫描出所有的目标类,放入我们定义好的容器中。
 *
 *  @author zy
 *  @since 2020/6/20 15:34
 */
@Slf4j
public class CustomizedBeanFactoryPostProcessor implements BeanFactoryPostProcessor{
    /*
     * 保存所有的切点修饰的方法名、以及切点的value()函数值
     */
    private Map<String,String> pointCutMap = new HashMap<>();

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory)  {
        //获取所有的bean name
        String[] beanDefinitionNames = configurableListableBeanFactory.getBeanDefinitionNames();
        for(String beanDefinitionName:beanDefinitionNames){
            BeanDefinition beanDefinition = configurableListableBeanFactory.getBeanDefinition(beanDefinitionName);
            //判断bean是否被注解修饰
            if(beanDefinition instanceof AnnotatedBeanDefinition){
                //获取bean上的注解元数据
                AnnotationMetadata metadata = ((AnnotatedBeanDefinition)beanDefinition).getMetadata();
                //获取注解类型  如:{org.springframework.boot.autoconfigure.SpringBootApplication,com.jnu.example.db.annotation.CoreMapperScan,com.zy.blog.common.annotation.EnableAspectAutoProxy}
                Set<String> annotations = metadata.getAnnotationTypes();
                //遍历所有注解、找到aop切面注解类
                for(String annotation:annotations){
                    //如果被@Aspect注解修饰 表明这是一个切面类 查找切面类中指定的切点(@Pointcut)
                    if(annotation.equals(AspectUtil.ASPECT)){
                        doScan((GenericBeanDefinition)beanDefinition);
                    }
                }
            }
        }
    }


    /**
     *  扫描切面类所有方法上的注解
     * 1、如果有@Pointcut注解:保存对应的切点
     * 2、找到所有的通知注解@After、@Before、@Around,获取对应的切点
     * 3、保存
     * @param beanDefinition
     */
    private void doScan(GenericBeanDefinition beanDefinition){
        try{
            //获取切面类名 如:com.jnu.example.blog.TestAop
            String className = beanDefinition.getBeanClassName();
            //加载类
            Class<?> beanDefinitionClazz = Class.forName(className);
            //获取所有方法
            Method[] methods = beanDefinitionClazz.getMethods();

            //遍历切面类所有方法 第一遍历找到所有切点
            for(Method method:methods){
                //获取注解类名  如:@com.zy.blog.common.annotation.Pointcut
                Annotation[] annotations = method.getAnnotations();
                for(Annotation annotation:annotations){
                    //获取注解类名  如:@com.zy.blog.common.annotation.Pointcut
                    String annotationName = annotation.annotationType().getName();
                    if(AspectUtil.POINTCUT.equals(annotationName)){
                        String value = getAnnotationValue(annotation);
                        if(StrUtil.isNotBlank(value)) {
                            //获取切点指定包
                            pointCutMap.put(method.getName()+"()", value);
                        }
                    }
                }
            }
            //遍历切面类所有方法 第二次遍历找到所有通知
            for(Method method:methods) {
                //获取注解类名  如:@com.zy.blog.common.annotation.After
                Annotation[] annotations = method.getAnnotations();
                for (Annotation annotation : annotations) {
                    //获取注解类名  如:@com.zy.blog.common.annotation.After
                    String annotationName = annotation.annotationType().getName();
                    //如果是Before、或者After、Around
                    if (AspectUtil.BEFORE.equals(annotationName) || AspectUtil.AFTER.equals(annotationName)
                            || AspectUtil.AROUND.equals(annotationName)) {
                        try {
                            doScan(className, method, annotation);
                        } catch (Exception e) {
                            log.error(e.getMessage(), e);
                        }
                    }
                }
            }
        }catch(ClassNotFoundException e){
            log.error(e.getMessage(),e);
        }
    }


    /**
     * 扫描切点指定的所有目标类  保存到map中:目标类 ->(切面类,代理方法,通知注解)
     * @param className:切面类
     * @param method: 通知方法
     * @param annotation:通知注解 @after或者@before、@Around
     */
    private void doScan(String className,Method method,Annotation annotation) throws URISyntaxException {
        //保存代理信息
        ProxyBeanHolder proxyBeanHolder = new ProxyBeanHolder(className,method.getName(),annotation.annotationType().getName());

        //获取通知value,即切点方法
        String pointCutMethod = getAnnotationValue(annotation);

        //获取包路径
        String packagePath = pointCutMap.get(pointCutMethod);

        //遍历包、找到包下的所有目标类
        if(StrUtil.isNotBlank(packagePath)){
            traverseDir(packagePath,proxyBeanHolder);
        }
    }

    /*
     * 遍历file对象
     */
    private void  traverseDir(String packagePath,ProxyBeanHolder proxyBeanHolder) throws URISyntaxException {
        //获取目标包路径:classpath + 包名
        String targetPackagePath = this.getClass().getResource("/").toURI().getPath() + packagePath.replace(".","/");
        File file = new File(targetPackagePath);
        File[] fileList = file.listFiles();
        if(fileList == null){
            return;
        }
        List<ProxyBeanHolder> proxyBeanHolderList = null;
        //遍历包路径
        for(File fp:fileList){
            //判断是不是文件
            if(fp.isFile()){
                String targetClass = packagePath + "." + fp.getName().replace(".class","");
                try{
                    proxyBeanHolderList = AspectUtil.classzzProxyBeanHolder.get(targetClass);
                }catch (Exception e){
                    log.error(e.getMessage(),e);
                }
                if(proxyBeanHolderList == null){
                    proxyBeanHolderList = new Vector<>();
                }
                proxyBeanHolderList.add(proxyBeanHolder);
                AspectUtil.classzzProxyBeanHolder.put(targetClass,proxyBeanHolderList);
            }else{
                traverseDir(packagePath + "." + fp.getName(),proxyBeanHolder);
            }
        }
    }

    /**
     * 获取注解value()方法的值
     * @param annotation:注解
     */
    private String getAnnotationValue(Annotation annotation){
        //获取注解上的所有方法  不包括继承的方法
        Method[] annotationMethods = annotation.annotationType().getDeclaredMethods();

        //遍历注解每一个方法
        for(Method annotationMethod:annotationMethods){
            //如果方法名称是value
            if(annotationMethod.getName().equals("value")){
                try{
                    //执行value()方法 第一个参数代表调用的对象,第二个参数传递的调用方法的参数
                    return (String)annotationMethod.invoke(annotation,null);
                }catch(IllegalAccessException | InvocationTargetException e){
                    log.error(e.getMessage(),e);
                }
            }
        }
        return "";
    }

}

3、问题3

针对问题3,我们可以定义一个BeanPostProcessor的实现类CustomizedBeanPostProcessor,在bean实例化之后,在放入容器之前,进行一个条件过滤,如果当前对象是我们的目标对象(即在我们定义好的Map中),则对对象进行代理,将目标对象替换成代理对象返回即可。

注:Spring实现AOP采用cglib和jdk动态代理两种方式,@EnableAspectJAutoProxy(proxyTargetClass=true)可以加开关控制,如果不加,目标对象如果有实现接口,则使用jdk动态代理,如果没有就采用cglib(因为我们知道cglib是基于继承的))

我们这里实现,都简单粗暴一点,统一采用cglib代理,这样就可以完成对任意对象的代理了。

package com.zy.blog.common.processor;

import com.zy.blog.common.util.AspectUtil;
import lombok.SneakyThrows;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cglib.proxy.Enhancer;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;


/**
 *  bean实例化之后调用
 *  我们可以利用BeanPostProcessor,在bean实例化之后,在放入容器之前,进行一个条件过滤,如果当前对象是我们的目标对象(即在我们定义好的Map中),
 *  则对对象进行代理,将目标对象替换成代理对象返回即可
 *
 *  @author zy
 *  @since 2020/6/20 16:52
 */
public class CustomizedBeanPostProcessor implements BeanPostProcessor {

    /**
     * bean实例化之后放到Spring IOC容器之前
     */
    @SneakyThrows
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)  {
        //获取bean class
        Class clazz = bean.getClass();
        //获取bean类名
        String className = clazz.getName();
        //如果这个bean是已经被代理后的  获取被代理前的类名
        className = className.substring(0,className.indexOf("$$") > 0 ? className.indexOf("$$"):className.length());
        Object object = bean;
        //对目标对象进行代理  采用cglib代理 继承方式
            if( AspectUtil.classzzProxyBeanHolder.containsKey(className)){
            // 创建加强器,用来创建动态代理类
            Enhancer enhancer = new Enhancer();
            // 为加强器指定要代理的业务类(即:为下面生成的代理类指定父类)
            enhancer.setSuperclass(clazz);
            // 设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦
            enhancer.setCallback(new CustomizedProxyInterceptor(AspectUtil.classzzProxyBeanHolder.get(className)));
            // 创建动态代理类对象并返回
            object = enhancer.create();

            //获取Class对象所表示的类或接口的所有(包含private修饰的)字段,不包括继承的字段
            Field[] fields = clazz.getDeclaredFields();
            //遍历字段
            for(Field field:fields){
                //排除静态方法
                if (Modifier.isFinal(field.getModifiers())){
                    continue;
                }
                //设置私有字段可以访问
                field.setAccessible(true);
                //实现相同字段赋值,解决代理对象中的自动注入bean为空的问题
                field.set(object,field.get(bean));
            }
        }
        return object;
    }


    /**
     * bean实例化之后放到Spring IOC容器之后执行
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean;
    }
}

2.4、测试

我们定义一个AspectTest切面类,在调用com.jnu.example.service包类中的方法前会执行我们的前置通知:

package com.zy.blog.common;

import com.zy.blog.common.annotation.Aspect;
import com.zy.blog.common.annotation.Before;
import com.zy.blog.common.annotation.Pointcut;
import org.springframework.stereotype.Component;


/**
 *  这是一个切面类 不可以使用@Configuration注解(会被动态代理)
 *
 *  @author zy
 *  @since 2020/6/21 10:12
 */
@Component
@Aspect
public class AspectTest {

    /**
     *  定义一个切点 目前只支持指定包路径
     */
    @Pointcut("com.zy.blog.server")
    public void servicePointCut(){

    }

    /**
     *  前置通知
     */
    @Before("servicePointCut()")
    public void testBefore(){
        System.out.println("before -----------------------,测试成功");
    }
}

最后就是开启我们自定义的AOP功能,在我们的BlogApplication类上加入@EnableAspectAutoProxy,就会将我们定义的后置处理器的实现类交给Spring IOC容器,Spring才能去扫描得到这些类,才能去执行我们自定义的后置处理器里面的方法。

package com.zy.blog.api;

import com.zy.blog.common.annotation.EnableAspectAutoProxy;
import com.zy.blog.server.annotation.CoreMapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 *  Spring Boot程序入口
 *
 *  @author zy
 *  @since 2020/4/14 22:40
 */
@SpringBootApplication(scanBasePackages ={"com.zy.blog"})
@CoreMapperScan
@EnableAspectAutoProxy
public class BlogApplication {
    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
    }
}

参考文章:

[1]浅析Spring AOP——基本概念

[2]Spring Aop底层原理详解(利用spring后置处理器实现AOP)

[3]Spring中最!最!最!重要的后置处理器!没有之一!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/345941.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Acwing---1246. 等差数列

等差数列1.题目2.基本思想3.代码实现1.题目 数学老师给小明出了一道等差数列求和的题目。 但是粗心的小明忘记了一部分的数列&#xff0c;只记得其中 N个整数。 现在给出这 N个整数&#xff0c;小明想知道包含这 N 个整数的最短的等差数列有几项&#xff1f; 输入格式 输入…

ChatGPT 接入微信,最强聊天机器人来了

最近的 ChatGPT 又再次火热起来了&#xff0c;各种周边工具也是层出不穷&#xff0c;今天我们就一起来做一个基于 ChatGPT 的微信聊天机器人&#xff0c;来感受 AI 世界的快乐吧~ 我们先来看几个我比较关心的问题吧 Python 语言算法 什么是 ChatGPT 人工智能会统治世界吗&a…

【Kubernetes】【二】环境搭建 环境初始化

本章节主要介绍如何搭建kubernetes的集群环境 环境规划 集群类型 kubernetes集群大体上分为两类&#xff1a;一主多从和多主多从。 一主多从&#xff1a;一台Master节点和多台Node节点&#xff0c;搭建简单&#xff0c;但是有单机故障风险&#xff0c;适合用于测试环境多主…

带你玩转Jetson之Deepstream简明教程(四)DeepstreamApp如何使用以及用于工程验证。

1.DeepstreamApp是什么&#xff1f; 如果你安装完毕deepstream整体框架&#xff0c;会在你的系统执行目录内有可执行文件&#xff0c;文件名字是deepstream-app。这是一个可执行脚本文件&#xff0c;通过deepstream框架中的代码在安装的时候编译后install到系统根目录内。 此脚…

家政服务小程序实战教程11-首页跳转到分类页面

小程序的分类页面&#xff0c;通常会以侧边栏导航显示内容&#xff0c;我们使用了侧边选项卡和数据列表组件实现了分类展示的功能 还有个需求是从首页点击某个分类的时候&#xff0c;跳转到分类页时候需要选中对应的类别 01 增加页面参数 页面如果跳转时候需要这种接收参数并实…

JAVA和Spring的SPI机制讲解

文章目录1 SPI机制讲解1.1 引言1.2 Java SPI实现1.2.1 示例说明1.2.2 相关测试1.2.3 源码分析1.3 Spring SPI1.3.1 Spring 示例1.3.2 相关测试类1.3.3 源码分析1 SPI机制讲解 1.1 引言 SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制&#xff0c;可以用来启…

力扣337题 打家劫舍Ⅲ Java语言版

/**每个节点都用一个长度为2的数组来表示其状态,其中dp[0]表示偷该节点所得到的最多钱币,dp[1]表示不偷该节点所得到的最多钱币*/ class Solution {public int rob(TreeNode root) {int[] robRoot robTree(root);return Math.max(robRoot[0],robRoot[1]);}public int[] robTre…

rtp协议

RTP协议 在实时音视频通话中&#xff0c;我们通常使用 UDP 作为传输层协议&#xff0c;使用 RTP 协议包荷载音视频数据&#xff0c;RTP&#xff08;Real-time Transport Protocol&#xff09;是一种在 Internet 上传输多媒体数据的应用层协议&#xff0c;它通常建立在 UDP 之上…

RabbitMQ学习(五):RabbitMQ持久化

一、持久化概念在上一章内容中我们已经看到了如何处理任务不丢失的情况&#xff0c;但是如何保障当 RabbitMQ 服务停掉后消 息生产者发送过来的消息不丢失呢&#xff1f;默认情况下 RabbitMQ 退出或由于某种原因崩溃时&#xff0c;它将忽视队列 和消息&#xff0c;除非告知它不…

Python 考试练习题4

1. 将元组 (1,2,3) 和集合 {4,5,6} 合并成一个列表。 tup(1,2,3) set{4,5,6} listlist(tup)list(set) print(list) 2. 在列表 [1,2,3,4,5,6] 首尾分别添加整型元素 7 和 0。 #方法1 list[1,2,3,4,5,6] list.insert(0,7) #list.append(0) 这两种也可以 #list.insert(len(list…

PowerJob的server启动都经历了哪些?代码不多也很简单,咱们来逐一理解。

这是一篇让你受益匪浅的文章&#xff0c;点个关注交流一下吧~ PowerJob如何使用&#xff0c;官方文档已经说的很详细了&#xff0c;即使没学过计算机的人&#xff0c;按照那上面的步骤来也是可以搭建出一个可以使用的例子来&#xff0c;所以今天就不在这里重复前人的工作&#…

DataWhale 大数据处理技术组队学习task1

DataWhale 大数据处理技术组队学习task1 一、大数据概述 1. 大数据时代&#xff08;详细内容参考参考文章&#xff09; 2. 大数据的概念&#xff08;又或者是特点&#xff09; 4V 数据量大&#xff08;Volume&#xff09; 数据来源&#xff1a;可以是计算机、手机&#xff…

Linux 常用命令——【 2.查看程序端口占用及网络连接 netstat 命令】

文章目录1. netstat 简介2.命令格式&#xff1a;3. 命令参数&#xff1a;4. 常用命令1. netstat 简介 netstat命令用于显示与IP、TCP、UDP和ICMP协议相关的统计数据&#xff0c;一般用于检验本机各端口的网络连接情况。netstat是在内核中访问网络及相关信息的程序&#xff0c;…

ACWING寒假每日一题python

ACWING寒假每日一题 一、孤独的照片 一个点一个点的来看&#xff0c;比如对于GHGHG中间的G&#xff0c;找到他的左边的G&#xff0c;以及右边的G的位置&#xff0c;l,r分别等于1&#xff0c;答案就要多加上11 但是如果对于 GHHGHHG 中间的G&#xff0c;我们可以看到l,r等于2&a…

【计算机网络期末复习】第六章 应用层

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4e3;专栏定位&#xff1a;为想复习学校计算机网络课程的同学提供重点大纲&#xff0c;帮助大家渡过期末考~ &#x1f4da;专栏地址&#xff1a; ❤️如果有收获的话&#xff0c;欢迎点…

Echarts 修改雷达图背景分割面,分割线颜色,设置数据线颜色

第016个点击查看专栏目录Echarts的雷达图背景可以是圆形的&#xff0c;也可以是多边形的&#xff0c;背景颜色&#xff0c;线条颜色都可以做个性化设置&#xff0c;这里是改变背景灰色&#xff0c;设置为浅红色。分割线也变成浅蓝色。同时数据线的颜色也变成了亮色。参考源代码…

【LeetCode】最大正方形 [M](动态规划)

221. 最大正方形 - 力扣&#xff08;LeetCode&#xff09; 一、题目 在一个由 0 和 1 组成的二维矩阵内&#xff0c;找到只包含 1 的最大正方形&#xff0c;并返回其面积。 示例 1&#xff1a; 输入&#xff1a;matrix [["1","0","1","0…

从零实现WebRTC(二):WebRTC的通信过程

文章目录一、WebRTC需要解决的问题二、ICE(Interactive Connectivity Establishment)三、ICE的详细步骤四、知识点四一、WebRTC需要解决的问题 WebRTC是由google提出的的一个用于端到端实现p2p音视频通信的框架。比起其他的hls, http-flv等直播方案&#xff0c;webrtc在公网的…

ChatGPT的来源-InstructGPT论文简要介绍

文章目录前言一、ChatGPT是什么&#xff1f;二、ChatGPT的前身InstructGPT论文解读论文下载地址&#xff1a;主要内容&#xff1a;模型训练数据类型结果效果示例总结前言 现在大火的ChatGPT功能十分强大&#xff0c;不仅可以回答用户问题&#xff0c;编写故事&#xff0c;甚至…

Spring Cloud Alibaba Sentinel 熔断降级与OpenFeign整合

熔断降级 概述 对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块&#xff0c;可能是另外的一个远程服务、数据库&#xff0c;或者第三方 API 等。例如&#xff0c;支付的时候&#xff0c;可能需要远程调用银联提供的 API&…