全局异常处理
概述
全局异常处理是构建健壮 REST API 的核心基础设施。Spring 3.2+ 提供 @ControllerAdvice + @ExceptionHandler 声明式处理;Spring Boot 3 引入 RFC 7807 Problem Details 原生支持。
1. @ControllerAdvice 源码原理
1.1 注解定义
@ControllerAdvice 是 @Component 的特殊变体,支持包/类/注解级别过滤:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages") String[] value() default {};
@AliasFor("value") String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}1.2 注册与发现
ExceptionHandlerExceptionResolver 在 afterPropertiesSet() 中扫描所有 @ControllerAdvice Bean,缓存到 LinkedHashMap:
public class ExceptionHandlerExceptionResolver
extends AbstractHandlerMethodExceptionResolver {
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>
exceptionHandlerAdviceCache = new LinkedHashMap<>();
@Override
public void afterPropertiesSet() {
ControllerAdviceBean.findAnnotatedBeans(getApplicationContext())
.stream().filter(a -> a.getBeanType() != null)
.map(a -> new ExceptionHandlerMethodResolver(a.getBeanType()))
.filter(ExceptionHandlerMethodResolver::hasExceptionMappings)
.forEach(resolver -> exceptionHandlerAdviceCache.put(...));
}
}1.3 异常匹配算法
按注册顺序遍历 → 检查 Advice 适用性 → 深度优先沿继承链查找最匹配方法:
@Nullable
protected MethodParameter resolveExceptionByAdvice(
Throwable exception, HandlerMethod handlerMethod) {
for (var entry : exceptionHandlerAdviceCache.entrySet()) {
if (!entry.getKey().isApplicableToBeanType(handlerMethod.getBeanType()))
continue;
Method method = entry.getValue().resolveMethod(exception);
if (method != null) return new MethodParameter(method, -1);
}
return null;
}2. @ExceptionHandler 优先级体系
2.1 就近原则
Spring 优先调用 Controller 内部 @ExceptionHandler,不存在时回退全局 @ControllerAdvice:
@RestController
public class AccountController {
@ExceptionHandler(InsufficientBalanceException.class)
public ResponseEntity<ErrorResponse> handle(InsufficientBalanceException ex) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("INSUFFICIENT_BALANCE", ex.getMessage()));
}
}优先级:类级 @ExceptionHandler > 全局 @ExceptionHandler > DefaultHandlerExceptionResolver > 自定义 HandlerExceptionResolver。
2.2 HandlerExceptionResolver 链
Spring MVC 通过 HandlerExceptionResolver 责任链处理异常,DispatcherServlet 按 @Order 排序:
| 优先级 | 解析器 | 职责 |
|---|---|---|
| 1 | ExceptionHandlerExceptionResolver | 处理 @ExceptionHandler |
| 2 | ResponseStatusExceptionResolver | 处理 @ResponseStatus 注解异常 |
| 3 | DefaultHandlerExceptionResolver | Spring MVC 标准异常兜底 |
注解无法满足时,可自定义 HandlerExceptionResolver 通过 @Order 注册到链中。
2.3 深度优先匹配
ExceptionHandlerMethodResolver 用 ConcurrentReferenceHashMap 缓存异常类型映射,避免重复遍历继承链:
public class ExceptionHandlerMethodResolver {
private final Map<Class<? extends Throwable>, Method> mappedMethods =
new ConcurrentReferenceHashMap<>(1024);
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new LinkedHashMap<>();
@Nullable
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = mappedMethods.get(exceptionType);
if (method != null) return method;
Class<? extends Throwable> closestType =
getClosestMatch(exceptionType, exceptionLookupCache);
if (closestType != null) {
method = exceptionLookupCache.get(closestType);
mappedMethods.put(exceptionType, method);
return method;
}
return null;
}
}示例:InsufficientBalanceException → BusinessException → RuntimeException,最近者胜出。
3. ResponseEntityExceptionHandler 基类
预定义了 Spring MVC 标准异常的处理器,所有 handler 汇聚到 handleExceptionInternal,子类可重写以定制统一响应格式:
public abstract class ResponseEntityExceptionHandler {
@ExceptionHandler({ HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
MethodArgumentNotValidException.class,
HttpMessageNotReadableException.class,
MissingServletRequestParameterException.class,
BindException.class })
@Nullable
protected ResponseEntity<Object> handleException(Exception ex,
BodyBuilder bodyBuilder, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request);
}
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, @Nullable Object body,
HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
if (body == null) {
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(statusCode))
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
body = HttpStatus.valueOf(statusCode.value()).getDefaultMessage();
}
return new ResponseEntity<>(body, headers, statusCode);
}
```java
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
var errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField,
fe -> fe.getDefaultMessage()));
return handleExceptionInternal(ex, ErrorResponse.builder()
.code("VALIDATION_FAILED").message("参数校验失败").errors(errors).build(),
headers, status, request);
}
@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, @Nullable Object body, HttpHeaders headers,
HttpStatusCode statusCode, WebRequest request) {
return new ResponseEntity<>(body != null ? body : ErrorResponse.builder()
.code("SYSTEM_ERROR").message(HttpStatus.valueOf(statusCode.value()).getReasonPhrase()).build(),
headers, statusCode);
}
}4. ProblemDetails(RFC 7807)Spring Boot 3 支持
4.1 规范定义
RFC 7807 定义 HTTP API 错误响应标准格式:
{
"type": "https://api.example.com/errors/insufficient-balance",
"title": "余额不足",
"status": 400,
"detail": "账户 #12345 可用余额 100.00,尝试扣款 200.00",
"instance": "/api/accounts/12345/withdraw"
}标准字段:type(URI 错误标识)、title、status、detail、instance,支持扩展自定义字段。
4.2 Spring Boot 3 自动配置
spring:
mvc:
problemdetails:
enabled: true底层通过 ProblemDetailsExceptionHandler(继承自 ResponseEntityExceptionHandler)实现:
@Configuration
@ConditionalOnClass(ProblemDetail.class)
@ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled",
havingValue = "true", matchIfMissing = false)
public class ProblemDetailsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
return new ProblemDetailsExceptionHandler();
}
}4.3 ProblemDetail 与 ErrorResponse API
Spring 6 新增 ProblemDetail 类(含 type、title、status、detail、instance、properties),支持扩展自定义字段。自定义异常可实现 ErrorResponse 接口携带结构化错误信息:
public class InsufficientBalanceException extends RuntimeException implements ErrorResponse {
private final ProblemDetail problemDetail;
public InsufficientBalanceException(String accountId,
BigDecimal available, BigDecimal requested) {
this.problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST,
String.format("账户 %s 余额不足:可用 %.2f,需要 %.2f", accountId, available, requested));
this.problemDetail.setTitle("余额不足");
this.problemDetail.setProperty("accountId", accountId);
this.problemDetail.setProperty("availableBalance", available);
}
@Override public HttpStatusCode getStatusCode() { return problemDetail.getStatus(); }
@Override public ProblemDetail getBody() { return problemDetail; }
}5. 自定义业务异常体系设计
5.1 架构
RuntimeException
└── BusinessException
├── AccountException(账户域)
│ ├── InsufficientBalanceException
│ ├── AccountFrozenException
│ └── AccountNotFoundException
├── TradeException(交易域)
│ ├── TradeTimeoutException
│ ├── DuplicateTradeException
│ └── TradeRollbackException
├── SystemException(系统级)
└── ThirdPartyException(外部服务)5.2 核心实现
public interface BaseErrorCode {
String getCode();
HttpStatus getHttpStatus();
String getMessage();
default String getDefaultMessage() { return getMessage(); }
}
public enum AccountErrorCode implements BaseErrorCode {
INSUFFICIENT_BALANCE("ACCOUNT_1001", HttpStatus.BAD_REQUEST, "账户余额不足"),
ACCOUNT_FROZEN("ACCOUNT_1002", HttpStatus.FORBIDDEN, "账户已被冻结"),
ACCOUNT_NOT_FOUND("ACCOUNT_1003", HttpStatus.NOT_FOUND, "账户不存在");
// constructor & getters omitted
}5.3 业务异常基类
public class BusinessException extends RuntimeException {
private final BaseErrorCode errorCode;
private final transient Object[] args;
private final String traceId;
private final Map<String, Object> metadata;
public BusinessException(BaseErrorCode errorCode, Object... args) {
super(formatMessage(errorCode.getMessage(), args));
this.errorCode = errorCode;
this.args = args;
this.traceId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
this.metadata = new HashMap<>();
}
public BusinessException addMetadata(String key, Object value) {
this.metadata.put(key, value);
return this;
}
public BaseErrorCode getErrorCode() { return errorCode; }
public Object[] getArgs() { return args; }
public String getTraceId() { return traceId; }
public Map<String, Object> getMetadata() { return metadata; }
public HttpStatus getHttpStatus() { return errorCode.getHttpStatus(); }
public String getCode() { return errorCode.getCode(); }
private static String formatMessage(String template, Object... args) {
return args.length == 0 ? template
: String.format(template.replace("{}", "%s"), args);
}
}5.4 领域异常
public class AccountException extends BusinessException {
private final String accountId;
public AccountException(BaseErrorCode errorCode, String accountId) {
super(errorCode, accountId);
this.accountId = accountId;
addMetadata("accountId", accountId);
}
public String getAccountId() { return accountId; }
}
public class InsufficientBalanceException extends AccountException {
private final BigDecimal availableBalance;
private final BigDecimal requestedAmount;
public InsufficientBalanceException(String accountId,
BigDecimal available, BigDecimal requested) {
super(AccountErrorCode.INSUFFICIENT_BALANCE, accountId, accountId, available, requested);
this.availableBalance = available;
this.requestedAmount = requested;
addMetadata("availableBalance", available)
.addMetadata("requestedAmount", requested);
}
}6. 统一错误响应格式
6.1 响应体
public class ApiResponse<T> {
private boolean success;
private String code;
private String message;
private T data;
private String traceId;
private long timestamp;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> r = new ApiResponse<>();
r.success = true; r.code = "SUCCESS"; r.message = "操作成功";
r.data = data; r.traceId = getCurrentTraceId(); r.timestamp = System.currentTimeMillis();
return r;
}
public static <T> ApiResponse<T> error(String code, String message) {
ApiResponse<T> r = new ApiResponse<>();
r.success = false; r.code = code; r.message = message;
r.traceId = getCurrentTraceId(); r.timestamp = System.currentTimeMillis();
return r;
}
}6.2 ResponseBodyAdvice 自动包装
@ControllerAdvice
public class ApiResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
if (returnType.getParameterType() == ApiResponse.class) return false;
return !returnType.hasMethodAnnotation(SkipResponseWrapper.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
return body == null ? null : ApiResponse.success(body);
}
}6.3 @RestControllerAdvice
@RestControllerAdvice = @ControllerAdvice + @ResponseBody,避免在每个方法重复加注解:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handle(BusinessException ex) {
return ApiResponse.error(ex.getCode(), ex.getMessage());
}
}7. 实战:金融交易系统全局异常处理
7.1 需求与方案
| 需求 | 方案 |
|---|---|
| 错误码国际化 | Spring MessageSource + Accept-Language |
| 交易流水号溯源 | UUID traceId + MDC 日志链路 |
| 敏感信息脱敏 | 全局分类脱敏,不暴露内部细节 |
| 幂等性保障 | 幂等 Token + 唯一索引 |
7.2 国际化配置
spring:
messages:
basename: i18n/messages
encoding: UTF-8
mvc:
problemdetails:
enabled: true
server:
error:
include-stacktrace: never
include-exception: false资源文件:messages.properties — ACCOUNT_1001=账户 {0} 余额不足:可用 {1},需要 {2};messages_en.properties — ACCOUNT_1001=Account {0} insufficient balance: available {1}, required {2}。
7.3 交易异常体系
public enum TradeErrorCode implements BaseErrorCode {
TRADE_TIMEOUT("TRADE_2001", HttpStatus.REQUEST_TIMEOUT, "交易超时"),
DUPLICATE_TRADE("TRADE_2002", HttpStatus.CONFLICT, "重复交易"),
TRADE_ROLLBACK("TRADE_2003", HttpStatus.INTERNAL_SERVER_ERROR, "交易回滚"),
TRADE_CHANNEL_UNAVAILABLE("TRADE_2006", HttpStatus.SERVICE_UNAVAILABLE, "通道不可用");
// constructor & getters omitted
}
public class TradeException extends BusinessException {
private final String tradeNo;
public TradeException(BaseErrorCode ec, String tradeNo) {
super(ec, tradeNo); this.tradeNo = tradeNo; addMetadata("tradeNo", tradeNo);
}
}
public class DuplicateTradeException extends TradeException {
private final String idempotentKey;
public DuplicateTradeException(String tradeNo, String idempotentKey) {
super(TradeErrorCode.DUPLICATE_TRADE, tradeNo, tradeNo);
this.idempotentKey = idempotentKey;
addMetadata("idempotentKey", idempotentKey);
}
}7.4 核心全局异常处理器
@RestControllerAdvice
@Slf4j
public class TradeGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Autowired private MessageSource messageSource;
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponseDTO> handleBusinessException(
BusinessException ex, WebRequest request) {
var body = ErrorResponseDTO.builder().code(ex.getCode())
.message(resolveMessage(ex.getErrorCode(), ex.getArgs(), request.getLocale()))
.traceId(ex.getTraceId())
.metadata(sanitizeMetadata(ex.getMetadata())).build();
log.info("[AUDIT] traceId={}, errorCode={}", ex.getTraceId(), ex.getCode());
return ResponseEntity.status(ex.getHttpStatus()).body(body);
}
@ExceptionHandler(InsufficientBalanceException.class)
public ResponseEntity<ErrorResponseDTO> handleInsufficientBalance(
InsufficientBalanceException ex, WebRequest request) {
return ResponseEntity.status(ex.getHttpStatus()).body(ErrorResponseDTO.builder()
.code(ex.getCode())
.message(resolveMessage(ex.getErrorCode(), ex.getArgs(), request.getLocale()))
.traceId(ex.getTraceId())
.metadata(Map.of("accountId", maskAccountId(ex.getAccountId()),
"availableBalance", "***.**",
"requestedAmount", ex.getRequestedAmount().toPlainString())).build());
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
var errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField,
fe -> fe.getDefaultMessage() == null ? "Invalid" : fe.getDefaultMessage()));
return ResponseEntity.badRequest().body(ErrorResponseDTO.builder()
.code("VALIDATION_FAILED").message("参数校验失败")
.traceId(generateTraceId()).metadata(Map.of("errors", errors)).build());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseDTO> handleUnknownException(
Exception ex, WebRequest request) {
String traceId = generateTraceId();
log.error("[{}] 未处理异常: ", traceId, ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponseDTO.builder()
.code("SYSTEM_ERROR")
.message(resolveMessage("SYSTEM_ERROR", null, request.getLocale()))
.traceId(traceId).build());
}
private String resolveMessage(BaseErrorCode ec, Object[] args, Locale locale) {
try { return messageSource.getMessage(ec.getCode(), args, ec.getDefaultMessage(), locale); }
catch (NoSuchMessageException e) { return ec.getDefaultMessage(); }
}
private String resolveMessage(String code, Object[] args, Locale locale) {
try { return messageSource.getMessage(code, args, code, locale); }
catch (NoSuchMessageException e) { return code; }
}
private String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
private Map<String, Object> sanitizeMetadata(Map<String, Object> metadata) {
if (metadata == null || metadata.isEmpty()) return Collections.emptyMap();
var m = new HashMap<>(metadata);
m.remove("internalErrorCode"); m.remove("dbQuery"); m.remove("sql");
return Collections.unmodifiableMap(m);
}
private String maskAccountId(String id) {
if (id == null || id.length() <= 4) return id;
return "****" + id.substring(id.length() - 4);
}
}
@Data @Builder @NoArgsConstructor @AllArgsConstructor
class ErrorResponseDTO {
@Builder.Default private boolean success = false;
private String code;
private String message;
private String traceId;
@Builder.Default private long timestamp = System.currentTimeMillis();
@JsonInclude(JsonInclude.Include.NON_NULL) private Map<String, Object> metadata;
}7.5 MDC 流水号溯源
通过 OncePerRequestFilter 在请求入口注入 traceId,实现全链路追踪:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isEmpty())
traceId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
MDC.put("traceId", traceId);
response.setHeader("X-Trace-Id", traceId);
try { chain.doFilter(request, response); }
finally { MDC.remove("traceId"); }
}
}7.6 敏感信息脱敏
public final class SensitiveDataMasker {
public static String maskPhone(String phone) {
return phone != null && phone.length() == 11
? phone.substring(0, 3) + "****" + phone.substring(7) : phone;
}
public static String maskIdCard(String idCard) {
return idCard != null && idCard.length() >= 10
? idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4) : idCard;
}
public static String maskBankCard(String cardNo) {
return cardNo != null && cardNo.length() >= 8
? "**** **** **** " + cardNo.substring(cardNo.length() - 4) : cardNo;
}
public static final Set<String> SENSITIVE_FIELDS = Set.of(
"password", "secret", "token", "authCode", "pin", "cvv", "certNo", "phone", "bankCard");
}7.7 幂等性与 Filter 层异常处理
幂等请求重复提交返回前次结果:
@ExceptionHandler(IdempotentException.class)
public ResponseEntity<?> handleIdempotent(IdempotentException ex) {
return ResponseEntity.ok()
.header("X-Idempotent-Replay", "true")
.header("X-Idempotent-Key", ex.getIdempotentKey())
.body(ApiResponse.success(ex.getPreviousResult()));
}@ControllerAdvice 无法捕获 Filter 层异常,需通过自定义 Filter 兜底捕获 BusinessException 和其他异常:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class ExceptionHandlerFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try { chain.doFilter(request, response); }
catch (BusinessException ex) {
writeError(response, ex.getHttpStatus().value(), ex.getCode(), ex.getMessage(), ex.getTraceId());
} catch (Exception ex) {
log.error("Filter 层异常: ", ex);
writeError(response, 500, "SYSTEM_ERROR", "系统繁忙", "unknown");
}
}
private void writeError(HttpServletResponse res, int status,
String code, String msg, String traceId) throws IOException {
res.setStatus(status);
res.setContentType(MediaType.APPLICATION_JSON_VALUE);
res.getWriter().write(new ObjectMapper().writeValueAsString(
ErrorResponseDTO.builder().code(code).message(msg).traceId(traceId).build()));
}
}8. 自测与常见问题
8.1 单元测试
@WebMvcTest(TradeController.class)
class GlobalExceptionHandlerTest {
@Autowired private MockMvc mockMvc;
@MockBean private TradeService tradeService;
@Test
void shouldHandleInsufficientBalanceWithI18n() throws Exception {
given(tradeService.transfer(any())).willThrow(
new InsufficientBalanceException("ACC-001", BigDecimal.TEN, BigDecimal.valueOf(5000)));
// 中文
mockMvc.perform(post("/api/v1/trade/transfer").contentType(MediaType.APPLICATION_JSON)
.content("{}").header("Accept-Language", "zh-CN"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("ACCOUNT_1001"))
.andExpect(jsonPath("$.traceId").isNotEmpty());
// 英文
mockMvc.perform(post("/api/v1/trade/transfer").contentType(MediaType.APPLICATION_JSON)
.content("{}").header("Accept-Language", "en-US"))
.andExpect(jsonPath("$.message")
.value("Account ACC-001 insufficient balance: available 10, required 5000"));
}
}8.2 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
@ExceptionHandler 不生效 | 未标注 @ControllerAdvice | 检查注解和扫描路径 |
| 异常未被捕获 | 从 Filter 抛出 | Filter 中 try-catch |
| 国际化不生效 | MessageSource 配置缺失 | 配置 spring.messages.basename |
| 堆栈泄露 | 未配置 include-stacktrace=never | 生产环境务必配置 |
9. 总结
| 主题 | 要点 |
|---|---|
| @ControllerAdvice | 全局切面 Bean,通过 ExceptionHandlerExceptionResolver 扫描注册 |
| 优先级 | 类级 > 全局级 > DefaultHandlerExceptionResolver > 自定义 |
| ResponseEntityExceptionHandler | 预定义 14 种标准异常处理器 |
| RFC 7807 | Spring Boot 3 通过 spring.mvc.problemdetails.enabled=true 启用 |
| 异常体系 | BusinessException → 领域异常分层设计 |
| 统一响应 | ApiResponse<T> + ResponseBodyAdvice 自动包装 |
| 金融实战 | 国际化(MessageSource)、溯源(MDC + traceId)、脱敏(SensitiveDataMasker) |
全局异常处理是 API 设计的一部分,良好的机制能显著提升系统的可观测性、可诊断性和用户体验,是生产级应用不可或缺的基础设施。