One of the most important disruptive decisions that Java developers can make is how to use the Java exception model. Java exceptions have always been the targets of many disputes in the community. Someone argues that the exception check in Java is a failed experiment. This article will tell you why the failure is not due to the Java exception model, but because the Java class library designer does not fully understand the two basic causes of method failure.
This article advocates a way of thinking about the nature of abnormal conditions and describes some patterns that help design. Finally, this article will discuss exception handling in the AOP model as a problem of mutual penetration. When you can use exceptions correctly, they will be of great benefit. This article will help you do this.
Why exceptions are so important
Exception Handling in Java applications reveals the framework-based strength to a large extent. Architecture is the decision made and followed at all levels of the application. The most important one is to determine the way the classes, sub-systems, or layers communicate in the application. Java exceptions are the way the Java method communicates alternative execution results, so it deserves special attention in the application architecture.
A good way to measure the Java designer level and development team discipline is to read the exception handling code in their applications. The first thing to note is how much code is used to capture exceptions, write them into the log file, decide what happens, and jump between different exceptions. Clean, simple, and highly correlated exception handling usually indicates that the development team has a stable way to use Java exceptions. When the number of exception handling codes exceeds the number of other codes, you can see that there is a big problem in communication and cooperation between teams (which may not exist at the beginning ), everyone is handling exceptions in their own way.
The handling results of sudden exceptions are foreseeable. If you ask a team member why an exception is thrown, captured, or ignored in a specific code, their answer is usually "I have nothing to do ". If you ask what happens when an exception occurs, they frown. The answer you get is similar to this: "I don't know. We have never tested it ."
You can use the client code to determine whether a java component has effectively exploited java exceptions. If they contain a bunch of logic to figure out when an operation fails, why it fails, and whether there is room for compensation, the cause is likely to be attributed to the component Error Reporting design. The error reporting system generates a large number of "record and forget" code on the client, which is rarely used. The worst is the wring logic, nested try/catch/finally code blocks, and some other chaotic applications that cause vulnerabilities and are difficult to manage.
After the event, we will solve the Java exception problem, or we will not solve it at all, which is the main cause of software project chaos and delay. Exception Handling is a problem that needs to be solved urgently in every part of the design. Setting up an implicit Convention for exception handling is the first decision of the project. The rational use of the Java exception model has a long-term impact on ensuring that your application is simple, easy to maintain, and correct.
Parsing exception
Correct use of the content contained in the Java exception model has always been controversial. Java is not the first language that supports the semantics of exception algorithms. However, it is the first language that uses compilers to execute declarations and handle certain exceptions. Many people think that the exception check during compilation is helpful for accurate software design. Figure 1 shows the Java exception level.
Figure 1: Java exception level
Generally, the Java compiler forces a method that throws exceptions based on java. lang. Throwable to add the exception to the "throws" section in its declaration. In addition, the compiler will also confirm the client method, capture declared exceptions, or specifically declare that it also throws the same exception. These simple rules have a profound impact on Java programmers around the world.
The compiler relaxed the exception check for the two branches in the Throwable inheritance tree. Classes of java. long. Error and java. lang. RuntimeException are not checked during compilation. In these two categories, software engineers are generally more interested in running exceptions. The "do not check" exception refers to this group, so as to distinguish it from all other "check" exceptions.
I can imagine that those who accept the "check" exception will also be very concerned with the Java data type. After all, compiler restrictions on data types encourage strict coding and accurate thinking. The type check during compilation helps reduce serious problems during runtime. The exception check during compilation can also play a similar role, it will remind developers that a method may have unexpected results to be processed.
Early suggestions were to try to use "exception detection" as much as possible to maximize the use of compiler help to write error-free software. Java class library API designers all agree with this, and they widely use "exception detection" to simulate almost all emergency response measures in the class library method. In the J2SE5.1 API specification, the ratio of the "checked exceptions" type is greater than that of the "unchecked exceptions" type.
For programmers, it seems that a large majority of common methods in the Java class library declare "exception of inspection" for every possible failure ". For example, java. io package
IOException is highly dependent on the "exception of inspection. There are at least 63 Java class library packages, or directly, or through a dozen of sub-classes below, this exception is thrown.
I/O failure is extremely rare, but it is very serious. In addition, once this happens, the code you write is basically irreparable. Java programmers realize that they have to provide IOException or similar irreparable events, and calls to a simple Java class library method may cause these events. Capturing these exceptions makes simple code obscure, because even the captured code block is basically not helpful. But it may be worse if you do not capture it, because the compiler requires that your method throw those exceptions. In this way, your implementation rules have to be exposed, and usually good object-oriented design is to hide details.
Such an irretrievable situation has led to a disruptive pattern of the vast majority of notorious Exception Handling issues we warn today. At the same time, it also derives many correct or wrong remedies.
Some well-known figures in the Java field began to question whether Java's "exception in inspection" model was a failed experiment. There are some things that must fail, but this is unrelated to the addition of the exception check in the Java language. Failure is because in the thinking of Java API designers, most failure situations are the same, so the same exception can be conveyed.
Faults and Strain
Let's consider the CheckingAccount class in a hypothetical bank application. A CheckingAcccount is a user that records the user's deposit balance. It can also accept deposits, receive redemption notifications, and process incoming cheques. A CheckingAcccount object must coordinate the access of the synchronization thread, because any thread may change its status. In the CheckingAcccount class, the processCheck method accepts a Check object as the parameter. Generally, the Check amount is subtracted from the account balance. However, when a client program that manages check clearing calls the processCheck method, there must be two possible contingency measures. 1. The CheckingAccount object may have a stop payment command for the check; 2. the account balance may be insufficient to meet the Check amount.
Therefore, the processCheck method can respond to the call from the client in three ways. Normally, the check is processed and the result declared in the method signature is returned to the caller. The response of the two kinds of strain is the actual situation in the banking field that requires communication with the check clearing end. All three results returned by the processCheck method are carefully designed based on the behavior of a typical bank check account.
In Java, a natural method is used to indicate that the above emergency response defines two types of exceptions, such as StopPaymentException (stop payment exception) and InsufficientFundsException (insufficient balance exception ). It is incorrect for a client to ignore these exceptions because these exceptions will be thrown during normal operations. They reflect the full behavior of a method just like the method signature.
The client can easily handle these two exceptions. If the payment for the check is stopped, the client will deliver the check for special handling. If the funds are insufficient, the user can transfer some funds from the user's savings account to the check account, and then try again.
When using the CheckingAccount API, these responses are both predictable and natural results. They do not mean that the software or runtime environment fails. These exceptions are different from the real failures caused by some internal implementation rules in the CheckingAccount class.
Assume that the CheckingAccount object maintains a constant state in the database and uses the jdbc api to access the object. In that API, almost all database access methods may fail for reasons unrelated to the CheckingAccount implementation. For example, someone may forget to run the database server, a network data cable that has not been connected, and the password for accessing the database has changed.
JDBC relies on SQLException, a "Check exception", to report any possible errors. Most errors may occur because of database configuration, connection, or hardware. For the processCheck method, it is not feasible for the above errors. This shouldn't be because processCheck knows at least its own implementation rules. The upstream methods in the call Stack may be less capable of solving these problems.
The CheckingAccount example shows two basic reasons why a method cannot return the expected result. Here are two descriptive terms:
Strain
In line with actual expectations, a method provides another response, which can represent one of the purposes of this method. The caller of this method expects this situation and has a corresponding way to cope with it.
Fault
In the absence of a plan, a method cannot achieve its original intention. This is a situation that is hard to find out without having to resort to the detailed implementation rules of this method.
Using these terms, for the processCheck method, a pay Stop command and an excess extraction are two possible responses. SQLException reflects possible faults. The caller of the processCheck method should be able to provide strain, but it may not be able to effectively handle possible faults.
Java exception matching
When establishing rules for Java exceptions in the application architecture, it is of long-term significance to carefully consider "what may be wrong" in the form of contingency and failure.
Condition
Strain
Fault
Considered
Part of the Design
A bad accident
Expected event
Frequent occurrence
Never happen
Followers
Upstream callers of this method
Persons who need to fix this problem
Another return method is provided.
Program bug, hardware system fault, configuration error, lost file, server not running
Best match
An exception in a check
An unchecked exception
The exception to the Java check is matched appropriately. Because they are an indispensable part of the semantic algorithm contract of the method, it makes sense to use the help of the compiler to ensure that they are resolved. If you find that the compiler insists that the exception must be handled or declared when it is inconvenient, it will bring you some trouble. You will almost certainly need to perform some refactoring in design. This is actually a good thing.
Faults are interesting for developers, but not for the software logic. Experts who use software to "digest the problem" need fault information to solve the problem. Therefore, an unchecked exception is a good way to indicate a fault. They let fault notifications remain intact from all the methods on the call stack, arrive at a specific place to capture them, and get information they contain that is conducive to diagnosis, provide a modest and elegant conclusion to the entire event. Methods that generate faults do not need to be declared (abnormal), upstream call methods do not need to capture them, and the Implementation Rules of the methods are correctly hidden-with the lowest Code complexity.
Some new Java APIs, such as the Spring architecture and the Java Data Ojects class library, have almost no dependency on the checked exceptions. In version 3.0, the Hibernate ORM architecture redefined some key functions to eliminate the use of checked exceptions. This means that the vast majority of Exceptions reported in these architectures are irrecoverable, and the fault is caused by incorrect method call code or failure of underlying components such as database servers. Specifically, forcing a caller to capture or declare these exceptions has almost no benefit.
Fault Handling in Design
In your plan, admit that you need to do the first step to effectively handle the fault. It is not easy for engineers who believe they can write impeccable software. Here are some helpful ways of thinking. First, if errors are found, the application development time will be long. Of course, the premise is that the programmer fixes his own bugs. Second, in Java class libraries, excessive use of checked exceptions to handle faults will force your code to handle faults, even if your call order is correct. If there is no fault handling architecture, the combined Exception Handling will cause information loss in the application.
A successful Fault Handling architecture must meet the following objectives:
Reduce Code complexity
Capture and save diagnostic information
Alert to appropriate persons
Exit gracefully
Faults are the interference of the true intention of the application. Therefore, the code used to process them should be as few as possible. Ideally, they should be separated from the semantic algorithm part of the application. Fault Handling must meet the needs of those responsible for correcting them. Developers need to know the fault and get information that helps them find out why it happened. Even if a fault is defined as irreparable, good troubleshooting will try to gracefully end the activity that causes the fault.
Exceptions not checked for use of faults
When making a decision on the framework, there are many reasons for the failure to be represented by unchecked exceptions. The Java Runtime Environment throws a subclass of "RunTime exception" for code errors, such as ArithmeticException or ClassCastException. This sets a precedent for your framework. Unchecked exceptions make it unnecessary for upstream calling methods to add code for situations unrelated to their purposes, thus reducing confusion.
Your troubleshooting policy should recognize that Java class library methods and other APIs may use check exceptions to indicate that your application may only be faulty. In this case, the design conventions are used to capture API exceptions, treat them as faults, and throw an unchecked exception to indicate the fault situation and capture diagnostic information.
In this case, the specific exception type thrown should be defined by your framework. Do not forget that the main purpose of a fault exception is to pass the recorded diagnostic information so that people can come up with the cause of the error. Using multiple fault exception types may be somewhat different, because your architecture treats them equally. In most cases, it is sufficient to embed a single fault type with good descriptive information. It is easy to use Java's basic RuntimeException to represent a fault. As of Java1.4, RuntimeException supports exception nesting like other throws, so that you can capture and report exceptions for fault-oriented checks.
You may define your own unchecked exceptions for the purpose of the fault report. This may be necessary. If you use Java1.3 or earlier versions, they do not support abnormal nesting. Implementing a similar nested function to capture and convert exceptions that constitute failures in your application is very easy. Your application may need a special behavior when reporting an error. This may be another reason for creating a RuntimeException subclass in the architecture.
Build a fault barrier
For your fault handling architecture, it is important to decide what kind of exceptions to throw and when to throw them. It is equally important to know when to catch a fault exception and what to do later. The purpose here is to make the functional part of your application do not need to handle faults. Separating problems from each other is usually a good thing. In the long run, it is very helpful to have a central fault handling mechanism.
In the fault barrier mode, any application component can throw a fault exception, but only the component as a "fault barrier" can catch the exception. This mode removes the complex code that most programmers insert to handle faults locally. The fault barrier is logically located at the upper layer of the Call Stack. In this way, before a default action is triggered, an abnormal report is blocked. The default action varies depending on the application type. For an independent Java application, this action indicates that the active thread is stopped. For a Web application located on an application server, this action means that the application server sends unfriendly (or even embarrassing) Responses to the browser.
The top priority of a fault barrier component is to record the information contained in the fault exception for future use. So far, an application log has become the first choice for this matter. Abnormal nested information, stack logs, and so on are all valuable information for diagnosis. The worst part of passing fault information is through the user interface. It is not good for you to roll application users into the process of checking errors. If you really want to put the diagnosis information on the user interface, it may mean that your log policy needs to be improved.
The next task of the fault barrier is to end the operation in a controllable way. The specific significance of this depends on the design of your application, but it usually includes generating a universal response to the Client that may be waiting. If your application is a Web service, this means that a soap fault Element <fault> is created using the <faultcode> and Common Failure Information <faultstring> of the SOAP: Server in the response. If your application is used for browser communication, this barrier will arrange a common HTML response to indicate that the demand cannot be processed.
In a Struts application, your fault barrier will appear in the form of a global exception processor and be configured as any subclass for processing RuntimeException. Your fault barrier class will extend to the org. apache. struts. action. ExceptionHandler class. If necessary, rewrite its method to implement your own special handling. In this way, the system will handle the unexpected faults and the faults found when processing a Struts action. Figure 2 shows strain and fault exceptions.
Figure 2 strain and fault exceptions
If you are using the Spring MVC Architecture, You can inherit the SimpleMappingExceptionResolver class and configure it to process RuntimeException and its sub-classes. This makes it easy to build a fault barrier. By Rewriting the resolveException method, you can add the user-oriented processing you need before using the parent class method to direct the requirement to a view component that sends a general error message.
When your architecture contains a fault barrier and programmers know it, the impulse to write a one-time failure exception will be sharply reduced. The result is that the application contains code that is cleaner and easier to maintain.
Response Processing in architecture
After the Fault Handling and barrier are handed over, the strain communication between the main components becomes much easier. One response represents another method result that is equally important to the primary returned results. Therefore, the exception type of the check is a tool that can well pass the existence of the contingency situation and provide the necessary information to compete with it. With the help of the Java compiler, this method reminds programmers about all aspects of their APIs and the necessity to provide a full set of method outputs.
It is possible to pass simple strain only by using the return value type of the method. For example, if an empty reference is returned instead of a specific object, it means that the object cannot be created for a defined reason. The Java I/O method usually returns an integer-1 instead of the byte value or the number of bytes to indicate the end of the file. If the semantics of your method is as simple as allowed, another method that returns a value can be used, because it discards the extra cost caused by exceptions. The disadvantage is that the method caller should check the returned value to determine whether it is the main result or the response result. However, the compiler cannot ensure that the method caller uses this judgment.
If a method has a return type of void, an exception is a unique method to indicate that the response has occurred. If a method returns an object reference, the returned value can only be null or non-null ). If a method returns an integer type and does not conflict with the primary return value, it is possible to indicate a number in multiple situations. But in this way, we enter the world of error code check, and this formal Java exception pattern is avoided.
Provide some useful information
Defining the exception types of different fault reports makes no sense, because the fault barrier treats all Exception types equally. Strain exceptions are very different, because their original intention is to pass various situations to method callers. Your architecture may point out that these exceptions should inherit java. lang. Exception or a specified base class.
Don't forget that your exception should be of the Java type. You can use it to store special fields, methods, and even constructors for your special purpose service. For example, the InsufficientFundsException thrown by the hypothetical processCheck () method should contain an OverdraftProtection object, which can transfer the shortage of funds from another account.
Log or not
It is useful to record fault exceptions, because logs are designed to attract attention when correction is required. But this is not the case for strain exceptions. Abnormal strain may represent only a few situations, but in your application, every situation will happen. They mean that your application is working properly as originally designed. Adding the log code to the capture block will make your code obscure without practical advantages. If a strain represents an important event, a log is generated before an exception strain is thrown to alert the caller. Recording this event may make this method better.
Exception
In Aspect Oriented Programming (AOP) Terminology, fault and strain handling are mutually penetrating issues. For example, to implement the fault barrier model, all involved classes must follow the general specification:
The fault barrier method must survive at the front end of the method calling diagram of the involved classes.
The participating classes must use unchecked exceptions to indicate faults.
Involved classes must use the fault barrier to expect targeted types of exceptions not checked
The involved classes must capture and convert the failures destined for the execution situation from the low-end Method to the exceptions of the check.
Participating in the process that cannot interfere with the transfer of a fault exception to the fault barrier
These problems go beyond the boundaries of irrelevant classes. The result is a few scattered Fault Handling codes, as well as the implied coupling between the Barrier class and the involved class (this is much better than the unused mode !). AOP encapsulates Fault Handling issues in a general sense and can be applied to the level of the involved classes. Java AOP architecture such as AspectJ and Spring AOP considers Exception Handling as the starting point for adding fault handling actions. In this way, the model of binding participants to the fault barrier can be relaxed. Fault Handling can survive in an independent and irrelevant aspect, thus eliminating the need to put the barrier method at the forefront of the method activation sequence.
If you use AOP in your architecture, fault and strain handling are ideal candidates for use in applications. A complete exploration of fault and strain handling in the use of the AOP architecture will be an interesting topic in the future.
Conclusion
Although the Java exception model has aroused heated discussions since its appearance, if it is used correctly, it is of great value. As a designer, your task is to build specifications to make the best use of this model. Failure and strain can help you make the right decisions. Properly using the Java exception model can make your application simple, easy to maintain, and correct. AOP technology positions faults and strain as problems of mutual penetration. This method may help your architecture.
Author: ERDP Technical Architecture"