简单的增删改查操作异常设计

9
我正在开发一个非常小的测试来模拟一个三层系统,以便了解异常如何工作。同时,我希望提出一种合理的方法,以便将此设计用作其他应用程序类似操作的参考。
我已经阅读了不同文章的主题,似乎在使用已检查或未检查异常方面存在巨大争议,这让我对我的最终设计产生怀疑。
我不会详细介绍批评或支持已检查/未检查异常的论点,因为它们可能都是众所周知的,而是为了寻求一些建议,以改进我的设计并使其(尽可能)类似于实际应用程序。
该系统负责使用JDBC在关系型数据库(比如MySQL)中执行基本的CRUD操作。我有以下内容:演示层、服务层和持久层。

根据 处理服务层中的Dao异常 的答案,不公开特定层实现并解耦层是有道理的。因此,我决定创建自定义异常,并将它们包装成每个层级的基本异常,以便我可以将特定层级的异常(即 SQLException)转换为一般层级的异常(即 PersistentException、BusinessException)。如果以后实现发生变化,我只需将新实现简单地包装在更高层级所期望的基本异常中即可。因此,我有以下异常:

com.user.persistent.exceptions
    |_ PersistentException
    |_ ExistingUserException
    |_ Some more..

com.user.business.exceptions
    |_ BusinessException
    |_ somemore....

从Josh Bloch的书Effective Java中:“对于调用方可以合理地预期恢复的条件,请使用已检查异常。”我也不太确定,但我相信用户可以从SQLExeption中恢复(即用户错误提供现有ID,他可以重新输入正确的ID),因此我决定将先前的异常更改为已检查异常。以下是类的概述:
持久层。
public interface UserDAO 
{
    public void create(User team) throws PersistentException;
}

//Implementation in DefaultUserDAO.java
@Override
    public void create(User team) throws PersistentException 
    {       
        try
        {
            System.out.println("Attempting to create an user - yikes the user already exists!");
            throw new SQLIntegrityConstraintViolationException();           
        }
        catch(SQLIntegrityConstraintViolationException e)
        {
            throw new ExistingUserException(e);
        }
        catch (SQLException e) 
        {
            throw new PersistentException(e);
        } 
        finally 
        {
            //close connection
        }
    }

服务层:

public interface UserService 
{
    void create(User user) throws BusinessException;
}

//Implementation of UserService
@Override
public void create(User user) throws BusinessException 
{
    System.out.println("Doing some business logic before persisting the user..");

    try 
    {
        userDao.create(user);
    } 
    catch (PersistentException e) 
    {
        throw new BusinessException(e);
    }

}

演示
    try 
    {
        userService.create(user);
    } catch (BusinessException e) 
    {   
        e.printStackTrace();
    }

现在有几点让我对这个设计感到不确定。
  1. 我喜欢编译器验证方法使用已声明异常时是否捕获/抛出的想法,但我同时认为这种方法会导致处理所有异常的混乱代码。不确定是因为我没有正确使用异常还是因为检查异常确实会导致混乱的代码。
  2. 我也喜欢通过将特定层的异常包装成通用异常来解耦层。但是,我可以看到会为每种可能的异常创建大量新类,而不是只抛出现有的Java异常。
  3. 我还可以看到,很多现有的代码都用于处理异常,只有一小部分用于系统的实际目标。
这些实际上是我的主要关注点,让我怀疑这是否真的是一个好的设计。我想听听你们对此的意见(也许还有一些小的示例代码片段)。你们能给我一些提示,让我如何可能改进它?以某种方式实现层之间的解耦,并避免泄漏层特定的问题。

我脑海中也有类似的问题。在我看来,当您想将所有系统故障记录在数据库的某些审计表中时,请使用自定义异常(例如您的业务异常)。在 EJB/RMI 时代,业务服务消费者位于系统外部,意味着 applet 客户端将在用户本地浏览器中运行,那时开发人员不想向客户端公开运行时异常。因此,他们发布了这个自定义异常,以便显示适当的消息,现在大多数层都驻留在同一个运行时环境中,因此除非您有充分的理由,否则不需要自定义异常。 - Chetan
6个回答

4

我认为在开发完成后,你的应用程序不应该处理SQL异常。因此,你不应该捕获像SQLException这样的异常。或者如果你被迫捕获它们,那么只需重新抛出RuntimeException。根据我的荣誉经验,只有当你为他人开发某种库时才有意义创建自己的异常。即使在这种情况下,在许多情况下你也可以使用现有的异常。尝试开发而不创建自己的异常。只有当你意识到不能没有它们时才创建它们。


2

我的想法是:
关于1 - 是的,受检异常会增加“杂乱代码” - 这是一种权衡,
你需要考虑什么对你更重要。在许多设计中,没有完美的解决方案,
你必须决定哪个更适合你。

关于BusinessException - 我个人不喜欢它。
当我添加用户时,我想在客户端知道它已经存在。我不想编写“Peals” BusinessException 的代码来获取根本原因。

还有一个普遍的建议 - 请为crud异常使用泛型。
例如,不要使用 UserAlreadyExistsException,而是使用 EntityAlreadyExistsException<User>


1

@Bartzilla:我也不太喜欢在每个层级中包装和解包异常对象,这会使应用程序代码变得混乱。我认为采用错误代码和错误消息的方法更好。

我认为有三种解决方案:

