Exception Handling With Jersey – 泽西岛的异常处理

最后修改: 2022年 4月 22日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this tutorial, we’re going to see different ways of handling exceptions with Jersey, which is a JAX-RS implementation.

在本教程中,我们将看到用exceptions处理Jersey的不同方法,这是一个JAX-RS实现。

JAX-RS gives us many mechanisms to handle exceptions, that we can choose and combine. Handling REST exceptions is an important step to build a better API. In our use case, we’ll build an API for buying stocks and see how each step affects the other.

JAX-RS为我们提供了许多处理异常的机制,我们可以选择和组合。处理REST异常是构建更好的API的一个重要步骤。在我们的用例中,我们将构建一个用于购买股票的API,并看看每一步是如何影响其他步骤的。

2. Scenario Setup

2.情景设置

Our minimal setup involves creating a repository, a couple of beans, and some endpoints. It starts with our resource configuration. There, we’ll define our starting URL with @ApplicationPath and our endpoints package:

我们的最小设置包括创建一个资源库、几个bean和一些端点。它从我们的资源配置开始。在那里,我们将用@ApplicationPath定义我们的起始URL和我们的端点包。

@ApplicationPath("/exception-handling/*")
public class ExceptionHandlingConfig extends ResourceConfig {
    public ExceptionHandlingConfig() {
        packages("com.baeldung.jersey.exceptionhandling.rest");
    }
}

2.1. Beans

2.1.Bean

We’ll need only two beans: Stock and Wallet, so we can save Stocks and buy them. For our Stock, we just need a price property to help with validations. More importantly, our Wallet class will have validation methods to help build our scenario:

我们只需要两种Bean。StockWallet,所以我们可以保存Stocks并购买它们。对于我们的Stock,我们只需要一个price属性来帮助验证。更重要的是,我们的Wallet类将有验证方法来帮助建立我们的场景。

public class Wallet {
    private String id;
    private Double balance = 0.0;

    // getters and setters

    public Double addBalance(Double amount) {
        return balance += amount;
    }

    public boolean hasFunds(Double amount) {
        return (balance - amount) >= 0;
    }
}

2.2. Endpoints

2.2.端点

Similarly, our API will have two endpoints. These will define standard methods to save and retrieve our beans:

同样地,我们的API将有两个端点。这些将定义标准方法来保存和检索我们的Bean。

@Path("/stocks")
public class StocksResource {
    // POST and GET methods
}
@Path("/wallets")
public class WalletsResource {
    // POST and GET methods
}

For example, let’s see our GET method in StocksResource:

例如,让我们看看我们在StocksResource的GET方法。

@GET
@Path("/{ticker}")
@Produces(MediaType.APPLICATION_JSON)
public Response get(@PathParam("ticker") String id) {
    Optional<Stock> stock = stocksRepository.findById(id);
    stock.orElseThrow(() -> new IllegalArgumentException("ticker"));

    return Response.ok(stock.get())
      .build();
}

In our GET method, we’re throwing our first exception. We’ll only handle that later, so we can see its effects.

在我们的GET方法中,我们要抛出第一个异常。我们只在后面处理这个问题,所以我们可以看到它的效果。

3. What Happens When We Throw an Exception?

3.当我们抛出一个异常时会发生什么?

When an unhandled exception occurs, we might expose sensitive information about the internals of our application. If we try that GET method from StocksResource with a nonexistent Stock, we get a page similar to this:

当一个未处理的异常发生时,我们可能会暴露关于我们应用程序内部的敏感信息。如果我们从StocksResource尝试那个GET方法,并使用一个不存在的Stock,我们会得到一个类似这样的页面。

default exception screen

This page shows the application server and version, which might help potential attackers to exploit vulnerabilities. Also, there is information regarding our class names and line numbers, which might also help attackers. Most importantly, most of this information is useless to API users and gives a bad impression.

这个页面显示了应用服务器和版本,这可能有助于潜在的攻击者利用漏洞。此外,还有关于我们的类名和行号的信息,这也可能帮助攻击者。最重要的是,这些信息对API用户来说大多是无用的,给人一种不好的印象。

To help control exceptional responses, JAX-RS provides the classes ExceptionMapper and WebApplicationException. Let’s see how they work.

为了帮助控制异常响应,JAX-RS提供了ExceptionMapperWebApplicationException类。让我们看看它们是如何工作的。

4. Custom Exceptions With WebApplicationException

4.使用WebApplicationException的自定义异常

With WebApplicationException, we can create custom exceptions. This special type of RuntimeException lets us define a response status and entity. We’ll start by creating an InvalidTradeException that sets a message and a status:

通过WebApplicationException,我们可以创建自定义异常。这种特殊类型的RuntimeException让我们可以定义一个响应状态和实体。我们将首先创建一个InvalidTradeException,它设置了一个消息和一个状态。

