Home  >  Article  >  Java  >  How to design elegant Java exceptions?

How to design elegant Java exceptions?

王林
王林forward
2023-04-26 11:25:071196browse

When do you need to throw an exception

First we need to understand a question, when do we need to throw an exception? Exceptions are designed to be convenient for developers to use, but not to be used indiscriminately. The author has also asked many friends about when to throw exceptions, and there are indeed not many who can give accurate answers. In fact, this problem is very simple. If you feel that certain "problems" cannot be solved, then you can throw an exception. For example, if you are writing a service, and when you write a certain piece of code, you find that a problem may occur, then please throw an exception. Believe me, this is the best time for you to throw an exception.

What kind of exception should be thrown

After understanding when we need to throw an exception, let's think about another question. When we really throw an exception, what kind of exception should we choose? Is it a checked exception or an unchecked exception (RuntimeException)? Let me give an example to illustrate this problem, starting with the checked exception. For example, if there is such a business logic, a certain data needs to be read from a certain file. This reading operation may not be possible due to other problems such as file deletion. If a read error occurs due to acquisition, then the data must be obtained from the redis or mysql database. Refer to the following code. getKey(Integer) is the entry program.

public String getKey(Integer key){

String value;

try {

InputStream inputStream = getFiles("/file/nofile");

//Next, read the value of the key from the stream

value = ...;

} catch (Exception e) {

//If an exception is thrown, it will be taken from mysql or 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 read error",e.getCause());

}

return inputStream;

}

Ok, after reading the above code, you may have some thoughts in your mind. It turns out that checked exceptions can control the obligation logic. Yes, that’s right, you can really control the business logic through checked exceptions, but remember not to use it like this. We should be reasonable. Throw exceptions because the program itself is the process. The function of exceptions is just an excuse when you can't proceed. It cannot be used as the entrance or exit to control the program flow. If used in this way, it is using exceptions. The expanded role will lead to increased code complexity, increased coupling, and reduced code readability. So we must not use such exceptions? In fact, no, when there is really such a need, we can use it in this way, but remember not to really regard it as a tool or means to control the process. So when should such an exception be thrown? It should be considered that if the caller makes an error in the call, the caller must be allowed to handle the error. Only when such requirements are met, we will consider using checked exceptions.

Next, let’s take a look at unchecked exceptions (RuntimeException). We actually see a lot of exceptions like RuntimeException, such as java.lang.NullPointerException/java.lang.IllegalArgumentException, etc. So when do we throw this kind of exception? ? When we write a certain method, we may encounter an error accidentally. We think this problem may occur during runtime, and theoretically speaking, if there is no such problem, the program will execute normally. The caller is not required to catch this exception. A RuntimeException is thrown at this time. For example, when a path is passed, a File object corresponding to the path needs to be returned:

public void test() {

myTest.getFiles("");

}

public File getFiles(String path) {

if(null == path || "".equals(path)){

throw new NullPointerException("The path cannot be empty!");

}

File file = new File(path);

return file;

}

The above example shows that if the path is empty when the caller calls getFiles(String), then a null pointer exception (which is a subclass of RuntimeException) will be thrown, and the caller does not need to explicitly perform a try...catch... operation for forced processing. This requires the caller to verify first when calling such a method to avoid RuntimeException. As follows:

Which exception should be used

Through the above description and examples, we can draw a conclusion. The difference between RuntimeException and checked exception is: whether the caller is forced to handle this exception. If the caller is forced to handle this exception, then use the checked exception. , otherwise select unchecked exception (RuntimeException). Generally speaking, if there are no special requirements, we recommend using RuntimeException.

Scenario introduction and technology selection

Architecture Description

As we know, traditional projects are developed based on the MVC framework. This article mainly uses the design of restful style interface to experience the elegance of exception handling.

We focus on the restful api layer (similar to the controller layer in the web) and service layer, study how exceptions are thrown in the service, and then how the api layer captures and transforms the exceptions.

The technologies used are: spring-boot, jpa (hibernate), mysql. If you are not familiar with these technologies, readers need to read the relevant materials by themselves.

Business scenario description

Choose a relatively simple business scenario, taking the delivery address management in e-commerce as an example. When users purchase goods on the mobile terminal, they need to manage the delivery address. In the project, some API interfaces are provided for mobile terminals to access. , such as: add delivery address, delete delivery address, change delivery address, default delivery address setting, delivery address list query, single delivery address query and other interfaces.

Build constraints

ok, this is a very basic business scenario that has been set up. Of course, no matter what kind of API operation, it contains some rules:

Add shipping address:

Input parameters:

User id

Shipping address entity information

Constraints:

The user id cannot be empty, and this user does exist

The required fields for the shipping address cannot be empty

