Статьи

Как построить и очистить кэш справочных данных с помощью Sgingleton EJB, Ehcache и MBeans

В этой статье я расскажу, как создать простой кэш справочных данных в Java EE, используя одноэлементные EJB и Ehcache . Кеш самопроизвольно сбрасывается через определенный промежуток времени и может быть очищен «вручную» путем вызова конечной точки REST или метода MBean. Этот пост фактически основан на предыдущем посте  Как создать и очистить кэш справочных данных с помощью одноэлементных EJB и MBeans ; единственное отличие состоит в том, что вместо хранения данных в  ConcurrentHashMap<String, Object>я буду использовать кеш Ehcache, а кеш может обновляться с помощью средств Ehcache.

1. Кеш

Предполагалось, что это будет кэш только для чтения с возможностью очистки его извне. Я хотел, чтобы кэш был своего рода оберткой для службы, предоставляющей фактические справочные данные для приложения — стиль AOP с кодом :)

1.1. Интерфейс

@Local
public interface ReferenceDataCache {

	/**
	 * Returns all reference data required in the application 
	 */
	ReferenceData getReferenceData();
 
	/**
	 * evict/flush all data from cache 
	 */
	void evictAll();
}

Функциональность кэширования определяет два простых метода:

  • getReferenceData() — который кэширует справочные данные, собранные за кулисами из разных источников
  • evictAll() — метод вызывается для полной очистки кэша

1.2. Реализация

@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
@Singleton
public class ReferenceDataCacheBean implements ReferenceDataCache {
	
	private static final String ALL_REFERENCE_DATA_KEY = "ALL_REFERENCE_DATA";
	
	private static final int CACHE_MINUTES_TO_LIVE = 100;
	
	private CacheManager cacheManager;
	
	private Cache refDataEHCache = null; 	
	
	@EJB
	ReferenceDataLogic referenceDataService;	

	@PostConstruct
	public void initialize(){		
		
		cacheManager = CacheManager.getInstance();
		CacheConfiguration cacheConfiguration = new CacheConfiguration("referenceDataCache", 1000);
		cacheConfiguration.setTimeToLiveSeconds(CACHE_MINUTES_TO_LIVE * 60);
		
		refDataEHCache = new Cache(cacheConfiguration );
		cacheManager.addCache(refDataEHCache);
	}
	
	@Override
	@Lock(LockType.READ)
	public ReferenceData getReferenceData() {
		Element element = refDataEHCache.get(ALL_REFERENCE_DATA_KEY);
		
		if(element != null){	
			return (ReferenceData) element.getObjectValue();
		} else {
			ReferenceData referenceData = referenceDataLogic.getReferenceData();
			
			refDataEHCache.putIfAbsent(new Element(ALL_REFERENCE_DATA_KEY, referenceData));
			
			return referenceData;
		}		
	}

	@Override
	public void evictAll() {
		cacheManager.clearAll();
	}	
	...........
}

Замечания:

  • @Singleton — вероятно, самая важная строка кода в этом классе. Эта аннотация указывает, что в приложении будет ровно один синглтон этого типа компонента. Этот бин может быть вызван одновременно несколькими потоками.

Давайте теперь разберем код на разные части:

1.2.1. Инициализация кэша

@PostConstructАннотаций используется на метод , который должен быть выполнен после инъекции зависимостей делается, чтобы выполнить инициализацию в нашем случае — это создать и инициализировать (Eh) кэш.

	@PostConstruct
	public void initialize(){		
		
		cacheManager = CacheManager.create();

		CacheConfiguration cacheConfiguration = new CacheConfiguration("referenceDataCache", 1000);
		cacheConfiguration.setTimeToLiveSeconds(CACHE_MINUTES_TO_LIVE * 60);
		
		refDataEHCache = new Cache(cacheConfiguration );
		cacheManager.addCache(refDataEHCache);
	}

Примечание. С помощью этой аннотации можно аннотировать только один метод.

