P2P系统异常处理方案

1. Java 异常最佳实践

1.1 不要在catch块中吞没异常

空的异常或者会使异常达不到应有的目的,因为它不仅隐藏了错误和异常,同时可能导致你的对象处于不可使用或者脏的状态。忽略异常就如同忽略火警信号一样——当真正的火灾发生时,就没有人能够看到火警信号了。如果在某些特殊情况下需要这样做,至少添加一条注释说明为什么可以忽略这个异常。


//Incorrect way
try {
    someMethod();    
} catch(InvalidArgumentException e){

}

1.2 捕获具体异常子类,不要捕获Exception类、RuntimeException类

假如被调用方法的开发者在方法签名中throws一个新的检查型异常(checked exception),该开发者的目的是需要调用者处理该类型的异常。如果调用者的代码如下,那么调用者可能永远都不会知道这个变化,并且可能使代码在运行时错误的运行。

另一种情况是被调用方法的开发者在方法块中throw一个非检查型异常(unchecked exception),那么调用者永远都不会知道这个变化,捕获了不该捕获的非检查异常,从而可能导致代码错误的运行。


//Incorrect way
try {
    someMethod();
} catch (Exception e) {
    LOGGER.error("method has failed", e);
}

1.3 不要捕获Throwable类

捕获Throwable类可能会导致很严重的问题,因为Java的Error类也是Throwable的子类,该类代表不可挽回的错误产生,这种错误甚至连JVM都不能处理。


//Incorrect way
try {
    someMethod();
} catch (Throwable e) {
    LOGGER.error("method has failed", e);
}

1.4 要么抛出,要么打印日志,不要两个都做

如下代码会导致在日志文件中打印多个错误日志信息,对查阅日志的工程师来说就是一个噩梦。


//Incorrect way
try {
    someMethod();
} catch (InvalidArgumentException e) {
    LOGGER.error("method has failed", e);
    throw e;
}    

1.5 永远不要在finally块中抛出异常


try {
    someMethod();  //Throws exceptionOne
} finally {
    cleanUp();    //If finally also threw any exception the exceptionOne will be lost forever
}
如以上代码所示,假如cleanup()方法不会抛出任何异常,那么这段代码是正确的。假如someMethod()抛出一个异常,并且finally块中的cleanup()方法也抛出了一个异常,那么第二个异常会被抛出去,第一个异常会被丢失。如果finally块中的方法可能会抛出异常,请确保你会在finally块中处理该异常。 ###1.6 始终提供关于异常的有意义的完整的信息 对于开发者异常信息是最重要的地方,因为这是首先看到的地方,这里能找到问题产生的根本原因。例如,对比IllegalArgumentException 异常的两条异常信息: 消息 1: "Incorrect argument for method" 消息 2: "Illegal value for ${argument}: ${value} 第一条消息仅说明了参数是非法的或者不正确,但第二条消息包括了参数名和非法值,而这对于找到错误的原因是很重要的。在用Java编写异常处理代码的时候,始终遵循该最佳实践。 ###1.7 在finally程序块中关闭或者释放资源 当在使用数据库连接或网络等资源的时候,请确保一定要在finally块中关闭资源。finally块在正常和异常执行的情况下,能保证资源的合理释放。 ###1.8 在堆栈跟踪中包含引起异常的原因 很多时候,当一个由另一个异常导致的异常被抛出的时候,Java库和开放源代码会将一种异常包装成另一种异常。日志记录和打印异常就变得非常重要。Java异常类提供了getCause()方法来检索导致异常的原因,这些(原因)可以对异常的根层次的原因提供更多的信息。该Java实践对在进行调试或排除故障大有帮助。时刻记住,如果你将一个异常包装成另一种异常时,构造一个新异常要传递源异常。 ###1.9 避免过度使用检查型异常(checked exception) 检查性异常是Java语言的一项特性。它们强迫开发者处理异常,大大增强了可靠性。但是,过度使用检查型异常会使调用者使用起来非常不便。如果一个方法抛出一个或者多个检查型异常,调用该方法的代码必须在一个或者多个catch块中处理这些异常,或者必须声明抛出这些异常。无论哪种方法,都给程序员增添了负担。 如果正确的调用方法并不能避免异常产生,并且一旦异常产生,调用方法的代码可以立即采取有用的动作,这种负担被认为是正确的。除非这两个条件都成立,否则更适合使用非检查型异常。如果开发者不确定,那么非检查型异常可能更为适合。 ###1.10 对可恢复的情况使用检查型异常,对编程错误使用运行时异常 Java语言提供了三种可抛出结构(Throwable):检查型异常(checked exception)、运行时异常(run-time exception)也叫非检查型异常、错误(error)。在决定使用检查型异常或是非检查型异常时,主要一个原则是:如果期望调用者能够适当地恢复,对于这种情况应该使用受检的异常。通过抛出检查型异常,强迫调用者在一个catch块中处理该异常,或者将它传播出去。 如果方法抛出检查型异常,强迫调用者从这个异常条件中恢复。调用者也可以忽视这种强制性要求,只需要捕获异常并忽略即可,但这往往不是个好办法。 如果方法抛出非检查型异常,往往就属于不可恢复的情形,程序继续执行下去有害无益。如果程序没有捕获这样的异常,将会导致当前线程停止,并出现适当的错误消息。大多数的运行时异常都表示前提违例(precondition violation),所谓前提违例是指方法的调用者没有遵守方法建立的约定。 总而言之,对于可恢复的情况,使用检查型异常;对于程序错误,则使用运行时异常。例如,考虑资源枯竭的情况,这可能是由于程序错误引起的,也可能确实是由于资源不足引起的。程序的开发者需要判断这样的资源枯竭是否允许恢复。如果相信一种情况可能允许恢复,则使用检查型异常。如果不是,则使用运行时异常。如果不确定,最好使用运行时异常。 ##2. P2P平台的异常设计 ###2.1 使用自定义异常 因为平台服务端默认会向前端提供error code和error message。Java标准的异常类不能满足此需求,所以服务端代码全部使用自定义的异常类。仅有少数地方可以使用Jersey框架定义的异常类。 ###2.2 自定义异常是非检查型异常 根据1.9和1.10我们可以明确:只有在可恢复的情况下才使用检查型异常。P2P平台服务端异常子基本都是不可恢复的情况,需要人工干预,所以自定义异常类全部继承自RuntimeException。 ###2.3 规范化异常类 平台默认会提供几种自定义的基础异常类:EntityNotFoundException、InvalidArgumentException、InvalidStateException、InternalServerErrorException等。根据资源类型种类和使用目的,实现时继承平台默认提供的基础异常类。例如,UserRepository根据userId查找User对象时,可能找不到,所以会报UserNotFoundException,它继承自定义的EntityNotFoundException。另一种情况,有多种原因会导致平台抛出SubjectInvalidArgumentException, 我们可以通过error code明确异常产生的原因。 注意以下代码有2个构造函数: 1. 第一个构造函数在异常产生处使用,也就是异常产生的起点处。 2. 第二个构造函数用于捕获一个异常,用自定义的异常包装(warp)被捕获的异常,将原始异常传入构造函数,目的是避免原始异常丢失。

