java-你还在用aop拦截帮你做缓存吗?

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

很多人都应该碰到过这种需求,需要优先从缓存中读取数据如果缓存中没有则从数据库中获取,大部分人的想法肯定是用aop对方法切面使用方法名称+参数作为key,但是这种方法有很大的弊端,本文会分析弊端并且提供解决方案。

一、旧的aop实现方式

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cached {
    String value() default ""; // 默认为空,表示使用方法名作为缓存的key
    // 过期时间,单位为秒
    long expireSeconds() default -1; // 默认为-1,表示不过期
    String keyPrefix() default "";// key的前缀
}
 
@Aspect
@Component
public class CachingAspect {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Pointcut("@annotation(com.test.user.annotation.Cached) || @within(com.test.user.annotation.Cached)")
    public void dsPointCut() {
    }

    @Around("dsPointCut()")
    public Object cacheMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        Cached cached = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(Cached.class);
        String cacheKey = getCacheKey(joinPoint,cached);
        Object cachedValue = redisTemplate.opsForValue().get(cacheKey);
        if (cachedValue != null) {
            if(cachedValue.equals("null")){
                return null;
            }
            return cachedValue;
        }
        Object result = joinPoint.proceed();
        if (result != null) {
            redisTemplate.opsForValue().set(cacheKey, result);
            if (cached.expireSeconds() > 0) {
                redisTemplate.expire(cacheKey, cached.expireSeconds(), java.util.concurrent.TimeUnit.SECONDS);
            }
        }else{
            // 防止空值缓存穿透
            redisTemplate.opsForValue().set(cacheKey, "null");
            if (cached.expireSeconds() > 0) {
                redisTemplate.expire(cacheKey, cached.expireSeconds(), java.util.concurrent.TimeUnit.SECONDS);
            }
        }
        return result;
    }

    private String getCacheKey(ProceedingJoinPoint joinPoint, Cached cached) {
        String name = joinPoint.getTarget()
                .getClass()
                .getName();
        Object[] args = joinPoint.getArgs();
        StringBuilder keyBuilder = new StringBuilder(name);
        for (Object arg : args) {
            keyBuilder.append(":").append(arg == null ? "" : arg.toString());
        }
        return cached.keyPrefix() + keyBuilder.toString();
    }
}

代码中使用

@Cached(keyPrefix = RedisKeyConstant.LOCATION_KEY_PREFIX, expireSeconds = 20)
	@Override
	public  MemberCompanyCoordinateVO getCompanyCoordinateCached(Long userId) {
		return this.getBaseMapper().getCompanyCoordinate(userId);
	}

弊端

我们分析代码可以发现在使用时有以下弊端

  1. 缓存过期时间不能动态指定,使用aop注解做缓存时,缓存过期时间只能手动写死在注解上,无法从配置中读取,如果修改失效时间需要重启服务,当然我们也可以在使用注解时不指定失效时间,统一在CachingAspect
    里面做处理,但是这样CachingAspect 就会变得比较复杂不建议这样处理。

@Cached(keyPrefix = RedisKeyConstant.LOCATION_KEY_PREFIX, expireSeconds = 20)

  1. 失效缓存不方便,因为缓存的key是通过rediskey的前缀+方法名称+参数(多个)生成的,所以在失效时极为不方法,存入的时候是在aop中存入,失效的时候却在程序中的另一个地方,不方便集中关联。

所以我们需要换另外一种实现方式

二、新的实现方式

