background

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 blockThe elegance of the 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

In version 3.2, Spring added an annotation @ControllerAdvice, which can be used with @ExceptionHandler, @initBinder, @ModelAttribute and other annotations. If you don’t know, please refer to the new annotation @controlleradvice in Spring3.2 for a general idea.

ExceptionHandler, which literally means ExceptionHandler, does what it does: If you define an exception handling method on a Controller class and add the annotation to the method, the exception handling method will be executed when the specified exception occurs, using data binding provided by SpringMVC, such as injecting HttpServletRequest, etc. You can also 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 define a mechanism for handling different exceptions in a separate place, such as a single class, and then annotate the class’s signature with @ControllerAdvice to handle different exceptions at different stages. This is how unified exception handling works.

Notice that the above exceptions are classified by stage, which can be roughly divided into: exceptions before entering Controller and exceptions at 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

Must Assert (claim) are familiar, such as Spring family org. Springframework. Util. Assert, when we write test cases are often used, use assertions can let’s code is a kind of general silky feeling, 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 {… } encapsulate, 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.

Assert

Public 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 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 @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; }Copy the code

The code example defines two instances of enumerations:

BAD_LICENCE_TYPE and LICENCE_NOT_FOUND 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); } 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 specific exception cases, such as BAD_LICENCE_TYPE and LICENCE_NOT_FOUND. 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.

Define a unified exception handler class

@Slf4j @Component @ControllerAdvice @ConditionalOnWebApplication @ ConditionalOnMissingBean (UnifiedExceptionHandler. Class) public class UnifiedExceptionHandler {/ * * * * / private production environment 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 types of exceptions: ServletException and ServiceException. Remember the classification by stage mentioned above, that is, the exceptions before entering the Controller and the 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 in 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: If the controller’s parameter signature contains the @requestBody annotation, but the request’s Content-Type header does not contain application/ JSON, the exception will be thrown (of course, This is not the only case where the exception is thrown);

  • MissingPathVariableException: path parameters was detected. For example, if the URL is /licence/{licenceId} and the parameter signature contains @pathvariable (“licenceId”), if the requested URL is /licence, if the url is not clearly defined as /licence, it will be judged as missing path parameters.

  • 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: The parameter type fails to match. For example, if the received parameter is Long, but the value passed is a string, the type conversion will fail and the exception will be thrown.

  • HttpMessageNotReadableException: and exactly the opposite of the above HttpMediaTypeNotSupportedException examples, namely the request header to carry the “content-type: application/json; Charset = utf-8 “, @requestbody is not added to the received argument, or the pojo fails to be serialized from the json string contained in the RequestBody.

  • HttpMessageNotWritableException: the returned pojo in serialized into json process fails, then throw the exception;

  • HttpMediaTypeNotAcceptableException: unknown;

  • ServletRequestBindingException: unknown;

  • ConversionNotSupportedException: unknown;

  • MissingServletRequestPartException: unknown;

  • AsyncRequestTimeoutException: unknown;

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 forward to the /error controller. Spring also provides a default error controller, as follows: BasicErrorController

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

spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false

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 return 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, which inherits BaseResponse and includes data.

To distinguish between success and failure returns, define an ErrorResponse;

Finally, a common return result is that the data is returned with paging information. Since this interface is common, it is necessary to define a separate return result class, QueryDataResponse, which inherits from CommonResponse, Only restrict the data field type to QueryDdata, which 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. The project structure is as follows:

The project structure will be introduced in this way

Introducing the common package

The main code

Here is the main source for validation:

@Service public 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:

-- licence INSERT 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, 'user', 'suitability - plus', 200189); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, Tokens tokens licence_allocated) VALUES (3, 2, 'user',' hr-powersuite ', 100,4); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, Tokens tokens licence_allocated) VALUES (4, 2, 'cod-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', '[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

Catch custom exceptions

To obtain details of a non-existing licence:http://localhost:10000/licence/5. Successfully responded request: licenceId=1 Check is not emptyLicense not found exception was caught Licence not found

2. Obtain the list of licenses according to the non-existent license type:http://localhost:10000/licence/list?licenceType=ddd. optionalThe licence type can be user or core-prod.Check is not emptyCatch a Bad Licence Type exception Bad licence type

Catch the exception before entering the Controller

Accessing a nonexistent interface:http://localhost:10000/licence/list/ddd Catching a 404 exception

The HTTP method does not support:http://localhost:10000/licence PostMapping Catch the Request Method Not Supported exception Request method not supported

3 Verification Exception 1:http://localhost:10000/licence/list?licenceType= getLicences LicenceParam Catch a parameter binding verification exception licence type cannot be empty

4. Check exception 2: Post request, which is simulated by Postman. addLicence LicenceAddRequest The request URL is the resultCatch a parameter binding verification 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 those before entering the Controller. The following is the collection logic of the exception information:Collect exception information

Catching unknown exceptions

SQL > alter table license alter table license alter table license alter table licensehttp://localhost:10000/licence/1. Add test fieldCatching a database exception Error querying 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.The network Is Abnormal in the production environment

You can modify the current environment in the following ways:Example Change the current environment to the production environment

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

In conclusion, global exceptions belong to the old topic, I hope this project through mobile phone for you to have a bit of guidance to learn. Everyone according to the actual situation to modify.

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

@slf4j @RestControllerAdvice Public class GlobalExceptionHandler {/** * No login * @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

Source: www.cnblogs.com/jurendage/p…