background
During the software development process, it is inevitable to handle all kinds of exceptions. For me, at least half of the time is spent dealing with all kinds of exceptions, so there will be a lot of try {...} catch {...} finally {...} code blocks in the code, which not only has a lot of redundant code, but also affects the readability of the code.
Compare the two figures below to see which style of code you are writing now?Then which coding style do you prefer?
Ugly try catch code block
Elegant Controller
The example above is only in the Controller layer, and if it is in the Service layer, there may be more try catch blocks.This will seriously affect the readability and "aesthetics" of the code.
So if it were me, I would certainly prefer the second one. I could concentrate more on the development of business code, and the code would be more concise.
Since business code does not explicitly capture and handle exceptions, and exceptions are certainly handled, otherwise the system does not crash at all, they must be caught and handled elsewhere.
So the question arises, how do you handle exceptions gracefully?
What is Unified Exception Handling
Spring added a comment @ControllerAdvice in version 3.2 that can be used with annotations such as @ExceptionHandler, @InitBinder, @ModelAttribute.
The role of these notes is not covered here. If you don't know anything about them, you can refer to the new Spring 3.2 note @ControllerAdvice to get a general idea.Or follow the WeChat Public Number: Java technology stack, reply in the background: spring, you can get the N Spring tutorials I organized, they are all dry goods.
However, only the comment @ExceptionHandler is associated with exception handling. Literally, it means exception handler. It also serves the purpose of defining an exception handling method in a Controller class and adding the comment to the method. When a specified exception occurs, the method for handling the exception is executed, which can use the data binding provided by springmvc.For example, injecting HttpServletRequest, and so on, can also accept a Throwable object that is currently thrown.
However, in this way, you must define a set of such exception handling methods for each Controller class, since exceptions can be varied.This results in a lot of redundant code, and if you need to add an exception handling logic, you must modify all the Controller classes, which is not elegant.
You might say, of course, that defines a base class like BaseController, and that's all.
That's true, but it's not perfect because such code is intrusive and coupled.Simple Controller, why should I inherit such a class in case I already inherit other base classes?It is well known that Java can inherit only one class.
Is there a way to apply the defined exception handler to all controllers without coupling with the Controller?So the annotation @ControllerAdvice appears, which simply applies an exception handler to all controllers, not to a single controller.
With this annotation, we can achieve: in a separate place, such as a single class, define a set of mechanisms to handle various exceptions, and then add the annotation @ControllerAdvice to the class signature to handle different stages of exceptions.This is the principle of uniform exception handling.
Notice that the above outliers are categorized by stages and can be roughly divided into: exceptions before entering Controller and Service layer exceptions, referring specifically to the following figure:
Exceptions at different stages
target
Eliminate more than 95% of try catch code blocks, verify business exceptions in an elegant Assert way, and focus solely on business logic without spending much effort writing redundant try catch code blocks.
Unified exception handling practice
Before defining a uniform exception handling class, let's talk about how to elegantly determine exceptions and throw them.
Replace throw exception with Assert
Assert must be familiar to everyone, such as the Spring familyOrg.springframework.utilAssert, which is often used when writing test cases, gives us an unusual silky feel when coding with assertions, such as:
Does it feel elegant to write the first judgment that it is not empty and ugly to write the second if {...} code block?So amazingAssert.notNullWhat is going on behind it?Here are some of the sources for Assert:
As you can see, Assert actually helps us encapsulate if {...} for a while. Isn't it amazing?While simple, it's undeniable that the coding experience has improved at least one notch.So can we imitate itOrg.springframework.utilAssert also writes an assertion class, but the exception thrown after the assertion fails is not IllegalArgumentException built-in exceptions, but our own defined exceptions.Let's try it now.
The Assert assertion method above is defined using the default method of the interface, and then whether you find that when the assertion fails, the exception thrown is not a specific exception, but is provided by two newException interface methods.
Because exceptions in business logic are basically corresponding to specific scenarios, such as getting user information based on user id and querying result is null, exceptions thrown at this time may be UserNotFoundException, with specific exception codes (such as 7001) and exception information "User does not exist".So what exception is thrown is determined by the implementation class of Assert.
Seeing this, you may have the question whether, according to the above statement, there are not so many anomalies that we have to define an equal number of assertion classes and anomaly classes. This is obviously anti-human, which is not superb.Take your time and listen to me in detail.
A friendly Enum
Custom exception BaseException has two attributes, code and message. If you have two attributes, do you think any classes will define these two attributes in general?Yes, it's an enumeration class.And see how I combine Enum with Assert, I'm sure I'll make you see.The following:
public interface IResponseEnum { int getCode(); String getMessage(); } /** * <p>Business Exception </p> * <p>An exception occurred during business processing and could be thrown </p> */ public class BusinessException extends BaseException { private static final long serialVersionUID = 1L; public BusinessException(IResponseEnum responseEnum, Object[] args, String message) { super(responseEnum, args, message); } public BusinessException(IResponseEnum responseEnum, Object[] args, String message, Throwable cause) { super(responseEnum, args, message, cause); } } public interface BusinessExceptionAssert extends IResponseEnum, Assert { @Override default BaseException newException(Object... args) { String msg = MessageFormat.format(this.getMessage(), args); return new BusinessException(this, args, msg); } @Override default BaseException newException(Throwable t, Object... args) { String msg = MessageFormat.format(this.getMessage(), args); return new BusinessException(this, args, msg, t); } } @Getter @AllArgsConstructor public enum ResponseEnum implements BusinessExceptionAssert { /** * Bad licence type */ BAD_LICENCE_TYPE(7001, "Bad licence type."), /** * Licence not found */ LICENCE_NOT_FOUND(7002, "Licence not found.") ; /** * Return code */ private int code; /** * Return message */ private String message; }
Seeing if there's a light in your eyes, the code sample defines two enumeration instances: BAD_LICENCE_TYPE, LICENCE_NOT_FOUND, which corresponds to two exceptions, BadLicenceTypeException and LicenceNotFoundException.
For each exception added in the future, simply add an enumeration instance, and no longer define an exception class for each exception.Then let's see how to use it, assuming that LicenceService has a way to check if Licence exists, as follows:
Without an assertion, the code might be as follows:
Using enumeration classes to bind (inherit) Assert s, you only need to define different enumeration instances based on specific exceptions, such as BAD_aboveLICENCE_TYPE, LICENCE_NOT_FOUND allows you to throw specific exceptions for different situations (in this case, carry specific exception codes and exception messages) without defining a large number of exception classes and with good readability of assertions, although the benefits of this scheme go beyond these, so read on and learn from them.
Note: The examples above are specific to a particular business, and some exceptions are common, such as server busy, network exceptions, server exceptions, parameter checking exceptions, 404, etc. So there are CommonResponseEnum, ArgumentResponseEnum, ServletResponseEnum, where ServletResponseEnum is described in more detail later.
Define Unified Exception Handler Class
@Slf4j @Component @ControllerAdvice @ConditionalOnWebApplication @ConditionalOnMissingBean(UnifiedExceptionHandler.class) public class UnifiedExceptionHandler { /** * production environment */ private final static String ENV_PROD = "prod"; @Autowired private UnifiedMessageSource unifiedMessageSource; /** * Current environment */ @Value("${spring.profiles.active}") private String profile; /** * Get International Messages * * @param e abnormal * @return */ public String getMessage(BaseException e) { String code = "response." + e.getResponseEnum().toString(); String message = unifiedMessageSource.getMessage(code, e.getArgs()); if (message == null || message.isEmpty()) { return e.getMessage(); } return message; } /** * Business Exceptions * * @param e abnormal * @return Exceptional results */ @ExceptionHandler(value = BusinessException.class) @ResponseBody public ErrorResponse handleBusinessException(BaseException e) { log.error(e.getMessage(), e); return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e)); } /** * Custom Exception * * @param e abnormal * @return Exceptional results */ @ExceptionHandler(value = BaseException.class) @ResponseBody public ErrorResponse handleBaseException(BaseException e) { log.error(e.getMessage(), e); return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e)); } /** * Controller Above related exception * * @param e abnormal * @return Exceptional results */ @ExceptionHandler({ NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, // BindException.class, // MethodArgumentNotValidException.class HttpMediaTypeNotAcceptableException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, MissingServletRequestPartException.class, AsyncRequestTimeoutException.class }) @ResponseBody public ErrorResponse handleServletException(Exception e) { log.error(e.getMessage(), e); int code = CommonResponseEnum.SERVER_ERROR.getCode(); try { ServletResponseEnum servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName()); code = servletExceptionEnum.getCode(); } catch (IllegalArgumentException e1) { log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName()); } if (ENV_PROD.equals(profile)) { //When in a production environment, it is not appropriate to present specific exception information to users, such as 404. code = CommonResponseEnum.SERVER_ERROR.getCode(); BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR); String message = getMessage(baseException); return new ErrorResponse(code, message); } return new ErrorResponse(code, e.getMessage()); } /** * Parameter Binding Exception * * @param e abnormal * @return Exceptional results */ @ExceptionHandler(value = BindException.class) @ResponseBody public ErrorResponse handleBindException(BindException e) { log.error("Parameter Binding Check Exception", e); return wrapperBindingResult(e.getBindingResult()); } /** * A parameter check exception that combines all the exceptions that failed the check into one error message * * @param e abnormal * @return Exceptional results */ @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody public ErrorResponse handleValidException(MethodArgumentNotValidException e) { log.error("Parameter Binding Check Exception", e); return wrapperBindingResult(e.getBindingResult()); } /** * Wrap Binding Exception Results * * @param bindingResult Binding Result * @return Exceptional results */ private ErrorResponse wrapperBindingResult(BindingResult bindingResult) { StringBuilder msg = new StringBuilder(); for (ObjectError error : bindingResult.getAllErrors()) { msg.append(", "); if (error instanceof FieldError) { msg.append(((FieldError) error).getField()).append(": "); } msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage()); } return new ErrorResponse(ArgumentResponseEnum.VALID_ERROR.getCode(), msg.substring(2)); } /** * No exception defined * * @param e abnormal * @return Exceptional results */ @ExceptionHandler(value = Exception.class) @ResponseBody public ErrorResponse handleException(Exception e) { log.error(e.getMessage(), e); if (ENV_PROD.equals(profile)) { //When in a production environment, it is not appropriate to present specific exception information to users, such as database exception information. int code = CommonResponseEnum.SERVER_ERROR.getCode(); BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR); String message = getMessage(baseException); return new ErrorResponse(code, message); } return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage()); } }
As you can see, there are actually only two categories of exceptions, ServletException and ServiceException. Remember that the above mentioned staged exceptions correspond to exceptions before entering Controller and Service layer exceptions, and ServiceException is then divided into custom exceptions and unknown exceptions.The corresponding relationships are as follows:
Exceptions before entering Controller: handleServletException, handleBindException, handleValidException
Custom exceptions: handleBusinessException, handleBaseException
Unknown exception: handleException
These exception handlers are described in detail next.
Exception Processor Description
handleServletException
An http request performs a series of checks on the request information and the target controller information before reaching the Controller.Here's a quick summary:
NoHandlerFoundException: First find out if there is a corresponding controller based on the request Url, and if not, throw the exception, which is a very familiar 404 exception.
HttpRequestMethodNotSupportedException: If it is matched (the result is a list, but the http methods are different, such as Get, Post, etc.), then try to match the requested http method to the controller of the list, and throw the exception if there is no controller corresponding to the http method;
HttpMediaTypeNotSupportedException: Then compare the request header with the controller's support, such as content-type request header, if the controller's parameter signature contains the comment @RequestBody, but the request's content-type request header value does not contain application/json, the exception will be thrown (of course, more than that);
MissingPathVariableException: No path parameters detected.For example, if the url is: /licence/{licenceId}, the parameter signature contains @PathVariable("licenceId"), the request url is/licence, and if the url is not explicitly defined as/licence, it will be judged as missing the path parameter;
Missing ServletRequestParameterException: Missing request parameter.For example, if the parameter @RequestParam("licenceId") String licenceId is defined and the request is made without the parameter, the exception is thrown;
TypeMismatchException: Parameter type matching failed.For example, if the receive parameter is Long type, but the value passed in is a string, the type conversion will fail and the exception will be thrown.
HttpMessageNotReadableException: In contrast to the above example of HttpMediaTypeNotSupportedException, where the request header carries "content-type: application/json;charset=UTF-8" but the receive parameter does not add a comment @RequestBody, or the request body carries a JSON string that has failed to deserialize into a pojo, the exception is thrown;
HttpMessageNotWritableException: The returned pojo fails in the serialization to json process, then throws the exception;
Recommended reading: Can you stand up to more than 10 questions about serialization?
handleBindException
The parameter check exception is described in more detail later.
handleValidException
The parameter check exception is described in more detail later.
handleBusinessException,handleBaseException
Handles custom business exceptions, except that handleBaseException handles all business exceptions except BusinessException.At present, these two can be merged into one.
handleException
Handle all unknown exceptions, such as those that failed to operate on the database.
Note: The handleServletException and handleException processors above may return different exception information in different environments, because these exception information are the exception information that comes with the framework, which is usually in English and not good for direct display to users, so they return SERVER_uniformly.Exception information represented by ERROR.
404 different from normal people
As mentioned above, a NoHandlerFoundException exception is thrown when the request does not match the controller, but it is not by default. By default, a page like the following appears:
Whitelabel Error Page
How does this page appear?In fact, when 404 occurs, the default is not abnormal, but forward jumps to the / error controller. spring also provides the default error controller, as follows:
So, how do you get 404 to throw an exception as well, simply add the following configuration to the properties file:
spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false
This allows it to be captured in the exception handler, and the front end can jump to page 404 as soon as it captures a specific status code.
Catch exception corresponding to 404
Unified Return Results
Before validating the Unified Exception Processor, by the way, Unified Return results.To put it plainly, it's really a unified data structure that returns results.code, message are required fields in all returned results, and when data needs to be returned, another field, data, is needed to represent it.
So first define a BaseResponse as the base class for all returned results;
Then define a generic return result class, CommonResponse, which inherits BaseResponse and has more fields, data;
To distinguish between success and failure returns, define an ErrorResponse
Finally, there is a common return result that returns data with paging information, because this interface is common, so it is necessary to define a separate return result class, QueryDataResponse, which inherits from CommonResponse, but restricts the type of data field to QueryDdata, which defines the corresponding fields for paging information, namely totalCount, pageNo, pageNoPageSize, records.
CommonResponse and QueryDataResponse are commonly used, but the name of the thief is too long. Why not define two super simple classes instead?So R and QR were born, and you can return the results later by simply writing: new R<> (data), new QR<> (queryData).
All definitions of return result classes are not pasted here
Verify Unified Exception Handling
Because this set of uniform exception handling can be said to be generic, a common package can be designed so that each new project/module can be introduced in the future.For validation, a new project needs to be created and the common package introduced.
Main Code
The following are the main sources for validation:
@Service public class LicenceService extends ServiceImpl<LicenceMapper, Licence> { @Autowired private OrganizationClient organizationClient; /** * Query {@link Licence} details * @param licenceId * @return */ public LicenceDTO queryDetail(Long licenceId) { Licence licence = this.getById(licenceId); checkNotNull(licence); OrganizationDTO org = ClientUtil.execute(() -> organizationClient.getOrganization(licence.getOrganizationId())); return toLicenceDTO(licence, org); } /** * Paging Get * @param licenceParam Paging Query Parameters * @return */ public QueryData<SimpleLicenceDTO> getLicences(LicenceParam licenceParam) { String licenceType = licenceParam.getLicenceType(); LicenceTypeEnum licenceTypeEnum = LicenceTypeEnum.parseOfNullable(licenceType); //Assertion, not empty ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(licenceTypeEnum); LambdaQueryWrapper<Licence> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Licence::getLicenceType, licenceType); IPage<Licence> page = this.page(new QueryPage<>(licenceParam), wrapper); return new QueryData<>(page, this::toSimpleLicenceDTO); } /** * Add {@link Licence} * @param request Requestor * @return */ @Transactional(rollbackFor = Throwable.class) public LicenceAddRespData addLicence(LicenceAddRequest request) { Licence licence = new Licence(); licence.setOrganizationId(request.getOrganizationId()); licence.setLicenceType(request.getLicenceType()); licence.setProductName(request.getProductName()); licence.setLicenceMax(request.getLicenceMax()); licence.setLicenceAllocated(request.getLicenceAllocated()); licence.setComment(request.getComment()); this.save(licence); return new LicenceAddRespData(licence.getLicenceId()); } /** * entity -> simple dto * @param licence {@link Licence} entity * @return {@link SimpleLicenceDTO} */ private SimpleLicenceDTO toSimpleLicenceDTO(Licence licence) { //Omit } /** * entity -> dto * @param licence {@link Licence} entity * @param org {@link OrganizationDTO} * @return {@link LicenceDTO} */ private LicenceDTO toLicenceDTO(Licence licence, OrganizationDTO org) { //Omit } /** * Verify {@link Licence} exists * @param licence */ private void checkNotNull(Licence licence) { ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence); } }
PS: The DAO framework used here is mybatis-plus.At startup, the automatically inserted data is:
-- licence INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated) VALUES (1, 1, 'user','CustomerPro', 100,5); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated) VALUES (2, 1, 'user','suitability-plus', 200,189); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated) VALUES (3, 2, 'user','HR-PowerSuite', 100,4); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated) VALUES (4, 2, 'core-prod','WildCat Application Gateway', 16,16); -- organizations INSERT INTO organization (id, name, contact_name, contact_email, contact_phone) VALUES (1, 'customer-crm-co', 'Mark Balster', 'mark.balster@custcrmco.com', '823-555-1212'); INSERT INTO organization (id, name, contact_name, contact_email, contact_phone) VALUES (2, 'HR-PowerSuite', 'Doug Drewry','doug.drewry@hr.com', '920-555-1212');
Start Validation
Capture custom exceptions
1. Get details of licence that does not exist:http://localhost:10000/licence/5.Requests that responded successfully: licenceId=1
Inspection not empty
Catch Licence not found ation exception
Licence not found
2. Obtain a licence list based on a licence type that does not exist:http://localhost:10000/licence/list?licenceType=ddd.Optional licence types are user, core-prod.
Check not empty
Catch Bad licence type exception
Bad licence type
Catch exceptions before entering Controller
1. Access interfaces that do not exist:http://localhost:10000/licence/list/ddd
Catch 404 Exception
2. The HTTP method does not support:http://localhost:10000/licence
PostMapping
Catch Request method not supported exception
Request method not supported
3. Check exception 1:http://localhost:10000/licence/list?licenceType=
getLicences
LicenceParam
Capture parameter binding check exceptions
licence type cannot be empty
4. Check exception 2: post request, using postman emulation here.
addLicence
LicenceAddRequest
Request url is result
Capture parameter binding check exceptions
Note: Because exception information for parameter binding check exceptions is obtained in a different way than other exceptions, exceptions in these two cases are separated from exceptions before entering Controller. Here is the logic for collecting exception information:
Collection of exception information
Catch unknown exception
Suppose we now freely add a new field, test, to Licence without modifying the database table structure, and then access:http://localhost:10000/licence/1.
Recommended reading: 10 best practices for exception handling
Add test field
Catch database exceptions
Error querying database
Summary
You can see that the test exception can be caught and returned as code, message.For each project/module, when defining a business exception, simply define an enumeration class, then implement the BusinessExceptionAssert interface, and finally define the corresponding enumeration instance for each business exception, without defining many exception classes.It is also convenient to use, similar to assertions.
extend
In a production environment, if an unknown exception or a ServletException is caught, because it is a long list of exceptional information, which is not professional enough to show directly to users, we can do this: when the current environment is detected as a production environment, then go back to "network exception" directly.
Production environment returns Network Exception
The current environment can be modified in the following ways:
Modify the current environment to a production environment
summary
With the combination of assertions and enumeration classes, combined with uniform exception handling, most exceptions can be caught.
Why do we say most exceptions, because when spring cloud security is introduced, there will be authentication/authorization exceptions, service degradation exceptions for gateways, cross-module call exceptions, remote call of third-party service exceptions, etc. These exceptions are captured in a different way than those described here, but they are not explained in detail here, and will be described in a separate article later.
In addition, when internationalization needs to be considered, exception information captured after an exception cannot be returned directly and needs to be converted to the corresponding language. However, this paper has taken this into account, and the internationalization mapping has been made when getting the message, the logic is as follows:
Get International Messages
Finally, the global exception belongs to the old topic, I hope that this project through the mobile phone will provide some guidance for everyone to learn.We will modify it according to the actual situation.
You can also use the following jsonResult object to process it and paste out the code.
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * not logged on * @param request * @param response * @param e * @return */ @ExceptionHandler(NoLoginException.class) public Object noLoginExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e) { log.error("[GlobalExceptionHandler][noLoginExceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.NO_LOGIN); jsonResult.setMessage("User logon failure or logon timeout,Please login first"); return jsonResult; } /** * Business Exceptions * @param request * @param response * @param e * @return */ @ExceptionHandler(ServiceException.class) public Object businessExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e) { log.error("[GlobalExceptionHandler][businessExceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.FAILURE); jsonResult.setMessage("Business Exceptions,Please contact your administrator"); return jsonResult; } /** * Global exception handling * @param request * @param response * @param e * @return */ @ExceptionHandler(Exception.class) public Object exceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e) { log.error("[GlobalExceptionHandler][exceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.FAILURE); jsonResult.setMessage("System Error,Please contact your administrator"); return jsonResult; } }