微服務緩存漫談之Guava Cache

原理

Cache 仿佛一直以來都是提高性能, 特別是I/O密集型服務提高性能的法寶

參見下面一組數字, 比較不同媒介的存取速度: 緩存 > 內存 > 網絡 > 磁盤
(來自 Google Fellow Jeff Dean 的 PPT )

  • 每個人都應知道的一組數字 :
存儲媒介 storage 性能 performance
L1 cache reference 0.5 ns
Branch mispredict 5 nsv
L2 cache reference 7 ns
Mutex lock/unlock 100 ns (25)
Main memory reference 100 ns
Compress 1K bytes with Zippy 10,000 ns (3,000)
Send 2K bytes over 1 Gbps network 20,000 ns
Read 1 MB sequentially from memory 250,000 ns
Round trip within same datacenter 500,000 ns
Disk seek 10,000,000 ns
Read 1 MB sequentially from network 10,000,000 ns
Read 1 MB sequentially from disk 30,000,000 ns (20,000,000)
Send packet CA->Netherlands->CA 150,000,000 ns

從上面的數據, 我們知道數據緩存在 CPU cache 和內存中存取是最快的, 磁盤最慢, 順序為

  1. 本地內存中的緩存數據
  2. 本地磁盤中的數據
  3. 遠程快速節(jié)點內存中的緩存數據
  4. 遠程快速節(jié)點磁盤中的數據
  5. 遠程慢速節(jié)點內存中的緩存數據
  6. 遠程慢速節(jié)點磁盤中的數據

第2, 第3取決于網絡和磁盤的存取速度, 順序可能會對調

遠程節(jié)點分兩種:
一種是在低延遲網絡中的遠程節(jié)點濒持,或者說同一個局域網或城域網中的比較近的節(jié)點补鼻,又或雖然相隔距離遠但是通過專用高速網絡互連的節(jié)點粤剧,RTT一般在50ms 以內

一種是在高延遲網絡中的遠程節(jié)點,比如跨地區(qū)终吼,跨網絡,RTT大于100ms的網絡,有時候,同一個城市不同的網絡運營商之前的網絡速度可能比不同城市同一個運營商之前速度更慢

Cache 的種類

  • 本地 Cache: 放在本地內存中的緩存, 典型代表就是內存中的 map
  • 遠程 Cache: 放在遠程服務器內存中的緩存, 比如 memcached, redis cache
  • 分布式 Cache: 它其實有兩種, 一種全部是遠程分布式 cache , 還有一種是本地和遠程cache 有同步機制, 存取都是在本地內存中

其實緩存無處不在, CPU 有L1/L2 緩存, 硬盤有讀寫緩存, 這里僅提及微服務常用的緩存

對于Cache 我們最關心的就是 Cache 的使用效率, 也就是命中率, 提高命中率的關鍵因素是

  1. Cache key size 緩存的鍵值數量
  2. Cache capacity 緩存的容量
  3. Cache lifetime 緩存的生命周期

Cache 不可能無限增長, 不可能永遠有效, 所以對于 Cache 的清除策略和失效策略要細細考量.
對于放在 Cache 中的數據也最好是讀寫比較高的, 即讀得多, 寫得少, 不會頻繁地更新.

Cache的常用操作通常有

  • get: 獲取
  • put: 設置
  • remove: 移除
  • clear: 清除
  • expire: 過期

對于Cluster Cache來說洼专,讀操作(get)肯定是本地方法,只需要從本臺計算機內存中獲取數據孵构。

Remove/clear 這兩個操作屁商,一般是本地方法加上遠程方法,需要和Cluster其他計算機進行同步颈墅。

Put這個寫方法蜡镶,可以是本地操作,也可以是遠程操作的恤筛。 Remote Put方法的場景是這樣官还,一臺計算機把數據放到Cache里面,這個數據就會被傳播到Cluster其他計算機上毒坛。這個做法的好處是Cluster各臺計算機的Cache數據可以及時得到補充妻枕,壞處是傳播的數據量比較大,這個代價比較大粘驰。
本地 Local Put方法的場景是這樣,一臺計算機把數據放到Cache里面述么,這個數據不會被傳播到Cluster其他計算機上蝌数。這個做法的好處是不需要傳播數據,壞處是Cluster各臺計算機的Cache數據不能及時得到補充度秘,這個不是很明顯的問題顶伞,從Cache中得不到數據,從數據庫獲取數據是很正常的現象剑梳。 Local Put比起Remote Put的優(yōu)勢很明顯唆貌,所以,通常的Cluster Cache都采用Local Put的策略垢乙。各Cache一般都提供了Local Put的配置選項锨咙,如果你沒有看到這個支持,那么請換一個Cache追逮。

