Spring Boot自定义异常读取YML属性初始化问题求助
说明:部分代码为西班牙语。
我为Spring Boot应用构建了一套自定义异常模型,抛出的每个自定义异常都会生成一个名为ErrorModel的个性化响应对象,可通过@RestControllerAdvice配合@ExceptionHandler捕获异常并返回对应的errorModel。每个异常生成的ErrorModel仅在code(对应codigo)和description(对应descripcion)字段上存在差异。
现有代码结构
全局异常处理器
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ClienteSinMorasException.class) @ResponseStatus(HttpStatus.NOT_FOUND) @ResponseBody public ErrorModel manejoDeClienteSinMoras(ClienteSinMorasException ex) { return ex.getErrorModel(); } }
ErrorModel定义
@Getter @AllArgsConstructor public class ErrorModel { private final String codigo; private final String descripcion; private final ArrayList<HashMap<String, String>> errores; //无需关注此字段 }
最初的异常实现(可正常运行)
抽象异常类:
public abstract class IErrorException extends RuntimeException { @Getter private final ErrorModel errorModel; public abstract String getCodigo(); public abstract String getDescripcion(); public IErrorException() { super(); this.errorModel = new ErrorModel(this.getCodigo(), this.getDescripcion(), null); } public IErrorException(ArrayList<HashMap<String, String>> errores) { super(); this.errorModel = new ErrorModel(this.getCodigo(), this.getDescripcion(), errores); } public IErrorException(String errorKey, String errorValue) { super(); ArrayList<HashMap<String, String>> errores = new ArrayList<>(); errores.add(new HashMap<>(Map.of(errorKey, errorValue))); this.errorModel = new ErrorModel(this.getCodigo(), this.getDescripcion(), errores); } }
子类异常:
// 其中一个异常类 @Getter public class ClienteSinMorasException extends IErrorException { private final String codigo = "code"; private final String descripcion = "desc"; public ClienteSinMorasException() { super(); } public ClienteSinMorasException(ArrayList<HashMap<String, String>> errores) { super(errores); } public ClienteSinMorasException(String errorKey, String errorValue) { super(errorKey, errorValue); } }
问题场景与原因
需求变更要求从.yml配置文件读取code和description的值,支持运行时修改无需重启应用。修改后子类异常代码如下,但原有逻辑失效:
// 修改后的异常类 @Getter public class ClienteSinMorasException extends IErrorException { private final String codigo = Yml.get("cod"); private final String descripcion = Yml.get("desc"); public ClienteSinMorasException() { super(); } public ClienteSinMorasException(ArrayList<HashMap<String, String>> errores) { super(errores); } public ClienteSinMorasException(String errorKey, String errorValue) { super(errorKey, errorValue); } }
问题核心:原逻辑中,子类final常量字段在父类构造器调用getCodigo()时已完成初始化;但改为Yml.get()读取后,子类字段要等父类构造器执行完毕才会初始化,导致ErrorModel中的codigo和descripcion为null。同时Java规定super()必须是构造器第一语句,无法提前初始化子类变量。
解决方案
方案1:构造器传参规避初始化顺序问题
修改抽象类构造器,要求子类直接传入从YML读取的codigo和descripcion值,跳过子类字段初始化的依赖。
修改后的抽象异常类
public abstract class IErrorException extends RuntimeException { @Getter private final ErrorModel errorModel; public IErrorException(String codigo, String descripcion) { super(); this.errorModel = new ErrorModel(codigo, descripcion, null); } public IErrorException(String codigo, String descripcion, ArrayList<HashMap<String, String>> errores) { super(); this.errorModel = new ErrorModel(codigo, descripcion, errores); } public IErrorException(String codigo, String descripcion, String errorKey, String errorValue) { super(); ArrayList<HashMap<String, String>> errores = new ArrayList<>(); errores.add(new HashMap<>(Map.of(errorKey, errorValue))); this.errorModel = new ErrorModel(codigo, descripcion, errores); } }
修改后的子类异常
@Getter public class ClienteSinMorasException extends IErrorException { private final String codigo = Yml.get("cod"); private final String descripcion = Yml.get("desc"); public ClienteSinMorasException() { super(Yml.get("cod"), Yml.get("desc")); } public ClienteSinMorasException(ArrayList<HashMap<String, String>> errores) { super(Yml.get("cod"), Yml.get("desc"), errores); } public ClienteSinMorasException(String errorKey, String errorValue) { super(Yml.get("cod"), Yml.get("desc"), errorKey, errorValue); } }
优点:逻辑简单直接,彻底解决初始化顺序问题;缺点:每个子类构造器需重复写YML读取代码,配置键变更时需修改所有子类。
方案2:懒加载ErrorModel延迟初始化
将ErrorModel的创建延迟到第一次调用getErrorModel()时,此时子类字段已完成初始化。
修改后的抽象异常类(手动实现懒加载)
public abstract class IErrorException extends RuntimeException { private ErrorModel errorModel; private final ArrayList<HashMap<String, String>> initErrores; private final String initErrorKey; private final String initErrorValue; public abstract String getCodigo(); public abstract String getDescripcion(); public IErrorException() { super(); this.initErrores = null; this.initErrorKey = null; this.initErrorValue = null; } public IErrorException(ArrayList<HashMap<String, String>> errores) { super(); this.initErrores = errores; this.initErrorKey = null; this.initErrorValue = null; } public IErrorException(String errorKey, String errorValue) { super(); this.initErrores = null; this.initErrorKey = errorKey; this.initErrorValue = errorValue; } public ErrorModel getErrorModel() { if (errorModel == null) { ArrayList<HashMap<String, String>> actualErrores = initErrores; if (initErrorKey != null && initErrorValue != null) { actualErrores = new ArrayList<>(); actualErrores.add(new HashMap<>(Map.of(initErrorKey, initErrorValue))); } errorModel = new ErrorModel(getCodigo(), getDescripcion(), actualErrores); } return errorModel; } }
子类异常保持修改后的代码不变
@Getter public class ClienteSinMorasException extends IErrorException { private final String codigo = Yml.get("cod"); private final String descripcion = Yml.get("desc"); public ClienteSinMorasException() { super(); } public ClienteSinMorasException(ArrayList<HashMap<String, String>> errores) { super(errores); } public ClienteSinMorasException(String errorKey, String errorValue) { super(errorKey, errorValue); } }
优点:子类无需修改构造器逻辑,保持原有结构;缺点:需注意线程安全(异常对象通常为线程局部,风险极低)。
方案3:Spring依赖注入管理配置(推荐)
利用Spring的配置注入能力,将配置封装为Bean,避免手动读取YML的初始化顺序问题,还支持运行时刷新配置。
首先创建配置类:
@Component @RefreshScope // 支持运行时刷新配置 public class ErrorConfig { @Value("${error.clienteSinMoras.cod}") private String clienteSinMorasCod; @Value("${error.clienteSinMoras.desc}") private String clienteSinMorasDesc; private static ErrorConfig instance; @PostConstruct public void init() { instance = this; } public static String getClienteSinMorasCod() { return instance.clienteSinMorasCod; } public static String getClienteSinMorasDesc() { return instance.clienteSinMorasDesc; } }
然后修改子类异常,并结合方案2的懒加载逻辑:
@Getter public class ClienteSinMorasException extends IErrorException { private final String codigo = ErrorConfig.getClienteSinMorasCod(); private final String descripcion = ErrorConfig.getClienteSinMorasDesc(); public ClienteSinMorasException() { super(); } public ClienteSinMorasException(ArrayList<HashMap<String, String>> errores) { super(errores); } public ClienteSinMorasException(String errorKey, String errorValue) { super(errorKey, errorValue); } }
优点:符合Spring最佳实践,配置管理规范,支持运行时刷新;缺点:需依赖Spring上下文,增加配置类代码。
内容的提问来源于stack exchange,提问作者Gonzalo Martinez

