Java自定义切面缓存注解如此简单(Redis + SpringAop)

自定义缓存切面注解

1:展示效果

  • 如果使用这种方式,建议使用切面缓存异常类,如果当前方法缓存失败,也应该执行具体业务逻辑而非报错终止程序,具体参考切面缓存@CacheException
  • 1.1: 成功保存到redis效果
  • 第一次Redis没有值,所以保存查询了数据库。
    在这里插入图片描述
    在这里插入图片描述

1.2:第二次请求

在这里插入图片描述

2.1:前置条件

  • 技术栈需要会简单的redis存、取、删和SpringAop即可

  • 2.2: 安装redis
    链接地址: 官网链接
    在这里插入图片描述

  • 下载完成之后解压到安装路径,如果需要设置redis密码,自己去配置文件中设置即可。我这边就是简单的下载然后再windows中演示了。

3:解压完成之后

在这里插入图片描述

4:如何启动redis

没设置密码直接双击即可
在这里插入图片描述

启动成功能看到如下界面
在这里插入图片描述

如果需要操作查看key则需要启动
在这里插入图片描述

4: 后端的application.yml

  • 我这边项目是微服务的,所以配置都在nacos中,这边只是截取为了方便。如果需要则将nacos的依赖删除
server:
  port: 8002
  servlet:
    context-path: /system # 上下文件路径,请求前缀 ip:port/article

spring:
  cloud:
    nacos:
      discovery:
        # 服务注册中心地址
        server-addr: localhost:8848
  servlet:
    multipart:
      max-request-size: 200MB
      max-file-size: 200MB
  application:
    name: system-service # 应用名
  # 数据源配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 2829
    url: jdbc:mysql://127.0.0.1:3306/qycq?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
    hikari:
      # 连接池名
      pool-name: DateHikariCP
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活最大时间,默认600000(10分钟)
      idle-timeout: 180000
      # 最大连接数,默认10
      maximum-pool-size: 10
      # 从连接池返回的连接的自动提交
      auto-commit: true
      # 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
      max-lifetime: 1800000
      # 连接超时时间,默认30000(30秒)
      connection-timeout: 30000
      # 测试连接是否可用的查询语句
      connection-test-query: SELECT 1 FROM DUAL


  #redis
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 100000ms  #超时时间
    lettuce:
      pool:
        max-active: 1024 #最大连接数
        max-wait: 10000ms #最大等待时间
        max-idle: 200 #最大空闲连接
        min-idle: 5 #最小空闲链接


mybatis-plus:
  type-aliases-package: com.qycq.api.pojo
  # xxxMapper.xml 路径
  mapper-locations: classpath*:/mapper/*Mapper.xml

# 日志级别,会打印sql语句
logging:
  level:
    com.qycq.system.mapper: debug

5:自定义注解

package com.qycq.api.annotations;

import io.swagger.annotations.ApiModel;

import java.lang.annotation.*;

/**
 * 概要:
 * <p> As we all know, where there is Wolong, there must be phoenix!</p>
 *
 * @author 七月初七
 * @version 1.0
 * @date 2021/11/17  6:02
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ApiModel(value = "自定义redis缓存注解")
public @interface CacheRedis {

    /**
     * redis的key
     * @return
     */
    String key() default "";

    /**
     * 缓存失效时间,默认为300秒
     * @return
     */
    long expireTime() default 300;


    /**
     * 是否存储空值,默认为true,防止雪崩
     * @return
     */
    boolean cacheNullValue() default true;

    /**
     * 缓存方法描述
     * @return
     */
    String describe() default "";

}


6:pom依赖

<!--  web依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
		
		  <!--Druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

        <!--  mysql   -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>


        <!--mybatis-plus启动器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>


        <!-- 配置处理器处理yml文件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!--lombok setter,getter-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>


        <!--  json  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <!--swagger依赖-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>

        <!--swaggerUI依赖-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
        </dependency>

        <!-- 工具类依赖 -->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>

        <!--  redis依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--对象池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>

7:Redis工具类

  • RedisService
package com.qycq.common.service;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 概要:
 * <p> As we all know, where there is Wolong, there must be phoenix!</p>
 *
 * @author 七月初七
 * @version 1.0
 * @date 2021/11/17  2:40
 */
public interface RedisService {



    /**
     * 根据key和类型获取缓存数据
     *
     * @param key
     * @param clazz
     * @param <T>
     * @return
     */
    <T> Optional<T> get(String key, Class<T> clazz);

    /**
     * 将数据以键值对的方式加入缓存
     *
     * @param key
     * @param value
     */
    void set(String key, Object value);

    /**
     * 将数据以键值对的方式加入缓存并设置失效时间,默认1天
     *
     * @param key
     * @param value
     * @param timeout
     * @param day
     */
    void setDay(String key, Object value, long timeout, final TimeUnit day);

