基于springboot+redis+国际化+定时任务的疫情项目【已上线】

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 这是我自己做的一套疫情实时数据项目,有地图、折线图、表格该项目涵盖了大部分springboot项目实际开发所必需的技术因为爬取的页面官网做了变更,所以会有一些小问题,需要自行处理。

一、效果图



1.png

2.png

3.png

4.png


二、技术栈



  1. 基础框架(springboot + mybatis + mybatis-plus)
  2. 缓存数据(redis)
  3. 国际化(一键切换不同语种)
  4. 定时任务(定时更新数据)
  5. 爬虫
  6. 加密
  7. 感知数据变更,进行通知推送
  8. 日志监控
  9. 数据计算和二次存储(为图表服务 - 比如趋势图)
  10. 数据展示和渲染 Thymeleaf Echarts
  11. 模拟http请求


三、项目背景



模拟疫情数据展示网站,做出一个完整的数据采集、数据存储、数据计算、数据展示的疫情数据系统。


四、搜索引擎原理



链接人和内容


网页爬取 -》 网页去重 -》 网页分析 -》 内容保存(倒排索引)


关键字查询 -》 关键字分析 -》 去匹配内容 -》 筛选出比如100条数据 -》 数据排序 (可口可乐的秘方)


分类:

通用型爬虫 和 垂直型爬虫


疫情数据系统:

数据采集 -》 数据存储 -》 数据计算 -》 数据展示


五、数据分析



5.1 分析数据源


确认能够通过代码 获取到数据 (定位到具体的http请求)


5.2 获取疫情数据


  1. 国内各省份表格数据-对应请求


  1. 对应的数据格式:json


  1. 国外表格数据-对应请求


  1. 国内趋势数据


六、数据处理



6.1 初识json


json = javascript object notation (js对象表示法)

独立于语言,具有自我描述性

6.png



Gson -> From Google


new Gson().toJson(Object obj) 将对象转化为json字符串

new Gson().fromJson(String jsonStr, T.class) 将json字符串转化为对象


6.2 数据展示


controller - service - handler


数据加载到model中 返回给html渲染


@Controller
public class DataController {
    @Autowired
    private DataService dataService;
    @GetMapping("/")
    public String list(Model model) {
        List<DataBean> beanList = dataService.list();
        model.addAttribute("beanList", beanList);
        return "list";
    }
}
public interface DataService {
    List<DataBean> list();
}
@Service
public class DataServiceImpl implements DataService {
    @Override
    public List<DataBean> list() {
        return MyDataHandler.getData();
    }
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2> 国内疫情情况如下 </h2>
<br>
<table>
    <thead>
    <tr>
        <th>地区</th>
        <th>现有</th>
        <th>累计</th>
        <th>治愈</th>
        <th>死亡</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="bean:${beanList}">
        <td th:text="${bean.area}">name</td>
        <td th:text="${bean.nowConfirm}">nowConfirm</td>
        <td th:text="${bean.confirm}">confirm</td>
        <td th:text="${bean.heal}">heal</td>
        <td th:text="${bean.dead}">dead</td>
    </tr>
    </tbody>
</table>
</body>
</html>


七、网络请求



HTTP


  1. “应用层协议”
  2. 了解HTTP不同版本的演进
  3. 了解GET和POST请求的区别


    // 需要的参数url
    // 创建一个远程的连接对象  设置方法类型 GET
    // 设置相关参数    发送请求
    // 通过io接收数据后返回
    public static String doGet(String urlStr) {
        HttpURLConnection conn = null;
        InputStream is = null;
        BufferedReader br = null;
        StringBuilder result = new StringBuilder();
        try {
            // 创建远程url连接对象
            URL url = new URL(urlStr);
            // 打开一个连接
            conn = (HttpURLConnection) url.openConnection();
            // 设置为GET请求
            conn.setRequestMethod("GET");
            // 设置重要的参数  连接超时时间 和 读取超时时间
            // 超时时间  更多被距离影响   读取时间  更多被数据量影响
            conn.setConnectTimeout(15000);
            conn.setReadTimeout(60000);
            // header参数设置  可以不设置
            conn.setRequestProperty("Accept", "application/json");
            // 发送请求
            conn.connect();
            // 状态码  200  302  404  500 ?
            // 如果比较时  可能出现空指针  把确定的值放在前面  可以避免
            if (200 == conn.getResponseCode()) {
                is = conn.getInputStream();
                br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                String line;
                while ((line = br.readLine()) != null) {
                    result.append(line);
                }
            } else {
                System.out.println("error responseCode :"
                        + conn.getResponseCode());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
                if (is != null) {
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            conn.disconnect();
        }
        return result.toString();
    }


八、数据存储



8.1 Mybatis整合


SSM的整合 -> 入门课的整合 -> mybatis-plus的整合


明确数据结构 -> 创建mapper文件夹 -> 调用getData方法 将数据据存储到数据库中 -> 查询时从数据库中读取


a) 引入依赖


     

        <!-- mybatis整合
             mybatis-spring-boot-starter
             mybatis-plus -boot-starter
             mysql驱动-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

b) 配置数据库连接参数


spring.datasource.url=jdbc:mysql://localhost:3306/illness?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456

c) 创建数据库