public class InvalidTradeException extends WebApplicationException {
    public InvalidTradeException() {
        super("invalid trade operation", Response.Status.NOT_ACCEPTABLE);
    }
}

Also worthy of mention, JAX-RS defines subclasses of WebApplicationException for common HTTP status codes. These include useful exceptions like NotAllowedException, BadRequestException, etc. But, when we want more complex error messages, we can return a JSON response.

同样值得一提的是,JAX-RS 为常见的 HTTP 状态代码定义了 WebApplicationException 的子类。这些包括有用的异常,如NotAllowedExceptionBadRequestException等。但是,当我们想要更复杂的错误信息时,我们可以返回一个JSON响应。

4.1. JSON Exceptions

4.1.JSON 异常

We can create simple Java classes and include them in our Response. In our example, we have a subject property, which we’ll use to wrap contextual data:

我们可以创建简单的Java类,并将其纳入我们的Response中。在我们的例子中,我们有一个subject属性,我们将用它来包住上下文数据。

public class RestErrorResponse {
    private Object subject;
    private String message;

    // getters and setters
}

Since this exception is not meant to be manipulated, we won’t worry about the type of subject.

由于这个异常不是用来操作的,我们不会担心subject的类型。

4.2. Putting Everything to Use

4.2.把所有东西都用上

To see how we can use custom exceptions, let’s define a method for buying a Stock:

为了了解我们如何使用自定义异常,让我们定义一个购买股票的方法。

@POST
@Path("/{wallet}/buy/{ticker}")
@Produces(MediaType.APPLICATION_JSON)
public Response postBuyStock(
  @PathParam("wallet") String walletId, @PathParam("ticker") String id) {
    Optional<Stock> stock = stocksRepository.findById(id);
    stock.orElseThrow(InvalidTradeException::new);

    Optional<Wallet> w = walletsRepository.findById(walletId);
    w.orElseThrow(InvalidTradeException::new);

    Wallet wallet = w.get();
    Double price = stock.get()
      .getPrice();

    if (!wallet.hasFunds(price)) {
        RestErrorResponse response = new RestErrorResponse();
        response.setSubject(wallet);
        response.setMessage("insufficient balance");
        throw new WebApplicationException(Response.status(Status.NOT_ACCEPTABLE)
          .entity(response)
          .build());
    }

    wallet.addBalance(-price);
    walletsRepository.save(wallet);

    return Response.ok(wallet)
      .build();
}

In this method, we use everything we’ve created so far. We throw an InvalidTradeException for nonexistent stocks or wallets. And, if we have insufficient funds, build a RestErrorResponse containing our Wallet, and throw it as a WebApplicationException.

在这个方法中,我们使用了到目前为止我们所创建的一切。对于不存在的股票或钱包,我们抛出一个InvalidTradeException而且,如果我们的资金不足,建立一个RestErrorResponse,包含我们的Wallet,并将其作为一个WebApplicationException抛出。

4.3. Use Case Example

4.3.用例

Firstly, let’s create a Stock:

首先,让我们创建一个股票

$ curl 'http://localhost:8080/jersey/exception-handling/stocks' -H 'Content-Type: application/json' -d '{
    "id": "STOCK",
    "price": 51.57
}'

{"id": "STOCK", "price": 51.57}

Then a Wallet to buy it:

然后用一个钱包来购买。

$ curl 'http://localhost:8080/jersey/exception-handling/wallets' -H 'Content-Type: application/json' -d '{
    "id": "WALLET",
    "balance": 100.0
}'

{"balance": 100.0, "id": "WALLET"}

After that, we’ll buy the Stock using our Wallet:

之后,我们将使用我们的钱包购买股票

$ curl -X POST 'http://localhost:8080/jersey/exception-handling/wallets/WALLET/buy/STOCK'

{"balance": 48.43, "id": "WALLET"}

And we’ll get our updated balance in the response. Also, if we try to buy again, we’ll get our detailed RestErrorResponse:

而我们会在回应中得到我们最新的余额。此外,如果我们再次尝试购买,我们会得到我们详细的RestErrorResponse

{
    "message": "insufficient balance",
    "subject": {
        "balance": 48.43,
        "id": "WALLET"
    }
}

5. Unhandled Exceptions With ExceptionMapper

5.用ExceptionMapper处理未处理的异常

To clarify, throwing a WebApplicationException won’t be enough to get rid of the default error page. We have to specify an entity for our Response, which is not the case for InvalidTradeException. Often, as much as we try to handle all scenarios, an unhandled exception still might occur. So it’s a good idea to start by handling those. With ExceptionMapper, we define catch points for specific types of exceptions, and modify the Response before committing it:

