• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Hyshmily / hotkey / 28637103998

03 Jul 2026 03:48AM UTC coverage: 90.399% (-0.2%) from 90.63%
28637103998

push

github

Hyshmily
fix: add packaging to gitignore, fix flaky jitter test, update docs references

Signed-off-by: Hyshmily <cxm8607@outlook.com>

1264 of 1461 branches covered (86.52%)

Branch coverage included in aggregate %.

3604 of 3924 relevant lines covered (91.85%)

4.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.89
/common/src/main/java/io/github/hyshmily/hotkey/annotation/HotKeyCacheExtensionAspect.java
1
/*
2
 * Copyright 2026 Hyshmily. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package io.github.hyshmily.hotkey.annotation;
17

18
import com.github.benmanes.caffeine.cache.Cache;
19
import com.github.benmanes.caffeine.cache.Caffeine;
20
import io.github.hyshmily.hotkey.HotKey;
21
import io.github.hyshmily.hotkey.Internal;
22
import io.github.hyshmily.hotkey.autoconfigure.HotKeyProperties;
23
import io.github.hyshmily.hotkey.cache.annotationsupporter.HotKeyCacheContext;
24
import io.github.hyshmily.hotkey.util.window.RollingWindow;
25
import java.lang.reflect.InvocationTargetException;
26
import java.lang.reflect.Method;
27
import java.util.Map;
28
import java.util.concurrent.ConcurrentHashMap;
29
import java.util.concurrent.TimeUnit;
30
import lombok.extern.slf4j.Slf4j;
31
import org.aspectj.lang.ProceedingJoinPoint;
32
import org.aspectj.lang.annotation.Around;
33
import org.aspectj.lang.annotation.Aspect;
34
import org.aspectj.lang.reflect.MethodSignature;
35
import org.springframework.cache.annotation.CacheEvict;
36
import org.springframework.cache.annotation.CachePut;
37
import org.springframework.cache.annotation.Cacheable;
38
import org.springframework.cache.interceptor.SimpleKey;
39
import org.springframework.context.expression.MethodBasedEvaluationContext;
40
import org.springframework.core.DefaultParameterNameDiscoverer;
41
import org.springframework.core.Ordered;
42
import org.springframework.core.ParameterNameDiscoverer;
43
import org.springframework.core.annotation.Order;
44
import org.springframework.expression.EvaluationContext;
45
import org.springframework.expression.Expression;
46
import org.springframework.expression.spel.standard.SpelExpressionParser;
47

48
/**
49
 * Companion AOP aspect for Spring {@link Cacheable @Cacheable},
50
 * {@link CachePut @CachePut}, and {@link CacheEvict @CacheEvict} methods.
51
 *
52
 * <p>Runs at {@link Ordered#HIGHEST_PRECEDENCE} — before Spring's own
53
 * {@code CacheInterceptor} — to set up thread-bound TTL overrides,
54
 * null-caching mode, broadcast suppression, and apply
55
 * {@link Intercept @Intercept} / {@link Fallback @Fallback} semantics.
56
 *
57
 * <p>For each {@code @Cacheable} invocation:
58
 * <ol>
59
 *   <li>Resolves the cache key from SpEL</li>
60
 *   <li>If {@code @Intercept} is present and the key is a local hot key,
61
 *       skips the method and returns the cached value or fallback</li>
62
 *   <li>Applies TTL from {@code @HotKeyCacheTTL}, null-caching from
63
 *       {@code @NullCaching}, and broadcast suppression from
64
 *       {@link Broadcast @Broadcast} into {@link HotKeyCacheContext}</li>
65
 *   <li>Proceeds to the Spring {@code CacheInterceptor}</li>
66
 *   <li>Restores the context snapshot in a {@code finally} block</li>
67
 *   <li>If the method throws and {@code @Fallback} is present, returns the fallback</li>
68
 * </ol>
69
 *
70
 * <p>For {@code @CachePut} and {@code @CacheEvict}, the aspect only reads
71
 * {@link Broadcast @Broadcast} to set the broadcast suppression flag. All other
72
 * companion annotations ({@code @HotKeyCacheTTL}, {@code @Intercept},
73
 * {@code @Fallback}, {@code @NullCaching}) are ignored on these methods.
74
 */
