preface

Record how Dubbo handles custom exceptions.

Achieve the goal

  • Service layer exception, thrown directly to the upper layer,webLayer unified capture processing
  • Return if the exception is a system – defined exception{"code":xxx,"msg":yyy}Among themcodeCorresponding toError code.msgThe corresponding information is abnormal information
  • Returns if the exception is not system – defined{"code":-1," MSG ":" unknown error "}In addition, the abnormal stack information is output to logs for fault locating

The project architecture

Let’s start with a system architecture diagram. This diagram comes from the network. I believe that the distributed cluster architecture of most small and medium-sized enterprises is similar to this design:

A brief description of the following hierarchical architecture:

  • Usually there is a special oneThe fortress machineDo unified proxy forwarding, client (PC, mobile, etc.) access bynginxUnified exposed entrance
  • nginxReverse proxy, load balancing towebServer, bytomcatOf a cluster,webThe layer serves only as an entry point for interface requests, with no actual business logic
  • webLayer withrpcRemote calls are registered tozookeeperthedubboService cluster,dubboServices interact with the data layer to process business logic

Separate the front and back ends and use JSON format for data interaction. The format can be unified as follows:

    {
    	"code": 200.// Status code: 200 Success, other status code is failure
    	"msg": "success".// Message, success is success, other is failure cause
    	"data": object     // The specific data content can be in any format
    }
Copy the code

Mapping to Javabeans can be uniformly defined as:

/ * * *@program: easywits
 * @description: the outermost object returned by the HTTP request *@author: zhangshaolin
 * @create: the 2018-04-27 10:43 * * /
@Data
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class BaseResult<T> implements Serializable{

    private static final long serialVersionUID = -6959952431964699958L;

    /** * Status code: 200 Successful, other status code: failed */
    public Integer code;

    / * * * success for success, the other for failure reason * /
    public String msg;

    /** ** Specific content */
    public T data;
}
Copy the code

Return result utility class encapsulation:

/ * * *@program: easywits
 * @description: HTTP return result utility class *@author: zhangshaolin
 * @create: the 2018-07-14 thy * * /
public class ResultUtil {

    /** * call include data * when access succeeds@param object
     * @return* /
    public static BaseResult success(Object object){
        BaseResult result = new BaseResult();
        result.setCode(200);
        result.setMsg("success");
        result.setData(object);
        return result;
    }

    /** * The call does not contain data *@return* /
    public static BaseResult success(a){
        return success(null);
    }

    /** * returns an exception without data *@param code
     * @param msg
     * @return* /
    public static BaseResult error(Integer code,String msg){
        BaseResult result = new BaseResult();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
    
     /** * returns an exception containing data *@paramResultEnum Result enumeration class & EMsp; Unified management & EMSP; code msg *@param object
     * @return* /
    public static BaseResult error(ResultEnum resultEnum,Object object){
        BaseResult result = error(resultEnum);
        result.setData(object);
        return result;
    }

    /** * Global base class custom exception exception handling *@param e
     * @return* /
    public static BaseResult error(BaseException e){
        return error(e.getCode(),e.getMessage());
    }

    /** * returns an exception without data *@paramResultEnum Result enumeration class & EMsp; Unified management & EMSP; code msg *@return* /
    public static BaseResult error(ResultEnum resultEnum){
        returnerror(resultEnum.getCode(),resultEnum.getMsg()); }}Copy the code

Therefore, the process of simulating a front-end call request could be as follows:

  • Web layer interface

    @RestController
    @RequestMapping(value = "/user")
    public class UserController {
    
        @Autowired
        UserService mUserService;
        
        @Loggable(descp = "User Profile", include = "")
        @GetMapping(value = "/info")
        public BaseResult userInfo(a) {
            returnmUserService.userInfo(); }}Copy the code
  • Service layer interface

     @Override
    public BaseResult userInfo(a) {
        UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
        UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
        return ResultUtil.success(userInfoVo);
    }
    Copy the code

Custom system exception

Define a custom exception that is used to manually throw an exception. Note that the base RuntimeException is unchecked:

To put it simply, RuntimeException and its subclasses are unchecked exceptions. Other exceptions are checked exceptions. Unchecked exceptions are exceptions thrown at runtime, and checked exceptions are mandatory errors at compile time

public class BaseException extends RuntimeException{

    private Integer code;

    public BaseException(a) {}public BaseException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode(); }... Omit set get method}Copy the code

To facilitate unified management of results, define a result enumeration class:

public enum ResultEnum {
    UNKNOWN_ERROR(-1."O (╥﹏╥)o~~ The system is abnormal! Please contact the administrator!!"),
    SUCCESS(200."success");
    
    private Integer code;
    
    private String msg;

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg; }}Copy the code

webLayers uniformly catch exceptions

Define the BaseController abstract class to uniformly catch exceptions thrown by the service layer. All new controllers inherit this class.

public abstract class BaseController {
    private final static Logger LOGGER = LoggerFactory.getLogger(BaseController.class);
    
       /** * unified exception handling **@param e
     */
    @ExceptionHandler(a)public Object exceptionHandler(Exception e) {
        if (e instanceof BaseException) {
            // Global base class custom exception returns {code, MSG}
            BaseException baseException = (BaseException) e;
            return ResultUtil.error(baseException);
        } else {
            LOGGER.error("System exception: {}", e);
            returnResultUtil.error(ResultEnum.UNKNOWN_ERROR); }}}Copy the code