public class EntityNotFoundException extends RuntimeException {

    private final UMSErrorCode code;
    private final Object[] args;

    public EntityNotFoundException(UMSErrorCode errorCode, Object... args) {
        super(new MessageFormat(errorCode.getMessageTemplate()).format(args));
        this.code = errorCode;
        this.args = args;
    }

    public EntityNotFoundException(UMSErrorCode errorCode, Throwable cause, Object... args) {
        super(new MessageFormat(errorCode.getMessageTemplate()).format(args), cause);
        this.code = errorCode;
        this.args = args;
    }

    public UMSErrorCode getCode() {
        return code;
    }

    public Object[] getArgs() {
        return args;
    }
}

2.4 使用commons-lang 2.5


LOGGER.error(ExceptionUtils.getFullStackTrace(e));

2.5 日志打印

在多线程并发环境下,多个线程的日志都打印在同一个日志文件中, 需要区分哪些信息是在同一个线程输出的。log4j已经提供了一种默认的实现方式,就是使用PatternLayout,在设定输出格式的时候增加%t参数,这样会输出各个线程的线程名称,这样我们就可以根据线程名称区分哪些内容是同一个线程输出出来的。但是如果是一个线程池,池中的线程会被重复利用的情况下,日志会重复打印这个线程名称。为了更好的让日志阅读者更方便的阅读日志,我们可以再使用log4j的MDC,具体的MDC的实现如下:


@Provider
@PreMatching
public class LogMDCFilter implements ContainerRequestFilter, ContainerResponseFilter {

    private static final String REQUEST_UID = "R_UID";
    private static final Random RANDOM = new Random();

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        MDC.put(REQUEST_UID, getRUID(8));
    }

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        MDC.clear();
    }

    private String getRUID(int len) {
        StringBuffer uid = new StringBuffer();
        for (int i = 0; i < len; i++) {
            int rand = RANDOM.nextInt(1000);
            int mod36 = rand % 36;
            encodeAndAdd(uid, mod36);
        }
        return uid.toString();
    }

    private void encodeAndAdd(StringBuffer ret, long mod36Val) {
        if (mod36Val < 10) {
            ret.append((char) (((int) '0') + (int) mod36Val));
        } else {
            ret.append((char) (((int) 'a') + (int) (mod36Val - 10)));
        }
    }

}

然后在输出格式中增加%X{R_UID}参数。注意在进入Jersey Filter之前或者Jersey已经返回Response,MDC的值都为空。

results matching ""

    No results matching ""