75
@Internal
76
@Aspect
77
@Order(Ordered.HIGHEST_PRECEDENCE)
78
@Slf4j
4✔
79
public class HotKeyCacheExtensionAspect {
80

81
  /** The HotKey facade for cache introspection. */
82
  private final HotKey hotKey;
83

84
  /** Configuration properties (for key separator). */
85
  private final HotKeyProperties properties;
86

87
  /** SpEL expression parser, shared across all evaluations. */
88
  private final SpelExpressionParser parser = new SpelExpressionParser();
5✔
89

90
  /** Discoverer for resolving method parameter names at runtime. */
91
  private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
5✔
92

93
  /** Cache of parsed SpEL expressions keyed by expression string. */
94
  private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();
5✔
95

96
  /** Cache of resolved naming-convention fallback methods, keyed by original Method. */
97
  private final Map<Method, Method> fallbackMethodCache = new ConcurrentHashMap<>();
5✔
98

99
  /**
100
   * Tracks which keys have already been preload-registered to avoid redundant
101
   * {@link HotKey#notifyLocalDetectorDirect(String, int)} calls.
102
   * Bounded at 100k entries with 1-hour TTL to prevent unbounded growth.
103
   */
104
  private final Cache<String, Boolean> registeredPreloadKeys = Caffeine.newBuilder()
3✔
105
    .maximumSize(100_000)
3✔
106
    .expireAfterWrite(1, TimeUnit.HOURS)
1✔
107
    .build();
2✔
108

109
  /** Cache of resolved {@link HotKeyPreload} annotations keyed by Method. */
110
  private final Map<Method, HotKeyPreload> preloadCache = new ConcurrentHashMap<>();
5✔
111

112
  /**
113
   * Per-key QPS sliding windows for {@link InterceptTrigger#QPS} interception.
114
   * Each key gets a 10-bucket / 1-second window.
115
   * Bounded at 100k entries; idle windows are evicted by Caffeine.
116
   */
117
  private final Cache<String, RollingWindow> qpsWindows = Caffeine.newBuilder().maximumSize(100_000).build();
6✔
118

119
  /**
120
   * Creates a new {@code HotKeyCacheExtensionAspect}.
121
   *
122
   * @param hotKey     the HotKey facade
123
   * @param properties configuration properties
124
   */
125
  public HotKeyCacheExtensionAspect(HotKey hotKey, HotKeyProperties properties) {
2✔
126
    this.hotKey = hotKey;
3✔
127
    this.properties = properties;
3✔
128
  }
1✔
129

130
  /**
131
   * Intercepts {@link Cacheable @Cacheable} methods to apply TTL overrides, intercept
132
   * logic, and fallback handling before the method reaches Spring's {@code CacheInterceptor}.
133
   *
134
   * <p>If {@code @Intercept} is present and the key is a local hot key, the method is
135
   * skipped entirely. If a {@code @Fallback} is provided, it is returned instead.
136
   * If the method throws, the exception is caught and the fallback is returned.
137
   *
138
   * @param pjp       the join point for the intercepted method
139
   * @param cacheable the {@code @Cacheable} annotation on the intercepted method
140
   * @return the cached or method return value
141
   */
142
  @Around("@annotation(cacheable)")
143
  public Object aroundCacheable(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
144
    Method method = resolveMethod(pjp);
4✔
145
    String cacheName = resolveCacheName(cacheable);
4✔
146
    String key = resolveKey(pjp, cacheable.key());
6✔
147
    String prefixedKey = cacheName + properties.getSpringCache().getKeySeparator() + key;
8✔
148

149
    HotKeyPreload preload = resolvePreloadAnnotation(method);
4✔
150
    HotKeyCacheTTL ttl = method.getAnnotation(HotKeyCacheTTL.class);
5✔
151
    Intercept intercept = method.getAnnotation(Intercept.class);
5✔
152
    Fallback fallback = method.getAnnotation(Fallback.class);
5✔
153
    NullCaching nullCaching = method.getAnnotation(NullCaching.class);
5✔
154
    Broadcast broadcast = method.getAnnotation(Broadcast.class);
5✔
155

156
    // @HotKeyPreload: inflate detection counts after @Intercept (so first call loads normally)
157
    // but before context setup (so pjp.proceed() → CacheInterceptor sees inflated counts)
158
    if (preload != null) {
2✔
159
      handlePreload(preload, pjp, cacheName);
5✔
160
    }
161

162
    // @Intercept: skip method when key is a local hot key if necessary
163
    if (intercept != null) {
2✔
164
      String interceptFallback = intercept.fallback();
3✔
165

166
      switch (intercept.trigger()) {
6!
167
        case FORCE -> {
168
          return resolveInterceptFallback(pjp, fallback, interceptFallback, prefixedKey);
7✔
169
        }
170
        case IS_LOCAL_HOT -> {
171
          if (hotKey.isLocalHotKey(prefixedKey)) {
5✔
172
            return resolveInterceptFallback(pjp, fallback, interceptFallback, prefixedKey);
7✔
173
          }
174
        }
175
        case QPS -> {
176
          int qpsThreshold = intercept.QPS();
3✔
177

178
          if (qpsThreshold > 0) {
2!
179
            RollingWindow window = qpsWindows.get(prefixedKey, k -> new RollingWindow(10, 1000));
13✔
180
            window.add(1);
3✔
181
            if (window.sum() > qpsThreshold) {
6✔
182
              return resolveInterceptFallback(pjp, fallback, interceptFallback, prefixedKey);
7✔
183
            }
184
          }
185
        }
186
      }
187
    }
188

189
    // Save-restore context to prevent leakage across nested @Cacheable calls
190
    HotKeyCacheContext.ContextValues prev = HotKeyCacheContext.get().snapshot();
3✔
191
    try {
192
      long hardTtlMs = (ttl != null && ttl.hardTtlMs() > 0) ? ttl.hardTtlMs() : 0L;
12!
193
      long softTtlMs = (ttl != null && ttl.softTtlMs() > 0) ? ttl.softTtlMs() : 0L;
9!
194
      boolean allowNull = nullCaching != null && nullCaching.value();
9!
195
      boolean skipBroadcast = broadcast != null && !broadcast.value();
9!
196

197
      HotKeyCacheContext.get().apply(hardTtlMs, softTtlMs, allowNull, skipBroadcast);
6✔
198
      return pjp.proceed();
5✔
199
    } catch (Throwable e) {
1✔
200
      if (fallback != null) {
2✔
201
        log.warn("[HotKeyCacheExtension] fallback triggered for key={}, reason={}", prefixedKey, e.getMessage());
6✔
202
        return resolveFallback(pjp, fallback);
7✔
203
      }
204
      throw e;
2✔
205
    } finally {
206
      HotKeyCacheContext.get().restore(prev);
3✔
207
    }
208
  }
209

210
  /**
211
   * Handle {@link HotKeyPreload @HotKeyPreload} by inflating HeavyKeeper counts
212
   * for static and/or dynamic cache keys.
213
   *
214
   * <p>Each key is inflated at most once, tracked by the bounded Caffeine cache
215
   * {@link #registeredPreloadKeys} (100k max, 1-hour TTL). This prevents
216
   * unbounded growth from dynamic key expressions while ensuring automatic
217
   * re-inflation if a key needs to be preloaded again after a long idle period.
218
   *
219
   * <p>Static keys ({@link HotKeyPreload#keys}) are inflated immediately.
220
   * Dynamic keys ({@link HotKeyPreload#keyExpr}) are evaluated via SpEL against
221
   * the current method parameters.
222
   *
223
   * @param preload   the annotation instance
224
   * @param pjp       the join point providing method arguments and target
225
   * @param cacheName the resolved cache name from {@code @Cacheable}
226
   */
227
  private void handlePreload(HotKeyPreload preload, ProceedingJoinPoint pjp, String cacheName) {
228
    String separator = properties.getSpringCache().getKeySeparator();
5✔
229
    int preloadCount = preload.count() > 0 ? preload.count() : Integer.MAX_VALUE;
5!
230

231
    // Static keys — register once per key
232
    for (String staticKey : preload.keys()) {
17✔
233
      String fullKey = cacheName + separator + staticKey;
5✔
234
      if (registeredPreloadKeys.getIfPresent(fullKey) == null) {
5✔
235
        hotKey.notifyLocalDetectorDirect(fullKey, preloadCount);
5✔
236
        registeredPreloadKeys.put(fullKey, Boolean.TRUE);
5✔
237
      }
238
    }
239

240
    // Dynamic key via SpEL — register once per unique evaluated key
241
    String keyExpr = preload.keyExpr();
3✔
242
    if (!keyExpr.isEmpty()) {
3✔
243
      try {
244
        Expression expression = expressionCache.computeIfAbsent("preload_" + keyExpr, k ->
10✔
245
          parser.parseExpression(keyExpr)
5✔
246
        );
247
        Object value = expression.getValue(buildEvaluationContext(pjp));
6✔
248

249
        if (value != null) {
2!
250
          String fullKey = cacheName + separator + value;
6✔
251
          if (registeredPreloadKeys.getIfPresent(fullKey) == null) {
5!
252
            hotKey.notifyLocalDetectorDirect(fullKey, preloadCount);
5✔
253
            registeredPreloadKeys.put(fullKey, Boolean.TRUE);
5✔
254
          }
255
        }
256
      } catch (Exception e) {
×
257
        log.warn("Failed to evaluate @HotKeyPreload keyExpr '{}': {}", keyExpr, e.toString());
×
258
      }
1✔
259
    }
260
  }
1✔
261

262
  /**
263
   * Intercepts {@link CachePut @CachePut} methods to set the broadcast suppression
264
   * flag from {@link Broadcast @Broadcast} before the method reaches Spring's
265
   * {@code CacheInterceptor}.
266
   *
267
   * <p>Only the {@code @Broadcast} annotation is read here. Other companion
268
   * annotations ({@code @HotKeyCacheTTL}, {@code @Intercept}, {@code @Fallback},
269
   * {@code @NullCaching}) are ignored on {@code @CachePut} methods.
270
   *
271
   * @param pjp      the join point for the intercepted method
272
   * @param cachePut the {@code @CachePut} annotation on the intercepted method
273
   * @return the method return value
274
   */
275
  @SuppressWarnings("unused")
276
  @Around("@annotation(cachePut)")
277
  public Object aroundCachePut(ProceedingJoinPoint pjp, CachePut cachePut) throws Throwable {
278
    return setBroadcast(pjp);
4✔
279
  }
280

281
  /**
282
   * Intercepts {@link CacheEvict @CacheEvict} methods to set the broadcast
283
   * suppression flag from {@link Broadcast @Broadcast} before the method reaches
284
   * Spring's {@code CacheInterceptor}.
285
   *
286
   * <p>Only the {@code @Broadcast} annotation is read here. Other companion
287
   * annotations ({@code @HotKeyCacheTTL}, {@code @Intercept}, {@code @Fallback},
288
   * {@code @NullCaching}) are ignored on {@code @CacheEvict} methods.
289
   *
290
   * @param pjp       the join point for the intercepted method
291
   * @param cacheEvict the {@code @CacheEvict} annotation on the intercepted method
292
   * @return the method return value
293
   */
294
  @SuppressWarnings("unused")
295
  @Around("@annotation(cacheEvict)")
296
  public Object aroundCacheEvict(ProceedingJoinPoint pjp, CacheEvict cacheEvict) throws Throwable {
297
    return setBroadcast(pjp);
4✔
298
  }
299

300
  private Object setBroadcast(ProceedingJoinPoint pjp) throws Throwable {
301
    Method method = resolveMethod(pjp);
4✔
302
    Broadcast broadcast = method.getAnnotation(Broadcast.class);
5✔
303
    boolean skipBroadcast = broadcast != null && !broadcast.value();
9!
304

305
    HotKeyCacheContext.ContextValues prev = HotKeyCacheContext.get().snapshot();
3✔
306
    try {
307
      HotKeyCacheContext.get().apply(0, 0, false, skipBroadcast);
6✔
308
      return pjp.proceed();
5✔
309
    } finally {
310
      HotKeyCacheContext.get().restore(prev);
3✔
311
    }
312
  }
313

314
  /**
315
   * Resolves the first available cache name from the {@link Cacheable @Cacheable} annotation.
316
   *
317
   * @param cacheable the annotation instance
318
   * @return the cache name, or {@code "hotkey"} if none is specified
319
   */
320
  private String resolveCacheName(Cacheable cacheable) {
321
    String[] names = cacheable.cacheNames();
3✔
322
    if (names.length > 0) {
3✔
323
      return names[0];
4✔
324
    }
325
    String[] value = cacheable.value();
3✔
326
    if (value.length > 0) {
3✔
327
      return value[0];
4✔
328
    }
329
    return "hotkey";
2✔
330
  }
331

332
  /**
333
   * Evaluates the cache key SpEL expression against method parameter variables.
334
   *
335
   * @param pjp        the join point providing method arguments
336
   * @param expression the SpEL expression to evaluate
337
   * @return the evaluated cache key string, or all arguments as string if expression is empty
338
   */
339
  private String resolveKey(ProceedingJoinPoint pjp, String expression) {
340
    if (expression.isEmpty()) {
3✔
341
      Object[] args = pjp.getArgs();
3✔
342
      if (args.length == 0) {
3✔
343
        return "empty";
2✔
344
      }
345
      if (args.length == 1 && args[0] != null && !args[0].getClass().isArray()) {
14✔
346
        return args[0].toString();
5✔
347
      }
348
      return new SimpleKey(args).toString();
6✔
349
    }
350
    return getExpression(expression).getValue(buildEvaluationContext(pjp), String.class);
10✔
351
  }
352

353
  /**
354
   * Resolves the concrete {@link Method} for the join point, unwrapping interface
355
   * methods to the implementation class when necessary.
356
   *
357
   * @param pjp the join point whose method to resolve
358
   * @return the resolved concrete method
359
   */
360
  private Method resolveMethod(ProceedingJoinPoint pjp) {
361
    MethodSignature sig = (MethodSignature) pjp.getSignature();
4✔
362
    Method method = sig.getMethod();
3✔
363
    if (method.getDeclaringClass().isInterface()) {
4✔
364
      try {
365
        method = pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());
9✔
366
      } catch (NoSuchMethodException ignored) {
1✔
367
        // Keep the interface method
368
      }
1✔
369
    }
370
    return method;
2✔
371
  }