CREATE DATABASE /*!32312 IF NOT EXISTS*/`illness` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `illness`;
/*Table structure for table `epidemic` */
DROP TABLE IF EXISTS `epidemic`;
CREATE TABLE `epidemic` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `area` varchar(100) DEFAULT NULL,
  `confirm` int(11) DEFAULT NULL,
  `now_confirm` int(11) DEFAULT NULL,
  `dead` int(11) DEFAULT NULL,
  `heal` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=308 DEFAULT CHARSET=utf8;

d) 编写类及文件夹


将实体类对应到数据库的表中 属性和字段也需对应


需注意 属性的命名为驼峰命名法


如 nowConfirm 字段的命名是下划线分隔 如 now_confirm


可以使用@TableName和@TableField等注解指定对应关系


@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("epidemic")
public class DataBean implements Serializable {
    // Alt + Insert 调用   安装插件
    private static final long serialVersionUID
            = 4938260405189292371L;
    // Alt + 7 查看类的方法
    // 地区  累计确诊人数  现有确诊人数   死亡人数  治愈人数
    private long id;
    private String area;
//    @TableField("all_confirm")
    // 如果属性名和表中字段名  不一致  可以通过 TableField注解指定
    private int confirm;
    private int nowConfirm;
    private int dead;
    private int heal;
}

在主程序类上 增加@MapperScan注解 指定mapper类所在文件夹


在此文件夹下 创建mapper类 实战中 如表名叫user mapper会命名为UserMapper


@SpringBootApplication
@MapperScan("com.duing.mapper")
public class DataHandlerApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataHandlerApplication.class, args);
    }
}
public interface DataMapper extends BaseMapper<DataBean> {
}

创建service接口及其实现类


整合mybatis-plus,用其提供的IService父接口 和 ServiceImpl实现父类


public interface DataService extends IService<DataBean> {
}
@Service
public class DataServiceImpl
        extends ServiceImpl<DataMapper,DataBean>
        implements DataService {
}

在controller中调用mybatis-plus实现的CRUD方法


@Controller
public class DataController {
    @Autowired
    private DataService dataService;
    @GetMapping("/")
    public String list(Model model) {
        List<DataBean> beanList = dataService.list();
        model.addAttribute("beanList", beanList);
        return "list";
    }
}


8.2 数据初始化及定时更新


在数据查询前,先将数据存储到数据库中,我们称之为数据初始化


可以在项目启动时,先采集一次数据存储到数据库中,然后再进行定期更新


而项目启动时执行且只执行一次的逻辑,可以使用注解 @PostConstruct


//  先将DataHandler托管到spring容器中  使用@Component
//  以便于获取到 dataService对象
@Component
public class DataHandler {
    @Autowired
    private DataService dataService;
    // 数据初始化
    //   在服务器加载Servlet时运行  且 只运行一次
    @PostConstruct
    public void saveData() {
        List<DataBean> dataBeans = getData();
        // mybatis-plus提供了可用的方法
        // 删除全部数据  批量新增数据
        dataService.remove(null);
        dataService.saveBatch(dataBeans);
    }
...
}

定时更新其实也是定时任务


可以通过注解@Scheduled + cron表达式来实现


Scheduled 英文原意是调度的意思 意思是我们将某段逻辑 按照指定的时间间隔 进行调度 即为定时处理


