欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過期時(shí)間及自動(dòng)刷新功能

 更新時(shí)間:2024年02月07日 09:31:02   作者:丫的都是昵稱已存在  
用過spring cache的朋友應(yīng)該會(huì)知道,Spring Cache默認(rèn)是不支持在@Cacheable上添加過期時(shí)間的,雖然可以通過配置緩存容器時(shí)統(tǒng)一指定,本文主要介紹了如何基于spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過期時(shí)間以及緩存即將到期自動(dòng)刷新,

1.前言

用過spring cache的朋友應(yīng)該會(huì)知道,Spring Cache默認(rèn)是不支持在@Cacheable上添加過期時(shí)間的,雖然可以通過配置緩存容器時(shí)統(tǒng)一指定。形如

@Bean
public CacheManager cacheManager(
        @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
   RedisCacheManager cacheManager= new RedisCacheManager(redisTemplate);
    cacheManager.setDefaultExpiration(60);
    Map<String,Long> expiresMap = new HashMap<>();
    expiresMap.put("customUser",30L);
    cacheManager.setExpires(expiresMap);
    return cacheManager;
}

但有時(shí)候我們會(huì)更習(xí)慣通過注解指定過期時(shí)間。今天我們就來聊一下如何擴(kuò)展@Cacheable實(shí)現(xiàn)緩存自動(dòng)過期以及緩存即將到期自動(dòng)刷新

2實(shí)現(xiàn)注解緩存過期前置知識(shí)

SpringCache包含兩個(gè)頂級接口,Cache和CacheManager,通過CacheManager可以去管理一堆Cache。因此我們要擴(kuò)展@Cacheable,就脫離不了對Cache和CacheManager進(jìn)行擴(kuò)展

其次要實(shí)現(xiàn)過期時(shí)間,首先是引入的緩存產(chǎn)品,他本身就要支持過期時(shí)間,比如引入的緩存為ConcurrentHashMap,他原本就是不支持過期時(shí)間,如果要擴(kuò)展,就要非常耗費(fèi)精力實(shí)現(xiàn)

3實(shí)現(xiàn)注解緩存過期

01方法一:通過自定義cacheNames方式

形如下

@Cacheable(cacheNames = "customUser#30", key = "#id")

通過#分隔,#后面部分代表過期時(shí)間(單位為秒)

實(shí)現(xiàn)邏輯步驟為:

1、自定義緩存管理器并繼承RedisCacheManager,同時(shí)重寫createRedisCache方法

示例:

public class CustomizedRedisCacheManager extends RedisCacheManager {
    public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }
    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        String[] array = StringUtils.delimitedListToStringArray(name, "#");
        name = array[0];
        if (array.length > 1) {
            long ttl = Long.parseLong(array[1]);
            cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl)); 
        }
        return super.createRedisCache(name, cacheConfig);
    }
}

2、將默認(rèn)的緩存管理器改成我們自定義的緩存管理器

示例:

@EnableCaching 
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1));
        CustomizedRedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory()), defaultCacheConfig);
        return redisCacheManager;
    }
}

通過如上2個(gè)步驟,即可實(shí)現(xiàn)緩存過期

02方法二:通過自定義派生@Cacheable注解

第一種方法的實(shí)現(xiàn)是簡單,但缺點(diǎn)是語義不直觀,因此得做好宣導(dǎo)以及wiki,不然對于新人來說,他可能都不知道cacheName用#分割是代表啥意思

方法二的實(shí)現(xiàn)邏輯步驟如下

1、自定義注解LybGeekCacheable

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Cacheable(cacheManager = CacheConstant.CUSTOM_CACHE_MANAGER,keyGenerator = CacheConstant.CUSTOM_CACHE_KEY_GENERATOR)
public @interface LybGeekCacheable {
  @AliasFor(annotation = Cacheable.class,attribute = "value")
  String[] value() default {};
  @AliasFor(annotation = Cacheable.class,attribute = "cacheNames")
  String[] cacheNames() default {};
  @AliasFor(annotation = Cacheable.class,attribute = "key")
  String key() default "";
  @AliasFor(annotation = Cacheable.class,attribute = "keyGenerator")
  String keyGenerator() default "";
  @AliasFor(annotation = Cacheable.class,attribute = "cacheResolver")
  String cacheResolver() default "";
  @AliasFor(annotation = Cacheable.class,attribute = "condition")
  String condition() default "";
  @AliasFor(annotation = Cacheable.class,attribute = "unless")
  String unless() default "";
  @AliasFor(annotation = Cacheable.class,attribute = "sync")
  boolean sync() default false;
   long expiredTimeSecond() default 0;
   long preLoadTimeSecond() default 0;
}

