Java业务异常处理最佳实践
包结构
自定义业务异常类,用于抛出业务异常
import lombok.Getter;@Getter
public class BusinessException extends RuntimeException{/*** 错误码*/private final int code;public BusinessException(int code, String message) {super(message);this.code = code;}public BusinessException(ErrorCode errorCode) {super(errorCode.getMessage());this.code = errorCode.getCode();}public BusinessException(ErrorCode errorCode, String message) {super(message);this.code = errorCode.getCode();}
}
异常代码
import lombok.Getter;@Getter
public enum ErrorCode {SUCCESS(0, "ok"),PARAMS_ERROR(40000, "请求参数错误"),NOT_LOGIN_ERROR(40100, "未登录"),NO_AUTH_ERROR(40101, "无权限"),TOO_MANY_REQUEST(42900, "请求过于频繁"),NOT_FOUND_ERROR(40400, "请求数据不存在"),FORBIDDEN_ERROR(40300, "禁止访问"),SYSTEM_ERROR(50000, "系统内部异常"),OPERATION_ERROR(50001, "操作失败");/*** 状态码*/private final int code;/*** 信息*/private final String message;ErrorCode(int code, String message) {this.code = code;this.message = message;}
}
全局异常处理器
package com.yupi.yuaicodemother.exception;import cn.hutool.json.JSONUtil;
import com.yupi.yuaicodemother.common.BaseResponse;
import com.yupi.yuaicodemother.common.ResultUtils;
import com.yupi.yuaicodemother.exception.BusinessException;
import com.yupi.yuaicodemother.exception.ErrorCode;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.util.Map;@Hidden
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(BusinessException.class)public BaseResponse<?> businessExceptionHandler(BusinessException e) {log.error("BusinessException", e);// 尝试处理 SSE 请求if (handleSseError(e.getCode(), e.getMessage())) {return null;}// 对于普通请求,返回标准 JSON 响应return ResultUtils.error(e.getCode(), e.getMessage());}@ExceptionHandler(RuntimeException.class)public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {log.error("RuntimeException", e);// 尝试处理 SSE 请求if (handleSseError(ErrorCode.SYSTEM_ERROR.getCode(), "系统错误")) {return null;}return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");}/*** 处理SSE请求的错误响应** @param errorCode 错误码* @param errorMessage 错误信息* @return true表示是SSE请求并已处理,false表示不是SSE请求*/private boolean handleSseError(int errorCode, String errorMessage) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return false;}HttpServletRequest request = attributes.getRequest();HttpServletResponse response = attributes.getResponse();// 判断是否是SSE请求(通过Accept头或URL路径)String accept = request.getHeader("Accept");String uri = request.getRequestURI();if ((accept != null && accept.contains("text/event-stream")) ||uri.contains("/chat/gen/code")) {try {// 设置SSE响应头response.setContentType("text/event-stream");response.setCharacterEncoding("UTF-8");response.setHeader("Cache-Control", "no-cache");response.setHeader("Connection", "keep-alive");// 构造错误消息的SSE格式Map<String, Object> errorData = Map.of("error", true,"code", errorCode,"message", errorMessage);String errorJson = JSONUtil.toJsonStr(errorData);// 发送业务错误事件(避免与标准error事件冲突)String sseData = "event: business-error\ndata: " + errorJson + "\n\n";response.getWriter().write(sseData);response.getWriter().flush();// 发送结束事件response.getWriter().write("event: done\ndata: {}\n\n");response.getWriter().flush();// 表示已处理SSE请求return true;} catch (IOException ioException) {log.error("Failed to write SSE error response", ioException);// 即使写入失败,也表示这是SSE请求return true;}}return false;}
}
异常抛出工具类
package com.yupi.yuaicodemother.exception;public class ThrowUtils {/*** 条件成立则抛出异常** @param condition* @param runtimeException*/public static void throwIf(boolean condition, RuntimeException runtimeException) {if (condition) {throw runtimeException;}}/*** 条件成立则抛异常** @param condition 条件* @param errorCode 错误码*/public static void throwIf(boolean condition, ErrorCode errorCode) {throwIf(condition, new BusinessException(errorCode));}/*** 条件成立则抛异常** @param condition 条件* @param errorCode 错误码* @param message 错误信息*/public static void throwIf(boolean condition, ErrorCode errorCode, String message) {throwIf(condition, new BusinessException(errorCode, message));}
}
使用示例
@GetMapping(value = "/chat/gen/code", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@RateLimit(limitType = RateLimitType.USER, rate = 5, rateInterval = 60, message = "AI 对话请求过于频繁,请稍后再试")
public Flux<ServerSentEvent<String>> chatToGenCode(@RequestParam Long appId,@RequestParam String message,HttpServletRequest request) {// 参数校验ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 id 错误");ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "提示词不能为空");// 获取当前登录用户User loginUser = userService.getLoginUser(request);// 调用服务生成代码(SSE 流式返回)Flux<String> contentFlux = appService.chatToGenCode(appId, message, loginUser);return contentFlux.map(chunk -> {Map<String, String> wrapper = Map.of("d", chunk);String jsonData = JSONUtil.toJsonStr(wrapper);return ServerSentEvent.<String>builder().data(jsonData).build();}).concatWith(Mono.just(// 发送结束事件ServerSentEvent.<String>builder().event("done").data("").build()));
}