关于异常处理:如何优雅的设计java异常

8次阅读

共计 16653 个字符,预计需要花费 42 分钟才能阅读完成。

我的公众号:MarkerHub,网站:https://markerhub.com

更多精选文章请点击:Java 笔记大全.md

小 Hub 领读:

作者以增删改查收货地址为实例,具体阐明了如何去设计一个好的异样解决,包含应用 Guava 中的 Preconditions、hibernate 的 hibernate-validator,还有如何异样和解决异样的逻辑,文章有点长,看完还是播种挺大!


  • 起源:lrwinx
  • https://lrwinx.github.io/

导语

异样解决是程序开发中必不可少操作之一,但如何正确优雅的对异样进行解决确是一门学识,笔者依据本人的开发教训来谈一谈我是如何对异样进行解决的。

因为本文只作一些经验之谈,不波及到基础知识局部,如果读者对异样的概念还很含糊,请先查看基础知识。

如何抉择异样类型

异样的类别

正如咱们所晓得的,java 中的异样的超类是 java.lang.Throwable(后文省略为 Throwable), 它有两个比拟重要的子类, java.lang.Exception(后文省略为 Exception) 和 java.lang.Error(后文省略为 Error),其中 Error 由 JVM 虚拟机进行治理, 如咱们所熟知的 OutOfMemoryError 异样等,所以咱们本文不关注 Error 异样,那么咱们细说一下 Exception 异样。

Exception 异样有个比拟重要的子类,叫做 RuntimeException。咱们将 RuntimeException 或其余继承自 RuntimeException 的子类称为非受检异样 (unchecked Exception),其余继承自 Exception 异样的子类称为受检异样 (checked Exception)。 本文重点来关注一下受检异样和非受检异样这两种异样。

如何抉择异样

从笔者的开发教训来看,如果在一个利用中,须要开发一个办法 (如某个性能的 service 办法),这个办法如果两头可能出现异常,那么你须要思考这个异样呈现之后是否调用者能够解决,并且你是否心愿调用者进行解决,如果调用者能够解决,并且你也心愿调用者进行解决,那么就要抛出受检异样,揭示调用者在应用你的办法时,思考到如果抛出异样时如果进行解决。

类似的,如果在写某个办法时,你认为这是个偶尔异样,实践上说,你感觉运行时可能会碰到什么问题,而这些问题兴许不是必然产生的,也不须要调用者显示的通过异样来判断业务流程操作的,那么这时就能够应用一个 RuntimeException 这样的非受检异样.

好了,预计我上边说的这段话,你读了很多遍也仍然感觉艰涩了。

那么,请跟着我的思路,在缓缓体会一下。

什么时候才须要抛异样

首先咱们须要理解一个问题,什么时候才须要抛异样?异样的设计是不便给开发者应用的,但不是乱用的,笔者对于什么时候抛异样这个问题也问了很多敌人,能给出精确答案的的确不多。其实这个问题很简略,如果你感觉某些”问题”解决不了了,那么你就能够抛出异样了。

比方,你在写一个 service, 其中在写到某段代码处, 你发现可能会产生问题,那么就请抛出异样吧,置信我,你此时抛出异样将是一个最佳时机。

应该抛出怎么的异样

理解完了什么时候才须要抛出异样后,咱们再思考一个问题, 真的当咱们抛出异样时,咱们应该选用怎么的异样呢?到底是受检异样还是非受检异样呢 (RuntimeException) 呢?

我来举例说明一下这个问题,先从受检异样说起, 比如说有这样一个业务逻辑,须要从某文件中读取某个数据,这个读取操作可能是因为文件被删除等其余问题导致无奈获取从而呈现读取谬误,那么就要从 redis 或 mysql 数据库中再去获取此数据, 参考如下代码,getKey(Integer) 为入口程序.

public String getKey(Integer key){
    String  value;
    try {InputStream inputStream = getFiles("/file/nofile");
        // 接下来从流中读取 key 的 value 指
        value = ...;
    } catch (Exception e) {
        // 如果抛出异样将从 mysql 或者 redis 进行取之
        value = ...;
    }
}

