@ExceptionHandler的执行顺序以及@ExceptionHandler源码分析

目录

使用背景

依赖版本

模块划分

结果分析

思考问题

解决过程

尝试解决过程一

尝试解决过程二

解决方案

方案一

方案二

结语


使用背景

  1. 使用框架:spring boot,dubbo,启用一个模块作为web端,接收所有的http请求,启用一个模块作为服务端(service端),负责核心的业务逻辑处理,web端和service端请求通过dubbo进行通信
  2. 业务背景:我们需要将service层抛出的异常,在web端进行统一处理,然后转换成和前端约定好的response格式

依赖版本

        Spring Boot版本: 2.2.5,Dubbo版本:2.7.12, 注册中心:nacos版本1.4.2

        依赖如下:

            <!--spring boot依赖-->
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-starter</artifactId>
				<version>2.2.5.RELEASE</version>
			</dependency>

			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-starter-test</artifactId>
				<version>2.2.5.RELEASE</version>
				<scope>test</scope>
			</dependency>
			<!--spring boot集成dubbo-->
			<dependency>
				<groupId>org.apache.dubbo</groupId>
				<artifactId>dubbo-spring-boot-starter</artifactId>
				<version>2.7.12</version>
			</dependency>
			<!--web依赖-->
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-starter-web</artifactId>
				<version>2.2.5.RELEASE</version>
			</dependency>
			<!-- Nacos依赖 -->
			<dependency>
				<groupId>com.alibaba.nacos</groupId>
				<artifactId>nacos-client</artifactId>
				<version>1.4.2</version>
			</dependency>

模块划分

 service模块:

web模块:

 service接口实现:

/**
 * @description: 用户接口实现类
 * @author: xiaonailiang
 * @create: 2023/04/17 10:32
 */
@DubboService
public class UserServiceImpl implements UserService {
    @Override
    public Integer addUser(User user) {
        if(user.getName() == null){
            throw new ServiceException("缺少用户名称");
        }
        //
        return 1;
    }
}

user参数实体:

/**
 * @description: 用户实体
 * @author: xiaonailiang
 * @create: 2023/04/17 10:34
 */
public class User implements Serializable {
    private static final long serialVersionUID = -4345528274182860290L;

    private String name;

    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

web请求入口:

/**
 * @description: 用户管理web服务接口
 * @author: xiaonailiang
 * @create: 2023/04/17 10:31
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @DubboReference(version = "1.0.0", group = "origin-service", protocol = "dubbo")
    UserService userService;

    /**
     *
     * @Return: java.lang.String
     * @author: xiaonailiang
     * @date:  2023/4/17 2:21 下午
     */
    @PostMapping("/add")
    public ResponseMessage addUser(@RequestBody User user){
        userService.addUser(user);
        return new ResponseMessage("200", "成功", 1);
    }

}

统一的response实体:

/**
 * @description: web统一响应消息体
 * @author: xiaonailiang
 * @create: 2023/04/17 15:06
 */
public class ResponseMessage implements Serializable {

    private static final long serialVersionUID = -7479838941015050423L;

    private String responseCode;

    private String message;

    private Object data;

    public ResponseMessage(String responseCode, String message, Object data) {
        this.responseCode = responseCode;
        this.message = message;
        this.data = data;
    }

    public String getResponseCode() {
        return responseCode;
    }

