SpringBoot搭建一个简单的天气预报系统(二)

章节目录

  • 0. 项目结构
  • 1. 剧情回顾
  • 2. 存在的问题
  • 3. SpringBoot集成Redis
    • 3.1 解决方案
    • 3.2 为什么选择Redis?
    • 3.3 使用Redis
      • 3.3.1 开发环境
      • 3.3.2 添加依赖
      • 3.3.3 功能需求
      • 3.3.4 手动编码
      • 3.3.5 测试
  • 4. SpringBoot集成Quartz
    • 4.1 为什么要进行天气数据的同步
    • 4.2 如何实现天气数据的同步呢?
    • 4.3 使用Quartz
      • 4.3.1 添加依赖
      • 4.3.2 手动编码
      • 4.3.3 测试
      • 4.3.4 完成WeatherDataSyncJob
        • 4.3.4.1 准备城市数据
        • 4.3.4.2 获取城市数据
          • 4.3.4.2.1 entity包
          • 4.3.4.2.2 util包
          • 4.3.4.2.3 service层
          • 4.3.4.2.4 job包
          • 4.3.4.2.5 测试
  • 5. SpringBoot集成Thymeleaf
    • 5.1 天气预报服务功能
    • 5.2 天气预报服务API
    • 5.3 使用Thymeleaf
      • 5.3.1 添加依赖
      • 5.3.2 手动编码
        • 5.3.2.1 service层
        • 5.3.2.2 controller层
        • 5.3.2.3 测试

0. 项目结构

整个项目结构图如下:
SpringBoot搭建一个简单的天气预报系统(二)_第1张图片
github源码下载

1. 剧情回顾

在 SpringBoot搭建一个简单的天气预报系统(一)博客中已经实现了一个非常简单的天气预报系统------用户访问这个天气预报系统的时候,那么系统会响应给用户一个天气数据。但这个数据的来源并不是我们自己产生的,我们是依赖于一个第三方的天气预报的数据API。通过调用这个API,我们将返回的Json数据进行解析,再返回给使用这个系统的用户。

2. 存在的问题

这种系统架构会存在什么问题呢?

  1. 我们的数据来源是依赖第三方的API,这种强依赖会导致系统服务的延迟(用户调用我们的系统接口时,而系统再通过HTTP(很耗时)去调用第三方的API,调用结束后,第三方接口将Json数据响应给系统,然后,系统再进行解析,并返回给系统用户,这样的结果会导致响应时间不及时)
  2. 我们使用的是别人的接口(虽然是免费,有些还限制调用次数),这种情况是有风险的:1)、有限制调用次数,会达到上限;2)、不稳定,随时挂掉;3)、若此第三方的API并发做的不太理想,我们这边系统并发稍微高一点,就有可能将第三方API系统给调死了。

3. SpringBoot集成Redis

3.1 解决方案

面对以上问题,这里采用Redis缓存来解决-----期望使用Redis提升应用的并发访问能力

3.2 为什么选择Redis?

这里针对此系统简要说说Redis的优点吧:

  1. 可以使请求响应得更及时:它是基于内存的缓存系统,查询、响应等速度很快(先将天气数据缓存到Redis中去,获取数据时,先从Redis中获取,如果有,直接返回;如果没有,再调用第三方API,调用完毕后,再把数据添加到Redis中去)
  2. 可以有效地减少服务调用的次数:天气的数据信息并不会实时更新,即使是真实的天气预报系统也是半小时或1小时更新,这种场景就非常适合使用缓存(半个小时就调用一次(一天12h也就调用了24次)第三方的API,将数据缓存到Redis中去,在半个小时内,都是从Redis中获取数据的)

3.3 使用Redis

3.3.1 开发环境

  • IDEA:2018.2(安装lombok插件)
  • JDK:8
  • MAVEN:3.6.0
  • SpringBoot:2.3.0
  • Redis:3.0.504

3.3.2 添加依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

3.3.3 功能需求

        用户访问这个天气预报系统的时候,系统先从Redis缓存中进行获取数据,如果有,直接返回;如果没有,再调用第三方API,调用完毕后,将数据返回,并把数据添加到Redis中去

3.3.4 手动编码

