范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

巧用GenericObjectPool创建自定义对象池

  作者:京东物流 高圆庆 1 前言
  通常一个对象创建、销毁非常耗时的时候,我们不会频繁的创建和销毁它,而是考虑复用。复用对象的一种做法就是对象池,将创建好的对象放入池中维护起来,下次再用的时候直接拿池中已经创建好的对象继续用,这就是池化的思想。在java中,有很多池管理的概念,典型的如线程池,数据库连接池,socket连接池。本文章讲介绍apache提供的通用对象池框架GenericObjectPool,以及基于GenericObjectPool实现的sftp连接池在国际物流调度履约系统中的应用。 2 GenericObjectPool剖析
  Apache Commons Pool是一个对象池的框架,他提供了一整套用于实现对象池化的API。它提供了三种对象池:GenericKeyedObjectPool,SoftReferenceObjectPool和GenericObjectPool,其中GenericObjectPool是我们最常用的对象池,内部实现也最复杂。GenericObjectPool的UML图如下所示:
  2.1 核心接口ObjectPool
  从图中可以看出,GenericObjectPool实现了ObjectPool接口,而ObjectPool就是对象池的核心接口,它定义了一个对象池应该实现的行为。 addObject方法:往池中添加一个对象 borrowObject方法:从池中借走到一个对象 returnObject方法:把对象归还给对象池 invalidateObject:验证对象的有效性 getNumIdle:返回对象池中有多少对象是空闲的,也就是能够被借走的对象的数量。 getNumActive:返回对象池中有对象对象是活跃的,也就是已经被借走的,在使用中的对象的数量。 clear:清理对象池。注意是清理不是清空,该方法要求的是,清理所有空闲对象,释放相关资源。 close:关闭对象池。这个方法可以达到清空的效果,清理所有对象以及相关资源。 2.2 对象工厂BasePooledObjectFactory
  对象的创建需要通过对象工厂来创建,对象工厂需要实现BasePooledObjectFactory接口。ObjectPool接口中往池中添加一个对象,就需要使用对象工厂来创建一个对象。该接口说明如下: public interface PooledObjectFactory {    /**    * 创建一个可由池提供服务的实例,并将其封装在由池管理的PooledObject中。    */   PooledObject makeObject() throws Exception;   /**    * 销毁池不再需要的实例    */   void destroyObject(PooledObject p) throws Exception;    /**    * 确保实例可以安全地由池返回    */   boolean validateObject(PooledObject p);    /**    * 重新初始化池返回的实例    */   void activateObject(PooledObject p) throws Exception;    /**    * 取消初始化要返回到空闲对象池的实例    */   void passivateObject(PooledObject p) throws Exception; }2.3 配置类GenericObjectPoolConfig
  GenericObjectPoolConfig是封装GenericObject池配置的简单"结构",此类不是线程安全的;它仅用于提供创建池时使用的属性。大多数情况,可以使用GenericObjectPoolConfig提供的默认参数就可以满足日常的需求,GenericObjectPoolConfig是一个抽象类,实际应用中需要新建配置类,然后继承它。 2.4 工作原理流程构造方法
  当我们执行构造方法时,主要工作就是创建了一个存储对象的LinkedList类型容器,也就是概念意义上的"池" 从对象池中获取对象
  获取池中的对象是通过borrowObject()命令,源码比较复杂,简单而言就是去LinkedList中获取一个对象,如果不存在的话,要调用构造方法中第一个参数Factory工厂类的makeObject()方法去创建一个对象再获取,获取到对象后要调用validateObject方法判断该对象是否是可用的,如果是可用的才拿去使用。LinkedList容器减一 归还对象到线程池
  简单而言就是先调用validateObject方法判断该对象是否是可用的,如果可用则归还到池中,LinkedList容器加一,如果是不可以的则则调用destroyObject方法进行销毁
  上面三步就是最简单的流程,由于取和还的流程步骤都在borrowObject和returnObject方法中固定的,所以我们只要重写Factory工厂类的makeObject()和validateObject以及destroyObject方法即可实现最简单的池的管理控制,通过构造方法传入该Factory工厂类对象则可以创建最简单的对象池管理类。这算是比较好的解耦设计模式,借和还的流程如下图所示:
  3 开源框架如何使用GenericObjectPool
  redis的java客户端jedis就是基于Apache Commons Pool对象池的框架来实现的。 3.1 对象工厂类JedisFactory
  对象工厂类只需实现activateObject、destroyObject、makeObject、validateObject方法即可,源码如下: class JedisFactory implements PooledObjectFactory {     private final String host;     private final int port;     private final int timeout;     private final int newTimeout;     private final String password;     private final int database;     private final String clientName;      public JedisFactory(String host, int port, int timeout, String password, int database) {         this(host, port, timeout, password, database, (String)null);     }      public JedisFactory(String host, int port, int timeout, String password, int database, String clientName) {         this(host, port, timeout, timeout, password, database, clientName);     }      public JedisFactory(String host, int port, int timeout, int newTimeout, String password, int database, String clientName) {         this.host = host;         this.port = port;         this.timeout = timeout;         this.newTimeout = newTimeout;         this.password = password;         this.database = database;         this.clientName = clientName;     }      public void activateObject(PooledObject pooledJedis) throws Exception {         BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();         if (jedis.getDB() != (long)this.database) {             jedis.select(this.database);         }     }      public void destroyObject(PooledObject pooledJedis) throws Exception {         BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();         if (jedis.isConnected()) {             try {                 try {                     jedis.quit();                 } catch (Exception var4) {                 }                  jedis.disconnect();             } catch (Exception var5) {             }         }     }      public PooledObject makeObject() throws Exception {         Jedis jedis = new Jedis(this.host, this.port, this.timeout, this.newTimeout);         jedis.connect();         if (null != this.password) {             jedis.auth(this.password);         }          if (this.database != 0) {             jedis.select(this.database);         }          if (this.clientName != null) {             jedis.clientSetname(this.clientName);         }          return new DefaultPooledObject(jedis);     }      public void passivateObject(PooledObject pooledJedis) throws Exception {     }      public boolean validateObject(PooledObject pooledJedis) {         BinaryJedis jedis = (BinaryJedis)pooledJedis.getObject();          try {             return jedis.isConnected() && jedis.ping().equals("PONG");         } catch (Exception var4) {             return false;         }     } }3.2 配置类JedisPoolConfigpublic class JedisPoolConfig extends GenericObjectPoolConfig {     public JedisPoolConfig() {         this.setTestWhileIdle(true);         this.setMinEvictableIdleTimeMillis(60000L);         this.setTimeBetweenEvictionRunsMillis(30000L);         this.setNumTestsPerEvictionRun(-1);     } }4 国际物流履约系统中的应用
  在国际物流履约系统中,我们和客户交互文件经常使用sftp服务器,因为创建sftp服务器的连接比较耗时,所以基于Apache Commons Pool对象池的框架来实现的我们自己的sftp链接池。 4.1 sftp对象池
  SftpPool比较简单,直接继承GenericObjectPool。 public class SftpPool extends GenericObjectPool {     public SftpPool(SftpFactory factory, SftpPoolConfig config, SftpAbandonedConfig abandonedConfig) {         super(factory, config, abandonedConfig);     } }4.2 对象工厂SftpFactory
  这是基于Apache Commons Pool框架实现自定义对象池的核心类,代码如下: public class SftpFactory extends BasePooledObjectFactory {      private static final String CHANNEL_TYPE = "sftp";     private static Properties sshConfig = new Properties();     private String host;     private int port;     private String username;     private String password;      static {         sshConfig.put("StrictHostKeyChecking", "no");     }      @Override     public Sftp create() {         try {             JSch jsch = new JSch();             Session sshSession = jsch.getSession(username, host, port);             sshSession.setPassword(password);             sshSession.setConfig(sshConfig);             sshSession.connect();             ChannelSftp channel = (ChannelSftp) sshSession.openChannel(CHANNEL_TYPE);             channel.connect();             log.info("sftpFactory创建sftp");             return new Sftp(channel);         } catch (JSchException e) {             log.error("连接sftp失败:", e);             throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);         }     }      /**      * @param sftp 被包装的对象      * @return 对象包装器      */     @Override     public PooledObject wrap(Sftp sftp) {         return new DefaultPooledObject<>(sftp);     }      /**      * 销毁对象      * @param p 对象包装器      */     @Override     public void destroyObject(PooledObject p) {         log.info("开始销毁channelSftp");         if (p!=null) {             Sftp sftp = p.getObject();             if (sftp!=null) {                 ChannelSftp channelSftp = sftp.getChannelSftp();                 if (channelSftp!=null) {                     channelSftp.disconnect();                     log.info("销毁channelSftp成功");                 }             }         }     }      /**      * 检查连接是否可用      *      * @param p 对象包装器      * @return {@code true} 可用,{@code false} 不可用      */     @Override     public boolean validateObject(PooledObject p) {         if (p!=null) {             Sftp sftp = p.getObject();             if (sftp!=null) {                 try {                     sftp.getChannelSftp().cd("./");                     log.info("验证连接是否可用,结果为true");                     return true;                 } catch (SftpException e) {                     log.info("验证连接是否可用,结果为false",e);                     return false;                 }             }         }         log.info("验证连接是否可用,结果为false");         return false;     }      public static class Builder {         private String host;         private int port;         private String username;         private String password;         public SftpFactory build() {             return new SftpFactory(host, port, username, password);         }         public Builder host(String host) {             this.host = host;             return this;         }         public Builder port(int port) {             this.port = port;             return this;         }         public Builder username(String username) {             this.username = username;             return this;         }         public Builder password(String password) {             this.password = password;             return this;         }     } }4.3 配置类SftpPoolConfig
  配置类继承了GenericObjectPoolConfig,可继承该类的默认属性,也可自定义配置参数。 public class SftpPoolConfig extends GenericObjectPoolConfig {     public static class Builder {         private int maxTotal;         private int maxIdle;         private int minIdle;         private boolean lifo;         private boolean fairness;         private long maxWaitMillis;         private long minEvictableIdleTimeMillis;         private long evictorShutdownTimeoutMillis;         private long softMinEvictableIdleTimeMillis;         private int numTestsPerEvictionRun;         private EvictionPolicy evictionPolicy; // 仅2.6.0版本commons-pool2需要设置         private String evictionPolicyClassName;         private boolean testOnCreate;         private boolean testOnBorrow;         private boolean testOnReturn;         private boolean testWhileIdle;         private long timeBetweenEvictionRunsMillis;         private boolean blockWhenExhausted;         private boolean jmxEnabled;         private String jmxNamePrefix;         private String jmxNameBase;         public SftpPoolConfig build() {             SftpPoolConfig config = new SftpPoolConfig();             config.setMaxTotal(maxTotal);             config.setMaxIdle(maxIdle);             config.setMinIdle(minIdle);             config.setLifo(lifo);             config.setFairness(fairness);             config.setMaxWaitMillis(maxWaitMillis);             config.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);             config.setEvictorShutdownTimeoutMillis(evictorShutdownTimeoutMillis);             config.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);             config.setNumTestsPerEvictionRun(numTestsPerEvictionRun);             config.setEvictionPolicy(evictionPolicy);             config.setEvictionPolicyClassName(evictionPolicyClassName);             config.setTestOnCreate(testOnCreate);             config.setTestOnBorrow(testOnBorrow);             config.setTestOnReturn(testOnReturn);             config.setTestWhileIdle(testWhileIdle);             config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);             config.setBlockWhenExhausted(blockWhenExhausted);             config.setJmxEnabled(jmxEnabled);             config.setJmxNamePrefix(jmxNamePrefix);             config.setJmxNameBase(jmxNameBase);             return config;         } }4.4 SftpClient配置类
  读取配置文件,创建SftpFactory、SftpPoolConfig、SftpPool,代码如下: @Configuration @ConditionalOnClass(SftpPool.class) @EnableConfigurationProperties(SftpClientProperties.class) public class SftpClientAutoConfiguration {      @Bean     @ConditionalOnMissingBean     public ISftpClient sftpClient(SftpClientProperties sftpClientProperties) {         if (sftpClientProperties.isMultiple()) {             MultipleSftpClient multipleSftpClient = new MultipleSftpClient();             sftpClientProperties.getClients().forEach((name, properties) -> {                 SftpFactory sftpFactory = createSftpFactory(properties);                 SftpPoolConfig sftpPoolConfig = createSftpPoolConfig(properties);                 SftpAbandonedConfig sftpAbandonedConfig = createSftpAbandonedConfig(properties);                 SftpPool sftpPool = new SftpPool(sftpFactory, sftpPoolConfig, sftpAbandonedConfig);                 ISftpClient sftpClient = new SftpClient(sftpPool);                 multipleSftpClient.put(name, sftpClient);             });             return multipleSftpClient;         }         SftpFactory sftpFactory = createSftpFactory(sftpClientProperties);         SftpPoolConfig sftpPoolConfig = createSftpPoolConfig(sftpClientProperties);         SftpAbandonedConfig sftpAbandonedConfig = createSftpAbandonedConfig(sftpClientProperties);         SftpPool sftpPool = new SftpPool(sftpFactory, sftpPoolConfig, sftpAbandonedConfig);         return new SftpClient(sftpPool);     }      public SftpFactory createSftpFactory(SftpClientProperties properties) {         return new SftpFactory.Builder()                 .host(properties.getHost())                 .port(properties.getPort())                 .username(properties.getUsername())                 .password(properties.getPassword())                 .build();     }      public SftpPoolConfig createSftpPoolConfig(SftpClientProperties properties) {         SftpClientProperties.Pool pool = properties.getPool();         return new SftpPoolConfig.Builder()                 .maxTotal(pool.getMaxTotal())                 .maxIdle(pool.getMaxIdle())                 .minIdle(pool.getMinIdle())                 .lifo(pool.isLifo())                 .fairness(pool.isFairness())                 .maxWaitMillis(pool.getMaxWaitMillis())                 .minEvictableIdleTimeMillis(pool.getMinEvictableIdleTimeMillis())                 .evictorShutdownTimeoutMillis(pool.getEvictorShutdownTimeoutMillis())                 .softMinEvictableIdleTimeMillis(pool.getSoftMinEvictableIdleTimeMillis())                 .numTestsPerEvictionRun(pool.getNumTestsPerEvictionRun())                 .evictionPolicy(null)                 .evictionPolicyClassName(DefaultEvictionPolicy.class.getName())                 .testOnCreate(pool.isTestOnCreate())                 .testOnBorrow(pool.isTestOnBorrow())                 .testOnReturn(pool.isTestOnReturn())                 .testWhileIdle(pool.isTestWhileIdle())                 .timeBetweenEvictionRunsMillis(pool.getTimeBetweenEvictionRunsMillis())                 .blockWhenExhausted(pool.isBlockWhenExhausted())                 .jmxEnabled(pool.isJmxEnabled())                 .jmxNamePrefix(pool.getJmxNamePrefix())                 .jmxNameBase(pool.getJmxNameBase())                 .build();     }      public SftpAbandonedConfig createSftpAbandonedConfig(SftpClientProperties properties) {         SftpClientProperties.Abandoned abandoned = properties.getAbandoned();         return new SftpAbandonedConfig.Builder()                 .removeAbandonedOnBorrow(abandoned.isRemoveAbandonedOnBorrow())                 .removeAbandonedOnMaintenance(abandoned.isRemoveAbandonedOnMaintenance())                 .removeAbandonedTimeout(abandoned.getRemoveAbandonedTimeout())                 .logAbandoned(abandoned.isLogAbandoned())                 .requireFullStackTrace(abandoned.isRequireFullStackTrace())                 .logWriter(new PrintWriter(System.out))                 .useUsageTracking(abandoned.isUseUsageTracking())                 .build();     } }4.5 对象SftpClient
  SftpClient是实际工作的类,从SftpClient 中可获取到一个sftp链接,使用完成后,归还给sftpPool。SftpClient代码如下: public class SftpClient implements ISftpClient {      private SftpPool sftpPool;      /**      * 从sftp连接池获取连接并执行操作      *      * @param handler sftp操作      */     @Override     public void open(ISftpClient.Handler handler) {         Sftp sftp = null;         try {             sftp = sftpPool.borrowObject();             ISftpClient.Handler policyHandler = new DelegateHandler(handler);             policyHandler.doHandle(sftp);         } catch (Exception e) {             log.error("sftp异常:", e);             throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);         } finally {             if (sftp != null) {                 sftpPool.returnObject(sftp);             }         }     }      @AllArgsConstructor     static class DelegateHandler implements ISftpClient.Handler {          private ISftpClient.Handler target;          @Override         public void doHandle(Sftp sftp) {             try {                 target.doHandle(sftp);             } catch (Exception e) {                 log.error("sftp异常:", e);                 throw new BizException(ResultCodeEnum.SFTP_EXCEPTION);             }         }     } }4.6 实战代码示例
  通过sftp上传文件到XX服务器  //通过SFTP上传到XX ((MultipleSftpClient) sftpClient).choose("XX"); sftpClient.open(sftp -> {     boolean exist = sftp.isExist(inventoryPath);     if(!exist){         sftp.mkdirs(inventoryPath);     }     // 执行sftp操作     InputStream is = new FileInputStream(oneColumnCSVFile);     sftp.upload(inventoryPath, titleName, is);     log.info("inventory upload over"); });5 总结
  通过本文的介绍可以知道,Apache Commons Pool定义了一个对象池的行为,提供了可扩展的配置类和对象工厂,封装了对象创建、从池中获取对象、归还对象的核心流程。还介绍了开源框架Jedis是如何基于GenericObjectPool来实现的连接池。最后介绍了国际物流履约系统中是如何基于GenericObjectPool来管理Sftp连接的。
  掌握了GenericObjectPool的核心原理,我们就可以通过实现几个关键的接口,创建一个对象池管理工具,在项目中避免了对象的频繁创建和销毁,从而显著提升程序的性能。

始祖鸟对标爱马仕,安踏丁世忠飘了文丨吴大郎出品丨牛刀财经(niudaocaijing)始祖鸟是丁世忠布局全球化的重要棋子。安踏通过买买买实现了国内市场份额第一的目标,而始祖鸟代表的高端化运动品牌,则是丁世忠向全球A股如果股票一直下跌,散户死拿不放,你猜庄家会怎么洗?小故事大道理三个人要坐牢三年,看守给了他们三个人一个请求。美国人喜欢抽烟,他们要三盒。法国人是最浪漫的,他们需要美女作伴。犹太则表示,他想要一个能和外面世界交流的手机。三年之后,美今年广东这些大学报考人数爆了!专科竞争都这么大2022年,广东高考共录取77。34万名考生,本专科层次均超额完成招生计划。从本科批专科批的投档情况来看,大部分省内高校实现一次性满档,即投档没有缺额今年广东本专科批次超多高校一次江门开平这条古村成功入选2022年江门市乡村旅游示范村南都讯记者严亮实习生李振基10月24日,江门市文化广电旅游体育局公布2022年江门市乡村旅游示范村名单,开平市塘口镇仓前村成功入选。开平市塘口镇仓前村历史悠久,毗邻世界文化遗产开平胡彦斌郭采洁孙怡回归,再见爱人2探讨全新婚姻议题10月28日,芒果TV热播综艺再见爱人第二季发布主视觉海报并正式定档,同时也官宣了观察室嘉宾和三对真人秀夫妻。节目将于11月1日首播,延续第一季的旅行设定,三对面临婚姻危机的夫妻苏长高电新2022年前三季度净利润8397。16万元同比下降56。20中证智能财讯长高电新(002452)10月28日披露2022年第三季度报告。2022年前三季度,公司实现营业总收入7。98亿元,同比下降27。86归母净利润8397。16万元,同比深圳租房参考价来了4319个小区在列,南山租金最高10月28日,深圳市房地产和城市建设发展研究中心发布关于深圳市2022年度房屋租赁参考价格的通知。涉及4319个小区房南山商品房租赁参考价最高通知提到,该参考价格发布旨在为促进深圳辛巴直播带货让优质农产品直连全国消费者直播电商行业近年来的发展大家有目共睹,其现在还处在方兴未艾的发展阶段,未来发展潜力巨大。作为新兴行业,如果能为社会发展作出贡献,更是一件美事。都说有人的地方才有江湖,在乡村振兴的问收评沪指跌超2退守2900点,行业板块全线飘绿,两市逾80股跌停金融界10月28日消息周五A股三大指数低开,早盘沪指横盘,深成指创业板指震荡下挫,其中创业板指失守2300点,上证50指数续创阶段新低,但科创50指数逆市小幅上涨午后A股继续走弱,ThinkPad30周年即将到来,或将迎来众多创新性产品10月25日,ThinkPad官微发布了第一张30周年预热海报,从ThinkPad30周年新商业领导力以思考进化时代,三个标题签和海报可以推断出,此次30周年是围绕着ThinkPa这几家企业一直被误认为央企,其实不是中国有很多中字头开头的企业,咋一听名字以为是央企,其实不是,这几家企业一直被误认为央企1。中国雄安集团中国雄安集团有限公司成立于2017年7月18日,2017年6月29日,国务院批
苹果在iOS16。3Beta2中调整SOS紧急联络功能触发方式IT之家1月11日消息,苹果于本周二面向开发者推出了iOS16。3Beta2版本更新,对iPhone和iPad系统的部分功能进行了细节调整。其中值得注意的一个变化就是,苹果在本次更独家无网无电支付?数字人民币解锁新功能,可解应急场景燃眉之急你是否遇到过手机没电,但又着急支付的情况?如今,数字人民币已让手机无电无网支付成为可能。1月11日,北京商报记者独家获悉,数字人民币无网无电支付功能正式上线,在部分安卓手机用户中,扫描全能王照片高清修复功能上线,3秒清晰还原旧时记忆一年一度的春运大幕已经拉开,人们开始纷纷奔赴家乡和亲人们团聚。为了便利大家一起围炉阅读昔日一张张泛黄的照片,回味动人的情感故事,近日,合合信息公司旗下扫描全能王上线并优化了照片高清原来微信右上角的号这么强大,这3个功能你都知道吗?很实用朋友们大家好,我是小俊,一个专注于知识分享的博主,那今天小俊来给大家聊聊,微信中隐藏的实用功能,可以说这些功能有很多,那今天我们重点要聊的呢是微信右上角号的3个隐藏功能,很多人呢只iOS16。3更新,iOS17新功能曝光时隔一个月,苹果终于发布了新版本系统。今天凌晨,苹果发布了iOS16。3的第二个测试版更新,系统版本号为20D5035i。此前已经安装了iOS16。3测试版的小伙伴,现在就可以去升被严重低估的三款冷门旗舰手机,性能不输丐版iPhone14,性价比高这三款手机没那么多人知道,比较冷门,但是性能不输丐版iPhone14,性价比更高,还买什么iPhone?第一款一加AcePro电池容量为4800mAh,配备150W充电器,能够23耳夹式设计,开放式听音体验,sanag塞那Z50S真无线耳机拆解sanag塞那是一个来自英国的新锐无线蓝牙耳机品牌,也是目前市场上少数同时兼具骨传导和气传导耳机产品的品牌之一。我爱音频网此前分享过塞那旗下一款挂耳式气传导运动耳机SANAGA5S我国这7款口碑封神的白酒,全喝过的人不多,看看你喝过几款只要是品质能保证的白酒在我国的白酒市场上都有着比较不错的口碑,当然,这些白酒也不好找,所以说我在这里也给大家盘点出来了我国7款口碑封神的白酒,这些白酒全喝过的人不多,来看看你喝过几为你喝彩过年啦!跟着三位顶级大厨学两招!美味新年,共赴团圆!2023年的春节格外令人期待,许多在外奔波的游子都归心似箭,渴望在除夕夜与家人吃上一顿美味的年夜饭,享受这难得的温暖时光。本期为你喝彩回访了三位大师级厨师,为大家介绍三道简单美味,下饭神器涪陵榨菜如今的涪陵榨菜已走进千家万户,成为人们喜爱的下饭神器。本报记者黄汉鑫摄光明图片国家地理标志产品探秘啥子?榨菜还曾经是奢侈品?!看着记者吃惊的表情,重庆涪陵榨菜产业发展中心副主任陈林奶茶里放姜,茶百道百分茶都在上!姜汁饮品突然走红2023年第一个月,茶饮店都在推什么?我研究了50个品牌近期上新的357款新品后,发现了一个冬日上新的新主角姜。百分茶上新姜枣桂三冬暖,用小黄姜汁搭配枣泥龙眼,被称为驱寒暖胃之光,