1)将DB层异常包装在应用程序定义的RunTimeException类中。这个RuntimeException应该包含一个错误码字段、一个错误消息和原始异常对象。由于所有DAO API都只会抛出运行时异常,所以业务层不一定需要捕获它。它将被允许一直上升到处理它有意义的地方。

class DAOException extends RuntimeException{
 private int errorCode;
 private String errorMessage;
 private Exception originalException;

 DAOException(int errorCode, String errorMessage, Exception originalException){
    this.errorCode=errorCode;
    this.errorMessage=errorMessage;
    this.originalException=originalException;
    }

}

现在,按照这种方式,您的DAO方法将根据异常来决定错误代码,例如:

int Integrity_Voildation_ERROR=3;
 public void create(User team) throws DAOException
    {       
        try
        {
            System.out.println("Attempting to create an user - yikes the user already exists!");
            throw new SQLIntegrityConstraintViolationException();           
        }
        catch(SQLIntegrityConstraintViolationException e)
        {   int errorCode=Integrity_Voildation_ERROR;
            throw new DAOException(Integrity_Voildation_ERROR,"user is already found",e);
        }
}

这个异常可以在它应该被捕获的层中被捕获。在这种情况下,每个错误代码都意味着可恢复(可操作)的异常。当然,应用程序的入口点(servlet或过滤器或其他任何东西)必须捕获一般异常来捕获不可恢复的异常并向用户显示有意义的错误。

2)让您的DAO API返回一个Result类型的对象,该对象包含与上述DAOException提供的相同信息。

因此,您有了Result类:

    Class Result implements IResult{

    private boolean isSuccess;
    private int errorCode;
     private String errorMessage;
     private Exception originalException;//this might be optional to put here.
    }


So a DAO API:
    public IResult create(User team) throws DAOException
    {      
   IResult result=new Result();
        try
        {
            System.out.println("Attempting to create an user - yikes the user already exists!");
            throw new SQLIntegrityConstraintViolationException();           
        }
        catch(SQLIntegrityConstraintViolationException e)
        {   int errorCode=Integrity_Voildation_ERROR;
            result.setSuccess(false);
        result.setErrorCode(errorCode);
        result.setErrorMessage("user is already found");    
        }
    return result;
}

在上述情况下,约束是每个DAO API都需要返回相同的结果对象。当然,您的业务相关数据可以填充在ResultClass的各种子类中。 请注意,在情况1和2中,您可以使用枚举来定义所有可能的错误代码。错误消息可以从数据库表等地方获取。

3)如果您想避免使用errorCode,可以这样做: 与DAOException(上述情况1)一样,您可以为每种可能的可恢复SQL异常定义一个异常层次结构,每个层次结构都是父类DAOException的子类。

Java基于Spring框架的DAO异常层次结构是同样的优秀示例。请查看this


@Bartzilla:虽然你已经接受了答案,但我仍然想知道你最终选择了哪个选项——只是出于我的好奇心? - ag112

1
基于约定优于配置的概念,您可以设计自己的异常处理程序。我认为最快速和可靠的方法是使用AOP。通过这种方式,您可以根据异常类型在方面中处理异常,并决定是否从异常中恢复。
例如,如果发生验证异常,您可以返回输入页面路径以将客户端发送到现有页面以正确填写数据。如果发生不可恢复的异常,则可以在异常处理程序中返回错误页面。
您可以为输入和错误页面创建一个公共名称的约定。通过这种方式,您可以根据异常类型决定将请求发送到适当的页面。
您可以参考此文章Aspect Oriented Programming
以下是示例。
    @Aspect
    public class ExceptionHandlerAspect {

        @Around("@annotation(ExceptionHandler)")
        public Object isException(ProceedingJoinPoint proceedingJoinPoint) {
            try {
                return proceedingJoinPoint.proceed();
            } catch (Exception exception) {
                if (exception instanceof UnRecoverableException) {
                    addErrorMessage("global system error occurred");
                    return "errorPage";
                } else if (exception instanceof ValidationException) {
                    addErrorMessage("validation exception occurred");
                    return "inputPage";
                } else {
                    addErrorMessage("recoverable exception occurred");
                    return "inputPage";
                }
            }
        }
    }

@Target({METHOD})
@Retention(RUNTIME)
public @interface ExceptionHandler {
}

表示层

@ExceptionHandler
public String createUser() {
    userService.create(user);
    return "success";
}

1

看看spring-jdbc是如何做的。我认为这是一个非常好的设计。Spring处理来自驱动程序层的已检查异常并抛出未检查的异常。它还将来自MySQL、Postgres等不同异常转换为标准的Spring异常。

在过去的6年中,我已经在我的所有代码中切换到了未检查的异常,并且没有回头看过。90%的时间你无法处理条件。而你能处理的情况,要么是通过设计或测试知道的,然后放入相关的catch块中。


0

你说的三层系统 - 这是否意味着这些层可能在不同的计算机或不同的进程中运行?还是只是将代码组织成一组由特定功能定义的层,并且所有内容都在单个节点上运行?

问这个问题的原因是 - 除非您的层在不同的进程中运行 - 否则使用 CheckedExceptions 没有任何价值。大量的 CheckedExceptions 只会使代码混乱并增加不必要的复杂性。

异常设计很重要,像 ag112 建议的那样使用 RunTimeException 是干净、简洁且易于管理的。

这只是我的想法。


谢谢。我是指在单个计算机和单个节点上运行。 - Bartzilla

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