All AEM as a Cloud Service skills: component development, Workflow model design, development, triggering, launchers, debugging, and triaging, Dispatcher config authoring, advisory, incident response, performance tuning, and security hardening, Rapid Development Environment (RDE) deployment, log inspection, snapshots, and troubleshooting via `aio aem rde`, Jackrabbit Oak query/index tuning (property, Lucene index definitions), and extracting/inventorying JCR queries (XPath, SQL2, QueryBuilder) across a codebase.
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
BPA pattern id: guavaCache. Not a Cloud-Service-native code-quality issue — it only shows up in code carried over from a pre-cloud (legacy AEM 6.x / AMS / on-prem) codebase, so this is a migration reference, not a code-assessment pattern.
On AEM as a Cloud Service the supported in-process cache library is Caffeine (com.github.benmanes.caffeine.cache.*). Bundles importing com.google.common.cache.* are flagged because Guava is shrinking in the CS uber-jar and relying on Guava's cache from a third-party classloader is unstable. Caffeine is the recommended successor (same author as Guava cache) and its API is intentionally near-identical, so the swap is mechanical with a few well-known call-site renames.
Findings come from getBpaFindings('guavaCache', …) (BPA CSV column subtype = custom.guava.cache, importance INFO).
BPA's identifier on this subtype is a Guava-internal class (e.g. com.google.common.cache.AbstractCache), not a customer class — BPA is bytecode-scanning Guava's own cache implementation wherever it's reachable on a bundle's classpath, not the customer code that imports it. A single bundle that embeds Guava can produce hundreds of raw CSV rows (one per Guava-internal class pulled in transitively), all for the same bundle.
The actionable unit is the bundle, named in the free-text message field ("The <Guava class> class in the <bundle> bundle uses <Guava class>."). getBpaFindings('guavaCache', …) already dedupes to one target per bundle — do not iterate the raw per-class rows.
Once you have the bundle name, locate the actual customer files: search that bundle's module (find <module>/src/main/java -name '*.java' | xargs grep -l 'import com.google.common.cache') for the real import com.google.common.cache.* occurrences — those are what you edit, not the Guava-internal class named in the BPA row.
When no BPA/CAM source is available, scan the workspace's .java files directly for import com.google.common.cache.… per bundle/module — treat this as a manual, unconfirmed lead, not a substitute for BPA.
BPA gives only a bundle name (no file, no line/snippet) — there is no analyzer detector to resolve those, unlike the code-assessment cascade patterns. Do not look for a guava-cache entry in the analyzer; open the module's files directly.
A file is in scope when it imports com.google.common.cache.* — Cache, CacheBuilder, LoadingCache, CacheLoader, RemovalListener, or RemovalNotification.
com.google.common.cache.* import; no Caffeine dependency needed.com.google.common.collect.*, com.google.common.base.*) → only the cache portion is in scope; leave other Guava usages alone unless the user asks. Keep the guava dependency if other code still imports com.google.common.*.Not in scope: look-alike cache classes from other packages — e.g. io.micrometer.core.instrument.binder.cache.GuavaCacheMetrics — are not com.google.common.cache.* and should not be flagged. If a manual scan flags one of these, treat it as a scan error and skip it; it is not a Guava cache usage.
Self-evident — the Guava → Caffeine API mapping below is fixed; no user input is required to plan the edit. The only judgment call is the Caffeine version: pin it to the AEM CS SDK BOM (default 3.1.8).
| Guava | Caffeine |
|---|---|
com.google.common.cache.Cache | com.github.benmanes.caffeine.cache.Cache |
com.google.common.cache.LoadingCache | com.github.benmanes.caffeine.cache.LoadingCache |
com.google.common.cache.CacheBuilder | com.github.benmanes.caffeine.cache.Caffeine |
com.google.common.cache.CacheLoader | com.github.benmanes.caffeine.cache.CacheLoader |
com.google.common.cache.RemovalListener | com.github.benmanes.caffeine.cache.RemovalListener |
com.google.common.cache.RemovalNotification | callback signature (K key, V value, RemovalCause cause) |
CacheBuilder.newBuilder() | Caffeine.newBuilder() |
.maximumSize(n) / .weakKeys() / .softValues() / .recordStats() / .removalListener(l) | identical |
.expireAfterWrite(d, TimeUnit) | .expireAfterWrite(Duration) (preferred) or (d, TimeUnit) |
.expireAfterAccess(d, TimeUnit) / .refreshAfterWrite(d, TimeUnit) | .expireAfterAccess(Duration) / .refreshAfterWrite(Duration) |
.build() | identical (returns Cache<K,V>) |
.build(cacheLoader) | identical (returns LoadingCache<K,V>) |
cache.getIfPresent(key) / .asMap() / .invalidate(key) / .invalidateAll() | identical |
cache.get(key, Callable<V>) | cache.get(key, Function<K,V>) — argument is a Function, not Callable |
LoadingCache.getUnchecked(key) | LoadingCache.get(key) — Caffeine's get already throws unchecked |
LoadingCache.refresh(key) | identical |
<!-- BEFORE -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
<scope>provided</scope>
</dependency><!-- AFTER -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
<scope>provided</scope>
</dependency>Rules:
<scope>provided</scope> — Caffeine is supplied by the AEM CS runtime; never embed it.guava dependency and add Caffeine alongside.// REMOVE
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;// ADD (only those actually used)
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.RemovalListener;
import com.github.benmanes.caffeine.cache.RemovalCause;// BEFORE — Guava
LoadingCache<String, User> users = CacheBuilder.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build(new CacheLoader<String, User>() {
@Override
public User load(String id) throws Exception {
return userRepo.findById(id);
}
});
User u = users.getUnchecked("u-42");// AFTER — Caffeine
LoadingCache<String, User> users = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats()
.build(id -> userRepo.findById(id));
User u = users.get("u-42");// BEFORE — get-or-compute (Callable) // AFTER — Caffeine (Function)
String v = cache.get(key, () -> compute(key)); String v = cache.get(key, k -> compute(k));// BEFORE — RemovalListener (RemovalNotification)
RemovalListener<String, User> rl = notification ->
log.info("evicted {} cause={}", notification.getKey(), notification.getCause());
// AFTER — RemovalListener (3-arg)
RemovalListener<String, User> rl = (key, value, cause) ->
log.info("evicted {} cause={}", key, cause);Surgical, formatting-preserving text edits — no reformatting / re-serialization:
com.google.common.cache.X import with its Caffeine counterpart (C2); drop imports for types no longer referenced.CacheBuilder.newBuilder() → Caffeine.newBuilder() (C3).getUnchecked( → get(, and convert any cache.get(key, Callable) to a Function lambda.CacheLoader to a lambda where the load body is a single expression.Anchor each replace on the smallest unique substring so unrelated identical text is not touched.
import-not-found: com.google.common.cache.* not present in <file> — the flagged import is no longer there (already migrated). Record skipped.guava-still-required: non-cache com.google.common.* usage in <file> — only remove the cache imports; keep the Guava dependency. Record as a partial apply note, not a skip.import com.google.common.cache.* remains in changed files.CacheBuilder.newBuilder() remains; all builders use Caffeine.newBuilder().LoadingCache.getUnchecked(...) remains; replaced with .get(...).cache.get(key, Callable) remains; the Callable is a Function (lambda).RemovalListener callbacks use the (key, value, cause) signature, not RemovalNotification.Caffeine is on the bundle's pom.xml with <scope>provided</scope>, version pinned to the AEM CS SDK BOM.mvn clean install passes.com.google.common.* usage remains.<scope>provided</scope>; Caffeine is supplied by the CS runtime, never embed it in the bundle.com.google.common.collect/base, keep the dependency and add Caffeine alongside.getUnchecked left in place — Caffeine has no getUnchecked; LoadingCache.get(key) already throws unchecked.Callable vs Function — cache.get(key, …) takes a Function<K,V> in Caffeine, not a Callable<V>.After the swap, generate a test that confirms cache behaviour is preserved — getIfPresent returns null for unknown keys, cache.get(key, Function) computes and caches, invalidate(key) removes the entry, and LoadingCache.get(key) does not throw checked exceptions. One test class per production class changed, suffix Test, under src/test/java/….
The skeleton below is illustrative (JUnit 4), not a template to copy verbatim. Match whatever the target module already uses — check the module's existing tests and pom.xml first:
org.junit.Test, @RunWith(MockitoJUnitRunner.class)) — use the skeleton as-is.org.junit.jupiter.api.Test, @ExtendWith) — swap org.junit.Before/Test/Assert.* for org.junit.jupiter.api.BeforeEach/Test/Assertions.*, and if the module uses io.wcm.testing.mock.aem.junit5.AemContextExtension for OSGi component activation, register the service via context.registerInjectActivateService(service) instead of calling service.activate(...) directly.Do not inject JUnit 4 into a JUnit 5 module (or vice versa) — check the existing test suite's imports before generating.
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Before;
import org.junit.Test;
public class UserCacheTest {
private UserCache service;
@Before
public void setUp() {
service = new UserCache();
service.activate(java.util.Collections.emptyMap());
}
@Test
public void shouldReturnNullForUnknownKey() {
assertNull(service.getIfPresent("u-unknown"));
}
@Test
public void shouldComputeAndCache() {
Object first = service.get("u-42");
assertNotNull(first);
assertEquals(first, service.get("u-42"));
}
@Test
public void shouldInvalidateEntry() {
service.get("u-42");
service.invalidate("u-42");
assertNull(service.getIfPresent("u-42"));
}
}.tessl-plugin
skills
aem-rde
aem-workflow
code-assessment
content-distribution
create-component
assets
references
dispatcher
incident-response
references
dispatcher-foundation
performance-tuning
references
dispatcher-foundation
security-hardening
references
dispatcher-foundation
technical-advisory
references
dispatcher-foundation
workflow-orchestrator
references
dispatcher-foundation
ensure-agents-md
references
migration
references
dispatcher
legacy-ui
template-modernization
validation
scripts
dispatcher-tool
validate-migration