Talk about the global exception handling of the springboot project

Posted by pesoto74 on Wed, 19 Jan 2022 11:59:38 +0100

preface

In the past, our business team added @ RestControllerAdvice+@ExceptionHandler to each business micro service to catch global exceptions when handling global exceptions. A leader once asked a question when walking through the code, why should each microservice project write a set of global exception code, why not draw the global exception block into a public jar, and then introduce each microservice in the form of jar. Later, the business team will separate the global exception block and package it into jar according to the requirements of the leader. Today's topic is about extracting global exceptions and some problems

Question 1: how to define the business error code after the global exception is extracted?

The previous definition of business error code of the team is: business service prefix + business module + error code. If it is an unrecognized exception, use business prefix + fixed module code + fixed error code.
The previous global exception pseudocode is as follows

@RestControllerAdvice
@Slf4j
public class GlobalExceptionBaseHandler {

    
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public AjaxResult handleException(Exception e) {
        String servicePrifix = "U";
        String moudleCode = "001";
        String code = "0001";
        String errorCode = servicePrifix + moudleCode + code;
        String msg = e.getMessage();
        if(StringUtils.isEmpty(msg)){
            msg = "Server exception";
        }
        log.error(msg, e);
        return AjaxResult.error(msg, errorCode);
    }
    }

Now, how to identify the business service prefix after the global exception is extracted? When it is not separated before, the business service prefix and each business service are directly written in the code.

At that time, our temporary solution was through spring application. Name to solve it. Because after the global exception code block is extracted, it will eventually be introduced by the service. Therefore, the pseudo code for obtaining the business service prefix can be obtained as follows

public enum  ServicePrefixEnum {

    USER_SERVICE("U","User center");

    private final String servicePrefix;

    private final String serviceDesc;

    ServicePrefixEnum(String servicePrefix,String serviceDesc) {
        this.servicePrefix = servicePrefix;
        this.serviceDesc = serviceDesc;
    }

    public String getServicePrefix() {
        return servicePrefix;
    }

    public String getServiceDesc() {
        return serviceDesc;
    }
}

  public String getServicePrefix(@Value("${spring.application.name}") String serviceName){
      return ServicePrefixEnum.valueOf(serviceName).getServicePrefix();
    }

But this scheme actually has disadvantages

Drawback 1: through enumeration and hard coding, the current microservice name is preset. Once the project changes the microservice name, the service prefix cannot be found.
Drawback 2: if the business service module is newly launched, this enumeration class has to be changed

Later, we added the configuration of custom business code in the global exception jar. Business personnel only need to configure it in the springboot configuration file, as shown below

lybgeek:
  bizcode:
    prefix: U

At this time, an example of global exception transformation is shown below

@RestControllerAdvice
@Slf4j
public class GlobalExceptionBaseHandler {
    
    
    @Autowired
    private ServiceCodeProperties serviceCodeProperties;

   
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public AjaxResult handleException(Exception e) {
        String servicePrifix = serviceCodeProperties.getPrifix();
        String moudleCode = "001";
        String code = "0001";
        String errorCode = servicePrifix + moudleCode + code;
        String msg = e.getMessage();
        if(StringUtils.isEmpty(msg)){
            msg = "Server exception";
        }
        log.error(msg, e);
        return AjaxResult.error(msg, errorCode);
    }
}

Problem 2: the global exception introduces the same dependent jar as the business, but the jar version is different

If the global exception is written directly as follows, there is no problem. Examples are as follows

@RestControllerAdvice
@Slf4j
public class GlobalExceptionBaseHandler {


    @Autowired
    private ServiceCodeProperties serviceCodeProperties;

    
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public AjaxResult handleException(Exception e) {
        String servicePrifix = serviceCodeProperties.getPrifix();
        String moudleCode = "001";
        String code = "0001";
        String errorCode = servicePrifix + moudleCode + code;
        String msg = e.getMessage();
        if(StringUtils.isEmpty(msg)){
            msg = "Server exception";
        }
        log.error(msg, e);
        return AjaxResult.error(msg, HttpStatus.INTERNAL_SERVER_ERROR.value());
    }


    @ExceptionHandler(BizException.class)
    public AjaxResult handleException(BizException e)
    {
        return AjaxResult.error(e.getMessage(), e.getErrorCode());
    }

}

