目录
作者编程能力一般,不是大神,作此文的目的更多的是记录,如有错误,欢迎指正。
一、功能需求分析
1.Java中日志管理
Java的日志管理是应用程序开发中的一个重要方面,目前比较常用的方式是使用SLF4J结合Logback或Log4j2作为日志框架。日志管理对应用程序的运维的意义不言而喻。类似于业务代码调用spring框架,我们一般只需要获取GDAL的Error级别以上的日志信息用于应用开发,其他级别的日志可以视情况决定是否需要。
2.GDAL中日志管理
GDAL作为一个强大的地理信息栅格、矢量数据读写库,在GIS开发领域发挥了重要的作用。经过查询官方文档可以了解到GDAL本身有着较为完善的日志管理功能,但是GDAL和核心功能是由C和C++代码完成的,其中与日志管理有关的函数有一部分在Java中无法使用,这对我们使用Java管理GDAL的日志造成了一定的难度。这里我们先学习或回顾一下GDAL的日志管理功能,通过这些初步了解如何怎么用Java调用GDAL绑定库来管理GDAL日志。
序号 | 参数 | 描述 |
---|---|---|
1 | CPL_CURL_VERBOSE=[YES/NO] | 设置为 “YES” 可让 curl 库显示详细信息。 |
2 | CPL_DEBUG=[ON/OFF/<PREFIX>] | 如果为 ON,则所有debug消息都打印到屏幕。如果为 OFF 或未设置,则不会。如果它设置为特定前缀值,如”Shape“,则只会报告具有 “Shape” 前缀的调试消息。 |
3 | CPL_LOG=<path> | 用于设置日志文件路径。 |
4 | CPL_LOG_ERRORS=[ON/OFF] | 为“ON”则打印错误消息。与 “CPL_LOG” 一起使用,以便将它们记录到到文件中。 |
5 | CPL_TIMESTAMP=[ON/OFF] | 为“ON”且CPL_DEBUG已启用,则向 CPL 调试消息添加时间戳 |
序号 | 日志级别 | 描述 | Java 日志框架中类似的级别 |
---|---|---|---|
1 | CE_None = 0 | 没有错误 | INFO |
2 | CE_Debug = 1 | 调试信息 | DEBUG |
3 | CE_Warning = 2 | 信息性警告 | WARN |
4 | CE_Failure = 3 | 操作失败,但将使用正常的恢复机制 | ERROR |
5 | CE_Fatal = 4 | 发生了致命错误,程序中断 | FATAL |
序号 | API名称 | 描述 |
---|---|---|
1 | UseExceptions() | 绑定库文档中无描述,其他资料显示此方法是python库中的,在Java代码中测试后,并不能在遇到错误时,抛出Java异常。 |
2 | Debug(String msg_class,String message) | 需要与 CPL_DEBUG 环境变量结合使用,msg_class是CE_None、CE_Debug等的值,message是自定义消息。 |
3 | Error(int msg_class, int err_code, java.lang.String msg) | 需要与 CPL_DEBUG 环境变量结合使用,msg_class是CE_Warning、CE_Failure、CE_Fatal等的int值,err_code是在cpl_error.h中定义的CPLE前缀的常量,msg是自定义消息。其默认想屏幕输出消息,如果是CE_Fatal级别的,则中断程序。可以使用 gdal.GetLastErrorNo() 和 gdal.GetLastErrorMsg()来恢复其错误信息 |
4 | SetErrorHandler() | 设置一个全局静默的异常处理器,如果使用了PushErrorHandler(),则不生效,除非使用PopErrorHandler()移除了异常处理器。 |
5 | SetErrorHandler(SWIGTYPE_p_CPLErrorHandler pfnErrorHandler) | 设置一个自定义异常处理器,经查阅GDAL的C语言文档这里的入参实际上是一个C语言的函数,由于开发环境是Java,所以实际上此方法及类似方法在本文的环境下,无法使用。 |
6 | PushErrorHandler(String callbackName) | 在本地库的异常处理堆栈中压入一个异常处理器,可以使用的有"CPLQuietErrorHandler", "CPLDefaultErrorHandler", "CPLLoggingErrorHandler",使用PopErrorHandler()移除了异常处理器后,失效。 |
7 | ErrorReset() | 擦除以前的错误 |
8 | GetLastErrorNo() | 获取最后一个错误代码,代码在cpl_error.h中定义的CPLE前缀的常量 |
9 | GetLastErrorType() | 获取最后一个错误的级别代码,是CE_None、CE_Debug、CE_Warning、CE_Failure、CE_Fatal的int值 |
10 | GetLastErrorMsg() | 获取最后一个错误文字描述 |
GDAL内置的3个异常处理器名称及其描述
CPLDefaultErrorHandler:默认的异常处理器;如果设置生成了本地库日志文件,会覆盖同名文件
CPLQuietErrorHandler:不输出任何内容的异常处理器;如果设置生成了本地库日志文件,会覆盖同名文件
CPLLoggingErrorHandler:按照配置CPL_LOG选项的要求输出日志的异常处理,如果没有定义CPL_LOG则在屏幕输出;如果设置生成了本地库日志文件,不会覆盖同名文件
3.实现思路
本文的目的是要通过Java捕获GDAL中CE_Failure级别及以上的异常消息并用Java日志框架记录并存储;
由于GDAL的核心是C/C++语言编写,按理说使用C++中有关的日志处理框架来管理日志是最理想的。但是这个对于一个Java开发者,可能有些难度。本次的实现思路还是依靠Java语言来完成需求,我们可以了解到C语言的传统错误处理方式是函数返回错误码,程序根据错误码还判断程序是否异常。这种错误检查方式,在Java中也很容易实现,类似于下列代码。对于一些使用GDAL比较少的程序,这种方式已经足够了。但是对于大量调用gdal代码的程序,在每行gdal代码后都写一个判断代码来处理异常,实在不是一个好方法。
Dataset dataset = gdal.Open(filepath,gdalconstConstants.GA_ReadOnly);
int errorNo = gdal.GetLastErrorNo();
if(errorNo != 0) {
String msg = gdal.GetLastErrorMsg();
throw new Exception("gdal has a error : " + msg);
}
针对这种情况,可以使用AOP(面向切面)编程的方式来处理这类日志记录问题,具体就是将获取最后的错误代码和错误消息的代码作为后置通知(After)、gdal的jar包内的方法制作为切入点(Pointcut)。
由于不能修改源码,所以静态代理不可行,由于gdal类未实现接口,所以无法用jdk动态代理,由于gdal类的方法均为静态方法,所以也无法使用Cglib子类代理、也不能用Spring AOP。这样看来,只能使用AspectJ来给gdal的方法创建切面了,AspectJ有三种织入方式,分别是编译时、编译后、类加载后。由于gdal的jar包已经提前编译好,所以本次我们选用编译后织入的方式来实现。
综上所述,本文使用slf4j+log4j2、AspectJ、gdal来实现对GDAL本地库日志的管理,项目管理工具使用Maven、IDE使用IDEA社区版。其中捕获gdal异常消息使用AspectJ,使用slf4j+log4j2记录GDAL异常以及Java程序的日志;记录gdal的调试信息,依靠调用gdal绑定库实现。
二、实现Java日志框架记录本地库日志
1.创建Maven项目
使用IDEA社区版,新建一个以maven-archetype-quickstart为模板的Maven项目。添加如下依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.gdal</groupId>
<artifactId>gdal</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.1</version>
</dependency>
在build标签的plugins标签下添加AspectJ的编译器包的插件如下:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<weaveDependencies>
<weaveDependency>
<groupId>org.gdal</groupId>
<artifactId>gdal</artifactId>
</weaveDependency>
</weaveDependencies>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
再添加打可执行Jar包的插件,这一步可以自己发挥,不一定非要用下列插件。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>org.example.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
这里将org.example.App作为主方法。
2.自定义异常
这里自定义一个异常,在获取到GDAL的错误消息时,抛出。
package org.example;
/**
* 调用GDAL本地库业务异常,通常用于GDAL本地库调用异常时抛出
*/
public class GDALBusinessException extends RuntimeException {
public GDALBusinessException() {
}
public GDALBusinessException(String message) {
super(message);
}
public GDALBusinessException(String message, Throwable cause) {
super(message, cause);
}
public GDALBusinessException(Throwable cause) {
super(cause);
}
public GDALBusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
3.编写AspectJ切面
package org.example;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.gdal.gdal.gdal;
import org.gdal.gdalconst.gdalconst;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.IllegalFormatException;
@Aspect
public class GDALAspect {
private static final Logger logger = LoggerFactory.getLogger(GDALAspect.class);
@Pointcut("execution(* org.gdal..*.*(..)) && !execution(int org.gdal.gdal.gdal.GetLastErrorNo()) && !execution(java.lang.String org.gdal.gdal.gdal.GetLastErrorMsg())")
public void gdalFunction() {
}
@After("gdalFunction()")
public void exception() {
try {
int errorNo = gdal.GetLastErrorNo();
String errorMsg = gdal.GetLastErrorMsg();
if (gdalconst.CPLE_None != errorNo) {
logger.error("GDAL errorNo: {}, errorMsg: {}", errorNo, errorMsg);
String message = String.format("gdal error no : %s,error message : %s",errorNo, errorMsg);
throw new GDALBusinessException(message);
}
} catch (IllegalFormatException ife) {
logger.error("Error formatting GDAL", ife);
throw new GDALBusinessException("Error formatting GDAL error message", ife);
} catch (GDALBusinessException e) {
logger.error("Error in gdalFunction", e);
throw new GDALBusinessException("Unexpected error in GDAL function", e);
}
}
}
4.模拟GDAL本地库异常
这里使用gdal.Error方法,使本地库能出现异常。gdal.PushErrorHandler()是装入一个静默的异常处理,用于屏蔽本地库向屏幕输出消息。
package org.example;
import org.gdal.gdal.gdal;
import org.gdal.gdalconst.gdalconst;
public class App {
public static void main(String[] args) {
try {
gdal.AllRegister();
gdal.PushErrorHandler();
gdal.Error(gdalconst.CE_Failure, gdalconst.CPLE_AppDefined, "Hello World!");
} catch (Exception e){
System.err.println("GDAL本地库异常 :" + e.getMessage());
}
}
}
5.编译并执行
这里使用下列命令来编译项目,即使是在IDE中执行App的主方法,也需要先执行编译命令。
mvn clean compile aspectj:compile
编译后,能在classes目录下找到gdal的包,并且反编译类后有如图所示的代码,则说明编译成功
在IDEA中执行App类的主方法,结果如下:
可以看到我们实现了用Java的日志框架记录了本地库的异常消息”HelloWorld“。并且捕捉了的异常,使得我们可以在主方法中统一对异常做出处理。
6.是否线程安全?
稍微深入一点讨论,gdal.GetLastErrorNo()是否为线程安全?GDAL的官网显示,GDAL并不一定是线程安全的,这里线程安全的定义采用了QT的定义。经过在源码中找到cpl_error.cpp文件,找到了如下片段,经过查询资料得知它使用TLS确保每个线程都有自己独立的错误上下文,所以可以认为gdal.GetLastErrorNo()是线程安全的。
static CPLErrorContext *CPLGetErrorContext()
{
int bError = FALSE;
CPLErrorContext *psCtx =
reinterpret_cast<CPLErrorContext *>(
CPLGetTLSEx( CTLS_ERRORCONTEXT, &bError ) );
if( bError )
return nullptr;
if( psCtx == nullptr )
{
psCtx = static_cast<CPLErrorContext *>(
VSICalloc( sizeof(CPLErrorContext), 1) );
if( psCtx == nullptr )
{
fprintf(stderr, "Out of memory attempting to report error.\n");
return nullptr;
}
psCtx->eLastErrType = CE_None;
psCtx->nLastErrMsgMax = sizeof(psCtx->szLastErrMsg);
CPLSetTLS( CTLS_ERRORCONTEXT, psCtx, TRUE );
}
return psCtx;
}