澄清一下,抛出一个WebApplicationException并不足以摆脱默认错误页面。我们必须为我们的Response指定一个实体,而InvalidTradeException却不是这样的。通常情况下,尽管我们试图处理所有的情况,但仍可能发生未处理的异常。所以从处理这些异常开始是个好主意。通过ExceptionMapper,我们为特定类型的异常定义了捕捉点,并在提交之前修改Response

public class ServerExceptionMapper implements ExceptionMapper<WebApplicationException> {
    @Override
    public Response toResponse(WebApplicationException exception) {
        String message = exception.getMessage();
        Response response = exception.getResponse();
        Status status = response.getStatusInfo().toEnum();

        return Response.status(status)
          .entity(status + ": " + message)
          .type(MediaType.TEXT_PLAIN)
          .build();
    }
}

For example, we’re just repassing exception information into our Response, which will display exactly what we return. Subsequently, we can go a little further by checking the status code before building our Response:

例如,我们只是将异常信息重新传递到我们的Response中,它将准确显示我们返回的内容。随后,我们可以更进一步,在构建我们的Response之前检查状态代码。

switch (status) {
    case METHOD_NOT_ALLOWED:
        message = "HTTP METHOD NOT ALLOWED";
        break;
    case INTERNAL_SERVER_ERROR:
        message = "internal validation - " + exception;
        break;
    default:
        message = "[unhandled response code] " + exception;
}

5.1. Handling Specific Exceptions

5.1.处理特定的例外情况

If there’s a specific Exception that is thrown often, we can also create an ExceptionMapper for it. In our endpoints, we throw an IllegalArgumentException for simple validations, so let’s start with a mapper for it. This time, with a JSON response:

如果有一个特定的Exception经常被抛出,我们也可以为它创建一个ExceptionMapper在我们的端点中,我们为简单的验证抛出了一个IllegalArgumentException,所以让我们先为它创建一个映射器。这次是一个JSON响应。

public class IllegalArgumentExceptionMapper
  implements ExceptionMapper<IllegalArgumentException> {
    @Override
    public Response toResponse(IllegalArgumentException exception) {
        return Response.status(Response.Status.EXPECTATION_FAILED)
          .entity(build(exception.getMessage()))
          .type(MediaType.APPLICATION_JSON)
          .build();
    }

    private RestErrorResponse build(String message) {
        RestErrorResponse response = new RestErrorResponse();
        response.setMessage("an illegal argument was provided: " + message);
        return response;
    }
}

Now every time an unhandled IllegalArgumentException occurs in our application, our IllegalArgumentExceptionMapper will handle it.

现在,每当我们的应用程序中出现未处理的IllegalArgumentException时,我们的IllegalArgumentExceptionMapper将处理它。

5.2. Configuration

5.2.配置

To activate our exception mappers, we have to go back to our Jersey resource configuration and register them:

为了激活我们的异常映射器,我们必须回到我们的Jersey资源配置并注册它们。

public ExceptionHandlingConfig() {
    // packages ...
    register(IllegalArgumentExceptionMapper.class);
    register(ServerExceptionMapper.class);
}

This is enough to get rid of the default error page. Then, depending on what is thrown, Jersey will use one of our exception mappers when an unhandled exception occurs. For instance, when trying to get a Stock that doesn’t exist, the IllegalArgumentExceptionMapper will be used:

这足以让我们摆脱默认的错误页面。然后,根据抛出的内容,当发生未处理的异常时,Jersey将使用我们的一个异常映射器。例如,当试图获取一个不存在的Stock时,将使用IllegalArgumentExceptionMapper

$ curl 'http://localhost:8080/jersey/exception-handling/stocks/NONEXISTENT'

{"message": "an illegal argument was provided: ticker"}

Likewise, for other unhandled exceptions, the broader ServerExceptionMapper will be used. For example, when we use the wrong HTTP method:

同样地,对于其他未处理的异常,将使用更广泛的ServerExceptionMapper。例如,当我们使用错误的HTTP方法时。

$ curl -X POST 'http://localhost:8080/jersey/exception-handling/stocks/STOCK'

Method Not Allowed: HTTP 405 Method Not Allowed

6. Conclusion

6.结语

In this article, we saw the many ways we can handle exceptions using Jersey. Moreover, why it’s important, and how to configure it. After that, we built a simple scenario where we could apply them. As a result, we now have a friendlier and more secure API.

在这篇文章中,我们看到了使用Jersey处理异常的多种方式。此外,为什么它很重要,以及如何配置它。之后,我们构建了一个简单的场景,在那里我们可以应用它们。结果是,我们现在有了一个更友好、更安全的API。

And as always, the source code is available over on GitHub.

一如既往,源代码可在GitHub上获得。