最近需要封裝一個redis連接工具,需要根據配置來初始化,但是可能是集羣模式也可能是單機模式,如果用普通的方法寫的話可能是這樣的
public class RedisSingle {
public JedisPool jedisPool;
// 初始化
public void setValue(String key, int second, String value) {
Jedis resource = jedisPool.getResource();
resource.setex(key, second, value);
resource.close();
}
// 其他一些常用操作
}
public class RedisCluster {
public JedisCluster jedisCluster
// 初始化
public void setValue(String key, String value) {
jedisCluster.set(key, value);
}
// 其他一些常用操作
}
public class RedisClient {
private RedisSingle redisSingle;
private RedisCluster redisCluster;
public RedisClient getInstance() {
// 初始化
}
public void setValue(String key, String value) {
if(null == redisSingle) {
//...調用集羣客戶端
} else {
//...調用單機客戶端
}
}
}
這樣做的缺點顯而易見,就是代碼量比較大,一個方法需要實現三次,這個時候就可以使用代理模式來簡化代碼,Java中提供了動態代理的類,代理可以理解爲是一個攔截器,在調用代理的對象的方法時進行攔截,進入invoke方法來調用相應的方法,這樣代理的步驟其實就不那麼複雜了,核心部分就下面三步:
// 1.構造方法裏進行初始化對象
public RedisProxy(String config) {
//初始化爲相應的子類
if(StringUtil.isNotBlank(config)) {
redisInterface = new RedisSingle(XXX);
} else {
redisInterface = new RedisCluster(XXX);
}
}
// 2.對對象進行代理
(RedisInterface) Proxy
.newProxyInstance(RedisInterface.class.getClassLoader(), new Class[]{RedisInterface.class},
new RedisProxy(config))
// 3.重寫攔截的方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(redisInterface, args);
}
具體的原理限於水平沒有去仔細研究,回到我們的需求上,如果按照一般的動態代理好像還是要分別實現一下單機版和集羣版的代碼,然後再使用代理,比如下面這段代碼
// 接口
public interface RedisInterface {
void setValue(String key, String value);
String getValue(String key) throws Exception;
}
// 單機版
public class RedisSingle implements RedisInterface {
private JedisPool jedisPool;
public RedisSingle(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
@Override
public void setValue(String key, String value){
Jedis resource = jedisPool.getResource();
resource.set(key, value);
resource.close();
}
@Override
public String getValue(String key){
Jedis resource = jedisPool.getResource();
String s = resource.get(key);
resource.close();
return s;
}
}
// 集羣版
public class RedisCluster implements RedisInterface {
private RedisCluster redisCluster;
// 省略構造
@Override
public void setValue(String key, String value){
// 業務邏輯
}
@Override
public String getValue(String key) {
// 業務邏輯
}
}
// 代理
public class RedisProxy implements InvocationHandler {
private RedisInterface redisInterface;
// 省略構造
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(redisInterface, args);
}
/**
* 初始化一些配置
* @param config
*/
public RedisProxy(String config) {
//初始化爲相應的子類
if(StringUtil.isNotBlank(config)) {
redisInterface = new RedisSingle(XXX);
} else {
redisInterface = new RedisCluster(XXX);
}
}
public static RedisInterface getInstance(String config) {
return (RedisInterface) Proxy
.newProxyInstance(RedisInterface.class.getClassLoader(), new Class[]{RedisInterface.class},
new RedisProxy(config));
}
}
// 調用一下試試
public class RedisTest {
public static void main(String[] args) throws Exception {
RedisInterface asd = RedisProxy.getInstance(config);
asd.setValue("asdadf", "Asdfasdf");
System.out.println(asd.getValue("asdadf"));
}
}
// 輸出
Asdfasdf
Process finished with exit code 0
還有一種更簡單的方法,但是通用性比較弱,因爲他需要兩個接口的方法名字段都相同,但是在一些特定的情況下還是可以用的。這裏我爲了方便用了lombok的註解和spring註解,也可以刪掉註解,手動實現這些過程
// 定義一個接口分別集成JedisCommands, BinaryJedisCommands,用於代理
public interface RedisClient extends JedisCommands, BinaryJedisCommands {
// 這裏不需要實現方法
}
// 配置類,也可以不用定義
@Data
@Configuration
public class RedisConfig {
// 註解的方式讀取參數,會自動查找application.yml配置文件的redis.host的值
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private String port;
@Value("${redis.timeout}")
private String timeout;
@Value("${redis.password}")
private String password;
@Value("${redis.clusterNodes}")
private String clusterNodes;
}
// 代理類,核心邏輯
@Configuration
public class RedisFactory implements InvocationHandler {
// 定義兩個真正調用方法的對象,jedisPool還需要getResource一下再執行
private JedisPool jedisPool;
private JedisCluster jedisCluster;
// 上面定義的redis配置類
private RedisConfig redisConfig;
// 將方法緩存起來,第二次調用的時候查找方便一些
private Map<RedisMethodKey, Method> cacheMap;
// 構造,私有的即可,因爲只在下面的getRedisClient裏使用
private RedisFactory(RedisConfig redisConfig) {
cacheMap = new LinkedHashMap<>();
this.redisConfig = redisConfig;
int maxAttempts = 3;
int timeout = 300;
int connectionTimeout = 300;
if (StringUtil.isNotBlank(redisConfig.getTimeout())) {
timeout = Integer.parseInt(redisConfig.getTimeout());
}
// 集羣版
if (StringUtil.isNotBlank(redisConfig.getClusterNodes())) {
Set<HostAndPort> hostAndPorts = Arrays.stream(redisConfig.getClusterNodes().split(","))
.map(line -> {
String[] split = line.split(":");
return new HostAndPort(split[0], Integer.parseInt(split[1]));
}).collect(Collectors.toSet());
// 如果有密碼
if (StringUtil.isNotBlank(redisConfig.getPassword())) {
jedisCluster = new JedisCluster(hostAndPorts, connectionTimeout, timeout, maxAttempts,
redisConfig.getPassword(), new GenericObjectPoolConfig());
} else {
// 如果沒有密碼
jedisCluster = new JedisCluster(hostAndPorts, connectionTimeout, timeout, maxAttempts,
new GenericObjectPoolConfig());
}
} else {
// 單機版
jedisPool = new JedisPool(redisConfig.getHost(), Integer.parseInt(redisConfig.getPort()));
}
}
// 這個是代理類的調用,在這裏實際調用的是上面兩個對象的方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Method myMethod = null;
RedisMethodKey redisMethodKey = new RedisMethodKey(method.getName(),
method.getParameterTypes());
if (cacheMap.containsKey(redisMethodKey)) {
myMethod = cacheMap.get(redisMethodKey);
}
if (StringUtil.isNotBlank(redisConfig.getClusterNodes())) {
if (myMethod == null) {
myMethod = MethodUtils
.getMatchingMethod(jedisCluster.getClass(), method.getName(), method.getParameterTypes());
cacheMap.put(redisMethodKey, myMethod);
}
return myMethod.invoke(jedisCluster, args);
} else {
Jedis jedis = jedisPool.getResource();
if (myMethod == null) {
// 因爲沒有直接實現方法,而且方法來自兩個不同的接口,所以需要根據名稱,類型等來找到method之後再invoke,不能直接invoke
myMethod = MethodUtils
.getMatchingMethod(jedis.getClass(), method.getName(), method.getParameterTypes());
cacheMap.put(redisMethodKey, myMethod);
}
try {
Object invoke = myMethod.invoke(jedis, args);
return invoke;
} finally {
jedis.close();
}
}
}
// 返回代理後的redisClient對象
@Bean(name = "redisClient")
public RedisClient getRedisClient(RedisConfig redisConfig) {
return (RedisClient) Proxy
.newProxyInstance(RedisClient.class.getClassLoader(), new Class[]{RedisClient.class},
new RedisFactory(redisConfig));
}
// 內部類,用於緩存方法
@Data
@AllArgsConstructor
class RedisMethodKey {
private String methodName;
private Class<?>[] methodType;
}
}
// 調用這裏我用了Spring註解將redisClient注入到Spring裏面了,可以直接用@Autowired聲明調用就行了
到這就是我能想到的最簡單的封裝了,這裏的寫法跟其他的動態代理寫法不太一樣,主要有兩點原因,第一是這個代理雖然同時只能代理一個類,但是他可以代理多個類,第二是將代理的過程放在代理類中進行了,對外只提供了一個getRedisClient方法,這樣開發用起來的時候也會方便一些。