feat: 完成二阶段的优化处理
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped

This commit is contained in:
2026-07-14 15:49:40 +08:00
parent 875a5edbd1
commit cab3fea666
17 changed files with 623 additions and 118 deletions

View File

@@ -513,15 +513,15 @@ jobs:
- 流水线 push 后能收到企微通知 - 流水线 push 后能收到企微通知
- `mode=block` 时任意 P0/P1/P2 变更导致 exit 1 - `mode=block` 时任意 P0/P1/P2 变更导致 exit 1
### Phase 2 — 增强(约 1 周) ### Phase 2 — 增强(约 1 周)
| 任务 | 说明 | | 任务 | 说明 | 状态 |
|------|------| |------|------|------|
| W04/W05 模式 | RedisTemplate 直写对象、Hash 写入 | | W04/W05 模式 | RedisTemplate 直写对象、Hash 写入 | 已完成 |
| 注解完整支持 | Fastjson/Jackson 注解 | | 注解完整支持 | Fastjson/Jackson 注解(含 `@JsonIgnoreProperties` | 已完成 |
| Key 推断增强 | `String.format`、常量追溯 | | Key 推断增强 | `String.format`、常量拼接追溯、`buildXxxKey` | 已完成 |
| 忽略规则完善 | 锁/计数器/token 自动过滤 | | 忽略规则完善 | 锁/计数器/token/字面量/setIfAbsent 自动过滤 | 已完成 |
| 多模块性能优化 | 并行解析、缓存索引 | | 多模块性能优化 | 并行读文件、索引批量装载、manual_mappings 覆盖 | 已完成 |
### Phase 3 — 运营(约 0.5 周) ### Phase 3 — 运营(约 0.5 周)

View File

@@ -94,8 +94,8 @@ detection:
- W01 # redisUtil.insert + JSON.toJSONString - W01 # redisUtil.insert + JSON.toJSONString
- W02 # redisTemplate.opsForValue().set + JSON.toJSONString - W02 # redisTemplate.opsForValue().set + JSON.toJSONString
- W03 # stringRedisTemplate + JsonUtil.getObjectToString - W03 # stringRedisTemplate + JsonUtil.getObjectToString
# - W04 # redisTemplate 直写对象Phase 2 - W04 # redisTemplate 直写对象
# - W05 # opsForHash().putPhase 2 - W05 # opsForHash().put
# 类型推断最低置信度,低于此值仅输出 P2 提示 # 类型推断最低置信度,低于此值仅输出 P2 提示
min_confidence: 0.6 min_confidence: 0.6
@@ -195,8 +195,8 @@ mode: block
| W01 | `redisUtil.insert(key, JSON.toJSONString(x), ttl)` | Phase 1 | | W01 | `redisUtil.insert(key, JSON.toJSONString(x), ttl)` | Phase 1 |
| W02 | `redisTemplate.opsForValue().set(key, JSON.toJSONString(x), ...)` | Phase 1 | | W02 | `redisTemplate.opsForValue().set(key, JSON.toJSONString(x), ...)` | Phase 1 |
| W03 | `stringRedisTemplate.opsForValue().set(key, JsonUtil.getObjectToString(x), ...)` | Phase 1 | | W03 | `stringRedisTemplate.opsForValue().set(key, JsonUtil.getObjectToString(x), ...)` | Phase 1 |
| W04 | `redisTemplate.opsForValue().set(key, obj, ...)` | Phase 2 | | W04 | `redisTemplate.opsForValue().set(key, obj, ...)` | Phase 2(已启用) |
| W05 | `redisTemplate.opsForHash().put(key, field, obj)` | Phase 2 | | W05 | `redisTemplate.opsForHash().put(key, field, obj)` | Phase 2(已启用) |
### 3.6 manual_mappings ### 3.6 manual_mappings

View File

@@ -31,18 +31,20 @@ public class FileScanner {
public Map<String, String> scan() { public Map<String, String> scan() {
Map<String, String> result = new LinkedHashMap<>(); Map<String, String> result = new LinkedHashMap<>();
try (Stream<Path> stream = Files.walk(repoRoot)) { try (Stream<Path> stream = Files.walk(repoRoot)) {
stream.filter(Files::isRegularFile) List<Path> javaFiles = stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java")) .filter(p -> p.toString().endsWith(".java"))
.forEach(p -> { .filter(p -> {
String rel = repoRoot.relativize(p).toString().replace('\\', '/');
return rel.contains("/src/main/java/") && moduleAllowed(rel);
})
.collect(java.util.stream.Collectors.toList());
javaFiles.parallelStream().forEach(p -> {
String rel = repoRoot.relativize(p).toString().replace('\\', '/'); String rel = repoRoot.relativize(p).toString().replace('\\', '/');
if (!rel.contains("/src/main/java/")) {
return;
}
if (!moduleAllowed(rel)) {
return;
}
try { try {
synchronized (result) {
result.put(rel, new String(Files.readAllBytes(p), StandardCharsets.UTF_8)); result.put(rel, new String(Files.readAllBytes(p), StandardCharsets.UTF_8));
}
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }

View File

@@ -111,6 +111,8 @@ public class SchemaCheckAnalyzer {
List<WritePoint> newWps = detectorNew.detect(path, newContent); List<WritePoint> newWps = detectorNew.detect(path, newContent);
List<WritePoint> oldWps = oldContent == null List<WritePoint> oldWps = oldContent == null
? new ArrayList<>() : detectorOld.detect(path, oldContent); ? new ArrayList<>() : detectorOld.detect(path, oldContent);
newWps.forEach(this::applyManualMappings);
oldWps.forEach(this::applyManualMappings);
Map<String, WritePoint> oldBySig = new LinkedHashMap<>(); Map<String, WritePoint> oldBySig = new LinkedHashMap<>();
for (WritePoint wp : oldWps) { for (WritePoint wp : oldWps) {
@@ -399,12 +401,30 @@ public class SchemaCheckAnalyzer {
private SourceIndex buildIndex(Iterable<String> contents) { private SourceIndex buildIndex(Iterable<String> contents) {
SourceIndex index = new SourceIndex(); SourceIndex index = new SourceIndex();
List<String> list = new ArrayList<>();
for (String content : contents) { for (String content : contents) {
index.addSource(content); list.add(content);
} }
index.addSources(list);
return index; return index;
} }
private void applyManualMappings(WritePoint wp) {
String location = wp.getEnclosingClass() + "#" + wp.getEnclosingMethod();
for (CheckerConfig.ManualMapping mapping : config.getManualMappings()) {
if (mapping.getWriterMethod() == null || !mapping.getWriterMethod().equals(location)) {
continue;
}
if (mapping.getKeyPattern() != null && !mapping.getKeyPattern().isEmpty()) {
wp.setResolvedKeyPattern(mapping.getKeyPattern());
}
if (mapping.getValueType() != null && !mapping.getValueType().isEmpty()) {
wp.setResolvedValueType(mapping.getValueType());
wp.setConfidence(Math.max(wp.getConfidence(), 1.0));
}
}
}
private void collectTypeNames(String content, Set<String> out) { private void collectTypeNames(String content, Set<String> out) {
if (content == null || content.isEmpty()) { if (content == null || content.isEmpty()) {
return; return;

View File

@@ -11,10 +11,17 @@ import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.BinaryExpr;
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
import com.github.javaparser.ast.expr.CastExpr;
import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.IntegerLiteralExpr;
import com.github.javaparser.ast.expr.LongLiteralExpr;
import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.NullLiteralExpr;
import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.Type; import com.github.javaparser.ast.type.Type;
@@ -26,13 +33,19 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
/** /**
* 从单个 Java 源文件中检测 Redis value 写入点W01~W03JSON 字符串写入)。 * 从单个 Java 源文件中检测 Redis value 写入点W01~W05)。
*/ */
public class RedisWritePointDetector { public class RedisWritePointDetector {
private static final Set<String> SERIALIZE_METHODS = new HashSet<>(Arrays.asList( private static final Set<String> SERIALIZE_METHODS = new HashSet<>(Arrays.asList(
"toJSONString", "toJsonString", "getObjectToString", "toJsonStr", "writeValueAsString")); "toJSONString", "toJsonString", "getObjectToString", "toJsonStr", "writeValueAsString"));
private static final Set<String> WRITE_METHODS = new HashSet<>(Arrays.asList("set", "insert")); private static final Set<String> VALUE_WRITE_METHODS = new HashSet<>(Arrays.asList("set", "insert"));
private static final Set<String> SKIP_METHODS = new HashSet<>(Arrays.asList(
"setIfAbsent", "setIfPresent", "increment", "decrement", "delete", "remove",
"expire", "get", "hasKey", "exists", "setnx", "getAndSet", "getAndDelete",
"multiGet", "multiSet", "keys", "scan"));
private static final Set<String> TRIVIAL_VALUE_CALLS = new HashSet<>(Arrays.asList(
"randomUUID", "toString", "valueOf"));
private static final Set<String> COLLECTION_SIMPLE = new HashSet<>(Arrays.asList( private static final Set<String> COLLECTION_SIMPLE = new HashSet<>(Arrays.asList(
"List", "ArrayList", "LinkedList", "Set", "HashSet", "Collection")); "List", "ArrayList", "LinkedList", "Set", "HashSet", "Collection"));
@@ -59,41 +72,79 @@ public class RedisWritePointDetector {
} }
for (MethodCallExpr mce : cu.findAll(MethodCallExpr.class)) { for (MethodCallExpr mce : cu.findAll(MethodCallExpr.class)) {
String method = mce.getNameAsString(); WritePoint wp = tryDetectValueWrite(mce, filePath);
if (!WRITE_METHODS.contains(method)) { if (wp == null) {
continue; wp = tryDetectHashWrite(mce, filePath);
} }
String scope = mce.getScope().map(Expression::toString).orElse(""); if (wp != null) {
if (!isRedisScope(scope)) { result.add(wp);
continue;
} }
if (mce.getArguments().size() < 2) {
continue;
} }
Expression valueArg = mce.getArgument(1); return result;
Expression serialized = unwrapSerializer(valueArg);
if (serialized == null) {
continue; // 非 JSON 字符串写入W04/W05 归后续阶段)
}
String pattern = classify(method, valueArg);
if (!enabledPatterns.contains(pattern)) {
continue;
} }
private WritePoint tryDetectValueWrite(MethodCallExpr mce, String filePath) {
String method = mce.getNameAsString();
if (!VALUE_WRITE_METHODS.contains(method) || SKIP_METHODS.contains(method)) {
return null;
}
if (!isValueOpsScope(mce)) {
return null;
}
if (mce.getArguments().size() < 2) {
return null;
}
Expression valueArg = mce.getArgument(1);
Expression serialized = unwrapSerializer(valueArg);
String pattern;
Expression typeExpr;
if (serialized != null) {
pattern = classifySerialized(method, valueArg);
typeExpr = serialized;
} else if (enabledPatterns.contains("W04") && !isTrivialValue(valueArg)) {
pattern = "W04";
typeExpr = valueArg;
} else {
return null;
}
if (!enabledPatterns.contains(pattern)) {
return null;
}
return buildWritePoint(mce, filePath, pattern, mce.getArgument(0), valueArg, typeExpr);
}
private WritePoint tryDetectHashWrite(MethodCallExpr mce, String filePath) {
if (!"put".equals(mce.getNameAsString()) || !enabledPatterns.contains("W05")) {
return null;
}
if (!isHashOpsScope(mce) || mce.getArguments().size() < 3) {
return null;
}
Expression valueArg = mce.getArgument(2);
if (isTrivialValue(valueArg)) {
return null;
}
Expression serialized = unwrapSerializer(valueArg);
Expression typeExpr = serialized != null ? serialized : valueArg;
return buildWritePoint(mce, filePath, "W05", mce.getArgument(0), valueArg, typeExpr);
}
private WritePoint buildWritePoint(MethodCallExpr mce, String filePath, String pattern,
Expression keyArg, Expression valueArg, Expression typeExpr) {
WritePoint wp = new WritePoint(); WritePoint wp = new WritePoint();
wp.setFilePath(filePath); wp.setFilePath(filePath);
wp.setLineNumber(mce.getBegin().map(p -> p.line).orElse(0)); wp.setLineNumber(mce.getBegin().map(p -> p.line).orElse(0));
wp.setPattern(pattern); wp.setPattern(pattern);
wp.setKeyExpression(mce.getArgument(0).toString()); wp.setKeyExpression(keyArg.toString());
wp.setValueExpression(valueArg.toString()); wp.setValueExpression(valueArg.toString());
fillEnclosing(mce, wp); fillEnclosing(mce, wp);
SourceIndex.IndexedType context = index.get(wp.getEnclosingClass()); SourceIndex.IndexedType context = index.get(wp.getEnclosingClass());
ClassOrInterfaceDeclaration enclosingDecl = mce ClassOrInterfaceDeclaration enclosingDecl = mce
.findAncestor(ClassOrInterfaceDeclaration.class).orElse(null); .findAncestor(ClassOrInterfaceDeclaration.class).orElse(null);
wp.setResolvedKeyPattern(keyResolver.resolve(mce.getArgument(0), enclosingDecl, context)); wp.setResolvedKeyPattern(keyResolver.resolve(keyArg, enclosingDecl, context));
InferredType inferred = inferType(serialized, mce, context); InferredType inferred = inferType(typeExpr, mce, context);
if (inferred != null) { if (inferred != null) {
wp.setResolvedValueType(inferred.fqn); wp.setResolvedValueType(inferred.fqn);
wp.setRootArray(inferred.isArray); wp.setRootArray(inferred.isArray);
@@ -101,16 +152,53 @@ public class RedisWritePointDetector {
} else { } else {
wp.setConfidence(0.4); wp.setConfidence(0.4);
} }
result.add(wp); return wp;
}
return result;
} }
private boolean isRedisScope(String scope) { private boolean isValueOpsScope(MethodCallExpr mce) {
String scope = mce.getScope().map(Expression::toString).orElse("");
String lower = scope.toLowerCase(); String lower = scope.toLowerCase();
return lower.contains("redis") || lower.contains("opsforvalue") || lower.contains("boundvalueops"); return lower.contains("redis") || lower.contains("opsforvalue") || lower.contains("boundvalueops");
} }
private boolean isHashOpsScope(MethodCallExpr mce) {
String scope = mce.getScope().map(Expression::toString).orElse("");
String lower = scope.toLowerCase();
return lower.contains("opsforhash") || lower.contains("boundhashops");
}
private boolean isTrivialValue(Expression expr) {
if (expr instanceof StringLiteralExpr
|| expr instanceof IntegerLiteralExpr
|| expr instanceof LongLiteralExpr
|| expr instanceof BooleanLiteralExpr
|| expr instanceof NullLiteralExpr) {
return true;
}
if (expr instanceof BinaryExpr) {
BinaryExpr be = (BinaryExpr) expr;
if (be.getOperator() == BinaryExpr.Operator.PLUS) {
return isTrivialValue(be.getLeft()) && isTrivialValue(be.getRight());
}
}
if (expr instanceof MethodCallExpr) {
MethodCallExpr call = (MethodCallExpr) expr;
String name = call.getNameAsString();
if (TRIVIAL_VALUE_CALLS.contains(name)) {
return true;
}
if ("valueOf".equals(name) && !call.getArguments().isEmpty()) {
return isTrivialValue(call.getArgument(0));
}
}
if (expr instanceof ObjectCreationExpr) {
ObjectCreationExpr oce = (ObjectCreationExpr) expr;
String typeName = oce.getType().getNameAsString();
return "UUID".equals(typeName);
}
return false;
}
private Expression unwrapSerializer(Expression valueArg) { private Expression unwrapSerializer(Expression valueArg) {
if (valueArg instanceof MethodCallExpr) { if (valueArg instanceof MethodCallExpr) {
MethodCallExpr call = (MethodCallExpr) valueArg; MethodCallExpr call = (MethodCallExpr) valueArg;
@@ -121,7 +209,7 @@ public class RedisWritePointDetector {
return null; return null;
} }
private String classify(String method, Expression valueArg) { private String classifySerialized(String method, Expression valueArg) {
String serializer = valueArg instanceof MethodCallExpr String serializer = valueArg instanceof MethodCallExpr
? ((MethodCallExpr) valueArg).getNameAsString() : ""; ? ((MethodCallExpr) valueArg).getNameAsString() : "";
if ("insert".equals(method)) { if ("insert".equals(method)) {
@@ -145,6 +233,9 @@ public class RedisWritePointDetector {
} }
private InferredType inferType(Expression expr, MethodCallExpr contextCall, SourceIndex.IndexedType context) { private InferredType inferType(Expression expr, MethodCallExpr contextCall, SourceIndex.IndexedType context) {
if (expr instanceof CastExpr) {
return resolveTypeNode(((CastExpr) expr).getType(), context);
}
if (expr instanceof ObjectCreationExpr) { if (expr instanceof ObjectCreationExpr) {
ClassOrInterfaceType t = ((ObjectCreationExpr) expr).getType(); ClassOrInterfaceType t = ((ObjectCreationExpr) expr).getType();
return resolveTypeNode(t, context); return resolveTypeNode(t, context);
@@ -157,6 +248,18 @@ public class RedisWritePointDetector {
} }
return null; return null;
} }
if (expr instanceof MethodCallExpr) {
MethodCallExpr call = (MethodCallExpr) expr;
Optional<ClassOrInterfaceDeclaration> clazz = contextCall
.findAncestor(ClassOrInterfaceDeclaration.class);
if (clazz.isPresent()) {
for (MethodDeclaration md : clazz.get().getMethods()) {
if (md.getNameAsString().equals(call.getNameAsString())) {
return resolveTypeNode(md.getType(), context);
}
}
}
}
return null; return null;
} }
@@ -164,20 +267,17 @@ public class RedisWritePointDetector {
Optional<CallableDeclaration> callable = contextCall.findAncestor(CallableDeclaration.class); Optional<CallableDeclaration> callable = contextCall.findAncestor(CallableDeclaration.class);
if (callable.isPresent()) { if (callable.isPresent()) {
CallableDeclaration<?> decl = callable.get(); CallableDeclaration<?> decl = callable.get();
// 局部变量
for (VariableDeclarator var : decl.findAll(VariableDeclarator.class)) { for (VariableDeclarator var : decl.findAll(VariableDeclarator.class)) {
if (var.getNameAsString().equals(name)) { if (var.getNameAsString().equals(name)) {
return var.getType(); return var.getType();
} }
} }
// 方法参数
for (Parameter p : decl.getParameters()) { for (Parameter p : decl.getParameters()) {
if (p.getNameAsString().equals(name)) { if (p.getNameAsString().equals(name)) {
return p.getType(); return p.getType();
} }
} }
} }
// 类字段
Optional<ClassOrInterfaceDeclaration> clazz = contextCall.findAncestor(ClassOrInterfaceDeclaration.class); Optional<ClassOrInterfaceDeclaration> clazz = contextCall.findAncestor(ClassOrInterfaceDeclaration.class);
if (clazz.isPresent()) { if (clazz.isPresent()) {
for (FieldDeclaration field : clazz.get().getFields()) { for (FieldDeclaration field : clazz.get().getFields()) {

View File

@@ -20,7 +20,7 @@ import java.util.Optional;
*/ */
public class RedisKeyResolver { public class RedisKeyResolver {
private static final int MAX_DEPTH = 6; private static final int MAX_DEPTH = 8;
private final SourceIndex index; private final SourceIndex index;
@@ -52,7 +52,7 @@ public class RedisKeyResolver {
} }
if (expr instanceof NameExpr) { if (expr instanceof NameExpr) {
String name = ((NameExpr) expr).getNameAsString(); String name = ((NameExpr) expr).getNameAsString();
String constVal = lookupConstant(enclosingClass, name); String constVal = lookupConstant(enclosingClass, name, context, depth);
if (constVal != null) { if (constVal != null) {
return constVal; return constVal;
} }
@@ -62,12 +62,23 @@ public class RedisKeyResolver {
if (expr instanceof FieldAccessExpr) { if (expr instanceof FieldAccessExpr) {
FieldAccessExpr fae = (FieldAccessExpr) expr; FieldAccessExpr fae = (FieldAccessExpr) expr;
String fieldName = fae.getNameAsString(); String fieldName = fae.getNameAsString();
String scope = fae.getScope().toString(); if (fae.getScope() instanceof NameExpr) {
String external = lookupExternalConstant(scope, fieldName, context); String scope = ((NameExpr) fae.getScope()).getNameAsString();
String local = lookupConstant(enclosingClass, fieldName, context, depth);
if (local != null && scope.equals(enclosingClass == null ? "" : enclosingClass.getNameAsString())) {
return local;
}
String external = lookupExternalConstant(scope, fieldName, context, depth);
if (external != null) { if (external != null) {
return external; return external;
} }
String local = lookupConstant(enclosingClass, fieldName); }
String scope = fae.getScope().toString();
String external = lookupExternalConstant(scope, fieldName, context, depth);
if (external != null) {
return external;
}
String local = lookupConstant(enclosingClass, fieldName, context, depth);
return local != null ? local : "*"; return local != null ? local : "*";
} }
if (expr instanceof MethodCallExpr) { if (expr instanceof MethodCallExpr) {
@@ -77,23 +88,26 @@ public class RedisKeyResolver {
String fmt = resolveExpr(call.getArgument(0), enclosingClass, context, depth + 1); String fmt = resolveExpr(call.getArgument(0), enclosingClass, context, depth + 1);
return fmt.replaceAll("%[-0-9.]*[sdxDX]", "*"); return fmt.replaceAll("%[-0-9.]*[sdxDX]", "*");
} }
// buildCacheKey(...) 等本类方法:解析其 return 表达式
String methodVal = lookupMethodReturn(enclosingClass, name, context, depth); String methodVal = lookupMethodReturn(enclosingClass, name, context, depth);
return methodVal != null ? methodVal : "*"; return methodVal != null ? methodVal : "*";
} }
return "*"; return "*";
} }
private String lookupConstant(ClassOrInterfaceDeclaration clazz, String name) { private String lookupConstant(ClassOrInterfaceDeclaration clazz, String name,
SourceIndex.IndexedType context, int depth) {
if (clazz == null) { if (clazz == null) {
return null; return null;
} }
for (FieldDeclaration field : clazz.getFields()) { for (FieldDeclaration field : clazz.getFields()) {
if (!field.isStatic()) {
continue;
}
for (VariableDeclarator var : field.getVariables()) { for (VariableDeclarator var : field.getVariables()) {
if (var.getNameAsString().equals(name)) { if (var.getNameAsString().equals(name)) {
Optional<Expression> init = var.getInitializer(); Optional<Expression> init = var.getInitializer();
if (init.isPresent() && init.get() instanceof StringLiteralExpr) { if (init.isPresent()) {
return ((StringLiteralExpr) init.get()).asString(); return resolveExpr(init.get(), clazz, context, depth + 1);
} }
} }
} }
@@ -101,7 +115,8 @@ public class RedisKeyResolver {
return null; return null;
} }
private String lookupExternalConstant(String scopeName, String fieldName, SourceIndex.IndexedType context) { private String lookupExternalConstant(String scopeName, String fieldName,
SourceIndex.IndexedType context, int depth) {
String fqn = index.resolveFqn(scopeName, context); String fqn = index.resolveFqn(scopeName, context);
if (fqn == null) { if (fqn == null) {
return null; return null;
@@ -110,7 +125,7 @@ public class RedisKeyResolver {
if (type == null) { if (type == null) {
return null; return null;
} }
return lookupConstant(type.getDeclaration(), fieldName); return lookupConstant(type.getDeclaration(), fieldName, type, depth);
} }
private String lookupMethodReturn(ClassOrInterfaceDeclaration clazz, String methodName, private String lookupMethodReturn(ClassOrInterfaceDeclaration clazz, String methodName,

View File

@@ -15,7 +15,7 @@ import java.util.Set;
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。 * 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
* <ul> * <ul>
* <li>企微:按 key 展示位置/类型/序列化骨架变更,不含字段明细,不分 P0/P1/P2</li> * <li>企微:按 key 展示位置/类型/序列化骨架变更,不含字段明细,不分 P0/P1/P2</li>
* <li>未解析 key 展示源码表达式 + 灰色「key 解析」提示</li> * <li>未解析 key 展示源码表达式 + 灰色「key 无法解析」提示</li>
* <li>多 key 优先拼成一条;超过企微上限则按 key 拆成多条</li> * <li>多 key 优先拼成一条;超过企微上限则按 key 拆成多条</li>
* <li>CI先打字段明细再完整输出企微 Markdown拆分后的每条</li> * <li>CI先打字段明细再完整输出企微 Markdown拆分后的每条</li>
* </ul> * </ul>

View File

@@ -1,12 +1,19 @@
package com.codechecker.cache.schema; package com.codechecker.cache.schema;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration; import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MemberValuePair; import com.github.javaparser.ast.expr.MemberValuePair;
import com.github.javaparser.ast.expr.NormalAnnotationExpr; import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr; import com.github.javaparser.ast.expr.StringLiteralExpr;
import java.util.HashSet;
import java.util.Set;
/** /**
* 处理 Fastjson / Jackson 序列化相关注解:字段忽略与字段名映射。 * 处理 Fastjson / Jackson 序列化相关注解:字段忽略与字段名映射。
*/ */
@@ -20,19 +27,14 @@ public final class AnnotationSupport {
*/ */
public static boolean isSerialized(FieldDeclaration field) { public static boolean isSerialized(FieldDeclaration field) {
for (AnnotationExpr annotation : field.getAnnotations()) { for (AnnotationExpr annotation : field.getAnnotations()) {
String name = annotation.getNameAsString(); String name = simpleName(annotation);
if (name.equals("JsonIgnore")) { if (isIgnoreAnnotation(name)) {
return false; return false;
} }
if (name.equals("JSONField") && annotation instanceof NormalAnnotationExpr) { if (name.equals("JSONField") && isJsonFieldSerializeDisabled(annotation)) {
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
if (pair.getNameAsString().equals("serialize")
&& pair.getValue().toString().equals("false")) {
return false; return false;
} }
} }
}
}
return true; return true;
} }
@@ -41,38 +43,93 @@ public final class AnnotationSupport {
*/ */
public static String jsonName(FieldDeclaration field, String defaultName) { public static String jsonName(FieldDeclaration field, String defaultName) {
for (AnnotationExpr annotation : field.getAnnotations()) { for (AnnotationExpr annotation : field.getAnnotations()) {
String name = annotation.getNameAsString(); String name = simpleName(annotation);
if (name.equals("JsonProperty")) { if (name.equals("JsonProperty")) {
String v = singleStringValue(annotation); String v = stringMember(annotation, "value");
if (v != null && !v.isEmpty()) { if (v != null && !v.isEmpty()) {
return v; return v;
} }
} }
if (name.equals("JSONField") && annotation instanceof NormalAnnotationExpr) { if (name.equals("JSONField")) {
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) { String v = stringMember(annotation, "name");
if (pair.getNameAsString().equals("name") if (v != null && !v.isEmpty()) {
&& pair.getValue() instanceof StringLiteralExpr) {
String v = ((StringLiteralExpr) pair.getValue()).asString();
if (!v.isEmpty()) {
return v; return v;
} }
} }
} }
}
}
return defaultName; return defaultName;
} }
private static String singleStringValue(AnnotationExpr annotation) { /**
* 类级别 @JsonIgnoreProperties 声明的忽略字段名。
*/
public static Set<String> ignoredProperties(ClassOrInterfaceDeclaration type) {
Set<String> ignored = new HashSet<>();
for (AnnotationExpr annotation : type.getAnnotations()) {
if (!"JsonIgnoreProperties".equals(simpleName(annotation))) {
continue;
}
collectIgnoredNames(annotation, ignored);
}
return ignored;
}
private static boolean isIgnoreAnnotation(String name) {
return name.equals("JsonIgnore")
|| name.equals("Transient")
|| name.equals("JsonIgnoreType");
}
private static boolean isJsonFieldSerializeDisabled(AnnotationExpr annotation) {
if (annotation instanceof NormalAnnotationExpr) {
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
if (pair.getNameAsString().equals("serialize")
&& isFalseLiteral(pair.getValue())) {
return true;
}
}
}
return false;
}
private static void collectIgnoredNames(AnnotationExpr annotation, Set<String> out) {
if (annotation instanceof SingleMemberAnnotationExpr) { if (annotation instanceof SingleMemberAnnotationExpr) {
if (((SingleMemberAnnotationExpr) annotation).getMemberValue() instanceof StringLiteralExpr) { addStringArray(((SingleMemberAnnotationExpr) annotation).getMemberValue(), out);
return ((StringLiteralExpr) ((SingleMemberAnnotationExpr) annotation) return;
.getMemberValue()).asString(); }
if (annotation instanceof NormalAnnotationExpr) {
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
if ("value".equals(pair.getNameAsString())) {
addStringArray(pair.getValue(), out);
}
}
}
}
private static void addStringArray(Expression expr, Set<String> out) {
if (expr instanceof StringLiteralExpr) {
out.add(((StringLiteralExpr) expr).asString());
return;
}
if (expr instanceof ArrayInitializerExpr) {
for (Expression value : ((ArrayInitializerExpr) expr).getValues()) {
if (value instanceof StringLiteralExpr) {
out.add(((StringLiteralExpr) value).asString());
}
}
}
}
private static String stringMember(AnnotationExpr annotation, String member) {
if (annotation instanceof SingleMemberAnnotationExpr) {
Expression value = ((SingleMemberAnnotationExpr) annotation).getMemberValue();
if (value instanceof StringLiteralExpr) {
return ((StringLiteralExpr) value).asString();
} }
} }
if (annotation instanceof NormalAnnotationExpr) { if (annotation instanceof NormalAnnotationExpr) {
for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) { for (MemberValuePair pair : ((NormalAnnotationExpr) annotation).getPairs()) {
if (pair.getNameAsString().equals("value") if (pair.getNameAsString().equals(member)
&& pair.getValue() instanceof StringLiteralExpr) { && pair.getValue() instanceof StringLiteralExpr) {
return ((StringLiteralExpr) pair.getValue()).asString(); return ((StringLiteralExpr) pair.getValue()).asString();
} }
@@ -80,4 +137,14 @@ public final class AnnotationSupport {
} }
return null; return null;
} }
private static boolean isFalseLiteral(Expression expr) {
return expr instanceof BooleanLiteralExpr && !((BooleanLiteralExpr) expr).getValue();
}
private static String simpleName(AnnotationExpr annotation) {
String name = annotation.getNameAsString();
int dot = name.lastIndexOf('.');
return dot >= 0 ? name.substring(dot + 1) : name;
}
} }

View File

@@ -79,6 +79,7 @@ public class JavaSchemaExtractor {
Set<String> nextAncestors = new LinkedHashSet<>(ancestors); Set<String> nextAncestors = new LinkedHashSet<>(ancestors);
nextAncestors.add(type.getFqn()); nextAncestors.add(type.getFqn());
Set<String> classIgnored = AnnotationSupport.ignoredProperties(type.getDeclaration());
for (FieldDeclaration field : collectFields(type, new HashSet<>())) { for (FieldDeclaration field : collectFields(type, new HashSet<>())) {
if (field.isStatic() || field.isTransient()) { if (field.isStatic() || field.isTransient()) {
continue; continue;
@@ -88,6 +89,9 @@ public class JavaSchemaExtractor {
} }
for (VariableDeclarator var : field.getVariables()) { for (VariableDeclarator var : field.getVariables()) {
String jsonName = AnnotationSupport.jsonName(field, var.getNameAsString()); String jsonName = AnnotationSupport.jsonName(field, var.getNameAsString());
if (classIgnored.contains(jsonName) || classIgnored.contains(var.getNameAsString())) {
continue;
}
String path = prefix.isEmpty() ? jsonName : prefix + "." + jsonName; String path = prefix.isEmpty() ? jsonName : prefix + "." + jsonName;
expandType(var.getType(), path, type, schema, nextAncestors, depth); expandType(var.getType(), path, type, schema, nextAncestors, depth);
} }

View File

@@ -8,9 +8,10 @@ import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.body.TypeDeclaration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* 一次提交快照下的源码类型索引。仅索引本仓库源码(不含依赖 jar供类型解析与字段展开使用。 * 一次提交快照下的源码类型索引。仅索引本仓库源码(不含依赖 jar供类型解析与字段展开使用。
@@ -21,9 +22,9 @@ import java.util.Map;
public class SourceIndex { public class SourceIndex {
/** FQN以 . 分隔,含内部类) -> 类型信息 */ /** FQN以 . 分隔,含内部类) -> 类型信息 */
private final Map<String, IndexedType> byFqn = new LinkedHashMap<>(); private final Map<String, IndexedType> byFqn = new ConcurrentHashMap<>();
/** 简单类名 -> FQN 列表(兜底解析) */ /** 简单类名 -> FQN 列表(兜底解析) */
private final Map<String, List<String>> bySimpleName = new LinkedHashMap<>(); private final Map<String, List<String>> bySimpleName = new ConcurrentHashMap<>();
static { static {
ParserConfiguration config = new ParserConfiguration() ParserConfiguration config = new ParserConfiguration()
@@ -31,6 +32,20 @@ public class SourceIndex {
StaticJavaParser.setConfiguration(config); StaticJavaParser.setConfiguration(config);
} }
/**
* 批量解析源文件并建立索引。
* <p>文件读取可在 {@link com.codechecker.cache.analyze.FileScanner} 中并行;
* AST 解析使用 StaticJavaParser 串行写入,避免其全局配置的线程安全问题。</p>
*/
public void addSources(Collection<String> contents) {
if (contents == null || contents.isEmpty()) {
return;
}
for (String content : contents) {
addSource(content);
}
}
/** /**
* 解析并加入一个 Java 源文件内容。解析失败时静默跳过(返回 false * 解析并加入一个 Java 源文件内容。解析失败时静默跳过(返回 false
*/ */
@@ -65,7 +80,6 @@ public class SourceIndex {
byFqn.put(fqn, indexed); byFqn.put(fqn, indexed);
bySimpleName.computeIfAbsent(simpleName, k -> new ArrayList<>()).add(fqn); bySimpleName.computeIfAbsent(simpleName, k -> new ArrayList<>()).add(fqn);
} }
// 递归内部类型
for (Object member : type.getMembers()) { for (Object member : type.getMembers()) {
if (member instanceof TypeDeclaration) { if (member instanceof TypeDeclaration) {
registerType((TypeDeclaration<?>) member, packageName, imports, fqn); registerType((TypeDeclaration<?>) member, packageName, imports, fqn);

View File

@@ -42,6 +42,8 @@ detection:
- W01 # redisUtil.insert(key, JSON.toJSONString(x), ttl) - W01 # redisUtil.insert(key, JSON.toJSONString(x), ttl)
- W02 # redisTemplate.opsForValue().set(key, JSON.toJSONString(x), ...) - W02 # redisTemplate.opsForValue().set(key, JSON.toJSONString(x), ...)
- W03 # stringRedisTemplate.opsForValue().set(key, JsonUtil.getObjectToString(x), ...) - W03 # stringRedisTemplate.opsForValue().set(key, JsonUtil.getObjectToString(x), ...)
- W04 # redisTemplate.opsForValue().set(key, obj, ...)
- W05 # redisTemplate.opsForHash().put(key, field, obj)
# 类型推断最低置信度,低于此值降级为 P2 提示 # 类型推断最低置信度,低于此值降级为 P2 提示
min_confidence: 0.6 min_confidence: 0.6
# 字段展开最大深度(防止循环引用) # 字段展开最大深度(防止循环引用)

View File

@@ -0,0 +1,101 @@
package com.codechecker.cache.detector;
import com.codechecker.cache.TestSupport;
import com.codechecker.cache.schema.SourceIndex;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RedisWritePointDetectorTest {
@Test
void detectsW04DirectObjectWrite() {
String source = ""
+ "package demo;\n"
+ "import demo.model.DemoVo;\n"
+ "public class DemoService {\n"
+ " private RedisTemplate<String, DemoVo> redisTemplate;\n"
+ " public void save(String key, DemoVo vo) {\n"
+ " redisTemplate.opsForValue().set(key, vo, 60, TimeUnit.SECONDS);\n"
+ " }\n"
+ "}\n";
String vo = ""
+ "package demo.model;\n"
+ "public class DemoVo { private String name; }\n";
SourceIndex index = new SourceIndex();
index.addSource(vo);
index.addSource(source);
Set<String> patterns = new HashSet<>(Arrays.asList("W04"));
List<WritePoint> wps = new RedisWritePointDetector(index, patterns)
.detect("DemoService.java", source);
assertEquals(1, wps.size());
WritePoint wp = wps.get(0);
assertEquals("W04", wp.getPattern());
assertEquals("demo.model.DemoVo", wp.getResolvedValueType());
}
@Test
void detectsW05HashPut() {
String source = ""
+ "package demo;\n"
+ "import demo.model.DemoVo;\n"
+ "public class DemoService {\n"
+ " private RedisTemplate<String, Object> redisTemplate;\n"
+ " public void save(String key, String field, DemoVo vo) {\n"
+ " redisTemplate.opsForHash().put(key, field, vo);\n"
+ " }\n"
+ "}\n";
String vo = ""
+ "package demo.model;\n"
+ "public class DemoVo { private String name; }\n";
SourceIndex index = new SourceIndex();
index.addSource(vo);
index.addSource(source);
Set<String> patterns = new HashSet<>(Arrays.asList("W05"));
List<WritePoint> wps = new RedisWritePointDetector(index, patterns)
.detect("DemoService.java", source);
assertEquals(1, wps.size());
assertEquals("W05", wps.get(0).getPattern());
assertEquals("demo.model.DemoVo", wps.get(0).getResolvedValueType());
}
@Test
void ignoresLockAndLiteralWrites() {
String source = TestSupport.fixture("fixtures/lock/LockService.txt");
Set<String> patterns = new HashSet<>(Arrays.asList("W01", "W02", "W03", "W04", "W05"));
List<WritePoint> wps = new RedisWritePointDetector(new SourceIndex(), patterns)
.detect("LockService.java", source);
assertTrue(wps.isEmpty(), "锁/计数器/token 写入应被忽略");
}
@Test
void stillDetectsW01ToW03() {
String helper = TestSupport.fixture("fixtures/tenant/HelperOld.txt");
String tenantVo = TestSupport.fixture("fixtures/tenant/TenantVO.txt");
String tenantLink = TestSupport.fixture("fixtures/tenant/TenantLinkModel.txt");
SourceIndex index = new SourceIndex();
index.addSource(tenantVo);
index.addSource(tenantLink);
index.addSource(helper);
Set<String> patterns = new HashSet<>(Arrays.asList("W01", "W02", "W03"));
List<WritePoint> wps = new RedisWritePointDetector(index, patterns)
.detect("Helper.java", helper);
assertEquals(1, wps.size());
assertEquals("W01", wps.get(0).getPattern());
}
}

View File

@@ -0,0 +1,68 @@
package com.codechecker.cache.key;
import com.codechecker.cache.schema.SourceIndex;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RedisKeyResolverTest {
@Test
void resolvesStringFormatWithConstant() {
String source = ""
+ "package demo;\n"
+ "public class AttendanceService {\n"
+ " private static final String ATTENDANCE_BASE_SETTING_CACHE_KEY = "
+ "\"fbt:attendance:base_setting:cache:%s\";\n"
+ " public void save(String tenantId) {\n"
+ " String key = String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY, tenantId);\n"
+ " }\n"
+ "}\n";
SourceIndex index = new SourceIndex();
index.addSource(source);
CompilationUnit cu = StaticJavaParser.parse(source);
ClassOrInterfaceDeclaration clazz = cu.getType(0).asClassOrInterfaceDeclaration();
MethodCallExpr formatCall = cu.findAll(MethodCallExpr.class).stream()
.filter(m -> "format".equals(m.getNameAsString()))
.findFirst()
.orElseThrow(IllegalStateException::new);
String pattern = new RedisKeyResolver(index).resolve(
formatCall, clazz, index.get("demo.AttendanceService"));
assertEquals("fbt:attendance:base_setting:cache:*", pattern);
}
@Test
void resolvesConstantConcatAndBuildMethod() {
String source = ""
+ "package jnpf.util;\n"
+ "public class TenantDbContentCacheHelper {\n"
+ " private static final String CACHE_KEY_PREFIX = \"tenant:db:content:\";\n"
+ " public String buildCacheKey(String encode) {\n"
+ " return CACHE_KEY_PREFIX + encode;\n"
+ " }\n"
+ " public void cache(String encode) {\n"
+ " String key = buildCacheKey(encode);\n"
+ " }\n"
+ "}\n";
SourceIndex index = new SourceIndex();
index.addSource(source);
CompilationUnit cu = StaticJavaParser.parse(source);
ClassOrInterfaceDeclaration clazz = cu.getType(0).asClassOrInterfaceDeclaration();
MethodCallExpr buildCall = cu.findAll(MethodCallExpr.class).stream()
.filter(m -> "buildCacheKey".equals(m.getNameAsString()))
.findFirst()
.orElseThrow(IllegalStateException::new);
String pattern = new RedisKeyResolver(index).resolve(
buildCall, clazz, index.get("jnpf.util.TenantDbContentCacheHelper"));
assertEquals("tenant:db:content:*", pattern);
}
}

View File

@@ -59,7 +59,7 @@ class ReportBuilderTest {
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report); String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
assertTrue(md.contains("- Key --> `saas:period-config:migration:current`")); assertTrue(md.contains("- Key --> `saas:period-config:migration:current`"));
assertFalse(md.contains("key 解析)")); assertFalse(md.contains("key 无法解析)"));
assertTrue(md.contains("> **位置**: `SaasPeriodConfigMigrationRedisSupport#putCurrent:41`")); assertTrue(md.contains("> **位置**: `SaasPeriodConfigMigrationRedisSupport#putCurrent:41`"));
assertTrue(md.contains("> **类型**: `MigrationCurrentVo`")); assertTrue(md.contains("> **类型**: `MigrationCurrentVo`"));
@@ -158,10 +158,10 @@ class ReportBuilderTest {
report.getKeyChanges().add(key); report.getKeyChanges().add(key);
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report); String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
assertTrue(md.contains("- Key --> `req.getKey()` <font color=\"comment\">key 解析)</font>")); assertTrue(md.contains("- Key --> `req.getKey()` <font color=\"comment\">key 无法解析)</font>"));
assertTrue(md.contains("> **位置**: `ClockInXxxService#export:128`")); assertTrue(md.contains("> **位置**: `ClockInXxxService#export:128`"));
assertTrue(md.contains("> **类型**: `List<ClockInExportVo>`")); assertTrue(md.contains("> **类型**: `List<ClockInExportVo>`"));
assertFalse(md.contains("unknown-key")); assertFalse(md.contains("`unknown-key`"));
} }
@Test @Test

View File

@@ -0,0 +1,84 @@
package com.codechecker.cache.schema;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JavaSchemaExtractorTest {
@Test
void honorsJsonIgnoreAndPropertyRename() {
String source = ""
+ "package demo;\n"
+ "import com.fasterxml.jackson.annotation.JsonIgnore;\n"
+ "import com.fasterxml.jackson.annotation.JsonProperty;\n"
+ "public class AnnotatedVo {\n"
+ " @JsonProperty(\"display_name\")\n"
+ " private String name;\n"
+ " @JsonIgnore\n"
+ " private String secret;\n"
+ " private String visible;\n"
+ "}\n";
SourceIndex index = new SourceIndex();
index.addSource(source);
TypeSchema schema = new JavaSchemaExtractor(index, 8).extract("demo.AnnotatedVo");
assertTrue(schema.getFields().containsKey("display_name"));
assertTrue(schema.getFields().containsKey("visible"));
assertFalse(schema.getFields().containsKey("secret"));
assertFalse(schema.getFields().containsKey("name"));
}
@Test
void honorsFastjsonFieldAndClassIgnoreProperties() {
String source = ""
+ "package demo;\n"
+ "import com.alibaba.fastjson.annotation.JSONField;\n"
+ "import com.fasterxml.jackson.annotation.JsonIgnoreProperties;\n"
+ "@JsonIgnoreProperties({\"password\"})\n"
+ "public class FastVo {\n"
+ " @JSONField(name = \"user_id\")\n"
+ " private String userId;\n"
+ " @JSONField(serialize = false)\n"
+ " private String token;\n"
+ " private String password;\n"
+ "}\n";
SourceIndex index = new SourceIndex();
index.addSource(source);
TypeSchema schema = new JavaSchemaExtractor(index, 8).extract("demo.FastVo");
assertTrue(schema.getFields().containsKey("user_id"));
assertFalse(schema.getFields().containsKey("token"));
assertFalse(schema.getFields().containsKey("password"));
}
@Test
void jsonNameHelpersWorkOnFieldAnnotations() {
CompilationUnit cu = StaticJavaParser.parse(""
+ "class X {\n"
+ " @com.fasterxml.jackson.annotation.JsonProperty(\"alias\")\n"
+ " private String field;\n"
+ "}");
FieldDeclaration field = cu.getType(0).asClassOrInterfaceDeclaration().getFields().get(0);
assertEquals("alias", AnnotationSupport.jsonName(field, "field"));
assertTrue(AnnotationSupport.isSerialized(field));
}
@Test
void classIgnorePropertiesCollected() {
CompilationUnit cu = StaticJavaParser.parse(""
+ "@com.fasterxml.jackson.annotation.JsonIgnoreProperties({\"a\", \"b\"})\n"
+ "class X {}");
ClassOrInterfaceDeclaration type = cu.getType(0).asClassOrInterfaceDeclaration();
assertTrue(AnnotationSupport.ignoredProperties(type).contains("a"));
assertTrue(AnnotationSupport.ignoredProperties(type).contains("b"));
}
}

View File

@@ -0,0 +1,16 @@
package jnpf.lock;
public class LockService {
private RedisTemplate<String, String> redisTemplate;
private RedisUtil redisUtil;
public void acquire(String bizId) {
redisTemplate.opsForValue().setIfAbsent("order:lock:" + bizId, "1", 30, TimeUnit.SECONDS);
redisTemplate.opsForValue().increment("loginCount:" + bizId);
redisTemplate.delete("temp:" + bizId);
redisUtil.insert("Authorization:" + bizId, "token-abc", 60);
redisTemplate.opsForValue().set("plain:flag", "1", 60, TimeUnit.SECONDS);
redisTemplate.opsForValue().set("uuid:key", UUID.randomUUID().toString(), 60, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,12 @@
package demo;
import demo.model.DemoVo;
public class TemplateService {
private RedisTemplate<String, DemoVo> redisTemplate;
public void cache(String key, DemoVo vo) {
redisTemplate.opsForValue().set(key, vo, 3600, TimeUnit.SECONDS);
}
}