If the user does not have a shipping address yet, the shipping address will be set as the default shipping address when it is created —

Delete shipping address:

Input parameters:

User id

Shipping address id

Constraints:

The user id cannot be empty, and this user does exist

The shipping address cannot be empty, and the shipping address does exist

Determine whether this shipping address is the user's shipping address

Determine whether this delivery address is the default delivery address. If it is the default delivery address, it cannot be deleted

Change shipping address:

Input parameters:

User id

Shipping address id

Constraints:

The user id cannot be empty, and this user does exist

The shipping address cannot be empty, and the shipping address does exist

Determine whether this shipping address is the user's shipping address

Default address setting:

Input parameters:

User id

Shipping address id

Constraints:

The user id cannot be empty, and this user does exist

The shipping address cannot be empty, and the shipping address does exist

Determine whether this shipping address is the user's shipping address

Shipping address list query:

Input parameters:

User id

Constraints:

The user id cannot be empty, and this user does exist

Single delivery address query:

Input parameters:

User id

Shipping address id

Constraints:

The user id cannot be empty, and this user does exist

The shipping address cannot be empty, and the shipping address does exist

Determine whether this shipping address is the user's shipping address

Constraint judgment and technology selection

For the constraints and function lists listed above, I selected several typical exception handling scenarios for analysis: adding a shipping address, deleting a shipping address, and obtaining a list of shipping addresses.

So what necessary knowledge reserves should we have? Let’s take a look at the shipping address function:

When adding a shipping address, the user ID and shipping address entity information need to be verified. So how do we choose a tool for non-empty judgment? The traditional judgment is as follows:

/**

* Add address

* @param uid

* @param address

* @return

*/

public Address addAddress(Integer uid,Address address){

if(null != uid){

//Process..

}

return null;

}

In the above example, if you only judge that uid is empty, it will be fine. If you then judge whether some necessary attributes in the address entity are empty, this will be catastrophic when there are many fields.

So how should we make these judgments about entering parameters? Let me introduce two knowledge points to you:

The Preconditions class in Guava implements the judgment of many parameter input methods

JSR 303 validation specification (the current implementation is relatively complete hibernate-validator implemented by hibernate)

If these two recommendation technologies are used, the judgment of entering parameters will become much simpler. It is recommended that everyone use these mature technologies and jar toolkits, which can reduce a lot of unnecessary workload. We just need to focus on business logic. Rather than delaying more time due to these input judgments.

How to design java exceptions elegantly

domain introduction

According to the project scenario, two domain models are required, one is the user entity and the other is the address entity.

Address domain is as follows:

@Entity

@Data

public class Address {

@Id

@GeneratedValue

private Integer id;

private String province;//province

private String city;//city

private String county;//District

private Boolean isDefault;//Is it the default address

@ManyToOne(cascade={CascadeType.ALL})

@JoinColumn(name="uid")

private User user;

}

User domain is as follows:

@Entity

@Data

public class User {

@Id

@GeneratedValue

private Integer id;

private String name;//name

@OneToMany(cascade= CascadeType.ALL,mappedBy="user",fetch = FetchType.LAZY)

private Set

addresses;

}

Ok, the above is a model relationship, and the relationship between user and delivery address is a 1-n relationship. The @Data above uses a tool called lombok, which automatically generates Setter and Getter methods, which is very convenient to use. Interested readers can learn about it by themselves.

dao introduction

For the data connection layer, we use the spring-data-jpa framework, which requires us to only inherit the interface provided by the framework and name the methods according to the convention to complete the database operations we want.

The user database operation is as follows:

@Repository

public interface IUserDao extends JpaRepository {

}

The shipping address operation is as follows:

@Repository

public interface IAddressDao extends JpaRepository {

}

As readers can see, our DAO only needs to inherit JpaRepository, and it has already helped us complete basic CURD and other operations. If you want to know more about the spring-data project, please refer to spring's official documentation. Than not program our research on anomalies.

Service exception design

ok, finally we have reached our focus. We need to complete some operations of the service: add shipping address, delete shipping address, and obtain the shipping address list.

First look at my service interface definition:

public interface IAddressService {

/**

* Create shipping address

* @param uid

* @param address

* @return

*/

Address createAddress(Integer uid,Address address);

/**

* Delete shipping address

* @param uid

* @param aid

*/

void deleteAddress(Integer uid,Integer aid);

/**

* Query all delivery addresses of the user

* @param uid

* @return

*/

List

listAddresses(Integer uid);

}

Let’s focus on the implementation:

Add shipping address

First, let’s take a look at the constraints we compiled earlier:

Input parameters:

User id

Shipping address entity information

Constraints:

The user id cannot be empty, and this user does exist

The required fields for the shipping address cannot be empty

If the user does not have a shipping address yet, the shipping address will be set as the default shipping address when it is created

