2. 场景设置
我们构建一个最小化的股票交易API,涉及仓库类、核心Bean和REST接口。先从资源配置开始:
@ApplicationPath("/exception-handling/*")
public class ExceptionHandlingConfig extends ResourceConfig {
public ExceptionHandlingConfig() {
packages("com.baeldung.jersey.exceptionhandling.rest");
}
}
2.1. 核心Bean
只需两个Bean:Stock
(股票)和Wallet
(钱包)。Stock
需包含价格属性用于验证,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. REST接口
API包含两个核心接口,定义Bean的增删改查操作:
@Path("/stocks")
public class StocksResource {
// POST and GET methods
}
@Path("/wallets")
public class WalletsResource {
// POST and GET methods
}
以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();
}
这里抛出了第一个异常,我们稍后处理,先观察其影响。
3. 抛出异常会发生什么?
未处理异常会暴露应用内部敏感信息。尝试获取不存在的Stock
时,默认返回类似页面:
[默认异常页面示意图]
这种页面暴露了:
- 应用服务器及版本(可能被攻击者利用)
- 类名和行号(辅助攻击者分析)
- 对用户无用的信息,损害API专业性
JAX-RS提供ExceptionMapper
和WebApplicationException
控制异常响应。下面详解用法。
4. 使用 WebApplicationException 自定义异常
WebApplicationException
是特殊RuntimeException
,可自定义响应状态和实体。创建InvalidTradeException
设置状态和消息:
public class InvalidTradeException extends WebApplicationException {
public InvalidTradeException() {
super("invalid trade operation", Response.Status.NOT_ACCEPTABLE);
}
}
JAX-RS已预定义常用状态码的子类(如NotAllowedException
、BadRequestException
)。需要复杂错误信息时,可返回JSON响应。
4.1. JSON异常响应
创建简单Java类封装错误信息:
public class RestErrorResponse {
private Object subject;
private String message;
// getters and setters
}
subject
属性用于包装上下文数据,类型不限。
4.2. 实战应用
定义股票购买方法,整合所有组件:
@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();
}
关键点:
- 股票或钱包不存在时抛
InvalidTradeException
- 余额不足时构建
RestErrorResponse
并包装为WebApplicationException
4.3. 用例演示
创建股票:
$ curl 'http://localhost:8080/jersey/exception-handling/stocks' -H 'Content-Type: application/json' -d '{
"id": "STOCK",
"price": 51.57
}'
{"id": "STOCK", "price": 51.57}
创建钱包:
$ curl 'http://localhost:8080/jersey/exception-handling/wallets' -H 'Content-Type: application/json' -d '{
"id": "WALLET",
"balance": 100.0
}'
{"balance": 100.0, "id": "WALLET"}
购买股票:
$ curl -X POST 'http://localhost:8080/jersey/exception-handling/wallets/WALLET/buy/STOCK'
{"balance": 48.43, "id": "WALLET"}
余额不足时返回详细错误:
{
"message": "insufficient balance",
"subject": {
"balance": 48.43,
"id": "WALLET"
}
}
5. 使用 ExceptionMapper 处理未捕获异常
重要: 抛WebApplicationException
需指定响应实体,否则仍显示默认错误页。用ExceptionMapper
捕获特定异常并修改响应:
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();
}
}
进阶版:根据状态码定制消息
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. 处理特定异常
为频繁抛出的异常创建专用Mapper。以IllegalArgumentException
为例:
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;
}
}
所有未捕获的IllegalArgumentException
都会被此Mapper处理。
5.2. 配置生效
在Jersey配置中注册Mapper:
public ExceptionHandlingConfig() {
// packages ...
register(IllegalArgumentExceptionMapper.class);
register(ServerExceptionMapper.class);
}
效果:
- 彻底消除默认错误页
- 根据异常类型自动选择Mapper
测试获取不存在的股票:
$ curl 'http://localhost:8080/jersey/exception-handling/stocks/NONEXISTENT'
{"message": "an illegal argument was provided: ticker"}
错误HTTP方法触发通用Mapper:
$ curl -X POST 'http://localhost:8080/jersey/exception-handling/stocks/STOCK'
Method Not Allowed: HTTP 405 Method Not Allowed
6. 总结
本文系统介绍了Jersey异常处理的多种方案:
- 核心价值:提升API友好性和安全性
- 关键组件:
WebApplicationException
和ExceptionMapper
- 最佳实践:组合使用自定义异常和全局Mapper
通过股票交易场景的实战演示,我们构建了更健壮的REST API。完整源码见GitHub仓库。