IgniteCacheAdapter.java
/*
* Copyright 2016-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.caches.ignite;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ignite.catalog.ColumnType;
import org.apache.ignite.catalog.definitions.ColumnDefinition;
import org.apache.ignite.catalog.definitions.TableDefinition;
import org.apache.ignite.client.IgniteClient;
import org.apache.ignite.sql.ResultSet;
import org.apache.ignite.sql.SqlRow;
import org.apache.ignite.table.KeyValueView;
import org.apache.ignite.table.Tuple;
/**
* Cache adapter for Ignite 3. Connects to a running Ignite 3 cluster via thin client. The server address is read from
* {@value #CFG_PATH} (property {@code ignite.addresses}), otherwise the default {@value #DEFAULT_ADDRESSES} is used.
*
* @author Roman Shtykh
*/
public final class IgniteCacheAdapter implements Cache {
/** Logger. */
private static final Log log = LogFactory.getLog(IgniteCacheAdapter.class);
/** Cache id. */
private final String id;
/** Table name derived from the cache id. */
private final String tableName;
/**
* {@code ReadWriteLock}.
*/
private final ReadWriteLock readWriteLock = new DummyReadWriteLock();
/** Ignite thin client (shared across all adapter instances). Lazily initialized. */
private static volatile IgniteClient sharedClient;
/** This adapter's Ignite client. */
private final IgniteClient client;
/** Key-value view for this cache's table. */
private final KeyValueView<Tuple, Tuple> cache;
/** Default Ignite 3 thin client port. */
static final String DEFAULT_ADDRESSES = "127.0.0.1:10800";
/** Ignite client configuration file path. */
static final String CFG_PATH = "config/default-config.properties";
/** Table key column name. */
static final String KEY_COL = "key";
/** Table value column name. */
static final String VAL_COL = "val";
/**
* Returns the shared {@link IgniteClient}, creating it lazily on first call.
*/
private static IgniteClient getOrCreateIgniteClient() {
if (sharedClient == null) {
synchronized (IgniteCacheAdapter.class) {
if (sharedClient == null) {
sharedClient = createIgniteClient();
}
}
}
return sharedClient;
}
/**
* Creates a new {@link IgniteClient} from the configuration file or defaults.
*/
static IgniteClient createIgniteClient() {
String addresses = DEFAULT_ADDRESSES;
Properties props = new Properties();
try (InputStream is = Files.newInputStream(Path.of(CFG_PATH))) {
props.load(is);
addresses = props.getProperty("ignite.addresses", DEFAULT_ADDRESSES);
} catch (IOException e) {
log.debug("Ignite config file not found at '" + CFG_PATH + "', using defaults.");
log.trace("" + e);
}
return IgniteClient.builder().addresses(addresses.split(",")).build();
}
/**
* Constructor.
*
* @param id
* Cache id.
*/
public IgniteCacheAdapter(String id) {
this(requireNonNullId(id), getOrCreateIgniteClient());
}
private static String requireNonNullId(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
return id;
}
/**
* Package-private constructor for testing: allows injection of a mock {@link IgniteClient} without requiring a
* running Ignite cluster.
*
* @param id
* Cache id.
* @param igniteClient
* The {@link IgniteClient} to use.
*/
IgniteCacheAdapter(String id, IgniteClient igniteClient) {
this.id = requireNonNullId(id);
this.tableName = toTableName(id);
this.client = igniteClient;
igniteClient.catalog().createTable(
TableDefinition.builder(tableName).ifNotExists().columns(ColumnDefinition.column(KEY_COL, ColumnType.VARBINARY),
ColumnDefinition.column(VAL_COL, ColumnType.VARBINARY)).primaryKey(KEY_COL).build());
cache = igniteClient.tables().table(tableName).keyValueView();
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
cache.put(null, Tuple.create().set(KEY_COL, serialize(key)), Tuple.create().set(VAL_COL, serialize(value)));
}
@Override
public Object getObject(Object key) {
Tuple valueTuple = cache.get(null, Tuple.create().set(KEY_COL, serialize(key)));
return valueTuple != null ? deserialize(valueTuple.bytesValue(VAL_COL)) : null;
}
@Override
public Object removeObject(Object key) {
Tuple valueTuple = cache.getAndRemove(null, Tuple.create().set(KEY_COL, serialize(key)));
return valueTuple != null ? deserialize(valueTuple.bytesValue(VAL_COL)) : null;
}
@Override
public void clear() {
cache.removeAll(null);
}
@Override
public int getSize() {
try (ResultSet<SqlRow> rs = client.sql().execute(null, "SELECT COUNT(*) FROM " + tableName)) {
return rs.hasNext() ? (int) rs.next().longValue(0) : 0;
}
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
static String toTableName(String id) {
// Sanitize to alphanumeric and underscore only, ensuring safe use in SQL identifiers.
return id.replaceAll("[^a-zA-Z0-9_]", "_").toUpperCase();
}
static byte[] serialize(Object obj) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
return baos.toByteArray();
} catch (IOException e) {
throw new IllegalArgumentException("Cannot serialize object of type " + obj.getClass().getName(), e);
}
}
static Object deserialize(byte[] bytes) {
if (bytes == null) {
return null;
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new IllegalStateException("Cannot deserialize cache object", e);
}
}
}