使用方式如下:


首先在入口类上 打开定时任务开关 使用注解 @EnableScheduling


@SpringBootApplication
@MapperScan("com.duing.mapper")
@EnableScheduling
public class DataHandlerApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataHandlerApplication.class, args);
    }
}

然后在方法上 使用 @Scheduled 搭配cron表达式 决定方法多久执行一次


    // 定时更新
    @Scheduled(cron = "0 0 0/1 * * ? ")
    public void updateData(){
        System.out.println("要更新数据啦");
        System.out.println("当前时间:" + dateFormat.format(new Date()));
        List<DataBean> dataBeans = getData();
        dataService.remove(null);
        dataService.saveBatch(dataBeans);
    }

其中cron表达式,是由6个表达不同时间单位的字段拼接而成


可以通过在线cron表达式生成器来生成


cron表达式几乎可以满足所有定时执行的需求


可以生成各类需求对应的表达式熟悉一下


除此之外,@Scheduled还支持固定时间间隔的参数设置


分别为 fixedRate 和 fixedDelay


// 固定频率任务   以ms为单位  10000代表每10s执行一次
//   使用时要注意间隔时间  和  任务消耗时间的  大小关系      
//   如设置间隔10s  而方法需执行20s   那么方法会等待上一次执行完成才会执行  
//   唤起方法真正的时间间隔为20s
@Scheduled(fixedRate = 10000)
public void updateData1(){}
// 固定间隔任务   上一次执行结束后   再隔10s进行下一次执行
// 如设置间隔10s  而方法需执行20s   那么会从上一次执行完成后开始计算  10s后开始下一次执行
// 唤起方法真正的时间间隔为30s
@Scheduled(fixedDelay = 10000)
public void updateData2(){}


九、图形化数据处理



9.1 折线图


<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="js/echarts.min.js"></script>
</head>
<body>
<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 600px;height:400px;"></div>
<script th:inline="javascript">
    var dateStr = [[${dateList}]];
    var confirmStr = [[${confirmList}]];
    var suspectStr = [[${suspectList}]];
    // 基于准备好的dom  初始化实例
    var mychart = echarts.init(document.getElementById("main"));
    var option = {
        // 标题
        title:{
            text: '全国疫情新增趋势'
        },
        legend:{
            data:['新增确诊','新增疑似']
        },
        // x轴的数据
        xAxis:{
            data: dateStr
        },
        // y轴的数据类型
        yAxis:{
            type:'value'
        },
        series:[
            {
                name: '新增确诊',
                type: 'line',
                data: confirmStr
            },
            {
                name: '新增疑似',
                type: 'line',
                data: suspectStr
            }
        ]
    };
    // 将参数设置进去
    mychart.setOption(option);
</script>
</body>
</html>

9.2 地图


<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="js/echarts.min.js"></script>
    <script type="text/javascript" src="js/china.js"></script>
</head>
<body>
<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 1000px;height:700px;"></div>
<script th:inline="javascript">
    var dataStr1 = [[${mapData1}]];
    var dataStr2 = [[${mapData2}]];
    // 基于准备好的dom  初始化实例
    var mychart = echarts.init(document.getElementById("main"));
    var option = {
        title: {
            text: '疫情地图',
            subtext: '仅供参考',
            x: 'center'
        },
        tooltip: {
            trigger: 'item'
        },
        legend: {
            orient: 'vertical',
            left: 'left',
            data: ['现有确诊', '累计确诊']
        },
        visualMap: {
            type: 'piecewise',
            pieces: [
                {min: 10000, max: 1000000, label: '10000人及以上', color: '#de1f05'},
                {min: 1000, max: 9999, label: '1000-9999人', color: '#ff2736'},
                {min: 500, max: 999, label: '500-999人', color: '#ff6341'},
                {min: 100, max: 499, label: '100-499人', color: '#ffa577'},
                {min: 10, max: 99, label: '10-99人', color: '#ffcea0'},
                {min: 1, max: 9, label: '1-9人', color: '#ffe7b2'},
                {min: 0, max: 0, label: '0人', color: '#e2ebf4'},
            ],
            calculate: true
        },
        series: [
            {
                name: '现有确诊',
                type: 'map',
                mapType: 'china',
                roam: false,
                label: {
                    normal: {
                        position: 'center',
                        show: true
                    }
                },
                data: JSON.parse(dataStr1)
            },
            {
                name: '累计确诊',
                type: 'map',
                mapType: 'china',
                roam: false,
                label: {
                    normal: {
                        position: 'center',
                        show: true
                    }
                },
                data: JSON.parse(dataStr2)
            }
        ]
    };
    // 将参数设置进去
    mychart.setOption(option);
