如果你想开发一个应用(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);
}
然后实现稍微复杂一些,来统计一些实现需完成的操作:
- 查询缓存内是否已有今天此地的天气,如有直接返回
- 通过字典表查询心知天气的api所需key
- 调用天气资源,查询此地天气
- 将返回天气存入db
- 返回天气
接下来就一步一步完成这个服务层:
@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部分
谢谢观看
祝大家春节愉快,提前拜个早年