validation

  1. The above Web layer interface UserController inherits BaseController and uniformly catches exceptions

  2. The service layer assumes that a customized system exception, BaseException, is thrown as follows:

     @Override
     public BaseResult userInfo(a) {
        UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
        UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
          if(userInfoVo ! =null) {
            {code:10228 MSG :" User exists! "} }
            throw new BaseException(ResultEnum.USER_EXIST);
        }
        return ResultUtil.success(userInfoVo);
    }
    Copy the code

However, after calling the result, the exception caught by the upper layer is not a BaseException, but is thrown as an unknown error. Take a look at Dubbo’s handling of exceptions with some doubt

Dubbo exception handling

Dubbo has uniform exception handling. Here is the main code of Dubbo exception interceptor:

 @Override
    public Result invoke(Invoker
        invoker, Invocation invocation) throws RpcException {
        try {
            // Service invocation
            Result result = invoker.invoke(invocation);
            // There is an exception and a non-generalization call
            if(result.hasException() && GenericService.class ! = invoker.getInterface()) {try {
                    Throwable exception = result.getException();

                    // directly throw if it's checked exception
                    // If it is a checked exception, throw it
                    if(! (exceptioninstanceof RuntimeException) && (exception instanceof Exception)) {
                        return result;
                    }
                    // directly throw if the exception appears in the signature
                    // There is a declaration on the method signature, which is thrown directly
                    try{ Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes()); Class<? >[] exceptionClassses = method.getExceptionTypes();for(Class<? > exceptionClass : exceptionClassses) {if (exception.getClass().equals(exceptionClass)) {
                                returnresult; }}}catch (NoSuchMethodException e) {
                        return result;
                    }

                    // ERROR logs are printed on the server side for exceptions not defined on the method signature
                    // for the exception not found in method's signature, print ERROR message in server's log.
                    logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
                            + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
                            + ", exception: " + exception.getClass().getName() + ":" + exception.getMessage(), exception);

                    // The exception class and the interface class are in the same JAR package, directly thrown
                    // directly throw if exception class and interface class are in the same jar file.
                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
                        return result;
                    }
                    // is a JDK exception thrown directly
                    // directly throw if it's JDK exception
                    String className = exception.getClass().getName();
                    if (className.startsWith("java.") || className.startsWith("javax.")) {
                        return result;
                    }
                    // Is an exception of Dubbo itself, thrown directly
                    // directly throw if it's dubbo exception
                    if (exception instanceof RpcException) {
                        return result;
                    }

                    // Otherwise, wrap it as RuntimeException and throw it to the client
                    // otherwise, wrap with RuntimeException and throw back to the client
                    return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
                } catch (Throwable e) {
                    logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()
                            + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
                            + ", exception: " + e.getClass().getName() + ":" + e.getMessage(), e);
                    returnresult; }}/ / return
            return result;
        } catch (RuntimeException e) {
            logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
                    + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
                    + ", exception: " + e.getClass().getName() + ":" + e.getMessage(), e);
            throwe; }}Copy the code

Brief description:

  • When there is an exception and a non-generalization call, if it is a checked exception, it is thrown directly
  • If there is an exception and a non-generalized call has a declaration on the method signature, it is thrown directly
  • The exception class and the interface class are the same when there is an exception and a non-generalized calljarIn the bag, you throw it out
  • If there is an exception and a non-generalization call, yesDubboRpcException is thrown directly
  • When you have an exception and you don’t generalize the call, the rest of the case, all wrapped up inRuntimeExceptionThrow to the client

By now it’s obvious that our BaseException is unchecked and doesn’t meet the requirements thrown directly by Dubbo’s exception interceptor, so Dubbo wraps it as a RuntimeException. So in the upper BaseController uniformly caught as a system unknown error.

The solution

  • Exception classBaseExceptionSame as the interface classjarBag, but this way to eachjarTo place an exception class, not unified maintenance management
  • Explicitly declare a throw on the interface method signatureBaseExceptionIn this way, it is relatively simple and easy to maintain in a unified way, but each interface should declare an exception explicitly. I choose this way to solve the problem

The problem

Return resultutil. error(ResultEnum ResultEnum) {code: XXX MSG: XXX}

If you’ve ever played with Spring, you know that the concept of declarative transactions is to add transaction annotations to the interface, and when an exception occurs, the entire interface performs transaction rollback. Take a look at the pseudocode below:

@Transactional(rollbackFor = Exception.class)
public BaseResult handleData(a){
    
    //1. Add data to table A and return the primary key ID of the newly added data
    
    //2. Perform operations on the database and add data in database B. Associate the data in table A with the primary key ID as the foreign key
    
    //3. Execute successfully   Returns the result
}
Copy the code
  • This interface declares the rollback of exceptions, and all exceptions are rolled back when sent
  • Procedure Step 1 If the data fails to be imported into the database, the primary key ID cannot be obtained theoretically. In this case, a customized exception should be thrown, indicating that the operation fails
  • If the data in Step 1 is successfully imported into the database but the data in Step 2 fails to be imported into the database, the data in Step 1 should also be rolled back theoretically. If the abnormal result is returned forcibly, the data in step 1 becomes dirty data. In this case, it is the most reasonable to throw the user-defined exception

Final thoughts

It is best to read the source code in a real problem scenario, and to look at the source code with the problem in mind will make people feel suddenly enlightened.

More original articles will be the first time to push the public account [Zhang Shaolin students], welcome to pay attention to!