public InputStream getFiles(String path) throws Exception {File file = new File(path);
    InputStream inputStream = null;
    try {inputStream = new BufferedInputStream(new FileInputStream(file));
    } catch (FileNotFoundException e) {throw new Exception("I/ O 读取谬误",e.getCause());
    }
    return inputStream;
}

ok,看了以上代码当前,你兴许心中有一些想法,原来受检异样能够管制任务逻辑,对,没错, 通过受检异样真的能够管制业务逻辑,然而切记不要这样应用 ,咱们应该正当的抛出异样,因为程序自身才是流程,异样的作用仅仅是当你进行不上来的时候找到的一个借口而已,它并不能当成管制程序流程的入口或进口,如果这样应用的话,是在将异样的作用扩大化,这样将会导致代码复杂程度的减少,耦合性会进步,代码可读性升高等问题。

那么就肯定不要应用这样的异样吗?其实也不是,在真的有这样的需要的时候,咱们能够这样应用,只是切记,不要把它真的当成管制流程的工具或伎俩。那么到底什么时候才要抛出这样的异样呢?要思考, 如果调用者调用出错后,肯定要让调用者对此谬误进行解决才能够,满足这样的要求时,咱们才会思考应用受检异样。

接下来,咱们来看一下非受检异样呢 (RuntimeException),对于 RuntimeException 这种异样,咱们其实很多见,比方 java.lang.NullPointerException/java.lang.IllegalArgumentException 等,那么这种异样咱们时候抛出呢?

当咱们在写某个办法的时候,可能会偶尔遇到某个谬误,咱们认为这个问题时运行时可能为产生的,并且实践上讲,没有这个问题的话,程序将会失常执行的时候,它不强制要求调用者肯定要捕捉这个异样,此时抛出 RuntimeException 异样。

举个例子,当传来一个门路的时候,须要返回一个门路对应的 File 对象:

public void test() {myTest.getFiles("");
}

public File getFiles(String path) {if(null == path || "".equals(path)){throw  new NullPointerException("门路不能为空!");
    }
    File file = new File(path);

    return file;
}

上述例子表明,如果调用者调用 getFiles(String) 的时候如果 path 是空,那么就抛出空指针异样 (它是 RuntimeException 的子类), 调用者不必显示的进行 try…catch… 操作进行强制解决. 这就要求调用者在调用这样的办法时先进行验证,防止产生 RuntimeException. 如下:

public void test() {
    String path = "/a/b.png";
    if(null != path && !"".equals(path)){myTest.getFiles("");
    }
}

public File getFiles(String path) {if(null == path || "".equals(path)){throw  new NullPointerException("门路不能为空!");
    }
    File file = new File(path);

    return file;
}

应该选用哪种异样

通过以上的形容和举例,能够总结出一个论断,RuntimeException 异样和受检异样之间的区别就是: 是否强制要求调用者必须解决此异样,如果强制要求调用者必须进行解决,那么就应用受检异样,否则就抉择非受检异样 (RuntimeException)。 一般来讲,如果没有非凡的要求,咱们倡议应用 RuntimeException 异样。

场景介绍和技术选型

架构形容

正如咱们所知,传统的我的项目都是以 MVC 框架为根底进行开发的,本文次要从应用 restful 格调接口的设计来体验一下异样解决的优雅。

咱们把关注点放在 restful 的 api 层 (和 web 中的 controller 层相似) 和 service 层,钻研一下在 service 中如何抛出异样,而后 api 层如何进行捕捉并且转化异样。

应用的技术是: spring-boot,jpa(hibernate),mysql, 如果对这些技术不是太相熟,读者须要自行浏览相干资料。

业务场景形容

抉择一个比较简单的业务场景,以电商中的收货地址治理为例,用户在挪动端进行购买商品时,须要进行收货地址治理,在我的项目中,提供一些给挪动端进行拜访的 api 接口,如: 增加收货地址,删除收货地址,更改收货地址,默认收货地址设置,收货地址列表查问,单个收货地址查问等接口。

构建约束条件

ok,这个是设置好的一个很根本的业务场景,当然,无论什么样的 api 操作,其中都蕴含一些规定:

增加收货地址:
入参:

  • 用户 id
  • 收货地址实体信息

束缚:

  • 用户 id 不能为空,且此用户的确是存在 的
  • 收货地址的必要字段不能为 空
  • 如果用户还没有收货地址,当此收货地址创立时设置成默认收货地址 —

