来源:JAVA日知录 大家好,中何我是飘渺! 今天带来DDD系列的第七篇,欢迎持续关注! 在领域驱动设计(DDD)中,接口层主要负责处理与外部系统的优雅异常交互,包括接收用户或外部系统的全局请求,调用应用层服务处理请求,格式以及将处理结果返回给请求方。处理 我发现一些代码中,系统接口的中何返回值类型众多,有的优雅异常直接返回数据传输对象(DTO),甚至直接返回数据对象(DO),全局还有的格式返回Result对象。在DailyMart项目中,处理为了简化客户端的系统处理流程,我们决定在接口层采用统一的中何返回格式——Result对象。 为了实现统一返回格式,优雅异常我们在DailyMart项目中构建了一个Result对象,全局代码如下: ) { ; String code; String message; T data; timestamp; } 为了便于创建Result对象,我们构建了一个辅助类ResultHelper: 4j { { Result .setCode(SUCCESS_CODE) .setData(data) .setTimestamp(System.currentTimeMillis()); } { Result .setCode(ErrorCode.SERVICE_ERROR.getCode()) .setMessage(message) .setTimestamp(System.currentTimeMillis()); } ... } 以DailyMart系统的注册接口为例,定义了Result对象后,我们可以在接口层这样优化代码: ) { { ResultHelper.success(customerService.register(customerDTO)); (Exception e){ ResultHelper.fail(e.getMessage()); } } 为了避免每个接口都这样写,我们可以利用SpringBoot的全局异常处理器来处理,这将在下一节讨论。 现在,当访问注册接口时,成功会返回如下响应: { , , : { , , , }, } 失败时会返回如下响应: { , , , } 这样,我们成功地实现了接口层的返回格式的统一。 在DailyMart的代码实现中,我们通常会在遇到问题时抛出RuntimeException。例如,在用户登录时,如果用户不存在,我们会抛出一个RuntimeException: { CustomerUser actualUser = customerUserRepository.findByUserName(usernamePasswordLoginDTO.getUsername()); ){ ); } actualUser; } 然而,在构建大型系统时,通常建议使用自定义异常来替代RuntimeException。自定义异常可以提供更精细和具有针对性的错误信息,有助于区分系统中的不同类型的错误。使用自定义异常不仅可以提高代码的可读性,因为它们的名称和内容可以直接反映出问题的性质,而且还可以包含更多的信息,比如错误码或其他相关的上下文数据。 在开发过程中,错误码的使用是网站模板提升异常处理可读性和效率的有效手段。根据《阿里巴巴开发规范-黄山版》,错误码的制定和使用应遵循一定的原则,以便实现快速溯源和标准化沟通。 错误码通常是一个包含5个字符的字符串,它分为两部分:错误来源标识(1个字符)和错误编号(4个数字)。错误来源标识可以是A、B或C: 错误编号是一个在0001到9999之间的四位数,用于进一步细化错误的类别。 错误码的主要目的是: 在 DailyMart 项目中,我们依据阿里巴巴的开发规范定义了一个错误码的枚举类。这个枚举类包含一系列预定义的错误码及其对应的错误信息。 ErrorCode { ), ), ), ), ), ), ), ), ), ); / * 错误码 String code; / * 错误信息 String message; ... } 每个错误码包含两个部分:错误码和错误信息,分别由code和message字段表示。 为了在 DailyMart 中更有效地处理错误,我们创建了三种自定义异常类:ClientException(客户端异常)、BusinessException(业务逻辑异常)和RemoteException(第三方服务异常)。这些异常类都继承自AbstractException,这是一个抽象的基类。 AbstractException基类包含错误码和错误信息,同时它继承自RuntimeException,这意味着它是一个非受检异常。 { String code; String message; { (message,throwable); .code = errorCode.getCode(); .message = Optional.ofNullable(message).orElse(errorCode.getMessage()); } } 接下来,我们通过继承AbstractException基类来定义具体的自定义异常类。 { { ); } { ); } } 以上是ClientException的示例。我们可以为BusinessException和RemoteException采用类似的方式定义。 现在,我们已经创建了自定义异常类,接下来让我们看看如何在 DailyMart 中使用它们来替代标准的RuntimeException。 例如,在验证用户登录时,如果用户不存在,我们不再抛出普通的RuntimeException,而是抛出我们的自定义ClientException。 { CustomerUser actualUser = customerUserRepository.findByUserName(usernamePasswordLoginDTO.getUsername()); ){ ); } actualUser; } 对于在多个地方常用的异常,我们甚至可以创建更具体的自定义异常类。例如,对于“用户不存在”的场景,我们可以创建一个UserNotFoundException类。 { / * Constructs a { (ErrorCode.USER_NOT_FOUND); } } 在处理异常时,频繁使用try...catch块可能会使代码变得混乱。为了简化异常处理并确保一致的响应格式,我们可以利用 SpringBoot 的全局异常处理功能。 SpringBoot 提供了一个特殊的注解@RestControllerAdvice,允许我们创建全局异常处理类。在这个类中,我们可以定义处理各种类型异常的方法。 在 DailyMart 中,我们创建一个GlobalExceptionHandler类,并使用@RestControllerAdvice注解。我们主要处理三类异常: 下面是GlobalExceptionHandler的实现: 4j { ) { BindingResult bindingResult = ex.getBindingResult(); FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors()); String exceptionStr = Optional.ofNullable(firstFieldError) .map(FieldError::getDefaultMessage) .orElse(StrUtil.EMPTY); , request.getMethod(), getUrl(request), exceptionStr); ResultHelper.fail(ErrorCode.CLIENT_ERROR, exceptionStr); } }) { String requestURL = getUrl(request); , request.getMethod(), requestURL, ex.toString()); ResultHelper.fail(ex); } ) { , request.getMethod(), getUrl(request), throwable); ResultHelper.fail(); } } } 在启用全局异常处理功能后,DailyMart的用户模块不再需要在接口层手动使用try...catch来处理异常。倘若出现其他异常,它们也会被defaultErrorHandler拦截,从而确保DailyMart能够一致地实施统一的返回格式。 经优化后,接口层代码变得更为简洁: ) { ResultHelper.success(customerService.register(customerDTO)); } ) { UserLoginDTO loginDTO = LoginDTOFactory.getLoginDTO(parameters); ResultHelper.success(customerService.login(loginDTO)); } 注意到目前所有的接口都需要通过手动调用 ResultHelper.success() 来对结果进行包装。这些重复的代码段可以优化吗? 答案是肯定的。在SpringBoot中,我们可以利用 ResponseBodyAdvice 来自动包装响应体。 提示: ResponseBodyAdvice 可以拦截控制器(Controller)方法的返回值,允许我们统一处理返回值或响应体。这对于统一返回格式、加密、签名等场景非常有用。 在 DailyMart 中,我们可以创建一个实现 ResponseBodyAdvice 接口的类,来自动包装响应体。下面是示例代码: 4j { ObjectMapper objectMapper; { ; } { String){ objectMapper.writeValueAsString(ResultHelper.success(body)); } Result ){ body; } ResultHelper.success(body); } } 经过这样的优化,我们的控制器层代码可以直接简写如下: ) { customerService.register(customerDTO); } ) { UserLoginDTO loginDTO = LoginDTOFactory.getLoginDTO(parameters); customerService.login(loginDTO); } 考虑到 DailyMart 项目包含多个服务,并且在其他服务中也需要全局异常处理和响应体自动包装的功能,我们可以将这些功能封装成一个 Spring Boot Starter。这样,任何需要这些功能的模块只需引入该 Starter 即可。 { / * 自定义全局异常处理器 ) { GlobalExceptionHandler(); } / * 接口自动包装 ) { GlobalResponseBodyAdvice(); } } 我们还需要在 resources/META-INF/spring 目录下创建一个名为 org.springframework.boot.autoconfigure.AutoConfiguration.imports 的文件,并在此文件中声明我们的自动配置类,以便 Spring Boot 在启动时能够找到并加载它。 com.jianzh5.dailymart.springboot.starter.web.config.WebAutoConfiguration 这样,当其他服务需要使用全局异常处理和自动响应体包装时,只需在它们的 pom.xml 文件中添加对这个 Starter 的依赖即可。 本文主要讨论了SpringBoot项目中响应体自动包装和全局异常处理的优化方法。通过使用ResponseBodyAdvice接口,我们能够自动化响应体的包装过程,消除了冗余的代码。此外,我们还探讨了如何创建一个Spring Boot Starter,以将全局异常处理和自动包装类作为插件,从而方便地在多个服务中重用这些功能。这些优化措施有助于简化代码,提高可维护性和项目效率。1. 统一返回格式
1.1 构建Result对象
1.2 优化DailyMart中的接口
1.3 优化后的云南idc服务商结果
2. 异常控制
2.1 错误码的概念与应用
2.2 在 DailyMart 中定义错误码
2.3 自定义异常的创建和使用
自定义异常的基类
2.3 在DailyMart中实施自定义异常
UsernameNotFoundException
3. 全局异常处理
3.1 使用@RestControllerAdvice进行全局异常处理
4. 自动包装类
5. 定义starter
小结