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

2020-04-27 10:25:57 浏览数 (1)

一,项目所需要的jar信息

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.2.6.RELEASE</version>        <relativePath/> <!-- lookup parent from repository -->    </parent>    <groupId>com.wpw</groupId>    <artifactId>springboot-redis</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>springboot-redis</name>    <description>Demo project for Spring Boot</description>
    <properties>        <java.version>1.8</java.version>    </properties>
    <dependencies>
        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>
        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <optional>true</optional>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>            <exclusions>                <exclusion>                    <groupId>org.junit.vintage</groupId>                    <artifactId>junit-vintage-engine</artifactId>                </exclusion>            </exclusions>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-redis</artifactId>        </dependency>        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-aop</artifactId>            <version>2.2.5.RELEASE</version>        </dependency>
    </dependencies>
    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build>
</project>

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

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

代码语言:javascript复制
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-redisserver:  port: 8080

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

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

代码语言:javascript复制
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 */@Configurationpublic class RedisConfig {    @Bean    @Primary    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {        RedisTemplate<String, Object> 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工具类进行操作的,所以很重要的。

代码语言:javascript复制
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 */@Componentpublic class RedisUtil {    @Autowired    private RedisTemplate<String, Object> 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<Object, Object> hmget(String key) {        return redisTemplate.opsForHash().entries(key);    }
    /**     * hashSet     *     * @param key 键     * @param map 对应多个键值     * @return true设置成功, false设置失败     */    public boolean hmset(String key, Map<String, Object> 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<String, Object> 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操作做下铺垫。

代码语言:javascript复制
package com.wpw.springbootredis.config;
import java.lang.annotation.*;
/** * @author wpw */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface CountInvokeTimes {}

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

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

代码语言:javascript复制
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@Componentpublic 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这样的测试工具手动测试的,不知道并发操作下会不会有问题,所以改成了下面的操作对了,就算出现并发操作,也没什么问题。

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

代码语言:javascript复制
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方法的测试,使用了三个方法进行模拟测试。

代码语言:javascript复制
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 */@RestControllerpublic class UserController {    @CountInvokeTimes    @RequestMapping(value = "/hello")    public String hello() {        return "hello redis";    }
    @CountInvokeTimes    @GetMapping(value = "/list")    public List<String> list() {        return Arrays.asList("hello", "hello");    }
    @GetMapping(value = "/say")    public String say() {        return "say";    }}

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

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

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

代码语言:javascript复制
https://github.com/myownmyway/springboot-redis.git

0 人点赞