看看这些设计能不能套用回你代码?

2023-05-11 00:00:00 响应 接口 枚举 自定义 状态

大家好,我3y啊。austin已经逐渐完善和成熟了,今天主要想跟大家聊聊austin-web这个模块层面上的东西。

还记得在初学时,从Servlet开始就有了web界面,也是那时候开始,[MVC架构]这个词就被我记住了。到后来学习使用各种的框架Strtus2/Hibernate/Spring/SpringMVC/Mybatis/SpringBoot都离不开dao/service/controller这些包。

因为学习这些框架,基本都要去模拟写接口,学多了,渐渐就理解了,原来网上说的[接口]就是这么一回事啊。

后来学着发现,在接口层面上很多地方都是可以做[统一处理]的,这样一来程序设计代码结构会更优雅些。我的工作经历以来,写纯web接口比较少,austin-web模块的的[统一处理]基本都来源各大开发的pull request,这个过程中我也学到了不少。

今天给大家来盘点下,看看这些设计能不能套用回你代码中!

统一接口返回值

来源:https://gitee.com/zhongfucheng/austin/pulls/2

我们的接口返回值,好是要有统一的数据结构,这样前端跟我们的交互就方便很多。这个pull request给我提供了全局的返回类,并定义了常用的枚举类。

@Getter
@ToString(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
public final class BasicResultVO<T{

    /**
     * 响应状态
     */

    private String status;

    /**
     * 响应编码
     */

    private String msg;

    /**
     * 返回数据
     */

    private T data;

    public BasicResultVO(String status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    public BasicResultVO(RespStatusEnum status) {
        this(status, null);
    }

    public BasicResultVO(RespStatusEnum status, T data) {
        this(status, status.getMsg(), data);
    }

    public BasicResultVO(RespStatusEnum status, String msg, T data) {
        this.status = status.getCode();
        this.msg = msg;
        this.data = data;
    }

    /**
     * @return 默认成功响应
     */

    public static BasicResultVO<Void> success() {
        return new BasicResultVO<>(RespStatusEnum.SUCCESS);
    }

    /**
     * 自定义信息的成功响应
     * <p>通常用作插入成功等并显示具体操作通知如: return BasicResultVO.success("发送信息成功")</p>
     *
     * @param msg 信息
     * @return 自定义信息的成功响应
     */

    public static <T> BasicResultVO<T> success(String msg) {
        return new BasicResultVO<>(RespStatusEnum.SUCCESS, msg, null);
    }

    /**
     * 带数据的成功响应
     *
     * @param data 数据
     * @return 带数据的成功响应
     */

    public static <T> BasicResultVO<T> success(T data) {
        return new BasicResultVO<>(RespStatusEnum.SUCCESS, data);
    }

    /**
     * @return 默认失败响应
     */

    public static <T> BasicResultVO<T> fail() {
        return new BasicResultVO<>(
                RespStatusEnum.FAIL,
                RespStatusEnum.FAIL.getMsg(),
                null
        );
    }

    /**
     * 自定义错误信息的失败响应
     *
     * @param msg 错误信息
     * @return 自定义错误信息的失败响应
     */

    public static <T> BasicResultVO<T> fail(String msg) {
        return fail(RespStatusEnum.FAIL, msg);
    }

    /**
     * 自定义状态的失败响应
     *
     * @param status 状态
     * @return 自定义状态的失败响应
     */

    public static <T> BasicResultVO<T> fail(RespStatusEnum status) {
        return fail(status, status.getMsg());
    }

    /**
     * 自定义状态和信息的失败响应
     *
     * @param status 状态
     * @param msg    信息
     * @return 自定义状态和信息的失败响应
     */

    public static <T> BasicResultVO<T> fail(RespStatusEnum status, String msg) {
        return new BasicResultVO<>(status, msg, null);
    }

}

/**
 * 全局响应状态枚举
 *
 * @author zzb
 * @since 2021.11.17
 **/

@Getter
@ToString
@AllArgsConstructor
public enum RespStatusEnum {

    /**
     * 错误
     */

    ERROR_500("500""服务器未知错误"),
    ERROR_400("400""错误请求"),

    /**
     * OK:操作成功
     */

    SUCCESS("0""操作成功"),
    FAIL("-1""操作失败"),


    /**
     * 客户端
     */

    CLIENT_BAD_PARAMETERS("A0001""客户端参数错误"),
    TEMPLATE_NOT_FOUND("A0002""找不到模板或模板已被删除"),
    TOO_MANY_RECEIVER("A0003""传入的接收者大于100个"),
    DO_NOT_NEED_LOGIN("A0004""非测试环境,无须登录"),
    NO_LOGIN("A0005""还未登录,请先登录"),

    /**
     * 系统
     */

    SERVICE_ERROR("B0001""服务执行异常"),
    RESOURCE_NOT_FOUND("B0404""资源不存在"),


    /**
     * pipeline
     */

    CONTEXT_IS_NULL("P0001""流程上下文为空"),
    BUSINESS_CODE_IS_NULL("P0002""业务代码为空"),
    PROCESS_TEMPLATE_IS_NULL("P0003""流程模板配置为空"),
    PROCESS_LIST_IS_NULL("P0004""业务处理器配置为空"),


    ;

    /**
     * 响应状态
     */

    private final String code;
    /**
     * 响应编码
     */

    private final String msg;
}

统一接口返回对象

在前面我们已经安排了统一接口返回BasicResultVO,接口代码里应该都要显式地返回BasicResultVO类。但是也有办法不用显式去做,就是依赖AOP

来源:https://gitee.com/zhongfucheng/austin/pulls/45/files

当类被修饰了@AustinResult注解,那这个类的方法都是返回BasicResultVO对象,不需要在方法定义下显式写了。

/**
 * @author kl
 * @version 1.0.0
 * @description 统一返回注解
 * @date 2023/2/9 19:00
 */

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AustinResult {
}


Advice拦截器:

/**
 * @author kl
 * @version 1.0.0
 * @description 统一返回结构
 * @date 2023/2/9 19:00
 */

@ControllerAdvice(basePackages = "com.java3y.austin.web.controller")
public class AustinResponseBodyAdvice implements ResponseBodyAdvice<Object{

    private static final String RETURN_CLASS = "BasicResultVO";

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return methodParameter.getContainingClass().isAnnotationPresent(AustinResult.class) || methodParameter.hasMethodAnnotation(AustinResult.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter methodParameter, MediaType mediaType, Class aClass,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse)
 
{
        if (Objects.nonNull(data) && Objects.nonNull(data.getClass())) {
            String simpleName = data.getClass().getSimpleName();
            if (RETURN_CLASS.equalsIgnoreCase(simpleName)) {
                return data;
            }
        }
        return BasicResultVO.success(data);
    }
}


统一入口打日志

我是很建议在接口或任务的入口打上日志,在austin里你会发现有很多这样的实践。因为我们查问题一般就是通过日志去看程序到底发生了什么,如果入口都没有日志,那我们可能会一口咬定:你TM就没调用啊。

austin-web会提供各种的接口给到前端去调用嘛,这块正常也是需要打日志的。与其在各个接口入口上打日志,不如用AOP啦。

来源:https://gitee.com/zhongfucheng/austin/pulls/45/files

只要我们的Controller带有@AustinAspect注解,就会在其方法上打印对应的日志。

/**
 * @author kl
 * @version 1.0.0
 * @description 接口切面注解
 * @date 2023/2/23 9:01
 */

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AustinAspect {

}

切面类的逻辑也不复杂,主要就是AOP的应用:

/**
 * @author kl
 * @version 1.0.0
 * @description 切面类
 * @date 2023/2/23 9:17
 */

@Slf4j
@Aspect
@Component
public class AustinAspect {

    @Autowired
    private HttpServletRequest request;

    /**
     * 同一个请求的KEY
     */

    private final String REQUEST_ID_KEY = "request_unique_id";

    /**
     * 只切AustinAspect注解
     */

    @Pointcut("@within(com.java3y.austin.web.annotation.AustinAspect) || @annotation(com.java3y.austin.web.annotation.AustinAspect)")
    public void executeService() {
    }

    /**
     * 前置通知,方法调用前被调用
     *
     * @param joinPoint
     */

    @Before("executeService()")
    public void doBeforeAdvice(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        this.printRequestLog(methodSignature, joinPoint.getArgs());
    }

    /**
     * 异常通知
     *
     * @param ex
     */

    @AfterThrowing(value = "executeService()", throwing = "ex")
    public void doAfterThrowingAdvice(Throwable ex) {
        printExceptionLog(ex);
    }

    /**
     * 打印请求日志
     *
     * @param methodSignature
     * @param argObs
     */

    public void printRequestLog(MethodSignature methodSignature, Object[] argObs) {
        RequestLogDTO logVo = new RequestLogDTO();
        //设置请求ID
        logVo.setId(IdUtil.fastUUID());
        request.setAttribute(REQUEST_ID_KEY, logVo.getId());
        logVo.setUri(request.getRequestURI());
        logVo.setMethod(request.getMethod());
        List<Object> args = Lists.newArrayList();
        //过滤掉一些不能转为json字符串的参数
        Arrays.stream(argObs).forEach(e -> {
            if (e instanceof MultipartFile || e instanceof HttpServletRequest
                    || e instanceof HttpServletResponse || e instanceof BindingResult) {
                return;
            }
            args.add(e);
        });
        logVo.setArgs(args.toArray());
        logVo.setProduct("austin");
        logVo.setPath(methodSignature.getDeclaringTypeName() + "." + methodSignature.getMethod().getName());
        logVo.setReferer(request.getHeader("referer"));
        logVo.setRemoteAddr(request.getRemoteAddr());
        logVo.setUserAgent(request.getHeader("user-agent"));
        log.info(JSON.toJSONString(logVo));
    }

    /**
     * 打印异常日志
     *
     * @param ex
     */

    public void printExceptionLog(Throwable ex) {
        JSONObject logVo = new JSONObject();
        logVo.put("id", request.getAttribute(REQUEST_ID_KEY));
        log.error(JSON.toJSONString(logVo), ex);
    }
}

统一枚举字段

austin的开发过程中,我定义了很多枚举。虽然不是特意的,但大多数枚举我的属性都是code+description。一般要在枚举类的代码里写GetEnumByCode()这样的方法

在前段时间接收了个pull request,写了个公共的枚举接口,然后做了个工具类去统一掉常见的枚举方法,这样就不用在枚举下写这种重复的方法了。

来源:https://github.com/ZhongFuCheng3y/austin/pull/33

/**
 * @author kyw7
 * 枚举接口
 */

interface PowerfulEnum {

    /**
     * 获取枚举的code
     *
     * @return
     */

    Integer getCode();

    /**
     * 获取枚举的描述
     *
     * @return
     */

    String getDescription();

}

枚举工具类:

/**
 * @author kyw7
 * 枚举工具类(获取枚举的描述、获取枚举的code、获取枚举的code列表)
 */

public class EnumUtil {

    private EnumUtil() {
    }

    public static <T extends PowerfulEnum> String getDescriptionByCode(Integer code, Class<TenumClass) {
        return Arrays.stream(enumClass.getEnumConstants())
                .filter(e -> Objects.equals(e.getCode(), code))
                .findFirst().map(PowerfulEnum::getDescription).orElse("");
    }

    public static <T extends PowerfulEnum> getEnumByCode(Integer code, Class<TenumClass) {
        return Arrays.stream(enumClass.getEnumConstants())
                .filter(e -> Objects.equals(e.getCode(), code))
                .findFirst().orElse(null);
    }

    public static <T extends PowerfulEnum> List<Integer> getCodeList(Class<TenumClass) {
        return Arrays.stream(enumClass.getEnumConstants())
                .map(PowerfulEnum::getCode)
                .collect(Collectors.toList());
    }
}

统一异常拦截

我自己写代码的风格,我是不愿意一直往外抛异常的。很多时候,就是得需要try catch不让中断请求的完成流程的。(比如我用redis来做缓存,如果redis挂了,不应该中断整个请求,把日志打下来,告警就可以了。)

我喜欢将异常都在控制在自己的手里(自己写try catch,自己中断流程),我不是[异常统一拦截]的拥簇者,但我不反对。

来源:https://gitee.com/zhongfucheng/austin/pulls/45/files

/**
 * @author kl
 * @version 1.0.0
 * @description 拦截异常统一返回
 * @date 2023/2/9 19:03
 */

@ControllerAdvice(basePackages = "com.java3y.austin.web.controller")
@ResponseBody
public class ExceptionHandlerAdvice {
    private static final Logger log = LoggerFactory.getLogger(ExceptionHandlerAdvice.class);

    public ExceptionHandlerAdvice() {
    }

    @ExceptionHandler({Exception.class})
    @ResponseStatus(HttpStatus.OK)
    public BasicResultVO exceptionResponse(Exception e) 
{
        BasicResultVO result = BasicResultVO.fail(RespStatusEnum.ERROR_500, "\r\n" + Throwables.getStackTrace(e) + "\r\n");
        log.error(Throwables.getStackTrace(e));
        return result;
    }

    @ExceptionHandler({CommonException.class})
    @ResponseStatus(HttpStatus.OK)
    public BasicResultVO commonResponse(CommonException ce) 
{
        log.error(Throwables.getStackTrace(ce));
        return new BasicResultVO(ce.getCode(), ce.getMessage(), ce.getRespStatusEnum());
    }
}

相关文章