Java中定义错误码/字符串的最佳方法是什么?

140

我正在使用Java编写一个Web服务,并且正在尝试找出定义错误代码及其关联错误字符串的最佳方法。我需要将数字错误代码和错误字符串组合在一起。错误代码和错误字符串都将被发送给访问Web服务的客户端。例如,当发生SQLException时,我可能想要执行以下操作:

// Example: errorCode = 1, 
//          errorString = "There was a problem accessing the database."
throw new SomeWebServiceException(errorCode, errorString);

客户端程序可能会显示以下消息:

"错误#1已发生:访问数据库时出现问题。"

我最初的想法是使用错误代码的Enum,并覆盖toString方法以返回错误字符串。下面是我的实现:

public enum Errors {
  DATABASE {
    @Override
    public String toString() {
      return "A database error has occured.";
    }
  },

  DUPLICATE_USER {
    @Override
    public String toString() {
      return "This user already exists.";
    }
  },

  // more errors follow
}

我的问题是:有没有更好的方法来完成这个任务?我希望能用代码解决,而不是从外部文件读取。我正在使用Javadoc进行此项目的开发,并且能够在行内记录错误代码并使其自动更新到文档中会很有帮助。


晚来的评论,但值得一提... 1)你真的需要在异常中使用错误代码吗?请参见下面blabla999的答案。2)传递太多错误信息给用户时应该小心。有用的错误信息应写入服务器日志,但客户端只应告知最少量(例如,“登录时出现问题”)。这是安全和防止欺骗者获得立足点的问题。 - wmorrison365
14个回答

190

那么肯定有更好的枚举解决方案(通常相当不错):

public enum Error {
  DATABASE(0, "A database error has occurred."),
  DUPLICATE_USER(1, "This user already exists.");

  private final int code;
  private final String description;

  private Error(int code, String description) {
    this.code = code;
    this.description = description;
  }

  public String getDescription() {
     return description;
  }

  public int getCode() {
     return code;
  }

  @Override
  public String toString() {
    return code + ": " + description;
  }
}

你可能想要重写toString()方法,只返回描述信息 - 不确定。无论如何,重点是你不需要为每个错误代码单独覆盖它。另外请注意,我明确指定了代码而不是使用序数值 - 这样可以更容易地在以后更改顺序或添加/删除错误。

请注意这并没有国际化 - 但除非你的Web服务客户端发送给你一个区域说明,否则你无法轻松地自行国际化它。至少他们将有错误代码可用于客户端端的国际化...


14
为了国际化,将描述字段替换为可以在资源包中查找的字符串代码。 - Marcus Downing
@Jon Skeet!我喜欢这个解决方案,如何制作一个易于本地化的解决方案(或翻译成其他语言等)。考虑在Android中使用它,我可以使用R.string.IDS_XXXX而不是硬编码字符串吗? - A.B.
1
@A.B.:一旦你有了枚举,你可以很容易地编写一个类来通过属性文件或其他方式从枚举值中提取相关的本地化资源。 - Jon Skeet
谢谢Jon!我已经设计了类似的东西,SUCCESS(0, App.R().getString(R.string.IDS_0206));其中App是从android.app.Application继承的类,而App.R()是>>public static Resources R(){ return getContext().getResources();} - A.B.
太棒了!@JonSkeet...不错的解决方案 :) - Tom Taylor
显示剩余7条评论

39

就我而言,我更喜欢将错误消息外部化到属性文件中。 这对于应用程序的国际化非常有帮助(每种语言一个属性文件)。同时修改错误消息也更加容易,不需要重新编译Java源代码。

在我的项目中,通常我会有一个包含错误代码的接口(字符串或整数,没什么关系),其中包含该错误在属性文件中的键:

public interface ErrorCodes {
    String DATABASE_ERROR = "DATABASE_ERROR";
    String DUPLICATE_USER = "DUPLICATE_USER";
    ...
}
在属性文件中:
DATABASE_ERROR=An error occurred in the database.
DUPLICATE_USER=The user already exists.
...

