简介:基于Java的手机APP信息统计分析系统设计源码,是一套面向移动端开发者、数据分析工程师及后端架构人员的完整项目,主要解决APP用户行为数据的采集、传输、存储、离线分析与可视化展示问题,帮助团队掌握功能使用情况并持续优化产品体验。项目采用多模块Maven工程结构,包含日志采集上报客户端、Flume日志接入、Web收集与可视化后台、公共模块以及Hive离线分析等环节,从数据入口到报表输出形成完整闭环,适合作为大数据分析项目的工程范例。资源共57个文件,核心为24个Java类文件、14个XML配置、2个JSP页面与2个Properties配置,覆盖业务逻辑、框架配置与动态页面;另含GeoIP数据库、Markdown/Word说明文档及图片素材,便于理解IP归属地处理和项目部署结构。压缩包56.75MB,已有338人学习下载,适合需要研究APP日志分析全链路、参考源码进行二次开发或准备相关课程设计/毕业设计的开发者。
1. 基于 Java 的手机 APP 信息统计分析系统:一份能跑通全链路的源代码
这套源码把“用户打开 App 之后发生了什么”这件事,完整地拆成了六段:客户端模拟打点、Web 端收集日志、Flume 采集落盘、Hive 清洗建模、可视化报表展示。我用它复现用户行为分析系统时最大的感受是:它不像网上那些只有 CRUD 的管理系统,而是把日志从产生到变成报表的每一环都留在工程里,适合正在学大数据、准备 Java 面试或者做课程设计的人。源码里有 57 个文件,24 个 Java 类、14 个 XML 配置、2 个 JSP 页面,还有一份完整的项目文档和 MaxMind 的 GeoIP 库,拿到的就是整套离线分析链路。接下来我从模块边界讲起,一直拆到排错和批量造数的技巧。
2. 先看模块边界:五个 Maven 子工程的分工与数据流向
2.1 从工程名反推架构:谁在采集、谁在清洗、谁在展示
拿到源码先别急着跑,把pom.xml和src目录扫一遍,架构基本就清楚了。这套源码按“模块职责”拆了五个 Maven 子工程,我整理成一张表,对照着看源码会顺很多。
| 模块目录 | 责任 | 关键点 |
|---|---|---|
app_logs_client | 模拟手机 App 端,生成启动日志、页面访问日志 | 产生日志的直接入口,可独立运行 |
app_logs_collect_web | 接收客户端上报的日志,写入本地磁盘 | Java Web 项目,处理 HTTP 请求 |
app_logs_flume | Flume 采集配置与打包 | 监控日志文件,写入 HDFS |
app_logs_common | 公共类:常量、日志实体、IP 解析工具 | 被其他模块依赖,是复用核心 |
app_logs_hive | Hive ETL:建表语句、清洗 SQL、指标计算 | 离线分析的“加工车间” |
app_logs_visualize_web | 可视化 Web 端,读取分析结果并渲染报表 | JSP/HTML 页面,面向业务展示 |
模块之间的数据流是一条直线:client模拟用户行为,把日志通过 HTTP 打到collect_web;collect_web把日志落成本地文件;flume监控这批文件,搬运到 HDFS;hive模块在 HDFS 数据之上建外部表、跑指标 SQL;最终visualize_web把指标查出来画成报表。理解这条链路比理解任何一个模块都重要,因为后面所有排错都是顺着这条线找断点的。
2.2 客户端打点:一条模拟日志是怎么被造出来的
app_logs_client是整个系统的源头。它做的事情很纯粹:模拟手机 App 启动、页面跳转、退出登录这些行为,然后把行为数据组装成一条 JSON 或 key-value 日志,发给收集端。常见的做法是拿 Java 的HttpURLConnection直接 POST,不引太重的东西。我复现时习惯先看它生成的日志长什么样,再决定 Hive 那边怎么建表。
// 模拟一条启动日志并上报到 collect_web String url = "http://localhost:8080/app_logs_collect_web/log/start"; HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); conn.setDoOutput(true); String json = "{" + "\"appId\":\"app_001\"," + "\"deviceId\":\"864393028571234\"," + "\"appVersion\":\"2.1.0\"," + "\"osType\":\"android\"," + "\"network\":\"wifi\"," + "\"entry\":\"push\"," + "\"action\":\"startup\"" + "}"; conn.getOutputStream().write(json.getBytes("UTF-8")); int code = conn.getResponseCode(); System.out.println("上报结果 code = " + code);这段代码的核心参数在 JSON 体里:deviceId是设备唯一标识,后续 Hive 算新增用户、活跃用户全靠它去重;entry表示用户从哪个入口进来,比如push是推送、icon是点击图标;action是具体动作。注意deviceId必须保持稳定,如果客户端每次启动都随机生成一个新 ID,后面所有用户指标都会虚高。我在第一次跑通后踩过这个坑,造数脚本里把deviceId写死成一个数组循环用,就正常了。
2.3 Flume 落 HDFS:TAILDIR 监控配置与参数拆解
collect_web把日志写到本地磁盘之后,Flume 负责搬运。这套源码里用的是 TAILDIR Source,它最大的好处是能记录读取位置:Flume 重启之后不会重复消费已经读过的日志。看app_logs_flume模块里的配置文件,核心是下面这段。
a1.sources = r1 a1.channels = c1 a1.sinks = k1 a1.sources.r1.type = TAILDIR a1.sources.r1.positionFile = /opt/flume/data/taildir_position.json a1.sources.r1.filegroups = f1 a1.sources.r1.filegroups.f1 = /opt/logs/app_logs/.*\.log a1.sources.r1.batchSize = 500 a1.sources.r1.channels = c1 a1.channels.c1.type = memory a1.channels.c1.capacity = 10000 a1.channels.c1.transactionCapacity = 2000 a1.sinks.k1.type = hdfs a1.sinks.k1.hdfs.path = /user/hive/warehouse/app_logs.db/ods_start_log/dt=%Y%m%d a1.sinks.k1.hdfs.filePrefix = app_log a1.sinks.k1.hdfs.fileType = DataStream a1.sinks.k1.hdfs.writeFormat = Text a1.sinks.k1.hdfs.rollInterval = 60 a1.sinks.k1.channels = c1三个关键参数要调明白:positionFile保存读取进度,如果多套 Flume 复用同一个文件会互相抢锁;filegroups.f1是正则匹配日志目录,目录路径写错日志就永远进不来;hdfs.path里%Y%m%d是时间变量,Flume 会按当前时间自动生成日期目录,这个日期要和 Hive 分区字段对齐,对不齐的话 Hive 查出来全是空。rollInterval默认 60 秒滚动一次文件,测试时可以调成 10 秒方便看效果,生产环境不要设太短,否则 HDFS 上小文件会爆炸。
2.4 为什么这条链路适合新手复现
整套架构没有引入 Kafka、没有 Spark Streaming,纯离线流程,单机就能跑通。这对学习者非常友好:Flume 挂了你查 Flume 日志,Hive 查不到数据你查分区路径,每一步都是独立的黑盒子,可以逐个击破。对于只想看报表效果的人,也可以跳过 Flume,直接把日志文件丢到 HDFS 对应目录,然后跑 Hive SQL。源码的价值在于它把这些环节都串好了,你只需要替换成自己的路径和参数。
3. 把日志变成可分析的表:Hive ETL 与核心指标口径
3.1 启动日志与页面日志分表建模:建外部表时的两个细节
app_logs_hive模块里的 SQL 脚本,核心是把 Flume 落到 HDFS 的日志文件映射成 Hive 外部表。注意一定要用外部表,因为数据是 Flume 写的,Hive 只管读;如果建成内部表,一不小心DROP TABLE会把 HDFS 上的原始日志一起删掉,没有后悔药。
-- 启动日志外部表,按天分区 CREATE EXTERNAL TABLE ods_start_log ( app_id STRING, device_id STRING, app_version STRING, os_type STRING, network STRING, entry STRING, action STRING, log_time STRING ) PARTITIONED BY (dt STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' STORED AS TEXTFILE LOCATION '/user/hive/warehouse/app_logs.db/ods_start_log';两个容易翻车的点。第一,FIELDS TERMINATED BY '\t'要和客户端实际输出的分隔符一致,客户端如果是逗号分隔,这里不改成','的话,整行数据会被当成一个字段塞进第一列。第二,分区字段dt不要写在表结构字段列表里,它通过PARTITIONED BY单独声明,数据文件里也没有这一列,分区值来自目录名。我见过有人把dt写进字段列表,加载数据时那一列永远是 NULL,还查不出原因。建完表后执行MSCK REPAIR TABLE ods_start_log;让 Hive 自动识别 HDFS 上的分区目录,这一步容易漏。
3.2 核心指标口径:新增用户、活跃用户、启动次数怎么算
有了基础表,剩下的就是指标计算。这套系统里的核心指标有三个:新增用户数、活跃用户数和启动次数。先说口径,再看 SQL,因为口径不一致,同一个数字两个人能算出两个结果。
-- 当日活跃用户数:当天去重后的设备数 SELECT dt, COUNT(DISTINCT device_id) AS active_users FROM ods_start_log WHERE dt = '2024-01-01' GROUP BY dt; -- 当日新增用户数:当天首次出现的设备数 SELECT COUNT(*) AS new_users FROM ( SELECT device_id FROM ods_start_log WHERE dt = '2024-01-01' GROUP BY device_id HAVING MIN(dt) = '2024-01-01' ) t;活跃用户数的口径是“当天有多少设备产生过启动行为”,直接用COUNT(DISTINCT device_id)。新增用户的逻辑是“这个设备之前从没出现过”,所以先用MIN(dt)找每个设备首次出现日期,再过滤出首次出现日等于当天的设备。这里有个隐性条件:日志表里的数据必须覆盖历史全量,如果只加载了当天数据,那MIN(dt)算出来的全是当天,新增数会等于活跃数,指标直接失真。我在测试环境验证指标时,会故意往表里灌前一天的数据,确认新增数降下来后再看当天报表。
启动次数稍微特殊,每一条启动日志代表一次启动会话,所以COUNT(1)就是启动次数。如果日志里还带页面停留时长字段,平均使用时长就是SUM(duration) / COUNT(DISTINCT device_id),单位通常是秒。这三个指标配合起来,能回答“产品改版后用户到底回没回来”这一类问题。
3.3 用 mmdb 把 IP 翻译成地域:GeoIP 解析的接入方式
源码里那个.mmdb文件是 MaxMind 的 GeoIP 数据库,用来把访问 IP 解析成省份、城市。它配合 Hive 的 UDF 或者直接写 Java 工具类使用。app_logs_common模块里应该有对应的解析封装,核心调用方式如下。
// 用 maxmind-db 读取 mmdb 文件,按 IP 查询地域信息 Reader reader = new Reader(new File("/opt/data/GeoLite2-City.mmdb")); Map<String, Object> result = reader.get(InetAddress.getByName("218.30.118.68")); Map<String, Object> provinceMap = (Map<String, Object>) result.get("subdivisions"); String province = provinceMap.get("names").get("zh-CN").toString(); System.out.println("解析结果: " + province); reader.close();这段代码里Reader.get()返回的是一个嵌套 Map,subdivisions下的names有多语言版本,取zh-CN才能拿到中文省份名。注意 mmdb 文件是只读的二进制库,不需要导入数据库,直接放文件系统用代码读就行。我踩过的坑是文件路径写死成src/main/resources/下的相对路径,打包后 classpath 变了就找不到文件,后来统一改成绝对路径,或者放进配置项用${geoip.path}注入,问题才消失。另外 mmdb 文件有版本更新周期,IP 段一直在变,代码写完后记得定期替换文件,否则新地区的用户会解析成空。
4. 可视化与联调:从 JSP 报表到接口参数映射
4.1 可视化端读的是哪张表:报表字段和结果表怎么对上
app_logs_visualize_web是分析结果的出口。它不做计算,只把 Hive 算好的结果表查出来渲染成页面。常见的做法是用 SpringMVC 或者原生 Servlet 查 MySQL,然后把 JSON 返回给前端 JSP/HTML。源码里只有 2 个 JSP 和 1 个 HTML,说明报表页面是轻量级的,重点是字段映射。我第一次接可视化时最晕的就是:页面上叫“活跃用户”,代码里查的字段可能叫active_users,而且可能来自两张不同的结果表。
| 页面指标 | 数据来源表 | 关键字段 |
|---|---|---|
| 今日新增用户 | dws_new_user_day | new_users |
| 今日活跃用户 | dws_active_user_day | active_users |
| 启动次数 | dws_start_count_day | start_count |
| 平均使用时长 | dws_avg_duration_day | avg_duration |
| 地域分布 | dws_area_distribution | province、user_count |
联调时先确认后端接口返回的 JSON 字段名和页面ajax里的取值一致。我遇到最频繁的问题是:后端返回province,页面里写的是areaName,结果图表渲染出来全是空,控制台也不报错,看着像数据丢了,其实是字段名对不上。
4.2 让报表页面动起来:数据访问与渲染的最小闭环
可视化端连 Hive 或 MySQL 的配置都在visualize_web模块里,数据库驱动和连接串要看清楚。如果是直连 Hive,连接串通常是jdbc:hive2://localhost:10000/app_logs,需要先启动 HiveServer2;如果结果已经同步到 MySQL,那连接串就是普通的jdbc:mysql://。我用一个简单的接口代码来说明这个闭环。
@WebServlet("/api/summary") public class SummaryServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String sql = "SELECT new_users, active_users, start_count " + "FROM dws_summary_day WHERE dt = '" + req.getParameter("dt") + "'"; List<Map<String, Object>> rows = JdbcUtil.query(sql); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(JSON.toJSONString(rows)); } }dws_summary_day是预处理好的汇总表,把当天三个核心指标算进一行,查询时按dt参数取某一天。这里有两个细节:dt参数一定要校验,否则拼 SQL 会存在注入风险;响应头里必须显式写charset=UTF-8,不然浏览器默认按 ISO-8859-1 解码,中文全部变成乱码。页面端拿到 JSON 后,用 ECharts 的setOption把数据填进去。第一次联调时先不要急着看图表,直接在浏览器地址栏访问/api/summary?dt=2024-01-01,看到 JSON 再渲染页面,这样能快速区分是接口问题还是前端问题。
4.3 可视化联调的验证顺序
跑可视化端必须遵循一个固定的验证顺序:先确认 Hive 结果表有数据,再确认接口返回 JSON,最后才看页面图表。如果结果表是空的,接口写得再好页面也是白屏。我一般用一条简单 SQL 快速验证:SELECT * FROM dws_summary_day WHERE dt = '2024-01-01' LIMIT 10;有数据再往下走。页面加载出来但图表无数据时,打开浏览器开发者工具看 Network 面板,检查接口状态码和响应体,百分之九十的问题在这一步就定位了。
5. 复现与排错:我把这套源码跑通前后的五件事
5.1 环境版本基线:JDK 8 + Hadoop 2.7 + Hive 2.3 + Flume 1.9
复现这套源码前,环境版本必须先定下来。我的建议是 JDK 1.8、Hadoop 2.7.x、Hive 2.3.x、Flume 1.9.x。这个组合在源码场景下最稳,原因有三:JDK 8 兼容所有 Maven 依赖,不会有 module-info 报错;Hive 2.3 的 HiveServer2 对 JDBC 驱动要求宽松,老驱动也能连上;Flume 1.9 的 TAILDIR 特性稳定。不要一上来就上 Hadoop 3.x 配 Hive 3.x,虽然能跑,但会遇到 YARN 资源调度差异、Hive 视图查询报错等一堆跟业务无关的环境问题。第一次复现没必要追求新版本,跑通链路才是目标。
5.2 五个高频踩坑记录
坑一:collect_web 启动后日志文件不落盘现象:接口返回 200,但指定目录下没有.log文件生成。 原因:日志输出目录不存在,或者 Linux 下进程没有该目录的写权限。 解决:先执行mkdir -p /opt/logs/app_logs并chmod 777目录,再看日志。我在根目录创建目录时经常漏掉权限,Java 进程写不进去又不会报错,只是静默失败,排查了很久才发现是权限问题。
坑二:Flume TAILDIR 不消费新产生的日志现象:日志文件持续增长,HDFS 上没有新文件出现。 原因:filegroups.f1的正则没匹配上文件路径,或者positionFile里记录了错误的文件 ID。 解决:先手动往日志目录写一条数据,然后看 Flume 日志里有没有Opening file的打印。没有就检查正则;有但不出数,删掉taildir_position.json重启 Flume 再试。
坑三:Hive 表能查出数据但分区时间是空的现象:WHERE dt = '2024-01-01'查不到,全表扫描有数据。 原因:Flume 写入 HDFS 的路径是按服务器本地时间生成的日期目录,本地时区或日期格式和 SQL 里写的不一致。比如 Flume 生成的是20240101,SQL 里写的是2024-01-01,永远对不上。 解决:统一日期格式,HDFS 目录用%Y%m%d生成,Hive 分区字段就存20240101,查询条件也用同格式。这个坑属于典型的“两边各写各的”。
坑四:IP 解析结果全部为空现象:报表地域分布一栏是空,其他指标都正常。 原因:mmdb 文件路径在打包后失效,或者拿到的字段名不对,subdivisions这个键在某些 IP 段下不存在。 解决:先单独写个 Java main 方法测试 IP 解析,用固定的公网 IP,比如218.30.118.68,能出结果再封装 UDF 或者工具类。字段读取前先判空:if (provinceMap != null) {...}。
坑五:页面 JSON 中文乱码现象:接口返回的中文变成???或乱字符,图表标题没法看。 原因:响应头没设置字符集,Tomcat 默认用 ISO-8859-1 编码输出。 解决:过滤器里统一加response.setContentType("application/json;charset=UTF-8"),同时确认 JSP 头部也声明了 UTF-8。这属于老生常谈,但几乎每次复现都会遇到一次。
5.3 启动顺序与验证命令
整个系统启动是有顺序的,顺序错了链路就断。我的标准顺序是:HDFS 和 YARN 先起,然后 HiveServer2,再 Flume Agent,最后启动collect_web和visualize_web。每次启动后按下面的检查点过一遍。
# 1. 检查 HDFS 上 Flume 是否写入了新目录 hdfs dfs -ls /user/hive/warehouse/app_logs.db/ods_start_log/ # 2. 检查 Hive 分区是否识别 hive -e "SHOW PARTITIONS app_logs.ods_start_log;" # 3. 手动触发分区修复 hive -e "MSCK REPAIR TABLE app_logs.ods_start_log;" # 4. 直连 HiveServer2 测试查询 beeline -u "jdbc:hive2://localhost:10000/app_logs" \ -e "SELECT dt, COUNT(DISTINCT device_id) FROM ods_start_log GROUP BY dt;"这套检查命令能帮你在 30 秒内确认链路断在哪:第一步看 Flume 有没有扛起活来,第二步看 Hive 认不认新分区,第四步直接验证查询能否出数。把这几条命令存在一个 shell 脚本里,每次启动后跑一遍,比到处翻日志高效得多。
6. 一个提效技巧:用模拟客户端批量造数,把验证周期从小时压到分钟
跑通一次全链路最慢的不是写代码,是造数据。手工改配置、一次次点击页面产生日志,几十分钟过去 Hive 里还是那几条数据,指标根本看不出趋势。我的做法是写一个批量造数的脚本,直击collect_web的接口,一次性灌入几百台设备的日志,验证周期立刻缩短到分钟级。
# 批量造数脚本:模拟 100 台设备,每台产生 3 次启动日志 BASE_URL="http://localhost:8080/app_logs_collect_web/log/start" for i in $(seq 1 100); do DEVICE_ID="8643930000$(printf '%06d' $i)" for j in 1 2 3; do curl -s -X POST "$BASE_URL" \ -H "Content-Type: application/json" \ -d "{\"appId\":\"app_001\",\"deviceId\":\"$DEVICE_ID\",\"appVersion\":\"2.1.0\",\"osType\":\"android\",\"network\":\"wifi\",\"entry\":\"push\",\"action\":\"startup\"}" \ > /dev/null sleep 1 done done echo "批量造数完成"这个脚本里DEVICE_ID是核心:用seq生成 100 个固定编号,保证设备 ID 稳定且唯一,这样 Hive 里算活跃用户、新增用户才能得到符合预期的数字。每条请求之间sleep 1是为了模拟真实节奏,同时避免并发太高把 Tomcat 打挂。跑完脚本后不要立刻查 Hive,Flume 的 TAILDIR 是轮询读取的,给它 1~2 分钟把日志搬完,再执行前面第 5.3 节的检查命令。
用这个方式,我可以在十分钟内验证:日志是否落盘、Flume 是否分区写入、Hive 指标是否符合预期、可视化是否渲染正常。从那以后我每次改完 Hive 口径,都强制用脚本重灌一批数据、跑一遍四步检查,再去看报表。数据链路这种东西,靠肉眼盯日志永远盯不干净,只有批量造数才能逼出问题。希望这个习惯能帮到你少走点弯路。
最后说一个最实用的边界认知:这套源码是离线链路的完整参考,不是在线实时系统,日志从产生到报表可见有分钟级延迟。如果你想做的业务要求秒级响应,那需要引入 Kafka 和 Spark Streaming,那是另一个技术方向了。先把离线链路跑通透,再往实时方向扩展,路会顺很多。
本文还有配套的精品资源,点击获取