Bootstrap

Java调用GDAL绑定库进行日志管理的一种方法的浅析

目录

一、功能需求分析

1.Java中日志管理

2.GDAL中日志管理

3.实现思路

二、实现Java日志框架记录本地库日志

1.创建Maven项目

2.自定义异常

3.编写AspectJ切面

 4.模拟GDAL本地库异常

 5.编译并执行

6.是否线程安全?


作者编程能力一般,不是大神,作此文的目的更多的是记录,如有错误,欢迎指正。

一、功能需求分析

1.Java中日志管理

Java的日志管理是应用程序开发中的一个重要方面,目前比较常用的方式是使用SLF4J结合Logback或Log4j2作为日志框架。日志管理对应用程序的运维的意义不言而喻。类似于业务代码调用spring框架,我们一般只需要获取GDAL的Error级别以上的日志信息用于应用开发,其他级别的日志可以视情况决定是否需要。

2.GDAL中日志管理

GDAL作为一个强大的地理信息栅格、矢量数据读写库,在GIS开发领域发挥了重要的作用。经过查询官方文档可以了解到GDAL本身有着较为完善的日志管理功能,但是GDAL和核心功能是由C和C++代码完成的,其中与日志管理有关的函数有一部分在Java中无法使用,这对我们使用Java管理GDAL的日志造成了一定的难度。这里我们先学习或回顾一下GDAL的日志管理功能,通过这些初步了解如何怎么用Java调用GDAL绑定库来管理GDAL日志。

GDAL的日志配置主要参数及其描述
序号参数描述
1CPL_CURL_VERBOSE=[YES​/​NO]设置为 “YES” 可让 curl 库显示详细信息。
2CPL_DEBUG=[ON​/​OFF​/​<PREFIX>]如果为 ON,则所有debug消息都打印到屏幕。如果为 OFF 或未设置,则不会。如果它设置为特定前缀值,如”Shape“,则只会报告具有 “Shape” 前缀的调试消息。
3CPL_LOG=<path>用于设置日志文件路径。
4CPL_LOG_ERRORS=[ON/OFF]为“ON”则打印错误消息。与 “CPL_LOG” 一起使用,以便将它们记录到到文件中。
5CPL_TIMESTAMP=[ON​/​OFF]为“ON”且CPL_DEBUG已启用,则向 CPL 调试消息添加时间戳
GDAL的日志级别
序号日志级别描述Java 日志框架中类似的级别
1CE_None = 0没有错误INFO
2CE_Debug = 1调试信息DEBUG
3CE_Warning = 2信息性警告WARN
4CE_Failure = 3操作失败,但将使用正常的恢复机制ERROR
5CE_Fatal = 4发生了致命错误,程序中断FATAL
GDAL的Java绑定库3.2.0版本中有关日志的主要方法
序号API名称描述
1UseExceptions()绑定库文档中无描述,其他资料显示此方法是python库中的,在Java代码中测试后,并不能在遇到错误时,抛出Java异常。
2Debug​(String msg_class,String message)需要与 CPL_DEBUG 环境变量结合使用,msg_class是CE_None、CE_Debug等的值,message是自定义消息。
3Error​(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()来恢复其错误信息
4SetErrorHandler()设置一个全局静默的异常处理器,如果使用了PushErrorHandler(),则不生效,除非使用PopErrorHandler()移除了异常处理器。
5SetErrorHandler​(SWIGTYPE_p_CPLErrorHandler pfnErrorHandler)设置一个自定义异常处理器,经查阅GDAL的C语言文档这里的入参实际上是一个C语言的函数,由于开发环境是Java,所以实际上此方法及类似方法在本文的环境下,无法使用。
6PushErrorHandler​(String callbackName)在本地库的异常处理堆栈中压入一个异常处理器,可以使用的有"CPLQuietErrorHandler", "CPLDefaultErrorHandler", "CPLLoggingErrorHandler",使用PopErrorHandler()移除了异常处理器后,失效。
7ErrorReset()擦除以前的错误
8GetLastErrorNo()获取最后一个错误代码,代码在cpl_error.h中定义的CPLE前缀的常量
9GetLastErrorType()获取最后一个错误的级别代码,是CE_None、CE_Debug、CE_Warning、CE_Failure、CE_Fatal的int值
10GetLastErrorMsg()获取最后一个错误文字描述

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;
}

;