删除收货地址:
入参:

  • 用户 id
  • 收货地址 id

束缚:

  • 用户 id 不能为空,且此用户的确是存在的
  • 收货地址不能为空,且此收货地址的确是存在的
  • 判断此收货地址是否是用户的收货地址
  • 判断此收货地址是否为默认收货地址,如果是默认收货地址,那么不能进行删除

更改收货地址:
入参:

  • 用户 id
  • 收货地址 id

束缚:

  • 用户 id 不能为空,且此用户的确是存在的
  • 收货地址不能为空,且此收货地址的确是存在的
  • 判断此收货地址是否是用户的收货地址

默认地址设置:
入参:

  • 用户 id
  • 收货地址 id

束缚:

  • 用户 id 不能为空,且此用户的确是存在的
  • 收货地址不能为空,且此收货地址的确是存在的
  • 判断此收货地址是否是用户的收货地址

收货地址列表查问:
入参:

  • 用户 id

束缚:

  • 用户 id 不能为空,且此用户的确是存在的

单个收货地址查问:
入参:

  • 用户 id
  • 收货地址 id

束缚:

  • 用户 id 不能为空,且此用户的确是存在的
  • 收货地址不能为空,且此收货地址的确是存在的
  • 判断此收货地址是否是用户的收货地址

束缚判断和技术选型

对于上述列出的约束条件和性能列表,我抉择几个比拟典型的异样解决场景进行剖析: 增加收货地址,删除收货地址,获取收货地址列表。

那么应该有哪些必要的常识储备呢,让咱们看一下收货地址这个性能:

增加收货地址中须要对用户 id 和收货地址实体信息就行校验,那么对于非空的判断,咱们如何进行工具的抉择呢?传统的判断如下:

/**
 * 增加地址
 * @param uid
 * @param address
 * @return
 */
public Address addAddress(Integer uid,Address address){if(null != uid){// 进行解决..}
    return null;
}

上边的例子,如果只判断 uid 为空还好,如果再去判断 address 这个实体中的某些必要属性是否为空,在字段很多的状况下,这无非是灾难性的。

那咱们应该怎么进行这些入参的判断呢,给大家介绍两个知识点:

  • Guava 中的 Preconditions 类实现了很多入参办法的判断
  • jsr 303 的 validation 标准 (目前实现比拟全的是 hibernate 实现的 hibernate-validator)

如果应用了这两种举荐技术,那么入参的判断会变得简略很多。 举荐大家多应用这些成熟的技术和 jar 工具包,他能够缩小很多不必要的工作量。 咱们只须要把重心放到业务逻辑上。 而不会因为这些入参的判断耽搁更多的工夫。

如何优雅的设计 jav 异样

domain 介绍

依据我的项目场景来看,须要两个 domain 模型,一个是用户实体,一个是地址实体.

Address domain 如下:

@Entity
@Data
public class Address {
    @Id
    @GeneratedValue
    private Integer id;
    private String province;// 省
    private String city;// 市
    private String county;// 区
    private Boolean isDefault;// 是否是默认地址

    @ManyToOne(cascade={CascadeType.ALL})
    @JoinColumn()
    private User user;
}

User domain 如下:

@Entity
@Data
public class User {
    @Id
   @GeneratedValue
   private Integer id;
   private String name;// 姓名

    @OneToMany(cascade= CascadeType.ALL,mappedBy="user",fetch = FetchType.LAZY)
        private Set<Address> addresses;
}

ok, 上边是一个模型关系,用户 – 收货地址的关系是 1-n 的关系。上边的 @Data 是应用了一个叫做 lombok 的工具,它主动生成了 Setter 和 Getter 等办法,用起来十分不便,感兴趣的读者能够自行理解一下。

dao 介绍

数据连贯层,咱们应用了 spring-data-jpa 这个框架,它要求咱们只须要继承框架提供的接口,并且依照约定对办法进行取名,就能够实现咱们想要的数据库操作。

用户数据库操作如下:

@Repository
public interface IUserDao extends JpaRepository<User,Integer> {

}

收货地址操作如下:

@Repository
public interface IAddressDao extends JpaRepository<Address,Integer> {

}

正如读者所看到的,咱们的 DAO 只须要继承 JpaRepository, 它就曾经帮咱们实现了根本的 CURD 等操作,如果想理解更多对于 spring-data 的这个我的项目,请参考一下 spring 的官网文档,它比不计划咱们对异样的钻研。

Service 异样设计

ok,终于到了咱们的重点了,咱们要实现 service 一些的局部操作: 增加收货地址,删除收货地址,获取收货地址列表.

首先看我的 service 接口定义:

public interface IAddressService {

/**
 * 创立收货地址
 * @param uid
 * @param address
 * @return
 */
Address createAddress(Integer uid,Address address);

/**
 * 删除收货地址
 * @param uid
 * @param aid
 */
void deleteAddress(Integer uid,Integer aid);

/**
 * 查问用户的所有收货地址
 * @param uid
 * @return
 */
List<Address> listAddresses(Integer uid);
}

咱们来关注一下实现:

增加收货地址

首先再来看一下之前整顿的约束条件:

入参:

  • 用户 id
  • 收货地址实体信息

束缚:

  • 用户 id 不能为空,且此用户的确是存在的
  • 收货地址的必要字段不能为空
  • 如果用户还没有收货地址,当此收货地址创立时设置成默认收货地址

先看以下代码实现:

 @Override
public Address createAddress(Integer uid, Address address) {
    //============ 以下为约束条件   ==============
    //1. 用户 id 不能为空,且此用户的确是存在的
    Preconditions.checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){throw new RuntimeException("找不到以后用户!");
    }
    //2. 收货地址的必要字段不能为空
    BeanValidators.validateWithException(validator, address);
    //3. 如果用户还没有收货地址,当此收货地址创立时设置成默认收货地址
    if(ObjectUtils.isEmpty(user.getAddresses())){address.setIsDefault(true);
    }

    //============ 以下为失常执行的业务逻辑   ==============
    address.setUser(user);
    Address result = addressDao.save(address);
    return result;
}