通常有很多不同的 cache 實現酪刀,例如

  • Guava Cache
  • EhCache
  • Memcached
  • Redis

這里我們先重點講講 Guava Cache

最簡單的cache 實現莫過于在內存中維護一張 map 了, 按 key 檢索或存儲 , 不過內存畢竟有限, 要自己實現 Cache 和各種清除和失效策略, Guava Cache 是不錯的選擇, 可以很方便地設置最大容量, 清除和失效策略等等.

它的主要特性有:

  • 自動加載數據項到緩存中
  • 當最大限度到達時應用最近最少策略驅逐數據項
  • 從上次的訪問或修改時間開始計算的數據項過期
  • 數據項的鍵自動封裝于弱引用中
  • 數據項的值自動封裝于弱引用或軟引用中
  • 當數據項被驅逐或清除時通知
  • 聚合緩存訪問統(tǒng)計

我們來舉一個天氣預報數據的緩存例子, 天氣預報是那種比較適合緩存的數據, 它在一定的時間范圍內變化不大, 讀寫比很高.

各大廠商紛紛推出各種 open API , 免費的大多有調用次數限制,例如百度的天氣API, 一天之內的限制是 5000條/天钮孵,并發(fā)上限是 200QPS骂倘, 一般使用用也夠了,可是如果不是一般情況巴席,使用緩存就可以輕松搞定

百度應用控制臺 http://lbs.baidu.com/apiconsole/key#/home

baidu application console

接口示例

接口參數說明

參數類型 參數名稱 是否必須 具體描述
String ak true 開發(fā)者密鑰
String sn false 若用戶所用ak的校驗方式為sn校驗時該參數必須历涝。 (sn生成算法)
String location true 支持經緯度和城市名兩種形式,一次請求最多支持15個城市,之間用"|"分隔荧库,同一個城市的經緯度之間用 "," 分隔堰塌。舉例:location=116.43,40.75|120.22,43,33或者是location=北京|上海|廣州。
String output false 輸出的數據格式电爹,默認為xml格式蔫仙,當output設置為’json’時,輸出的為json格式的數據;
String coord_type false 請求參數坐標類型丐箩,默認為gcj02經緯度坐標摇邦。允許的值為bd09ll、bd09mc屎勘、gcj02施籍、wgs84。bd09ll表示百度經緯度坐標概漱,bd09mc表示百度墨卡托坐標丑慎,gcj02表示經過國測局加密的坐標。wgs84表示gps獲取的坐標瓤摧。
String callback false 將json格式的返回值通過callback函數返回以實現jsonp功能竿裂。舉例:callback=showLocation(JavaScript函數名)。

返回結果

參數名稱 含義 說明
currentCity 當前城市 返回城市名
status 返回結果狀態(tài)信息 請求狀態(tài)照弥,如果成功返回0腻异,如果失敗返回其他數字,詳細見狀態(tài)碼附錄这揣。
date 當前時間 年-月-日
results 天氣預報信息 白天可返回近期3天的天氣情況(今天悔常、明天、后天)给赞、晚上可返回近期4天的天氣情況(今天机打、明天、后天片迅、大后天)
results.currentCity 當前城市
results.pm25 pm2.5 0~50残邀,一級,優(yōu)障涯,綠色罐旗;51~100,二級唯蝶,良九秀,黃色; 101~150粘我,三級鼓蜒,輕度污染痹换,橙色; 151~200都弹,四級娇豫,中度污染 ,紅色畅厢; 201~300冯痢,五級,重度污染 框杜,紫色浦楣; >300,六級咪辱,嚴重污染振劳, 褐紅色。
results.index.title 指數title 分為:穿衣油狂、洗車历恐、感冒、運動专筷、紫外線這幾個類型弱贼。
results.index.zs 指數取值 不同指數描述不一
results.index.tipt 指數含義 指數含義
results.index.des 指數詳情 指數詳情
results.weather_data 天氣預報時間
weather_data.dayPictureUrl 白天的天氣預報圖片url
weather_data.nightPictureUrl 晚上的天氣預報圖片url
weather_data.weather 天氣狀況 常見天氣情況(“|”分隔符):晴|多云|陰|陣雨|雷陣雨|雷陣雨伴有冰雹|雨夾雪|小雨|中雨|大雨|暴雨|大暴雨|特大暴雨|陣雪|小雪|中雪|大雪|暴雪|霧|凍雨|沙塵暴|小雨轉中雨|中雨轉大雨|大雨轉暴雨|暴雨轉大暴雨|大暴雨轉特大暴雨|小雪轉中雪|中雪轉大雪|大雪轉暴雪|浮塵|揚沙|強沙塵暴|霾
weather_data.wind 風力 風力值
weather_data.temperature 溫度 溫度范圍,如:29~22℃

