My official account is MarkerHub and my website is Markerhub.com
For more selected articles, please click: Java Notes Complete.md
Small Hub read:
Using Guava’s Preconditions, Hibernate’s Hibernate-Validator, as well as the logic of how to handle exceptions and handle exceptions, this article is a bit long, but I still gain a lot after reading.
- Source: lrwinx
- lrwinx.github.io/
Introduction:
Exception processing is one of the essential operations in the program development, but how to correctly and elegantly handle the exception is really a knowledge, the author according to their own development experience to talk about how I am to deal with the exception.
Since this article is only about experience and does not cover the basics, if the reader is still confused about the concept of exceptions, please check the basics first.
How do I choose an exception type
Categories of exceptions
As we all know, The exception superclass in Java is java.lang.throwable, which has two more important subclasses, Java.lang. Exception(Exception) and java.lang.Error(Error) Errors, such as outofMemoryErrors, are managed by the JVM, so we will not focus on errors in this article, so we will talk about exceptions in detail.
Exception has an important subclass called RuntimeException. We call RuntimeException, or other subclasses that inherit from RuntimeException, unchecked Exception, Other subclasses that inherit from Exception are called Checked Exceptions. This article focuses on two types of anomalies, detected and undetected.
How to select exceptions
From the author’s development experience, if in an application, you need to develop a method (such as a function of the service method), the method may appear if the exception, so you need to consider whether the anomaly appeared after the caller can handle, and if you want to call for processing, if the caller can handle, And if you want the caller to handle it, throw checked exceptions, reminding the caller to use your method to consider handling if it throws an exception.
Similarly, if you’re writing a method and you think it’s an accidental exception, in theory, you think the runtime might run into some kind of problem that might not necessarily happen or require the caller to display the exception to determine the operation of the business process, You can then use a non-checked exception such as RuntimeException.
Well, I guess I said above this paragraph, you read many times also still feel obscure.
So, please follow my train of thought, slowly understand.
When do I need to throw an exception
The first thing we need to know is when do we need to throw an exception? The design of exceptions is convenient for developers to use, but it is not indiscriminate. The author also asked a lot of friends about when to throw exceptions, but few of them can give an accurate answer. The problem is simple: if you feel that some “problem” can’t be solved, you can throw an exception.
For example, if you’re writing a service and you’re writing some code that might cause problems, throw an exception, and trust me, this is the best time to throw an exception.
What kind of exception should be thrown
Now that we know when we need to throw an exception, we can ask ourselves, when we do throw an exception, what kind of exception should we use? Is it a checked exception or an unchecked exception?
Let me illustrate this problem by starting with the checked exception. For example, there is a business logic that needs to read some data from a file. This read operation may be due to the file being deleted or other problems that result in a read error. Get this data from redis or mysql database, see the following code, getKey(Integer) is the entry program.
public String getKey(Integer key){ String value; try { InputStream inputStream = getFiles("/file/nofile"); // Then read the value of the key from the stream. ; } catch (Exception e) {// If an Exception is thrown, 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 ", LLDB); } return inputStream; }Copy the code
Ok, after looking at the above code, you may have some ideas in mind, originally checked exceptions can control the obligation logic, yes, yes, through checked exceptions can really control the business logic, but remember not to use this, we should reasonably throw exceptions, because the program itself is the process, Abnormal function is only when you do not find an excuse, it does not as a control program flow entry or exit, if such use, is a function of the abnormal enlargement, this will lead to increased code complexity, coupling increases, reduce problems such as code readability.
So should you never use such an exception? In fact, it is not, when there is really such a need, we can use it, just remember, do not really use it as a tool or means to control the process. So when exactly do you throw an exception like this? We will only consider using checked exceptions if the caller needs to handle the error if the call goes wrong.
Now, let’s take a look at runtimeExceptions, which we see a lot, Such as Java. Lang. NullPointerException/Java. Lang. IllegalArgumentException, etc., so this exception when we throw?
Throw a RuntimeException when we accidentally encounter an error while writing a method that we think could happen at runtime, and theoretically, without this problem, the program would execute normally. It does not force the caller to catch the exception.
For example, when passing a path, we need to return the File object corresponding to the path:
public void test() { myTest.getFiles(""); {if} public File getFiles (String path) (null = = path | | "" equals (path)) {throw new NullPointerException (" path cannot be empty!" ); } File file = new File(path); return file; }Copy the code
As the above example shows, if path is empty when the caller calls getFiles(String), then the null pointer exception (which is a subclass of RuntimeException) is thrown. The caller does not have to explicitly try… The catch… Operation to force processing. This requires the caller to validate before calling such a method to avoid runtimeExceptions. As follows:
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 (" path cannot be empty!" ); } File file = new File(path); return file; }Copy the code
Which exception should be selected
From the above description and examples, it can be concluded that the difference between **RuntimeException and checked exception is: Whether it is mandatory for the caller to handle this exception, if it is mandatory, then use checked exception, otherwise select unchecked exception. ** In general, if there are no special requirements, we recommend using RuntimeException.
Scenario introduction and technology selection
Architectural description
As we all know, traditional projects are developed on the basis of the MVC framework. This article focuses on the use of restful interface design to experience the elegance of exception handling.
Let’s focus on the restful API layer (similar to the Controller layer on the Web) and the Service layer to see how exceptions are thrown in the Service and then caught and translated by the API layer.
The technologies used are: Spring-boot, JPA (Hibernate),mysql. If you are not familiar with these technologies, you need to read the relevant materials by yourself.
Service Scenario Description
Choose a relatively simple business scenario and take the receiving address management in e-commerce as an example. When users purchase goods on mobile terminals, they need to manage the receiving address. In the project, some API interfaces for mobile terminals to access are provided, such as: Add shipping address, delete shipping address, change shipping address, default shipping address setting, shipping address list query, single shipping address query interface.
Construction constraints
Ok, this is a very basic business scenario set up, of course, no matter what the API operation, it contains some rules:
Add shipping address:
-
The user id
-
Receiving address entity information
Constraints:
-
The user ID cannot be empty and does exist
-
The required fields of the shipping address cannot be empty
-
If the user does not already have a shipping address, set this shipping address to the default when it is created –
Delete shipping address:
-
The user id
-
Shipping address ID
Constraints:
-
The user ID cannot be empty and does exist
-
The shipping address cannot be empty and does exist
-
Determine if this shipping address is the user’s shipping address
-
Check whether this shipping address is the default shipping address. If it is the default shipping address, you cannot delete it
Change of shipping address:
-
The user id
-
Shipping address ID
Constraints:
-
The user ID cannot be empty and does exist
-
The shipping address cannot be empty and does exist
-
Determine if this shipping address is the user’s shipping address
Default address setting: Input parameter:
-
The user id
-
Shipping address ID
Constraints:
-
The user ID cannot be empty and does exist
-
The shipping address cannot be empty and does exist
-
Determine if this shipping address is the user’s shipping address
Shipping address list query: Input parameter:
- The user id
Constraints:
- The user ID cannot be empty and does exist
Single delivery address query: input parameter:
-
The user id
-
Shipping address ID
Constraints:
-
The user ID cannot be empty and does exist
-
The shipping address cannot be empty and does exist
-
Determine if this shipping address is the user’s shipping address
Constraint judgment and technology selection
For the list of constraints and functions listed above, I chose a few typical exception handling scenarios: add a receiving address, delete a receiving address, and get a list of receiving addresses.
Let’s take a look at the shipping address function:
The user ID and entity information of the receiving address need to be checked when adding the receiving address. So how do we select the tool for non-empty judgment? The conventional judgment is as follows:
/** * addAddress * @param uid * @param address * @return */ public address addAddress(Integer uid, address address){if(null! = uid){// Process.. } return null; }Copy the code
In the example above, it would be fine if uid were null, but if some necessary attributes in the address entity were null, it would be disastrous if there were many fields.
So how should we do the judgment of these input parameters? Let’s introduce two points of knowledge:
-
The Preconditions class in Guava implements many of the input method judgments
-
Validation specification for JSR 303
If you use these two recommendation techniques, the input parameter judgment becomes much easier. ** recommends the use of these mature technologies and JAR toolkits, which can reduce a lot of unnecessary work. **** We just need to focus on the business logic. ** will not delay any more time because of these input judgment.
How to elegantly design JAV exceptions
Domain is introduced
Depending on the project scenario, you need two domain models, one for the user entity and one for the address entity.
Address domain:
@Entity @Data public class Address { @Id @GeneratedValue private Integer id; private String province; // private String City; // city private String county; // private Boolean isDefault; @manyToOne (cascade={cascadeType.all}) @joinColumn () private User User; }Copy the code
User domains are 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<Address> addresses; }Copy the code
Ok, this is a model relationship. The user-shipping address relationship is 1-n. The @data feature uses a tool called Lombok, which automatically generates setters and getters and other methods.
The dao is introduced
Data connection layer, we use the spring-data-JPA framework, which requires that we only need to inherit the interface provided by the framework, and according to the convention to name the method, we can complete the database operations we want.
User database operations are as follows:
@Repository
public interface IUserDao extends JpaRepository<User,Integer> {
}
Copy the code
The delivery address operation is as follows:
@Repository
public interface IAddressDao extends JpaRepository<Address,Integer> {
}
Copy the code
As you can see, our DAO only needs to inherit from JpaRepository, which already does the basic CURD operations for us. If you want to learn more about the spring-Data project, please refer to the spring documentation. It does not scheme our study of anomalies.
Service Exception Design
Ok, here we go. We need to complete the parts of service: add shipping address, delete shipping address, and get the list of shipping addresses.
Let’s start with my service interface definition:
Public interface IAddressService {/** * Create a delivery address * @param uid * @param address * @return */ address createAddress(Integer) uid,Address address); /** * void deleteAddress(Integer uid,Integer aid); /** * query all user's shipping addresses * @param uid * @return */ List<Address> listAddresses(Integer uid); }Copy the code
Let’s focus on the implementation:
Add a shipping address
First of all, let’s take a look at the previously sorted constraints:
The arguments:
-
The user id
-
Receiving address entity information
Constraints:
-
The user ID cannot be empty and does exist
-
The required fields of the shipping address cannot be empty
-
If the user does not already have a shipping address, set this shipping address to the default when it is created
Let’s start with the following code implementation:
@ Override public Address createAddress (Integer uid, Address the Address) {/ / = = = = = = = = = = = = as constraint conditions under the = = = = = = = = = = = = = = / / 1. User id cannot be empty, and the user exists Preconditions. CheckNotNull (uid); User user = userDao.findOne(uid); If (null == user){throw new RuntimeException(" Cannot find current user!" ); } / / 2. The shipping address of the necessary fields can't be empty BeanValidators. ValidateWithException (validator, address); If (objectutils.isempty (user.getaddresses ())){address.setisdefault (true); if(objectutils.isempty (user.getaddresses ())){address. } / / = = = = = = = = = = = = here is normal execution of business logic = = = = = = = = = = = = = = address. SetUser (user); Address result = addressDao.save(address); return result; }Copy the code
When the three constraints described above are met, normal business logic can proceed; otherwise, an exception (RuntimeException is generally recommended here) will be thrown.
Here are some of the techniques I used:
1, Preconfitions. CheckNotNull (T T) this is the use of Guava the com.google.com mon. Base. The Preconditions for judgment, because of the large used in the validation of the service, Therefore, you are advised to change Preconfitions to static import mode:
import static com.google.common.base.Preconditions.checkNotNull;
Copy the code
Of course, Guava’s Github notes also suggest this.
2, BeanValidators. ValidateWithException (validator, address); This is done using hibernate’s JSR 303 specification, which requires passing in a Validator and an entity to validate.
@Configuration public class BeanConfigs { @Bean public javax.validation.Validator getValidator(){ return new LocalValidatorFactoryBean(); }}Copy the code
It will get a Validator object, which we can inject into the service and use:
@Autowired
private Validator validator ;
Copy the code
So how is the BeanValidators class implemented? In fact, the implementation is very simple, just to judge the ANNOTATIONS of JSR 303.
So where are the annotations for JSR 303? In the address entity class:
@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; }Copy the code
Write down the constraints you need to judge, and if it makes sense, then you can do business, and then operate on the database.
This validation is necessary for one major reason: it avoids the insertion of dirty data.
If you have any formal experience with the game, you will understand that any code errors can be tolerated and fixed, but if there is a dirty data problem, it can be a devastating disaster. ** program problems can be fixed, but the presence of dirty data may not be recoverable. This is why it is important to determine the constraints before performing business logic operations in a service.
The judgment here is business logic judgment, which is filtered from the business perspective. In addition, there may be different business conditions in many scenarios, so you just need to follow the requirements.
The constraints are summarized as follows:
-
Basic judgment constraints (null values, etc.)
-
Entity attribute constraints (satisfy basic judgments such as JSR 303)
-
Business constraints (different business constraints on requirements)
When the three points are met, you can proceed to the next step
Ok, basically introduced how to make a basic judgment, so back to the exception design problem, the above code has clearly described how to determine an exception in the appropriate place, so how to throw an exception reasonably?
Is throwing only RuntimeException considered elegant? Of course not. There are roughly two ways to throw an exception in a service:
-
Throw RumtimeException with status code
-
Throws a RuntimeException of the specified type
In contrast to these two exceptions, the first exception means that all my exceptions throw runtimeExceptions, but with a status code, the caller can use the status code to see what kind of exception the service threw.
The second type of exception is to define a specified exception error for any exception thrown in the service, and then throw the exception.
Generally speaking, if the system has no other special requirements, in the development design, it is recommended to use the second method. But exceptions like basic judgment, for example, can be manipulated entirely using the library that Guava provides us. JSR 303 exceptions can also be operated on using their own wrapped exception judgment classes, because both exceptions are base judgments and no special exceptions need to be specified for them. However, for the third obligation condition to determine the exception to be thrown, it is necessary to throw an exception of the specified type.
for
Throw new RuntimeException(" Current user cannot be found!" );Copy the code
Define a specific exception class for this obligatory exception determination:
Public Class NotFindUserException extends RuntimeException {public NotFindUserException() {super(" This user cannot be found "); } public NotFindUserException(String message) { super(message); }}Copy the code
Then change this to:
Throw new NotFindUserException(" Can't find current user!" ); or throw new NotFindUserException();Copy the code
Ok, with the above changes to the service layer, the code changes as follows:
@ Override public Address createAddress (Integer uid, Address the Address) {/ / = = = = = = = = = = = = as constraint conditions under the = = = = = = = = = = = = = = / / 1. The user ID cannot be empty and the user really exists checkNotNull(UID); User user = userDao.findOne(uid); If (null == user){throw new NotFindUserException(" Cannot find current user!" ); } / / 2. The shipping address of the necessary fields can't be empty BeanValidators. ValidateWithException (validator, address); If (objectutils.isempty (user.getaddresses ())){address.setisdefault (true); if(objectutils.isempty (user.getaddresses ())){address. } / / = = = = = = = = = = = = here is normal execution of business logic = = = = = = = = = = = = = = address. SetUser (user); Address result = addressDao.save(address); return result; }Copy the code
This makes the service seem more stable and understandable.
Delete shipping address:
The arguments:
-
The user id
-
Shipping address ID
Constraints:
-
The user ID cannot be empty and does exist
-
The shipping address cannot be empty and does exist
-
Determine if this shipping address is the user’s shipping address
-
Check whether this shipping address is the default shipping address. If it is the default shipping address, you cannot delete it
The service design of delete is as follows:
@ Override public void deleteAddress (Integer uid, Integer aid) {/ / = = = = = = = = = = = = as constraint conditions under the = = = = = = = = = = = = = = / / 1. The user ID cannot be empty and the user really exists checkNotNull(UID); User user = userDao.findOne(uid); if(null == user){ throw new NotFindUserException(); } //2. CheckNotNull (aid) exists; Address address = addressDao.findOne(aid); if(null == address){ throw new NotFindAddressException(); } //3. Check whether this shipping address is the user's shipping address if(! address.getUser().equals(user)){ throw new NotMatchUserAddressException(); } / / 4. Determine whether the shipping address as the default shipping address, if this is the default shipping address, so can't be deleted if (address) getIsDefault ()) {throw new DefaultAddressNotDeleteException (); } / / = = = = = = = = = = = = here is normal execution of business logic = = = = = = = = = = = = = = addressDao. Delete (address); }Copy the code
Four related exception classes are designed: NotFindUserException,NotFindAddressException,NotMatchUserAddressException,DefaultAddressNotDeleteException. Different exceptions are thrown based on different business requirements.
To get a list of shipping addresses:
- The user id
Constraints:
- The user ID cannot be empty and does exist
The code is as follows:
@ Override public List < Address > listAddresses (Integer uid) {/ / = = = = = = = = = = = = as constraint conditions under the = = = = = = = = = = = = = = / / 1. The user ID cannot be empty and the user really exists checkNotNull(UID); User user = userDao.findOne(uid); if(null == user){ throw new NotFindUserException(); } / / = = = = = = = = = = = = here is normal execution of business logic = = = = = = = = = = = = = = User result. = userDao findOne (uid); return result.getAddresses(); }Copy the code
API exception Design
There are roughly two ways to throw:
-
Throw RumtimeException with status code
-
Throws a RuntimeException of the specified type
This is mentioned in the design of the service layer exception. Through the introduction of the service layer exception, we choose the second way to throw the exception. The difference is that we need to use the two ways to throw the exception in the API layer. To specify the type of API exception, and to specify the relevant status code, and then throw the exception, the core of this exception design is to let the users of the API call more clearly understand the details of the exception.
In addition to throwing exceptions, we also need to make a corresponding table to display the details of the exceptions corresponding to the status code and the possible problems of the exceptions to the users, which is convenient for users to query. (such as API documents provided by Github, API documents provided by wechat, etc.), there is another advantage: if the user needs to customize the prompt message, the prompt can be modified according to the returned status code.
API validation constraints
First of all, for the design of the API, there needs to be a DTO object, which is responsible for the communication and transmission of data to the caller, and then the DTO ->domain in the service to operate, this must be noted.
In addition to null validation and JSR 303 validation, the API layer also needs to perform relevant validation. If the validation fails, the API layer directly returns to the caller and tells the caller that the call failed. You should never attempt to access a service with illegal data.
The reader may wonder why the API layer needs validation when the Service already does it. The concept here is murphy’s Law of programming: if the API layer fails to validate data, it is possible that illegal data will be brought to the service layer, which in turn saves dirty data to the database.
So the core of careful programming is this: Never believe that the data you receive is legitimate.
API exception Design
When designing API layer exceptions, as we mentioned above, we need to provide error codes and error messages, so we can design to provide a generic API superclass exception from which all other different API exceptions inherit:
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; }}Copy the code
Then define API layer exceptions separately: ApiDefaultAddressNotDeleteException,ApiNotFindAddressException,ApiNotFindUserException,ApiNotMatchUserAddressException For example, the default address cannot be deleted:
public class ApiDefaultAddressNotDeleteException extends ApiException { public ApiDefaultAddressNotDeleteException(String message) { super(AddressErrorCode.DefaultAddressNotDeleteErrorCode, message, null); }}Copy the code
AddressErrorCode. DefaultAddressNotDeleteErrorCode is the need to provide the error code to the caller. Error codes are as follows:
public abstract class AddressErrorCode { public static final Long DefaultAddressNotDeleteErrorCode = 10001L; Public static Final Long NotFindAddressErrorCode = 10002L; Public static Final Long NotFindUserErrorCode = 10003L; / / can't find this user public static final Long NotMatchUserAddressErrorCode = 10004 l; // User does not match shipping address}Copy the code
The AddressErrorCode error class stores possible error codes, and it makes more sense to manage them in a configuration file.
API handling exception
The API layer will call the Service layer and handle any exceptions that occur in the service. First, make sure that the API layer is very light and basically a forwarding function (interface parameters, pass parameters to the service, Return data to the caller, the three basic functions), and then do exception handling on the method call passed to the service parameter.
Here is an example for adding an IP address:
@Autowired private IAddressService addressService; /** * add addressDTO * @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(" not found "); }catch (Exception e){throw new ApiException(e); } AddressDTO resultDTO = new AddressDTO(); BeanUtils.copyProperties(result,resultDTO); resultDTO.setUid(result.getUser().getId()); return resultDTO; }Copy the code
The solution here is to call the service, determine the type of exception, and then convert any service exception into an API exception, and then throw an API exception. This is a common method of exception conversion. Similarly deleting the shipping address and obtaining the shipping address are similarly treated, which will not be described here.
API exception conversion
Now that we’ve explained how to throw an exception and how to convert a service exception into an API exception, does that complete exception handling? The answer is no, when thrown API abnormal, we need to put the API data of abnormal return (json or XML) allow the user to understand, you need to convert API exception to dto objects (ErrorDTO), see the following code:
@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 exception ApiException = (ApiException)exception; errorDTO.setErrorCode(apiException.getErrorCode()); }else{// Unknown exception 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; }}Copy the code
Ok, now that the API exception has been converted into a DTO object that the user can read, the code uses @ControllerAdvice, a special aspect processing provided by Spring MVC.
The user can also receive the data in the normal format when an exception occurs when calling the API, such as adding a shipping address to a user when there is no user (uid = 2) :
{"errorCode": 10003, "tip": "user not found"}Copy the code
conclusion
In this paper, only from how to design exceptions as the focus to explain, involving API transmission and service processing, still need to be optimized, such as API interface access needs to use HTTPS encryption, API interface needs OAuth2.0 authorization or API interface needs signature authentication and other issues. The focus of this article is on how exceptions are handled, so the reader only needs to focus on the issues related to exceptions and how they are handled.
Recommended reading
Java Notes Complete.md
Great, this Java site, everything! https://markerhub.com
The UP master of this B station, speaks Java really good!
Too great! The latest edition of Java programming ideas can be viewed online!