大部分注解和@Cacheable保持一致,新增expiredTimeSecond緩存過期時(shí)間以及緩存自動(dòng)刷新時(shí)間preLoadTimeSecond

2、自定義緩存管理器并繼承RedisCacheManager并重寫loadCaches和createRedisCache

public class CustomizedRedisCacheManager extends RedisCacheManager implements BeanFactoryAware {
    private Map<String, RedisCacheConfiguration> initialCacheConfigurations;
    private RedisTemplate cacheRedisTemplate;
    private RedisCacheWriter cacheWriter;
    private DefaultListableBeanFactory beanFactory;
    private RedisCacheConfiguration defaultCacheConfiguration;
    protected CachedInvocation cachedInvocation;
    public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations,RedisTemplate cacheRedisTemplate) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
        this.initialCacheConfigurations = initialCacheConfigurations;
        this.cacheRedisTemplate = cacheRedisTemplate;
        this.cacheWriter = cacheWriter;
        this.defaultCacheConfiguration = defaultCacheConfiguration;
        //采用spring事件驅(qū)動(dòng)亦可
        //EventBusHelper.register(this);
    }
    public Map<String, RedisCacheConfiguration> getInitialCacheConfigurations() {
        return initialCacheConfigurations;
    }
    @Override
    protected Collection<RedisCache> loadCaches() {
        List<RedisCache> caches = new LinkedList<>();
        for (Map.Entry<String, RedisCacheConfiguration> entry : getInitialCacheConfigurations().entrySet()) {
            caches.add(createRedisCache(entry.getKey(), entry.getValue()));
        }
        return caches;
    }
    @Override
    public RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
       CustomizedRedisCache customizedRedisCache = new CustomizedRedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfiguration);
       return customizedRedisCache;
    }
}

3、在spring bean初始化完成后,設(shè)置緩存過期時(shí)間,并重新初始化緩存。

Component
@Slf4j
public class CacheExpireTimeInit implements SmartInitializingSingleton, BeanFactoryAware {
    private DefaultListableBeanFactory beanFactory;
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (DefaultListableBeanFactory)beanFactory;
    }
    @Override
    public void afterSingletonsInstantiated() {
        Map<String, Object> beansWithAnnotation = beanFactory.getBeansWithAnnotation(Component.class);
        if(MapUtil.isNotEmpty(beansWithAnnotation)){
            for (Object cacheValue : beansWithAnnotation.values()) {
                ReflectionUtils.doWithMethods(cacheValue.getClass(), method -> {
                    ReflectionUtils.makeAccessible(method);
                    boolean cacheAnnotationPresent = method.isAnnotationPresent(LybGeekCacheable.class);
                    if(cacheAnnotationPresent){
                         LybGeekCacheable lybGeekCacheable = method.getAnnotation(LybGeekCacheable.class);
                          CacheHelper.initExpireTime(lybGeekCacheable);
                    }
                });
            }
            CacheHelper.initializeCaches();
        }
    }

注: 為啥要重新初始化緩存,主要是為了一開始默認(rèn)的是沒設(shè)置緩存過期,重新初始化是為了設(shè)置過期時(shí)間。為啥調(diào)用initializeCaches()這個(gè)方法,看下官方描述就知道了

/**
   * Initialize the static configuration of caches.
   * <p>Triggered on startup through {@link #afterPropertiesSet()};
   * can also be called to re-initialize at runtime.
   * @since 4.2.2
   * @see #loadCaches()
   */
  public void initializeCaches() {
    Collection<? extends Cache> caches = loadCaches();
    synchronized (this.cacheMap) {
      this.cacheNames = Collections.emptySet();
      this.cacheMap.clear();
      Set<String> cacheNames = new LinkedHashSet<>(caches.size());
      for (Cache cache : caches) {
        String name = cache.getName();
        this.cacheMap.put(name, decorateCache(cache));
        cacheNames.add(name);
      }
      this.cacheNames = Collections.unmodifiableSet(cacheNames);
    }
  }

他就是在運(yùn)行的時(shí)候,可以重新初始化緩存