    public void setResponseCode(String responseCode) {
        this.responseCode = responseCode;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

 异常拦截器:

/**
 * 统一异常处理中心
 * @author: xiaonailiang
 * @date:  2023/4/17 3:10 下午
 */
@ControllerAdvice
public class ControllerExceptionHandler {

	@ExceptionHandler(IllegalArgumentException.class)
	@ResponseBody
	public ResponseMessage illegalArgumentException(IllegalArgumentException ex) {
		ex.printStackTrace();
		return new ResponseMessage("501", "非法参数", 0);
	}

	@ExceptionHandler(ServiceException.class)
	@ResponseBody
	public ResponseMessage serviceExceptionHandler(ServiceException ex) {
		ex.printStackTrace();
		return new ResponseMessage("502", ex.getMessage(), 0);
	}


	@ExceptionHandler(RuntimeException.class)
	@ResponseBody
	public ResponseMessage runtimeExceptionHandler(RuntimeException ex) {
		ex.printStackTrace();
		return new ResponseMessage("503", "运行时异常", 0);
	}


	/**
	 * 其他错误
	 */
	@ExceptionHandler({Exception.class})
	@ResponseBody
	public ResponseMessage exception(Exception ex) {
		ex.printStackTrace();
		return new ResponseMessage("504", "未知异常", 0);
	}
}

结果分析

        注意:(ServiceException用的是api模块包下的,而非service包下,此处注意,后面会分析

预期结果:

        UserServiceImpl中的添加用户接口,如果缺少用户名,那么需要将该业务异常信息抛给接口调用端,调用端针对ServiceException该种业务异常,统一将异常信息取出,返回给前端(web服务调用端)。

实际结果:

        如下图,异常处理器拦截到异常后,走的是代码2处,而非预期的代码1处

111
异常拦截器图

思考问题

  1. 会不会是因为ServiceException继承了RuntimeException,所以异常拦截器匹配到的是异常拦截器图中的代码2
  2. 如果自定义异常,不继承RuntimeException,那么异常会拦截器会走异常拦截器图中代码1
  3. 业务前提,因为我们项目用的是Spring AOP事务管理器,指定的是RuntimeException类型异常才会进行事务回滚,那么我们如何在不改变继承的异常的情况下,将我们所需的业务异常信息提取出来呢

解决过程

尝试解决过程一

        必应了一下,发现有同学指出,可以用@Order注解,来创建多个异常处理起,然后通过@Order来指定它加载的顺序,那样就可以指定我们自定义异常ServiceException优先处理然后返回,至于@Order的作用,大家可以自己去必应一下,有博主有更具体的讲解

        改变的代码如下:

        

 

指定业务异常的加载顺序先于其他异常

测试结果:

        并未改变实际的运行结果,最终RunTImeException处理方法处理返回

此处我们看下Debug的过程:

        当web端接收到service端抛出的异常后,由ExceptionHandlerExceptionResolver.doResolveHandlerMethodException()方法来获取最终处理该异常的handler方法。doResolveHandlerMethodException方法主要做的事是:找到处理该异常的方法,然后执行该方法,将该方法的返回值以ModelAndView的方式返回给http的请求端

如下标记的地方:

下面我们看下getExceptionHandlerMethod方法实现

        官方注释:

        Find an @ExceptionHandler method for the given exception. The default implementation searches methods in the class hierarchy of the controller first and if not found, it continues searching for additional @ExceptionHandler methods assuming some @ControllerAdvice Spring-managed beans were detected.

        大概的意思为:

        查找给定异常的@ExceptionHandler方法。默认实现首先在控制器的类层次结构中搜索方法,如果未找到,则假设检测到一些 Spring 管理的 bean,它将继续搜索其他@ExceptionHandler方法和标识了@ControllerAdvice注解的实例

获取解析方法图

         如图,获取解析方法图中,代码3处为获取该类上的异常处理方法,代码4处为获取全局的ExceptionHandler的异常处理方法,最终都是通过调用ExceptionHandlerMethodResolver.resolveMethod()方法获取最终的解析方法,本次测试中,我们走的是代码4的分支逻辑,下面我们来看看resolveMethod()方法的实现

获取异常处理方法图1

              如上图,获取异常处理方法图1中按照1->2->3->4步骤去获取处理方法,最终由getMappedMethod来确定最终的处理方法

获取异常处理方法图2

      如图,获取异常处理方法图2中,代码1处判断所有注解了@ExceptionHandler的异常类,是否为当前拦截到的异常类或者父类,代码2处,将所有遍历后符合的类集合,按照最低类深度进行排序,具体排序算法,我这边不展开了。

        排序后:

        所以,我们也就明白了为什么尝试解决过程一,通过@Order不能达到预期的效果了,异常处理器方法匹配的过程跟spring bean的加载顺序无关,跟异常的匹配度有关

尝试解决过程二

        如下图,我发现这个返回到web层的异常被重写了,并不是我们service抛出ServiceException,猜想会不会dubbo在写回返回值时,对异常做了处理

                                                    

 于是我们找到dubbo处理异常的ExceptionFilter :

         整个方法的注释,原开发者已经为我们写好了,大家注意关注我标记的地方,特别是2处,如果这个异常类和我们被调用的接口在同一哥jar包内,也就是同一个项目模块中,那么这个异常,dubbo不会二次处理直接返回,这里也呼应了我上面说的结果分析(ServiceException用的是api模块包下的,而非service包下,此处注意,后面会分析),因为我用的是另外一个依赖包中的ServiceException,所以此时,dubbo异常过滤器走到了代码3,将我们service抛出的异常重新封装为了一个RuntimeException,所以web端的ExceptionHandler识别的异常类型就是RuntimeExcepiton。

解决方案

        根据dubbo的ExceptionFilter中的源码逻辑,我们可以给出一下解决方案

方案一

        将引用的api包中的ServiceExcepiton换成Service模块中自带的ServiceExcepiton

        验证:符合预期

方案二

        重写dubbo的ExceptionFilter,加入我们自定义异常的处理逻辑,我们将dubbo的ExceptionFilter复制一份到我们项目中改名为BusinessDubboExceptionFilter

package com.xnl.origincode.service.dubbo.filter;

import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.ReflectUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.*;
import org.apache.dubbo.rpc.service.GenericService;

import java.lang.reflect.Method;


/**
 * ExceptionInvokerFilter
 * <p>
 * Functions:
 * <ol>
 * <li>unexpected exception will be logged in ERROR level on provider side. Unexpected exception are unchecked
 * exception not declared on the interface</li>
 * <li>Wrap the exception not introduced in API package into RuntimeException. Framework will serialize the outer exception but stringnize its cause in order to avoid of possible serialization problem on client side</li>
 * </ol>
 */
@Activate(group = CommonConstants.PROVIDER)
public class BusinessDubboExceptionFilter implements Filter, Filter.Listener {
    private Logger logger = LoggerFactory.getLogger(BusinessDubboExceptionFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        return invoker.invoke(invocation);
    }

    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
       
        if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
            try {
                Throwable exception = appResponse.getException();

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

                // 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);

                // 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;
                }
                // directly throw if it's JDK exception
                String className = exception.getClass().getName();
                if (className.startsWith("java.") || className.startsWith("javax.")) {
                    return;
                }
                if(className.startsWith("com.xnl")){
                    return;
                }
                // directly throw if it's dubbo exception
                if (exception instanceof RpcException) {
                    return;
                }

                // otherwise, wrap with RuntimeException and throw back to the client
                appResponse.setException(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);
            }
        }
    }

    @Override
    public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
        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);
    }

    // For test purpose
    public void setLogger(Logger logger) {
        this.logger = logger;
    }
}

如下图标识处,添加一段条件判断:

为了让dubbo加载我们的异常过滤器,还要进行一个资源文件配置

配置dubbo的生产者的过滤器,dubboExceptionFilter表示指定加载我们自定义的异常过滤器,

-exception表示去掉dubbo自身的ExceptionFilter

测试结果:符合预期

结语

  1. 了解@ExceptionHandler注解的方法,加载和匹配异常处理方法的原理
  2. dubbo调用时,会对抛出的异常进行一次过滤器封装处理
  3. 可以通过重写dubbo的ExceptionFilter和改变业务异常包ServiceExcepiton的方式来达到我们返回自定义异常信息给dubbo调用端的目的

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值