在 SpringBoot搭建一个简单的天气预报系统(一)博客中已经实现了一个非常简单的天气预报系统------用户访问这个天气预报系统的时候,那么系统会响应给用户一个天气数据。但这个数据的来源并不是我们自己产生的,我们是依赖于一个第三方的天气预报的数据API。通过调用这个API,我们将返回的Json数据进行解析,再返回给使用这个系统的用户。
这种系统架构会存在什么问题呢?
面对以上问题,这里采用Redis缓存来解决-----期望使用Redis提升应用的并发访问能力
这里针对此系统简要说说Redis的优点吧:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
用户访问这个天气预报系统的时候,系统先从Redis缓存中进行获取数据,如果有,直接返回;如果没有,再调用第三方API,调用完毕后,将数据返回,并把数据添加到Redis中去
这里主要修改WeatherDataServiceImpl类中的doGetWeather()方法
WeatherDataServiceImpl
添加了个注解@Slf4j和StringRedisTemplate 成员变量
@Service
@Slf4j
public class WeatherDataServiceImpl implements WeatherDataService {
private static final String WEATHER_URL = "http://wthrcdn.etouch.cn/weather_mini?";
@Autowired
private RestTemplate restTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
...
}
doGetWeather()
private <T> T doGetWeather(String uri, Class<T> type) {
String key = uri;
String strBody = null;
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 先查询缓存,如果缓存有,则从缓存中取
if (stringRedisTemplate.hasKey(key)) {
log.info("Redis has data");
strBody = ops.get(key);
} else {
log.info("Redis don't has data");
// 如果缓存没有,则再调用服务接口
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
if (StatusCodeConstant.OK == responseEntity.getStatusCodeValue()) {
strBody = responseEntity.getBody();
}
// 将数据写入缓存
ops.set(key, strBody, RedisConstant.TIME_OUT, TimeUnit.SECONDS);
}
ObjectMapper objectMapper = new ObjectMapper();
T t = null;
try {
t = objectMapper.readValue(strBody, type);
} catch (Exception e) {
log.error("Error!", e);
}
return t;
}
RedisConstant
public interface RedisConstant {
// 过期时间:1800s
Long TIME_OUT = 1800L;
}
启动项目之前,先启动Redis。测试时,可以先把过期时间设置短一点,如:10s。完成之后,再可以设置成1800s。假如设置10s的话,你第一次访问,控制台会打印“Redis don’t has data”,在10s以内再次访问时,会打印“Redis has data”,因为数据已经被存储到了Redis中去了,可通过Redis的客户端工具Redis DeskTop Manager进行查看。超过10s后,再访问,就会打印“Redis don’t has data”,因为数据在Redis中过期了。
Quartz是一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。
在此之前,这个系统使用了Redis缓存来减少了调用第三方API的次数,但我们还是对它是强依赖,毕竟我们仅仅只是数据的搬运工,并不是产生者。况且,我们也做不了产生者,我们只能依赖于第三方的天气服务商来提供数据给我们。
之前使用Redis缓存来减少调用第三方API的次数是如何做的-----当用户访问系统时,系统才会触发去调用第三方接口(缓存中没有此数据)的动作。实际上,这个动作应该自动完成,不应该等到用户请求时,再去拉取最新的数据(如果是这样的话,这就比较迟了)。
实现数据同步就需要一个定时器来帮助我们实现这种定时任务,因为我们的天气数据大概半小时或1小时变更一次,那么,我们的定时器就设置为半小时或1小时去执行这个定时动作。这里的定时器就选择Quartz,它在业界算比较流行
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-quartzartifactId>
dependency>
WeatherDataSyncJob
定义了一个Job
@Slf4j
public class WeatherDataSyncJob extends QuartzJobBean {
@Autowired
private CityDataService cityDataService;
@Autowired
private WeatherDataService weatherDataService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Weather Data Sync Job. Start!");
}
}
这里用日志打印一条语句,先不做任何业务逻辑
QuartzConfig
这个配置类是用来配置Quartz的
@Configuration
public class QuartzConfig {
// 频率(多长时间执行一次)
private static final Integer TIME = 1800;
@Bean
public JobDetail weatherDataSyncJobJobDetail() {
return JobBuilder
.newJob(WeatherDataSyncJob.class)
.withIdentity("weatherDataSyncJobJobDetail")
.storeDurably()
.build();
}
@Bean
public Trigger weatherDataSyncJobTrigger() {
SimpleScheduleBuilder schedule = SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(TIME)
.repeatForever();
return TriggerBuilder
.newTrigger()
.forJob(weatherDataSyncJobJobDetail())
.withIdentity("weatherDataSyncJobTrigger")
.withSchedule(schedule)
.build();
}
}
配置Quartz需要两个Bean:
启动项目时(要启动Redis),控制台会打印一条语句:“Weather Data Sync Job. Start!”。可以把定时任务执行的时间(QuartzConfig 中的TIME)设置为2s,再次启动项目,发现每隔2s,控制台都会打印那条语句。测试成功后,再把时间设置成1800s。
之前在WeatherDataSyncJob类中只是用日志打印了一条语句 ,没做任务业务逻辑,接下来就在那里面做真实的业务----拉取第三方API的天气数据到缓存中去。
接口url是需要城市ID,所以,我们只需要获取城市列表的数据,然后,对它进行遍历,即可得到城市ID。由于城市列表(打开此链接,右键“另存为”)文件中的数据庞大,所以,这里只选取部分数据-----广东省里面的城市。
为了减少调用第三方API,可以把这部分数据存储到本地,可用mysql,或者xml文件。这里就用xml文件存储,文件名为:citylist.xml。由于这个文件的内容比较多,这里就不贴了,这个文件在我的项目中,项目的源代码会放到github中去,需要的可以自行下载。这里贴出部分数据:
<c c1="0">
<d d1="101280101" d2="广州" d3="guangzhou" d4="广东"/>
<d d1="101280102" d2="番禺" d3="panyu" d4="广东"/>
<d d1="101280103" d2="从化" d3="conghua" d4="广东"/>
<d d1="101280104" d2="增城" d3="zengcheng" d4="广东"/>
<d d1="101280105" d2="花都" d3="huadu" d4="广东"/>
<d d1="101280201" d2="韶关" d3="shaoguan" d4="广东"/>
<d d1="101280202" d2="乳源" d3="ruyuan" d4="广东"/>
<d d1="101280203" d2="始兴" d3="shixing" d4="广东"/>
<d d1="101280204" d2="翁源" d3="wengyuan" d4="广东"/>
<d d1="101280205" d2="乐昌" d3="lechang" d4="广东"/>
<d d1="101280206" d2="仁化" d3="renhua" d4="广东"/>
<d d1="101280207" d2="南雄" d3="nanxiong" d4="广东"/>
c>
将citylist.xml中的数据映射成Java对象
City
@Data
@XmlRootElement(name = "d")
@XmlAccessorType(XmlAccessType.FIELD)
public class City {
@XmlAttribute(name = "d1")
private String cityId;
@XmlAttribute(name = "d2")
private String cityName;
@XmlAttribute(name = "d3")
private String cityCode;
@XmlAttribute(name = "d3")
private String province;
}
CityList
@Data
@XmlRootElement(name = "c")
@XmlAccessorType(XmlAccessType.FIELD)
public class CityList {
@XmlElement(name = "d")
private List<City> cityList;
}
XmlBuilderUtil
定义一个工具类,将xml字符串转换为Java对象
public class XmlBuilderUtil {
// 将XML转换为指定的POJO
public static Object xmlStrToObject(String xmlStr, Class<?> clazz) throws Exception {
Object xmlObject = null;
JAXBContext context = JAXBContext.newInstance(clazz);
// XML转换为对象的接口
Unmarshaller unmarshaller = context.createUnmarshaller();
Reader in = new StringReader(xmlStr);
xmlObject = unmarshaller.unmarshal(in);
if (null != in) {
in.close();
}
return xmlObject;
}
}
CityDataService
public interface CityDataService {
// 获取城市列表
List<City> listCity() throws Exception;
}
CityDataServiceImpl
@Service
public class CityDataServiceImpl implements CityDataService {
@Override
public List<City> listCity() throws Exception {
// 读取XML文件
Resource resource = new ClassPathResource("citylist.xml");
BufferedReader in = new BufferedReader(new InputStreamReader(resource.getInputStream(), "utf-8"));
StringBuffer buffer = new StringBuffer();
String line = "";
while (null != (line = in.readLine())) {
buffer.append(line);
}
in.close();
// XML转换为对象
CityList cityList = (CityList)XmlBuilderUtil.xmlStrToObject(buffer.toString(), CityList.class);
return cityList.getCityList();
}
}
WeatherDataService
// 根据城市id查询天气数据
WeatherResponseVO getDataByCityId(String cityId);
// 根据城市名称查询天气数据
WeatherResponseVO getDataByCityName(String cityName);
//根据城市id来同步天气
void syncDataByCityId(String cityId);
添加了一个方法syncDataByCityId(),用来同步天气数据的
WeatherDataServiceImpl
@Override
public void syncDataByCityId(String cityId) {
String uri = WEATHER_URL + "citykey=" + cityId;
saveWeatherData(uri);
}
// 把天气数据保存到缓存中
private void saveWeatherData(String uri) {
String key = uri;
String strBody = null;
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
if (StatusCodeConstant.OK == responseEntity.getStatusCodeValue()) {
strBody = responseEntity.getBody();
}
ops.set(key, strBody, RedisConstant.TIME_OUT, TimeUnit.SECONDS);
}
WeatherDataSyncJob
@Slf4j
public class WeatherDataSyncJob extends QuartzJobBean {
@Autowired
private CityDataService cityDataService;
@Autowired
private WeatherDataService weatherDataService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Weather Data Sync Job. Start!");
// 获取城市ID列表
List<City> cityList = null;
try {
cityList = cityDataService.listCity();
} catch (Exception e) {
log.error("Exception!", e);
}
// 遍历城市ID,获取天气
for (City city : cityList) {
String cityId = city.getCityId();
log.info("Weather Data Sync Job, cityId:" + cityId);
weatherDataService.syncDataByCityId(cityId);
}
log.info("Weather Data Sync Job. End!");
}
}
启动项目。
数据开始同步
数据同步结束
数据同步到Redis中去了
给此系统添加一个页面
功能:
获取到该城市ID的天气预报信息
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
WeatherReportService
public interface WeatherReportService {
// 根据城市ID查询天气信息
WeatherVO getDataByCityId(String cityId);
}
WeatherReportServiceImpl
@Service
public class WeatherReportServiceImpl implements WeatherReportService {
@Autowired
private WeatherDataService weatherDataService;
@Override
public WeatherVO getDataByCityId(String cityId) {
WeatherResponseVO responseVO = weatherDataService.getDataByCityId(cityId);
return responseVO.getData();
}
}
WeatherReportController
@RestController
@RequestMapping("/report")
public class WeatherReportController {
@Autowired
private CityDataService cityDataService;
@Autowired
private WeatherReportService weatherReportService;
@GetMapping("/cityId/{cityId}")
public ModelAndView getReportByCityId(@PathVariable("cityId") String cityId, Model model) throws Exception {
model.addAttribute("title", "小朱的天气预报");
model.addAttribute("cityId", cityId);
model.addAttribute("cityList", cityDataService.listCity());
model.addAttribute("report", weatherReportService.getDataByCityId(cityId));
return new ModelAndView("weather/report", "reportModel", model);
}
}
report.html
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<title>小朱的天气预报title>
head>
<body>
<div class="container">
<div class="row">
<h3 th:text="${reportModel.title}">waylauh3>
<select style="width: 80px" class="custom-select" id="selectCityId">
<option th:each="city : ${reportModel.cityList}"
th:value="${city.cityId}" th:text="${city.cityName}"
th:selected="${city.cityId eq reportModel.cityId}">option>
select>
div>
<div class="row">
<h1 class="text-success" th:text="${reportModel.report.city}">深圳h1>
div>
<div class="row">
<p>
当前温度:<span th:text="${reportModel.report.wendu}">span>
p>
div>
<div class="row">
<p>
温馨提示:<span th:text="${reportModel.report.ganmao}">span>
p>
div>
<div class="row">
<div class="card border-info" th:each="forecast : ${reportModel.report.forecast}">
<div class="card-body text-info">
<p class="card-text" th:text="${forecast.date}">日期p>
<p class="card-text" th:text="${forecast.type}">天气类型p>
<p class="card-text" th:text="${forecast.high}">最高温度p>
<p class="card-text" th:text="${forecast.low}">最低温度p>
<p class="card-text" th:text="${forecast.fengxiang}">风向p>
div>
div>
div>
div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
crossorigin="anonymous">script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous">script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"
integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI"
crossorigin="anonymous">script>
<script type="text/javascript" th:src="@{/js/weather/report.js}">script>
body>
html>
前端页面使用了Bootstrap框架。
Bootstrap官网
report.js
/**
* report页面下拉框事件
*/
$(function () {
$("#selectCityId").change(function () {
var cityId = $("#selectCityId").val()
var url = '/report/cityId/' + cityId
window.location.href = url
})
})