Skip to content

Commit 803a6a1

Browse files
authored
Cache modules refactored and standardized (#635)
* Cache modules refactored and standardized CommonUtils and MethodUtils type check improved and reinforced * Fixed proper method naming for generated impls and modules Fixed parent cache interface with proper type args scan * Cleanup * Updated and upgraded Netty dependency and module with IoUring support Added support for NettyModule in LettuceModule to reuse global EventLoop Added and updated module-info.java for netty and lettuce and cache modules * Fix * Fix module-info.java and downgrade kotlinpoet * Fixed proper CommonUtils#isFuture and CommonUtils#isCompletionStage and MethodUtils checks * Fixed MdcAspect to not support for CompletionStage * Fixed CacheSymbolProcessor.kt replace types * Updated Redis Telemetry * Added support for null keys and extended mappers for Redis (#641) * feat(cache): добавлена поддержка null-ключей и расширенные мапперы для Redis - Поддержка null в ключах кэша с выделенными значениями для безопасной сериализации - Добавлены мапперы для Boolean, Character, Short, Float, Double, BigDecimal, временных типов и колл * Reinforced AbstractRedisCache map key & value serialization Reinforced and enchanted RedisCacheMapperModule with more mappers (cherry picked from commit 85330a0) # Conflicts: # cache/cache-annotation-processor/src/main/java/io/koraframework/cache/annotation/processor/CacheAnnotationProcessor.java # cache/cache-redis-lettuce/src/main/java/io/koraframework/cache/redis/RedisCacheKeyMapper.java # cache/cache-redis-lettuce/src/main/java/io/koraframework/cache/redis/RedisCacheMapperModule.java # cache/cache-redis/src/main/java/ru/tinkoff/kora/cache/redis/AbstractRedisCache.java * Fixed check on computeIfAbsent * Standartized telemetry
1 parent 62d791f commit 803a6a1

130 files changed

Lines changed: 3149 additions & 1702 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cache/cache-annotation-processor/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ dependencies {
77
testImplementation testFixtures(project(":annotation-processor-common"))
88
testImplementation project(":internal:test-logging")
99
testImplementation project(":cache:cache-caffeine")
10-
testImplementation project(":cache:cache-redis")
10+
testImplementation project(":cache:cache-redis-lettuce")
1111
testImplementation project(":json:json-common")
1212
testImplementation project(":config:config-common")
1313
}

cache/cache-annotation-processor/src/main/java/io/koraframework/cache/annotation/processor/CacheAnnotationProcessor.java

Lines changed: 117 additions & 57 deletions
Large diffs are not rendered by default.

cache/cache-annotation-processor/src/main/java/io/koraframework/cache/annotation/processor/CacheOperationUtils.java

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,20 @@ public final class CacheOperationUtils {
2828
private static final ClassName KEY_MAPPER_8 = ClassName.get("io.koraframework.cache", "CacheKeyMapper", "CacheKeyMapper8");
2929
private static final ClassName KEY_MAPPER_9 = ClassName.get("io.koraframework.cache", "CacheKeyMapper", "CacheKeyMapper9");
3030

31-
private static final ClassName ANNOTATION_CACHEABLE = ClassName.get("io.koraframework.cache.annotation", "Cacheable");
32-
private static final ClassName ANNOTATION_CACHEABLES = ClassName.get("io.koraframework.cache.annotation", "Cacheables");
33-
private static final ClassName ANNOTATION_CACHE_PUT = ClassName.get("io.koraframework.cache.annotation", "CachePut");
34-
private static final ClassName ANNOTATION_CACHE_PUTS = ClassName.get("io.koraframework.cache.annotation", "CachePuts");
35-
private static final ClassName ANNOTATION_CACHE_INVALIDATE = ClassName.get("io.koraframework.cache.annotation", "CacheInvalidate");
36-
private static final ClassName ANNOTATION_CACHE_INVALIDATES = ClassName.get("io.koraframework.cache.annotation", "CacheInvalidates");
31+
public static final ClassName ANNOTATION_CACHEABLE = ClassName.get("io.koraframework.cache.annotation", "Cacheable");
32+
public static final ClassName ANNOTATION_CACHEABLES = ClassName.get("io.koraframework.cache.annotation", "Cacheables");
33+
public static final ClassName ANNOTATION_CACHE_PUT = ClassName.get("io.koraframework.cache.annotation", "CachePut");
34+
public static final ClassName ANNOTATION_CACHE_PUTS = ClassName.get("io.koraframework.cache.annotation", "CachePuts");
35+
public static final ClassName ANNOTATION_CACHE_INVALIDATE = ClassName.get("io.koraframework.cache.annotation", "CacheInvalidate");
36+
public static final ClassName ANNOTATION_CACHE_INVALIDATES = ClassName.get("io.koraframework.cache.annotation", "CacheInvalidates");
37+
public static final ClassName ANNOTATION_CACHE_INVALIDATE_ALL = ClassName.get("io.koraframework.cache.annotation", "CacheInvalidateAll");
38+
public static final ClassName ANNOTATION_CACHE_INVALIDATE_ALLS = ClassName.get("io.koraframework.cache.annotation", "CacheInvalidateAlls");
3739

3840
private static final Set<String> CACHE_ANNOTATIONS = Set.of(
3941
ANNOTATION_CACHEABLE.canonicalName(), ANNOTATION_CACHEABLES.canonicalName(),
4042
ANNOTATION_CACHE_PUT.canonicalName(), ANNOTATION_CACHE_PUTS.canonicalName(),
41-
ANNOTATION_CACHE_INVALIDATE.canonicalName(), ANNOTATION_CACHE_INVALIDATES.canonicalName()
43+
ANNOTATION_CACHE_INVALIDATE.canonicalName(), ANNOTATION_CACHE_INVALIDATES.canonicalName(),
44+
ANNOTATION_CACHE_INVALIDATE_ALL.canonicalName(), ANNOTATION_CACHE_INVALIDATE_ALLS.canonicalName()
4245
);
4346

4447
private CacheOperationUtils() {}
@@ -47,42 +50,37 @@ public static CacheOperation getCacheOperation(ExecutableElement method, Process
4750
final List<AnnotationMirror> cacheables = getRepeatedAnnotations(method, ANNOTATION_CACHEABLE.canonicalName(), ANNOTATION_CACHEABLES.canonicalName());
4851
final List<AnnotationMirror> puts = getRepeatedAnnotations(method, ANNOTATION_CACHE_PUT.canonicalName(), ANNOTATION_CACHE_PUTS.canonicalName());
4952
final List<AnnotationMirror> invalidates = getRepeatedAnnotations(method, ANNOTATION_CACHE_INVALIDATE.canonicalName(), ANNOTATION_CACHE_INVALIDATES.canonicalName());
53+
final List<AnnotationMirror> invalidateAlls = getRepeatedAnnotations(method, ANNOTATION_CACHE_INVALIDATE_ALL.canonicalName(), ANNOTATION_CACHE_INVALIDATE_ALLS.canonicalName());
5054

5155
final String className = method.getEnclosingElement().getSimpleName().toString();
5256
final String methodName = method.getSimpleName().toString();
5357
final CacheOperation.Origin origin = new CacheOperation.Origin(className, methodName);
5458

5559
if (!cacheables.isEmpty()) {
56-
if (!puts.isEmpty() || !invalidates.isEmpty()) {
60+
if (!puts.isEmpty() || !invalidates.isEmpty() || !invalidateAlls.isEmpty()) {
5761
throw new ProcessingErrorException(new ProcessingError(Diagnostic.Kind.ERROR,
5862
"Method must have Cache annotations with same operation type, but got multiple different operation types for " + origin, method));
5963
}
6064

6165
return getOperation(method, cacheables, CacheOperation.Type.GET, env, aspectContext);
6266
} else if (!puts.isEmpty()) {
63-
if (!invalidates.isEmpty()) {
67+
if (!invalidates.isEmpty() || !invalidateAlls.isEmpty()) {
6468
throw new ProcessingErrorException(new ProcessingError(Diagnostic.Kind.ERROR,
6569
"Method must have Cache annotations with same operation type, but got multiple different operation types for " + origin, method));
6670
}
6771

6872
return getOperation(method, puts, CacheOperation.Type.PUT, env, aspectContext);
6973
} else if (!invalidates.isEmpty()) {
70-
var invalidateAlls = invalidates.stream()
71-
.flatMap(a -> a.getElementValues().entrySet().stream())
72-
.filter(e -> e.getKey().getSimpleName().contentEquals("invalidateAll"))
73-
.map(e -> ((boolean) e.getValue().getValue()))
74-
.toList();
75-
76-
final boolean anyInvalidateAll = !invalidateAlls.isEmpty() && invalidateAlls.stream().anyMatch(v -> v);
77-
final boolean allInvalidateAll = !invalidateAlls.isEmpty() && invalidateAlls.stream().allMatch(v -> v);
78-
79-
if (anyInvalidateAll && !allInvalidateAll) {
74+
if (!invalidateAlls.isEmpty()) {
8075
throw new ProcessingErrorException(new ProcessingError(Diagnostic.Kind.ERROR,
81-
ANNOTATION_CACHE_INVALIDATE.canonicalName() + " not all annotations are marked 'invalidateAll' out of all for " + origin, method));
76+
ANNOTATION_CACHE_INVALIDATE.canonicalName() + " not all annotations are marked 'invalidate' out of all for " + origin, method));
8277
}
8378

84-
final CacheOperation.Type type = (allInvalidateAll) ? CacheOperation.Type.EVICT_ALL : CacheOperation.Type.EVICT;
79+
final CacheOperation.Type type = CacheOperation.Type.EVICT;
8580
return getOperation(method, invalidates, type, env, aspectContext);
81+
} else if (!invalidateAlls.isEmpty()) {
82+
final CacheOperation.Type type = CacheOperation.Type.EVICT_ALL;
83+
return getOperation(method, invalidateAlls, type, env, aspectContext);
8684
}
8785

8886
throw new ProcessingErrorException(new ProcessingError(Diagnostic.Kind.ERROR,
@@ -102,7 +100,7 @@ private static CacheOperation getOperation(ExecutableElement method,
102100
final List<CacheOperation.CacheExecution> cacheExecutions = new ArrayList<>();
103101
for (var annotation : cacheAnnotations) {
104102
var parameters = annotation.getElementValues().entrySet().stream()
105-
.filter(e -> e.getKey().getSimpleName().contentEquals("parameters"))
103+
.filter(e -> e.getKey().getSimpleName().contentEquals("args"))
106104
.map(e -> ((List<?>) (e.getValue()).getValue()).stream()
107105
.filter(a -> a instanceof AnnotationValue)
108106
.map(a -> ((AnnotationValue) a).getValue().toString())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.koraframework.cache.annotation.processor.aop;
2+
3+
import com.palantir.javapoet.ClassName;
4+
import com.palantir.javapoet.CodeBlock;
5+
import io.koraframework.annotation.processor.common.CommonClassNames;
6+
import io.koraframework.annotation.processor.common.MethodUtils;
7+
import io.koraframework.annotation.processor.common.ProcessingErrorException;
8+
import io.koraframework.cache.annotation.processor.CacheOperation;
9+
import io.koraframework.cache.annotation.processor.CacheOperationUtils;
10+
11+
import javax.annotation.processing.ProcessingEnvironment;
12+
import javax.lang.model.element.ExecutableElement;
13+
import java.util.Set;
14+
15+
import static io.koraframework.cache.annotation.processor.CacheOperationUtils.ANNOTATION_CACHE_INVALIDATE_ALL;
16+
import static io.koraframework.cache.annotation.processor.CacheOperationUtils.ANNOTATION_CACHE_INVALIDATE_ALLS;
17+
18+
public class CacheInvalidateAllAopKoraAspect extends AbstractAopCacheAspect {
19+
20+
private final ProcessingEnvironment env;
21+
22+
public CacheInvalidateAllAopKoraAspect(ProcessingEnvironment env) {
23+
this.env = env;
24+
}
25+
26+
@Override
27+
public Set<ClassName> getSupportedAnnotationClassNames() {
28+
return Set.of(ANNOTATION_CACHE_INVALIDATE_ALL, ANNOTATION_CACHE_INVALIDATE_ALLS);
29+
}
30+
31+
@Override
32+
public ApplyResult apply(ExecutableElement method, String superCall, AspectContext aspectContext) {
33+
if (MethodUtils.isPublisher(method)) {
34+
throw new ProcessingErrorException("@%s can't be applied for type ".formatted(ANNOTATION_CACHE_INVALIDATE_ALL.simpleName()) + CommonClassNames.publisher, method);
35+
} else if (MethodUtils.isFuture(method)) {
36+
throw new ProcessingErrorException("@%s can't be applied for type ".formatted(ANNOTATION_CACHE_INVALIDATE_ALL) + method.getReturnType().toString(), method);
37+
}
38+
39+
final CacheOperation operation = CacheOperationUtils.getCacheOperation(method, env, aspectContext);
40+
final CodeBlock body = buildBodySyncAll(method, operation, superCall);
41+
return new ApplyResult.MethodBody(body);
42+
}
43+
44+
private CodeBlock buildBodySyncAll(ExecutableElement method,
45+
CacheOperation operation,
46+
String superCall) {
47+
final String superMethod = getSuperMethod(method, superCall);
48+
49+
// cache variables
50+
final StringBuilder builder = new StringBuilder();
51+
52+
// cache super method
53+
if (MethodUtils.isVoid(method)) {
54+
builder.append(superMethod).append(";\n");
55+
} else {
56+
builder.append("var _value = ").append(superMethod).append(";\n");
57+
}
58+
59+
// cache invalidate
60+
for (var cache : operation.executions()) {
61+
builder.append(cache.field()).append(".invalidateAll();\n");
62+
}
63+
64+
if (!MethodUtils.isVoid(method)) {
65+
builder.append("return _value;");
66+
}
67+
68+
return CodeBlock.builder()
69+
.add(builder.toString())
70+
.build();
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.koraframework.cache.annotation.processor.aop;
2+
3+
import io.koraframework.aop.annotation.processor.KoraAspect;
4+
import io.koraframework.aop.annotation.processor.KoraAspectFactory;
5+
6+
import javax.annotation.processing.ProcessingEnvironment;
7+
import java.util.Optional;
8+
9+
public class CacheInvalidateAllAopKoraAspectFactory implements KoraAspectFactory {
10+
11+
@Override
12+
public Optional<KoraAspect> create(ProcessingEnvironment processingEnvironment) {
13+
return Optional.of(new CacheInvalidateAllAopKoraAspect(processingEnvironment));
14+
}
15+
}

cache/cache-annotation-processor/src/main/java/io/koraframework/cache/annotation/processor/aop/CacheInvalidateAopKoraAspect.java

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,17 @@
1212
import javax.lang.model.element.ExecutableElement;
1313
import java.util.Set;
1414

15-
public class CacheInvalidateAopKoraAspect extends AbstractAopCacheAspect {
15+
import static io.koraframework.cache.annotation.processor.CacheOperationUtils.ANNOTATION_CACHE_INVALIDATE;
16+
import static io.koraframework.cache.annotation.processor.CacheOperationUtils.ANNOTATION_CACHE_INVALIDATES;
1617

17-
private static final ClassName ANNOTATION_CACHE_INVALIDATE = ClassName.get("io.koraframework.cache.annotation", "CacheInvalidate");
18-
private static final ClassName ANNOTATION_CACHE_INVALIDATES = ClassName.get("io.koraframework.cache.annotation", "CacheInvalidates");
18+
public class CacheInvalidateAopKoraAspect extends AbstractAopCacheAspect {
1919

2020
private final ProcessingEnvironment env;
2121

2222
public CacheInvalidateAopKoraAspect(ProcessingEnvironment env) {
2323
this.env = env;
2424
}
2525

26-
@Override
27-
public Set<String> getSupportedAnnotationTypes() {
28-
return Set.of(ANNOTATION_CACHE_INVALIDATE.canonicalName(), ANNOTATION_CACHE_INVALIDATES.canonicalName());
29-
}
30-
3126
@Override
3227
public Set<ClassName> getSupportedAnnotationClassNames() {
3328
return Set.of(ANNOTATION_CACHE_INVALIDATE, ANNOTATION_CACHE_INVALIDATES);
@@ -36,18 +31,13 @@ public Set<ClassName> getSupportedAnnotationClassNames() {
3631
@Override
3732
public ApplyResult apply(ExecutableElement method, String superCall, AspectContext aspectContext) {
3833
if (MethodUtils.isPublisher(method)) {
39-
throw new ProcessingErrorException("@CacheInvalidate can't be applied for type " + CommonClassNames.publisher, method);
34+
throw new ProcessingErrorException("@%s can't be applied for type ".formatted(ANNOTATION_CACHE_INVALIDATE.simpleName()) + CommonClassNames.publisher, method);
35+
} else if (MethodUtils.isFuture(method)) {
36+
throw new ProcessingErrorException("@%s can't be applied for type ".formatted(ANNOTATION_CACHE_INVALIDATE) + method.getReturnType().toString(), method);
4037
}
4138

4239
final CacheOperation operation = CacheOperationUtils.getCacheOperation(method, env, aspectContext);
43-
44-
final CodeBlock body;
45-
if (operation.type() == CacheOperation.Type.EVICT_ALL) {
46-
body = buildBodySyncAll(method, operation, superCall);
47-
} else {
48-
body = buildBodySync(method, operation, superCall);
49-
}
50-
40+
final CodeBlock body = buildBodySync(method, operation, superCall);
5141
return new ApplyResult.MethodBody(body);
5242
}
5343

@@ -114,33 +104,4 @@ private CodeBlock buildBodySync(ExecutableElement method,
114104

115105
return builder.build();
116106
}
117-
118-
private CodeBlock buildBodySyncAll(ExecutableElement method,
119-
CacheOperation operation,
120-
String superCall) {
121-
final String superMethod = getSuperMethod(method, superCall);
122-
123-
// cache variables
124-
final StringBuilder builder = new StringBuilder();
125-
126-
// cache super method
127-
if (MethodUtils.isVoid(method)) {
128-
builder.append(superMethod).append(";\n");
129-
} else {
130-
builder.append("var _value = ").append(superMethod).append(";\n");
131-
}
132-
133-
// cache invalidate
134-
for (var cache : operation.executions()) {
135-
builder.append(cache.field()).append(".invalidateAll();\n");
136-
}
137-
138-
if (!MethodUtils.isVoid(method)) {
139-
builder.append("return _value;");
140-
}
141-
142-
return CodeBlock.builder()
143-
.add(builder.toString())
144-
.build();
145-
}
146107
}

cache/cache-annotation-processor/src/main/java/io/koraframework/cache/annotation/processor/aop/CachePutAopKoraAspect.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.palantir.javapoet.ClassName;
44
import com.palantir.javapoet.CodeBlock;
5+
import io.koraframework.annotation.processor.common.CommonClassNames;
56
import io.koraframework.annotation.processor.common.CommonUtils;
67
import io.koraframework.annotation.processor.common.MethodUtils;
78
import io.koraframework.annotation.processor.common.ProcessingErrorException;
@@ -25,28 +26,23 @@ public CachePutAopKoraAspect(ProcessingEnvironment env) {
2526
this.env = env;
2627
}
2728

28-
@Override
29-
public Set<String> getSupportedAnnotationTypes() {
30-
return Set.of(ANNOTATION_CACHE_PUT.canonicalName(), ANNOTATION_CACHE_PUTS.canonicalName());
31-
}
32-
3329
@Override
3430
public Set<ClassName> getSupportedAnnotationClassNames() {
3531
return Set.of(ANNOTATION_CACHE_PUT, ANNOTATION_CACHE_PUTS);
3632
}
3733

3834
@Override
3935
public ApplyResult apply(ExecutableElement method, String superCall, AspectContext aspectContext) {
40-
if (MethodUtils.isPublisher(method) || MethodUtils.isFuture(method)) {
41-
throw new ProcessingErrorException("@CachePut can't be applied for async methods", method);
42-
}
43-
if (MethodUtils.isVoid(method)) {
44-
throw new ProcessingErrorException("@CachePut can't be applied for type Void", method);
36+
if (MethodUtils.isPublisher(method)) {
37+
throw new ProcessingErrorException("@%s can't be applied for type ".formatted(ANNOTATION_CACHE_PUT.simpleName()) + CommonClassNames.publisher, method);
38+
} else if (MethodUtils.isFuture(method)) {
39+
throw new ProcessingErrorException("@%s can't be applied for type ".formatted(ANNOTATION_CACHE_PUT) + method.getReturnType().toString(), method);
40+
} else if (MethodUtils.isVoid(method)) {
41+
throw new ProcessingErrorException("@%s can't be applied for type Void".formatted(ANNOTATION_CACHE_PUT), method);
4542
}
4643

4744
final CacheOperation operation = CacheOperationUtils.getCacheOperation(method, env, aspectContext);
4845
final CodeBlock body = buildBodySync(method, operation, superCall);
49-
5046
return new ApplyResult.MethodBody(body);
5147
}
5248

cache/cache-annotation-processor/src/main/java/io/koraframework/cache/annotation/processor/aop/CacheableAopKoraAspect.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.palantir.javapoet.ClassName;
44
import com.palantir.javapoet.CodeBlock;
5+
import io.koraframework.annotation.processor.common.CommonClassNames;
56
import io.koraframework.annotation.processor.common.CommonUtils;
67
import io.koraframework.annotation.processor.common.MethodUtils;
78
import io.koraframework.annotation.processor.common.ProcessingErrorException;
@@ -26,28 +27,23 @@ public CacheableAopKoraAspect(ProcessingEnvironment env) {
2627
this.env = env;
2728
}
2829

29-
@Override
30-
public Set<String> getSupportedAnnotationTypes() {
31-
return Set.of(ANNOTATION_CACHEABLE.canonicalName(), ANNOTATION_CACHEABLES.canonicalName());
32-
}
33-
3430
@Override
3531
public Set<ClassName> getSupportedAnnotationClassNames() {
3632
return Set.of(ANNOTATION_CACHEABLE, ANNOTATION_CACHEABLES);
3733
}
3834

3935
@Override
4036
public ApplyResult apply(ExecutableElement method, String superCall, AspectContext aspectContext) {
41-
if (MethodUtils.isPublisher(method) || MethodUtils.isFuture(method)) {
42-
throw new ProcessingErrorException("@Cacheable can't be applied for async methods", method);
43-
}
44-
if (MethodUtils.isVoid(method)) {
45-
throw new ProcessingErrorException("@Cacheable can't be applied for type Void", method);
37+
if (MethodUtils.isPublisher(method)) {
38+
throw new ProcessingErrorException("@%s can't be applied for type ".formatted(ANNOTATION_CACHEABLE.simpleName()) + CommonClassNames.publisher, method);
39+
} else if (MethodUtils.isFuture(method)) {
40+
throw new ProcessingErrorException("@%s can't be applied for type ".formatted(ANNOTATION_CACHEABLE) + method.getReturnType().toString(), method);
41+
} else if (MethodUtils.isVoid(method)) {
42+
throw new ProcessingErrorException("@%s can't be applied for type Void".formatted(ANNOTATION_CACHEABLE), method);
4643
}
4744

4845
final CacheOperation operation = CacheOperationUtils.getCacheOperation(method, env, aspectContext);
4946
final CodeBlock body = buildBodySync(method, operation, superCall);
50-
5147
return new ApplyResult.MethodBody(body);
5248
}
5349

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
io.koraframework.cache.annotation.processor.aop.CacheableAopKoraAspectFactory
2-
io.koraframework.cache.annotation.processor.aop.CacheInvalidateAopKoraAspectFactory
32
io.koraframework.cache.annotation.processor.aop.CachePutAopKoraAspectFactory
3+
io.koraframework.cache.annotation.processor.aop.CacheInvalidateAopKoraAspectFactory
4+
io.koraframework.cache.annotation.processor.aop.CacheInvalidateAllAopKoraAspectFactory

0 commit comments

Comments
 (0)