@Slf4j
@Component
public class RedisCacheUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取缓存,如果缓存不存在则调用supplier获取数据并缓存
     * @param cacheKey
     * @param expireSeconds
     * @param timeUnit
     * @param supplier
     * @return
     * @param <T>
     */
    public  <T> T getOrSaveWithExpiration (String cacheKey, Long expireSeconds, TimeUnit timeUnit, Supplier<T> supplier){
        Object cachedValue = redisTemplate.opsForValue().get(cacheKey);
        // 优先取缓存
        if (cachedValue != null) {
            if(cachedValue.equals("null")){
                return null;
            }
            try {
                return (T) cachedValue;
            }catch (Exception e){
                log.error("缓存数据类型错误",e);
                throw new BusinessException("缓存数据类型错误");
            }
        }
        T t = supplier.get();
        if (t != null) {
            redisTemplate.opsForValue().set(cacheKey, t, expireSeconds, timeUnit);
        }else{
            // 防止空值缓存穿透
            redisTemplate.opsForValue().set(cacheKey, "null",expireSeconds, timeUnit);
        }
        return t;
    }

    public void delete(String cacheKey){
        redisTemplate.delete(cacheKey);
    }
}

redisCacheUtil.getOrSaveWithExpiration 方法会根据传入的key优先从缓存中读取,如果缓存中没有则会调用传入的获取数据库中数据的方法从数据库,并且写入到缓存中设置好失效时间。

在代码中使用

// 优先从数据库中读取门店数据
 @Override
    public MemberCompanyCoordinateVO getRedisCompanyData(Long companyId) {
        Long expireSeconds = 86400L;  // 可以改成从配置文件中读取
        return redisCacheUtil.getOrSaveWithExpiration(buildKey(companyId),
                expireSeconds,
                java.util.concurrent.TimeUnit.SECONDS,
                () -> this.getBaseMapper().getCompany(userId));
		// this.getBaseMapper().getCompany(userId)这里是查库 返回MemberCompanyCoordinateVO 对象
    }
	public String buildKey(Long companyId){
       return  RedisKeyConstant.LOGIN_COMPANY_DATA + companyId;
    }

// 失效缓存
   @Override
    public void removeRedisCompanyData(Long companyId) {
        redisCacheUtil.delete(buildKey(companyId));
    }

如果有别的库中的数据也需要维护缓存只需要替换以下参数就能自动实现缓存维护,下面是具体实现。

 @Override
    public User getRedisUserData(Long userId) {
        Long expireSeconds = 86400L;  // 可以改成从配置文件中读取
        String key = "user:" + userId;
        return redisCacheUtil.getOrSaveWithExpiration(key ,
                expireSeconds,
                java.util.concurrent.TimeUnit.SECONDS,
                () -> this.getBaseMapper().getUser(userId));
		// this.getBaseMapper().getUser(userId)这里是查库 返回User 对象
    }

新的实现方式存入时就指定好了失效时间,失效时间可以从配置中读取,后续修改时间失效无需重启服务,而且存入的方法可以和失效的方法写在同一个类中更好维护。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Java中,可以使用注解和AOP(面向切面编程)技术来拦截Controller的所有方法。 首先,需要创建一个自定义的注解,用于标识需要拦截的方法。可以使用如下的注解定义: ```java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Intercept { } ``` 接下来,创建一个切面类,用于实现拦截逻辑。可以使用如下的切面类定义: ```java import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class ControllerInterceptor { @Pointcut("@annotation(Intercept)") public void interceptedMethods() {} @Before("interceptedMethods()") public void beforeIntercept() { // 在方法执行之前拦截的逻辑 } @After("interceptedMethods()") public void afterIntercept() { // 在方法执行之后拦截的逻辑 } } ``` 在上述的切面类中,使用了`@Aspect`注解表示这是一个切面类,使用了`@Component`注解将切面类交由Spring管理。`@Pointcut`注解定义了需要拦截的方法,此处使用了`@annotation(Intercept)`表示拦截带有`Intercept`注解的方法。`@Before`和`@After`注解分别表示在方法执行前和执行后进行拦截处理。 最后,在需要拦截的Controller的方法上使用`@Intercept`注解进行标记,例如: ```java @RestController public class MyController { @Intercept @GetMapping("/") public String index() { return "Hello World"; } } ``` 这样,只要在Controller的方法上使用了`@Intercept`注解,就会触发切面类中的拦截逻辑。 需要注意的是,上述代码使用了Spring AOP来实现AOP功能,因此需要在Spring配置文件中开启AOP的支持。另外,还需要引入相关的依赖,例如Spring AOP的依赖。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值