Spring AOP + 自定义注解

一、什么是AOP

Spring AOP(面向切面编程)是Spring框架的一个重要模块,用于通过在运行时动态地织入到现有的类中,实现横切关注点的功能。横切关注点是那些会影响多个模块的功能,如日志、事务管理、安全性等。

Spring AOP提供了一种方便的方式来实现横切关注点的功能,而无需修改目标对象的源代码。他通过代理机制,在目标对象的方法执行前、执行后或发生异常时插入额外的逻辑,从而实现注入性能监控、事务管理、日志记录等功能。这样可以避免在每个需要这些功能的方法中都编写重复的代码,提高了代码的重用性和可维护性。

二、核心概念

切点(Pointcut):定义了在那些连接点(Join Point)上应用通知(Advice)。切点是一个表达式,用于匹配连接点。

通知(Advice):定义了在切点处执行的逻辑,比如在方法执行前后执行的代码。

连接点(Join Point):在应用程序执行过程中的某个特定点,必须方法执行时、异常抛出等,连接点是代码的具体执行位置,它代表了横切关注点可以插入的地方。

切面(Aspect):切面是切点和通知的组合,他定义了在哪里以及何时应用通知。

三、Spring AOP支持的通知类型

前置通知使用@Before: 通知方法会在方法调用之前执行

后置通知使用@After: 通知方法会在目标方法返回或者抛出异常后调用

返回通知使用@AfterReturning: 通知方法在目标方法返回后调用

异常通知使用 @AfterThrowing: 通知方法在目标方法抛出异常后调用

环绕通知使用 @Around: 通知包裹目标方法,在目标方法执行之前、执行之后添加自定义的行为。

四、切入点表达式
execution:用于匹配方法执行的连接点

within:用于匹配指定类型内的方法执行

this:用于匹配当前AOP代理对象类型的执行方法的切入点表达式;在spring AOP中,AOP代理对象是Spring框架动态生成的对象,他包含了原始对象的引用,并且可以执行通知逻辑。使用this来匹配的话,会通过AOP代理对象的类型进行匹配,这也就意味着可能会包括引入的接口类型(举个例子:假设有个接口UserService,并且有一个实现类UserServiceImpl,如果你在配置AOP时使用this(UserService)那么无论是代理对象还是目标对象,只要类型是UserService或其子类,都会被匹配到)

target:用于匹配当前目标对象类型的执行方法的切入点表达式,目标对象是指被代理的原始对象,而不是代理对象本身。当使用target时,匹配的是目标对象的类型,不包括引入的接口类型(举个例子:如果使用target(UserService),那么只有目标对象的类型为UserService的方法会被匹配到,而不包括代理对象的类型))

针对this、target来说,this匹配的是AOP代理对象的类型(包括引入的接口类型),而target匹配的是目标对象的类型(不包括引入的接口接口类型)

args:用于匹配当前执行方法传入参数为指定类型的执行方法

@within: 用于匹配所有持有执行注解类型内的方法,当在切面配置中使用@within注解时,会匹配所有类级别被指定注解标记的方法
示例:@within(com.example.annotation.MyAnnotation)匹配所有被MyAnnotation注解标记的方法

@target: 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解。在切面配置中使用@target注解时,会匹配当前目标对象(而不是代理对象)持有指定注解的方法。

@args:用于匹配当前执行方法传入的参数持有指定注解的执行。在切面配置中使用@args注解时,会匹配当前方法参数中包含指定注解的方法

@annotation:用于匹配带有特定注解的连接点,即匹配被某个特定注解标记的方法。

类型匹配语法

* :匹配任何数量的字符
.. :匹配任何数量的字符,如在类型模式中匹配任何数量的子包;而在方法参数模式中匹配任何数量参数。
+ :匹配指定类型及其所有子类型,仅能作为后缀放在类型模式后边

java.lang.String    匹配String类型
java.*.String    匹配java包下的任何“一级子包”下的String类型;如匹配java.lang.String而不匹配                     java.lang.ss.String
java..*    匹配java包及任何子包下的任何类型,如匹配java.lang.String
java.lang.*ing    匹配任何java.lang包下的以ing结尾的类型
java.lang.Number+    匹配java.lang包下类型为Number及Number的子类型数据

1.execution()  用于描述方法【掌握】
    语法:execution(修饰符  返回值  包.类.方法名(参数) throws异常)
        修饰符,一般省略
            public      公共方法
            *           任意
        返回值,不能省略
            void        返回没有值
            String      返回值字符串
            *           任意
        包,[可以省略]
            com.itheima.crm                 固定的包
            com.itheima.crm.*.service       crm包下面的任意子包,固定目录service(例如:com.itheima.crm.staff.service)
            com.itheima.crm..               crm包下面的所有子包(含自己)
            com.itheima.crm.*.service..     crm包下面的任意子包,固定目录service,service目录任意包(含自己)
        类,[可以省略]
            UserServiceImpl                 指定的类
            *Impl                           以Impl结尾的类
            User*                           以User开头的类
            *                               任意的类
        方法名,不能省略
            addUser                         固定的方法名
            add*                            以add开头的方法名
            *Do                             以Do结尾的方法名
            *                               任意的方法名
        (参数)
            ()                              无参
            (int)                           一个整型
            (int, int)                      两个整型
            (..)                            参数任意
        throws,[可以省略],一般省略。
    综合案例1:
        execution(* com.itheima.crm.*.service..*.*(..))
    综合案例2:
        <aop:pointcut expression="execution(* com.itheima.*WithCommit.*(..)) || 
                                  execution(* com.itheima.*Service.*(..))" id="myPointCut"/>