其中,曾经实现了上述所形容的三点约束条件,当三点约束条件都满足时,才能够进行失常的业务逻辑,否则将抛出异样 (个别在此处倡议抛出运行时异样 – RuntimeException)。

介绍以下以上我所用到的技术:

1、Preconfitions.checkNotNull(T t) 这个是应用 Guava 中的 com.google.common.base.Preconditions 进行判断的,因为 service 中用到的验证较多,所以倡议将 Preconfitions 改成动态导入的形式:

import static com.google.common.base.Preconditions.checkNotNull; 

当然 Guava 的 github 中的阐明也倡议咱们这样应用。

2、BeanValidators.validateWithException(validator, address);
这个应用了 hibernate 实现的 jsr 303 标准来做的,须要传入一个 validator 和一个须要验证的实体, 那么 validator 是如何获取的呢, 如下:

@Configuration
public class BeanConfigs {

@Bean
public javax.validation.Validator getValidator(){return new LocalValidatorFactoryBean();
}
}

他将获取一个 Validator 对象,而后咱们在 service 中进行注入便能够应用了:

 @Autowired     
private Validator validator ;

那么 BeanValidators 这个类是如何实现的?其实实现形式很简略,只有去判断 jsr 303 的标注注解就 ok 了。

那么 jsr 303 的注解写在哪里了呢?当然是写在 address 实体类中了:

@Entity
@Setter
@Getter
public class Address {
@Id
    @GeneratedValue
    private Integer id;
    @NotNull
private String province;// 省
@NotNull
private String city;// 市
@NotNull
private String county;// 区
private Boolean isDefault = false;// 是否是默认地址

@ManyToOne(cascade={CascadeType.ALL})
@JoinColumn()
private User user;
}

写好你须要的约束条件来进行判断,如果正当的话,才能够进行业务操作,从而对数据库进行操作。

这块的验证是必须的,一个最次要的起因是: 这样的验证能够防止脏数据的插入。

如果读者有正式上线的教训的话,就能够了解这样的一个事件, 任何的代码谬误都能够容忍和批改,然而如果呈现了脏数据问题,那么它有可能是一个毁灭性的劫难。 程序的问题能够批改,然而脏数据的呈现有可能无奈复原。所以这就是为什么在 service 中肯定要判断好约束条件,再进行业务逻辑操作的起因了。