完整代碼參見 https://github.com/walterfan/helloworld/tree/master/hellocache

主要界面如下:

主要代碼如下:

  • WeatherApplication
package com.github.walterfan.hellocache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WeatherApplication {

    public static void main(String[] args) {
        SpringApplication.run(WeatherApplication.class, args);
    }
}

  • WeatherController
package com.github.walterfan.hellocache;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;

/**
 * Created by yafan on 8/10/2017.
 */
@Slf4j
@Controller
public class WeatherController {
    @Autowired
    private WeatherService weatherService;
    // inject via application.properties
    @Value("${hello.message:test}")
    private String message = "Hello World";

    @RequestMapping("/")
    public String index(Model model) {
        model.addAttribute("cityName", "hefei");
        model.addAttribute("currentTime", Instant.now().toString());
        return "index";
    }



    @GetMapping("/weather")
    protected String queryWeather(Model model, HttpServletRequest req, HttpServletResponse resp) {


        String cityName = req.getParameter("cityName");
        model.addAttribute("cityName", cityName);
        Optional<CityWeather> cityWeather =  weatherService.getWeather(cityName);
        if(cityWeather.isPresent()) {
            log.info("weather: {}", cityWeather.get());
            model.addAttribute("cityWeather", cityWeather.get());
            model.addAttribute("weatherString", cityWeather.map(x->x.toString()).orElse(""));
        }
        return "index";
    }



}

  • WeatherService.java
package com.github.walterfan.hellocache;

import com.codahale.metrics.MetricRegistry;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.concurrent.ExecutionException;


@Slf4j
@Service
public class WeatherService {
    @Autowired
    private LoadingCache<String, CityWeather> cityWeatherCache;

    @Autowired
    private MetricRegistry metricRegistry;


    public Optional<CityWeather> getWeather(String city) {
        try {
            return Optional.ofNullable(cityWeatherCache.get(city));
        } catch (ExecutionException e) {
            log.error("getWeather error", e);
            return Optional.empty();
        }
    }
}

  • WeatherCacheConfig.java 構建 Weather Cache 和相關依賴
package com.github.walterfan.hellocache;


import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Created by yafan on 14/10/2017.
 */
@EnableAspectJAutoProxy
@ComponentScan
@Configuration
@Slf4j
public class WeatherCacheConfig {//implements EnvironmentAware

    @Autowired
    private Environment environment;

    @Bean
    public WeatherCacheLoader weatherCacheLoader() {
        return new WeatherCacheLoader();
    }

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();

