Concern public number: IT elder brother, read a dry goods technical article every day, a year later you will find a different self

Exceptions are a part of software development, and in my case, I spend at least half of my time handling exceptions, so there are a lot of try {… } catch {… } finally {… } code blocks, not only have a lot of redundant code, but also affect 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 block:

Elegant Controller:

The example above, which is still at the Controller layer, might have more try catch blocks if it were at the Service layer. This can seriously affect the readability and “aesthetics” of your code.

So if it were me, I would definitely prefer the second option, where I can focus more on business code development and the code will be more concise.

Since the business code does not explicitly catch and handle exceptions, and exceptions certainly do, the system will crash all the time, so they must be caught and handled somewhere else.

So the question is, how do you handle exceptions gracefully?

What is unified exception handling

Spring added an annotation @ControllerAdvice in version 3.2 that works with @ExceptionHandler, @initBinder, @ModelAttribute, and other annotations.

I don’t want to go into too much detail here, but if you don’t know anything about these annotations, you can refer to the new annotation @controllerAdvice in Spring 3.2 for a general idea.

The only annotation associated with exception handling is @exceptionhandler, which literally means ExceptionHandler.

** If you define an exception handling method in a Controller class and add the annotation to the method, the exception handling method will be executed when the specified exception occurs.

It can use data binding provided by SpringMVC, such as injecting HttpServletRequest and so on, and can accept a Throwable object that is currently thrown.

However, you would have to define a set of such exception handling methods in each Controller class, because exceptions can be different. This would result in a lot of redundant code, and if you needed to add a new exception handling logic, you would have to change all the Controller classes, which would be inelegant.

Of course, you might say, well, let’s just define a base class like BaseController, and that’s it.

This is true, but it’s not perfect, because such code is intrusive and coupled. A simple Controller, why would I want to inherit such a class, if I already inherit from another base class? Everyone knows that Java can only inherit one class.

Is there a way to apply defined exception handlers to all controllers without coupling them to controllers?

So here comes the @ControllerAdvice annotation, which, in short, applies exception handlers to all controllers, not just a single controller.

** With this annotation, we can implement: ** Define a mechanism for handling various exceptions in a separate place, such as a single class, and then add @controllerAdvice to the class’s signature to handle the different exceptions at different stages. This is how unified exception handling works.

Note that the above exceptions are classified by phase, which can be roughly divided into: exceptions before entering Controller and exceptions at the Service layer.

For details, please refer to the following figure:

Anomalies at different stages

The target

Eliminate 95% or more try catch blocks, validate business exceptions in an elegant Assert style, focus on business logic, and don’t spend a lot of effort writing redundant try Catch blocks.

Unified exception handling practice

Before defining a unified exception handling class, let’s look at how to gracefully determine an exception and throw it.

| replace throw exception with Assert (claim)

Must Assert (claim) are familiar, such as Spring family org. Springframework. Util. Assert, when we write test cases are often used.

Using assertions gives us an unusually silky feel when coding, such as:

@Test public void test1() { ... User user = userDao.selectById(userId); Assert.notNull(user, "User does not exist."); . } @test public void test2() {User User = userdao.selectById (userId); If (user == null) {throw new IllegalArgumentException(" User does not exist."); }}Copy the code

Do you feel that the first non-empty rule is elegant, and the second rule is relatively ugly if {… } code block. So what’s behind the magical assert.notnull ()?

Here’s some of the Assert source code:

public abstract class Assert { public Assert() { } public static void notNull(@Nullable Object object, String message) { if (object == null) { throw new IllegalArgumentException(message); }}}Copy the code

As you can see, Assert is basically a way of putting an if {… } encapsulated, isn’t it amazing? Simple as it is, there is no denying that the coding experience has at least improved a notch.

So we can’t imitate org. Springframework. Util. Assert, also write an assertion class, but an assertion fails after the exception thrown not IllegalArgumentException these built-in exceptions, but our own custom exception.

Let’s give it a try:

Assertpublic Interface Assert {/** * Create an exception * @param args * @return */ BaseException newException(Object... args); @param t * @param args * @return */ BaseException newException(Throwable t, Object... args); /** * <p> Asserts that the object <code>obj</code> is not null. If the object <code>obj</code> is empty, Default void assertNotNull(Object obj) {if (obj == null) {throw newException(obj); }} /** * <p> Asserts that the object <code>obj</code> is not empty. If the object <code>obj</code> is empty, an exception is thrown * < P > Exception message< code>message</code> supports passing parameters. Avoid string concatenation before judgment * * @param obj Object * @param args Message placeholder parameter list */ default void assertNotNull(Object obj, Object... args) { if (obj == null) { throw newException(args); }}}Copy the code