</script>
</body>
</html>


9.3 拓展部分


我们可以根据国内表格数据的处理方式、以及图形化数据的处理方式,举一反三的去处理海外数据,只要掌握了处理流程,其实是可以模拟腾讯新闻,将全部数据展示出来的,要注意的是,我们的项目是为了练习springboot+mybatis+thymeleaf+echarts的使用,了解http请求+json数据处理+定时任务等等功能是如何实现的,那么对数据采集方面要慎重,尽量只采集需要的少部分数据,并且控制请求次数和请求频率,一定要做到“友好访问”。


十、页面数据处理



Jsoup(Ji soup 鸡汤)----页面解析器


解析一个HTML文档的方式如下:
String html = "<html><head><title>First parse</title></head>"
+ "<body><p>Parsed HTML into a doc.</p></body></html>";
Document doc = Jsoup.parse(html);
解析器能够尽最大可能从你提供的HTML文档中创建出一个干净的解析结果,无论HTML的格式是否完整。
比如它可以处理:
1)没有关闭的标签 
   <p>Lorem <p>Ipsum  
   可以解析成
   <p>Lorem</p> <p>Ipsum</p>
2)隐式标签 (补充没有显示的标签)
   它可以自动将 <td>Table data</td> 包装成 <table><tr><td>...
3)创建可靠的文档结构  
   html标签包含head 和 body,且在head只出现恰当的元素
文档的对象模型                
1)文档由多个Elements和TextNodes组成
2)继承结构如下:Document继承Element继承Node. TextNode继承 Node.
3)一个Element包含一系列的子节点,并拥有一个父Element。他们还唯一提供了一个子元素过滤列表。


十一、国际化



根据不同的用户群,显示不同的语言,此功能叫做国际化,如丁香医生页面切换语种的功能。


要想实现动态显示不同语言,首先要将页面中的数据转为动态显示,放在配置文件中读取


创建list.properties


list.title=步尔斯特
list.h2=国内疫情情况如下
list.table.name1=地区
list.table.name2=现有确诊
list.table.name3=累计确诊
list.table.name4=治愈
list.table.name5=死亡

我们将list.properties放在i18n的文件夹下,然后在application.properties中设置好指定配置


spring.messages.basename=i18n.list

这样就可以再创建一个list_en_US.properties文件,内容如下


list.title=burst
list.h2=As Follows
list.table.name1=Area
list.table.name2=Now Confirm
list.table.name3=Confirm
list.table.name4=Heal
list.table.name5=Dead

对应的list.html页面修改如下


<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="#{list.title}">Title</title>
    <link rel="stylesheet" th:href="@{css/bootstrap.css}">
</head>
<body>
<h2 th:text="#{list.h2}"> 国内疫情情况如下 </h2>
<br>
<table class="table table-hover">
    <thead>
    <tr>
        <th><p th:text="#{list.table.name1}">地区</p></th>
        <th><p th:text="#{list.table.name2}">现有</p></th>
        <th><p th:text="#{list.table.name3}">累计</p></th>
        <th><p th:text="#{list.table.name4}">治愈</p></th>
        <th><p th:text="#{list.table.name5}">死亡</p></th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="bean:${beanList}">
        <td th:text="${bean.area}">name</td>
        <td th:text="${bean.nowConfirm}">nowConfirm</td>
        <td th:text="${bean.confirm}">confirm</td>
        <td th:text="${bean.heal}">heal</td>
        <td th:text="${bean.dead}">dead</td>
    </tr>
    </tbody>
</table>
</body>
</html>

当我们运行项目后,通过浏览器更改语言,就可以切换成不同语种的显示了。


以chrome为例,将中文置顶,则显示中文,将英语(美国)置顶,则显示英文,这里要注意,如果没有英语(美国)这个语言,可以在底下的添加语言中,先搜索后添加。