        List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
        for (HttpMessageConverter<?> converter : converters) {
            if (converter instanceof MappingJackson2HttpMessageConverter) {
                MappingJackson2HttpMessageConverter jsonConverter = (MappingJackson2HttpMessageConverter) converter;
                jsonConverter.setObjectMapper(new ObjectMapper());
                jsonConverter.setSupportedMediaTypes(ImmutableList.of(
                        new MediaType("application", "json", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET),
                        new MediaType("text", "javascript", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET)));
            }
        }
        return restTemplate;
    }

    @Bean
    public String appToken() {
        return this.environment.getProperty("BAIDU_AK");
    }


    @Bean
    public LoadingCache<String, CityWeather> cityWeatherCache() {
        LoadingCache<String, CityWeather> cache = CacheBuilder.newBuilder()
                .recordStats()
                .maximumSize(1000)
                .expireAfterWrite(60, TimeUnit.MINUTES)
                .build(weatherCacheLoader());

        recordCacheMetrics("cityWeatherCache", cache);
        return cache;
    }

    public void recordCacheMetrics(String cacheName, Cache cache) {
        MetricRegistry metricRegistry = metricRegistry();
        metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "hitCount"), () -> () -> cache.stats().hitCount());
        metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "hitRate"), () -> () -> cache.stats().hitRate());

        metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "missCount"), () -> () -> cache.stats().missCount());
        metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "missRate"), () -> () -> cache.stats().missRate());

        metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "requestCount"), () -> () -> cache.stats().requestCount());

        metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadCount"), () -> () -> cache.stats().loadCount());
        metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadSuccessCount"), () -> () -> cache.stats().loadSuccessCount());
        metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadExceptionCount"), () -> () -> cache.stats().loadExceptionCount());
    }

    public String generateMetricsKeyForCache(String cacheName, String keyName) {
        String metricKey = MetricRegistry.name("cache", cacheName, keyName);
        log.info("metric key generated for cache: {}", metricKey);
        return metricKey;
    }

    @Bean
    public DurationTimerAspect durationTimerAspect() {
        return new DurationTimerAspect();
    }

    @Bean
    @Lazy
    public MetricRegistry metricRegistry() {
        return new MetricRegistry();
    }
}


  • WeatherCacheLoader 從遠程的 Baidu API 獲取天氣預報數據放入緩存中
    同時把 main 函數放在這里, 也就是構建 Spring Application Context, 并從cache 中獲取天氣預報數據
    第一次花的時間比較長, 用了352 毫秒, 之后從cache 中獲取數據, 都只了幾十到幾百微秒
    注意對于百度天氣API, 你需要自己申請一個apptoken, 我申請了一個, 5000次/天的調用都免費的, 由于我用了一小時的緩存, 所以無需付出任何費用
package com.github.walterfan.hellocache;

import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Snapshot;


import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;


import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.ExecutionException;

/**
 * Created by yafan on 11/10/2017.
 *
 * refer to api http://lbsyun.baidu.com/index.php?title=car/api/weather
 */

@Slf4j
@Component
public class WeatherCacheLoader extends CacheLoader<String, CityWeather> {


    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private String appToken;

    @Autowired
    private LoadingCache<String, CityWeather> cityWeatherCache;

    @Autowired
    private MetricRegistry metricRegistry;

    @DurationTimer(name="getCityWeather")
    public CityWeather getCityWeather(String city) throws ExecutionException {
        return this.cityWeatherCache.get(city);
    }


    @Override
    public CityWeather load(String city) throws Exception {

        String url = "http://api.map.baidu.com/telematics/v3/weather";
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
                .queryParam("location", city)
                .queryParam("output", "json")
                .queryParam("ak", appToken);

        ResponseEntity<CityWeather> resp = restTemplate.getForEntity(builder.toUriString(), CityWeather.class);

        log.debug("response status: " + resp.getStatusCode());
        return resp.getBody();
    }

    public static void main(String[] args) throws ExecutionException {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WeatherCacheConfig.class)) {
            WeatherCacheLoader helloCacheLoder = (WeatherCacheLoader) context.getBean("WeatherCacheLoader");

            CityWeather cityWeather = helloCacheLoder.getCityWeather("hefei");
            for(int i=0;i<10;++i) {
                cityWeather = helloCacheLoder.getCityWeather("hefei");
            }

            log.info("----- weather -----");
            log.info(cityWeather.toString());

            MetricRegistry metricRegistry = (MetricRegistry) context.getBean("metricRegistry");


            SortedMap<String, Histogram> histograms =  metricRegistry.getHistograms();

            for(Map.Entry<String, Histogram> entry: histograms.entrySet()) {
                Snapshot snapshot = entry.getValue().getSnapshot();
                log.info("{}: size={},values: {}",  entry.getKey(), snapshot.size(), snapshot.getValues());
                log.info(" max={}, min={}, mean={}, median={}",
                        snapshot.getMax(), snapshot.getMin(), snapshot.getMean(), snapshot.getMedian());
            }
        }
    }


}

  • output