Все случаи использования Ehcache начинаются с создания a CacheManager, который является контейнером для Ehcaches, который поддерживает все аспекты их жизненного цикла. Я использую  CacheManager.create()метод, который является фабричным методом для создания одноэлементного CacheManager с конфигурацией по умолчанию, или возвращаю его, если он существует:

cacheManager = CacheManager.create();

Затем я построил  CacheConfigurationобъект, указав имя кэша («referenceDataCache») и количество максимального количества элементов в памяти ( maxEntriesLocalHeap), до того, как они будут выселены (0 == без ограничений), и, наконец, я установил значение по умолчанию. количество времени жизни для элемента с даты его создания:

CacheConfiguration cacheConfiguration = new CacheConfiguration("referenceDataCache", 1000);
cacheConfiguration.setTimeToLiveSeconds(CACHE_MINUTES_TO_LIVE * 60);

Теперь с помощью объекта CacheConfiguration я  программно  создаю свой кеш справочных данных и добавляю его в CacheManager. Обратите внимание, что кеши нельзя использовать, пока они не добавлены в CacheManager:

refDataEHCache = new Cache(cacheConfiguration );
cacheManager.addCache(refDataEHCache);

Примечание.   Вы также можете создавать кэши декларативным способом: при создании CacheManager создаются кэши, найденные в конфигурации. Вы можете создать CacheManager, указав путь к файлу конфигурации, из конфигурации в classpath, из конфигурации в InputStream или путем использования файла ehcache.xml по умолчанию в вашем classpath. Взгляните на примеры кода Ehcache  для получения дополнительной информации.

1.2.2. Получить данные из кеша

@Override
@Lock(LockType.READ)
public ReferenceData getReferenceData() {
	Element element = refDataEHCache.get(ALL_REFERENCE_DATA_KEY);
	
	if(element != null){	
		return (ReferenceData) element.getObjectValue();
	} else {
		ReferenceData referenceData = referenceDataLogic.getReferenceData();
		
		refDataEHCache.put(new Element(ALL_REFERENCE_DATA_KEY, referenceData));
		
		return referenceData;
	}		
}

Сначала я пытаюсь получить элемент из кэша, основываясь на его ключе, и, если он присутствует в cache ( ==null), то если он будет получен из класса обслуживания и помещен в кэш для будущих запросов.

Примечание 
@Lock(LockType.READ) Указывает тип блокировки параллелизма для одноэлементных компонентов с управляемым контейнером параллелизмом. Когда установлено значение  LockType.READ, он применяет метод для разрешения полного одновременного доступа к нему (при условии, что блокировки записи не удерживаются). Это именно то, что я хотел, так как мне нужно только выполнять операции чтения. Другой более консервативный вариант  @Lock(LockType.WRITE), который, кстати, является DEFAULT, обеспечивает исключительный доступ к экземпляру компонента. Это должно замедлить метод в среде с высокой степенью параллелизма …

1.2.3. Очистить кеш

@Override
public void evictAll() {
	cacheManager.clearAll();
}

clearAll()Метод CacheManager очищает содержимое всех кэшей в CacheManager, но без удаления каких — либо тайников. Я просто использовал это здесь для простоты, и потому что у меня есть только один кеш, мне нужно обновить.

Примечание. Если у вас есть несколько кешей, то есть несколько кеш-имен, и вы хотите очистить только один, вам нужно использовать тот CacheManager.clearAllStartingWith(String prefix), который очищает содержимое всех кешей в CacheManager с именем, начинающимся с префикса, но не удаляя их.

2. Как вызвать сброс кеша

Вторая часть этого поста будет посвящена возможностям очистки кеша. Поскольку реализация кэша является корпоративным Java-бином, мы можем вызвать его из MBean или, почему нет, из веб-службы.

2.1. MBean

Если вы новичок в Java Management Extensions (JMX),  который является технологией Java, которая предоставляет инструменты для управления и мониторинга приложений, системных объектов, устройств (например, принтеров) и сервис-ориентированных сетей. Эти ресурсы представлены объектами, называемыми MBeans (для Managed Bean) , я настоятельно рекомендую вам начать с этого учебного  курса: Java Management Extensions (JMX)

2.1.1. Интерфейс