372

373
  /**
374
   * Builds an {@link EvaluationContext} with method parameters registered
375
   * as context variables via {@link MethodBasedEvaluationContext}.
376
   *
377
   * @param pjp the join point providing method arguments and target
378
   * @return a configured evaluation context with method parameters as variables
379
   */
380
  private EvaluationContext buildEvaluationContext(ProceedingJoinPoint pjp) {
381
    return new MethodBasedEvaluationContext(
4✔
382
      pjp.getTarget(),
3✔
383
      resolveMethod(pjp),
2✔
384
      pjp.getArgs(),
4✔
385
      parameterNameDiscoverer
386
    );
387
  }
388

389
  /**
390
   * Returns a cached {@link Expression} for the given expression string, parsing it
391
   * on first access.
392
   *
393
   * @param expressionString the SpEL expression string to retrieve or parse
394
   * @return the cached or freshly parsed expression
395
   */
396
  private Expression getExpression(String expressionString) {
397
    return expressionCache.computeIfAbsent(expressionString, parser::parseExpression);
12✔
398
  }
399

400
  /**
401
   * Resolve the intercept fallback chain when the method call is intercepted.
402
   *
403
   * <p>Priority order:
404
   * <ol>
405
   *   <li>{@link Intercept#fallback()} — SpEL expression evaluated against
406
   *       method parameters</li>
407
   *   <li>{@link Fallback @Fallback} — naming-convention method or SpEL expression</li>
408
   *   <li>{@link io.github.hyshmily.hotkey.HotKey#peek(String)} — stale cached
409
   *       value if available, otherwise {@code null}</li>
410
   * </ol>
411
   *
412
   * @param pjp               the join point providing method arguments
413
   * @param fallback          the method-level {@code @Fallback} annotation
414
   *                          (may be {@code null})
415
   * @param interceptFallback the fallback SpEL from {@code @Intercept.fallback()}
416
   *                          (may be blank)
417
   * @param prefixedKey       the fully-qualified cache key (cache name + separator + key)
418
   * @return the resolved fallback value, or {@code null} if no fallback is available
419
   *         and the cache does not contain the key
420
   * @throws Throwable if the SpEL evaluation or fallback method invocation fails
421
   */
