一、前言
Spring中的AOP(Aspect Oriented Programming)是基于代理的AOP实现,通过使用基于代理的技术,可以在不修改原有代码的情况下,对原有代码进行增强和改进。Spring AOP实现了面向切面编程的功能,将横切关注点(Cross-cutting concern)从业务逻辑中抽离出来,通过将切面应用到目标对象的方法上实现功能增强。Spring AOP支持多种通知类型:前置通知(@Before)、后置通知(@AfterReturning)、抛出通知(@AfterThrowing)、最终通知(@After)以及环绕通知(@Around),可以根据不同的需求进行选择。
本文实现方式主要利用的技术就是Spring的AOP,通过自定义注解,AOP切入到注解上面,然后在切入点里面获取注解的参数等信息,调用Redis进行缓存。如果缓存未命中,切入点进行放行,让方法正常执行,拿到返回结果,然后进行Redis的缓存,最后返回结果。如果命中,则直接返回Redis的缓存,从而实现提升性能的目的。
二、依赖引入
项目是基于Gradle构建,依赖如下:
// gradle 自身需求资源库 放头部
buildscript {
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }// 加载其他Maven仓库
mavenCentral()
}
dependencies {
classpath('org.springframework.boot:spring-boot-gradle-plugin:2.1.1.RELEASE')// 加载插件,用到里面的函数方法
}
}
apply plugin: 'java'
apply plugin: 'idea'
// 使用spring boot 框架
apply plugin: 'org.springframework.boot'
// 使用spring boot的自动依赖管理
apply plugin: 'io.spring.dependency-management'
// 版本信息
group 'com.littledyf'
version '1.0-SNAPSHOT'
// 执行项目中所使用的的资源仓库
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
mavenCentral()
}
// 项目中需要的依赖
dependencies {
// 添加 jupiter 测试的依赖
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
// 添加 jupiter 测试的依赖
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
// 添加 spring-boot-starter-web 的依赖 必须 排除了security 根据自身需求
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.security', module: 'spring-security-config'
}
// 添加 spring-boot-starter-test 该依赖对于编译测试是必须的,默认包含编译产品依赖和编译时依赖
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 添加 junit 测试的依赖
testImplementation group: 'junit', name: 'junit', version: '4.11'
// 添加 lombok
annotationProcessor 'org.projectlombok:lombok:1.18.22' // annotationProcessor代表main下代码的注解执行器
testAnnotationProcessor 'org.projectlombok:lombok:1.18.22'// testAnnotationProcessor代表test下代码的注解执行器
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.22' // compile代表编译时使用的lombok
// redis
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.7.14'
// aspectjrt
implementation group: 'org.aspectj', name: 'aspectjrt', version: '1.9.19'
// https://mvnrepository.com/artifact/cn.hutool/hutool-all
implementation group: 'cn.hutool', name: 'hutool-all', version: '5.8.21'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '2.6.15'
}
test {
useJUnitPlatform()
}
如果使用maven构建,主要依赖就是spring-boot-starter、spring-boot-starter-data-redis、aspectjrt、hutool-all、spring-boot-starter-aop,因为是要缓存到Redis中,Redis依赖是必不可少的;hutool-all是其中使用到的工具类,主要用来生成一些MD5加密数据,用来当做存入Redis的key;要使用Spring的AOP,除了要引入aspectjrt以外,AOP也是必不可少的,否则是无法切入的。
三、代码
SpringBoot启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyTestApplication {
public static void main(String[] args) {
SpringApplication.run(MyTestApplication.class, args);
}
}
配置文件:
server:
port: 8080
spring:
application:
name: my-test-service
redis:
host: 127.0.0.1
port: 6379
Redis配置类:
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
/**
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
return createRedisTemplate(redisConnectionFactory);
}
private RedisTemplate<String, Object> createRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> fastJsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用fastJson
template.setValueSerializer(fastJsonRedisSerializer);
// hash的value序列化方式采用fastJson
template.setHashValueSerializer(fastJsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
注解类:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCacheable {
// 过期时间,多久之后过期
String expire() default "";
// 过期时间,几点过期
String expireAt() default "";
// 名称,一般用服务名
String invoker();
}
Aspect类,这里是从注解那里切入进去,只要有调用注解的地方,就会切入进去:
import cn.hutool.crypto.digest.DigestUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
import static org.springframework.http.HttpStatus.LOCKED;
@Aspect
@Slf4j
@Component
public class MyCacheableAspect {
private final RedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
public MyCacheableAspect(RedisTemplate redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
@Around("@annotation(com.littledyf.cache.redis.MyCacheable)")
public Object getCache(ProceedingJoinPoint joinPoint) {
log.info("进入Cacheable切面");
// 获取方法参数
Object[] arguments = joinPoint.getArgs();
// 获取方法签名
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 获取方法名
String methodName = method.getName();
// 获取注解
MyCacheable annotation = method.getAnnotation(MyCacheable.class);
// 获取注解的值
String invoker = annotation.invoker();
// 获取digest,即redis的key
log.info("Digest准备生成:methodName:" + methodName + ",invoker=" + invoker);
String digest = generateDigest(methodName, invoker, arguments);
log.info("Digest生成:digest=" + digest);
Object redisValue = redisTemplate.opsForValue().get(digest);
if (redisValue == null) {
log.info("缓存未命中:" + digest);
log.info("缓存刷新开始:" + digest);
String expire = annotation.expire();
String expireAt = annotation.expireAt();
redisValue = executeSynOperate(result -> {
if (!result) {
log.error("分布式锁异常");
return null;
}
Object checkGet = redisTemplate.opsForValue().get(digest);
if (checkGet != null) {
return checkGet;
}
// 刷新缓存
refreshCache(joinPoint, digest, expire, expireAt);
if (method.getAnnotation(PostMapping.class) != null) {
redisTemplate.opsForSet().add(methodName, arguments);
}
return redisTemplate.opsForValue().get(digest);
}, digest + "1", 50000
);
}
log.info("Cache返回:digest=" + digest);
return redisValue;
}
private void refreshCache(ProceedingJoinPoint joinPoint, String key, String expire, String expireAt) {
Object methodResult = null;
try {
// 放行,让切面捕获的任务继续执行并获取返回结果
methodResult = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
if (methodResult == null) {
methodResult = new Object();
}
// 如果注解传入的expire参数不为空,则直接设置过期时间,否则看expireAt是否为空,否则设置默认过期时间
if (!expire.equals("")) {
long expireLong = Long.parseLong(expire);
redisTemplate.opsForValue().set(key, methodResult, expireLong, TimeUnit.SECONDS);
} else if (!expireAt.equals("")) {
LocalTime expireAtTime = LocalTime.parse(expireAt);
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireDateTime = LocalDateTime.of(now.toLocalDate(), expireAtTime);
if (expireDateTime.compareTo(now) <= 0) {
expireDateTime = expireDateTime.plusDays(1);
}
redisTemplate.opsForValue().set(key, methodResult, Duration.between(now, expireDateTime));
} else {
redisTemplate.opsForValue().set(key, methodResult, 3600 * 12, TimeUnit.SECONDS);
}
}
// 生成digest,用来当做redis中的key
private String generateDigest(String methodName, String invoker, Object[] arguments) {
String argumentsDigest = "";
if (arguments != null && arguments.length > 0) {
StringBuilder stringBuilder = new StringBuilder();
for (Object argument : arguments) {
try {
String valueAsString = objectMapper.writeValueAsString(argument);
stringBuilder.append(valueAsString);
} catch (JsonProcessingException e) {
log.error("参数" + argument + "字符串处理失败", e);
}
}
byte[] bytes = DigestUtil.md5(stringBuilder.toString());
argumentsDigest = new String(bytes);
}
return methodName + (invoker == null ? "" : invoker) + argumentsDigest;
}
// 等待时间重复获取
private <T> T executeSynOperate(MainOperator<T> operator, String lockCacheKey, long milliTimeout) {
try {
if (operator != null && lockCacheKey != null && milliTimeout >= 0L) {
boolean locked = false;
long startNano = System.nanoTime();
boolean waitFlag = milliTimeout > 0L;
long nanoTimeOut = (waitFlag ? milliTimeout : 50L) * 1000000L;
T resultObj = null;
try {
while (System.nanoTime() - startNano < nanoTimeOut) {
if (redisTemplate.opsForValue().setIfAbsent(lockCacheKey, LOCKED, 120L, TimeUnit.SECONDS)) {
locked = true;
break;
}
if (!waitFlag) {
break;
}
Thread.sleep(1000);
}
resultObj = operator.executeInvokeLogic(locked);
} catch (Exception ex) {
log.error("处理逻辑", ex);
return null;
} finally {
if (locked) {
releaseRedisLock(lockCacheKey);
}
}
return resultObj;
} else {
throw new Exception("参数不合法");
}
} catch (Exception e) {
return null;
}
}
/**
* 释放锁
*
* @param cacheKey
*/
public boolean releaseRedisLock(final String cacheKey) {
Boolean deleteLock = redisTemplate.delete(cacheKey);
if (Boolean.TRUE.equals(deleteLock)) {
return true;
}
return false;
}
// 函数式接口
public interface MainOperator<T> {
boolean HOLD_LOCK_TAG = false;
T executeInvokeLogic(boolean result) throws Exception;
}
}
controller层和service层,service层中的方法会加上自定义注解:
import com.littledyf.cache.redis.service.RedisCacheServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/my-test/redis-cache")
public class RedisCacheController {
private final RedisCacheServiceImpl redisCacheService;
public RedisCacheController(RedisCacheServiceImpl redisCacheService) {
this.redisCacheService = redisCacheService;
}
@GetMapping(value = "/test/{value}")
public String testRedisCache(@PathVariable("value") String value) {
return redisCacheService.testRedisCache(value);
}
}
import com.littledyf.cache.redis.MyCacheable;
import org.springframework.stereotype.Service;
@Service
public class RedisCacheServiceImpl {
@MyCacheable(expire = "60", invoker = "my-test")
public String testRedisCache(String value) {
System.err.println("testRedisCache");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟业务逻辑处理
return value;
}
}
这里使用休眠5秒来查验效果,注解中用的expire,即redis的缓存在60秒后过期,也可以使用expireAt指定在具体什么时间过期。
四、测试结果
首先可以查看Redis中的key,可以看到目前是没有缓存数据的:
启动项目,第一次调用结果:
第一次调用显示时间5s以上,并且Redis中也有了缓存数据:
在60s时间内再进行第二次调用:
可以发现时间直接来到15ms,证明缓存是有效的,直接跳过了代码中休眠的5s。等60s后,刚才缓存的数据自动会过期,其余测试就不展示了,有兴趣也可以试试 expireAt在指定时间过期的方法。