    /**
     * 将数据以键值对的方式加入缓存并设置失效时间,默认为5分钟
     *
     * @param key
     * @param value
     * @param timeout
     */
    void set(String key, Object value, long timeout);


    /**
     * 将数据以键值对的方式加入缓存并设置失效时间,默认为5秒
     *
     * @param key
     * @param value
     * @param miao
     */
    void set(Object key, Object value, long miao,final TimeUnit second);


    /**
     * 删除多个key
     *
     * @param keys
     */
    void deletes(String... keys);


    /**
     * 从缓存中删除
     *
     * @param keyPattern
     */
    void deleteWithPattern(String keyPattern);

    /**
     * 获取string类型的值
     *
     * @param key
     * @return
     */
    Object getJson(String key);

    /**
     * 设置失效时间
     *
     * @param key
     * @param timeout
     * @param timeUnit
     * @return
     */
    boolean expired(String key, long timeout, TimeUnit timeUnit);

    /**
     * 查看当前key是否存在
     * @param key
     * @return
     */
    boolean hasKey(String key);

}

2: impl

package com.qycq.common.service;

import com.qycq.common.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 概要:
 * <p> As we all know, where there is Wolong, there must be phoenix!</p>
 *
 * @author 七月初七
 * @version 1.0
 * @date 2021/11/17  2:41
 */
@Service("redisService")
@Transactional
@Slf4j
public class RedisServiceImpl implements RedisService{


    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


    @Override
    public <T> Optional<T> get(String key, Class<T> clazz) {
        String value = (String) redisTemplate.opsForValue().get(key);
        if (value == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(JsonUtil.toObject(value, clazz));
    }


    @Override
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, Objects.requireNonNull(value));
    }



    @Override
    public void setDay(String key, Object value, long timeout, TimeUnit day) {
        redisTemplate.opsForValue().set(key, Objects.requireNonNull(value), timeout, TimeUnit.DAYS);
    }

    @Override
    public void set(String key, Object value, long timeout) {
        redisTemplate.opsForValue().set(key, Objects.requireNonNull(value), 5, TimeUnit.MINUTES);
    }

    /**
     * second
     *
     * @param key
     * @param value
     * @param miao
     * @param second
     */
    @Override
    public void set(Object key, Object value, long miao, TimeUnit second) {
        redisTemplate.opsForValue().set((String) key, Objects.requireNonNull(value), 300, TimeUnit.SECONDS);
    }

    @Override
    public void deletes(String... keys) {
        if (keys != null) {
            for (String key : keys) {
                redisTemplate.delete(key);
            }
        }
    }

    @Override
    public void deleteWithPattern(String keyPattern) {
        Set<String> keys = scan(keyPattern);
        if (keys != null) {
            redisTemplate.delete(keys);
        }
    }

    private Set<String> scan(String matchKey) {
        return redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
            Set<String> keysTmp = new HashSet<>();
            Cursor<byte[]> cursor = connection.scan(
                    new ScanOptions.ScanOptionsBuilder().match("*" + matchKey + "*").count(1000).build()
            );
            while (cursor.hasNext()) {
                keysTmp.add(new String(cursor.next()));
            }
            return keysTmp;
        });
    }


    @Override
    public Object getJson(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 判断是否过期
     *
     * @param key
     * @param timeout
     * @param timeUnit
     * @return
     */
    @Override
    public boolean expired(String key, long timeout, TimeUnit timeUnit) {
        return Objects.requireNonNull(redisTemplate.expire(key, timeout, timeUnit));
    }


    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    @Override
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            log.error(key, e);
            return false;
        }
    }
}

3: 暴露bean,这便是做成了父类,因为其他模块引用直接继承就好了或者向下面这样写也可以

package com.qycq.common.config;

import com.qycq.common.service.RedisService;
import com.qycq.common.service.RedisServiceImpl;
import io.swagger.annotations.ApiModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * 概要:
 * <p> As we all know, where there is Wolong, there must be phoenix!</p>
 *
 * @author 七月初七
 * @version 1.0
 * @date 2021/11/17  2:45
 */
@Configuration
@ApiModel(value = "redis配置类")
public class BaseRedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }

    /**
     * 配置redis组件
     *
     * @return
     */
    @Bean
//    @Primary
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        //序列化key value
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        //序列化Hash
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }

    /**
     * 暴露bean
     * @return
     */
    @Bean
    public RedisService redisService(){
        return new RedisServiceImpl();
    }

}

4: 此处制作展示,继承/import注解导入也行,然后将这个注解加到启动类

package com.qycq.api.annotations;

import com.qycq.common.config.BaseRedisConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

/**
 * 概要:
 * <p> As we all know, where there is Wolong, there must be phoenix!</p>
 * 开启redis配置
 *
 * @author 七月初七
 * @version 1.0
 * @date 2021/11/17  3:01
 */