Let’s first look at the following code implementation:

@Override

public Address createAddress(Integer uid, Address address) {

//============ The following are constraints ==============

//1. The user id cannot be empty, and this user does exist

Preconditions.checkNotNull(uid);

User user = userDao.findOne(uid);

if(null == user){

throw new RuntimeException("Current user not found!");

}

//2. The necessary fields of the delivery address cannot be empty

BeanValidators.validateWithException(validator, address);

//3. If the user does not have a shipping address yet, when the shipping address is created, it is set to the default shipping address

if(ObjectUtils.isEmpty(user.getAddresses())){

address.setIsDefault(true);

}

//============ The following is the normally executed business logic ==============

address.setUser(user);

Address result = addressDao.save(address);

return result;

}

Among them, the three constraints described above have been completed. When the three constraints are satisfied, normal business logic can be carried out, otherwise an exception will be thrown (it is generally recommended to throw a runtime exception - RuntimeException here) .

Introducing the following technologies I used above:

Preconfitions.checkNotNull(T t) is judged using com.google.common.base.Preconditions in Guava. Because a lot of verification is used in the service, it is recommended to change Preconfitions to a static import method:

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

Of course, the instructions in Guava's github also recommend that we use it this way.

BeanValidators.validateWithException(validator, address);

This is done using the jsr 303 specification implemented by hibernate. You need to pass in a validator and an entity that needs to be verified. So how to obtain the validator, as follows:

@Configuration

public class BeanConfigs {

@Bean

public javax.validation.Validator getValidator(){

return new LocalValidatorFactoryBean();

}

}

It will get a Validator object, and then we can inject it in the service and use it:

@Autowired

private Validator validator ;

So how is the BeanValidators class implemented? In fact, the implementation is very simple. Just judge the annotation of jsr 303 and it will be ok.

So where are the jsr 303 annotations written? Of course it is written in the address entity class:

@Entity

@Setter

@Getter

public class Address {

@Id

@GeneratedValue

private Integer id;

@NotNull

private String province;//province

@NotNull

private String city;//city

@NotNull

private String county;//District

private Boolean isDefault = false;//Is it the default address

@ManyToOne(cascade={CascadeType.ALL})

@JoinColumn(name="uid")

private User user;

}

Write the constraints you need to make judgments. If they are reasonable, you can perform business operations and operate the database.

The verification of this piece is necessary. One of the main reasons is that such verification can avoid the insertion of dirty data. If readers have formal online experience, they can understand such a thing. Any code errors can be tolerated and modified, but if a dirty data problem occurs, it may be a devastating disaster. Program problems can be corrected, but the appearance of dirty data may not be recoverable. So this is why you must determine the constraints in the service before performing business logic operations.

The judgment here is a business logic judgment, which is made from a business perspective. In addition, there may be different business condition constraints in many scenarios, and you only need to do it according to the requirements.

The summary of the constraints is as follows:

Basic judgment constraints (basic judgments such as null values)

Entity attribute constraints (meet basic judgments such as jsr 303)

Business condition constraints (different business constraints proposed by requirements)

When these three points are satisfied, you can proceed to the next step

ok, this basically introduces how to make a basic judgment. Let's go back to the issue of exception design. The above code has clearly described how to reasonably judge an exception at the appropriate location. So how to throw an exception reasonably?

Does just throwing a RuntimeException count as throwing an exception gracefully? Of course not. For exceptions thrown in service, I think there are roughly two methods of throwing:

Throws an exception with status code RumtimeException

Throws a RuntimeException of the specified type

Compared with the two exception methods, the first exception means that all my exceptions throw RuntimeException exceptions, but they need to have a status code. The caller can query what kind of thing the service threw based on the status code. exception.

The second type of exception refers to customizing a specified exception error for any exception thrown in the service, and then throwing the exception.

Generally speaking, if the system has no other special requirements, it is recommended to use the second method during development and design. But for example, basic judgment exceptions can be completely operated using the class library provided by guava. JSR 303 exceptions can also be operated using their own encapsulated exception judgment classes, because these two exceptions are basic judgments and there is no need to specify special exceptions for them. But for the exception thrown by the third obligation condition constraint judgment, you need to throw an exception of the specified type.

For

1throw new RuntimeException("Current user not found!");

Define a specific exception class to judge this obligatory exception:

public class NotFindUserException extends RuntimeException {

public NotFindUserException() {

super("This user cannot be found");

}

public NotFindUserException(String message) {

super(message);

}

}

Then change this to:

1throw new NotFindUserException("The current user cannot be found!");

or

1throw new NotFindUserException();

ok, through the above modifications to the service layer, the code changes are as follows:

@Override

