簡體   English   中英

如何解決Java中的事務日志?

[英]How to solve transactional logging in Java?

我想實現以下目標:

在事務內部,我想生成多個日志消息。 只有在成功提交事務時才應寫入這些日志消息。 如果事務回滾,則不得記錄日志消息。

我找不到任何東西來實現這個(使用spring,hibernate,atomikos),所以我寫了這個小包裝器(我遺漏了幾個方便方法):

public class TransactionLogger {
    private Logger logger;
    private Map<Long, LinkedList<LogRecord>> threadBuffers =
        new HashMap<Long, LinkedList<LogRecord>>();

    public TransactionLogger(Logger logger) {
        this.logger = logger;
    }

    private void addRecord(LogRecord rec) {
        LinkedList<LogRecord> list =
            threadBuffers.get(Thread.currentThread().getId());
        if (list == null) {
            list = new LinkedList<LogRecord>();
            threadBuffers.put(Thread.currentThread().getId(), list);
        }
        list.add(rec);
    }

    private LinkedList<LogRecord> getRecords() {
        if (threadBuffers.containsKey(Thread.currentThread().getId())) {
            return threadBuffers.remove(Thread.currentThread().getId());
        } else {
            return new LinkedList<LogRecord>();
        }
    }

    public void commit() {
        for (LogRecord rec : getRecords()) {
            rec.setLoggerName(logger.getName());
            logger.log(rec);
        }
    }

    public void rollback() {
        getRecords();
    }

    /**
     * If the resulting log entry should contain the sourceMethodName
     * you should use logM(Level,String,String) instead,
     * otherwise TransactionLogger.commit() will get
     * inferred as sourceMethodName.
     */
    public void log(Level l, String sourceClassName, String msg) {
        LogRecord rec = new LogRecord(l, msg);
        rec.setSourceClassName(sourceClassName);
        addRecord(rec);
    }

    /**
     * Same as log(Level,String,String), but the sourceMethodName gets set.
     */
    public void logM(Level l, String sourceClassName, String msg) {
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        LogRecord rec = new LogRecord(l, msg);
        rec.setSourceClassName(sourceClassName);
        if (trace != null && trace.length > 1) {
            rec.setSourceMethodName(trace[2].getMethodName());
        }
        addRecord(rec);
    }
}

您如何看待這種方法? 它有任何重大或輕微的缺陷或問題嗎? 或者甚至更好,是否有任何現成的解決方案?

更新:

由於我也使用JTA,我有了一個新的想法。 將TransactionLogger實現為事務感知消息隊列的接收器可以解決時間問題還是只會使事情變得更復雜?

更新:

我認為記錄到數據庫然后定期將該數據庫中的日志條目寫入評論中建議的定期任務中的文件,這是解決此問題的一個非常好的解決方案:

優點:

  • 正常的實施能力
  • 與TransactionManager集成
  • 日志文件中的日志條目可以按時間戳排序

缺點:

  • 日志文件不是最新的(取決於periodic-task-interval)
  • 數據庫結構依賴
  • 記錄簡單的非事務性事件將變為依賴於dbconnection
  • 可能更大的整體日志開銷

以下是我發布的包裝器的優缺點:

優點:

  • 數據庫和框架獨立
  • 簡單的實施
  • 日志文件始終是最新的

