大家好,我是一航!
今天1024程序员节日,在这里祝大家节日快乐!
近期的一个需求开发中,遇到了一个非常诡异的小bug,忍不住要分享一下;第一眼看到这个bug时,满脑子就是曹老板的那句:不可能,绝对不可能。问题总结起来就一句话:明明一个方法执行 throw 了一个异常,调用方也没有 try-catch 捕获,结果异常后,代码依然很丝滑的往下继续执行了
看一段示例伪代码:
一个用于验证请求的工具类
代码语言:javascript复制@Slf4j
@Component
public class VerifyUtil {
public void userVerify(Integer userId){
log.info("验证失败!抛出异常");
throw new BaseException(UserErrStatusCode.ERR_2000);
}
}
去掉了无关的逻辑,目前调用这个方法就只会抛个异常
一个测试接口
代码语言:javascript复制@Slf4j
@RequestMapping("test")
@RestController
public class TestController extends BaseController {
@Autowired
VerifyUtil verifyUtil;
@GetMapping("get/{userId}")
public BaseResponceDto getUserInfo(@PathVariable Integer userId) {
log.info("接收用户ID:{}", userId);
log.info("开始验证用户:{}", userId);
verifyUtil.userVerify(userId);
log.info("验证完成,返回数据!");
return ReturnUtils.success();
}
}
当代码跑起来之后,我们调用接口http://127.0.0.1:8085/test/get/1
,哪怕你是刚入行 java 的同学,也能很容易看明白这段代码,最后的执行结果肯定会在verifyUtil.userVerify(userId);
这里抛出个异常,并响应前端错误,后续流程不会继续执行!
我作为一个练习时长两年半的 javaer ,自然也是这么认为的,可执行结果却是:
代码语言:javascript复制com.ehang.responce.rest.TestController : 接收用户ID:1
com.ehang.responce.rest.TestController : 开始验证用户:1
com.ehang.responce.rest.VerifyUtil : 验证失败!抛出异常
com.ehang.responce.rest.TestController : 验证完成,返回数据!
我写的代码,他居然在异常之后,还继续执行了后续的代码。
问题原因
事出反常必有妖...
经过一圈的排查,发现这里的代码并没有问题;导致这个bug的主要是因为一个不太规范的AOP操作,拦截了异常,使得异常虽然抛是抛了,但是抛了个寂寞,后续的流程依然继续在执行;
问题复现
项目中的所有接口都放在一个rest的目录下,为了规范前后端的交互,确保前端的每次请求,无论是正常还是异常,都能够拿到一个友好的 JSON 应答,于是项目中使用了AOP来切了所有的 Controller 接口,做了一些未处理异常的拦截操作,代码如下:
代码语言:javascript复制@Slf4j
@Aspect
@Component
public class ExAop {
@Around("(execution(public * com.ehang.*.rest..*.*(..)))")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
try {
return pjp.proceed();
} catch (BaseException e) {
return ReturnUtils.error(e);
} catch (Exception e) {
return ReturnUtils.error(BaseStatusCode.ERR_9999);
}
}
}
切面通过 @Around("(execution(public * com.ehang.*.rest..*.*(..)))")
设置了rest目录下的所有方法为切点,一旦方法执行异常,且没有处理成自定义 BaseException
异常,就统一响应一个未知错误
的应答。
就到目前为止,这样写是没有任何问题的;
现在来一个新的需求,在 Controller 接收到参数就需要做一些特殊的校验,因为是只在 Controller 中处理,开发的时候,就顺手在rest目录下建了个util目录,写了个校验的 Util 工具类,来校验参数,在不满足条件的情况下抛出异常;
伪代码和目录结构如下:
这么一写,就出现了文章一开头说的问题了,这个Controller里面的校验方法不管怎么抛异常,都能正常执行后续的代码;
原因分析
就单从细节上来说,无论是AOP,还是 Controller ,都是正常的;但是把AOP切点表达式:(execution(public * com.ehang.*.rest..*.*(..)))
;和新增加的 util 目录结合在一起,问题就出现了;
这个表达式指定了 rest 及其子目录下所有类的所有方法;那么这一刀下去,不光切了 rest 目录下的 Contrller ,连 util 的所有方法也一并切了,当执行verifyUtil.userVerify(userId)
并throw 异常之后,ExAop
拦截了异常,并执行了return ReturnUtils.error(e);
,将异常处理并返回了一个对象,由于本身userVerify
无返回参数,最终的效果就是verifyUtil.userVerify
成功执行并继续执行了后续的代码。
相当于变成如下代码:
代码语言:javascript复制try {
verifyUtil.userVerify(userId);
} catch (BaseException exception) {
exception.printStackTrace();
}
虽然这个try - catch 我并没有写,但是AOP的代理增强帮我做了这个事情,这么说的话,这个 bug 的出现似乎就可以解释通了。
解决方式
只要分析出了原因,要解决起来还是比较容易的,方法也有很多种:
将rest目录下的 util 挪走
既然是AOP增强了rest目录下所有类的所有方法,那 rest 目录下就不要放Controller 以外的无关东西;
将 Util 工具类定义成静态方法
将工具类的方法变成静态方法之后,就不会被AOP增强;
细化AOP的切点表达式
代码语言:javascript复制@Around("(execution(public * com.ehang.*.rest..*.*Controller(..)))")
统一Controller的类名格式为xxxController
,然后在表达式中限定,只切类名为Controller后缀下的所有方法;
总结
这种多个正常的设计结合,化学反应后产生的问题,排查起来就会有些头疼;如果你不太了解AOP、或者是接手了一个新的项目,不熟悉项目结构,这种违背常理的问题出现,还是有些让人匪夷所思的...
最后,再次祝所有的同行,1024节日快乐!