此处的判断为业务逻辑判断,是从业务角度来进行筛选判断的,除此之外,有可能在很多场景中都会有不同的业务条件束缚,只须要依照要求来做就好。

对于约束条件的总结如下:

  • 根本判断束缚 (null 值等根本判断)
  • 实体属性束缚 (满足 jsr 303 等根底判断)
  • 业务条件束缚 (需要提出的不同的业务束缚)

当这个三点都满足时,才能够进行下一步操作

ok, 根本介绍了如何做一个根底的判断,那么再回到异样的设计问题上,上述代码曾经很分明的形容如何在适当的地位正当的判断一个异样了,那么如何正当的抛出异样呢?

只抛出 RuntimeException 就算是优雅的抛出异样吗?当然不是,对于 service 中的抛出异样,笔者认为大抵有两种抛出的办法:

  • 抛出带状态码 RumtimeException 异样
  • 抛出指定类型的 RuntimeException 异样

绝对这两种异样的形式进行完结,第一种异样指的是我所有的异样都抛 RuntimeException 异样,然而须要带一个状态码,调用者能够依据状态码再去查问到底 service 抛出了一个什么样的异样。

第二种异样是指在 service 中抛出什么样的异样就自定义一个指定的异样谬误,而后在进行抛出异样。

一般来讲,如果零碎没有别的非凡需要的时候,在开发设计中,倡议应用第二种形式。然而比如说像根底判断的异样,就能够齐全应用 guava 给咱们提供的类库进行操作。jsr 303 异样也能够应用本人封装好的异样判断类进行操作,因为这两种异样都是属于根底判断,不须要为它们指定非凡的异样。然而对于第三点任务条件束缚判断抛出的异样,就须要抛出指定类型的异样了。

对于

throw new RuntimeException("找不到以后用户!");

定义一个特定的异样类来进行这个任务异样的判断:

public class NotFindUserException extends RuntimeException {public NotFindUserException() {super("找不到此用户");
}

public NotFindUserException(String message) {super(message);
}
}

而后将此处改为:

throw new NotFindUserException("找不到以后用户!");
or

throw new NotFindUserException();

ok, 通过以上对 service 层的批改,代码更改如下:

@Override
public Address createAddress(Integer uid, Address address) {
    //============ 以下为约束条件   ==============
    //1. 用户 id 不能为空,且此用户的确是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){throw new NotFindUserException("找不到以后用户!");
    }
    //2. 收货地址的必要字段不能为空
    BeanValidators.validateWithException(validator, address);
    //3. 如果用户还没有收货地址,当此收货地址创立时设置成默认收货地址
    if(ObjectUtils.isEmpty(user.getAddresses())){address.setIsDefault(true);
    }

    //============ 以下为失常执行的业务逻辑   ==============
    address.setUser(user);
    Address result = addressDao.save(address);
    return result;
}

这样的 service 就看起来稳定性和了解性就比拟强了。

删除收货地址:

入参:

  • 用户 id
  • 收货地址 id

束缚:

  • 用户 id 不能为空,且此用户的确是存在的
  • 收货地址不能为空,且此收货地址的确是存在的
  • 判断此收货地址是否是用户的收货地址
  • 判断此收货地址是否为默认收货地址,如果是默认收货地址,那么不能进行删除

它与上述增加收货地址相似,故不再赘述,delete 的 service 设计如下:

@Override
public void deleteAddress(Integer uid, Integer aid) {
    //============ 以下为约束条件   ==============
    //1. 用户 id 不能为空,且此用户的确是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){throw new NotFindUserException();
    }
    //2. 收货地址不能为空,且此收货地址的确是存在的
    checkNotNull(aid);
    Address address = addressDao.findOne(aid);
    if(null == address){throw new NotFindAddressException();
    }
    //3. 判断此收货地址是否是用户的收货地址
    if(!address.getUser().equals(user)){throw new NotMatchUserAddressException();
    }
    //4. 判断此收货地址是否为默认收货地址,如果是默认收货地址,那么不能进行删除
    if(address.getIsDefault()){throw  new DefaultAddressNotDeleteException();
    }

    //============ 以下为失常执行的业务逻辑   ==============
    addressDao.delete(address);
}