The Assert Assert method is defined using the default method of the interface. Notice that when the Assert fails, instead of a specific exception, the two newException interface methods provide the exception.

For example, if the query result is null, the thrown exception may be UserNotFoundException with a specific exception code (for example, 7001) and the exception message “User does not exist”.

So it’s up to Assert’s implementation class to decide exactly what exceptions to throw.

If you look at this, you might wonder if the above statement requires equal assertion classes and exception classes for as many exceptions as possible, which is obviously anti-human and not as smart as you might think. Don’t worry, just listen to me.

| considerate Enum

Custom BaseException has two attributes: code and message. Do you have any classes that define these two attributes?

That’s right, enumeration classes. See how I combine Enum and Assert, and I’m sure I’ll blow your mind.

As follows:

public interface IResponseEnum { int getCode(); String getMessage(); }/** * <p> Service exception </p> * <p> An exception occurs during service processing. </p> */public class BusinessException extends BaseException {private static Final Long serialVersionUID = 1; 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@AllArgsConstructorpublic 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; }Copy the code

The code example defines two instances of enumerations:

  • BAD_LICENCE_TYPE

  • LICENCE_NOT_FOUND

They correspond to BadLicenceTypeException and LicenceNotFoundException respectively.

Instead of defining an exception class for each exception, you can simply add an enumeration instance. Then let’s see how to use it. Suppose LicenceService has a method to check whether Licence exists as follows:

/** * Check {@link Licence} exists * @param Licence */ private void checkNotNull(Licence) { ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence); }Copy the code

Without assertions, the code might look like this:

private void checkNotNull(Licence licence) { if (licence == null) { throw new LicenceNotFoundException(); // throw new BusinessException(7001, "Bad Licence type."); }}Copy the code

Using enumerated classes in combination with Assert, you can throw specific exceptions (with specific exception codes and exception messages) just by defining different enumeration instances for a particular exception case, such as BAD_LICENCE_TYPE and LICENCE_NOT_FOUND above.

This way, you don’t have to define a lot of exception classes, but you also have good readability of assertions. Read on to find out more.

Note: The above example is specific to a particular business, but some exceptions are general. For example: Server busy, network exception, server exception, parameter verification exception, 404, etc., so CommonResponseEnum, ArgumentResponseEnum, ServletResponseEnum The ServletResponseEnum is explained later.

| defined unified exception handler class

@Slf4j@Component@ControllerAdvice@ConditionalOnWebApplication@ConditionalOnMissingBean(UnifiedExceptionHandler.class)pub Lic class UnifiedExceptionHandler {/** * production environment */ private final static String ENV_PROD = "prod"; @Autowired private UnifiedMessageSource unifiedMessageSource; /** * current environment */ @value ("${spring.profile.active}") private String profile; 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; } /** * BusinessException ** @param exception * @return exception result */ @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 exception * @return exception */ @ExceptionHandler(value = baseException.class) @responseBody public ErrorResponse handleBaseException(BaseException e) { log.error(e.getMessage(), e); return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e)); } / Controller on a layer of the * * * * * @ param e anomaly * @ * / @ ExceptionHandler abnormal return results ({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()); } the if (ENV_PROD equals (profile)) {/ / when to production environment, not suitable for the specific exception information display to the user, 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 exception * @return exception */ @ExceptionHandler(value = BindException ErrorResponse handleBindException(BindException e) {log.error(" Parameter binding check exception ", e); return wrapperBindingResult(e.getBindingResult()); } /** * Will check all the abnormal failure combined into an error message @ param e * * * * @ return anomaly results / @ ExceptionHandler (value = MethodArgumentNotValidException. Class) @ ResponseBody public ErrorResponse handleValidException (MethodArgumentNotValidException e) {log. The error (" parameter binding validation exception ", e); return wrapperBindingResult(e.getBindingResult()); } /** * wrap binding exception result ** @param bindingResult bindingResult * @return exception result */ 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)); } /** * @exceptionHandler (value = exception.class) @responseBody public ErrorResponse handleException(Exception e) { log.error(e.getMessage(), e); If (env_prod. equals(profile)) {if (env_prod. equals(profile)) {if (env_prod. equals(profile)) { 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()); }}Copy the code

As you can see, there are only two main categories: ServletException and ServiceException.

Remember the classification by stage mentioned above, which corresponds to exceptions before entering Controller and exceptions at the Service layer; ServiceException is then divided into custom exceptions and unknown exceptions.

The corresponding relationship is as follows:

  • Exceptions before entering Controller: handleServletException, handleBindException, handleValidException

  • Custom exceptions include handleBusinessException and handleBaseException

  • Unknown exception: handleException

These exception handlers are described in detail.

Exception Handler Description

| handleServletException

An HTTP request performs a series of checks between the request information and the target Controller information before reaching the Controller.

Here is a brief description:

**NoHandlerFoundException: ** If the request Url does not contain a controller, the exception will be thrown.

HttpRequestMethodNotSupportedException: * * * * if the match to the (matching result is a list of different is the HTTP methods, such as: Get, Post, etc.), attempts to match the HTTP method of the request with the controller of the list. If there is no controller corresponding to the HTTP method, the exception is thrown.

HttpMediaTypeNotSupportedException: * * * * and then head for the request and the support of the controller.

For example, a Content-Type request header will throw an exception if the controller’s parameter signature contains the @RequestBody annotation, but the request’s Content-Type header value does not contain Application/JSON (of course, this exception will not be thrown in this case).

MissingPathVariableException: * * * * path parameters was detected. For example, the URL is /licence/{licenceId} and the parameter signature contains @pathvariable (“licenceId”).

If the requested URL is /licence and the url is not clearly defined as /licence, it will be judged that the path parameter is missing.

MissingServletRequestParameterException: * * * * lack of request parameters. For example, if the @requestParam (“licenceId”) String licenceId parameter is defined but not carried when the request is initiated, the exception will be thrown.

**TypeMismatchException: ** Parameter type matching failed. For example, if a Long is received but the value is a string, the conversion fails and the exception is thrown.

HttpMessageNotReadableException: * * * * with the opposite of HttpMediaTypeNotSupportedException examples above.

The request header contains “Content-Type: application/json; Charset = utf-8 “, but the @requestbody annotation was not added to the received argument, or the json string carried by the RequestBody failed to be serialized into a pojo.

HttpMessageNotWritableException: * * * * return pojo during serialization into a json failed, then throw the exception.

| handleBindException

Parameter verification is abnormal.

| handleValidException

Parameter verification is abnormal.

| handleBusinessException, handleBaseException

Handles custom business exceptions, except that handleBaseException handles all business exceptions except BusinessException. For now, these two can be combined into one.

| handleException

Handle all unknown exceptions, such as database failure exceptions.

Note: HandleServletException and handleException may return different exception messages in different environments. It is thought that these exception messages are the exception messages of the framework, which are generally in English, so it is not easy to show them to users directly. Therefore, all exception information represented by SERVER_ERROR is returned.

404 is unusual

As mentioned above, NoHandlerFoundException is thrown if the request does not match the controller, but this is not the case by default. By default, a page similar to the following appears:

Whitelabel Error Page

How did this page come about? In fact, when a 404 occurs, the default is not to throw an exception, but instead to jump forward to the /error controller.

Spring also provides a default error controller, as follows:

To get 404 to throw an exception, add the following configuration to the properties file:

spring.mvc.throw-exception-if-no-handler-found=truespring.resources.add-mappings=false
Copy the code

This way, it can be caught in the exception handler, and the front end can jump to the 404 page as soon as it catches the specific status code.

Catch the exception corresponding to 404

| unified returns the result

Before we validate the unified exception handler, we should mention the unified return result. In plain English, it’s about unifying the data structures that return the results.

Code and message are mandatory fields in all returned results. When data needs to be returned, another field data is required to represent it.

So first define a BaseResponse as the base class for all returned results; We then define a generic return result class, CommonResponse, that inherits from BaseResponse and has additional fields, data.

To distinguish between success and failure returns, we define an ErrorResponse.

Finally, a common return result is that the data is returned with paging information. Because this interface is common, it is necessary to define a separate return result class, QueryDataResponse.

This class inherits from CommonResponse, but restricts the data field type to QueryDdata. QueryDdata defines the corresponding page-information fields, namely totalCount, pageNo, pageSize, and Records.

CommonResponse and QueryDataResponse are only commonly used, but their names are very long, so why not define two classes with very simple names instead?

So R and QR are born, and when you return the result, you just say: new R<>(data), new QR<>(queryData).

The definition of all return result classes is not posted here.

| verify unified exception handling

Because this uniform set of exception handling is so generic, it can be designed as a common package that each new project/module will introduce later. So to verify, you need to create a new project and import the common package.

Here is the main source for validation:

@Servicepublic class LicenceService extends ServiceImpl<LicenceMapper, Licence> { @Autowired private OrganizationClient organizationClient; /** * query {@ 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); } /** * public QueryData< simple elicencedto > getdate forum (licensing forum param licenceParam) { String licenceType = licenceParam.getLicenceType(); LicenceTypeEnum licenceTypeEnum = LicenceTypeEnum.parseOfNullable(licenceType); // assert, non-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); } /** * New {@link Licence} * @param Request body * @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) {// omitted} /** * Verify {@link Licence} exists * @param Licence */ private void checkNotNull(Licence) { ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence); }}Copy the code

