1.JCL概念
JEDI 代码库 (JCL) 由一组经过全面测试并完整记录的代码组成实用程序函数和非视觉类,可以立即在您的Delphi中重用和 C++ Builder 项目。JCL主要用于采集程序异常报错的堆栈信息,从而能更快的定位到异常的位置,无需拿到相关的配置去运行代码debug断点跟踪。
2.JCL安装
JCL为开源项目,代码的位置在GitHub,请自行搜索。下载jcl,还要下载project-jedi/jedi里的2个INC文件并放到jcl-master\jcl\source\include\jedi目录里。运行jcl\install.bat 安装,没有DPK工程文件。运行bat文件,弹出下面的界面,点install即可。
安装程序会默认加载电脑所有的Delphi编译器,点击安装之前,先选择“MPL 1.1 License”勾选agree,然后默认安装即可,记住安装之前要关闭Delphi编译器。
3.JCL功能简介
安装成功之后,打开Delphi编译器,新建一个项目,然后你可以点击project菜单下面看到JCL Debug expert选项,下面有3个菜单
- Generate .jdbg files (使用可执行文件生成和部署 JDBG 文件。 这是基于 MAP 文件的二进制文件,但其大小通常约为 原始 MAP 文件的 12%。您可以通过 jcl\experts\debug\tools 文件夹中的 MapToJdbg 工具生成它。与MAP文件相比,其优点是体积更小,安全性更好 的文件内容,因为它不是纯文本文件,而且它也 包含一个校验和。IDE 专家可以自动创建此文件 当项目编译时(见下文))
- Insert JDBG data into the binary(将 JCL 调试信息插入到可执行文件中。这添加的数据的大小类似于JDBG 文件,但会插入直接添加到可执行文件中。这可能是最好的选择,因为它结合了小尺寸的包含数据,并且不需要部署其他文件。IDE EA 可以自动插入这些信息)
- Delete map files after conversion(转换完成后自动删除 MAP 文件)
每个选项下面都有四个subitem:Always enabled(一直开启)、Enabled for this project(只对当前项目开启)、Disabled for this project(只对当前项目禁用)、Always disabled(一直禁用)。我们一般推荐使用第二种方式,虽然这将增大exe程序的体积,但是增大的限度有限,并且发布程序的时候不必携带MAP或者JDBG文件一起发布,最为便捷。
当我们给一个项目开启jcldebug的时候我们需要在Delphi编辑器中选择MAP file为“Detailed”,在Project->options->Linker 中可以找到。如果没开启的话,在编译程序的时候jcl会给出相应的提示,选择接受即可。
4.JCL的业务集成
我们代码里面集成JCL一般都做全局性的部署,我们程序可以通过try..except..end代码块显示的捕获异常,如果异常块未被try..except..end捕捉,那么异常会向上传播。如果异常在应用程序的最顶层仍未被捕获,Delphi 的运行时环境(VCL 或 FMX 框架)可能会提供自己的异常处理机制。所以我们需要对Delphi项目的2块内容做处理:
a.已经在代码里面使用try..except..end,并且异常已经被Exception捕捉的,在我们show出异常的时候除了E.Message可能需要提供更多的信息以用来定位异常代码发生的堆栈数据。
b.异常的发生未显示的使用try..except..end,异常信息被VCL、消息、操作系统捕获的时候,我们需要从JCL中获取当前异常的堆栈并通过自定义弹窗的方式来show出来。
针对这两块内容我们的处理方式是:
a.提供一个函数获取当前异常的堆栈并返回格式化的字符串,在Except中获取的E.message的时候同时取得堆栈数据。
b.利用JCL自定义Application.OnException过程,并封装统一格式的弹窗界面来展示异常。
c.利用JCL(JclAddExceptNotifier)添加通知过程,当程序不管在任何位置发生的异常都会调用到这个通知的procedure过程,我们可以自定义这个过程并形成文件日志的记录。
以下附上部分参考代码:
program ExceptionTest;
uses
Forms,
JclDebug,
JclHookExcept,
utMain in 'utMain.pas' {Form1},
utJCLFrmExcept in 'utJCLFrmExcept.pas' {FrmExcept},
utDataM in 'utDataM.pas' {DataModule1: TDataModule},
utJCLException in 'utJCLException.pas';
{$R *.res}
begin
Application.Initialize;
//初始化 JclStackTrackingOptions
Include(JclStackTrackingOptions, stRawMode);
Include(JclStackTrackingOptions, stStaticModuleList);
//开始JCL跟踪
JclStartExceptionTracking;
//注册通知程序
JclAddExceptNotifier(EJCLException.LogException);
//截获程序异常
Application.OnException := EJCLException.ShowException;
Application.CreateForm(TDataModule1, DataModule1);
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
unit utJCLException;
interface
uses
Windows, SysUtils, Classes, JclDebug, Dialogs, utJCLFrmExcept, Forms;
type
// 定义一个自定义异常类
EJCLException = class
private
public
{-------------------------------------------------------------------------------
函数名: ShowException
函数说明: 显示出一个最近的一次异常详细信息 此过程和Application.OnException绑定,
用于处理代码中未用try except捕捉,而由应用程序自动捕捉的异常信息,
请注意try except包裹的代码块不会触发Application.OnException。
作者: trump
日期: 2024-08-02
参数: Sender: TObject; E: Exception
返回值: 无
-------------------------------------------------------------------------------}
class procedure ShowException(Sender: TObject; E: Exception);
{-------------------------------------------------------------------------------
函数名: LogException
函数说明: 记录异常报错的日志。与JclAddExceptNotifier事件共同绑定,JclAddExceptNotifier
注册一个事件用于响应所有的异常,并通过JCLLogException来生成日志信息
作者: trump
日期: 2024-08-02
参数: ExceptObj: TObject; ExceptAddr: Pointer; IsOS: Boolean
返回值: 无
-------------------------------------------------------------------------------}
class procedure LogException(ExceptObj: TObject; ExceptAddr: Pointer; IsOS: Boolean);
{-------------------------------------------------------------------------------
函数名: GetExceptionInfo
函数说明: 获取当前最近的一次异常的报错信息,此函数一般用作在try except中在弹出
异常的时候取到E.messgae之后再调用这个函数获取到详细的堆栈信息。
作者: trump
日期: 2024-08-02
参数: 无
返回值: 异常信息
-------------------------------------------------------------------------------}
class function GetExceptionInfo:string;
end;
var
JCLLogList:TStringlist;
JCLShowList:TStringlist;
implementation
{ EJCLException }
procedure WriteExceptionLog(sLogInfo: string);
var
sDate, sTime, sDirName, sInfo: string;
sFName: OleVariant;
fLog: TextFile;
begin
sDate := FormatDatetime('YYYYMMDD', Now);
sDirName := ExtractFilePath(Application.ExeName) + 'Log\';
if not DirectoryExists(sDirName) then CreateDir(sDirName);
if not DirectoryExists(sDirName) then //如果创建不成功
CreateDirectory(PChar(sDirName), nil);
sFName := sDirName + sDate + '.log';
AssignFile(fLog, sFName);
if not FileExists(sFName) then
ReWrite(fLog)
else
Append(fLog);
sTime := FormatDatetime('YYYY-MM-DD HH:NN:SS', Now);
sInfo := sTime + ' msg: ' + sLogInfo + '';
WriteLn(fLog, sInfo);
CloseFile(fLog);
end;
class function EJQException.GetExceptionInfo: string;
var
ModInfo: TJclLocationInfo;
sLogMsg:string;
iListCnt:Integer;
begin
if JCLShowList = nil then JCLShowList := TStringList.Create;
JCLShowList.Clear; sLogMsg := '';
JclLastExceptStackListToStrings(JCLShowList, False, False, False, False);
ModInfo := GetLocationInfo(ExceptAddr);
sLogMsg := JCLShowList.Text;
Result := Format(cstExceptionShow, [ModInfo.UnitName, ModInfo.ProcedureName,
ModInfo.SourceName, IntToStr(ModInfo.LineNumber),
sLogMsg]);
end;
class procedure EJQException.LogException(ExceptObj: TObject; ExceptAddr: Pointer; IsOS: Boolean);
var
sLogMsg,sEMsg,sOsMsg,sEnCrp: string;
ModInfo: TJclLocationInfo;
begin
if JCLLogList = nil then JCLLogList := TStringList.Create;
JCLLogList.Clear;
JclLastExceptStackListToStrings(JCLLogList, False, False, False, False);
if ExceptObj is Exception then
sEMsg := Exception(ExceptObj).Message
else sEMsg := '';
if IsOS then sOsMsg := '1' else sOsMsg := '';
ModInfo := GetLocationInfo(ExceptAddr);
sLogMsg := Format(cstExceptionLog, [ExceptObj.ClassName,sEMsg,sOsMsg,
ModInfo.UnitName, ModInfo.ProcedureName,
ModInfo.SourceName, IntToStr(ModInfo.LineNumber)]);
sLogMsg := sLogMsg + #13#10 + JCLLogList.Text;
WriteExceptionLog(sLogMsg);
end;
class procedure EJQException.ShowException(Sender: TObject; E: Exception);
var
ModInfo: TJclLocationInfo;
iListCnt:Integer;
sMsgShow:string;
begin
if JCLShowList = nil then JCLShowList := TStringList.Create;
JCLShowList.Clear; sMsgShow := '';
JclLastExceptStackListToStrings(JCLShowList, False, False, False, False);
ModInfo := GetLocationInfo(ExceptAddr);
sMsgShow := JCLShowList.Text;
CrtFrmExcept(E.ClassName, ModInfo.UnitName, ModInfo.ProcedureName, ModInfo.SourceName,
IntToStr(ModInfo.LineNumber), E.Message, sMsgShow);
end;
end.
ps:程序的堆栈信息比较敏感,我们做日志记录的时候或者弹窗的时候,需要做好加密和数据筛选。