在范畴驱动设计(DDD)中,接口层首要负责处理与外部体系的交互,包括接收用户或外部体系的请求,调用应用层服务处理请求,以及将处理成果回来给请求方。

我发现一些代码中,接口的回来值类型很多,有的直接回来数据传输目标(DTO),乃至直接回来数据目标(DO),还有的回来Result目标。在DailyMart项目中,为了简化客户端的处理流程,咱们决议在接口层选用一致的回来格局——Result目标。

1. 一致回来格局

1.1 构建Result目标

为了完成一致回来格局,咱们在DailyMart项目中构建了一个Result目标,代码如下:

@Data
@Accessors(chain = true)
public class Result<T> {
    public static final String SUCCESS_CODE = "OK";
    private String code;
    private String message;
    private T data;
    private long timestamp;
}

为了便于创立Result目标,咱们构建了一个辅助类ResultHelper:

@Slf4j
public class ResultHelper {
    public static <T> Result<T> success(T data) {
        return new Result<T>()
                .setCode(SUCCESS_CODE)
                .setData(data)
                .setTimestamp(System.currentTimeMillis());
    }
    public static <T> Result<T> fail(String message) {
        return new Result<T>()
                .setCode(ErrorCode.SERVICE_ERROR.getCode())
                .setMessage(message)
                .setTimestamp(System.currentTimeMillis());
    }
...
}

1.2 优化DailyMart中的接口

以DailyMart体系的注册接口为例,界说了Result目标后,咱们可以在接口层这样优化代码:

@PostMapping("/api/customer/register")
public Result<UserRegistrationDTO> register(@RequestBody @Valid UserRegistrationDTO customerDTO){
	try {
		return ResultHelper.success(customerService.register(customerDTO));
	}catch (Exception e){
		return ResultHelper.fail(e.getMessage());
	}
}

为了避免每个接口都这样写,咱们可以运用SpringBoot的大局反常处理器来处理,这将鄙人一节评论。

1.3 优化后的成果

现在,当拜访注册接口时,成功会回来如下呼应:

{
    "code": "OK",
    "message": null,
    "data": {
        "userName": "jianzh1",
        "password": null,
        "email": "jianzh5@162.com",
        "phone": "18811117882"
    },
    "timestamp": 1687338445851
}

失利时会回来如下呼应:

{
    "code": "B0001",
    "message": "用户已存在",
    "data": null,
    "timestamp": 1687338319457
}

这样,咱们成功地完成了接口层的回来格局的一致。

2. 反常操控

在DailyMart的代码完成中,咱们一般会在遇到问题时抛出RuntimeException。例如,在用户登录时,假如用户不存在,咱们会抛出一个RuntimeException:

@Override
protected CustomerUser authenticate(UserLoginDTO loginDTO) {
	CustomerUser actualUser = customerUserRepository.findByUserName(usernamePasswordLoginDTO.getUsername());
	if(actualUser == null){
		throw new RuntimeException("用户不存在");
	}
	return actualUser;
}

然而,在构建大型体系时,一般建议运用自界说反常来代替RuntimeException。自界说反常可以供给更精细和具有针对性的过错信息,有助于区别体系中的不同类型的过错。运用自界说反常不仅可以提高代码的可读性,由于它们的称号和内容可以直接反映出问题的性质,并且还可以包括更多的信息,比如过错码或其他相关的上下文数据。

2.1 过错码的概念与应用

在开发过程中,过错码的运用是提升反常处理可读性和功率的有用手段。依据《阿里巴巴开发标准-黄山版》,过错码的拟定和运用应遵循一定的准则,以便完成快速溯源和标准化沟通。

过错码的组成

过错码一般是一个包括5个字符的字符串,它分为两部分:过错来历标识(1个字符)和过错编号(4个数字)。过错来历标识可以是ABC

  • A 表明过错源于用户,例如参数过错、版本过低或付出超时。
  • B 表明过错源于当时体系,一般是由于事务逻辑过错或程序健壮性不足。
  • C 表明过错源于第三方服务,例如 CDN 服务故障或消息投递超时。

过错编号是一个在0001到9999之间的四位数,用于进一步细化过错的类别。

过错码的意图

过错码的首要意图是:

  • 快速指示过错来历,帮助开发者迅速判别问题所在。
  • 明晰地对过错进行分类和标识。
  • 有助于团队成员快速达到对过错原因的共识。

2.2 在 DailyMart 中界说过错码

在 DailyMart 项目中,咱们依据阿里巴巴的开发标准界说了一个过错码的枚举类。这个枚举类包括一系列预界说的过错码及其对应的过错信息。