PS: The DAO framework used here is Mybatis – Plus.

At startup, the data automatically inserted is:

-- licenceINSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, Tokens tokens 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, the 'user', 'suitability - plus', 200189); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated)VALUES (3, 2, the '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); -- organizationsINSERT INTO organization (id, name, contact_name, contact_email, contact_phone)VALUES (1, 'customer-crm-co', 'Mark Balster', '[email protected]', '823-555-1212'); INSERT INTO organization (id, name, contact_name, contact_email, contact_phone)VALUES (2, 'HR-PowerSuite', 'Doug Drewry','[email protected]', '920-555-1212');Copy the code

To verify

| capture custom exception

(1) for there is no licence for details: http://localhost:10000/licence/5

Request successfully responded to:

licenceId=1
Copy the code

Test non-empty:

License not found:

Licence not found:

(2) according to there is no licence type get licence list: http://localhost:10000/licence/list? LicenceType = DDD. The available licence types are user and core-prod.

Check non-null:

Catch a Bad Licence type exception:

Bad licence type:

| capture anomalies of the Controller

* * (1) access does not exist interface: * * http://localhost:10000/licence/list/ddd

Catching a 404 exception:

* * (2) does not support HTTP method: * * http://localhost:10000/licence

PostMapping:

Request Method not supported

Request Method not supported:

* * (3) check exception 1: * * http://localhost:10000/licence/list? licenceType=

GetLicences:

LicenceParam:

Catch parameter binding check exception:

Licence type cannot be empty:

**④ Check exception 2: ** POST request, which is simulated by Postman

AddLicence:

LicenceAddRequest:

The request URL is the result:

Catch parameter binding check exception:

** Note: ** Because the way of obtaining the exception information of the parameter binding check exception is different from other exceptions, the exceptions in these two cases are separated from the exceptions before entering the Controller. The following is the collection logic of the exception information.

Collecting exception information:

| catch exceptions is unknown

Suppose we casually to Licence now add a field test, but not change the database table structure, and then visit: http://localhost:10000/licence/1.

Add the test field:

Catching a database exception:

The Error querying the database:

summary

As you can see, the test exceptions can be caught and returned as code or message.

Instead of defining many exception classes, each project/module only needs to define an enumeration class when defining a business exception, implement the interface BusinessExceptionAssert, and finally define an enumeration instance for each business exception. It is also convenient to use, similar to assertions.

extension

In the production environment, if an unknown exception or a ServletException is caught, it would be unprofessional to show the user a long list of exception information. Instead, we can return “network exception” when the current environment is detected as a production environment.

Network exception is returned in the production environment:

You can modify the current environment in the following ways:

conclusion


Using a combination of assertions and enumerated classes, combined with uniform exception handling, almost all exceptions can be caught.

The reason for most exceptions is that after the introduction of Spring Cloud Security, there will also be authentication/authorization exceptions, gateway service degradation exceptions, cross-module invocation exceptions, remote invocation of third-party services exceptions, etc. The capture methods of these exceptions are different from those described in this paper, but due to space limitations, there is no detailed explanation here. A separate article will be published later.

In addition, when internationalization needs to be considered, the exception information captured after the exception generally cannot be returned directly and needs to be converted into the corresponding language. However, this article has taken this into account, and the internationalization mapping has been done when the message is obtained. The logic is as follows:

Getting internationalization messages

Finally, the global exception belongs to the old topic, I hope this project through the mobile phone for you to have a little guidance to learn, we modify according to the actual situation.

You can also use the following jsonResult object for processing and post code as well:

@ Slf4j @ RestControllerAdvicepublic class GlobalExceptionHandler {login / * * * not * @ 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 login invalid or login timeout, please login first "); return jsonResult; } /** * ServiceException * @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(" Service exception, please contact 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 administrator "); return jsonResult; }}Copy the code