AOP

116 阅读6分钟

深入理解下AOP(个人理解)

一、概念:

aop就是利用反射和代理模式实现动态代理

二、需求:

利用aop结合redis实现页面访问量的统计

不多BB直接上代码!

三、代码实现

1.自定义一个注解

/**
 * 自定义注解:自定义一个注解作为切点
 * 用于统计接口访问量
 */
@Retention(RetentionPolicy.RUNTIME)//运行时有效
@Target({ElementType.METHOD,ElementType.TYPE})//可以用在方法和类上
@Documented
public @interface CustomTrafficStatistics {
}

2.定义切面和切点以及代码需要进行环绕的代码

/**
 * @Author xumin
 * @Date 2022/5/15 20:55
 *
 * @Aspect:标识切面
 * @Compoment:加入Spring-boot容器工工单
 *
 **/
@Aspect
@Component
public class CustomTrafficStatisticsAspect {
    //配置日志输出
    private final static Logger logger = LoggerFactory.getLogger(CustomTrafficStatisticsAspect.class);

    /**
     * 以自定义的@CustomTrafficStatistics注解为为切点
     * 目前的理解是,AOP约定@Pointcut只能定义在一个方法上,然后他们反射出被这个方法标记的对象(方法、类什么的),用target代理出来然后对这个代理对象做一个前后操作
     * 总之,感觉这个定义在方法上多此一举为啥不能直接定义在注解上呢。求大佬们解惑!
     */
    @Pointcut("@annotation(org.jeecg.common.annotation.CustomTrafficStatistics)")
    public void customTrafficStatistics(){};

    /**
     * 在切点之前做如下操作
     * @param joinPoint
     */
    @Before("customTrafficStatistics()")
    public void doBefore(JoinPoint joinPoint) throws IOException, ClassNotFoundException {
        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 打印请求相关参数
        logger.info("========================================== Start ==========================================");
        // 打印请求 url
        logger.info("URL            : {}", request.getRequestURL().toString());
        // 打印 Http method
        logger.info("HTTP Method    : {}", request.getMethod());
        // 打印调用 controller 的全路径以及执行方法
        logger.info("Class Method   : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        // 打印请求的 IP
        logger.info("IP             : {}", request.getRemoteAddr());
        // 打印请求入参
        logger.info("Request Args   : {}",new ObjectMapper().writeValueAsString(joinPoint.getArgs()));

//        动态代理应该就是动态反射
//        1.把所有被AOP标记的类的全路径获取到,(找到所有@Aspect切面,找到这个切面下面的所有切点)
//        2.根据全路径反射出来,但后通过反射出来的对象作为代理对象,拿到它的方法(AOP应该是约定@Pointcut只能定义在一个方法上,然后他们反射出被这个方法标记的对象(方法、类什么的))
//        3.在这个方法前后添加我们的操作
        //自制AOP可能实现的方式@Aspect、@Pointcut
        //spring工具类,可以获取指定路径下的全部类
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        //定义需要监听的包
        String basePackage = "org.jeecg.modules.demo.test.controller";
        String resourcePattern = "/*.class";
        //CLASSPATH_ALL_URL_PREFIX:类路径中所有匹配资源的伪URL前缀:" classpath*:" 检索给定名称的所有匹配资源
        //ClassUtils.convertClassNameToResourcePath:找到指定包名下的所有class,会将传入进去的路径的所有.替换为/
        //最终路径模样:classpath*:org/jeecg/modules/demo/test/controller/*.class
        String pattern = resourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(basePackage) + resourcePattern;
        //获取指定路径下的类资源
        Resource[] resources = resourcePatternResolver.getResources(pattern);
        //MetadataReader 的工厂类:元数据读取工厂类,用来读取元数据
        //构造将resourcePatternResolver转换一下赋值给属性metadataReaderCache
        CachingMetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
        for (Resource resource : resources) {
            //元数据信息读取器(Metadata:元数据)——可以读取类、注解等的元数据信息
            MetadataReader reader = readerFactory.getMetadataReader(resource);
            //读取器调用读取类的元数据的方法,读取到类的元数据
            ClassMetadata classMetadata = reader.getClassMetadata();
            //类元数据读取器拿到类名,里面包含很判断,可以判断类是否是接口、抽象类、注解等
            String className = classMetadata.getClassName();
            //反射获取类的代理对象
            Class<?> clazz = Class.forName(className);
            //判断该类是否包含CustomTrafficStatistics注解
            CustomTrafficStatistics annotation = clazz.getAnnotation(CustomTrafficStatistics.class);
            if (annotation!=null) {
                logger.info(clazz.getName()+":存在该注解");
            }
            //获取该类的下的所有方法,并判断是否被注解,如果被注解可以继续做逻辑操作
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                CustomTrafficStatistics methodAnnotation = method.getAnnotation(CustomTrafficStatistics.class);
                if (methodAnnotation!=null) {
                    logger.info(method.getName()+":存在该注解");
                }
            }
            //获取该类的下的所有属性,并判断是否被注解,如果被注解可以继续做逻辑操作
            Field[] fields = clazz.getFields();
            for (Field field : fields) {
                CustomTrafficStatistics fieldAnnotation = field.getAnnotation(CustomTrafficStatistics.class);
                if (fieldAnnotation!=null) {
                    logger.info(field.getName()+":存在该注解");
                }
            }
        }
    }
    /**
     * 在切点之后织入
     * @throws Throwable
     */
    @After("customTrafficStatistics()")
    public void doAfter() throws Throwable {
        // 结束后打个分隔线,方便查看
        logger.info("=========================================== End ===========================================");
    }

