异常启示录

异常启示录

《Effective Java》中提到,尽量使用判断代替异常,因为在jvm处理中,异常需要耗费更多性能,
但是在异常带来的便利性上,这些性能损耗是可以接受的…

如何分类异常?如何处理异常?

异常分为两大类,一种是Checked Exception,另一种是Runtime Exception,以下简称 CE 和 RE;

对于CE,编译器要求必须置于_try/catch_中,此时,应该在_catch_中处理此异常,处理的原则是:可继续,则继续,无法继续,则强制终止;

如何判断可继续?产生错误的代码处理的逻辑与数据对后续代码影响不大,对整个业务的影响也不大,是可有可无的代码,或者在_catch_中有完美的替代解决方案(此时,应该尽量按照《Effective Java》所述,使用判断,代替异常);否则,皆是“无法继续”,应该强制退出;

常规处理方式有:日志记录,后备替补方案或者再次包装为RE抛出;

对于RE,非强制_try/catch_捕获,也一般不会手动_try/catch_捕获,如果对这种异常置之不理,则服务器会使用默认的处理方案,产生白页、500页面等;

对待此种异常,常规的处理方式有:在spring环境下,添加全局异常拦截器,判断处理;或者在无spring的web环境中,配置友好的错误页面;

不管是CE还是RE,都不应该直接打印堆栈,或者封装为RE时打破异常链,只使用错误提示封装新RE;这全部为错误做法。

如何利用异常?

此处的利用异常,主要说的是RE,对于CE,需要直接捕获或者抛出,如果滥用,会使代码中充斥着各种throws/try/catch,造成代码可读性差;

考虑如下代码:

Controller:

public class UserController {
	
	private UserService userService;
	
	@RequestMapping("/saveUser")
	public String save(@RquestBody User user) {
		int result = userService.saveUser(user);
		if(result == 0) {
			return "保存成功";
		} else if(result == -1) {
			return "名字校验错误";
		} else if(result == -1) {
			return "Email已被使用";
		} else {
			return "保存失败";
		}
	}
	
}

Service:

public class UserService {
	
	private User userDao;
	
	public int saveUser(User user) {
		if(!checkName(user.getName())) {
			// 名字校验不合格,返回-1
			return -1;
		}
		int count = userDao.countByEmail(user.getEmail());
		if(count > 0) {
			// 用户Email已被使用,返回-2
			return -2;
		}
		// 其他保存逻辑
		...
		return 0;
	}
	
}

以上代码,首先controller接收前端的User对象,然后调用Service保存;

保存时,进行判断,如果数据有问题,返回错误码-1或者-2,如果没有问题,返回0,代表成功;

最后controller进行判断,如果为0,提示前端成功,否则,皆以不同的提示语提示前端处理失败。

可以是可以,但是不是感觉不是很好?如果Service还需要返回其他信息怎么办?下边还有一个DAO层,难道DAO层也用返回状态码来处理?

再看下边这种写法:

Controller:

public class UserController {
	
	private UserService userService;
	
	@RequestMapping("/saveUser")
	public String save(@RquestBody User user) {
		userService.saveUser(user);
		return "保存成功";
	}
	
}

Service:

public class UserService {
	
	private User userDao;
	
	public void saveUser(User user) {
		if(!checkName(user.getName())) {
			throw new ServiceException("名字校验不合格");
		}
		int count = userDao.countByEmail(user.getEmail());
		if(count > 0) {
			throw new ServiceException("用户Email已被使用");
		}
		// 其他保存逻辑
		...
	}
	
}

这种写法,既不用返回状态码,也可以再返回其他数据,调用方更不用if/else进行各种判断,因为被调用方,如果可以运行,则执行到底,如果无法运行,就抛出异常,终止了操作;此处ServiceException继承的肯定是RuntimeException;

使用这种方式,还有一个要处理的就是,需要添加异常拦截器,否则服务器默认处理为500错误;

拦截器最好为全局拦截器,所有的异常处理使用统一的处理,那拦截器中的代码逻辑为何?不管遇到什么异常,皆提示用户“服务器异常”吗?

异常拦截器
先来介绍下spring中的异常拦截器

spring的异常拦截器有很多种方式,以下是其中一种:

public class GlobalExceptionResolver implements HandlerExceptionResolver {
	
	@Override
	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
				
	}
}

这是比较简单的一种,只需要继承_HandlerExceptionResolver_,实现_resolveException()方法即可,参数中有request、response还有产生错误的exception,
最后返回一个_ModelAndView
,实现重定向视图的效果;

不过,有一种简单的处理方式是,不在使用_ModelAndView_的重定向,而是直接使用response输出消息,实现如下:

String rmsg = ...
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.getOutputStream().write(JSON.toJSONString(rmsg).getBytes("UTF-8"));
return new ModelAndView();

rmsg_是根据_exception_处理之后的错误提示语,需要输出给前端;最后返回一个空的_ModelAndView;

再次回到异常的讨论话题上来…

以上产生的ServiceException,显然应该将其中的message提示给前端用户,就和第一种使用状态码的返回效果一样;

所以,异常拦截器中,需要进行判断:

if(ex instanceof ServiceException) {
	rmsg = ex.getMessage();
}

rmsg代表了最终需要返回给前端的消息;

这样,就实现了和第一种代码中一样的效果;

那其他的异常怎么办?要不就…

if(ex instanceof ServiceException) {
	rmsg = ex.getMessage();
} else {
	rmsg = "服务器异常!";
}

这种处理方式也是可行的!

但是一个ServiceException就能代表系统中所有的异常了吗?

比如说,封装的工具类中,有一个CE,需要进行try/catch处理,然后catch中需要再次抛出RE,那这个RE是啥?用ServiceException?还是直接用RuntimeException?

再者,需要在Controller中实现RESTful风格的处理(这里不讨论链接的生成),RESTful返回时需要添加额外的HTTP状态码,那怎么处理?RE实现类再加个字段?像这样:

throw new ServiceException("名字不合法", 400);

400是HTTP状态码,意为:bad request;

如果你在Service层中,写了这样的代码,那我估摸着,你的IT生涯也差不多到头了…

所以,现在的需求变成了:

  • 需要丰富异常类型;
  • 异常类型可以映射为HTTP码,或者可以推断出HTTP状态码
  • 扩展异常拦截器,使其可以处理更多的异常类型;
异常的详细分类

所有的异常扩展自RE这是毫无疑问的,相似的还有spring框架封装的异常类,都是扩展自RE;

在我看来,异常应该分为两种:

1、业务异常,类似于ServiceException,作用是中断程序,并将提示语传递到用户交互层;

2、架构异常,这种异常通常是编码上的错误,或者通过判断之后觉得是无法提示给用户的、无法解决的、必须通过更改代码来解决的异常,这种异常一般以友好提示语返回给用户;

一级:

RuntimeException: 父类异常

二级:

BussinessException: 业务异常,此类及子类异常皆将提示语返回给前端用户,默认HTTP状态码400;

ArchException: 架构异常,此类及子类异常的提示语需记录用于排查错误,返回给前端友好提示语,默认状态码500;

三级:

此级用来细化二级,范围不固定,可灵活扩展;

BussinessException的子类:

  • OKException HTTP-200
  • CreatedException HTTP-201
  • AcceptedException HTTP-202
  • NoContentException HTTP-204
  • MovedPermanentlyException HTTP-301
  • SeeOtherException HTTP-303
  • NotModifiedException HTTP-304
  • BadInvokeException HTTP-400
  • NotFoundException HTTP-404
  • NotAcceptableException HTTP-406
  • ConflictException HTTP-409
  • PreconditionFailedException HTTP-412
  • UnsupportedException HTTP-415

ArchException的子类:

  • InternalErrorException HTTP-500
  • UnavailableException HTTP-503

有一些异常适合在Controller层使用,有一些只适合在Service层使用,也有一些异常基本不会被使用,知道怎么区分不?

扩展异常拦截器

异常分类更详细了,异常拦截器也应该修改;

至少,要添加根据不同的异常类型,返回不同的HTTP状态码的逻辑!

挺简单,不写了;

扩展话题-异常的日志打印

有异常产生时,绝大多数是要打印日志的,那这个日志如何打印?在什么时机打印?

打印堆栈,肯定不行,System.out也肯定不行,较好的办法是用日志框架,基于SLF4J的任何实现;

考虑如下代码:

if(...) {
	log.error("XXX不合法!");
	throw new ServiceException("XXX不合法!");
}

能看出问题来吗?

代码出现了冗余,日志中的消息和异常中的消息内容基本一致,至少意思可能一致;即使使用变量引用消息内容,也感觉冗余…

不如将代码改成这样:

if(...) {
	throw new ServiceException("XXX不合法!");
}

然后在异常拦截器中:

log.error(exception);

将日志的打印更改到了异常拦截器中,减少了冗余;

还有一个问题是,如果非要在发生问题的时候打印日志,然后异常拦截器又打印了一份,不是重复了吗?

这就需要提供灵活性,即:

在throw出异常时,默认是需要拦截器打印日志的;但是,可以通过传递一个true/false的参数来控制异常拦截器是否打印日志;

这样如何?

END.

你可能感兴趣的:(异常处理,系统架构,架构)