4、將默認(rèn)的緩存管理器改成我們自定義的緩存管理器

@Bean(CacheConstant.CUSTOM_CACHE_MANAGER)
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory,RedisTemplate cacheRedisTemplate) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1));
        Map<String, RedisCacheConfiguration> initialCacheConfiguration = new HashMap<>();
        return new CustomizedRedisCacheManager(redisCacheWriter,defaultCacheConfig,initialCacheConfiguration,cacheRedisTemplate);
    }

5、測試

@LybGeekCacheable(cacheNames = "customUser", key = "#id",expiredTimeSecond = 30)
    public User getUserFromRedisByCustomAnno(String id){
        System.out.println("get user with id by custom anno: 【" + id + "】");
        Faker faker = Faker.instance(Locale.CHINA);
        return User.builder().id(id).username(faker.name().username()).build();
    }
@Test
    public void testCacheExpiredAndPreFreshByCustom() throws Exception{
        System.out.println(userService.getUserFromRedisByCustomAnno("1"));
    }

以上就是擴(kuò)展緩存過期的實(shí)現(xiàn)主要方式了,接下來我們來聊一下緩存自動(dòng)刷新

4緩存自動(dòng)刷新

一般來說,當(dāng)緩存失效時(shí),請求就會(huì)打到后端的數(shù)據(jù)庫上,此時(shí)可能就會(huì)造成緩存擊穿現(xiàn)象。因此我們在緩存即將過期時(shí)主動(dòng)刷新緩存,提高緩存的命中率,進(jìn)而提高性能。

spring4.3的@Cacheable提供了一個(gè)sync屬性。當(dāng)緩存失效后,為了避免多個(gè)請求打到數(shù)據(jù)庫,系統(tǒng)做了一個(gè)并發(fā)控制優(yōu)化,同時(shí)只有一個(gè)線程會(huì)去數(shù)據(jù)庫取數(shù)據(jù)其它線程會(huì)被阻塞

5緩存即將到期自動(dòng)刷新

1、封裝緩存注解對象CachedInvocation