422
  private Object resolveInterceptFallback(
423
    ProceedingJoinPoint pjp,
424
    Fallback fallback,
425
    String interceptFallback,
426
    String prefixedKey
427
  ) throws Throwable {
428
    if (!interceptFallback.isBlank()) {
3✔
429
      return getExpression(interceptFallback).getValue(buildEvaluationContext(pjp));
8✔
430
    }
431
    if (fallback != null) {
2✔
432
      return resolveFallback(pjp, fallback);
5✔
433
    }
434
    return hotKey.peek(prefixedKey).orElse(null);
7✔
435
  }
436

437
  /**
438
   * Resolves the fallback value from a {@link Fallback @Fallback} annotation.
439
   * <p>If the annotated SpEL expression is non-empty it is evaluated first;
440
   * otherwise the naming-convention method ({@code {methodName}Fallback}) is invoked.
441
   *
442
   * @param pjp      the join point providing method arguments
443
   * @param fallback the fallback annotation
444
   * @return the resolved fallback value
445
   */
446
  private Object resolveFallback(ProceedingJoinPoint pjp, Fallback fallback) throws Throwable {
447
    if (!fallback.value().isEmpty()) {
4✔
448
      return getExpression(fallback.value()).getValue(buildEvaluationContext(pjp));
9✔
449
    }
450
    return invokeFallbackMethod(pjp);
4✔
451
  }
