上报系统:java工程,SpringBoot框架
1.Spring Boot简介
Spring Boot是一个基于Spring之上的快速应用构建框架。使用Spring Boot可以快速开发出基于Spring的应用。Spring Boot主要解决两方面的问题。
-
依赖太多问题
-
轻量级JavaEE开发,需要导入大量的依赖
-
依赖之间还存在版本冲突
-
-
配置太多问题
- 大量的XML配置
Spring Boot内部整合了大量的依赖,而且经过大量测试,选择的依赖都是没有版本冲突的。Spring Boot简化了大量的配置,通过少量的配置,就可以让程序工作。
开发Spring Boot程序的基本步骤
- 导入Spring Boot依赖(起步依赖)
- 编写
application.properties
配置文件 - 编写
Application
入口程序
2.导入Maven依赖
主要导入以下依赖:
- 导入
Spring Boot
依赖 - 操作JSON导入
FastJSON
依赖 - 导入
Kafka
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.13.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xu.flinkpyg</groupId>
<artifactId>report</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>report</name>
<description>Demo project for Spring Boot</description>
<modelVersion>4.0.0</modelVersion>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>1.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>http-client</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3. 创建项目包结构
包名 | 说明 |
---|---|
com.xu.report.controller | 存放Spring MVC的controller |
com.xu.report.bean | 存放相关的Java Bean实体类 |
com.xu.report.util | 用来存放相关的工具类 |
4.验证Spring Boot工程是否创建成功
步骤
- 创建SpringBoot
入口程序
Application
package com.xu.report;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// 添加@SpringBootApplication注解,表示该类是个启动类
@SpringBootApplication
public class ReportApplication {
public static void main(String[] args) {
SpringApplication.run(ReportApplication.class,args);
}
}
- 创建
application.properties
配置文件
properties server.port=8080
- 编写一个简单
Spring MVC
Controller/Handler,接收浏览器请求参数并打印回显
package com.xu.report.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// 表示这是一个Controller,并且其中的所有的方法都是带有@ResponseBody的注解
@RestController
public class TestController {
@RequestMapping
public String test(String json) {
System.out.println(json);
return json;
}
}
- 打开浏览器测试
http://127.0.0.1:8080/test?json=1212
结果:
五 安装Kafka-Manager
Kafka-manager是Yahoo!开源的一款Kafka监控管理工具。
资料:
链接:https://pan.baidu.com/s/18iC5OsAloqeu_6X7-cPcTQ
提取码:n40p
安装步骤
-
上传
资料\软件包
中的kafka-manager-1.3.3.7.tar.gz
-
解压到
/export/servers
tar -zxf kafka-manager-1.3.3.7.tar.gz -C /export/servers/
-
修改
conf/application.conf
kafka-manager.zkhosts="node01:2181,node02:2181,node03:2181"
-
启动
zookeeper
cd /export/servers/zookeeper-3.4.5-cdh5.14.0 bin/zkServer.sh start
-
启动
kafka
# 启动3台机器的Kafka cd /export/servers/kafka_2.11-0.10.1.0 bin/kafka-server-start.sh config/server.properties > /dev/null 2>&1 &
-
直接运行
bin/kafka-manager
cd /export/servers/kafka-manager-1.3.3.7 nohup bin/kafka-manager 2>&1 &
-
浏览器中使用
node01:9000
访问即可
默认kafka-manager的端口号为
9000
,如果该端口被占用,请使用下面的命令修改端口
bin/kafka-manager -Dconfig.file=/export/servers/kafka-manager-1.3.3.7/conf/application.conf -Dhttp.port=10086
6 编写Kafka生产者配置工具类
由于我们项目要操作Kafka, 我们先来构建出KafkaTemplate, 这是一个Kafka的模板对象, 通过它我们可以很方便的发送消息到Kafka.
开发步骤
- 编写Kafka生产者配置
- 编写Kafka生产者SpringBoot配置工具类
KafkaProducerConfig
,构建KafkaTemplate
实现
-
导入Kafka生产者配置文件
将下面的代码拷贝到
application.properties
中
#
# kakfa
#
#kafka的服务器地址
kafka.bootstrap_servers_config=node01:9092,node02:9092,node03:9092
#如果出现发送失败的情况,允许重试的次数
kafka.retries_config=0
#每个批次发送多大的数据
kafka.batch_size_config=4096
#定时发送,达到1ms发送
kafka.linger_ms_config=1
#缓存的大小
kafka.buffer_memory_config=40960
#TOPIC的名字
kafka.topic=pyg
>**定时定量**
>
>1. kafka生产者发送一批数据的大小:kafka.producer.batch.size=4096 (单位:字节)
>
> 实际环境可以调大一些,提高效率
>
>2. 定时发送:kafka.producer.linger=1
>
> 达到一毫秒后发送
-
编写
KafkaProducerConfig
,主要创建KafkaTemplate
,用于发送Kafka消息- 使用@Value("
${配置项}
")来读取配置 - 构建
DefaultKafkaProducerFactory
- 构建
KafkaTemplate
@Configuration public class KafkaConfig { @Value("${kafka.bootstrap_servers_config}") private String bootstrap_servers_config; @Value("${kafka.retries_config}") private int retries_config; @Value("${kafka.batch_size_config}") private int batch_size_config; @Value("${kafka.linger_ms_config}") private int linger_ms_config; @Value("${kafka.buffer_memory_config}") private int buffer_memory_config; @Bean public KafkaTemplate<String, String> kafkaTemplate() { Map<String, Object> configs = new HashMap<>(); configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap_servers_config); configs.put(ProducerConfig.RETRIES_CONFIG, retries_config); configs.put(ProducerConfig.BATCH_SIZE_CONFIG, batch_size_config); configs.put(ProducerConfig.LINGER_MS_CONFIG, linger_ms_config); configs.put(ProducerConfig.BUFFER_MEMORY_CONFIG, buffer_memory_config); configs.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, RoundRobinPartitioner.class); configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); ProducerFactory<String, String> producerFactory = new DefaultKafkaProducerFactory<>(configs); return new KafkaTemplate<String, String>(producerFactory); } }
- 使用@Value("
-
在
test
测试源码中创建一个Junit测试用例-
整合Spring Boot Test
-
注入
KafkaTemplate
-
测试发送100条消息到
test
topic
@RunWith(SpringRunner.class) @SpringBootTest public class KafkTest { @Autowired private KafkaTemplate kafkaTemplate; @Test public void sendTest01() { for(int i = 0; i < 100; i++) { kafkaTemplate.send("test", "key", "test msg!"); } } }
-
-
在KafkaManager创建
test
topic,三个分区、两个副本
注意:如果发现 kafka-manager中新建的Cluster,Brokers的个数为0:
原因:查看zookeeper和kafka集群是否已经启动,如果已经启动,那么就是上面配置的问题:
node01:2181,node02:2181,node03:2181/kafka
这里后缀kafka,默认是没有的,我这边里有,是因为配置kafka集群的时候,加上的后缀,这个配置在kafka的 config/server.properties中的zookeeper.connect
创建topic,3个分区,2个副本
-
启动
kafka-console-consumer
bin/kafka-console-consumer.sh --zookeeper node01:2181/kafka --from-beginning --topic test
启动SpringBoot的程序:
可能出现的错误:
spring boot Unable to find a @SpringBootConfiguration, you need to use @ContextConfigur
包的目录,需要保持一致!!!
- 打开kafka-manager的consumer监控页面,查看对应的
logsize
参数,消息是否均匀的分布在不同的分区中
控制台结果:
从上面可知,虽然有三个分区,但是消息发送到了一个!!
7 均匀分区
编写RoundRobbinPartitioner
,实现Partitioner
接口,确保消息能够发送到Kafka的每个分区
- 实现
Partitioner
接口的partition方法 - 创建一个
AtomicInteger
变量,用来保存当前的计数器,每次生产一条消息加1 - 使用计数器变量模除以
分区数量
得到当前消息需要发送的分区号
八 上报系统,正式开发
上报服务系统要能够接收http请求,并将http请求中的数据写入到kafka
步骤
-
创建
Message
实体类对象所有的点击流消息都会封装到Message实体类中
-
设计一个Controller来接收http请求
-
将http请求发送的消息封装到一个
Message
实体类对象 -
使用
FastJSON
将Message
实体类对象转换为JSON字符串 -
将JSON字符串使用
kafkaTemplate
写入到kafka
-
返回给客户端一个写入结果JSON串
实现
- 创建
Message
实体类
- 包含以下字段:消息次数(count)、消息时间(timeStamp)、消息体(message)
- 生成getter/setter、toString方法
Message.java
package com.xu.report.bean;
public class Message {
// 消息次数
private int count;
// 消息的时间戳
private long timeStamp;
// 消息体
private String message;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public long getTimeStamp() {
return timeStamp;
}
public void setTimeStamp(long timeStamp) {
this.timeStamp = timeStamp;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "Message{" +
"count=" + count +
", timeStamp=" + timeStamp +
", message='" + message + '\'' +
'}';
}
}
-
在
com.xu.report.controller
包下创建ReportController
类- 编写
receiveData
Handler接收从客户端JSON数据,并将响应结果封装到Map
结构中,返回给客户端
注意:接收JSON数据要在参数前面添加@RequestBody
注解 - 将接收的参数封装到
Message
实体类 - 使用
FastJSON
将Message
实体类对象转换为JSON字符串 - 将JSON字符串发送到Kafka的
pyg
topic - 将响应结果封装到
Map
结构中,返回给客户端
注意:
- 在ReportController类上要添加
@RestController
注解 - 需要添加
@AutoWired
注解来注入KafkaTemplate - 请求参数上要加上
@RequestBody
注解
- 编写
ReportController.java
package com.xu.report.controller;
import com.alibaba.fastjson.JSON;
import com.xu.report.bean.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class ReportController {
@Autowired
KafkaTemplate kafkaTemplate;
@RequestMapping("/receive")
public Map<String, String> receive(@RequestBody String json) {
Map<String, String> map = new HashMap<>();
try {
//构建Message
Message msg = new Message();
msg.setMessage(json);
msg.setCount(1);
msg.setTimeStamp(System.currentTimeMillis());
String msgJson = JSON.toJSONString(msg);
//发送Message到kafka
kafkaTemplate.send("pyg", msgJson);
map.put("success", "true");
} catch (Exception ex) {
ex.printStackTrace();
map.put("success", "false");
}
return map;
}
}
9 模拟生产点击流日志消息到Kafka
为了方便进行测试,我们可以使用一个消息生成工具来生成点击流日志,然后发送给上报服务系统。该消息生成工具可以一次生成100条ClickLog信息,并转换成JSON,通过HttpClient把消息内容发送到我们编写好的ReportController。
步骤
- 编写ClickLog实体类(ClickLog.java)
- 编写点击流日志生成器(ClickLogGenerator.java)
package com.xu.report.bean;
public class ClickLog {
//频道ID
private long channelID ;
//产品的类别ID
private long categoryID ;
//产品ID
private long produceID ;
//用户的ID
private long userID ;
//国家
private String country ;
//省份
private String province ;
//城市
private String city ;
//网络方式
private String network ;
//来源方式
private String source ;
//浏览器类型
private String browserType;
//进入网站时间
private Long entryTime ;
//离开网站时间
private long leaveTime ;
public long getChannelID() {
return channelID;
}
public void setChannelID(long channelID) {
this.channelID = channelID;
}
public long getCategoryID() {
return categoryID;
}
public void setCategoryID(long categoryID) {
this.categoryID = categoryID;
}
public long getProduceID() {
return produceID;
}
public void setProduceID(long produceID) {
this.produceID = produceID;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getNetwork() {
return network;
}
public void setNetwork(String network) {
this.network = network;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getBrowserType() {
return browserType;
}
public void setBrowserType(String browserType) {
this.browserType = browserType;
}
public Long getEntryTime() {
return entryTime;
}
public void setEntryTime(Long entryTime) {
this.entryTime = entryTime;
}
public long getLeaveTime() {
return leaveTime;
}
public void setLeaveTime(long leaveTime) {
this.leaveTime = leaveTime;
}
public long getUserID() {
return userID;
}
public void setUserID(long userID) {
this.userID = userID;
}
@Override
public String toString() {
return "ClickLog{" +
"channelID=" + channelID +
", categoryID=" + categoryID +
", produceID=" + produceID +
", country='" + country + '\'' +
", province='" + province + '\'' +
", city='" + city + '\'' +
", network='" + network + '\'' +
", source='" + source + '\'' +
", browserType='" + browserType + '\'' +
", entryTime=" + entryTime +
", leaveTime=" + leaveTime +
", userID=" + userID +
'}';
}
}
package com.xu.report.util;
import com.alibaba.fastjson.JSONObject;
import com.xu.report.bean.ClickLog;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.junit.Test;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
/**
* 点击流日志模拟器
*/
public class ClickLogGenerator {
private static Long[] channelID = new Long[]{1l,2l,3l,4l,5l,6l,7l,8l,9l,10l,11l,12l,13l,14l,15l,16l,17l,18l,19l,20l};//频道id集合
private static Long[] categoryID = new Long[]{1l,2l,3l,4l,5l,6l,7l,8l,9l,10l,11l,12l,13l,14l,15l,16l,17l,18l,19l,20l};//产品类别id集合
private static Long[] produceID = new Long[]{1l,2l,3l,4l,5l,6l,7l,8l,9l,10l,11l,12l,13l,14l,15l,16l,17l,18l,19l,20l};//产品id集合
private static Long[] userID = new Long[]{1l,2l,3l,4l,5l,6l,7l,8l,9l,10l,11l,12l,13l,14l,15l,16l,17l,18l,19l,20l};//用户id集合
/**
* 地区
*/
private static String[] contrys = new String[]{"china"};//地区-国家集合
private static String[] provinces = new String[]{"HeNan","HeBei"};//地区-省集合
private static String[] citys = new String[]{"ShiJiaZhuang","ZhengZhou", "LuoYang"};//地区-市集合
/**
*网络方式
*/
private static String[] networks = new String[]{"电信","移动","联通"};
/**
* 来源方式
*/
private static String[] sources = new String[]{"直接输入","百度跳转","360搜索跳转","必应跳转"};
/**
* 浏览器
*/
private static String[] browser = new String[]{"火狐","qq浏览器","360浏览器","谷歌浏览器"};
/**
* 打开时间 离开时间
*/
private static List<Long[]> usetimelog = producetimes();
//获取时间
public static List<Long[]> producetimes(){
List<Long[]> usetimelog = new ArrayList<Long[]>();
for(int i=0;i<100;i++){
Long [] timesarray = gettimes("2020-07-12 24:60:60:000");
usetimelog.add(timesarray);
}
return usetimelog;
}
private static Long [] gettimes(String time){
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss:SSS");
try {
Date date = dateFormat.parse(time);
long timetemp = date.getTime();
Random random = new Random();
int randomint = random.nextInt(10);
long starttime = timetemp - randomint*3600*1000;
long endtime = starttime + randomint*3600*1000;
return new Long[]{starttime,endtime};
} catch (ParseException e) {
e.printStackTrace();
}
return new Long[]{0l,0l};
}
/**
* 模拟发送Http请求到上报服务系统
* @param url
* @param json
*/
public static void send(String url, String json) {
try {
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(url);
JSONObject response = null;
try {
StringEntity s = new StringEntity(json.toString(), "utf-8");
s.setContentEncoding("utf-8");
// 发送json数据需要设置contentType
s.setContentType("application/json");
post.setEntity(s);
HttpResponse res = httpClient.execute(post);
if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// 返回json格式:
String result = EntityUtils.toString(res.getEntity());
System.out.println(result);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Random random = new Random();
for (int i = 0; i < 100; i++) {
//频道id 类别id 产品id 用户id 打开时间 离开时间 地区 网络方式 来源方式 浏览器
ClickLog clickLog = new ClickLog();
clickLog.setChannelID(channelID[random.nextInt(channelID.length)]);
clickLog.setCategoryID(categoryID[random.nextInt(categoryID.length)]);
clickLog.setProduceID(produceID[random.nextInt(produceID.length)]);
clickLog.setUserID(userID[random.nextInt(userID.length)]);
clickLog.setCountry(contrys[random.nextInt(contrys.length)]);
clickLog.setProvince(provinces[random.nextInt(provinces.length)]);
clickLog.setCity(citys[random.nextInt(citys.length)]);
clickLog.setNetwork(networks[random.nextInt(networks.length)]);
clickLog.setSource(sources[random.nextInt(sources.length)]);
clickLog.setBrowserType(browser[random.nextInt(browser.length)]);
Long[] times = usetimelog.get(random.nextInt(usetimelog.size()));
clickLog.setEntryTime(times[0]);
clickLog.setLeaveTime(times[1]);
String jonstr = JSONObject.toJSONString(clickLog);
System.out.println(jonstr);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
send("http://localhost:8888/receive", jonstr);
}
}
}
点击流日志字段
字段 | 说明 |
---|---|
channelID | 频道ID |
categoryID | 产品的类别ID |
produceID | 产品ID |
country | 国家 |
province | 省份 |
city | 城市 |
network | 网络方式(移动、联通、电信…) |
source | 来源方式 |
browserType | 浏览器类型 |
entryTime | 进入网站时间 |
leaveTime | 离开网站时间 |
userID | 用户ID |
10 验证测试代码
步骤
- 创建Kafka的topic(
pyg
) - 使用
kafka-console-consumer.sh
消费 topic中的数据 - 启动上报服务
- 执行
ClickLogGenerator
的main方法,生成一百条用户浏览消息到Kafka
实现
-
创建kafka topic
bin/kafka-topics.sh --create --zookeeper node01:2181/kafka --replication-factor 2 --partitions 3 --topic pyg
-
启动kafka消费者
bin/kafka-console-consumer.sh --zookeeper node01:2181/kafka --from-beginning --topic pyg
-
执行程序
启动SpringBoot项目;
执行ClickLogGenerator中的main方法
结果:
注意:
1.程序的端口是否对应;
2.kafka-manager没有正常关闭,启动失败,
cd /export/servers/kafka-manager-1.3.3.7
tail -400 nohup.out
查看一下:
一般删除 kafka-manager-1.3.3.7目录下的RUNNING_PID,再次启动即可!!