这里主要修改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;
}
  1. 使用Redis,需要用到一个类:StringRedisTemplate,它对Redis的API做了一层封装
  2. Redis是K-V的数据结构。这里的K是uri,V是根据uri返回的Json数据
  3. 这里也使用了日志注解@Slf,使用日志进行打印

RedisConstant

public interface RedisConstant {
     
    // 过期时间:1800s
    Long TIME_OUT = 1800L;
}

3.3.5 测试

        启动项目之前,先启动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中过期了。

4. SpringBoot集成Quartz

Quartz是一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。

4.1 为什么要进行天气数据的同步

        在此之前,这个系统使用了Redis缓存来减少了调用第三方API的次数,但我们还是对它是强依赖,毕竟我们仅仅只是数据的搬运工,并不是产生者。况且,我们也做不了产生者,我们只能依赖于第三方的天气服务商来提供数据给我们。
        之前使用Redis缓存来减少调用第三方API的次数是如何做的-----当用户访问系统时,系统才会触发去调用第三方接口(缓存中没有此数据)的动作。实际上,这个动作应该自动完成,不应该等到用户请求时,再去拉取最新的数据(如果是这样的话,这就比较迟了)。

4.2 如何实现天气数据的同步呢?

        实现数据同步就需要一个定时器来帮助我们实现这种定时任务,因为我们的天气数据大概半小时或1小时变更一次,那么,我们的定时器就设置为半小时或1小时去执行这个定时动作。这里的定时器就选择Quartz,它在业界算比较流行

4.3 使用Quartz

4.3.1 添加依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-quartzartifactId>
dependency>

4.3.2 手动编码

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:

  1. JobDetail :定义一个特定的Job
  2. Trigger :定义了一个特定的Job何时以什么样的形式触发

4.3.3 测试

        启动项目时(要启动Redis),控制台会打印一条语句:“Weather Data Sync Job. Start!”。可以把定时任务执行的时间(QuartzConfig 中的TIME)设置为2s,再次启动项目,发现每隔2s,控制台都会打印那条语句。测试成功后,再把时间设置成1800s。

4.3.4 完成WeatherDataSyncJob

        之前在WeatherDataSyncJob类中只是用日志打印了一条语句 ,没做任务业务逻辑,接下来就在那里面做真实的业务----拉取第三方API的天气数据到缓存中去。
        接口url是需要城市ID,所以,我们只需要获取城市列表的数据,然后,对它进行遍历,即可得到城市ID。由于城市列表(打开此链接,右键“另存为”)文件中的数据庞大,所以,这里只选取部分数据-----广东省里面的城市。

4.3.4.1 准备城市数据

        为了减少调用第三方API,可以把这部分数据存储到本地,可用mysql,或者xml文件。这里就用xml文件存储,文件名为:citylist.xml。由于这个文件的内容比较多,这里就不贴了,这个文件在我的项目中,项目的源代码会放到github中去,需要的可以自行下载。这里贴出部分数据:
SpringBoot搭建一个简单的天气预报系统(二)_第2张图片


<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>

4.3.4.2 获取城市数据

将citylist.xml中的数据映射成Java对象

4.3.4.2.1 entity包

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;
}
4.3.4.2.2 util包

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;
    }
}
4.3.4.2.3 service层

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);
}
4.3.4.2.4 job包

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!");
    }
}
4.3.4.2.5 测试

启动项目。
数据开始同步
在这里插入图片描述
数据同步结束
在这里插入图片描述
数据同步到Redis中去了
SpringBoot搭建一个简单的天气预报系统(二)_第3张图片

5. SpringBoot集成Thymeleaf

给此系统添加一个页面

5.1 天气预报服务功能

功能:

  • 按照不同的城市ID来进行查询
  • 查询进今天的预报天气
  • 界面简洁、优雅

5.2 天气预报服务API

获取到该城市ID的天气预报信息

  • GET /report/cityId/{cityId}

5.3 使用Thymeleaf

5.3.1 添加依赖

<dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-thymeleafartifactId>
dependency>

5.3.2 手动编码

5.3.2.1 service层

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();
    }
}

5.3.2.2 controller层

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
    })
})

5.3.2.3 测试

启动项目。
SpringBoot搭建一个简单的天气预报系统(二)_第4张图片
选择下拉框,换一个城市
SpringBoot搭建一个简单的天气预报系统(二)_第5张图片
好了,一个单体架构的天气预报项目就完成了。

你可能感兴趣的:(springboot)