/**
 * @description: 標(biāo)記了緩存注解的方法類信息,用于主動(dòng)刷新緩存時(shí)調(diào)用原始方法加載數(shù)據(jù)
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public final class CachedInvocation {
    private CacheMetaData metaData;
    private Object targetBean;
    private Method targetMethod;
    private Object[] arguments;
    public Object invoke()
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        final MethodInvoker invoker = new MethodInvoker();
        invoker.setTargetObject(this.getTargetBean());
        invoker.setArguments(this.getArguments());
        invoker.setTargetMethod(this.getTargetMethod().getName());
        invoker.prepare();
        return invoker.invoke();
    }
}

2、編寫一個(gè)獲取即將到期時(shí)間參數(shù)切面,并進(jìn)行事件發(fā)布調(diào)用對象CachedInvocation

@Component
@Aspect
@Slf4j
@Order(2)
public class LybGeekCacheablePreLoadAspect {
    @Autowired
    private ApplicationContext applicationContext;
    @SneakyThrows
    @Around(value = "@annotation(lybGeekCacheable)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint,LybGeekCacheable lybGeekCacheable){
        buildCachedInvocationAndPushlish(proceedingJoinPoint,lybGeekCacheable);
        Object result = proceedingJoinPoint.proceed();
        return result;
    }
    private void buildCachedInvocationAndPushlish(ProceedingJoinPoint proceedingJoinPoint,LybGeekCacheable lybGeekCacheable){
        Method method = this.getSpecificmethod(proceedingJoinPoint);
        String[] cacheNames = getCacheNames(lybGeekCacheable);
        Object targetBean = proceedingJoinPoint.getTarget();
        Object[] arguments = proceedingJoinPoint.getArgs();
        KeyGenerator keyGenerator = SpringUtil.getBean(CacheConstant.CUSTOM_CACHE_KEY_GENERATOR,KeyGenerator.class);
        Object key = keyGenerator.generate(targetBean, method, arguments);
        CachedInvocation cachedInvocation = CachedInvocation.builder()
                .arguments(arguments)
                .targetBean(targetBean)
                .targetMethod(method)
                .metaData(CacheMetaData.builder()
                        .cacheNames(cacheNames)
                        .key(key)
                        .expiredTimeSecond(lybGeekCacheable.expiredTimeSecond())
                        .preLoadTimeSecond(lybGeekCacheable.preLoadTimeSecond())
                        .build()
                )
                .build();
      // EventBusHelper.post(cachedInvocation);
        applicationContext.publishEvent(cachedInvocation);
    }

3、自定義緩存管理器,接收CachedInvocation

示例

public class CustomizedRedisCacheManager extends RedisCacheManager implements BeanFactoryAware {
    //@Subscribe
    @EventListener
    private void doWithCachedInvocationEvent(CachedInvocation cachedInvocation){
        this.cachedInvocation = cachedInvocation;
    }

4、自定義cache并重寫get方法

@Slf4j
public class CustomizedRedisCache extends RedisCache {
    private ReentrantLock lock = new ReentrantLock();
    public CustomizedRedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
        super(name, cacheWriter,cacheConfig);
    }
    @Override
    @Nullable
    public ValueWrapper get(Object key) {
        ValueWrapper valueWrapper = super.get(key);
        CachedInvocation cachedInvocation = CacheHelper.getCacheManager().getCachedInvocation();
        long preLoadTimeSecond = cachedInvocation.getMetaData().getPreLoadTimeSecond();
        if(ObjectUtil.isNotEmpty(valueWrapper) && preLoadTimeSecond > 0){
            String cacheKey = createCacheKey(key);
            RedisTemplate cacheRedisTemplate = CacheHelper.getCacheManager().getCacheRedisTemplate();
            Long ttl = cacheRedisTemplate.getExpire(cacheKey, TimeUnit.SECONDS);
            if(ObjectUtil.isNotEmpty(ttl) && ttl <= preLoadTimeSecond){
                log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}",cacheKey,ttl,preLoadTimeSecond);
                ThreadPoolUtils.execute(()->{
                     lock.lock();
                     try{
                         CacheHelper.refreshCache(super.getName());
                     }catch (Exception e){
                         log.error("{}",e.getMessage(),e);
                     }finally {
                         lock.unlock();
                     }
                });
            }
        }
        return valueWrapper;
    }
}

5、緩存即將到期主動(dòng)刷新緩存方法

public static void refreshCache(String cacheName){
        boolean isMatchCacheName = isMatchCacheName(cacheName);
        if(isMatchCacheName){
            CachedInvocation cachedInvocation = getCacheManager().getCachedInvocation();
            boolean invocationSuccess;
            Object computed = null;
            try {
                computed = cachedInvocation.invoke();
                invocationSuccess = true;
            } catch (Exception ex) {
                invocationSuccess = false;
                log.error(">>>>>>>>>>>>>>>>> refresh cache fail",ex.getMessage(),ex);
            }
            if (invocationSuccess) {
                    Cache cache = getCacheManager().getCache(cacheName);
                    if(ObjectUtil.isNotEmpty(cache)){
                        Object cacheKey = cachedInvocation.getMetaData().getKey();
                        cache.put(cacheKey, computed);
                        log.info(">>>>>>>>>>>>>>>>>>>> refresh cache with cacheName-->【{}】,key--> 【{}】 finished !",cacheName,cacheKey);
                    }
            }
        }
    }

6、測試

@LybGeekCacheable(cacheNames = "customUserName", key = "#username",expiredTimeSecond = 20,preLoadTimeSecond = 15)
    public User getUserFromRedisByCustomAnnoWithUserName(String username){
        System.out.println("get user with username by custom anno: 【" + username + "】");
        Faker faker = Faker.instance(Locale.CHINA);
        return User.builder().id(faker.idNumber().valid()).username(username).build();
    }
@Test
    public void testCacheExpiredAndPreFreshByCustomWithUserName() throws Exception{
        System.out.println(userService.getUserFromRedisByCustomAnnoWithUserName("zhangsan"));
        TimeUnit.SECONDS.sleep(5);
        System.out.println("sleep 5 second :" + userService.getUserFromRedisByCustomAnnoWithUserName("zhangsan"));
        TimeUnit.SECONDS.sleep(10);
        System.out.println("sleep 10 second :" + userService.getUserFromRedisByCustomAnnoWithUserName("zhangsan"));
        TimeUnit.SECONDS.sleep(5);
        System.out.println("sleep 5 second :" + userService.getUserFromRedisByCustomAnnoWithUserName("zhangsan"));
    }

6總結(jié)

本文主要介紹了如何基于spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過期時(shí)間以及緩存即將到期自動(dòng)刷新。

不知道有沒有朋友會(huì)有疑問,為啥@Cacheable不提供一個(gè)ttl屬性,畢竟也不是很難。在我看來,spring更多提供的是一個(gè)通用的規(guī)范和標(biāo)準(zhǔn),如果定義的緩存,本身不支持ttl,你在@Cacheable里面配置ttl就不合適了,有時(shí)候?qū)崿F(xiàn)一個(gè)組件或者框架,考慮的是不是能不能實(shí)現(xiàn),而是有沒有必要實(shí)現(xiàn),更多是一種權(quán)衡和取舍

轉(zhuǎn)自:聊聊如何基于spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過期時(shí)間以及自動(dòng)刷新-騰訊云開發(fā)者社區(qū)-騰訊云 (tencent.com)

到此這篇關(guān)于spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過期時(shí)間以及自動(dòng)刷新的文章就介紹到這了,更多相關(guān)spring @Cacheable緩存自動(dòng)過期時(shí)間內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 基于XML配置Spring的自動(dòng)裝配過程解析

    基于XML配置Spring的自動(dòng)裝配過程解析

    這篇文章主要介紹了基于XML配置Spring的自動(dòng)裝配過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-10-10
  • Java編程實(shí)現(xiàn)NBA賽事接口調(diào)用實(shí)例代碼

    Java編程實(shí)現(xiàn)NBA賽事接口調(diào)用實(shí)例代碼

    這篇文章主要介紹了Java編程實(shí)現(xiàn)NBA賽事接口調(diào)用實(shí)例代碼,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-11-11
  • java優(yōu)化if-else的11種方案

    java優(yōu)化if-else的11種方案

    If-Else結(jié)構(gòu)是一種常見的條件判斷語句,通過優(yōu)化If-Else結(jié)構(gòu),可以提高代碼的可讀性和執(zhí)行效率,本文主要介紹了java優(yōu)化if-else的11種方案,感興趣的可以了解一下
    2024-08-08
  • 一文搞懂Spring中@Autowired和@Resource的區(qū)別

    一文搞懂Spring中@Autowired和@Resource的區(qū)別

    @Autowired?和?@Resource?都是?Spring/Spring?Boot?項(xiàng)目中,用來進(jìn)行依賴注入的注解。它們都提供了將依賴對象注入到當(dāng)前對象的功能,但二者卻有眾多不同,并且這也是常見的面試題之一,所以我們今天就來盤它
    2022-08-08
  • 全面詳解java代碼重構(gòu)與設(shè)計(jì)模式

    全面詳解java代碼重構(gòu)與設(shè)計(jì)模式

    這篇文章主要為大家介紹了全面詳解java代碼重構(gòu)與設(shè)計(jì)模式的全面詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • Java中YYYY-MM-dd與yyyy-MM-dd的區(qū)別及跨年問題

    Java中YYYY-MM-dd與yyyy-MM-dd的區(qū)別及跨年問題

    YYYY-MM-dd可能會(huì)導(dǎo)致跨年周的日期被歸屬到錯(cuò)誤的年份, yyyy-MM-dd總是表示實(shí)際的日歷年份,無論日期所在的周是否跨年,本文就來介紹一下兩者的區(qū)別,感興趣的可以了解一下
    2024-01-01
  • Java反射通過Getter方法獲取對象VO的屬性值過程解析

    Java反射通過Getter方法獲取對象VO的屬性值過程解析

    這篇文章主要介紹了Java反射通過Getter方法獲取對象VO的屬性值過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-02-02
  • struts2配置靜態(tài)資源代碼詳解

    struts2配置靜態(tài)資源代碼詳解

    這篇文章主要介紹了struts2配置靜態(tài)資源的相關(guān)內(nèi)容,文中涉及了具體代碼介紹,需要的朋友可以參考下。
    2017-09-09
  • 總結(jié)Bean的三種自定義初始化和銷毀方法

    總結(jié)Bean的三種自定義初始化和銷毀方法

    這篇文章主要介紹了Bean的三種自定義初始化和銷毀方法,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-01-01
  • java生成彩色附logo二維碼

    java生成彩色附logo二維碼

    這篇文章主要為大家介紹了java生成帶logo的多彩二維碼,比一般二維碼顏色鮮艷,美觀,如何生成二維碼,下面小編為大家分享實(shí)現(xiàn)代碼,感興趣的小伙伴們可以參考一下
    2016-04-04

最新評論