Представленный метод позволит только сброс кеша через JMX:

@MXBean
public interface CacheResetMXBean {
    void resetReferenceDataCache();    
}

 «MXBean — это тип MBean, который ссылается только на предопределенный набор типов данных. Таким образом, вы можете быть уверены, что ваш MBean будет использоваться любым клиентом, включая удаленные клиенты, без каких-либо требований, чтобы клиент имел доступ к классам, зависящим от модели, представляющим типы ваших MBean. MXBeans предоставляют удобный способ связать связанные значения вместе, не требуя, чтобы клиенты были специально настроены для обработки пакетов ». [5]

 2.1.2. Реализация

@Singleton
@Startup
public class CacheReset implements CacheResetMXBean {
    
	private MBeanServer platformMBeanServer;
    private ObjectName objectName = null;
    	
	@EJB
	ReferenceDataCache referenceDataCache;
	
    @PostConstruct
    public void registerInJMX() {
        try {
        	objectName = new ObjectName("org.codingpedia.simplecacheexample:type=CacheReset");
            platformMBeanServer = ManagementFactory.getPlatformMBeanServer();

            //unregister the mbean before registerting again
            Set<ObjectName> existing = platformMBeanServer.queryNames(objectName, null);
            if(existing.size() > 0){
            	platformMBeanServer.unregisterMBean(objectName);
            }
            
            platformMBeanServer.registerMBean(this, objectName);
        } catch (Exception e) {
            throw new IllegalStateException("Problem during registration of Monitoring into JMX:" + e);
        }
    }	
	
	@Override
	public void resetReferenceDataCache() {
		referenceDataCache.evictAll();

	}
	
}

Замечания: 

  • как уже упоминалось, реализация вызывает только evictAll() метод внедренного  синглтон-компонента, описанный в предыдущем разделе
  • боб также определяется как @Singleton
  • @Startup аннотации вызывают боб быть обработаны с помощью контейнера при запуске приложения —  нетерпеливый инициализации
  • Я снова использую  @PostConstruct функциональность. Здесь  этот  bean-компонент зарегистрирован в JMX, прежде чем проверять,  ObjectName используется ли его для удаления, если это так…

2.2. Вызов службы отдыха

Я также встроил возможность очищать кеш, вызывая ресурс REST. Это происходит, когда вы выполняете HTTP POST в (rest-context) / reference-data / flush-cache:

@Path("/reference-data")
public class ReferenceDataResource {
	
	@EJB
	ReferenceDataCache referenceDataCache;
	
        @POST
	@Path("flush-cache")
	public Response flushReferenceDataCache() {
		referenceDataCache.evictAll();
		
		return Response.status(Status.OK).entity("Cache successfully flushed").build();
	}	
	
	@GET
	@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
	public Response getReferenceData(@QueryParam("version") String version) {
		ReferenceData referenceData = referenceDataCache.getReferenceData();				
		
		if(version!=null && version.equals(referenceData.getVersion())){
			return Response.status(Status.NOT_MODIFIED).entity("Reference data was not modified").build();				
		} else {
			return Response.status(Status.OK)
					.entity(referenceData).build();				
		}
	}	
}

Обратите внимание на наличие параметра запроса версии в  @GET getReferenceData(...) методе. Это представляет собой хэш справочных данных, и если он не был изменен, клиент получит статус  HTTP 304 Not Modified . Это хороший способ сэкономить пропускную способность, особенно если у вас есть мобильные клиенты. См. Мой пост  Tutorial — Проектирование и реализация REST API на Java с Джерси и Спрингом , где подробно обсуждается проектирование и реализация сервисов REST.

Примечание.  В кластерной среде необходимо вызывать метод resetCache (…) для каждой JVM, в которой развернуто приложение, при изменении справочных данных.

Ну вот и все. В этом посте мы узнали, как с помощью Ehcache создать простой кэш справочных данных в Java EE. Конечно, вы можете легко расширить функциональность кэша, чтобы предложить более детальный доступ / очистку кешированных объектов. Не забудьте использовать  LockType.WRITE для методов очистки в этом случае …