AgentSkillsCN

lutece-cache

在Lutece 8插件中实现缓存的规则与模式。包括AbstractCacheableService、CDI初始化、缓存操作,以及通过CDI事件触发缓存失效。

SKILL.md
--- frontmatter
name: lutece-cache
description: "Rules and patterns for implementing cache in a Lutece 8 plugin. AbstractCacheableService, CDI initialization, cache operations, invalidation via CDI events."

Lutece 8 Cache Implementation

Before implementing cache, consult ~/.lutece-references/lutece-form-plugin-forms/ — specifically FormsCacheService.java.

Architecture Overview

code
AbstractCacheableService<K, V>  (lutece-core, JSR-107 JCache)
    ↑ extends
MyCacheService (@ApplicationScoped, @PostConstruct initCache)
    ↓ used by
Home / Service (put, get, remove, resetCache)
    ↓ invalidated by
CDI Events (@Observes)

Step 1 — Cache Service Class

IMPORTANT: The put()/get()/remove() methods inherited from AbstractCacheableService delegate directly to _cache without null/closed checks. If the cache is disabled in the datastore (default state), _cache is null and these methods throw NullPointerException. You MUST override them with defensive guards.

java
import javax.cache.CacheException;

import fr.paris.lutece.portal.service.cache.AbstractCacheableService;
import fr.paris.lutece.portal.service.util.AppLogService;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class EntityCacheService extends AbstractCacheableService<String, Object>
{
    private static final String CACHE_NAME = "myplugin.entityCacheService";

    @PostConstruct
    public void init( )
    {
        initCache( CACHE_NAME, String.class, Object.class );
    }

    @Override
    public String getName( )
    {
        return CACHE_NAME;
    }

    @Override
    public void put( String key, Object value )
    {
        if ( isCacheEnable( ) && isCacheAvailable( ) )
        {
            try
            {
                super.put( key, value );
            }
            catch( CacheException | IllegalStateException e )
            {
                AppLogService.error( "EntityCacheService : error putting key {} in cache", key, e );
            }
        }
    }

    @Override
    public Object get( String key )
    {
        if ( isCacheEnable( ) && isCacheAvailable( ) )
        {
            try
            {
                return super.get( key );
            }
            catch( CacheException | IllegalStateException e )
            {
                AppLogService.error( "EntityCacheService : error getting key {} from cache", key, e );
            }
        }
        return null;
    }

    @Override
    public boolean remove( String key )
    {
        if ( isCacheEnable( ) && isCacheAvailable( ) )
        {
            try
            {
                return super.remove( key );
            }
            catch( CacheException | IllegalStateException e )
            {
                AppLogService.error( "EntityCacheService : error removing key {} from cache", key, e );
            }
        }
        return false;
    }

    private boolean isCacheAvailable( )
    {
        return _cache != null && !_cache.isClosed( );
    }
}

Rules:

  • @ApplicationScoped — singleton CDI bean, one instance per application
  • @PostConstruct calls initCache( name, keyClass, valueClass ) — registers the cache with CacheService
  • Cache name convention: pluginName.entityCacheService
  • Generic types: <String, Object> is the standard — key is always String, value is the cached object
  • Override put/get/remove with isCacheEnable() && isCacheAvailable() guards + try/catch — the core AbstractCacheableService does NOT check for null/closed cache in these methods

Step 2 — Cache Key Builders

Define static methods for consistent key generation:

java
@ApplicationScoped
public class EntityCacheService extends AbstractCacheableService<String, Object>
{
    private static final String KEY_PREFIX = "myplugin.entity.";

    // Key for a single entity
    public static String getEntityCacheKey( int nIdEntity )
    {
        return KEY_PREFIX + nIdEntity;
    }

    // Key for the full list
    public static String getListCacheKey( )
    {
        return KEY_PREFIX + "list";
    }

    // Key with parameters (e.g., filtered list)
    public static String getFilteredCacheKey( int nIdCategory, int nPage )
    {
        return KEY_PREFIX + "category." + nIdCategory + ".page." + nPage;
    }

    // ...
}

Step 3 — Usage in Home or Service

Access pattern: @Inject (CDI-managed beans) vs CDI.current().select() (static contexts)

ContextPatternExample
CDI-managed bean (@ApplicationScoped, @RequestScoped, etc.)@InjectService class
Static context (Home class, static utility)CDI.current().select() direct field initHome class

NEVER provide a getInstance() method on the cache service — it is @Deprecated(since = "8.0", forRemoval = true) in lutece-core.

Pattern A — @Inject in a CDI-managed Service (preferred)

java
@ApplicationScoped
public class EntityService
{
    @Inject
    private EntityCacheService _cacheService;

    public Entity findByPrimaryKey( int nId )
    {
        String strCacheKey = EntityCacheService.getEntityCacheKey( nId );
        Entity entity = (Entity) _cacheService.get( strCacheKey );

        if ( entity == null )
        {
            entity = EntityHome.findByPrimaryKey( nId );

            if ( entity != null )
            {
                _cacheService.put( strCacheKey, entity );
            }
        }

        return entity;
    }