public Address createAddress(Integer uid, Address address) {

//============ The following are constraints ==============

//1. The user id cannot be empty, and this user does exist

checkNotNull(uid);

User user = userDao.findOne(uid);

if(null == user){

throw new NotFindUserException("The current user cannot be found!");

}

//2. The necessary fields of the delivery address cannot be empty

BeanValidators.validateWithException(validator, address);

//3. If the user does not have a shipping address yet, when the shipping address is created, it is set to the default shipping address

if(ObjectUtils.isEmpty(user.getAddresses())){

address.setIsDefault(true);

}

//============ The following is the normally executed business logic ==============

address.setUser(user);

Address result = addressDao.save(address);

return result;

}

Such a service seems to be more stable and understandable.

Delete shipping address:

Input parameters:

User id

Shipping address id

Constraints:

The user ID cannot be empty, and this user does exist

The shipping address cannot be empty, and the shipping address does exist

Determine whether this shipping address is the user's shipping address

Determine whether this delivery address is the default delivery address. If it is the default delivery address, it cannot be deleted

It is similar to adding the delivery address above, so I won’t go into details. The service design of delete is as follows: @Override

public void deleteAddress(Integer uid, Integer aid) {

//============ The following are constraints ==============

//1. The user id cannot be empty, and this user does exist

checkNotNull(uid);

User user = userDao.findOne(uid);

if(null == user){

throw new NotFindUserException();

}

//2. The delivery address cannot be empty, and the delivery address does exist

checkNotNull(aid);

Address address = addressDao.findOne(aid);

if(null == address){

throw new NotFindAddressException();

}

//3. Determine whether this delivery address is the user's delivery address

if(!address.getUser().equals(user)){

throw new NotMatchUserAddressException();

}

//4. Determine whether the delivery address is the default delivery address. If it is the default delivery address, it cannot be deleted

if(address.getIsDefault()){

throw new DefaultAddressNotDeleteException();

}

//============ The following is the normally executed business logic ==============

addressDao.delete(address);

}

Four related exception classes are designed: NotFindUserException, NotFindAddressException, NotMatchUserAddressException, DefaultAddressNotDeleteException. Different exceptions are thrown according to different business requirements.

Get the shipping address list:

Input parameters:

User id

Constraints:

The user id cannot be empty, and this user does exist

The code is as follows:@Override

public List

listAddresses(Integer uid) {

//============ The following are constraints ==============

//1. The user id cannot be empty, and this user does exist

checkNotNull(uid);

User user = userDao.findOne(uid);

if(null == user){

throw new NotFindUserException();

}

//============ The following is the normally executed business logic ==============

User result = userDao.findOne(uid);

return result.getAddresses();

}

api exception design

There are roughly two methods of throwing:

Throws an exception with status code RumtimeException

Throws a RuntimeException of the specified type

This was mentioned when designing service layer exceptions. Through the introduction of the service layer, we chose the second throwing method when the service layer throws exceptions. The difference is that when throwing exceptions in the api layer, we need to use There are two ways to throw: specify the type of API exception, and specify the relevant status code, and then throw the exception. The core of this exception design is to allow users who call the API to more clearly understand the exception. In addition to throwing exceptions, we also need to make a corresponding table to display the detailed information of the exception corresponding to the status code and the possible problems that may occur with the exception to the user to facilitate the user's query. (Such as the api documentation provided by github, the api documentation provided by WeChat, etc.), there is another benefit: if the user needs to customize the prompt message, the prompt can be modified based on the returned status code.

api validation constraints

First of all, for the design of the API, a dto object needs to exist. This object is responsible for communicating and transferring data with the caller, and then the dto->domain is passed to the service for operation. This must be paid attention to. Secondly, Point, in addition to the mentioned service needing basic judgment (null judgment) and jsr 303 verification, the api layer also needs to perform related verification. If the verification fails, it will be directly returned to the caller to inform that the call failed and should not be If you access the service with illegal data, readers may be a little confused. If the service has not been verified, why does the API layer still need to be verified? A concept is designed here: Murphy's Law in programming. If the data verification at the API layer is neglected, then illegal data may be brought to the service layer, and then dirty data may be saved to the database.

So the core of rigorous programming is: never believe that the data received is legal.

api exception design

When designing API layer exceptions, as we said above, you need to provide error codes and error messages. Then you can design it like this to provide a general API super class exception. Other different API exceptions inherit from this super class:

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;

/**

*Add shipping address

* @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 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 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提供的一个特殊的切面处理。

When an exception occurs when calling the API interface, the user can also receive the normal data format. For example, when there is no user (uid is 2), but the delivery address is added for this user, postman (Google plugin is used to simulate http requests) The data:

{

"errorCode": 10003,

"tip": "The user cannot be found"

}

The above is the detailed content of How to design elegant Java exceptions?. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete