文章目录
  1. 1. 工程结构
  2. 2. 代码讲解
    1. 2.1. DemoController.java 模拟接口及DemoService.java 模拟业务
    2. 2.2. Result统一返回实体
    3. 2.3. CodeMsg异常编码封装类
  3. 3. 异常处理
    1. 3.1. GlobalExceptionHandler全局异常处理类
    2. 3.2. GlobalException自定义全局异常
    3. 3.3. 测试
  4. 4. 总结

不知道各位有没有这种体验,做需求的时候代码逻辑实现完了,但是看自己的代码总是有些别扭。

一个controller里实现了很多非必要的业务逻辑,比如异常处理逻辑,参数校验等,和业务逻辑夹杂在一起看起来很冗长。

这种感受我也体会过,在经过一段时间的练习和学习他人的写码技巧后,感觉这种全局异常处理、基于切面的异常处理等方式对提升代码可读性和美观性、简洁性很有帮助,因此分享出来,一方面是自己学习的总结,另一方面也希望能够为读者朋友们提供些许帮助。

我是基于一个springboot应用去实践的,版本为2.0.3.RELEASE,spring3.x以上版本均兼容。

工程结构

com.snowalker
    |-controller
        |-DemoController.java 模拟接口
    |-service
        |-DemoService.java 模拟业务
    |-exception
        |-GlobalException.java 全局异常类
        |-GlobalExceptionHandler.java 全局异常处理,ControllerAdvice类
    |-result
        |-CodeMsg.java 异常代码封装类
        |-Result.java 统一返回实体
    |-GlobalExceptionApplication.java 启动类

代码讲解

首先对代码进行讲解,最后分析这么做的原因,话不多说,上代码。

DemoController.java 模拟接口及DemoService.java 模拟业务

@Controller
@RequestMapping(value = "demo")
public class DemoController {

    @Autowired
    DemoService service;

    @RequestMapping(value = "test", method = {RequestMethod.GET, RequestMethod.POST})
    public @ResponseBody Result test(@RequestParam
    (value = "username", required = true) String username) {
        String userId = service.execute(username);
        return Result.success(userId);
    }
}

@Service
public class DemoService {

    public String execute(String username) {
        if (username == null) {
            throw new GlobalException(CodeMsg.SERVER_ERROR);
        }
        return UUID.randomUUID().toString();
    }
}

这里简单的模拟了一个业务场景,对输入的用户名称进行判断,如果为不为空就为他分配一个id并返回。

也许你看到Result和GlobalException有疑问,为什么接口只处理了正常的逻辑,异常不需要处理吗?接下来我就说这两个类。

Result统一返回实体

public class Result<T> {

    private int code;
    private String msg;
    private T data;

    /**
    *  成功时候的调用
    * */
    public static  <T> Result<T> success(T data){
        return new Result<T>(0000, "SUCCESS", data);
    }

    /**
    *  失败时候的调用
    * */
    public static  <T> Result<T> error(CodeMsg codeMsg){
        return new Result<T>(codeMsg);
    }

    private Result(T data) {
        this.data = data;
    }

    private Result(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    private Result(int code, String msg, T t) {
        this.code = code;
        this.msg = msg;
        this.data = t;
    }

    private Result(CodeMsg codeMsg) {
        if(codeMsg != null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg();
        }
    }

    public int getCode() {
        return code;
    }
    public void setCode(int code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

这种写法想必很多小伙伴都用过,封装一个全局的返回实体,将泛型传入就可以对前端返回各种类型的数据,一般都会序列化为JSON格式的数据。

我这里写了两个静态方法,success(T data)和error(CodeMsg codeMsg),调用的时候类似这样

return Result.success(userId);

有没有觉得简洁了很多。这个打住不说。

CodeMsg异常编码封装类

public class CodeMsg {

    private int code;
    private String msg;

    //通用的错误码
    public static CodeMsg SUCCESS = new CodeMsg(0, "success");
    public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常");
    public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
    //登录模块 5002XX
    public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效");
    public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空");
    public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空");

    private CodeMsg( ) {
    }

    private CodeMsg( int code,String msg ) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }
    public void setCode(int code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }

    public CodeMsg fillArgs(Object... args) {
        int code = this.code;
        String message = String.format(this.msg, args);
        return new CodeMsg(code, message);
    }

    @Override
    public String toString() {
        return "CodeMsg [code=" + code + ", msg=" + msg + "]";
    }

}

这个类是全局的错误码封装实体,你也可以用一个枚举替代。主要作用就是将错误码集中在一处管理,通过

Result.error(CodeMsg codeMsg)

这种方式调用,将业务错误码直接传回前端。错误码可以随意扩展。

异常处理

这里讲到重头戏了,我们一般都会在业务代码中对异常的情况写逻辑处理,这样的后果就是业务代码中掺杂了很多不相干的异常处理逻辑,对我们阅读代码干扰很大,也不利于后续的扩展,因此这里提供一个全局异常处理类,基于ControllerAdvice,它的本质也是AOP

GlobalExceptionHandler全局异常处理类

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(value=Exception.class)
    public Result<String> exceptionHandler(
        HttpServletRequest request, Exception e){
        e.printStackTrace();
        if(e instanceof GlobalException) {
            GlobalException ex = (GlobalException)e;
            return Result.error(ex.getCm());
        }else if(e instanceof BindException) {
            BindException ex = (BindException)e;
            List<ObjectError> errors = ex.getAllErrors();
            ObjectError error = errors.get(0);
            String msg = error.getDefaultMessage();
            return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
        }else {
            return Result.error(CodeMsg.SERVER_ERROR);
        }
    }
}

这里我们对业务层传输来的异常做了判断,通过

Result.error(ex.getCm());

对不同类型的异常统一返回。

@ControllerAdvice这个注解是在spring 3.2中新增的。用于拦截全局的Controller的异常,注意:ControllerAdvice注解只拦截Controller不会拦截Interceptor的异常

GlobalException自定义全局异常

public class GlobalException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    private CodeMsg cm;

    public GlobalException(CodeMsg cm) {
        super(cm.toString());
        this.cm = cm;
    }

    public GlobalException() {}

    public CodeMsg getCm() {
        return cm;
    }
}

通过组合,内部维持一个CodeMsg的引用,在业务层对异常通过构造方法进行实例化并抛出,让切面捕获进行处理

if (username == null) {
    throw new GlobalException(CodeMsg.SERVER_ERROR);
}
return UUID.randomUUID().toString();

测试

  1. 启动工程
  2. 访问http://localhost:8080/demo/test?username=snowalkerz这个链接(我应用对外暴露的端口是8080,你可以改成自己喜欢的)。返回正确的调用结果。

    {
        "code": 0,
        "msg": "SUCCESS",
        "data": "cc935a8b-4bd0-4fa3-a393-a2efa14e3b93"
    }
    
  3. 我们模拟一个错误的情况,让业务层抛出异常。访问http://localhost:8080/demo/test
    返回如下

    {
        "code": 500100,
        "msg": "服务端异常",
        "data": null
    }
    

可以看到达到了我们的目的,异常被捕获并通过全局异常处理器返回到前端,这样前端就可以用统一的业务逻辑去判断返回状态码,对于前后端的交互都有着积极作用。

总结

到这里,通过自定义异常及异常切面处理器简化代码逻辑的讲解就告一段落了。
总的来看,我们通过自定义全局异常的方式,在业务层只需要抛出对应的自定义异常即可,当然这是针对可以抛出的异常而言,对于业务性的错误,还是要做具体的业务处理。

这种做法能够让我们的接口层变得简洁清爽,只需要书写你的正确的业务逻辑,将业务代码封闭在业务层,接口层只进行返回。这种方式将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护

对于异常,这种全局异常处理的方式将异常信息统一起来,对于前端的同学来讲也是一件好事,前端能够对异常代码做统一的处理和展示等逻辑,接口更加“干净”。

核心思想就是降低代码的熵,提升可读性和扩展性。

代码我上传到了github上,感兴趣的可以下载下来玩耍,地址https://github.com/TaXueWWL/springboot-global-exception

文章目录
  1. 1. 工程结构
  2. 2. 代码讲解
    1. 2.1. DemoController.java 模拟接口及DemoService.java 模拟业务
    2. 2.2. Result统一返回实体
    3. 2.3. CodeMsg异常编码封装类
  3. 3. 异常处理
    1. 3.1. GlobalExceptionHandler全局异常处理类
    2. 3.2. GlobalException自定义全局异常
    3. 3.3. 测试
  4. 4. 总结
Fork me on GitHub