452

453
  /**
454
   * Invokes the naming-convention fallback method on the target bean with
455
   * the original method arguments.
456
   *
457
   * <p>The fallback method is expected to follow the naming convention
458
   * {@code {originalMethodName}Fallback} with the same parameter types
459
   * as the original method. The resolved method is cached in
460
   * {@link #fallbackMethodCache} for subsequent invocations.
461
   *
462
   * <p>If no matching fallback method exists anywhere in the class
463
   * hierarchy, {@code null} is returned silently (no error is logged).
464
   *
465
   * @param pjp the join point providing the original method's arguments,
466
   *            target object, and signature metadata
467
   * @return the return value of the fallback method, or {@code null} if
468
   *         no fallback method was found
469
   * @throws Throwable the unwrapped cause if the fallback method throws
470
   */
471
  private Object invokeFallbackMethod(ProceedingJoinPoint pjp) throws Throwable {
472
    MethodSignature sig = (MethodSignature) pjp.getSignature();
4✔
473
    Method originalMethod = sig.getMethod();
3✔
474
    Object target = pjp.getTarget();
3✔
475
    Object[] args = pjp.getArgs();
3✔
476

477
    Method fallbackMethod = fallbackMethodCache.computeIfAbsent(originalMethod, m ->
9✔
478
      findFallbackMethod(target.getClass(), m.getName() + "Fallback", m.getParameterTypes())
10✔
479
    );
480
    if (fallbackMethod == null) {
2✔
481
      return null;
2✔
482
    }
483
    try {
484
      return fallbackMethod.invoke(target, args);
5✔
485
    } catch (InvocationTargetException e) {
1✔
486
      throw e.getCause();
3✔
487
    }
488
  }
