I.はじめに
Spring の AOP (Aspect Oriented Programming) はエージェントベースの AOP 実装であり、エージェントベースの技術を使用することで、元のコードを変更することなく拡張および改善できます。Spring AOPはアスペクト指向プログラミングの機能を実現し、ビジネスロジックから横断的な関心事(Cross-cutting Concerns)を抽出し、対象オブジェクトにアスペクトを適用することで機能強化を実現する。Spring AOP は、複数のタイプのアドバイスをサポートしています: 事前アドバイス (@Before)、事後アドバイス (@AfterReturning)、スローアドバイス (@AfterThrowing)、最終アドバイス (@After)、およびサラウンドアドバイス (@Around)。異なるものを選択する必要があります。
この記事の実装で使用される主なテクノロジは Spring の AOP で、AOP はカスタム アノテーションを通じてアノテーションに切り込み、エントリ ポイントでアノテーション パラメータやその他の情報を取得し、キャッシュのために Redis を呼び出します。キャッシュがミスした場合は、エントリ ポイントが解放され、メソッドが正常に実行され、返された結果が取得され、その後 Redis キャッシュが実行され、最後に結果が返されます。ヒットした場合は、パフォーマンスを向上させるという目的を達成するために、直接 Redis キャッシュに戻ります。
2. 依存関係の導入
プロジェクトは 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 を使用してビルドする場合、主な依存関係は、Redis にキャッシュされるため、spring-boot-starter、spring-boot-starter-data-redis、aspectjrt、hutool-all、spring-boot-starter-aop になります。 Redis の依存関係が必要不可欠; hutool-all はその中で使用されるツール クラスであり、主に Redis に保存されるキーとして MD5 暗号化データを生成するために使用されます; Spring の AOP を使用するには、aspectjrt の導入に加えて、AOP も必須です。そうでなければ割り込むことは不可能です。
3. コード
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();
}
アスペクトクラス、ここではアノテーションから割り込みます。アノテーションを呼び出す場所がある限り、割り込みます。
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;
}
}
コントローラー層とサービス層では、サービス層のメソッドによってカスタム アノテーションが追加されます。
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 秒間スリープします。アノテーションで使用されている有効期限は、Redis キャッシュが 60 秒後に期限切れになることを意味します。期限切れになるタイミングを指定するには、expireAt を使用することもできます。
4. 試験結果
まず、Redis でキーを確認すると、現在キャッシュされたデータがないことがわかります。
プロジェクトを開始します。最初の呼び出しの結果は次のとおりです。
最初の呼び出しの表示時間は 5 秒を超えており、Redis にはキャッシュされたデータもあります。
60 秒以内に 2 回目の電話をかけます。
時間が直接 15 ミリ秒に達していることがわかります。これは、キャッシュが有効であり、コード内の 5 秒の休止状態を直接スキップしていることを証明しています。60 秒後、キャッシュされたデータは自動的に期限切れになり、残りのテストは表示されません。興味がある場合は、指定した時間に期限切れになる ExpireAt メソッドを試すこともできます。