java进阶|Springboot整合Redis+Aop+自定义注解实现数据埋点操作

java进阶|Springboot整合Redis+Aop+自定义注解实现数据埋点操作_第1张图片

一,项目所需要的jar信息



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.6.RELEASE
         
    
    com.wpw
    springboot-redis
    0.0.1-SNAPSHOT
    springboot-redis
    Demo project for Spring Boot


    
        1.8
    


    


        
            org.springframework.boot
            spring-boot-starter-web
        


        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            org.springframework.boot
            spring-boot-starter-aop
            2.2.5.RELEASE
        


    


    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    





这里就把需要的jar信息的pom文件信息粘贴出来了,主要是为了日后方便,里面主要用了web,redis操作需要的jar包信息以及aop需要的jar包依赖信息,到这里需要的jar包信息就结束了。

二,项目的配置文件信息如下

spring:
  redis:
    database: 0
    host: localhost
    port: 6379
    password:
    jedis:
      pool:
        max-active: 200
        max-wait: -1ms
        max-idle: 10
        min-idle: 0
  application:
    name: springboot-redis
server:
  port: 8080

项目配置信息,如端口号,项目名称,redis连接地址,端口号,连接数配置信息,写到这突然觉得redis这个点自己还没有去写,之前只有一篇关于docker安装redis以及springboot整合redis文章的操作,还有关于redis操作中缓存雪崩,缓存穿透之类的文字表述,代码方面原不及自己已经写得java基础性操作,以及mybatis系列性文章,以及mysql系列文章的操作,后面有时间自己也需要看下这方面的内容,这里先扯到这里,下面我们看下核心代码的编写过程吧。

三,首先,我们编写一个redis的配置类,首先spring已经提供了下面的操作,只需要注入就可以了,但是它不满足我们这里设置数据的操作,所以重新写了一个redis配置类。

package com.wpw.springbootredis.config;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


/**
 * redis配置类
 *
 * @author wpw
 */
@Configuration
public class RedisConfig {
    @Bean
    @Primary
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        //hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        //value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}


四,基于redis配置类,这里封装了一下常用操作的redis工具类,代码如下,需要的可以看下,本文就是基于这个redis工具类进行操作的,所以很重要的。

package com.wpw.springbootredis.util;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;


import java.util.Map;
import java.util.concurrent.TimeUnit;


/**
 * Redis工具类
 *
 * @author wpw
 */
@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 设置缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据key获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒)返回0代表永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


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


    /**
     * 删除缓存
     *
     * @param key 键 可以传一个值或者多个值
     */
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    /**
     * 根据键获取值
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }


    /**
     * 设置key之间的对应关系
     *
     * @param key   键
     * @param value 值
     * @return true设置成功,false设置失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 设置key/value之间对应的关系且设置过期时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return true成功, false失败
     */
    public boolean setKeyWithTime(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * hashGet
     *
     * @param key  键 ,不能为null
     * @param item 项 ,不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }




    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }


    /**
     * hashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true设置成功, false设置失败
     */
    public boolean hmset(String key, Map map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * hashSet
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true设置成功, false设置失败
     */
    public boolean hmset(String key, Map map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true设置成功,false设置失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * hashSet  设置时间
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒)
     * @return true设置成功, false设置失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}


五,关于redis操作的信息上面都介绍完了,下面我们先定义一个自定义注解,然后使用这个注解进行方法的标注,为下面基于aop操作做下铺垫。

package com.wpw.springbootredis.config;


import java.lang.annotation.*;


/**
 * @author wpw
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CountInvokeTimes {
}


上面定义了一个名字为CountInvokeTimes,生命周期在运行时,作用范围在方法上的自定义注解,关于自定义注解,自己这方面也写过一点,不过用的也少了一些,其中写了一篇关于自定义注解内容的介绍,以及写了一篇基于aop和自定义注解进行统计方法执行耗时时间的,有需要的可以查看历史文章数据进行查找,所以这篇就自己再写了一下关于注解的作用。

六,下面我们定义一个切面类,这个切面类也是本篇文章的重点内容,这里先贴上代码,然后具体看下里面实现的内容。

package com.wpw.springbootredis.config;


import com.wpw.springbootredis.util.RedisUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;


import java.lang.reflect.Method;


/**
 * @author wpw
 */
@Aspect
@Component
public class CountInvokedTimesAspect {
    private final RedisUtil redisUtil;


    public CountInvokedTimesAspect(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }


    @Pointcut("@annotation(com.wpw.springbootredis.config.CountInvokeTimes)")
    public void countInvokeTimes() {
    }


    @Around(value = "countInvokeTimes()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Class[] argTypes = new Class[args.length];
        for (int i = 0, length = args.length; i < length; i++) {
            argTypes[i] = args[i].getClass();
        }
        try {
            String methodName = joinPoint.getSignature().getName();
            Method method = joinPoint.getTarget().getClass().getMethod(methodName, argTypes);
            boolean isAnnotationPresent = method.isAnnotationPresent(CountInvokeTimes.class);
            if (isAnnotationPresent) {
                if (redisUtil.get(methodName) == null) {
                    redisUtil.set(methodName, 1);
                } else {
                    Integer countTimes = (Integer) redisUtil.get(methodName);
                    countTimes += 1;
                    redisUtil.set(methodName, countTimes);
                }
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        Object object = null;
        try {
            object = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return object;
    }
}


    首先获取方法的参数,然后获取方法的名称即methodName,根据方法的名称以及所在的类得到具体的方法,判断方法上是否标注了CountInvokeTimes注解。

    若标记了这个注解,则我们需要对其进行操作,首先我们先根据方法名称去redis里面去查询,判断是否已经存在,若没有存在则把对应的方法名设置为key,值设置为1。

          若存在,则获取对应的方法名称,然后值自增,最后再设置一下,这里由于自己基于postman这样的测试工具手动测试的,不知道并发操作下会不会有问题,所以改成了下面的操作对了,就算出现并发操作,也没什么问题。

     因为我要的数据不一定是非常精确的,只要误差不太大就可以了,关于如何模拟多人操作,这里自己还没有真正的实操过,所以暂时不做测试分析了,这里还是继续下面的分析好了,日后写到关于这方面的操作时再进行说明一下吧。

if (isAnnotationPresent) {
                if (redisUtil.get(methodName) == null) {
                    redisUtil.set(methodName, 1);
                } else {
                    AtomicInteger countTimes = (AtomicInteger) redisUtil.get(methodName);
                    redisUtil.set(methodName, countTimes.incrementAndGet());
                }
            }

七,最后这里贴下关于controller层的代码,由于很简单,只涉及到get方法的测试,使用了三个方法进行模拟测试。

package com.wpw.springbootredis.controller;


import com.wpw.springbootredis.config.CountInvokeTimes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


import java.util.Arrays;
import java.util.List;


/**
 * @author wpw
 */
@RestController
public class UserController {
    @CountInvokeTimes
    @RequestMapping(value = "/hello")
    public String hello() {
        return "hello redis";
    }


    @CountInvokeTimes
    @GetMapping(value = "/list")
    public List list() {
        return Arrays.asList("hello", "hello");
    }


    @GetMapping(value = "/say")
    public String say() {
        return "say";
    }
}


八,最后测试了一下,我手动通过postman进行调用list方法15次,hello方法2次,say方法2次,我们看下redis数据库的数据信息,看下是否和我们操作的一致。

java进阶|Springboot整合Redis+Aop+自定义注解实现数据埋点操作_第2张图片

这里由于使用了windows下安装redis的操作,所以redis可以看成是单机版服务,这里说下为啥采用了redis进行数据的存储,而不是map或者其它的缓存服务器,其一,redis是基于内存级别的,所以可以达到高性能,其二,redis可以以集群的方式进行部署,即redis的cluster模式可以达到高可用,其三redis是可以将数据持久化到磁盘数据进行保存的,所以避免了数据丢失,最后redis也是很重要的一点是可以达到缓存一致性的,这是其他map所不具备的,所以基于其这么多优点,自己采用了redis进行数据的保存,关于缺点吗,自己暂时先说下,因为引入了第三方的依赖包,所以如何保证其高可用特性就很有必要了,后面关于redis的操作,自己有时间再写了,到这里关于redis的操作基于aop和自定义注解实现数据埋点操作就到这里结束了。

为啥会写这篇文章呢?就是为了日后遇到这样的需求操作时,能很快的完成,以及自己将这个内容保存到互联网上,如果能帮助到别人再合适不过了,其实就是一个总结和分享的过程,到这里结束了,需要内容的可以直接下载代码,代码地址为:

https://github.com/myownmyway/springboot-redis.git

java进阶|Springboot整合Redis+Aop+自定义注解实现数据埋点操作_第3张图片

你可能感兴趣的:(java进阶|Springboot整合Redis+Aop+自定义注解实现数据埋点操作)