那是如何实现切换的呢,当我们调出控制台,比对两笔请求的http,可以发现请求头中Accept-Language参数是不同的,请求中文时以zh-CN开头,请求英文时以en-US开头,如果你对其他语种感兴趣,也可以看一下其他语种的简称。


但是通过浏览器切换仍有些麻烦,所以我们尝试在页面中添加切换按钮


当我们设置了参数lan用来区分不同语种时,需要使用一个处理器来接收语种参数


然后将处理器注入到spring容器中


同时增加一个list_zh_CN.properties文件,声明各字段的中文值

再次启动项目,就可在页面中进行中英文切换了。


十二、Redis的使用



三大客户端框架: Jedis Redission Lettuce


其中spring-data-redis 是将redis整合在spring中使用的jar包


在springboot2.0以后的版本 将原本的jedis升级成Lettuce 而本质上是一种连接池的封装


redis数据的存储,可以在项目启动时存储,另外可以在请求第一次被调用时存储(懒加载)

请求 -》 redis(存在 -》 直接返回,不存在 -》 mysql中查询 -》 存储到redis中 -》 返回)

-》 mysql(存在 ,不存在 -》 发送http请求 -》 数据清洗和计算后存储 -》 返回)


以折线图数据为例

在处理数据的过程中,有三种数据形态


1)url直接返回的原始数据

2)第一次解析后处理的 List<GraphBean>

3) echarts的直观需要的三个list, 一个对应x轴,两个对应y轴


第一种数据重要,但一般不放在redis中存储,甚至不放在mysql中。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
Java 数据库连接 Maven
springBoot:项目建立&配置修改&yaml的使用&resource 文件夹(二)
本文档介绍了如何创建一个基于Maven的项目,并配置阿里云仓库、数据库连接、端口号、自定义启动横幅及多环境配置等。同时,详细说明了如何使用YAML格式进行配置,以及如何处理静态资源和模板文件。文档还涵盖了Spring Boot项目的`application.properties`和`application.yaml`文件的配置方法,包括设置数据库驱动、URL、用户名、密码等关键信息,以及如何通过配置文件管理不同环境下的应用设置。
|
1月前
|
NoSQL Java MongoDB
Springboot WebFlux项目结合mongodb进行crud
这篇文章介绍了如何使用Spring Boot WebFlux框架结合MongoDB进行基本的CRUD(创建、读取、更新、删除)操作,包括项目设置、实体类和Repository的创建、控制器的实现以及配置文件的编写。
46 0
Springboot WebFlux项目结合mongodb进行crud
|
14天前
|
Java 应用服务中间件
SpringBoot获取项目文件的绝对路径和相对路径
SpringBoot获取项目文件的绝对路径和相对路径
52 1
SpringBoot获取项目文件的绝对路径和相对路径
|
4天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
15 2
|
9天前
|
分布式计算 关系型数据库 MySQL
SpringBoot项目中mysql字段映射使用JSONObject和JSONArray类型
SpringBoot项目中mysql字段映射使用JSONObject和JSONArray类型 图像处理 光通信 分布式计算 算法语言 信息技术 计算机应用
30 8
|
6天前
|
NoSQL Java API
springboot项目Redis统计在线用户
通过本文的介绍,您可以在Spring Boot项目中使用Redis实现在线用户统计。通过合理配置Redis和实现用户登录、注销及统计逻辑,您可以高效地管理在线用户。希望本文的详细解释和代码示例能帮助您在实际项目中成功应用这一技术。
15 3
|
1月前
|
JavaScript 前端开发 Java
解决跨域问题大集合:vue-cli项目 和 java/springboot(6种方式) 两端解决(完美解决)
这篇文章详细介绍了如何在前端Vue项目和后端Spring Boot项目中通过多种方式解决跨域问题。
334 1
解决跨域问题大集合:vue-cli项目 和 java/springboot(6种方式) 两端解决(完美解决)
|
16天前
|
JavaScript 前端开发 Java
SpringBoot项目的html页面使用axios进行get post请求
SpringBoot项目的html页面使用axios进行get post请求
40 2
|
16天前
|
前端开发 Java Spring
SpringBoot项目thymeleaf页面支持词条国际化切换
SpringBoot项目thymeleaf页面支持词条国际化切换
44 2
|
16天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
32 1