@Documented
@Target({ElementType.TYPE})
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@Import({BaseRedisConfig.class})
public @interface EnableRedisConfig {
    Class<? extends BaseRedisConfig> value() default BaseRedisConfig.class;
    boolean required() default true;

}

8:AOP切面

package com.qycq.system.aspect;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.qycq.api.annotations.CacheException;
import com.qycq.api.annotations.CacheRedis;
import com.qycq.common.service.RedisService;
import com.qycq.common.utils.HttpServletRequestUtil;
import com.qycq.common.utils.IpUtils;
import io.swagger.annotations.ApiModel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 概要:
 * <p> As we all know, where there is Wolong, there must be phoenix!</p>
 *
 * @author 七月初七
 * @version 1.0
 * @date 2021/11/17  6:07
 */
@Configuration
@Slf4j
@Aspect
@ApiModel(value = "redis缓存切面类")
@Order(2)
public class CacheRedisAspect {

    @Autowired
    private RedisService redisService;


    /**
     * 默认为查询数据空也存入到redis,防止雪崩
     */
    private boolean FLAG = true;


    /**
     * redis的key,输入则走输入的,不输入默认以前缀 + 接口方法名为主
     */
    private String KEY = "";


    /**
     * 默认存在时间
     */
    public long EXPIRE_TIME = 300;


    /**
     * 切点
     */
    @Pointcut("@annotation(com.qycq.api.annotations.CacheRedis)")
    public void pointcut() {
        // TODO document why this method is empty
    }

    /**
     * 打印日志
     *
     * @param joinPoint
     * @param httpServletRequest
     */
    private void setLog(JoinPoint joinPoint, HttpServletRequest httpServletRequest) {
        log.info("methodType:{}", httpServletRequest.getMethod());
        log.info("requestMethodName:{}", joinPoint.getSignature().getName());
        log.info("requestURL:{}", httpServletRequest.getRequestURL());
        log.info("requestURI:{}", IpUtils.getIpAddr(httpServletRequest));
        log.info("className:{}", joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName());
        log.info("time:{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    }


    /**
     * 环绕增强
     *
     * @param point
     * @return
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point){
        log.info("。。。。。。进入到环绕增强。。。。。。");

        Object result = null;


        /**
         * 判断当前的key是否存在
         */
        if (redisService.hasKey(KEY)) {
            log.warn("...该数据在缓存中已存在,去缓存查数据....");
            result = redisService.getJson(KEY);
            return result;
        }

        //获取全局上下文的HttpServletRequest
        HttpServletRequest httpServletRequest = HttpServletRequestUtil.getContextHttpServletRequest();
        Method method = getMethod(point);
        try {

            //先判断当前方法上是否含有注解或注解为空
            if (method.isAnnotationPresent(CacheRedis.class)) {
                CacheRedis annotation = method.getAnnotation(CacheRedis.class);
                if (annotation == null) {
                    log.warn("当前方法" + "[" + point.getSignature().getName() + "]不需要拦截,因为判断没有注解!");
                    return point.proceed();
                } else {
                    /**
                     * 获取redis的key
                     */
                    if (StringUtils.isEmpty(annotation.key())) {
                        //名字默认为方法名称
                        KEY = point.getSignature().getName();
                        log.info("当前key为:》》》》》》》》》》》{}", KEY);
                    } else {
                        KEY = annotation.key();
                    }


                    /**
                     * 获取超时时间
                     */
                    long expireTime = annotation.expireTime();
                    if (expireTime == EXPIRE_TIME) {
                        //说明没有设置时间
                        EXPIRE_TIME = 300;
                    } else {
                        //否则设置过期时间
                        EXPIRE_TIME = annotation.expireTime();
                    }

                    /**
                     * 如果为true则不需要动
                     */
                    if (annotation.cacheNullValue()) {
                        FLAG = annotation.cacheNullValue();
                    } else {
                        FLAG = false;
                    }
                    //将数据存到redis
                    result = point.proceed();
                    redisService.set(KEY, result, EXPIRE_TIME, TimeUnit.SECONDS);
                    log.info("数据存入redis成功!当前的redisKey为:=========>{}", KEY);
                }
            }
            this.setLog(point, httpServletRequest);
        } catch (Throwable throwable) {
            log.error("缓存方法发生异常,打印的堆栈信息为:{}", throwable.getMessage());
            throwable.printStackTrace();
        }
        //输出返回结果
        return result;
    }


    /**
     * 获取被拦截方法对象
     *
     * @param pjp
     * @return
     */
    private Method getMethod(ProceedingJoinPoint pjp) {
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        return methodSignature.getMethod();
    }
}

9: 最后将注解放到你的方法上即可,效果如刚进来展示那个样子

在这里插入图片描述

10:项目地址

https://gitee.com/qycq/whistling/tree/master/whistling-parent/whistling-system/src/main/java/com/qycq/system/aspect

注解在api模块, 切面在system下面。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值