public enum ErrorCode {
    OK("00000","操作已成功"),
    CLIENT_ERROR("A0001", "客户端过错"),
    USER_NOT_FOUND("A0010", "用户不存在"),
    USER_ALREADY_EXISTS("A0011", "用户已存在"),
    USERNAME_PASSWORD_INCORRECT("A0012", "用户名或暗码过错"),
    VERIFICATION_CODE_EXPIRED("A0013", "验证码已过期"),
    BAD_CREDENTIALS_EXPIRED("A0014", "用户认证反常"),
    SERVICE_ERROR("B0001", "体系内部过错"),
    SERVICE_TIMEOUT_ERROR("B0010", "体系执行超时"),
    REMOTE_ERROR("C0001", "第三方服务过错");
    /**
     * 过错码
     */
    private final String code;
    /**
     * 过错信息
     */
    private final String message;
	...
}

每个过错码包括两个部分:过错码和过错信息,分别由codemessage字段表明。

2.3 自界说反常的创立和运用

为了在 DailyMart 中更有用地处理过错,咱们创立了三种自界说反常类:ClientException(客户端反常)、BusinessException(事务逻辑反常)和RemoteException(第三方服务反常)。这些反常类都承继自AbstractException,这是一个笼统的基类。

DDD系列第7篇:统一接口层返回格式以及全局异常处理

自界说反常的基类

AbstractException基类包括过错码和过错信息,一起它承继自RuntimeException,这意味着它是一个非受检反常。

@Getter
public abstract class AbstractException extends RuntimeException{
    private final String code;
    private final String message;
    public AbstractException(ErrorCode errorCode,String message,Throwable throwable){
        super(message,throwable);
        this.code = errorCode.getCode();
        this.message = Optional.ofNullable(message).orElse(errorCode.getMessage());
    }
}

界说详细的自界说反常类

接下来,咱们经过承继AbstractException基类来界说详细的自界说反常类。

public class ClientException extends AbstractException{
    public ClientException(){
        this(ErrorCode.CLIENT_ERROR,null,null);
    }
    public ClientException(String message){
        this(ErrorCode.CLIENT_ERROR,message,null);
    }
	// ... 其他构造办法 ...
}

以上是ClientException的示例。咱们可以为BusinessExceptionRemoteException选用类似的方式界说。

2.3 在DailyMart中实施自界说反常

现在,咱们现已创立了自界说反常类,接下来让咱们看看怎么在 DailyMart 中运用它们来代替标准的RuntimeException

例如,在验证用户登录时,假如用户不存在,咱们不再抛出普通的RuntimeException,而是抛出咱们的自界说ClientException

@Override
protected CustomerUser authenticate(UserLoginDTO loginDTO) {
	CustomerUser actualUser = customerUserRepository.findByUserName(usernamePasswordLoginDTO.getUsername());
	if(actualUser == null){
		throw new ClientException(ErrorCode.USER_NOT_FOUND,"用户不存在");
	}
	return actualUser;
}

关于在多个地方常用的反常,咱们乃至可以创立更详细的自界说反常类。例如,关于“用户不存在”的场景,咱们可以创立一个UserNotFoundException类。

public class UserNotFoundException extends ClientException{
    /**
     * Constructs a <code>UsernameNotFoundException</code>
     */
    public UserNotFoundException(){
        super(ErrorCode.USER_NOT_FOUND);
    }
}

DDD系列第7篇:统一接口层返回格式以及全局异常处理

3. 大局反常处理

在处理反常时,频频运用try...catch块可能会使代码变得混乱。为了简化反常处理并确保一致的呼应格局,咱们可以运用 SpringBoot 的大局反常处理功用。

3.1 运用@RestControllerAdvice进行大局反常处理

SpringBoot 供给了一个特别的注解@RestControllerAdvice,答应咱们创立大局反常处理类。在这个类中,咱们可以界说处理各种类型反常的办法。

在 DailyMart 中,咱们创立一个GlobalExceptionHandler类,并运用@RestControllerAdvice注解。咱们首要处理三类反常:

  • MethodArgumentNotValidException:处理参数验证反常,并供给明晰的过错信息。
  • AbstractException:处理咱们之前界说的自界说反常。
  • Throwable:作为最终的兜底,阻拦一切其他反常。

下面是GlobalExceptionHandler的完成:

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    // 处理参数验证反常
    @SneakyThrows
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result<Void> handleValidException(HttpServletRequest request, MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
        String exceptionStr = Optional.ofNullable(firstFieldError)
                .map(FieldError::getDefaultMessage)
                .orElse(StrUtil.EMPTY);
        log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
        return ResultHelper.fail(ErrorCode.CLIENT_ERROR, exceptionStr);
    }
    // 处理自界说反常
    @ExceptionHandler(value = {AbstractException.class})
    public Result<Void> handleAbstractException(HttpServletRequest request, AbstractException ex) {
        String requestURL = getUrl(request);
        log.error("[{}] {} [ex] {}", request.getMethod(), requestURL, ex.toString());
        return ResultHelper.fail(ex);
    }
    // 兜底处理
    @ExceptionHandler(value = Throwable.class)
    public Result<Void> handleThrowable(HttpServletRequest request, Throwable throwable) {
        log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
        return ResultHelper.fail();
    }
  }
}