缺點:

  • 日志文件中的日志條目不是按事件時間戳排序,而是按“事務兼容” - 時間戳排序(長事務導致非常混合的日志文件。
  • 必須“手動”調用rollback()commit() ,這可能導致編程錯誤(如果忘記調用這些方法,可能會出現OutOfMemoryError)

我認為這兩者的組合,比如在“包裝器” - 方法中緩沖日志記錄比使用上述兩種方法中的一種更糟糕,因為可能存在不一致的日志文件(由於應用程序崩潰而忘記了日志條目)。

我現在的決定是保留我的“包裝”。 以下原因對此決定至關重要(按重要性排序):

  1. 我更喜歡總是最新的日志文件,而不是完美排序的日志條目
  2. 在我的案例中,長期交易非常罕見
  3. 我能夠將rollback()commit()的使用減少到只有幾個方法。
  4. 這個解決方案現在已經存在。

順便說一下:我想提高我的英語水平。 因此,如果您發現我的文章中有任何錯誤,如果您指出它們,我將很高興。

更新:

簡化我正在使用它:

/*
 * This objects contains one or more TransactionLogger(s) and exposes rollback()
 * and commit() through rollbackLogs() and commitLogs().
 */
@Autowired
private ITransactionalServer server;

public void someMethod(String someParam) {
    boolean rollback = false;
    try {
        /*
         * This method is for example annotated with @Transactional.
         */
        return server.someTransactionalMethod(someParam);
    } catch (Exception ex) {
        logError(ex);
        rollback = true;
    } finally {
        if (rollback) {
            server.rollbackLogs();
        } else {
            server.commitLogs();
        }
    }
}

這仍然不是完美的,但現在它對我來說似乎是一個“足夠好的解決方案”。 下一步將使用方面來裝飾我的事務方法。

更新:

我在我的問題中添加了這個,因為我對接受我自己的答案感到不好,盡管其他人讓我在路上。

我現在基本上使用AOP方法使用以下Logger。 (在我的實際應用程序中,我有多個這樣的Logger,所有這些Logger都由自定義單例管理器管理。):

public class AopLogger extends Logger {

    public static AopLogger getLogger(String name) {
        LogManager manager = LogManager.getLogManager();
        Object l = manager.getLogger(name);
        if (l == null) {
            manager.addLogger(new AopLogger(name));
        }
        l = manager.getLogger(name);
        return (AopLogger)l;
    }

    private Map<Long, LinkedList<LogRecord>> threadBuffers = new HashMap<Long, LinkedList<LogRecord>>();

    public AopLogger(String name) {
        super(name, null);
    }

    public void beginTransaction() {
        LinkedList<LogRecord> list = threadBuffers.get(Thread.currentThread().getId());
        if (list == null) {
            list = new LinkedList<LogRecord>();
            threadBuffers.put(Thread.currentThread().getId(), list);
        }
    }

    private void addRecord(LogRecord rec) {
        LinkedList<LogRecord> list = threadBuffers.get(Thread.currentThread().getId());
        if (list != null) {
            list.add(rec);
        } else {
            super.log(record);
        }
    }

    private LinkedList<LogRecord> getRecords() {
        if (threadBuffers.containsKey(Thread.currentThread().getId())) {
            return threadBuffers.remove(Thread.currentThread().getId());
        } else {
            return new LinkedList<LogRecord>();
        }
    }

    public void commit() {
        for (LogRecord rec : getRecords()) {
            rec.setMillis(System.currentTimeMillis());
            super.log(rec);
        }
    }

    public void rollback() {
        getRecords();
    }

    public void log(LogRecord record) {
        addRecord(record);
    }
}

而這方面:

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;

@Service
@Aspect
@Order(10)
public class AopLogManager implements Ordered {

    @Autowired
    private AopLogger logger;
    private Logger errorLogger = Logger.getLogger("ExceptionLogger");

    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object logTransaction(ProceedingJoinPoint pjp) throws Throwable {
        logger.beginTransaction();
        Exception ex = null;
        try {
            return pjp.proceed();
        } catch (Exception e) {
            ex = e;
            throw e;
        } finally {
            if (ex != null) {
                logger.rollback();
                errorLogger.severe(ex.getMessage());
            } else {
                logger.commit();
            }
        }
    }

    private int mOrder;

    @Override
    public int getOrder() {
        return mOrder;
    }

    public void setOrder(int order) {
        mOrder = order;
    }
}

在我的applicationContext.xml中,我有以下幾行:

<aop:aspectj-autoproxy />
<tx:annotation-driven transaction-manager="springTransactionManager" order="5"/>

這個工作到現在為止。

優點:

  • 數據庫和框架獨立
  • 簡單的實施
  • 日志文件始終是最新的
  • 每次事務后自動調用rollback()commit()

缺點:

  • (日志文件中的日志條目不按事件時間戳排序,而是按“transaction-completition”-timestamp排序。我認為這不是一個很大的缺點,因為DB-actions實際上是在提交事務和LogRecords時發生的。仍然正確訂購了一筆交易。)

如果您希望Logger僅在提交事務時“提交”其日志消息,那么您最好的解決方案是讓您的Logger成為XAResource並使用XA事務。 這意味着您的XALogger會從事務管理器收到准備/提交通知。 這樣,您的XAResource就成為參與XA轉換的“資源管理器”。 您的XALogger需要在事務管理器中注冊,就像注冊JMS和JDBC資源一樣。 您可以編寫JCA實現,也可以(更簡單)使用當前的java.transaction.Transaction注冊java.transaction.Transaction

transaction.enlistResource(myXALogger)

並讓它實現XAResource (JavaDoc)

為什么不創建一個方面並實現建議,特別是在返回建議之后,檢查一下自2.0以來可用的spring文檔:

Types of advice:

Before advice: Advice that executes before a join point, but which does not have the ability to prevent execution flow proceeding to the join point (unless it throws an exception).

After returning advice: Advice to be executed after a join point completes normally: for example, if a method returns without throwing an exception.

After throwing advice: Advice to be executed if a method exits by throwing an exception.

After (finally) advice: Advice to be executed regardless of the means by which a join point exits (normal or exceptional return).

Around advice: Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.

如果您只需要記錄每件事情是否正常,那么創建建議並保持代碼清潔:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
    // ...
    }
}

所以,我想知道評論留給你的地方:) @MichaelBorgwardt建議登錄數據庫,@ cherouvim補充說你應該定期將數據庫中的日志條目寫入文件。

它還可以在事務由不同的線程提交之后直接開始將日志條目轉換為文件(這樣可以避免提交和在文件中提交它之間的差距)。

另一種可能性是為每個事務編寫一個日志文件。 在'回滾'上刪除它。 在'commit'中,您將它附加到您現有的日志文件中(或者您決定不使用單個日志文件,而是使用logdir並在'commit'上移動單個日志文件)。

目前還不清楚你用rec.setMillis(System.currentTimeMillis());實現了什么rec.setMillis(System.currentTimeMillis()); 記錄的事件發生在記錄創建時,而不是在提交時。 它只是緩存在RAM中直到提交時間。 通過以這種方式覆蓋它,您可以記錄提交時間而不是事件發生時間。 在閱讀日志時,您必須有一些實際排序的指示,否則您將無法解釋因果關系。

您的解決方案也會遇到崩潰恢復問題。 事務管理器將處理事務資源的提交,但是易失性日志消息將丟失。 根據您的要求,這可能是也可能是不可容忍的。

一節關於Logging in Spring Reference。

它顯示了如何配置不同的日志框架,其中包括log4j

在您的情況下,配置的最后一行是:

log4j.logger.org.springframework.transactions=DEBUG

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM