diff --git a/pom.xml b/pom.xml index 7e66651..a98b44b 100644 --- a/pom.xml +++ b/pom.xml @@ -112,9 +112,9 @@ | test dependencies --> - org.junit.jupiter - junit-jupiter-engine - 5.7.0 + junit + junit + 4.12 test diff --git a/src/main/java/org/mybatis/caches/redis/JDKSerializer.java b/src/main/java/org/mybatis/caches/redis/JDKSerializer.java index d0355f7..5464f73 100644 --- a/src/main/java/org/mybatis/caches/redis/JDKSerializer.java +++ b/src/main/java/org/mybatis/caches/redis/JDKSerializer.java @@ -17,9 +17,9 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.util.Arrays; import org.apache.ibatis.cache.CacheException; @@ -31,10 +31,12 @@ private JDKSerializer() { // prevent instantiation } - public byte[] serialize(Object object) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + public byte[] serialize(long timestamp, Object object) { + try ( + ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(object); + baos.write(longToBytes(timestamp)); return baos.toByteArray(); } catch (Exception e) { throw new CacheException(e); @@ -42,10 +44,11 @@ public byte[] serialize(Object object) { } public Object unserialize(byte[] bytes) { - if (bytes == null) { + if (bytes == null || bytes.length < 8) { return null; } - try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + try ( + ByteArrayInputStream bais = new ByteArrayInputStream(Arrays.copyOf(bytes, bytes.length - 8)); ObjectInputStream ois = new ObjectInputStream(bais)) { return ois.readObject(); } catch (Exception e) { diff --git a/src/main/java/org/mybatis/caches/redis/KryoSerializer.java b/src/main/java/org/mybatis/caches/redis/KryoSerializer.java index c12fd76..2bdca8a 100644 --- a/src/main/java/org/mybatis/caches/redis/KryoSerializer.java +++ b/src/main/java/org/mybatis/caches/redis/KryoSerializer.java @@ -1,5 +1,5 @@ /** - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 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. @@ -57,7 +57,7 @@ private KryoSerializer() { fallbackSerializer = JDKSerializer.INSTANCE;// use JDKSerializer as fallback } - public byte[] serialize(Object object) { + public byte[] serialize(long timestamp, Object object) { output.clear(); if (!unnormalClassSet.contains(object.getClass())) { /** @@ -66,30 +66,33 @@ public byte[] serialize(Object object) { */ try { kryo.writeClassAndObject(output, object); + // Add 8 bytes of timestamp to the tail + output.write(longToBytes(timestamp)); return output.toBytes(); } catch (Exception e) { // For unnormal class occurred for the first time, exception will be thrown unnormalClassSet.add(object.getClass()); - return fallbackSerializer.serialize(object);// use fallback Serializer to resolve + return fallbackSerializer.serialize(timestamp, object);// use fallback Serializer to resolve } } else { // For unnormal class - return fallbackSerializer.serialize(object); + return fallbackSerializer.serialize(timestamp, object); } } public Object unserialize(byte[] bytes) { - if (bytes == null) { + if (bytes == null || bytes.length < 8) { return null; } - int hashCode = Arrays.hashCode(bytes); - if (!unnormalBytesHashCodeSet.contains(hashCode)) { + byte[] stripped = Arrays.copyOf(bytes, bytes.length - 8); + int hashCode = Arrays.hashCode(stripped); + if (!unnormalBytesHashCodeSet.contains(stripped)) { /** * In the following cases: 1. This bytes occurs for the first time. 2. This bytes have occurred and can be * resolved by default kryo serializer */ try { - input.setBuffer(bytes); + input.setBuffer(stripped); return kryo.readClassAndObject(input); } catch (Exception e) { // For unnormal bytes occurred for the first time, exception will be thrown diff --git a/src/main/java/org/mybatis/caches/redis/RedisCache.java b/src/main/java/org/mybatis/caches/redis/RedisCache.java index 1401d6c..6deb26e 100644 --- a/src/main/java/org/mybatis/caches/redis/RedisCache.java +++ b/src/main/java/org/mybatis/caches/redis/RedisCache.java @@ -1,5 +1,5 @@ /** - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 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. @@ -20,8 +20,8 @@ import org.apache.ibatis.cache.Cache; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; +import org.mybatis.caches.redis.client.GenericRedisClient; +import org.mybatis.caches.redis.client.RedisClientBuilder; /** * Cache adapter for Redis. @@ -34,9 +34,9 @@ public final class RedisCache implements Cache { private String id; - private static JedisPool pool; + private static GenericRedisClient client; - private final RedisConfig redisConfig; + private static RedisConfig redisConfig; private Integer timeout; @@ -45,20 +45,16 @@ public RedisCache(final String id) { throw new IllegalArgumentException("Cache instances require an ID"); } this.id = id; - redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration(); - pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), - redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName(), - redisConfig.isSsl(), redisConfig.getSslSocketFactory(), redisConfig.getSslParameters(), - redisConfig.getHostnameVerifier()); - } - - // TODO Review this is UNUSED - private Object execute(RedisCallback callback) { - Jedis jedis = pool.getResource(); - try { - return callback.doWithRedis(jedis); - } finally { - jedis.close(); + synchronized (RedisCache.class) { + if (client == null) { + redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration(); + RedisClientBuilder builder = new RedisClientBuilder(redisConfig, redisConfig.getHosts(), + redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getMaxAttempts(), + redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName(), redisConfig.isSsl(), + redisConfig.getSslSocketFactory(), redisConfig.getSslParameters(), redisConfig.getHostnameVerifier(), + redisConfig.getHostAndPortMap()); + client = builder.getClient(); + } } } @@ -69,60 +65,42 @@ public String getId() { @Override public int getSize() { - return (Integer) execute(new RedisCallback() { - @Override - public Object doWithRedis(Jedis jedis) { - Map result = jedis.hgetAll(id.getBytes()); - return result.size(); - } - }); + Map result = client.hgetAll(id.getBytes()); + return result.size(); } @Override public void putObject(final Object key, final Object value) { - execute(new RedisCallback() { - @Override - public Object doWithRedis(Jedis jedis) { - final byte[] idBytes = id.getBytes(); - jedis.hset(idBytes, key.toString().getBytes(), redisConfig.getSerializer().serialize(value)); - if (timeout != null && jedis.ttl(idBytes) == -1) { - jedis.expire(idBytes, timeout); - } - return null; - } - }); + final byte[] idBytes = id.getBytes(); + long ts = 0; + if (timeout != null) { + ts = System.currentTimeMillis() + timeout * 1000; + } + final byte[] objBytes = redisConfig.getSerializer().serialize(ts, value); + client.hset(idBytes, key.toString().getBytes(), objBytes); } @Override public Object getObject(final Object key) { - return execute(new RedisCallback() { - @Override - public Object doWithRedis(Jedis jedis) { - return redisConfig.getSerializer().unserialize(jedis.hget(id.getBytes(), key.toString().getBytes())); - } - }); + byte[] objBytes = client.hget(id.getBytes(), key.toString().getBytes()); + if (objBytes == null || objBytes.length < 8) return null; + long ts = redisConfig.getSerializer().getTimestamp(objBytes); + if (ts > 0 && ts < System.currentTimeMillis()) { + client.hdel(id, key.toString()); + return null; + } else { + return redisConfig.getSerializer().unserialize(objBytes); + } } @Override public Object removeObject(final Object key) { - return execute(new RedisCallback() { - @Override - public Object doWithRedis(Jedis jedis) { - return jedis.hdel(id, key.toString()); - } - }); + return client.hdel(id, key.toString()); } @Override public void clear() { - execute(new RedisCallback() { - @Override - public Object doWithRedis(Jedis jedis) { - jedis.del(id); - return null; - } - }); - + client.del(id); } @Override diff --git a/src/main/java/org/mybatis/caches/redis/RedisCallback.java b/src/main/java/org/mybatis/caches/redis/RedisCallback.java deleted file mode 100644 index e7ccd5a..0000000 --- a/src/main/java/org/mybatis/caches/redis/RedisCallback.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright 2015-2018 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 - * - * http://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.redis; - -import redis.clients.jedis.Jedis; - -public interface RedisCallback { - - Object doWithRedis(Jedis jedis); -} diff --git a/src/main/java/org/mybatis/caches/redis/RedisConfig.java b/src/main/java/org/mybatis/caches/redis/RedisConfig.java index 9138a25..c3b6d91 100644 --- a/src/main/java/org/mybatis/caches/redis/RedisConfig.java +++ b/src/main/java/org/mybatis/caches/redis/RedisConfig.java @@ -1,5 +1,5 @@ /** - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 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. @@ -19,15 +19,16 @@ import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocketFactory; +import redis.clients.jedis.JedisClusterHostAndPortMap; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.Protocol; public class RedisConfig extends JedisPoolConfig { - private String host = Protocol.DEFAULT_HOST; - private int port = Protocol.DEFAULT_PORT; + private String hosts = Protocol.DEFAULT_HOST + ':' + Protocol.DEFAULT_PORT; private int connectionTimeout = Protocol.DEFAULT_TIMEOUT; private int soTimeout = Protocol.DEFAULT_TIMEOUT; + private int maxAttempts = 5; private String password; private int database = Protocol.DEFAULT_DATABASE; private String clientName; @@ -35,6 +36,7 @@ public class RedisConfig extends JedisPoolConfig { private SSLSocketFactory sslSocketFactory; private SSLParameters sslParameters; private HostnameVerifier hostnameVerifier; + private JedisClusterHostAndPortMap hostAndPortMap; private Serializer serializer = JDKSerializer.INSTANCE; public boolean isSsl() { @@ -69,23 +71,20 @@ public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { this.hostnameVerifier = hostnameVerifier; } - public String getHost() { - return host; + public JedisClusterHostAndPortMap getHostAndPortMap() { + return hostAndPortMap; } - public void setHost(String host) { - if (host == null || "".equals(host)) { - host = Protocol.DEFAULT_HOST; - } - this.host = host; + public void setHostAndPortMap(JedisClusterHostAndPortMap hostAndPortMap) { + this.hostAndPortMap = hostAndPortMap; } - public int getPort() { - return port; + public String getHosts() { + return hosts; } - public void setPort(int port) { - this.port = port; + public void setHosts(String hosts) { + this.hosts = hosts; } public String getPassword() { @@ -134,6 +133,14 @@ public void setSoTimeout(int soTimeout) { this.soTimeout = soTimeout; } + public int getMaxAttempts() { + return maxAttempts; + } + + public void setMaxAttempts(int maxAttempts) { + this.maxAttempts = maxAttempts; + } + public Serializer getSerializer() { return serializer; } diff --git a/src/main/java/org/mybatis/caches/redis/RedisConfigurationBuilder.java b/src/main/java/org/mybatis/caches/redis/RedisConfigurationBuilder.java index 4f95ba1..a16fc08 100644 --- a/src/main/java/org/mybatis/caches/redis/RedisConfigurationBuilder.java +++ b/src/main/java/org/mybatis/caches/redis/RedisConfigurationBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 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. @@ -118,7 +118,8 @@ private void setConfigProperties(Properties properties, RedisConfig jedisConfig) // Custom serializer is not supported yet. throw new CacheException("Unknown serializer: '" + value + "'"); } - } else if (Arrays.asList("sslSocketFactory", "sslParameters", "hostnameVerifier").contains(name)) { + } else if (Arrays.asList("sslSocketFactory", "sslParameters", "hostnameVerifier", "hostAndPortMap") + .contains(name)) { setInstance(metaCache, name, value); } else if (metaCache.hasSetter(name)) { Class type = metaCache.getSetterType(name); diff --git a/src/main/java/org/mybatis/caches/redis/Serializer.java b/src/main/java/org/mybatis/caches/redis/Serializer.java index 2520f11..040796d 100644 --- a/src/main/java/org/mybatis/caches/redis/Serializer.java +++ b/src/main/java/org/mybatis/caches/redis/Serializer.java @@ -1,5 +1,5 @@ /** - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 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. @@ -15,6 +15,8 @@ */ package org.mybatis.caches.redis; +import java.nio.ByteBuffer; + public interface Serializer { /** @@ -23,7 +25,19 @@ public interface Serializer { * @param object * @return serialized bytes */ - public byte[] serialize(Object object); + byte[] serialize(long timestamp, Object object); + + /** + * Read the timestamp + */ + default long getTimestamp(byte[] bytes) { + if (bytes == null || bytes.length < 8) { + return -1; + } + byte[] copy = new byte[8]; + System.arraycopy(bytes, bytes.length - 8, copy, 0, 8); + return bytesToLong(copy); + } /** * Unserialize method @@ -31,6 +45,18 @@ public interface Serializer { * @param bytes * @return unserialized object */ - public Object unserialize(byte[] bytes); + Object unserialize(byte[] bytes); + + default byte[] longToBytes(long x) { + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); + buffer.putLong(x); + return buffer.array(); + } + default long bytesToLong(byte[] bytes) { + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); + buffer.put(bytes); + buffer.flip();//need flip + return buffer.getLong(); + } } diff --git a/src/main/java/org/mybatis/caches/redis/client/ClusterRedisClient.java b/src/main/java/org/mybatis/caches/redis/client/ClusterRedisClient.java new file mode 100644 index 0000000..788bc08 --- /dev/null +++ b/src/main/java/org/mybatis/caches/redis/client/ClusterRedisClient.java @@ -0,0 +1,173 @@ +/** + * Copyright 2015-2020 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 + * + * http://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.redis.client; + +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.params.SetParams; + +import java.util.List; +import java.util.Map; + +/** + * @author Milton Lai(millton.lai@gmail.com) + */ +public class ClusterRedisClient implements GenericRedisClient { + private JedisCluster cluster; + + public ClusterRedisClient(JedisCluster cluster) { + this.cluster = cluster; + } + + @Override + public Object eval(String script, int keyCount, String... params) { + return cluster.eval(script, keyCount, params); + } + + @Override + public List blpop(int timeout, String key) { + return cluster.blpop(timeout, key); + } + + @Override + public List blpop(int timeout, String... keys) { + return cluster.blpop(timeout, keys); + } + + @Override + public List blpop(int timeout, byte[]... keys) { + return cluster.blpop(timeout, keys); + } + + @Override + public List brpop(int timeout, String key) { + return cluster.brpop(timeout, key); + } + + @Override + public List brpop(int timeout, String... keys) { + return cluster.brpop(timeout, keys); + } + + @Override + public List brpop(int timeout, byte[]... keys) { + return cluster.brpop(timeout, keys); + } + + @Override + public Long del(String key) { + return cluster.del(key); + } + + @Override + public Long del(String... keys) { + return cluster.del(keys); + } + + @Override + public Long exists(String... keys) { + return cluster.exists(keys); + } + + @Override + public Boolean exists(String key) { + return cluster.exists(key); + } + + @Override + public Long expire(byte[] key, int seconds) { + return cluster.expire(key, seconds); + } + + @Override + public String get(String key) { + return cluster.get(key); + } + + @Override + public byte[] get(byte[] key) { + return cluster.get(key); + } + + @Override + public Long hdel(String key, String... fields) { + return cluster.hdel(key, fields); + } + + @Override + public byte[] hget(byte[] key, byte[] field) { + return cluster.hget(key, field); + } + + @Override + public Map hgetAll(byte[] key) { + return cluster.hgetAll(key); + } + + @Override + public Long hset(byte[] key, byte[] field, byte[] value) { + return cluster.hset(key, field, value); + } + + @Override + public Long llen(String key) { + return cluster.llen(key); + } + + @Override + public Long lpush(String key, String... strings) { + return cluster.lpush(key, strings); + } + + @Override + public Long pttl(String key) { + return cluster.pttl(key); + } + + @Override + public Long rpush(String key, String... strings) { + return cluster.rpush(key, strings); + } + + @Override + public String set(String key, String value) { + return cluster.set(key, value); + } + + @Override + public String set(byte[] key, byte[] value) { + return cluster.set(key, value); + } + + @Override + public String set(String key, String value, SetParams params) { + return cluster.set(key, value, params); + } + + @Override + public String set(byte[] key, byte[] value, SetParams params) { + return cluster.set(key, value, params); + } + + @Override + public Long ttl(String key) { + return cluster.ttl(key); + } + + @Override + public Long ttl(byte[] key) { + return cluster.ttl(key); + } +} diff --git a/src/main/java/org/mybatis/caches/redis/client/GenericRedisClient.java b/src/main/java/org/mybatis/caches/redis/client/GenericRedisClient.java new file mode 100644 index 0000000..0c9245b --- /dev/null +++ b/src/main/java/org/mybatis/caches/redis/client/GenericRedisClient.java @@ -0,0 +1,84 @@ +/** + * Copyright 2015-2020 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 + * + * http://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.redis.client; + +import redis.clients.jedis.params.SetParams; + +import java.util.List; +import java.util.Map; + +/** + * @author Milton Lai(millton.lai@gmail.com) + */ +public interface GenericRedisClient { + + Object eval(final String script, final int keyCount, final String... params); + + List blpop(int timeout, String key); + + List blpop(int timeout, String... keys); + + List blpop(int timeout, byte[]... keys); + + List brpop(int timeout, String key); + + List brpop(int timeout, String... keys); + + List brpop(int timeout, byte[]... keys); + + Long del(String key); + + Long del(String... keys); + + Long exists(String... keys); + + Boolean exists(String key); + + Long expire(byte[] key, int seconds); + + String get(String key); + + byte[] get(byte[] key); + + Long hdel(String key, String... fields); + + byte[] hget(byte[] key, byte[] field); + + Map hgetAll(byte[] key); + + Long hset(byte[] key, byte[] field, byte[] value); + + Long llen(String key); + + Long lpush(String key, String... strings); + + Long pttl(final String key); + + Long rpush(String key, String... strings); + + String set(String key, String value); + + String set(byte[] key, byte[] value); + + String set(String key, String value, SetParams params); + + String set(byte[] key, byte[] value, SetParams params); + + Long ttl(final String key); + + Long ttl(byte[] key); + +} diff --git a/src/main/java/org/mybatis/caches/redis/client/PooledRedisClient.java b/src/main/java/org/mybatis/caches/redis/client/PooledRedisClient.java new file mode 100644 index 0000000..8410fd4 --- /dev/null +++ b/src/main/java/org/mybatis/caches/redis/client/PooledRedisClient.java @@ -0,0 +1,184 @@ +/** + * Copyright 2015-2020 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 + * + * http://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.redis.client; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPoolAbstract; +import redis.clients.jedis.params.SetParams; + +import java.util.List; +import java.util.Map; + +/** + * @author Milton Lai(millton.lai@gmail.com) + */ +public class PooledRedisClient implements GenericRedisClient { + private JedisPoolAbstract pool; + + public PooledRedisClient(JedisPoolAbstract pool) { + this.pool = pool; + } + + @Override + public Object eval(String script, int keyCount, String... params) { + return execute((Jedis jedis) -> jedis.eval(script, keyCount, params)); + } + + @Override + public List blpop(int timeout, String key) { + return (List) execute((Jedis jedis) -> jedis.blpop(timeout, key)); + } + + @Override + public List blpop(int timeout, String... keys) { + return (List) execute((Jedis jedis) -> jedis.blpop(timeout, keys)); + } + + @Override + public List blpop(int timeout, byte[]... keys) { + return (List) execute((Jedis jedis) -> jedis.blpop(timeout, keys)); + } + + @Override + public List brpop(int timeout, String key) { + return (List) execute((Jedis jedis) -> jedis.brpop(timeout, key)); + } + + @Override + public List brpop(int timeout, String... keys) { + return (List) execute((Jedis jedis) -> jedis.brpop(timeout, keys)); + } + + @Override + public List brpop(int timeout, byte[]... keys) { + return (List) execute((Jedis jedis) -> jedis.brpop(timeout, keys)); + } + + @Override + public Long del(String key) { + return (Long) execute((Jedis jedis) -> jedis.del(key)); + } + + @Override + public Long del(String... keys) { + return (Long) execute((Jedis jedis) -> jedis.del(keys)); + } + + @Override + public Long exists(String... keys) { + return (Long) execute((Jedis jedis) -> jedis.exists(keys)); + } + + @Override + public Boolean exists(String key) { + return (Boolean) execute((Jedis jedis) -> jedis.exists(key)); + } + + @Override + public Long expire(byte[] key, int seconds) { + return (Long) execute((Jedis jedis) -> jedis.expire(key, seconds)); + } + + @Override + public String get(String key) { + return (String) execute((Jedis jedis) -> jedis.get(key)); + } + + @Override + public byte[] get(byte[] key) { + return (byte[]) execute((Jedis jedis) -> jedis.get(key)); + } + + @Override + public Long hdel(String key, String... fields) { + return (Long) execute((Jedis jedis) -> jedis.hdel(key, fields)); + } + + @Override + public byte[] hget(byte[] key, byte[] field) { + return (byte[]) execute((Jedis jedis) -> jedis.hget(key, field)); + } + + @Override + public Map hgetAll(byte[] key) { + return (Map) execute((Jedis jedis) -> jedis.hgetAll(key)); + } + + @Override + public Long hset(byte[] key, byte[] field, byte[] value) { + return (Long) execute((Jedis jedis) -> jedis.hset(key, field, value)); + } + + @Override + public Long llen(String key) { + return (Long) execute((Jedis jedis) -> jedis.llen(key)); + } + + @Override + public Long lpush(String key, String... strings) { + return (Long) execute((Jedis jedis) -> jedis.lpush(key, strings)); + } + + @Override + public Long pttl(String key) { + return (Long) execute((Jedis jedis) -> jedis.pttl(key)); + } + + @Override + public Long rpush(String key, String... strings) { + return (Long) execute((Jedis jedis) -> jedis.rpush(key, strings)); + } + + @Override + public String set(String key, String value) { + return (String) execute((Jedis jedis) -> jedis.set(key, value)); + } + + @Override + public String set(byte[] key, byte[] value) { + return (String) execute((Jedis jedis) -> jedis.set(key, value)); + } + + @Override + public String set(String key, String value, SetParams params) { + return (String) execute((Jedis jedis) -> jedis.set(key, value, params)); + } + + @Override + public String set(byte[] key, byte[] value, SetParams params) { + return (String) execute((Jedis jedis) -> jedis.set(key, value, params)); + } + + @Override + public Long ttl(String key) { + return (Long) execute((Jedis jedis) -> jedis.ttl(key)); + } + + @Override + public Long ttl(byte[] key) { + return (Long) execute((Jedis jedis) -> jedis.ttl(key)); + } + + private Object execute(RedisCallback callback) { + try (Jedis jedis = pool.getResource()) { + return callback.doWithRedis(jedis); + } + } + + public interface RedisCallback { + Object doWithRedis(Jedis var1); + } +} diff --git a/src/main/java/org/mybatis/caches/redis/client/RedisClientBuilder.java b/src/main/java/org/mybatis/caches/redis/client/RedisClientBuilder.java new file mode 100644 index 0000000..c50cd60 --- /dev/null +++ b/src/main/java/org/mybatis/caches/redis/client/RedisClientBuilder.java @@ -0,0 +1,71 @@ +/** + * Copyright 2015-2020 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 + * + * http://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.redis.client; + +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisClusterHostAndPortMap; +import redis.clients.jedis.JedisPool; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocketFactory; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * @author Milton Lai(millton.lai@gmail.com) + */ +public class RedisClientBuilder { + private GenericRedisClient client; + + public RedisClientBuilder(GenericObjectPoolConfig poolConfig, String hosts, int connectionTimeout, int soTimeout, + int maxAttempts, String password, int database, String clientName, boolean ssl, SSLSocketFactory sslSocketFactory, + SSLParameters sslParameters, HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap hostAndPortMap) { + + if (hosts.indexOf(',') > 0) { + // cluster config + Set nodes = new LinkedHashSet<>(); + for (String host : hosts.split(",[ ]*")) { + String[] hostParts = host.split(":"); + if ((hostParts.length != 2) || !(hostParts[1].matches("\\d+"))) { + throw new RuntimeException("Invalid host name set for redis cluster: " + host); + } + nodes.add(new HostAndPort(hostParts[0], Integer.parseInt(hostParts[1]))); + } + JedisCluster cluster = new JedisCluster(nodes, connectionTimeout, soTimeout, maxAttempts, password, clientName, + poolConfig, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMap); + this.client = new ClusterRedisClient(cluster); + + } else { + // pool config + password = (password == null || password.trim().length() == 0) ? null : password.trim(); + + String[] hostParts = hosts.split(":"); + if ((hostParts.length != 2) || !(hostParts[1].matches("\\d+"))) { + throw new RuntimeException("Invalid host name set for redis cluster: " + hosts); + } + JedisPool pool = new JedisPool(poolConfig, hostParts[0], Integer.parseInt(hostParts[1]), connectionTimeout, + soTimeout, password, database, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier); + this.client = new PooledRedisClient(pool); + } + } + + public GenericRedisClient getClient() { + return client; + } +} diff --git a/src/test/java/org/mybatis/caches/redis/RedisConfigurationBuilderTest.java b/src/test/java/org/mybatis/caches/redis/RedisConfigurationBuilderTest.java index f5e6628..678a65b 100644 --- a/src/test/java/org/mybatis/caches/redis/RedisConfigurationBuilderTest.java +++ b/src/test/java/org/mybatis/caches/redis/RedisConfigurationBuilderTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 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. @@ -15,13 +15,9 @@ */ package org.mybatis.caches.redis; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; import org.mybatis.caches.redis.sslconfig.TestHostnameVerifier; import org.mybatis.caches.redis.sslconfig.TestSSLParameters; import org.mybatis.caches.redis.sslconfig.TestSSLSocketFactory; @@ -33,11 +29,11 @@ public void testDefaults() throws Exception { System.setProperty(RedisConfigurationBuilder.SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME, "no-such-file.properties"); RedisConfig redisConfig = RedisConfigurationBuilder.getInstance() .parseConfiguration(this.getClass().getClassLoader()); - assertEquals(JDKSerializer.class, redisConfig.getSerializer().getClass()); - assertFalse(redisConfig.isSsl()); - assertNull(redisConfig.getSslSocketFactory()); - assertNull(redisConfig.getSslParameters()); - assertNull(redisConfig.getHostnameVerifier()); + Assert.assertEquals(JDKSerializer.class, redisConfig.getSerializer().getClass()); + Assert.assertFalse(redisConfig.isSsl()); + Assert.assertNull(redisConfig.getSslSocketFactory()); + Assert.assertNull(redisConfig.getSslParameters()); + Assert.assertNull(redisConfig.getHostnameVerifier()); } @Test @@ -45,14 +41,14 @@ public void test1() throws Exception { System.setProperty(RedisConfigurationBuilder.SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME, "test1.properties"); RedisConfig redisConfig = RedisConfigurationBuilder.getInstance() .parseConfiguration(this.getClass().getClassLoader()); - assertEquals(KryoSerializer.class, redisConfig.getSerializer().getClass()); - assertTrue(redisConfig.isSsl()); - assertEquals(TestSSLSocketFactory.class, redisConfig.getSslSocketFactory().getClass()); - assertEquals(TestSSLParameters.class, redisConfig.getSslParameters().getClass()); - assertEquals(TestHostnameVerifier.class, redisConfig.getHostnameVerifier().getClass()); + Assert.assertEquals(KryoSerializer.class, redisConfig.getSerializer().getClass()); + Assert.assertTrue(redisConfig.isSsl()); + Assert.assertEquals(TestSSLSocketFactory.class, redisConfig.getSslSocketFactory().getClass()); + Assert.assertEquals(TestSSLParameters.class, redisConfig.getSslParameters().getClass()); + Assert.assertEquals(TestHostnameVerifier.class, redisConfig.getHostnameVerifier().getClass()); } - @AfterEach + @After public void after() { System.setProperty(RedisConfigurationBuilder.SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME, RedisConfigurationBuilder.REDIS_RESOURCE); diff --git a/src/test/java/org/mybatis/caches/redis/RedisTestCase.java b/src/test/java/org/mybatis/caches/redis/RedisTestCase.java index 5d57a3e..1b63178 100644 --- a/src/test/java/org/mybatis/caches/redis/RedisTestCase.java +++ b/src/test/java/org/mybatis/caches/redis/RedisTestCase.java @@ -1,5 +1,5 @@ /** - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 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. @@ -15,13 +15,10 @@ */ package org.mybatis.caches.redis; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; /** * Test with Ubuntu sudo apt-get install redis-server execute the test @@ -32,25 +29,27 @@ public final class RedisTestCase { private static RedisCache cache; - @BeforeAll - public static void newCache() { + @Before + public void newCache() { cache = new RedisCache(DEFAULT_ID); + cache.setTimeout(300); } @Test public void shouldDemonstrateCopiesAreEqual() { for (int i = 0; i < 1000; i++) { cache.putObject(i, i); - assertEquals(i, cache.getObject(i)); + + Assert.assertEquals(i, cache.getObject(i)); } } @Test public void shouldRemoveItemOnDemand() { cache.putObject(0, 0); - assertNotNull(cache.getObject(0)); + Assert.assertNotNull(cache.getObject(0)); cache.removeObject(0); - assertNull(cache.getObject(0)); + Assert.assertNull(cache.getObject(0)); } @Test @@ -58,28 +57,26 @@ public void shouldFlushAllItemsOnDemand() { for (int i = 0; i < 5; i++) { cache.putObject(i, i); } - assertNotNull(cache.getObject(0)); - assertNotNull(cache.getObject(4)); + Assert.assertNotNull(cache.getObject(0)); + Assert.assertNotNull(cache.getObject(4)); cache.clear(); - assertNull(cache.getObject(0)); - assertNull(cache.getObject(4)); + Assert.assertNull(cache.getObject(0)); + Assert.assertNull(cache.getObject(4)); } - @Test + @Test(expected = IllegalArgumentException.class) public void shouldNotCreateCache() { - assertThrows(IllegalArgumentException.class, () -> { - cache = new RedisCache(null); - }); + cache = new RedisCache(null); } @Test public void shouldVerifyCacheId() { - assertEquals("REDIS", cache.getId()); + Assert.assertEquals("REDIS", cache.getId()); } @Test public void shouldVerifyToString() { - assertEquals("Redis {REDIS}", cache.toString()); + Assert.assertEquals("Redis {REDIS}", cache.toString()); } @Test @@ -90,20 +87,26 @@ public void shouldDeleteExpiredCache() throws Exception { Thread.sleep(2000); cache.putObject(1, 1); // 2 secs : not expired yet - assertEquals(0, cache.getObject(0)); + Assert.assertEquals(0, cache.getObject(0)); Thread.sleep(2000); - // 4 secs : should be expired - assertNull(cache.getObject(0)); - assertNull(cache.getObject(1)); - // Make sure timeout is re-set + // 4 secs : (0) should be expired + Assert.assertNull(cache.getObject(0)); + Assert.assertNotNull(cache.getObject(1)); + Thread.sleep(2000); + // 4 secs : (1) should be expired + Assert.assertNull(cache.getObject(1)); + cache.putObject(2, 2); Thread.sleep(2000); // 2 secs : not expired yet cache.putObject(3, 3); - assertEquals(2, cache.getObject(2)); + Assert.assertEquals(2, cache.getObject(2)); + Thread.sleep(2000); + // 4 secs : (2) should be expired + Assert.assertNull(cache.getObject(2)); + Assert.assertNotNull(cache.getObject(3)); Thread.sleep(2000); // 4 secs : should be expired - assertNull(cache.getObject(2)); - assertNull(cache.getObject(3)); + Assert.assertNull(cache.getObject(3)); } } diff --git a/src/test/java/org/mybatis/caches/redis/SerializerTestCase.java b/src/test/java/org/mybatis/caches/redis/SerializerTestCase.java index fbf0c72..ff18127 100644 --- a/src/test/java/org/mybatis/caches/redis/SerializerTestCase.java +++ b/src/test/java/org/mybatis/caches/redis/SerializerTestCase.java @@ -1,5 +1,5 @@ /** - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2020 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. @@ -15,18 +15,19 @@ */ package org.mybatis.caches.redis; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; public class SerializerTestCase { @@ -35,7 +36,7 @@ public class SerializerTestCase { Serializer kryoSerializer; Serializer jdkSerializer; - @BeforeEach + @Before public void setup() { kryoSerializer = KryoSerializer.INSTANCE; jdkSerializer = JDKSerializer.INSTANCE; @@ -44,30 +45,30 @@ public void setup() { @Test public void testKryoUnserializeNull() { Object obj = kryoSerializer.unserialize(null); - assertNull(obj); + Assert.assertNull(obj); } @Test public void testJDKUnserializeNull() { Object obj = jdkSerializer.unserialize(null); - assertNull(obj); + Assert.assertNull(obj); } public void testKryoSerialize() { SimpleBeanStudentInfo rawSimpleBean = new SimpleBeanStudentInfo(); for (int i = 0; i != max; ++i) { - kryoSerializer.serialize(rawSimpleBean); + kryoSerializer.serialize(1, rawSimpleBean); } - byte[] serialBytes = kryoSerializer.serialize(rawSimpleBean); + byte[] serialBytes = kryoSerializer.serialize(1, rawSimpleBean); SimpleBeanStudentInfo unserializeSimpleBean = (SimpleBeanStudentInfo) kryoSerializer.unserialize(serialBytes); for (int i = 0; i != max; ++i) { kryoSerializer.unserialize(serialBytes); } - assertEquals(rawSimpleBean, unserializeSimpleBean); + Assert.assertEquals(rawSimpleBean, unserializeSimpleBean); } @@ -75,23 +76,23 @@ public void testKryoSerialize() { public void testKryoFallbackSerialize() throws IOException { SimpleBeanStudentInfo rawSimpleBean = new SimpleBeanStudentInfo(); - byte[] serialBytes = jdkSerializer.serialize(rawSimpleBean); + byte[] serialBytes = jdkSerializer.serialize(1, rawSimpleBean); SimpleBeanStudentInfo unserializeSimpleBean = (SimpleBeanStudentInfo) kryoSerializer.unserialize(serialBytes); - assertEquals(rawSimpleBean, unserializeSimpleBean); + Assert.assertEquals(rawSimpleBean, unserializeSimpleBean); } @Test public void testKryoUnserializeWithoutRegistry() throws IOException { SimpleBeanStudentInfo rawSimpleBean = new SimpleBeanStudentInfo(); - - byte[] serialBytes = kryoSerializer.serialize(rawSimpleBean); + byte[] serialBytes = kryoSerializer.serialize(1, rawSimpleBean); Kryo kryoWithoutRegisty = new Kryo(); - Input input = new Input(serialBytes); + byte[] stripped = Arrays.copyOf(serialBytes, serialBytes.length - 8); + Input input = new Input(stripped); SimpleBeanStudentInfo unserializeSimpleBean = (SimpleBeanStudentInfo) kryoWithoutRegisty.readClassAndObject(input); - assertEquals(rawSimpleBean, unserializeSimpleBean); + Assert.assertEquals(rawSimpleBean, unserializeSimpleBean); } @@ -118,10 +119,11 @@ public void testKryoUnserializeWithoutRegistryWithFile() throws IOException { while ((nRead = inputStream.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } + buffer.write(kryoSerializer.longToBytes(System.currentTimeMillis())); buffer.flush(); SimpleBeanCourseInfo unserializeSimpleBean = (SimpleBeanCourseInfo) kryoSerializer.unserialize(data); - assertEquals(rawSimpleBean, unserializeSimpleBean); + Assert.assertEquals(rawSimpleBean, unserializeSimpleBean); } @@ -130,23 +132,26 @@ public void testJDKSerialize() { SimpleBeanStudentInfo rawSimpleBean = new SimpleBeanStudentInfo(); for (int i = 0; i != max; ++i) { - jdkSerializer.serialize(rawSimpleBean); + long ts = Long.MAX_VALUE - i; + byte[] bytes = jdkSerializer.serialize(ts, rawSimpleBean); + Assert.assertEquals(jdkSerializer.getTimestamp(bytes), ts); + Assert.assertEquals(rawSimpleBean, jdkSerializer.unserialize(bytes)); } - byte[] serialBytes = jdkSerializer.serialize(rawSimpleBean); + byte[] serialBytes = jdkSerializer.serialize(1, rawSimpleBean); SimpleBeanStudentInfo unserializeSimpleBean = (SimpleBeanStudentInfo) jdkSerializer.unserialize(serialBytes); for (int i = 0; i != max; ++i) { jdkSerializer.unserialize(serialBytes); } - assertEquals(rawSimpleBean, unserializeSimpleBean); + Assert.assertEquals(rawSimpleBean, unserializeSimpleBean); } @Test public void testSerializeCofig() { RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration(); - assertEquals(JDKSerializer.class, redisConfig.getSerializer().getClass()); + Assert.assertEquals(JDKSerializer.class, redisConfig.getSerializer().getClass()); } } diff --git a/src/test/resources/redis.properties b/src/test/resources/redis.properties index ce57d8a..0d05cbd 100644 --- a/src/test/resources/redis.properties +++ b/src/test/resources/redis.properties @@ -1,5 +1,5 @@ # -# Copyright 2015-2018 the original author or authors. +# Copyright 2015-2020 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. @@ -14,10 +14,10 @@ # limitations under the License. # -redis.host=localhost -redis.port=6379 +redis.hosts=localhost:6379 redis.connectionTimeout=5000 redis.soTimeout=5000 +redis.maxAttempts=5 redis.password= redis.database=0 redis.clientName= @@ -30,3 +30,4 @@ redis.serializer=jdk #redis.sslSocketFactory= #redis.sslParameters= #redis.hostnameVerifier= +#redis.hostAndPortMap=