在启用大局反常处理功用后,DailyMart的用户模块不再需求在接口层手动运用try...catch来处理反常。假使出现其他反常,它们也会被defaultErrorHandler阻拦,然后确保DailyMart可以一致地实施一致的回来格局。

经优化后,接口层代码变得更为简洁:

@PostMapping("/api/customer/register")
public Result<UserRegistrationDTO> register(@RequestBody @Valid UserRegistrationDTO customerDTO){  
	return ResultHelper.success(customerService.register(customerDTO));  
}
@PostMapping("/api/customer/login")
public Result<UserLoginRespDTO> login(@RequestBody Map<String, String> parameters){
	UserLoginDTO loginDTO = LoginDTOFactory.getLoginDTO(parameters);
	return ResultHelper.success(customerService.login(loginDTO));
}

4. 主动包装类

注意到目前一切的接口都需求经过手动调用 ResultHelper.success() 来对成果进行包装。这些重复的代码段可以优化吗?

答案是必定的。在SpringBoot中,咱们可以运用 ResponseBodyAdvice 来主动包装呼应体。

提示: ResponseBodyAdvice 可以阻拦操控器(Controller)办法的回来值,答应咱们一致处理回来值或呼应体。这关于一致回来格局、加密、签名等场景十分有用。

在 DailyMart 中,咱们可以创立一个完成 ResponseBodyAdvice 接口的类,来主动包装呼应体。下面是示例代码:

@Slf4j
@RestControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;
    // 此处可以经过判别决议哪些呼应需求包装
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if(body instanceof String){
            // 当呼应体是String类型时,运用ObjectMapper转化,由于Spring默认运用StringHttpMessageConverter处理字符串,不会将字符串识别为JSON
            return objectMapper.writeValueAsString(ResultHelper.success(body));
        }
        if(body instanceof Result<?>){
            // 现已包装过的成果无需再次包装
            return body;
        }
        // 对呼应体进行包装
        return ResultHelper.success(body);
    }
}

经过这样的优化,咱们的操控器层代码可以直接简写如下:

@PostMapping("/api/customer/register")
public UserRegistrationDTO register(@RequestBody @Valid UserRegistrationDTO customerDTO){
	return customerService.register(customerDTO);
}
@PostMapping("/api/customer/login")
public UserLoginRespDTO login(@RequestBody Map<String, String> parameters){
	UserLoginDTO loginDTO = LoginDTOFactory.getLoginDTO(parameters);
	return customerService.login(loginDTO);
}

5. 界说starter

考虑到 DailyMart 项目包括多个服务,并且在其他服务中也需求大局反常处理和呼应体主动包装的功用,咱们可以将这些功用封装成一个 Spring Boot Starter。这样,任何需求这些功用的模块只需引入该 Starter 即可。

@SpringBootConfiguration
@ConditionalOnWebApplication
public class WebAutoConfiguration {
    /**
     * 自界说大局反常处理器
     */
    @Bean
    @ConditionalOnMissingBean(GlobalExceptionHandler.class)
    public GlobalExceptionHandler dailyMartGlobalExceptionHandler() {
        return new GlobalExceptionHandler();
    }
    /**
     *  接口主动包装
     */
    @Bean
    @ConditionalOnMissingBean(GlobalResponseBodyAdvice.class)
    public GlobalResponseBodyAdvice dailyMartGlobalResponseBodyAdvice(){
        return new GlobalResponseBodyAdvice();
    }
}

咱们还需求在 resources/META-INF/spring 目录下创立一个名为 org.springframework.boot.autoconfigure.AutoConfiguration.imports 的文件,并在此文件中声明咱们的主动配置类,以便 Spring Boot 在启动时可以找到并加载它。

com.jianzh5.dailymart.springboot.starter.web.config.WebAutoConfiguration

DDD系列第7篇:统一接口层返回格式以及全局异常处理

这样,当其他服务需求运用大局反常处理和主动呼应体包装时,只需在它们的 pom.xml 文件中添加对这个 Starter 的依靠即可。

小结

本文首要评论了SpringBoot项目中呼应体主动包装和大局反常处理的优化办法。经过运用ResponseBodyAdvice接口,咱们可以主动化呼应体的包装过程,消除了冗余的代码。此外,咱们还讨论了怎么创立一个Spring Boot Starter,以将大局反常处理和主动包装类作为插件,然后方便地在多个服务中重用这些功用。这些优化措施有助于简化代码,提高可维护性和项目功率。

DDD&微服务系列源码现已上传至GitHub,假如需求获取源码地址,请关注公号 java日知录 并回复关键字 DDD 即可。