    public Entity update( Entity entity )
    {
        EntityHome.update( entity );

        _cacheService.remove( EntityCacheService.getEntityCacheKey( entity.getId( ) ) );
        _cacheService.remove( EntityCacheService.getListCacheKey( ) );

        return entity;
    }

    public void remove( int nId )
    {
        EntityHome.remove( nId );

        _cacheService.remove( EntityCacheService.getEntityCacheKey( nId ) );
        _cacheService.remove( EntityCacheService.getListCacheKey( ) );
    }
}

Pattern B — CDI.current().select() in a static Home class

Use direct field initialization — no lazy-init getters. This is the pattern used by all Home classes in lutece-core (RoleHome, PageHome, FileHome) and in the Forms plugin (FormHome, StepHome). The CDI container is fully initialized before plugin classes are loaded.

java
import jakarta.enterprise.inject.spi.CDI;

public class EntityHome
{
    private static IEntityDAO _dao = CDI.current( ).select( IEntityDAO.class ).get( );
    private static EntityCacheService _cacheService = CDI.current( ).select( EntityCacheService.class ).get( );
    private static final Plugin _plugin = PluginService.getPlugin( "pluginname" );

    private EntityHome( )
    {
    }

    public static Entity findByPrimaryKey( int nId )
    {
        String strCacheKey = EntityCacheService.getEntityCacheKey( nId );
        Entity entity = (Entity) _cacheService.get( strCacheKey );

        if ( entity == null )
        {
            entity = _dao.load( nId, _plugin );
            if ( entity != null )
            {
                _cacheService.put( strCacheKey, entity );
            }
        }
        return entity;
    }
}

Step 4 — Cache Invalidation via CDI Events

Observe domain events to automatically invalidate cache:

java
import fr.paris.lutece.portal.service.event.ResourceEvent;
import jakarta.enterprise.event.Observes;

@ApplicationScoped
public class EntityCacheService extends AbstractCacheableService<String, Object>
{
    // ...

    public void onResourceEvent( @Observes ResourceEvent event )
    {
        if ( isCacheEnable( ) && "MYPLUGIN_ENTITY".equals( event.getResourceType( ) ) )
        {
            resetCache( );
        }
    }
}

For finer-grained invalidation (remove specific key instead of full reset):

java
public void onResourceEvent( @Observes ResourceEvent event )
{
    if ( isCacheEnable( ) && "MYPLUGIN_ENTITY".equals( event.getResourceType( ) ) )
    {
        String strId = event.getIdResource( );

        if ( strId != null )
        {
            remove( EntityCacheService.getEntityCacheKey( Integer.parseInt( strId ) ) );
            remove( EntityCacheService.getListCacheKey( ) );
        }
        else
        {
            resetCache( );
        }
    }
}

Cache Operations Reference

MethodUsage
put( key, value )Add or update an entry
get( key )Retrieve (returns null on miss)
remove( key )Delete a single entry
resetCache( )Clear all entries
enableCache( boolean )Toggle cache on/off
isCacheEnable( )Check if cache is active
getCacheSize( )Current entry count
getKeys( )List all keys
containsKey( key )Check key existence

Optional — Prevent Global Reset

If your cache should survive when an admin clicks "Reset all caches":

java
@Override
public boolean isPreventGlobalReset( )
{
    return true;
}

Use sparingly — only for caches that are expensive to rebuild (e.g., configuration caches).

Configuration (properties)

Cache behavior can be tuned via caches.properties or datastore:

properties
# Default settings (apply to all caches without specific config)
lutece.cache.default.maxElementsInMemory=1000
lutece.cache.default.eternal=false
lutece.cache.default.timeToIdleSeconds=1000
lutece.cache.default.timeToLiveSeconds=1000

# Per-cache override
core.cache.status.myplugin.entityCacheService.eternal=true
core.cache.status.myplugin.entityCacheService.enabled=1

Cache enabled/disabled state is persisted in the datastore database with key: core.cache.status.{cacheName}.enabled

File Checklist

FileWhat to add
EntityCacheService.javaNew class in service/cache/, @ApplicationScoped, extends AbstractCacheableService, key builders
EntityService.java@Inject EntityCacheService, cache-through logic (get → miss → load → put)
beans.xmlAlready present (required for CDI)

No changes needed in plugin.xml or messages.properties — cache is infrastructure, not UI.

Reference Sources

NeedFile to consult
CDI cache service (v8 pattern)~/.lutece-references/lutece-form-plugin-forms/src/java/**/service/FormsCacheService.java
Core AbstractCacheableService~/.lutece-references/lutece-core/src/java/**/service/cache/AbstractCacheableService.java
Core CacheService (static facade)~/.lutece-references/lutece-core/src/java/**/service/cache/CacheService.java
Cache manager (JSR-107)~/.lutece-references/lutece-core/src/java/**/service/cache/Lutece107CacheManager.java
Cache configuration~/.lutece-references/lutece-core/src/java/**/service/cache/CacheConfigUtil.java