一,项目所需要的jar信息
<?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包信息就结束了。
二,项目的配置文件信息如下
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配置类。
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工具类进行操作的,所以很重要的。
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操作做下铺垫。
package com.wpw.springbootredis.config;
import java.lang.annotation.*;
/** * @author wpw */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @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@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这样的测试工具手动测试的,不知道并发操作下会不会有问题,所以改成了下面的操作对了,就算出现并发操作,也没什么问题。
因为我要的数据不一定是非常精确的,只要误差不太大就可以了,关于如何模拟多人操作,这里自己还没有真正的实操过,所以暂时不做测试分析了,这里还是继续下面的分析好了,日后写到关于这方面的操作时再进行说明一下吧。
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 */@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和自定义注解实现数据埋点操作就到这里结束了。
为啥会写这篇文章呢?就是为了日后遇到这样的需求操作时,能很快的完成,以及自己将这个内容保存到互联网上,如果能帮助到别人再合适不过了,其实就是一个总结和分享的过程,到这里结束了,需要内容的可以直接下载代码,代码地址为:
https://github.com/myownmyway/springboot-redis.git