你的解决方案还存在另一个问题,即可维护性:仅存在两个错误,但已经有12行代码了。 那么想象一下当你需要管理数百个错误时,你的枚举文件会变得多么庞大臃肿!


3
如果可以的话,我会把这个评分提高到超过1。硬编码字符串对于维护来说很丑陋。 - Robin
7
在接口中存储字符串常量是一个不好的想法。你可以使用枚举或者在最终类中使用字符串常量,这些类具有私有构造函数,针对每个包或相关区域。请参考John Skeet提供的答案,使用枚举类型实现。请查看以下链接: https://dev59.com/dXRC5IYBdhLWcg3wXPwC#320642 - Anand Varkey Philips

22

重载 toString() 似乎有些别扭 - 这似乎是对 toString() 正常用法的一种过度解释。

那么怎么办呢:

public enum Errors {
  DATABASE(1, "A database error has occured."),
  DUPLICATE_USER(5007, "This user already exists.");
  //... add more cases here ...

  private final int id;
  private final String message;

  Errors(int id, String message) {
     this.id = id;
     this.message = message;
  }

  public int getId() { return id; }
  public String getMessage() { return message; }
}

对我来说,看起来更加清爽...而且不那么啰嗦。


5
重载任何对象的 toString() 方法(更不用说枚举了)都很正常。 - cletus
+1 这个解决方案虽然不如Jon Skeet的方案灵活,但仍然很好地解决了问题。谢谢! - William Brendel
2
我的意思是,toString()最常见且最有用的用途是提供足够的信息来识别对象 - 它通常包括类名或某种方式来有意义地告知对象的类型。在许多情况下,返回仅为“发生了数据库错误”的toString()将会令人惊讶。 - Cowan
1
我同意Cowan的观点,以这种方式使用toString()似乎有点“hackish”。这只是一个快速的解决方案,而不是正常的用法。对于枚举类型,toString()应该返回枚举常量的名称。当您想要变量的值时,在调试器中查看这将会很有趣。 - Robin

21

在我上一份工作中,我更深入地了解了枚举版本:

public enum Messages {
    @Error
    @Text("You can''t put a {0} in a {1}")
    XYZ00001_CONTAINMENT_NOT_ALLOWED,
    ...
}

@Error、@Info和@Warning在类文件中保留,并可在运行时使用。 (我们还有另外一些注释来帮助描述消息传递)

@Text是一个编译时注释。

我为此编写了一个注释处理器,它执行以下操作:

  • 验证没有重复的消息编号(第一个下划线之前的部分)
  • 语法检查消息文本
  • 生成包含文本的messages.properties文件,由枚举值进行键控。

我编写了几个实用程序例程,以帮助记录错误,将它们包装为异常(如果需要),等等。

我正在努力让他们让我开源它...--斯科特


2
处理错误信息的方式很好。你已经开源了吗? - bobbel

6

仅仅为了强调这个陈词滥调,当错误信息显示给终端客户时,数字错误代码对我们非常有用,因为他们经常会忘记或者误读实际的错误信息,但是有时候会记住并报告一个数字值,这可以给你一些线索来了解实际发生了什么。


5
我建议你看一下java.util.ResourceBundle。即使你不关心I18N,也值得这样做。将消息外部化是一个非常好的想法。我发现让业务人员填写他们想要看到的确切语言的电子表格非常有用。我们编写了一个Ant任务,在编译时生成.properties文件。它使I18N变得微不足道。
如果你也在使用Spring,那就更好了。他们的MessageSource类对于这些事情非常有用。

4

有很多种方法可以解决这个问题。我倾向的方法是使用接口:

public interface ICode {
     /*your preferred code type here, can be int or string or whatever*/ id();
}