设计了相干的四个异样类:
NotFindUserException,NotFindAddressException,NotMatchUserAddressException,DefaultAddressNotDeleteException.
依据不同的业务需要抛出不同的异样。

获取收货地址列表:
入参:

  • 用户 id

束缚:

  • 用户 id 不能为空,且此用户的确是存在的

代码如下:

 @Override
public List<Address> listAddresses(Integer uid) {
    //============ 以下为约束条件   ==============
    //1. 用户 id 不能为空,且此用户的确是存在的
    checkNotNull(uid);
    User user = userDao.findOne(uid);
    if(null == user){throw new NotFindUserException();
    }

    //============ 以下为失常执行的业务逻辑   ==============
    User result = userDao.findOne(uid);
    return result.getAddresses();}

api 异样设计

大抵有两种抛出的办法:

  • 抛出带状态码 RumtimeException 异样
  • 抛出指定类型的 RuntimeException 异样

这个是在设计 service 层异样时提到的,通过对 service 层的介绍,咱们在 service 层抛出异样时抉择了第二种抛出的形式,不同的是,在 api 层抛出异样咱们须要应用这两种形式进行抛出: 要指定 api 异样的类型,并且要指定相干的状态码,而后才将异样抛出,这种异样设计的外围是让调用 api 的使用者更能分明的理解产生异样的详细信息。

除了抛出异样外,咱们还须要将状态码对应的异样详细信息以及异样有可能产生的问题制作成一个对应的表展现给用户,不便用户的查问。(如 github 提供的 api 文档,微信提供的 api 文档等), 还有一个益处: 如果用户须要自定义提醒音讯,能够依据返回的状态码进行提醒的批改。

api 验证束缚

首先对于 api 的设计来说,须要存在一个 dto 对象,这个对象负责和调用者进行数据的沟通和传递,而后 dto->domain 在传给 service 进行操作,这一点肯定要留神。

第二点,除了说道的 service 须要进行根底判断 (null 判断) 和 jsr 303 验证以外,同样的,api 层也须要进行相干的验证,如果验证不通过的话,间接返回给调用者,告知调用失败,不应该带着不非法的数据再进行对 service 的拜访。

那么读者可能会有些蛊惑,不是 service 曾经进行验证了,为什么 api 层还须要进行验证么?这里便设计到了一个概念: 编程中的墨菲定律,如果 api 层的数据验证忽略了,那么有可能不非法数据就带到了 service 层,进而讲脏数据保留到了数据库。

所以周密编程的外围是: 永远不要置信收到的数据是非法的。

api 异样设计

设计 api 层异样时,正如咱们上边所说的,须要提供错误码和错误信息,那么能够这样设计,提供一个通用的 api 超类异样,其余不同的 api 异样都继承自这个超类:

public class ApiException extends RuntimeException {
protected Long errorCode ;
protected Object data ;

public ApiException(Long errorCode,String message,Object data,Throwable e){super(message,e);
    this.errorCode = errorCode ;
    this.data = data ;
}

public ApiException(Long errorCode,String message,Object data){this(errorCode,message,data,null);
}

public ApiException(Long errorCode,String message){this(errorCode,message,null,null);
}

public ApiException(String message,Throwable e){this(null,message,null,e);
}

public ApiException(){}

public ApiException(Throwable e){super(e);
}

public Long getErrorCode() {return errorCode;}

public void setErrorCode(Long errorCode) {this.errorCode = errorCode;}

public Object getData() {return data;}

public void setData(Object data) {this.data = data;}
}

而后别离定义 api 层异样:
ApiDefaultAddressNotDeleteException,ApiNotFindAddressException,ApiNotFindUserException,ApiNotMatchUserAddressException
以默认地址不能删除为例:

public class ApiDefaultAddressNotDeleteException extends ApiException {public ApiDefaultAddressNotDeleteException(String message) {super(AddressErrorCode.DefaultAddressNotDeleteErrorCode, message, null);
}
}

AddressErrorCode.DefaultAddressNotDeleteErrorCode
就是须要提供给调用者的错误码。错误码类如下:

public abstract class AddressErrorCode {
    public static final Long DefaultAddressNotDeleteErrorCode = 10001L;// 默认地址不能删除
    public static final Long NotFindAddressErrorCode = 10002L;// 找不到此收货地址
    public static final Long NotFindUserErrorCode = 10003L;// 找不到此用户
    public static final Long NotMatchUserAddressErrorCode = 10004L;// 用户与收货地址不匹配
}

ok, 那么 api 层的异样就曾经设计完了,在此多说一句,AddressErrorCode 错误码类寄存了可能呈现的错误码,更正当的做法是把他放到配置文件中进行治理。

api 解决异样

api 层会调用 service 层,而后来解决 service 中呈现的所有异样,首先,须要保障一点,肯定要让 api 层十分轻,基本上做成一个转发的性能就好 (接口参数,传递给 service 参数,返回给调用者数据, 这三个基本功能),而后就要在传递给 service 参数的那个办法调用上进行异样解决。

此处仅以增加地址为例:

 @Autowired
private IAddressService addressService;


/**
 * 增加收货地址
 * @param addressDTO
 * @return
 */
@RequestMapping(method = RequestMethod.POST)
public AddressDTO add(@Valid @RequestBody AddressDTO addressDTO){Address address = new Address();
    BeanUtils.copyProperties(addressDTO,address);
    Address result;
    try {result = addressService.createAddress(addressDTO.getUid(), address);
    }catch (NotFindUserException e){throw new ApiNotFindUserException("找不到该用户");
    }catch (Exception e){// 未知谬误
        throw new ApiException(e);
    }
    AddressDTO resultDTO = new AddressDTO();
    BeanUtils.copyProperties(result,resultDTO);
    resultDTO.setUid(result.getUser().getId());

    return resultDTO;
}

这里的解决计划是调用 service 时,判断异样的类型,而后将任何 service 异样都转化成 api 异样,而后抛出 api 异样,这是罕用的一种异样转化形式。类似删除收货地址和获取收货地址也相似这样解决,在此,不在赘述。

api 异样转化

曾经解说了如何抛出异样和何如将 service 异样转化为 api 异样,那么转化成 api 异样间接抛出是否就实现了异样解决呢?答案是否定的,当抛出 api 异样后,咱们须要把 api 异样返回的数据 (json or xml) 让用户看懂,那么须要把 api 异样转化成 dto 对象 (ErrorDTO), 看如下代码:

@ControllerAdvice(annotations = RestController.class)
class ApiExceptionHandlerAdvice {

/**
 * Handle exceptions thrown by handlers.
 */
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseEntity<ErrorDTO> exception(Exception exception,HttpServletResponse response) {ErrorDTO errorDTO = new ErrorDTO();
    if(exception instanceof ApiException){//api 异样
        ApiException apiException = (ApiException)exception;
        errorDTO.setErrorCode(apiException.getErrorCode());
    }else{// 未知异样
        errorDTO.setErrorCode(0L);
    }
    errorDTO.setTip(exception.getMessage());
    ResponseEntity<ErrorDTO> responseEntity = new ResponseEntity<>(errorDTO,HttpStatus.valueOf(response.getStatus()));
    return responseEntity;
}

@Setter
@Getter
class ErrorDTO{
    private Long errorCode;
    private String tip;
}
}

ok, 这样就实现了 api 异样转化成用户能够读懂的 DTO 对象了,代码中用到了 @ControllerAdvice,这是 spring MVC 提供的一个非凡的切面解决。

当调用 api 接口产生异样时,用户也能够收到失常的数据格式了, 比方当没有用户 (uid 为 2) 时,却为这个用户增加收货地址, postman(Google plugin 用于模仿 http 申请) 之后的数据:

{
  "errorCode": 10003,
  "tip": "找不到该用户"
}

总结

本文只从如何设计异样作为重点来解说,波及到的 api 传输和 service 的解决,还有待优化,比方 api 接口拜访须要应用 https 进行加密,api 接口须要 OAuth2.0 受权或 api 接口须要签名认证等问题,文中都未曾提到,本文的重心在于异样如何解决,所以读者只需关注波及到异样相干的问题和解决形式就能够了。

举荐浏览

Java 笔记大全.md

太赞了,这个 Java 网站,什么我的项目都有!https://markerhub.com

这个 B 站的 UP 主,讲的 java 真不错!

太赞了!最新版 Java 编程思维能够在线看了!

正文完
 0