零基础鸿蒙应用开发第三十五节:高级日志工具hilog封装
【学习目标】
- 掌握hilog与console的本质区别,明确鸿蒙应用开发环境下日志工具的选型标准与开发规范。
- 熟练运用hilog分级日志,结合模块化开发需求,实现日志的分类管理与高效筛选。
- 独立实现可直接落地的HiLogUtil工具类,支持双模式兼容(可输入/可不输入%d/%s/%b/%o/%n占位符),同时覆盖长日志分段、敏感数据保护、环境开关等核心场景。
- 掌握项目日志体系迁移技巧,高效替换console,提升开发调试与问题排查效率。
- 强化静态方法、函数重载、静态属性、正则表达式等ArkTS/TypeScript核心知识点的实战应用能力。
【学习重点】
- 工具类封装逻辑:通过
interface+私有构造器实现结构化数据处理,新增占位符检测逻辑实现双模式分支处理。 - 核心功能实现:
- 占位符模式:兼容
%d/%s/%b/%o/%n及%{private}s脱敏写法,保留原有使用习惯; - 无占位符模式:直接传参数,工具类自动匹配类型格式化,仅用
%{private}标记实现脱敏; - 通用功能:长日志分段(规避4096(1024×4)字节截断)、环境开关、模块TAG筛选。
- 占位符模式:兼容
- 实战落地性:工具类可直接复制编译运行,全局替换快速完成日志迁移,契合ArkTS模块化开发理念。
一、工程结构
基于上一节ClassObjectDemo_9复制并重命名为ClassObjectDemo_Complete,核心目录如何下:
ClassObjectDemo_Complete
├─ AppScope
├─ entry
│ ├─ src
│ │ ├─ main
│ │ │ ├─ ets
│ │ │ │ ├─ constants
│ │ │ │ ├─ entryability
│ │ │ │ ├─ entrybackupability
│ │ │ │ ├─ model
│ │ │ │ ├─ pages
│ │ │ │ ├─ utils # 通用工具类:新增HiLogUtil
│ │ │ │ │ ├─ JsonUtil.ets
│ │ │ │ │ ├─ HiLogUtil.ets
│ │ │ │ │ └─ ValidationUtil.ets
│ │ │ │ └─ viewmodel
│ │ │ ├─ resources
│ │ │ └─ module.json5
│ ├─ mock
│ ├─ ohosTest
│ └─ test
二、日志工具选型核心:hilog替代console的必要性
在鸿蒙ArkTS应用开发中,console.log的固定参数、无分类、无扩展等短板,无法满足多模块协作与高效调试需求。而hilog作为鸿蒙系统原生日志工具,完全适配ArkTS强类型、可维护的开发理念,是开发阶段与测试阶段的最优选择。
2.1 核心差异对比
| 特性 | console.log | hilog |
|---|---|---|
| 配置灵活性 | 固定领域标识、TAG、级别,不可自定义 | 领域标识(0x0000~0xFFFF)、TAG、级别全自定义,适配模块化开发 |
| 输出结构 | 纯文本输出,无分类标识 | 结构化四元组(领域+TAG+级别+正文),Log面板多维度筛选 |
| 敏感数据处理 | 无脱敏功能,存在数据保护漏洞 | 支持脱敏配置,可通过占位符/标记实现隐私数据保护 |
| 占位符使用 | 需手动编写%d/%s等,类型匹配繁琐 | 可通过工具类封装实现双模式兼容(可输/不输占位符) |
| 日志长度限制 | 存在隐性长度限制,超长日志易被截断 | 明确4096(1024×4)字节限制,可通过工具类分段处理避免截断 |
| 适用场景 | 开发阶段临时调试 | 多模块开发、测试排查、日志分类管理 |
2.2 双模式兼容的核心价值
系统hilog使用需要手动编写占位符,且需保证参数类型与占位符匹配,增加开发成本;而纯无占位符模式又会让习惯原有写法的开发者感到不适。通过对系统hilog封装,既保留了占位符的使用习惯,又提供了极简的参数传递方式,兼顾了不同开发者的使用偏好,同时降低了学习和迁移成本。
三、HiLogUtil工具类
工具类存放路径:entry/src/main/ets/utils/HiLogUtil.ets
3.1 完整代码实现
import { hilog } from '@kit.PerformanceAnalysisKit';
// 日志参数类型声明 (不支持bigint)
export type LogParam = string | number | boolean | object | undefined | null;
// 结构化返回接口(保留format字段,支持双模式处理)
interface ParseResult {
tag: string;
format: string;
params: LogParam[];
}
/**
* hilog工具类
* 核心特性:
* 1. 双模式兼容:支持占位符模式(%d/%s/%b/%o/%n)和无占位符模式(直接传参);
* 2. 敏感数据脱敏:占位符模式支持%{private}s,无占位符模式支持%{private}标记;
* 3. 长日志分段:自动处理4096字节限制,避免日志截断;
* 4. 环境开关:可屏蔽DEBUG级别日志;
* 5. 模块筛选:支持自定义TAG,实现模块日志分类。
*/
export class HiLogUtil {
// 业务域标识(0x0000~0xFFFF,建议按业务模块划分)
private static readonly DOMAIN: number = 0x0001;
// 全局默认日志标签
private static readonly DEFAULT_TAG: string = 'AppCore';
// 日志分段阈值(小于4096字节,预留冗余空间)
private static readonly MAX_SEGMENT_LEN: number = 3500;
// 环境开关(开发=true/测试/生产=false,屏蔽DEBUG日志)
public static isDebug: boolean = true;
// 固定格式化字符串(遵循hilog调用规范)
private static readonly BASE_FORMAT: string = '%{public}s';
// 无占位符模式的脱敏标记
private static readonly PRIVATE_MARKER: string = '%{private}';
// 占位符正则:匹配%d/%s/%b/%o/%n、%{public}xxx、%{private}xxx(用于检测占位符模式)
private static readonly PLACEHOLDER_REG = /%({(public|private)})?([dsbon])/g;
// 私有构造器:禁止实例化工具类(静态工具类设计模式)
private constructor() {}
/** DEBUG级别:开发调试专用,测试/生产环境可屏蔽 */
public static debug(format: string, ...args: LogParam[]): void;
public static debug(tag: string, format: string, ...args: LogParam[]): void;
public static debug(...args: LogParam[]): void {
if (!HiLogUtil.isDebug) return;
HiLogUtil.processLog(hilog.LogLevel.DEBUG, args);
}
/** INFO级别:业务关键节点记录 */
public static info(format: string, ...args: LogParam[]): void;
public static info(tag: string, format: string, ...args: LogParam[]): void;
public static info(...args: LogParam[]): void {
HiLogUtil.processLog(hilog.LogLevel.INFO, args);
}
/** WARN级别:非致命异常警告 */
public static warn(format: string, ...args: LogParam[]): void;
public static warn(tag: string, format: string, ...args: LogParam[]): void;
public static warn(...args: LogParam[]): void {
HiLogUtil.processLog(hilog.LogLevel.WARN, args);
}
/** ERROR级别:功能异常记录 */
public static error(format: string, ...args: LogParam[]): void;
public static error(tag: string, format: string, ...args: LogParam[]): void;
public static error(...args: LogParam[]): void {
HiLogUtil.processLog(hilog.LogLevel.ERROR, args);
}
/** FATAL级别:致命错误记录 */
public static fatal(format: string, ...args: LogParam[]): void;
public static fatal(tag: string, format: string, ...args: LogParam[]): void;
public static fatal(...args: LogParam[]): void {
HiLogUtil.processLog(hilog.LogLevel.FATAL, args);
}
/** 统一处理日志:参数解析→模式判断→格式化/脱敏→分段打印 */
private static processLog(level: hilog.LogLevel, args: LogParam[]): void {
const parseResult: ParseResult = HiLogUtil.parseArgs(args);
const tag: string = parseResult.tag;
let format: string = parseResult.format;
let params: LogParam[] = parseResult.params;
let fullLog: string = '';
// 步骤1:判断是否为「占位符模式」(格式化字符串包含占位符)
if (HiLogUtil.hasPlaceholders(format)) {
// 占位符模式:按原有逻辑处理(补全占位符、格式化参数、脱敏)
const handledFormat = HiLogUtil.fixPlaceholder(format);
const handledParams = HiLogUtil.formatParams(params);
fullLog = HiLogUtil.replacePlaceholder(handledFormat, handledParams);
} else {
// 无占位符模式:将format也作为参数,合并后走自动类型格式化+脱敏
const allParams = [format, ...params];
fullLog = HiLogUtil.formatAndDesensitize(allParams);
}
// 步骤2:分段打印长日志(规避4096字节限制)
let remainingLog: string = fullLog;
while (remainingLog.length > 0) {
const segment: string = remainingLog.slice(0, HiLogUtil.MAX_SEGMENT_LEN);
remainingLog = remainingLog.slice(HiLogUtil.MAX_SEGMENT_LEN);
HiLogUtil.callHilog(level, tag, segment);
}
}
/** 遍历解析参数:识别「模块TAG」「格式化字符串」「内容参数」 */
private static parseArgs(args: LogParam[]): ParseResult {
const result: ParseResult = {
tag: HiLogUtil.DEFAULT_TAG,
format: '',
params: []
};
for (let i = 0; i < args.length; i++) {
const arg: LogParam = args[i];
// 1. 识别自定义TAG:第一个参数为字符串,且下一个参数也为字符串
if (i === 0 && typeof arg === 'string' && args.length > 1) {
const nextArg: LogParam = args[i + 1];
if (typeof nextArg === 'string') {
result.tag = arg as string;
continue;
}
}
// 2. 识别格式化字符串:第一个非TAG的字符串
if (result.format.length === 0 && typeof arg === 'string') {
result.format = arg as string;
continue;
}
// 3. 剩余参数归入内容参数
result.params.push(arg);
}
// 异常处理:无格式化字符串时的默认提示
if (result.format.length === 0) {
result.format = '日志参数错误:请传入参数或「TAG+参数」';
}
return result;
}
/** 辅助方法:检测字符串是否包含占位符 */
private static hasPlaceholders(str: string): boolean {
// 重置正则匹配状态,避免多次匹配出现异常(正则对象的lastIndex陷阱)
HiLogUtil.PLACEHOLDER_REG.lastIndex = 0;
return HiLogUtil.PLACEHOLDER_REG.test(str);
}
/** 补全占位符:将%s/%d等无修饰符的占位符自动转为%{public}s */
private static fixPlaceholder(format: string): string {
// 为replace回调参数添加显式类型注解
return format.replace(/%(?![{]private|[{]public)(d|s|b|o|n)/g, (_: string, placeholder: string) => {
return `%{public}${placeholder}`;
});
}
/** 格式化参数:对象转JSON,null/undefined转字符串(占位符模式) */
private static formatParams(params: LogParam[]): string[] {
const handledParams: string[] = [];
for (let i = 0; i < params.length; i++) {
const param: LogParam = params[i];
if (param === null || param === undefined) {
handledParams.push(String(param));
} else if (typeof param === 'object') {
// 对象/数组转格式化JSON,提升可读性(缩进2个空格)
handledParams.push(JSON.stringify(param, null, 2));
} else {
handledParams.push(String(param));
}
}
return handledParams;
}
/** 替换占位符:处理敏感数据脱敏(占位符模式,修复type参数类型注解) */
private static replacePlaceholder(format: string, params: string[]): string {
let index: number = 0;
// 为回调参数添加显式类型注解,处理type为undefined的边界情况
return format.replace(/%\{(public|private)\}(d|s|b|o|n)/g, (_: string, type?: string) => {
const param: string = params[index] || '';
index++;
// 兜底处理:type为undefined时默认按public处理
return (type || 'public') === 'private' ? '<private>' : param;
});
}
// ====== 无占位符模式相关方法(自动类型格式化+脱敏)======
/** 自动格式化参数类型 + 处理敏感数据脱敏(无占位符模式) */
private static formatAndDesensitize(params: LogParam[]): string {
let logContent: string = '';
let needDesensitize: boolean = false; // 标记下一个参数是否需要脱敏
for (let i = 0; i < params.length; i++) {
const param: LogParam = params[i];
// 1. 识别脱敏标记:如果当前参数是%{private},则下一个参数需要脱敏
if (typeof param === 'string' && param === HiLogUtil.PRIVATE_MARKER) {
needDesensitize = true;
continue; // 跳过标记本身,不加入日志内容
}
// 2. 自动格式化参数(根据类型处理,替代占位符逻辑)
let formattedParam: string = HiLogUtil.formatSingleParam(param);
// 3. 脱敏处理:如果标记为需要脱敏,则替换为<private>
if (needDesensitize) {
formattedParam = '<private>';
needDesensitize = false; // 重置脱敏标记
}
// 4. 拼接参数(用空格分隔,提升日志可读性)
logContent += formattedParam + ' ';
}
// 去除末尾多余的空格
return logContent.trim();
}
/** 自动格式化单个参数 */
private static formatSingleParam(param: LogParam): string {
switch (typeof param) {
case 'string':
return param; // 字符串直接返回
case 'number':
// case 'bigint':
case 'boolean':
return String(param); // 数字、布尔、大整数转字符串
case 'object':
// null单独处理(typeof null === 'object',需特殊判断)
if (param === null) {
return 'null';
}
// 对象/数组转格式化JSON(缩进2个空格)
return JSON.stringify(param, null, 2);
case 'undefined':
return 'undefined'; // 未定义转字符串
default:
return 'unknown'; // 未知类型(兜底处理)
}
}
/** 调用hilog原生接口 */
private static callHilog(level: hilog.LogLevel, tag: string, content: string): void {
switch (level) {
case hilog.LogLevel.DEBUG:
hilog.debug(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
break;
case hilog.LogLevel.INFO:
hilog.info(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
break;
case hilog.LogLevel.WARN:
hilog.warn(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
break;
case hilog.LogLevel.ERROR:
hilog.error(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
break;
case hilog.LogLevel.FATAL:
hilog.fatal(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
break;
default:
hilog.info(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
}
}
}
四、实战使用
4.1 前置准备:定义测试数据接口与实例
// 定义测试数据接口
interface RequestParams {
data: string;
page: number;
size: number;
}
interface BigDataResponse {
code: number;
data: RequestParams[];
}
// 初始化测试数据
const requestParams: RequestParams = { page: 10, size: 20, data: "测试数据内容" };
const bigData: BigDataResponse = {
code: 200,
data: Array(100).fill(requestParams) // 生成100条测试数据,模拟超长JSON
};
4.2 模式1:占位符模式
适用于习惯编写%d/%s等占位符的开发者,原有代码无需修改即可直接使用:
// 导入工具类(匹配工程结构的实际路径)
import { HiLogUtil } from '../utils/HiLogUtil';
// 1. 无模块TAG(使用默认TAG=AppCore)
HiLogUtil.info("商品数量:%d,是否上架:%b", 50, true);
HiLogUtil.warn("用户手机号:%{private}s", "13800138000"); // 敏感数据脱敏
HiLogUtil.error("数据解析失败:%s,错误码:%d", "JSON格式错误", 500);
// 2. 带模块TAG(自定义TAG=GoodsModule/UserModule)
HiLogUtil.debug("GoodsModule", "接口请求参数:%o", requestParams); // 对象参数
HiLogUtil.fatal("ServiceModule", "核心服务启动失败:%s", "数据库连接超时");
4.3 模式2:无占位符模式
适用于希望简化开发的开发者,直接传递参数,工具类自动处理类型格式化:
// 导入工具类(匹配工程结构的实际路径)
import { HiLogUtil } from '../utils/HiLogUtil';
// 1. 无模块TAG(使用默认TAG=AppCore)
HiLogUtil.info("商品数量:", 50, "是否上架:", true, "商品信息:", requestParams, null, undefined);
// 2. 带模块TAG(自定义TAG=UserModule/DataModule)
HiLogUtil.debug("UserModule", "用户名称:", "张三", "手机号:", "%{private}", "13800138000"); // 敏感数据脱敏
HiLogUtil.error("DataModule", "数据解析失败:", "JSON格式错误", "错误详情:", new Error("invalid json"));
4.4 通用进阶功能
(1)模块日志筛选
通过自定义模块TAG,可在鸿蒙DevEco Studio的Log面板快速筛选指定模块的日志,提升调试效率:
// 商品模块日志(TAG=GoodsModule)
HiLogUtil.info("GoodsModule", "商品列表加载完成,共", 20, "条数据"); // 无占位符模式
HiLogUtil.info("GoodsModule", "商品库存:%d件", 100); // 占位符模式
// 用户模块日志(TAG=UserModule)
HiLogUtil.info("UserModule", "用户登录成功,ID:", 1001); // 无占位符模式
HiLogUtil.info("UserModule", "用户登录成功,ID:%d", 1001); // 占位符模式
// 在Log面板筛选「TAG=GoodsModule」,仅显示商品模块日志,过滤无关日志
测试打印效果(1):

(2)长日志分段(自动处理4096字节限制)
hilog存在4096字节的输出限制,工具类会自动将长日志分段打印,无需手动处理:
// 对比测试:console会截断超长日志,HiLogUtil会分段打印
console.debug("LongLogModule", "使用console接口返回大数据:%o", bigData);
// 占位符模式:自动分段打印
HiLogUtil.debug("LongLogModule", "使用HiLogUtil接口返回大数据:%o", bigData);
// 无占位符模式:自动分段打印
HiLogUtil.debug("LongLogModule", "使用HiLogUtil接口返回大数据:", bigData);
测试打印效果(2):

(3)环境动态开关(区分开发/生产环境)
在项目入口文件中设置环境开关,可屏蔽测试/生产环境的DEBUG日志,仅保留关键日志(INFO/WARN/ERROR/FATAL):
// 项目入口文件(如entryability/index.ets)
import { HiLogUtil } from '../utils/HiLogUtil';
// 测试/生产环境屏蔽DEBUG日志,仅保留INFO及以上级别
// 这里演示,真实开发中需配置自动切换环境。
HiLogUtil.isDebug = true// 开发环境=true,生产环境=false
// DEBUG日志:开发环境显示,生产环境隐藏(无论哪种模式)
HiLogUtil.debug("这行DEBUG日志仅在开发环境显示");
// INFO/WARN/ERROR/FATAL日志:所有环境正常显示
HiLogUtil.info("这行INFO日志正常显示");
HiLogUtil.warn("这行WARN日志正常显示");
测试打印效果(3):

五、日志迁移技巧(高效替换console)
5.1 全局替换步骤(DevEco Studio)
- 打开全局替换功能:使用快捷键
Ctrl+Shift+R(Windows)/Cmd+Shift+R(Mac); - 按以下规则批量替换原有
console代码(保留参数,仅替换方法名):
| 原console代码 | 替换为HiLogUtil代码 | 对应日志级别 | 适用场景 |
|---|---|---|---|
| console.log( | HiLogUtil.info( | INFO(信息) | 业务关键节点记录 |
| console.debug( | HiLogUtil.debug( | DEBUG(调试) | 开发阶段调试信息 |
| console.warn( | HiLogUtil.warn( | WARN(警告) | 非致命异常警告 |
| console.error( | HiLogUtil.error( | ERROR(错误) | 功能异常记录 |
5.2 提升日志实用性
- 添加模块TAG:给多模块日志添加专属TAG(如
HiLogUtil.info("OrderModule", "订单创建成功")),便于日志筛选; - 敏感数据脱敏:对隐私数据(手机号、密码、身份证号)添加脱敏处理(占位符模式用
%{private}s,无占位符模式用%{private}标记); - 对象参数格式化:对对象/数组参数,占位符模式使用
%o占位符,无占位符模式直接传递,工具类会自动格式化JSON; - 环境变量自动切换:配置自动切换
isDebug状态,避免手动修改代码。
5.3 常见问题与解决方案
| 问题现象 | 原因分析 | 解决方案 |
|---|---|---|
| 日志不显示 | 1. DOMAIN配置错误;2. TAG筛选错误;3. isDebug=false | 1. 检查DOMAIN是否为0x0001(或自定义值);2. 取消TAG筛选或选择正确TAG;3. 开发环境设置isDebug=true |
| 脱敏功能失效 | 1. 占位符写法错误;2. 标记位置错误 | 1. 占位符模式使用%{private}s(而非%private%s);2. 无占位符模式将%{private}放在脱敏参数前 |
| 长日志仍被截断 | MAX_SEGMENT_LEN设置过大 | 将MAX_SEGMENT_LEN调整为3500~4000之间(建议3500,预留冗余空间) |
六、内容总结
本次实现的HiLogUtil工具类为生产级可用版本,核心解决了传统hilog使用的痛点,同时结合了静态方法、函数重载、静态属性、正则表达式等核心知识点,既是日志管理工具,也是0基础开发者的实战训练案例。
核心要点
- 双模式兼容:自动识别参数中是否包含占位符,分支处理占位符模式和无占位符模式,开发者可自由选择使用习惯,原有代码无需修改即可迁移。
- 功能完整且健壮:支持敏感数据脱敏、长日志分段、模块TAG筛选、环境开关,处理了正则匹配、类型注解、边界值等易出错场景,符合生产级开发规范。
- 低学习成本与高效迁移:占位符模式保留原有使用习惯,无占位符模式无需记忆占位符规则;全局替换
console的逻辑简单,可快速完成日志体系迁移。 - 知识点实战强化:工具类封装过程中综合运用了静态工具类设计模式、函数重载、正则表达式、类型注解等核心知识点,提升开发者的实战能力。
七、仓库代码
- 工程名称:
ClassObjectDemo_Complete - 代码地址:https://gitee.com/juhetianxia321/harmony-os-code-base.git
八、阶段总结与界面开发阶段预告
基础阶段35节内容已全部圆满收官!回顾这一阶段,我们从ArkTS核心语法(类型、函数、类、接口、正则等)打牢基础,再通过商品管理项目完成了工程化封装的完整实践——不管是代码分层、工具类设计,还是日志体系(HiLogUtil)的标准化落地,都让你从“只会写零散代码”升级为“能按工程化规范开发”,为后续界面开发筑牢了底层能力。
接下来,我们正式进入 ArkUI阶段!这一阶段的核心目标是:从“逻辑层”转向“交互层”,系统掌握鸿蒙ArkTS各类UI组件的使用方法、布局逻辑和交互设计,最终能独立搭建美观、流畅、符合鸿蒙设计规范的应用界面,让之前写的业务逻辑“有界面可承载”。
浙公网安备 33010602011771号