public interface IMessage {
    ICode code();
}

现在您可以定义任意数量的枚举,以提供消息:
public enum DatabaseMessage implements IMessage {
     CONNECTION_FAILURE(DatabaseCode.CONNECTION_FAILURE, ...);
}

现在您有几种选项将它们转换为字符串。您可以将字符串编译到您的代码中(使用注释或枚举构造函数参数),也可以从配置/属性文件、数据库表或混合中读取。后者是我首选的方法,因为您总是需要一些消息,可以尽早地将其转换为文本(即在连接到数据库或读取配置时)。 我正在使用单元测试和反射框架来查找实现我的接口的所有类型,以确保每个代码都被某处使用,并且配置文件包含所有预期的消息等。使用可以解析Java的框架,如https://github.com/javaparser/javaparserEclipse的框架, 您甚至可以检查枚举的使用情况并找到未使用的枚举。

2
我(以及我所在公司的团队)更喜欢引发异常而不是返回错误代码。错误代码必须在各处进行检查、传递,并且当代码量变大时往往会使代码难以阅读。
然后,错误类将定义消息。
附注:实际上也关心国际化!
另外,如果需要(至少在可以扩展/更改异常类和相关类的环境中),您还可以重新定义raise方法并添加日志记录、过滤等。

抱歉,Robin,但是(至少从上面的例子来看),这应该是两个异常 - “数据库错误”和“重复用户”是如此完全不同,应该创建两个单独可捕获的错误子类(一个是系统错误,另一个是管理员错误)。 - blabla999
错误代码的作用是什么,难道不是为了区分不同的异常吗?因此,至少在处理程序之上,他正在处理传递并进行 if-switch 的错误代码。 - blabla999
我认为异常的名称比错误代码更具说明性和自我描述性。在发现良好的异常名称方面,最好多花些时间思考。 - duffymo
@blabla999 啊,我的想法完全一样。为什么要捕获粗粒度的异常,然后测试“如果错误代码等于x、y或z”。这样很麻烦,而且违背了常规。此外,在堆栈的不同级别上无法捕获不同的异常。你必须在每个级别上进行捕获并测试每个错误代码。这使得客户端代码变得更加冗长... 如果可以的话,我会给+1和更多。话虽如此,我想我们必须回答OP的问题。 - wmorrison365
2
请记住,这是一个网络服务。客户端只能解析字符串。在服务器端仍会抛出异常,其中包含一个errorCode成员,可以在最终响应中向客户端使用。 - pkrish

1
有点晚了,但我只是在为自己寻找一个漂亮的解决方案。如果您有不同类型的错误消息,可以添加简单的自定义消息工厂,以便稍后指定更多细节和格式。
public enum Error {
    DATABASE(0, "A database error has occured. "), 
    DUPLICATE_USER(1, "User already exists. ");
    ....
    private String description = "";
    public Error changeDescription(String description) {
        this.description = description;
        return this;
    }
    ....
}

Error genericError = Error.DATABASE;
Error specific = Error.DUPLICATE_USER.changeDescription("(Call Admin)");

编辑: 好的,这里使用枚举有点危险,因为你会永久地更改特定的枚举。 我想更好的方法是改成类并使用静态字段,但那样你就不能再使用'=='了。所以我想这是一个很好的例子,告诉我们不要这样做(或只在初始化期间这样做):)


1
完全同意您的编辑,运行时更改枚举字段不是一个好的做法。采用这种设计,每个人都可以编辑错误消息。这是非常危险的。 枚举字段应该始终是final的。 - b3nyc

1

使用interface作为消息常量通常不是一个好主意。它将永久地泄漏到客户端程序中,成为导出API的一部分。谁知道,以后的客户端程序员可能会将这些错误消息(公共)作为他们程序的一部分进行解析。

如果字符串格式发生变化,你将永远被锁定以支持此功能,因为这可能会破坏客户端程序。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接