2.within:匹配包或子包中的方法(了解)
    within(com.itheima.aop..*)

3.this:匹配实现了接口的代理对象中的方法(了解)
    this(com.itheima.aop.user.UserDAO)

4.target:匹配实现了接口的目标对象中的方法(了解)
    target(com.itheima.aop.user.UserDAO)

5.args:匹配参数格式符合标准的方法(了解)
    args(int, int)

6.bean(id):对指定的bean所有的方法(了解)
    bean('userServiceId')
 五、自定义注解

描述:

AOP+分布式锁:请求到来时,根据redisKey先从缓存中取数据,如果命中缓存,则将结果返回,如果缓存中没有该条数据,则执行业务逻辑代码查询该条数据,如果未查到该条数据,则根据redisKey缓存redis中一个空值,时间为6分钟,如果该条数据不为空,则缓存该条数据24小时

package com.project.common.annotation.cache;

import java.lang.annotation.*;

/**
 * 从缓存中获取数据
 * @Author:yjj
 * @Package:com.project.common.annotation.cache
 * @Date:2024/3/3 10:39
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheData {
    /**
     * 缓存数据前缀
     */
    String prefix() default "cache:";

    /**
     * 缓存数据后缀
     */
    String suffix() default ":info";
}
package com.project.common.annotation.cache;

import com.alibaba.excel.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import com.project.model.base.CommonMessage;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * AOP + 分布式锁
 * @Author:yjj
 * @Package:com.project.common.annotation.cache
 * @Date:2024/3/3 10:43
 */
@Component
@Aspect
@Slf4j
public class CacheDataAspect {

    @Autowired
    private RedisTemplate redisTemplate;
    @Resource
    private RedissonClient redissonClient;
    int count = 0;


    @SneakyThrows
    @Around("@annotation(com.project.common.annotation.cache.CacheData)")
    public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint){

        try {
            Object resultObject = null;
            log.info("前置通知");
            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
            // 1、获取缓存注解---从方法签名中获取到注解的相关信息
            CacheData cacheData = methodSignature.getMethod().getAnnotation(CacheData.class);
            // 1.1 获取方法中的参数,并将方法参数转为String类型,采用 | 分割
            Object[] args = joinPoint.getArgs();
            String argStr = Arrays.asList(args).stream().map(item -> {
                return item.toString();
            }).collect(Collectors.joining("|"));
            // 1.2 拼接redis的key
            String dataKey = cacheData.prefix() + argStr + cacheData.suffix();
            // 2、 从redis中查询数据--如果数据为空则需要去数据库中查询,如果不为空则直接返回结果
            resultObject =  getRedisData(dataKey,methodSignature);
            // 如果缓存数据为空,则需要查询数据库,并将查询结果缓存到redis中
            if (resultObject != null){
                // 2.1 缓存数据不为空,则直接返回
                return resultObject;
            }
            // 2.2 代码执行到此时说明缓存为空,则需要从数据库中查询数据
            // 2.2.1 设置lockKey
            String lockKey = cacheData.prefix() + argStr + CommonMessage.CACHE_DATA_LOCK_SUFFIX;
            RLock lock = redissonClient.getLock(lockKey);
            try {
                boolean flag = lock.tryLock(CommonMessage.CACHE_DATA_EXPIRE_PX1, CommonMessage.CACHE_DATA_EXPIRE_PX2, TimeUnit.SECONDS);
                // 如果获取锁失败,则进行自旋
                if (!flag){
                    count++;
                    if (count<=3){
                        Thread.sleep(100);
                        return this.cacheAroundAdvice(joinPoint);
                    }else {
                        return new RuntimeException("重试3次后仍未获取到锁!!!");
                    }
                }
                // 如果获取锁成功,则执行目标方法
                resultObject = joinPoint.proceed(args);
                if (resultObject == null){
                    redisTemplate.opsForValue().set(dataKey,JSONObject.toJSONString(resultObject),CommonMessage.CACHE_DATA_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                    return resultObject;
                }else{
                    redisTemplate.opsForValue().set(dataKey,JSONObject.toJSONString(resultObject),CommonMessage.CACHE_DATA_TIMEOUT,TimeUnit.SECONDS);
                    return resultObject;
                }
            } finally {
                // 释放锁资源
                lock.unlock();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        log.info("后置通知");
        return joinPoint.proceed(joinPoint.getArgs());
    }



    /**
     * 从缓存中获取数据
     * @param dataKey
     * @param methodSignature
     * @return
     */
    public Object getRedisData(String dataKey, MethodSignature methodSignature) {
        String resultObject = (String) redisTemplate.opsForValue().get(dataKey);
        if (StringUtils.isNotBlank(resultObject)){
            return JSONObject.parseObject(resultObject,methodSignature.getReturnType());
        }
        return null;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值