    /**
     * 环绕
     */
    @Around("customTrafficStatistics()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //开始时间
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 打印出参
        logger.info("Response Args  : {}", new ObjectMapper().writeValueAsString(result));
        // 执行耗时
        logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
        return result;
    }


}

basePackage(路径): image.png

3.接口实现

/**
 * @Author xumin
 * @Date 2022/5/15 21:29
 **/
@Slf4j
@Api("aop测试类")
@RestController
@RequestMapping("aop-test")
public class AopTestController {
    //遇到的小问题
    //这里必须用@Autowired注入而不是new,因为RedisUtil类上标记了@Component注解放到交给Spring管理
    //如想了解请看下图
    @Autowired
    private RedisUtil redisUtil;



    @ApiOperation("自定义注解AOP")
    @GetMapping("hello")
    @CustomTrafficStatistics
    public String hello(){


        return "hello"+redisUtil.incr("aop-test-hello",1);
    }
}

问题如下: image.png

四、@Pointcut切点注解详解

表达式标签(10种)

  • execution:用于匹配方法执行的连接点
  • within:用于匹配指定类型内的方法执行
  • this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也* 类型匹配
  • target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
  • args:用于匹配当前执行的方法传入的参数为指定类型的执行方法
  • @within:用于匹配所以持有指定注解类型内的方法
  • @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
  • @args:用于匹配当前执行的方法传入的参数持有指定注解的执行
  • @annotation:用于匹配当前执行方法持有指定注解的方法
  • bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法
    10种标签组成了12种用法

1、execution

使用execution(方法表达式)匹配方法执行。 基本格式:

@Pointcut("execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)")
  • 其中带 ?号的 modifiers-pattern?,declaring-type-pattern?,throws-pattern?是可选项
  • ret-type-pattern,name-pattern, parameters-pattern是必选项
  • modifier-pattern? 修饰符匹配,如public 表示匹配公有方法
  • ret-type-pattern 返回值匹配,* 表示任何返回值,全路径的类名等
  • declaring-type-pattern? 类路径匹配
  • name-pattern 方法名匹配,* 代表所有,set*,代表以set开头的所有方法
  • (param-pattern) 参数匹配,指定方法参数(声明的类型),(..)代表所有参数,( ,String)代表第一个参数为任何值,第 * 二个为String类型,(..,String)代表最后一个参数是String类型
  • throws-pattern? 异常类型匹配

举例说明

image.png

以上是频繁重点使用的其余参考连接,重点!!!!!

-----------其他用法后续更新的时候加上!---------------

参考:

spring面试冲刺

利用反射获取任意包下所有类、方法、字段的自定义注解信息