13:02:03.273 [main] INFO com.github.walterfan.hello.DurationTimerAspect - Duration of getCityWeather: 352404 MICROSECONDS, threshold: 0 MICROSECONDS
13:02:03.274 [main] INFO com.github.walterfan.hello.DurationTimerAspect - Duration of getCityWeather: 251 MICROSECONDS, threshold: 0 MICROSECONDS
13:02:03.274 [main] INFO com.github.walterfan.hello.DurationTimerAspect - Duration of getCityWeather: 127 MICROSECONDS, threshold: 0 MICROSECONDS
13:02:03.274 [main] INFO com.github.walterfan.hello.DurationTimerAspect - Duration of getCityWeather: 87 MICROSECONDS, threshold: 0 MICROSECONDS
...
13:02:03.309 [main] INFO com.github.walterfan.hello.HelloCacheLoader - getCityWeather: size=11, count=[34, 36, 40, 45, 60, 63, 85, 87, 127, 251, 352404],values: {}
13:02:03.309 [main] INFO com.github.walterfan.hello.HelloCacheLoader -  max=352404, min=34, mean=32112.0, median=63.0

這里順手寫了兩個 Metrics 相關的輔助類, 利用 AOP 和 Metrics 來記錄調用時間

  • class DurationTimer
ackage com.github.walterfan.hello;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * Created by yafan on 15/10/2017.
 */
