zoukankan      html  css  js  c++  java
  • 如果你想开发一个应用(1-20)

    天气api##

    上一章里我们已经可以手动设置天气情况,但在一般情况下,天气情况都是客观的,所以他不应该由人手动设置。所以读取天气接口自动获取就是一个必须的功能点了。

    天气预报的接口有很多,最早的weather.cn有时好时坏,所以最终选择了心知天气的接口。

    这个接口的免费版可以支持国内市级的几乎所有城市,这也是我在上一章把选择地区的精确度定为市级的原因之一。并且可以根据名称和坐标等功能获取实时天气,当前阶段,免费版也可以支持现有的功能.

    心知天气的用法很简单,首先注册一个账号,然后就回有一个key,接下来将key嵌入到url中就可以通过webapi的方式get回一个json的字符串,解析即可。

    key的保存方式##

    为了未来程序的扩展性和保密性,天气api的key不可以写在代码内,可以选择保存在配置文件中或者创立一个字典表。经过多方便考虑,我选择使用字典表的方式来进行保存,首先在数据库中创建字典表,然后在程序中,他所对应的数据模型如下:

    @Entity(name = "dictionaryitems")
    public class DictionaryItem {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;   			
        private Integer sort; 			//排序
        private String dicname;			//字典key
        private String dicvalue;		//字典值
        private String typevalue;		//字典值类型(多项 分组用)
    	//getter  setter
    }
    

    有了之前的框架,接下来的代码就比较容易了,还是一样的,持久层jpa接口:

    public interface DictionaryItemRepository extends JpaRepository<DictionaryItem,Integer> {
        List<DictionaryItem> findByDicname(String dicName);  //根据字典key获取值
        List<DictionaryItem> findByTypevalue(String typeValue);  //根据类型获取值
        List<DictionaryItem> findByTypevalueOrderBySort(String typeValue);//根据类型获取值并排序
    }
    

    其实在当前,我们需要使用的只有第一个。

    需要注意,在当前不考虑做系统后台的情况下,此字典表均需手动录入,也就是或只有jpa层,不需要服务层

    天气数据模型##

    现在假设你已经注册完成,并且进入面试使用api的界面,可以看到若干接口,因为属于欠高端用户,所以我们只看接口名后边没有付费接口字样的接口。经过查询,很容易就找到我们需要的:
    逐日天气预报和昨日天气,可以看到接口路径为/weather/daily.json

    接口文档及参数:

    key
    你的API密钥
    location
    所查询的位置
    参数值范围:
    
    城市ID 例如:location=WX4FBXXFKE4F
    城市中文名 例如:location=北京
    省市名称组合 例如:location=辽宁朝阳、location=北京朝阳
    城市拼音/英文名 例如:location=beijing(如拼音相同城市,可在之前加省份和空格,例:shanxi yulin)
    经纬度 例如:location=39.93:116.40(格式是 纬度:经度,英文冒号分隔)
    IP地址 例如:location=220.181.111.86(某些IP地址可能无法定位到城市)
    “ip”两个字母 自动识别请求IP地址,例如:location=ip
    language
    语言 (可选)
    
    unit
    单位 (可选)
    参数值范围:
    
    c 当参数为c时,温度c、风速km/h、能见度km、气压mb
    f 当参数为f时,温度f、风速mph、能见度mile、气压inch
    默认值:c
    
    start
    起始时间 (可选)
    参数值范围:
    
    日期 例如:start=2015/10/1
    整数 例如:start=-2 代表前天、start=-1 代表昨天、start=0 代表今天、start=1 代表明天
    默认值:0
    
    days
    天数 (可选) 返回从start算起days天的结果。默认为你的权限允许的最多天数。
    

    经过筛选,我们可以看到:

    key:自己当前的key
    location:前端定位或选择的省市级组合单位
    language:zh-Hans
    unit:c
    start:0(今天)
    days:1(不需要预报功能,只是实时查询今天天气)
    

    ok,假设选择了北京,最终的参数查询url为:

    https://api.seniverse.com/v3/weather/daily.json?key=mykey&location=北京北京&language=zh-Hans&unit=c&start=0&days=1
    

    返回的查询结果为(已手动格式化):

    {
        "results":[
            {
                "location":{
                    "id":"mykey",
                    "name":"北京",
                    "country":"CN",
                    "path":"北京,北京,中国",
                    "timezone":"Asia/Shanghai",
                    "timezone_offset":"+08:00"
                },
                "daily":[
                    {
                        "date":"2018-02-11",
                        "text_day":"晴",
                        "code_day":"0",
                        "text_night":"晴",
                        "code_night":"1",
                        "high":"0",
                        "low":"-8",
                        "precip":"",
                        "wind_direction":"西北",
                        "wind_direction_degree":"315",
                        "wind_speed":"20",
                        "wind_scale":"4"
                    }
                ],
                "last_update":"2018-02-11T18:00:00+08:00"
            }
        ]
    }
    

    下面看看,在这些属性中我们需要的和不需要的,貌似除了时区和最后更新时间外,均需要可以保存,所以最终数据模型为:

    @Entity(name = "weather")
    public class Weather {
        private Integer id;
        private String name;
        private String path;
        private String weatherdate;
        private String text_day;
        private Integer code_day;
        private Integer temp_high;
        private Integer temp_low;
        private String  precip;
        private String wind_direction;
        private String wind_direction_degree;
        private String wind_speed;
        private String wind_scale;
        private Integer isweb;
    	setter... getter...
    }
    

    其中isweb的属性用来确认是网络获取还是本地设置。

    网络访问##

    由于金钱的原因,现有账户每小时只能访问400次,所以需要必要的缓存机制缓存到本地,这样就不能由客户端直接访问心知天气的api,只能由服务器端缓存后在发送至客户端。这样,就需要java端进行必须的服务器访问操作。

    按照RESTful的思想,访问的都是资源,也就是可以把它理解为一个网络数据库,所以同样,创建一个包用来存放web持久层,当然这里没有jpa了,只能够自己写实现.同时,想到之后可能会有切换天气api的需求,所以将逻辑封装到实现内,这里只返回一个weather对象:

    public interface WeatherWebData {
        String serviceUrl="https://api.seniverse.com/v3/weather/daily.json?key=%s&location=%s&language=zh-Hans&unit=c&start=0&days=1";
        public Weather getWeatherByLocation(String weatherKey, String location);
    }
    

    将配置好的链接参数保存在接口内。

    对于网络资源的访问选择了apache的http组件,所以同意需要使用Maven进行引入:

    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
      <version>4.5.5</version>
    </dependency>
    

    然后在实现中完成对接口的访问:

    @Repository
    public class WeatherWebDataImpl implements WeatherWebData {
        public Weather getWeatherByLocation(String weatherKey, String location) {
        try {
    	    HttpClient client =  new DefaultHttpClient();
            HttpUriRequest request=new HttpGet(String.format(this.serviceUrl,weatherKey,location));
            request.setHeader("Content-type","application/json;charset=utf-8");
            HttpResponse response= client.execute(request);
            String result = EntityUtils.toString(response.getEntity(), "utf-8");
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    

    }

    这段代码天生就适合进行提取方法的重构,所以在工具包内创建一个HttpUtil类,首先封装一下最简单get访问形式,返回String即可:

    public class HttpUtil {
        public static String get(String url){
            HttpClient client =  new DefaultHttpClient();
            HttpUriRequest request=new HttpGet(url);
            request.setHeader("Content-type","application/json;charset=utf-8");
            HttpResponse response= null;
            String result = null;
            try {
                response = client.execute(request);
                result = EntityUtils.toString(response.getEntity(), "utf-8");
            } catch (IOException e) {
                e.printStackTrace();
            }
            return result;
        }
    }
    

    此时先不考虑异常情况,实际情况下异常需前端配合,直接显示手动天气设置按钮。

    然后在接口实现里替换掉即可:

    String url=String.format(this.serviceUrl,weatherKey,location);
    String result= HttpUtil.get(url);
    

    在进行对象创建之前,还要先看一下请求成功之外的情况,把随便给个非法的参数,比如用户key为空,看看返回情况:

    {
        "status":"The API key is invalid.",
        "status_code":"AP010003"
    }
    

    格式不一致就好办了,可以通过判断status来判断返回的成功或者失败。

    JSON解析##

    由于Weather的转换不具有普遍性,所以就不创建共有的工具类,在实现类中通过私有类来实现,String到对象的转换有很多种方法,比如之前刚刚用过的jackson,但这里由于实体类和json对象的属性并没有一一对应,所以jackson就不那么特别适合。
    那么有没有其他方法呢,答案当然是肯定的,这里使用阿里出的fastjson,还是一样的,通过maven进行引入:

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.46</version>
    </dependency>
    

    他的使用很简单,就好像是mybatis一样,将一个对象以Map<String,Object>或List<Map<String,Object>>的形式返回,这样,只要我们知道json的结构,就可以轻而易举的将它转换为任何形式的对象,这里即没啥好说了,直接贴方法代码:

     private Weather jsonToWeather(String json){
        Weather weather=new Weather();
        Map<String,Object> map = JSON.parseObject(json);
        //判断失败
        if(!map.containsKey("status")) {
            //正常情况
            //weather是result节点的第一项
            Map<String,Object> weatherMap= ((List<Map<String,Object>>)map.get("results")).get(0);
            Map<String, Object> locationJson = (Map<String, Object>) weatherMap.get("location");
            weather.setName(locationJson.get("name").toString());
            weather.setPath(locationJson.get("path").toString());
            Map<String, Object> dailyJson = ((List<Map<String, Object>>) weatherMap.get("daily")).get(0);
            weather.setWeatherdate(dailyJson.get("date").toString());
            weather.setCode_day(Integer.parseInt(dailyJson.get("code_day").toString()));
            weather.setText_day(dailyJson.get("text_day").toString());
            weather.setTemp_high(Integer.parseInt(dailyJson.get("high").toString()));
            weather.setTemp_low(Integer.parseInt(dailyJson.get("low").toString()));
            weather.setWind_direction(dailyJson.get("wind_direction").toString());
            weather.setWind_direction_degree(dailyJson.get("wind_direction_degree").toString());
            weather.setWind_scale(dailyJson.get("wind_scale").toString());
            weather.setWind_speed(dailyJson.get("wind_speed").toString());
            weather.setPrecip(dailyJson.get("precip").toString());
            weather.setIsweb(1);
            return weather;
        }
        return null;
    }
    

    最终完成实现方法:

    @Repository
    public class WeatherWebDataImpl implements WeatherWebData {
        public Weather getWeatherByLocation(String weatherKey, String location) {
            String url=String.format(this.serviceUrl,weatherKey,location);
            String result= HttpUtil.get(url);
            return jsonToWeather(result);
        }
    	private Weather jsonToWeather(String json){
    	......
    	}
    }
    

    服务层代码##

    接下来是服务层,这层就没啥好说的了,接口定义了一个方法,通过地址查询天气:

    public interface WeatherService {
        public Object weather(String address);
    }
    

    然后实现稍微复杂一些,来统计一些实现需完成的操作:

    1. 查询缓存内是否已有今天此地的天气,如有直接返回
    2. 通过字典表查询心知天气的api所需key
    3. 调用天气资源,查询此地天气
    4. 将返回天气存入db
    5. 返回天气

    接下来就一步一步完成这个服务层:

    @Service
    public class WeatherServiceImpl implements WeatherService{
        public Weather weather(String address) {
        	return null;
        }
    }
    

    查询缓存内是否已有今天此地的天气,如有直接返回###

    注入天气持久层,并根据日期进行查询:

    @Autowired
    private WeatherRepository weatherRepository;
    
    .....
    public Weather weather(String address) {
    	 Weather weather=getWeatherByDb(address,(new SimpleDateFormat("yyyy-MM-dd")).format(new Date()));
    	return weather;
    }
    

    通过字典表查询心知天气的api所需key###

    首先还是引入字典持久层,然后封装一个查询key的私有方法(后期可能改为工具类),并放入缓存(暂时使用静态字段代替,后期使用Spring-Cache框架管理):

    @Autowired
    private DictionaryItemRepository dictionaryItemRepository;
    
    private static String weatherKey="";
    private String getWeatherKey(){
    	//缓存为空
        if(WeatherServiceImpl.weatherKey.equals("")){
            //查询字典表
            List<DictionaryItem> dicList=dictionaryItemRepository.findByDicname("weatherKey");
            if(dicList.size()>0)
                weatherKey= dicList.get(0).getDicvalue();
        }
        return weatherKey;
    }
    

    调用天气资源,查询此地天气##

    注入之前封装好的网络持久层,并继续增量代码:

    @Autowired
    private WeatherWebData weatherWebData;
    ...
    
    public Weather weather(String address) {
    	...
    	if(weather==null){
            //如果没有,则查询,并存储到db 返回新内容
            weather= weatherWebData.getWeatherByLocation(getWeatherKey(),address);
        }
    }
    

    将返回天气存入db###

    同样封装一个天气存储的方法,保存的同时还可获取db的自增ID:

    private Weather saveWeather(Weather weather){
        return weatherRepository.saveAndFlush(weather);
    }
    

    最终,返回天气(接口方法完整代码):

    public Weather weather(String address) {
        //查询db中是否有此日此地天气
        Weather weather=getWeatherByDb(address,(new SimpleDateFormat("yyyy-MM-dd")).format(new Date()));
        if(weather==null){
            //如果没有,则查询,并存储到db 返回新内容
            weather= weatherWebData.getWeatherByLocation(getWeatherKey(),address);
            weather =  saveWeather(weather);
        }
        return weather;
    
    }
    

    控制器##

    由于操作均封装到了服务层,所以控制器已经尽可能的薄了:

    @RequestMapping(value = "/api/weather",method = RequestMethod.POST)
    public Object getWeather(HttpServletRequest request,@RequestBody Map map){
        return result(weatherService.weather(map.get("address").toString()));
    }
    

    前端逻辑修改##

    后端折腾了一条线,终于要修改前端了,其实前端相对来说修改的地方很少。

    由于没有真机测试,所以现在只完成手动设置地点后天气获取

    继续进入CreateOrShowDiaryItem.vue组件,修改设置地区的关闭按钮事件:

    addressClose:function(event){
    	this.adddialog=false;
    	//查询此地的天气 省市组合
    	this.searchWeather( this.addressProvince+""+this.addressCity);
    },
    

    使用searchWeather方法进行服务器端查询:

    searchWeather:function(address){
    	var data={
    		address:address
    	};
    	this.$http.post("/api/weather",data,{headers:{"token":this.token}}).then(res=>{
    		if(res.data.msg!=""){
    			//使用手动天气设置
    			this.$store.commit('setWeatherIsShow',true);
    		}
    		var result=res.data.data;
    		if(!(result== undefined ||result=="")){
    			//关闭手动设置按钮
    			this.$store.commit('setWeatherIsShow',false);
    			this.weatherContent=result;
    			this.weatherText= result.text_day+" "+result.temp_high+"度/"+result.temp_low+"度";
    		}
    
    	},res=>{
    		//查询服务器失败,同样显示天气设定界面
    		this.$store.commit('setWeatherIsShow',true);
    	})
    }
    

    最后,看看效果:

    顺便和天气预报比对一下:

    可以看到,已经获取到了实时天气。

    本章代码(github):
    客户端vue部分
    服务端Java部分

    谢谢观看

    祝大家春节愉快,提前拜个早年

  • 相关阅读:
    Tomcat源码(二):tomcat启动之前的初始化
    Tomcat源码(一):整体架构
    SSH的三个组件ssh、sftp、scp介绍
    远程连接linux服务上的mysql
    linux系统上安装mysql5.6(详细步骤)
    一种基于数字证书的单点登录的实现方法(转)
    jacob操作word (转)
    解决fis3下载慢,没反应
    Spring框架集成mybatis框架的配置(笔记)
    eclipse导入lombok后打不开(如果你的lombok不是最新的,那就来下载最新的)
  • 原文地址:https://www.cnblogs.com/jiangchao226/p/8444286.html
Copyright © 2011-2022 走看看