That is, global exceptions are directly divided into business exceptions and exception. The disadvantage of this division is that there is no way to subdivide exceptions, and the module code and business code defined by the project team cannot be subdivided. Therefore, we also list common predictable system exceptions, as shown below

  /**
     *Parameter validation failed
     * @param e
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public AjaxResult handleException(ConstraintViolationException e)
    {
        log.error("Parameter validation failed", e);
        return AjaxResult.error("Parameter validation failed", HttpStatus.BAD_REQUEST.value());
    }

   /**
     * Database exception
     * @param e
     * @return
     */
    @ExceptionHandler({SQLException.class, MybatisPlusException.class,
            MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class,
            BadSqlGrammarException.class
    })
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public AjaxResult dbException(Exception e) {
        String msg = ExceptionUtil.getExceptionMessage(e);
        log.error(msg, e);
        return AjaxResult.error(msg,HttpStatus.BAD_REQUEST.value());
    }

    /**
     * The record already exists in the database
     * @param e
     * @return
     */
    @ExceptionHandler(DuplicateKeyException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public AjaxResult handleException(DuplicateKeyException e)
    {
        log.error("The record already exists in the database", e);
        return AjaxResult.error("The record already exists in the database", HttpStatus.CONFLICT.value());
    }

However, this leads to a problem, that is, when the global exception and the business party use the same dependent jar, but there are version differences, there may be dependency conflicts, resulting in an error when the business project is started. Therefore, the solution is to add the optional tag to the pom file. Examples are as follows

    <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <optional>true</optional>
        </dependency>

This tag means that the jar coordinates are optional. Therefore, if the project already has the coordinates of the jar, the coordinates of the jar will be used directly

Problem 3: after the maven optional tag is introduced, the project startup error is reported because the business does not introduce the jar required by the global exception

Generation of this problem: for example, our business micro service project has an aggregation layer. Some aggregation layers do not need to rely on storage media, such as mysql. Therefore, these aggregation layer projects pom will not introduce dependencies similar to mybatis. However, our global exception needs a dependency similar to mybatis, so if we want to reference the global exception module, we have to add additional jar s that the business party does not need.

Therefore, the conditional annotation of springboot comes in handy, using the @ ConditionalOnClass annotation. Examples are as follows

@RestControllerAdvice
@Slf4j
@ConditionalOnClass({SQLException.class, MybatisPlusException.class,
        MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class,
        BadSqlGrammarException.class, DuplicateKeyException.class})
public class GlobalExceptionDbHandler {




    /**
     * Database exception
     * @param e
     * @return
     */
    @ExceptionHandler({SQLException.class, MybatisPlusException.class,
            MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class,
            BadSqlGrammarException.class
    })
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public AjaxResult dbException(Exception e) {
        String msg = ExceptionUtil.getExceptionMessage(e);
        log.error(msg, e);
        return AjaxResult.error(msg,HttpStatus.BAD_REQUEST.value());
    }

    /**
     * The record already exists in the database
     * @param e
     * @return
     */
    @ExceptionHandler(DuplicateKeyException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public AjaxResult handleException(DuplicateKeyException e)
    {
        log.error("The record already exists in the database", e);
        return AjaxResult.error("The record already exists in the database", HttpStatus.CONFLICT.value());
    }
}

@The function of the ConditionalOnClass annotation is that if the specified class exists in the classpath, the class on the annotation will take effect.

At the same time, there is a fine node here, that is, global exceptions may have to be subdivided, that is, the original unified global exceptions are separated according to business scenarios, such as storage media related storage exceptions and web related exceptions

summary

This article focuses on the problems that may occur when global exceptions are extracted into jar s. There are some details not mentioned here, such as why we need to define service prefix + business module code + error code. In fact, it is mainly for troubleshooting.

Maybe a friend will ask, you all have microservices, don't you go on Distributed Link Tracking? According to the distributed link tracking, it is easy to locate the whole link. However, when developing microservices, if the company does not have an operation and maintenance platform, sometimes for the sake of cost, the testing and development environment will not be used for distributed link tracking, even at the initial stage of online projects. Therefore, it is very important to define the relevant business code

demo link

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-exception

Topics: Spring Boot