@Target({ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface DurationTimer {

    String name() default "";

    long logThreshold() default 0;

    //default timeunit μs
    TimeUnit thresholdTimeUnit() default TimeUnit.MICROSECONDS;
}

  • class DurationTimerAspect
package com.github.walterfan.hello;

import com.codahale.metrics.MetricRegistry;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.concurrent.TimeUnit;

/**
 * Created by yafan on 15/10/2017.
 */
@Aspect
public class DurationTimerAspect {


    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private MetricRegistry metricRegistry;

    public <T> T proxy(T o) {
        final AspectJProxyFactory factory = new AspectJProxyFactory(o);
        factory.setProxyTargetClass(true);
        factory.addAspect(this);
        return factory.getProxy();
    }

    @Around("@annotation( durationAnnotation ) ")
    public Object measureTimeRequest(final ProceedingJoinPoint pjp, DurationTimer durationAnnotation) throws Throwable {
        final long start = System.nanoTime();
        final Object retVal = pjp.proceed();

        String timerName = durationAnnotation.name();
        if("".equals(timerName)) {
            timerName = pjp.getSignature().toShortString();
        }
        TimeUnit timeUnit = durationAnnotation.thresholdTimeUnit();
        long threshold = durationAnnotation.logThreshold();
        //System.out.println("timerName=" + timerName);
        try {
            long difference = timeUnit.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS);

            if(difference > threshold) {
                metricRegistry.histogram(timerName).update(difference);
                logger.info("Duration of {}: {} {}, threshold: {} {}", timerName, difference, timeUnit.name(), threshold, timeUnit.name());
            }

        } catch (Exception ex) {
            logger.error("Cannot measure api timing.... :" + ex.getMessage(), ex);
        }
        return retVal;
    }

}

相關的 DTO 如下

package com.github.walterfan.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Date;
import java.util.List;

/**
 * Created by yafan on 14/10/2017.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class CityWeather  {
    private int error;

    private String status;

    private Date date;

    private List<WeatherResult> results;

    public int getError() {
        return error;
    }

    public void setError(int error) {
        this.error = error;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public List<WeatherResult> getResults() {
        return results;
    }

    public void setResults(List<WeatherResult> results) {
        this.results = results;
    }

    @Override
    public String toString() {
        try {
            return new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
}


//---------------------
package com.github.walterfan.dto;

/**
 * Created by yafan on 14/10/2017.
 */
public class WeatherData {

    private String date;
    private String dayPictureUrl;
    private String nightPictureUrl;
    private String weather;
    private String wind;
    private String temperature;

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getDayPictureUrl() {
        return dayPictureUrl;
    }

    public void setDayPictureUrl(String dayPictureUrl) {
        this.dayPictureUrl = dayPictureUrl;
    }

    public String getNightPictureUrl() {
        return nightPictureUrl;
    }

    public void setNightPictureUrl(String nightPictureUrl) {
        this.nightPictureUrl = nightPictureUrl;
    }

    public String getWeather() {
        return weather;
    }

    public void setWeather(String weather) {
        this.weather = weather;
    }

    public String getWind() {
        return wind;
    }

    public void setWind(String wind) {
        this.wind = wind;
    }

    public String getTemperature() {
        return temperature;
    }

    public void setTemperature(String temperature) {
        this.temperature = temperature;
    }
}
//------------------
package com.github.walterfan.dto;

/**
 * Created by yafan on 14/10/2017.
 */
public class WeatherIndex {
    private String title;
    private String zs;
    private String tipt;
    private String des;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getZs() {
        return zs;
    }

    public void setZs(String zs) {
        this.zs = zs;
    }

    public String getTipt() {
        return tipt;
    }

    public void setTipt(String tipt) {
        this.tipt = tipt;
    }

    public String getDes() {
        return des;
    }

    public void setDes(String des) {
        this.des = des;
    }
}
//---------------------
package com.github.walterfan.dto;

import java.util.List;

/**
 * Created by yafan on 14/10/2017.
 */
public class WeatherResult {
    private String currentCity;
    private String pm25;

    private List<WeatherIndex> index;

    private List<WeatherData> weather_data;

    public String getCurrentCity() {
        return currentCity;
    }

    public void setCurrentCity(String currentCity) {
        this.currentCity = currentCity;
    }

    public String getPm25() {
        return pm25;
    }

    public void setPm25(String pm25) {
        this.pm25 = pm25;
    }

    public List<WeatherIndex> getIndex() {
        return index;
    }

    public void setIndex(List<WeatherIndex> index) {
        this.index = index;
    }

    public List<WeatherData> getWeather_data() {
        return weather_data;
    }

    public void setWeather_data(List<WeatherData> weather_data) {
        this.weather_data = weather_data;
    }
}

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末磷蛹,一起剝皮案震驚了整個濱河市哮洽,隨后出現的幾起案子,更是在濱河造成了極大的恐慌弦聂,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氛什,死亡現場離奇詭異莺葫,居然都是意外死亡,警方通過查閱死者的電腦和手機枪眉,發(fā)現死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門捺檬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贸铜,你說我怎么就攤上這事堡纬。” “怎么了蒿秦?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵烤镐,是天一觀的道長。 經常有香客問我棍鳖,道長炮叶,這世上最難降的妖魔是什么碗旅? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮镜悉,結果婚禮上祟辟,老公的妹妹穿的比我還像新娘。我一直安慰自己侣肄,他們只是感情好旧困,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著稼锅,像睡著了一般吼具。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缰贝,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天馍悟,我揣著相機與錄音,去河邊找鬼剩晴。 笑死锣咒,一個胖子當著我的面吹牛,可吹牛的內容都是我干的赞弥。 我是一名探鬼主播毅整,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绽左!你這毒婦竟也來了悼嫉?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拼窥,失蹤者是張志新(化名)和其女友劉穎戏蔑,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體鲁纠,經...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡总棵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了改含。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片情龄。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖捍壤,靈堂內的尸體忽然破棺而出骤视,到底是詐尸還是另有隱情,我是刑警寧澤鹃觉,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布专酗,位于F島的核電站,受9級特大地震影響盗扇,放射性物質發(fā)生泄漏笼裳。R本人自食惡果不足惜唯卖,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躬柬。 院中可真熱鬧拜轨,春花似錦、人聲如沸允青。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颠锉。三九已至法牲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間琼掠,已是汗流浹背拒垃。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓷蛙,地道東北人悼瓮。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像艰猬,于是被迫代替她去往敵國和親横堡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理冠桃,服務發(fā)現命贴,斷路器,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • 一食听、簡介 Ehcache是一個用Java實現的使用簡單胸蛛,高速,實現線程安全的緩存管理類庫樱报,ehcache提供了用內...
    小程故事多閱讀 43,858評論 9 59
  • 1基本安裝1.1在基于RHEL的系統(tǒng)中安裝Cassandra1.1.1必要條件? YUM包管理器? Root或...
    戰(zhàn)神湯姆閱讀 1,048評論 0 4
  • 理論總結 它要解決什么樣的問題胚泌? 數據的訪問、存取肃弟、計算太慢、太不穩(wěn)定零蓉、太消耗資源笤受,同時,這樣的操作存在重復性敌蜂。因...
    jiangmo閱讀 2,851評論 0 11
  • 轉載地址:http://gnucto.blog.51cto.com/3391516/998509 Redis與Me...
    Ddaidai閱讀 21,452評論 0 82