489

490
  /**
491
   * Recursively searches the class hierarchy (superclasses, not interfaces)
492
   * for a method matching the given name and parameter types.
493
   *
494
   * <p>The search starts at the provided class and walks up the inheritance
495
   * chain via {@link Class#getSuperclass()}, stopping at {@link Object}.
496
   * This ensures that fallback methods defined in abstract base classes
497
   * or superclasses are discovered.
498
   *
499
   * <p>Interface default methods are <em>not</em> searched, as the
500
   * annotation-based contract is that the fallback method resides in the
501
   * concrete bean's class hierarchy.
502
   *
503
   * @param clazz      the class to start the search from (typically the
504
   *                   target object's runtime class)
505
   * @param name       the name of the fallback method to find (e.g.,
506
   *                   {@code "findUserFallback"})
507
   * @param paramTypes the parameter types of the method to find (must
508
   *                   exactly match the original method's parameter types)
509
   * @return the matching {@link Method}, or {@code null} if no method
510
   *         with the given name and parameter types exists in the
511
   *         hierarchy
512
   */
513
  private Method findFallbackMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
514
    try {
515
      return clazz.getMethod(name, paramTypes);
5✔
516
    } catch (NoSuchMethodException e) {
1✔
517
      Class<?> superclass = clazz.getSuperclass();
3✔
518
      if (superclass != null && superclass != Object.class) {
5!
519
        return findFallbackMethod(superclass, name, paramTypes);
×
520
      }
521
      return null;
2✔
522
    }
523
  }
524

525
  /**
526
   * Resolve {@link HotKeyPreload @HotKeyPreload} from the given method,
527
   * using a local cache to avoid repeated reflection lookups.
528
   *
529
   * @param method the candidate method
530
   * @return the {@code @HotKeyPreload} annotation if present, or {@code null}
531
   */
532
  private HotKeyPreload resolvePreloadAnnotation(Method method) {
533
    return preloadCache.computeIfAbsent(method, m -> m.getAnnotation(HotKeyPreload.class));
12✔
534
  }
535
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc