架构优化之 springboot 优雅处理异常

说明

日常开发中少不了会抛出各种各样的业务异常,而在抛出异常之前又要写一些代码对日志进行处理,有没有一种更为优雅简洁并且能处理大部分应用场景的优化方式来解决这一问题呢,今天接着从json-script-rule上面扒下来一些优秀的代码稍微修改一下,采用一种特别的设计来优雅的解决这个问题,看代码

import com.xx.common.interfaces.IHttpResponse;

public class ServerException extends RuntimeException{

    /**
     * <p>其子类为返回给调用端的消息对象</>
     * */
    public IHttpResponse response;
    /**
     * <p>写入到本地日志的消息,如xxx:{}</>
     * */
    public String log;
    /**
     * <p>写入到本地日志的参数,如log.error("xxx:{}",params)</>
     * */
    public Object[] params;

    public ServerException() {super();}
    /**
     * <p>只支持后端做日志,调用端返回默认消息对象</>
     * */
    public ServerException(Throwable cause) {
        super(cause);
        this.log = cause.getMessage();
        this.params = new Object[0];
    }
    /**
     * <p>支持后端做日志并且支持打印log参数行为,调用端返回默认消息对象</>
     * <p>使用该方法时需注意该异常是否在其它地方被捕获,这样有可能因为处理该异常的地方没有处理参数,进而导致参数为空{}</>
     * @param params 参考{@link ServerException#params}说明
     * */
    public ServerException(String log,Object...params){
        super(log);
        this.log = log;
        this.params = params;
        this.response = CommonResponse.fail(log);
    }
    /**
     * <p>当抛出异常消息只有一个时默认调用端以及服务端均为此消息</>
     * */
    public ServerException(String log){
        this(log,new Object[0]);
        this.response = CommonResponse.fail(log);
    }
    /**
     * <p>支持后端做日志,同时支持自定义返回调用端日志对象</>
     * */
    public ServerException(IHttpResponse response, Throwable cause) {
        this(cause);
        this.response = response;
    }
    public ServerException(String message, Throwable cause) {
        this(CommonResponse.fail(message),cause);
    }
    public ServerException(Integer code,String message, Throwable cause) {
        this(CommonResponse.fail(code,message),cause);
    }
    /**
     * <p>只支持调用端做日志,后端日志默认使用调用端日志</>
     * */
    public ServerException(IHttpResponse response) {
        this(response, response.getMessage());
    }
    /**
     * <p>支持后端做日志并且支持打印log参数行为,同时支持自定义返回调用端日志对象</>
     * <p>使用该方法时需注意该异常是否在其它地方被捕获,这样有可能因为处理该异常的地方没有处理参数,进而导致参数为空{}</>
     * @param params 参考{@link ServerException#params}说明
     * */
    public ServerException(IHttpResponse response, String log, Object...params) {
        this(log,params);
        this.response = response;
    }

    public ServerException(Integer code, String message, String log, Object...params){
        this(CommonResponse.fail(code,message),log,params);
    }

    public ServerException(String message, String log, Object...params){
        this(CommonResponse.fail(message),log,params);
    }
}

上面是自定义一个异常类,可以直接copy代码复制到自己的项目中,可以把类名字改一改,接下来是如何处理这个异常类,代码如下

import com.xx.common.base.ServerException;
import com.xx.common.base.CommonResponse;
import com.xx.common.interfaces.IHttpResponse;
import com.xx.common.utils.zs.ZSString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
@ControllerAdvice
public final class ServerExceptionHandler {
    /**
     * <p>此外若外部项目使用同样的手法对该异常进行捕获,则该方法有可能被截断而无法正常执行
     * */
    @ExceptionHandler(value = ServerException.class)
    @ResponseBody
    public IHttpResponse ServerExceptionProcess(ServerException e) {
        /*处理本地log日志*/
        if (ZSString.isBlank(e.log)){
            log.error(e.getMessage(),e);
        }else{
            log.error(e.log,e.params);
        }
        /*调用端响应对象*/
        if (e.response ==null){
            return CommonResponse.fail().setLog(e.getMessage());
        }else{
            return e.response;
        }
    }
}

上面是定义一个全局异常捕获的处理类,可以直接copy代码到自己的项目中,到这一步其实看着非常的简单,其实这里的设计才是最为关键的

注意:CommonResponse.fail();result.setLog(e.getMessage());这行代码可以替换成项目中默认的实现了IHttpResponse 这个接口的响应对象

接下来补充一个代码后进行使用说明

public interface IHttpResponse {
    /**
     * <p>状态码
     * */
    Integer getCode();
    /**
     * <p>状态码对应的消息内容
     * */
    String getMessage();
}

上面定义一个接口,规定使用者必须实现getCode()和getMessage()方法,通常在类中或者实现了该接口的enum中定义两个字段并在类或者enum上加注@Data或者@Getter注解就可以了,如下

import com.xx.common.interfaces.IHttpResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.experimental.Accessors;

@Data
@Accessors(chain=true)
public class CommonResponse<T> implements IHttpResponse {
    /**
     * <p>状态码
     * */
    public Integer code;
    /**
     * <p>系统消息
     * */
    public String message;
    /**
     * <p>响应日志
     * */
    public String log;
    /**
     * <p>结果
     * */
    public T result;

    public static <T> CommonResponse<T> instance(){
        return new CommonResponse<>();
    }

    public static <T> CommonResponse<T> instance(T data,Integer code,String message){
        CommonResponse<T> response = instance();
        return response.setCode(code).setMessage(message).setResult(data);
    }

    public static <T> CommonResponse<T> success(T data,String message){
        return instance(data, HttpResponseEnum.SUCCESS.getCode(),message);
    }

    public static <T> CommonResponse<T> success(T data){
        return success(data, HttpResponseEnum.SUCCESS.getMessage());
    }

    public static <T> CommonResponse<T> success(String message){
        return success(null, message);
    }

    public static <T> CommonResponse<T> success(){
        return success(null);
    }

    public static <T> CommonResponse<T> fail(Integer code,String message){
        return instance(null,code,message);
    }

    public static <T> CommonResponse<T> fail(){
        return fail(HttpResponseEnum.FAIL.getCode(), HttpResponseEnum.FAIL.getMessage());
    }

    public static <T> CommonResponse<T> fail(String message){
        return fail(HttpResponseEnum.FAIL.getCode(),message);
    }

    @Getter
    @AllArgsConstructor
    public enum HttpResponseEnum implements IHttpResponse {

        SUCCESS(200, "operation successfully"),
        FAIL(500,"operation failure");

        private final Integer code;
        private final String message;
    }
}

上面的代码使用了泛型,项目中可以根据自己的实际情况改成对应的代码即可,当然了也可以保持现在的泛型设计(推荐),如果需要更改默认的code和message,直接修改HttpResponseEnum 里面的值即可,接下来是演示如何快速的简单的处理所有的相应消息以及异常的处理

controller返回

  • CommonResponse.success("xxx");:返回默认的成功代码(HttpResponseEnum 中的默认值),返回消息为xxx,返回默认的result(这里为null)
    剩下的如CommonResponse.fail()就不一一列举了,看其方法的参数就全明白了,最终的目的无非是简化开发的操作,增加处理业务逻辑的手感。

异常的使用

在任意代码处只需要throw new ServerException(),框架便会根据其中的构造参数进行相应的处理,而无需开发者再去关注如何构建日志的问题,接下来只对常用的构造参数进行说明

  • throw new ServerException(String log,Object...params):它等同于log.error("xxx:{}",params),于此同时框架会对调用端(前端或其它第三方)返回的对象采用默认的处理,如上面的例子中将会返回一个result为null的CommonResponse对象
  • throw new ServerException(IHttpResponse status,String log,Object...params):这个构造参数比上面的多了一个IHttpResponse类型的参数,它支持自定义前端返回对象,此处扔进去一个要返回的响应对象(如上面的CommonResponse对象)就可以了,值得注意的是,返回的响应消息和后台的日志消息完全分离,可以分别指定不同的消息响应,也就是说返回前端的消息可以是abc,于此同时后端的日志可以打印xyz,结合国际化文件的话可以做到全方位无死角的处理
    至于其它处理可以参考上面各个构造函数的使用说明,如throw new ServerException("xx"),它所代表的含义是前端返回消息xx,同时后端也返回消息xx,如果后端的log需要params,那么可以参考刚刚说明的上面这个异常的构造函数
    注意:需要保证ServerExceptionHandler 中的@Slf4j注解有效,通常情况下项目都会配置logback.xml,相关知识点自行搜索,这里就不细说了

以此类推,上面的自定义异常类足够支撑绝大部分业务场景了,当需要抛出Throwable cause时也可以通过对应的构造函数来实现。
其实代码非常的简单,但是这里如何用最少的代码实现最为复杂的功能,这里面的设计其实才是最重要的,对于开发者而言,在做异常处理的时候只需要throw一个异常就可以了,这样既保证了代码的统一,又减少了开发者的工作

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容