flea-frame-cache使用之Redis接入 源代碼
java
<!-- Java redis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.0.1</version> </dependency>
spring-context-4.3.18.RELEASE.jargithub
<!-- Spring相關 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.18.RELEASE</version> </dependency>
spring-context-support-4.3.18.RELEASE.jarredis
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>4.3.18.RELEASE</version> </dependency>
可參考筆者的這篇博文 Memcached接入,再也不贅述。算法
可參考筆者的這篇博文 Memcached接入,再也不贅述。spring
/** * <p> Redis客戶端對外接口 </p> * * @author huazie */ public interface RedisClient { /** * <p> 往Redis塞數據 </p> * * @param key 數據鍵 * @param value 數據值 * @return 狀態碼 (OK :成功) */ String set(final String key, final String value); /** * <p> 往Redis賽數據(用於序列化對象) </p> * * @param key 數據鍵 * @param value 數據值 * @return 狀態碼 (OK :成功) */ String set(final byte[] key, final byte[] value); /** * <p> 往Redis塞數據 (能夠帶失效時間) </p> * <p> 注意 : (單位:s)</p> * * @param key 數據鍵 * @param value 數據值 * @param expiry 失效時間(單位:s) * @return 狀態碼 (OK :成功) */ String set(final String key, final String value, final int expiry); /** * <p> 往Redis塞數據 (能夠帶失效時間,用於序列化對象) </p> * <p> 注意 : (單位:s)</p> * * @param key 數據鍵 * @param value 數據值 * @param expiry 失效時間(單位:s) * @return 狀態碼 (OK :成功) */ String set(final byte[] key, final byte[] value, final int expiry); /** * <p> 往Redis塞數據 (能夠帶失效時間) </p> * <p> 注意:(單位:ms) </p> * * @param key 數據鍵 * @param value 數據值 * @param expiry 失效時間(單位:ms) * @return 狀態碼 (OK :成功) */ String set(final String key, final String value, final long expiry); /** * <p> 往Redis塞數據 (能夠帶失效時間,用於序列化對象) </p> * <p> 注意:(單位:ms) </p> * * @param key 數據鍵 * @param value 數據值 * @param expiry 失效時間(單位:ms) * @return 狀態碼 (OK :成功) */ String set(final byte[] key, final byte[] value, final long expiry); /** * <p> 往Redis塞數據 (帶參數) </p> * * @param key 數據鍵 * @param value 數據值 * @param params 參數 * @return 狀態碼 (OK :成功) */ String set(final String key, final String value, SetParams params); /** * <p> 往Redis塞數據 (帶參數,用於序列化對象) </p> * * @param key 數據鍵 * @param value 數據值 * @param params 參數 * @return 狀態碼 (OK :成功) */ String set(final byte[] key, final byte[] value, SetParams params); /** * <p> 從Redis取數據 </p> * * @param key 數據鍵 * @return 數據值 */ String get(final String key); /** * <p> 從Redis取數據(用於獲取序列化對象) </p> * * @param key 數據鍵 * @return 數據值 */ byte[] get(final byte[] key); /** * <p> 從Redis中刪除數據 </p> * * @param key 數據鍵 * @return 被刪除key的數量 */ Long del(final String key); /** * <p> 獲取數據所在的Redis服務器ip(主機地址+端口) </p> * * @param key 數據鍵 * @return 當前數據所在的Redis服務器ip */ String getLocation(final String key); /** * <p> 獲取數據所在的Redis服務器ip(主機地址+端口) </p> * * @param key 數據鍵(字節數組) * @return 當前數據所在的Redis服務器ip */ String getLocation(final byte[] key); /** * <p> 獲取數據所在的Redis服務器主機 </p> * * @param key 數據鍵 * @return 數據所在的Redis服務器主機 */ String getHost(final String key); /** * <p> 獲取數據所在的Redis服務器主機 </p> * * @param key 數據鍵(字節數組) * @return 數據所在的Redis服務器主機 */ String getHost(final byte[] key); /** * <p> 獲取數據所在的Redis服務器主機端口 </p> * * @param key 數據鍵 * @return 數據所在的Redis服務器主機端口 */ Integer getPort(final String key); /** * <p> 獲取數據所在的Redis服務器主機端口 </p> * * @param key 數據鍵(字節數組) * @return 數據所在的Redis服務器主機端口 */ Integer getPort(final byte[] key); /** * <p> 獲取數據所在的客戶端類 </p> * * @param key 數據鍵 * @return 數據所在的客戶端類 */ Client getClient(final String key); /** * <p> 獲取數據所在的客戶端類 </p> * * @param key 數據鍵 * @return 數據所在的客戶端類 */ Client getClient(final byte[] key); /** * <p> 獲取分佈式Redis集羣客戶端鏈接池 </p> * * @return 分佈式Redis集羣客戶端鏈接池 */ ShardedJedisPool getJedisPool(); /** * <p> 設置分佈式Redis集羣客戶端 </p> * * @param shardedJedis 分佈式Redis集羣客戶端 */ void setShardedJedis(ShardedJedis shardedJedis); /** * <p> 獲取分佈式Redis集羣客戶端 </p> * * @return 分佈是Redis集羣客戶端 */ ShardedJedis getShardedJedis(); /** * <p> 獲取鏈接池名 </p> * * @return 鏈接池名 */ String getPoolName(); /** * <p> 設置鏈接池名 </p> * * @param poolName 鏈接池名 */ void setPoolName(String poolName); }
該類實現 RedisClient 接口, 其中分佈式Jedis鏈接池 ShardedJedisPool 用於獲取分佈式Jedis對象 ShardedJedis, ShardedJedis能夠自行根據初始化的算法,計算當前傳入的數據鍵在某一臺初始化的 Redis 服務器上,從而操做對數據的添加,查找,刪除功能。數組
/** * <p> Flea Redis客戶端類 </p> * * @author huazie */ public class FleaRedisClient implements RedisClient { private ShardedJedisPool shardedJedisPool; // 分佈式Jedis鏈接池 private ShardedJedis shardedJedis; // 分佈式Jedis對象 private String poolName; // 鏈接池名 /** * <p> Redis客戶端構造方法 (默認) </p> */ private FleaRedisClient() { this(null); } /** * <p> Redis客戶端構造方法(指定鏈接池名)</p> * * @param poolName 鏈接池名 */ private FleaRedisClient(String poolName) { this.poolName = poolName; init(); } /** * <p> 初始化分佈式Jedis鏈接池 </p> */ private void init() { if (StringUtils.isBlank(poolName)) { poolName = CommonConstants.FleaPoolConstants.DEFAULT_POOL_NAME; shardedJedisPool = RedisPool.getInstance().getJedisPool(); } else { shardedJedisPool = RedisPool.getInstance(poolName).getJedisPool(); } } @Override public String set(final String key, final String value) { return shardedJedis.set(key, value); } @Override public String set(byte[] key, byte[] value) { return shardedJedis.set(key, value); } @Override public String set(final String key, final String value, final int expiry) { return shardedJedis.setex(key, expiry, value); } @Override public String set(byte[] key, byte[] value, int expiry) { return shardedJedis.setex(key, expiry, value); } @Override public String set(String key, String value, long expiry) { return shardedJedis.psetex(key, expiry, value); } @Override public String set(byte[] key, byte[] value, long expiry) { return shardedJedis.psetex(key, expiry, value); } @Override public String set(final String key, final String value, final SetParams params) { return shardedJedis.set(key, value, params); } @Override public String set(byte[] key, byte[] value, SetParams params) { return shardedJedis.set(key, value, params); } @Override public String get(final String key) { return shardedJedis.get(key); } @Override public byte[] get(byte[] key) { return shardedJedis.get(key); } @Override public Long del(final String key) { return shardedJedis.del(key); } @Override public String getLocation(final String key) { return getLocationByKey(key); } @Override public String getLocation(byte[] key) { return getLocationByKey(key); } @Override public String getHost(final String key) { return getHostByKey(key); } @Override public String getHost(byte[] key) { return getHostByKey(key); } @Override public Integer getPort(final String key) { return getPortByKey(key); } @Override public Integer getPort(byte[] key) { return getPortByKey(key); } @Override public Client getClient(String key) { return getClientByKey(key); } @Override public Client getClient(byte[] key) { return getClientByKey(key); } /** * <p> 獲取數據所在的Redis服務器ip(主機地址+端口) </p> * * @param key 數據鍵 * @return 當前數據所在的Redis服務器ip */ private String getLocationByKey(Object key) { StringBuilder location = new StringBuilder(); Client client = getClientByKey(key); if (ObjectUtils.isNotEmpty(client)) { location.append(client.getHost()).append(CommonConstants.SymbolConstants.COLON).append(client.getPort()); } return location.toString(); } /** * <p> 獲取數據所在的Redis服務器主機 </p> * * @param key 數據鍵 * @return 數據所在的Redis服務器主機 */ private String getHostByKey(Object key) { Client client = getClientByKey(key); if (ObjectUtils.isNotEmpty(client)) { return client.getHost(); } return null; } /** * <p> 獲取數據所在的Redis服務器主機端口 </p> * * @param key 數據鍵 * @return 數據所在的Redis服務器主機端口 */ private Integer getPortByKey(Object key) { Client client = getClientByKey(key); if (ObjectUtils.isNotEmpty(client)) { return client.getPort(); } return null; } /** * <p> 獲取客戶端類 </p> * * @param key 數據鍵 * @return 客戶端類 */ private Client getClientByKey(Object key) { Client client = null; if (ObjectUtils.isNotEmpty(key)) { if (key instanceof String) { client = shardedJedis.getShard(key.toString()).getClient(); } else if (key instanceof byte[]) { client = shardedJedis.getShard((byte[]) key).getClient(); } } return client; } @Override public ShardedJedisPool getJedisPool() { return shardedJedisPool; } @Override public void setShardedJedis(ShardedJedis shardedJedis) { this.shardedJedis = shardedJedis; } @Override public ShardedJedis getShardedJedis() { return shardedJedis; } @Override public String getPoolName() { return poolName; } @Override public void setPoolName(String poolName) { this.poolName = poolName; init(); } /** * <p> 內部建造者類 </p> * * @author huazie */ static class Builder { private String poolName; // 鏈接池名 /** * <p> 默認構造器 </p> */ Builder() { } /** * <p> 指定鏈接池的構造器 </p> * * @param poolName 鏈接池名 */ Builder(String poolName) { this.poolName = poolName; } /** * <p> 構建Redis客戶端對象 </p> * * @return Redis客戶端 */ RedisClient build() { if (StringUtils.isBlank(poolName)) { return new FleaRedisClient(); } else { return new FleaRedisClient(poolName); } } } }
該類的構造函數初始化邏輯,能夠看出咱們使用了 RedisPool, 下面來介紹一下。緩存
RedisPool 用於Redis相關配置信息的初始化,其中重點是獲取分佈式Jedis鏈接池 ShardedJedisPool ,該類其中一個構造方法以下:bash
/** * @param poolConfig 鏈接池配置信息 * @param shards Jedis分佈式服務器列表 * @param algo 分佈式算法 */ public ShardedJedisPool(final GenericObjectPoolConfig poolConfig, List<JedisShardInfo> shards, Hashing algo)
/** * <p> Flea Redis 鏈接池 </p> * * @author huazie */ public class RedisPool { private static final ConcurrentMap<String, RedisPool> redisPools = new ConcurrentHashMap<>(); private String poolName; // 鏈接池名 private ShardedJedisPool shardedJedisPool; // 分佈式Jedis鏈接池 private RedisPool(String poolName) { this.poolName = poolName; } /** * <p> 獲取Redis鏈接池實例 (指定鏈接池名)</p> * * @param poolName 鏈接池名 * @return Redis鏈接池實例對象 */ public static RedisPool getInstance(String poolName) { if (!redisPools.containsKey(poolName)) { synchronized (redisPools) { if (!redisPools.containsKey(poolName)) { RedisPool redisPool = new RedisPool(poolName); redisPools.putIfAbsent(poolName, redisPool); } } } return redisPools.get(poolName); } /** * <p> 獲取Redis鏈接池實例 (默認) </p> * * @return Redis鏈接池實例對象 */ public static RedisPool getInstance() { return getInstance(CommonConstants.FleaPoolConstants.DEFAULT_POOL_NAME); } /** * <p> 默認初始化 </p> */ void initialize() { // ...省略初始化的代碼 } /** * <p> 初始化 (非默認鏈接池) </p> * * @param cacheServerList 緩存服務器集 * @param cacheParams 緩存參數 * @since 1.0.0 */ void initialize(List<CacheServer> cacheServerList, CacheParams cacheParams) { // ...省略初始化的代碼 } /** * <p> 分佈式Redis集羣客戶端鏈接池 </p> * * @return 分佈式Redis集羣客戶端鏈接池 */ public ShardedJedisPool getJedisPool() { if (ObjectUtils.isEmpty(shardedJedisPool)) { throw new RuntimeException("獲取分佈式Redis集羣客戶端鏈接池失敗:請先調用initialize初始化"); } return shardedJedisPool; } /** * <p> 獲取當前鏈接池名 </p> * * @return 鏈接池名 */ public String getPoolName() { return poolName; } }
flea-frame-cache讀取 redis.properties(Redis配置文件),用做初始化RedisPool服務器
# Redis配置 # Redis緩存所屬系統名 redis.systemName=FleaFrame # Redis服務器地址 redis.server=127.0.0.1:10001,127.0.0.1:10002,127.0.0.1:10003 # Redis服務登陸密碼 redis.password=huazie123,huazie123,huazie123 # Redis服務器權重分配 redis.weight=1,1,1 # Redis客戶端socket鏈接超時時間 redis.connectionTimeout=2000 # Redis客戶端socket讀寫超時時間 redis.soTimeout=2000 # Redis分佈式hash算法 # 1 : MURMUR_HASH # 2 : MD5 redis.hashingAlg=1 # Redis客戶端鏈接池配置 # Redis客戶端Jedis鏈接池最大鏈接數 redis.pool.maxTotal=100 # Redis客戶端Jedis鏈接池最大空閒鏈接數 redis.pool.maxIdle=10 # Redis客戶端Jedis鏈接池最小空閒鏈接數 redis.pool.minIdle=0 # Redis客戶端Jedis鏈接池獲取鏈接時的最大等待毫秒數 redis.pool.maxWaitMillis=2000
該類繼承抽象Flea緩存類 AbstractFleaCache ,其構造方法可見如須要傳入Redis客戶端 RedisClient ,相關使用下面介紹:
/** * <p> Redis Flea緩存類 </p> * * @author huazie */ public class RedisFleaCache extends AbstractFleaCache { private RedisClient redisClient; // Redis客戶端 /** * <p> 帶參數的構造方法,初始化Redis Flea緩存類 </p> * * @param name 緩存主關鍵字 * @param expiry 失效時長 * @param redisClient Redis客戶端 */ public RedisFleaCache(String name, long expiry, RedisClient redisClient) { super(name, expiry); this.redisClient = redisClient; cache = CacheEnum.Redis; } @Override public Object getNativeValue(String key) { if (LOGGER.isDebugEnabled()) { LOGGER.debug1(new Object() {}, "KEY = {}", key); } // 反序列化 return ObjectUtils.deserialize(redisClient.get(key.getBytes())); } @Override public void putNativeValue(String key, Object value, long expiry) { // 序列化 if (ObjectUtils.isNotEmpty(value)) { byte[] valueBytes = ObjectUtils.serialize(value); if (expiry == CommonConstants.NumeralConstants.ZERO) { redisClient.set(key.getBytes(), valueBytes); } else { redisClient.set(key.getBytes(), valueBytes, (int) expiry); } } } @Override public void deleteNativeValue(String key) { redisClient.del(key); } @Override public String getSystemName() { return RedisConfig.getConfig().getSystemName(); } }
可參考筆者的這篇博文 Memcached接入,再也不贅述。
該類繼承抽象Flea緩存管理類 AbstractFleaCacheManager,構造方法使用了Redis 客戶端代理類 RedisClientProxy 獲取Redis客戶端 RedisClient,可在 3.10 查看。newCache 方法返回的是 RedisFleaCache 的實例對象,每一類 Redis 緩存數據都對應了一個 RedisFleaCache 的實例對象。
/** * <p> Redis Flea緩存管理類 </p> * * @author huazie */ public class RedisFleaCacheManager extends AbstractFleaCacheManager { private RedisClient redisClient; /** * <p> 默認構造方法,初始化Redis Flea緩存管理類 </p> */ public RedisFleaCacheManager() { // 初始化默認鏈接池 RedisPool.getInstance().initialize(); redisClient = RedisClientProxy.getProxyInstance(); } @Override protected AbstractFleaCache newCache(String name, long expiry) { return new RedisFleaCache(name, expiry, redisClient); } }
Redis 客戶端代理類 RedisClientProxy 中 getProxyInstance() 返回 默認鏈接池的 Redis 客戶端,getProxyInstance(String poolName) 返回 指定鏈接池的 Redis 客戶端。它們返回的都是 Redis 客戶端接口類 RedisClient ,實際代理的是 Flea Redis 客戶端 FleaRedisClient。
/** * <p> RedisClient代理類 </p> * * @author huazie */ public class RedisClientProxy extends FleaProxy<RedisClient> { private final static ConcurrentMap<String, RedisClient> redisClients = new ConcurrentHashMap<String, RedisClient>(); /** * <p> 獲取RedisClient代理類 (默認)</p> * * @return RedisClient代理類 * @since 1.0.0 */ public static RedisClient getProxyInstance() { return getProxyInstance(CacheConstants.FleaCacheConstants.DEFAUTL_POOL_NAME); } /** * <p> 獲取RedisClient代理類 (指定鏈接池名)</p> * * @param poolName 鏈接池名 * @return RedisClient代理類 */ public static RedisClient getProxyInstance(String poolName) { if (!redisClients.containsKey(poolName)) { synchronized (redisClients) { if (!redisClients.containsKey(poolName)) { // 新建一個Flea Redis客戶端類, 用於被代理 RedisClient originRedisClient; if(CacheConstants.FleaCacheConstants.DEFAUTL_POOL_NAME.equals(poolName)) { originRedisClient = new FleaRedisClient(); } else { originRedisClient = new FleaRedisClient(poolName); } RedisClient proxyRedisClient = newProxyInstance(originRedisClient.getClass().getClassLoader(), originRedisClient.getClass().getInterfaces(), new RedisClientInvocationHandler(originRedisClient)); redisClients.put(poolName, proxyRedisClient); } } } return redisClients.get(poolName); } }
該類在 RedisClientProxy 中被調用,用於處理 Flea Redis 客戶端類相應方法被代理調用先後的其餘操做,本處主要實現代理前的分佈式 Jedis 對象 ShardedJedis 的獲取,代理後的分佈式 Jedis 對象 ShardedJedis 的關閉,歸還相關資源給分佈式 Jedis 鏈接池 ShardedJedisPool。
/** * <p> Redis客戶端調用處理類 </p> * * @author huazie */ public class RedisClientInvocationHandler extends FleaInvocationHandler { /** * <p> 帶參數的構造方法 </p> * * @param proxyObject 代理對象 */ RedisClientInvocationHandler(Object proxyObject) { super(proxyObject); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (ObjectUtils.isEmpty(proxyObject)) { throw new Exception("The proxyObject must be initialized"); } if (!(proxyObject instanceof RedisClient)) { throw new Exception("The proxyObject must implement RedisClient interface"); } RedisClient redisClient = (RedisClient) proxyObject; ShardedJedisPool jedisPool = redisClient.getJedisPool(); if (ObjectUtils.isNotEmpty(jedisPool)) { // 從分佈式Jedis鏈接池中獲取分佈式Jedis對象,並將其初始化給Redis客戶端類中的分佈式Jedis對象 redisClient.setShardedJedis(jedisPool.getResource()); } try { return super.invoke(proxy, method, args); } finally { ShardedJedis shardedJedis = redisClient.getShardedJedis(); if (ObjectUtils.isNotEmpty(shardedJedis)) { // 使用後,關閉鏈接 shardedJedis.close(); } } } }
哇,終於Redis接入差很少要完成了,下面一塊兒開始啓動單元測試吧
首先,這裏須要按照 Redis 配置文件中的地址部署相應的 Redis 服務,可參考筆者的 這篇博文。
@Test public void testRedisFleaCache() { try { AbstractFleaCacheManager manager = FleaCacheManagerFactory.getFleaCacheManager(CacheEnum.Redis.getName()); AbstractFleaCache cache = manager.getCache("fleaparadetail"); LOGGER.debug("Cache={}", cache); //#### 1. 簡單字符串 // cache.put("menu1", "huazie"); cache.get("menu1"); // cache.delete("menu1"); cache.getCacheKey(); LOGGER.debug(cache.getCacheName() + ">>>" + cache.getCacheDesc()); } catch (Exception e) { LOGGER.error("Exception:", e); } }
可參考筆者的這篇博文 Memcached接入,再也不贅述。
該類繼承抽象 Spring 緩存 AbstractSpringCache,用於對接 Spring; 從構造方法可見,該類初始化仍是使用 Redis Flea 緩存類 RedisFleaCache。
/** * <p> Redis Spring緩存類 </p> * * @author huazie */ public class RedisSpringCache extends AbstractSpringCache { /** * <p> 帶參數的構造方法,初始化Redis Spring緩存類 </p> * * @param name 緩存主關鍵字 * @param fleaCache 具體緩存實現 */ public RedisSpringCache(String name, IFleaCache fleaCache) { super(name, fleaCache); } /** * <p> 帶參數的構造方法,初始化Redis Spring緩存類 </p> * * @param name 緩存主關鍵字 * @param expiry 失效時長 * @param redisClient Redis客戶端 */ public RedisSpringCache(String name, long expiry, RedisClient redisClient) { this(name, new RedisFleaCache(name, expiry, redisClient)); } }
可參考筆者的這篇博文 Memcached接入,再也不贅述。
該類繼承抽象 Spring 緩存管理類 AbstractSpringCacheManager,用於對接Spring; 基本實現同 Redis Flea 緩存管理類 RedisFleaCacheManager,惟一不一樣在於 newCache 的實現。
/** * <p> Redis Spring緩存管理類 </p> * * @author huazie */ public class RedisSpringCacheManager extends AbstractSpringCacheManager { private RedisClient redisClient; /** * <p> 默認構造方法,初始化Redis Spring緩存管理類 </p> */ public RedisSpringCacheManager() { // 初始化默認鏈接池 RedisPool.getInstance().initialize(); redisClient = RedisClientProxy.getProxyInstance(); } @Override protected AbstractSpringCache newCache(String name, long expiry) { return new RedisSpringCache(name, expiry, redisClient); } }
<!-- 配置緩存管理RedisSpringCacheManager 配置緩存時間 configMap (key緩存對象名稱 value緩存過時時間) --> <bean id="redisSpringCacheManager" class="com.huazie.frame.cache.redis.RedisSpringCacheManager"> <property name="configMap"> <map> <entry key="fleaparadetail" value="86400"/> </map> </property> </bean> <!-- 開啓緩存 --> <cache:annotation-driven cache-manager="redisSpringCacheManager" proxy-target-class="true"/>
private ApplicationContext applicationContext; @Before public void init() { applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); LOGGER.debug("ApplicationContext={}", applicationContext); } @Test public void testRedisSpringCache() { try { AbstractSpringCacheManager manager = (RedisSpringCacheManager) applicationContext.getBean("redisSpringCacheManager"); LOGGER.debug("RedisCacheManager={}", manager); AbstractSpringCache cache = manager.getCache("fleaparadetail"); LOGGER.debug("Cache={}", cache); //#### 1. 簡單字符串 // cache.put("menu1", "huazie"); // cache.get("menu1"); // cache.get("menu1", String.class); //#### 2. 簡單對象(要是能夠序列化的對象) // String user = new String("huazie"); // cache.put("user", user); // LOGGER.debug(cache.get("user", String.class)); // cache.get("FLEA_RES_STATE"); cache.clear(); //#### 3. List塞對象 // List<String> userList = new ArrayList<String>(); // userList.add("huazie"); // userList.add("lgh"); // cache.put("user_list", userList); // LOGGER.debug(cache.get("user_list",userList.getClass()).toString()); } catch (Exception e) { LOGGER.error("Exception:", e); } }
好了, Redis 的接入工做已經所有完成了。到目前爲止,不管是Memcached的接入仍是 Redis的接入,都是單一的緩存接入,筆者的 下一篇博文 將介紹如何整合Memcached和Redis接入,以應對日益複雜的業務需求。 敬請期待!!!